From 975c76e3e81ffd4f1fe1a7f24b1f3eed3cb52c57 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:24:50 -0400 Subject: [PATCH 01/14] fix: don't count a re-attached unchanged buffer as a new game frame (#731) --- app/src/main/cpp/waylandcomp/src/compositor.c | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index 36516a12d..3d0794667 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -780,8 +780,11 @@ static void take_pixel(struct surface *s, const uint8_t bgra[4], struct wl_resou /* The window the app's performance HUD follows: the latest one at least as big as the last to * start presenting GPU frames (take_dmabuf) (X11 binds the HUD to the _MESA_DRV window and counts X presents instead). JNI upcalls. */ static struct surface *g_hud_surface; -/* When the HUD's window last presented a frame (now_ns), to let a replacement take over. */ -static int64_t g_hud_last_ns; +/* When the HUD's window last presented a new frame (now_ns), and when it last committed at all, + * a repeat of the frame it shows included. A replacement may take over only once it has gone + * quiet in the second sense: gamescope repeats a paused game's frame under a busy overlay, and + * that overlay must not become the game. */ +static int64_t g_hud_last_ns, g_hud_alive_ns; /* That window committed a frame the screen has not drawn yet. */ static int g_hud_fresh; extern void droiddeck_on_game_surface(const char *window, const char *gpu); /* window NULL = gone */ @@ -791,7 +794,14 @@ extern void droiddeck_on_game_frame(void); extern void droiddeck_on_game_program(int pid, const char *program); static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_resource *buffer) { - if (s->dmabuf != buffer || s->dmabuf_buf != b) { + /* The buffer this surface already shows, committed again. Gamescope's Wayland backend attaches + * every plane on every frame it outputs, so its game plane comes back unchanged whenever only + * something above it moved (MangoHud's graph, Steam's overlay). The surface still holds it and + * never released it, so the client cannot have drawn into it: it is no new frame. Counted as + * one, a game capped at 30 read 50-60 fps in the HUD, and frame generation took those repeats + * as source frames ("LSFG 2x 59 -> 54" instead of 30 -> 60). */ + const int repeat = s->dmabuf == buffer && s->dmabuf_buf == b; + if (!repeat) { drop_dmabuf(s, 1); s->dmabuf = buffer; s->dmabuf_buf = b; @@ -826,7 +836,7 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re s->buf_h = b->height; s->buf_alpha = (b->format & 0xff) == 'A'; s->has_content = b->img != NULL; - g_stat_dmabuf++; + if (!repeat) g_stat_dmabuf++; const int announced_now = !s->announced_vulkan; if (!s->announced_vulkan) { char name[160]; @@ -857,7 +867,7 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re * full-screen gamescope over the desktop still does), and a window gone quiet for a second * gives way to whatever draws next. */ if (s != g_hud_surface && (b->img || ahb_swapchain_has_ahb(b)) && - (!g_hud_surface || now_ns() - g_hud_last_ns > 1000000000LL || + (!g_hud_surface || now_ns() - g_hud_alive_ns > 1000000000LL || (announced_now && (int64_t)b->width * b->height >= (int64_t)g_hud_surface->buf_w * g_hud_surface->buf_h))) { char name[160]; describe(s, name, sizeof(name)); @@ -886,7 +896,10 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re * Asking whether the compositor could import it, as this used to, is a question about the * copy path and not about whether a frame happened: a game whose buffers go straight to * the display layer draws on screen while the counter sat at 0.0 fps and 1000.0 ms. */ - if (s == g_hud_surface) { g_hud_last_ns = now_ns(); g_hud_fresh = 1; droiddeck_on_game_frame(); } + if (s == g_hud_surface) { + g_hud_alive_ns = now_ns(); + if (!repeat) { g_hud_last_ns = g_hud_alive_ns; g_hud_fresh = 1; droiddeck_on_game_frame(); } + } } /* ---- hooks for ahb_swapchain.c (zero-copy layers) */ From f0c52c273b7a2d8871ea8a6b106a2245975fadb1 Mon Sep 17 00:00:00 2001 From: MaxsTechReview Date: Tue, 13 Oct 2026 13:33:19 -0400 Subject: [PATCH 02/14] Add opt-in zRAM compression for the Steam client while a game runs (#442) * Add opt-in zRAM compression for the Steam client while a game runs A "zRAM compression" toggle under Steam > Settings > Session, below "Keep downloads running". Off by default, kept once set, and disabled on devices without swap or with a kernel older than 5.4. When on, the session watches for Steam's reaper. 20 s into a game the client and its web helpers page their private anonymous memory out to the device's zram with MADV_PAGEOUT from inside their own process (libblsession zram.c, SIGURG), again every 5 minutes, and read every swapped page back once the game exits. No root is needed: only a process itself may do this without CAP_SYS_NICE. With the toggle off nothing is installed or started. RedMagic NP06J, Palworld: Steam's resident memory fell from ~1.7 GB to ~1.1 GB with ~560 MB in zram, available memory during play rose by ~0.5-0.8 GB, and ~2 MB was left in zram after the read-back. * Turn zRAM compression on by default below 12 GB of RAM Until the toggle is changed, it follows the device: on when the kernel supports it and the device has less than 12 GB of RAM, read from the advertised memory size on Android 14+ and from total memory before that. Turning it off is kept. --------- Co-authored-by: xXJsonDeruloXx --- .../com/droiddeck/launcher/MainActivity.kt | 8 + .../droiddeck/launcher/core/ZramSupport.kt | 35 ++++ .../launcher/session/SessionPrefs.kt | 11 ++ .../launcher/session/SessionService.kt | 2 + .../launcher/ui/ModeSettingsDialog.kt | 11 ++ app/src/main/res/values/strings.xml | 3 + .../launcher/core/ZramSupportTest.kt | 37 ++++ .../overlay/usr/local/bin/droiddeck-session | 54 ++++++ tools/linuxfs/preload/zram.c | 176 ++++++++++++++++++ tools/tests/test_zram_preload.py | 176 ++++++++++++++++++ 10 files changed, 513 insertions(+) create mode 100644 app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/core/ZramSupportTest.kt create mode 100644 tools/linuxfs/preload/zram.c create mode 100644 tools/tests/test_zram_preload.py diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index 360380956..6e447339d 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -350,6 +350,8 @@ class MainActivity : ComponentActivity() { private var touchMode by mutableStateOf(SessionPrefs.TOUCH_AUTO) private var suspendPolicy by mutableStateOf(SessionPrefs.SUSPEND_MANUAL) private var steamDownloadsInBackground by mutableStateOf(false) + private var zramCompression by mutableStateOf(false) + private var zramSupported by mutableStateOf(null) private var oscMode by mutableStateOf(SessionPrefs.OSC_AUTO) private var backActionsInverted by mutableStateOf(false) private var gameStorage by mutableStateOf("") @@ -1293,6 +1295,8 @@ class MainActivity : ComponentActivity() { touchMode = touchMode, suspendPolicy = suspendPolicy, steamDownloadsInBackground = steamDownloadsInBackground, + zramCompression = if (mode == SessionService.MODE_STEAM) zramCompression else null, + zramSupported = zramSupported, pipSupported = com.droiddeck.launcher.session.SessionPipController.supported(this), pipAutoEnter = pipAutoEnter, unfoldedControls = if (mode == SessionService.MODE_STEAM && @@ -1351,6 +1355,7 @@ class MainActivity : ComponentActivity() { SessionPrefs.setSteamDownloadsInBackground(this, enabled) steamDownloadsInBackground = enabled }, + onZramCompression = { on -> SessionPrefs.setZramCompression(this, on); zramCompression = on }, onPipAutoEnter = { on -> SessionPrefs.setPipAutoEnter(this, on); pipAutoEnter = on }, onUnfoldedControls = { on -> SessionPrefs.setUnfoldedControls(this, on); unfoldedControls = on }, onOsc = { o -> SessionPrefs.setOscMode(this, o); oscMode = o }, @@ -1539,6 +1544,9 @@ class MainActivity : ComponentActivity() { // The sheet lifts at once, on what was last read; the slow part (driver files, a walk of the // added-games folders, the storage volumes) lands while it rises. Thread({ + val zram = com.droiddeck.launcher.core.ZramSupport.supported() + val zramOn = SessionPrefs.zramCompression(this) + ui.post { zramSupported = zram; zramCompression = zramOn } drivers.refreshDrivers() val storage = GameStorage.options(this) val deckyInstalled = if (mode == SessionService.MODE_STEAM) DeckyManager.installed(this) else null diff --git a/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt b/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt new file mode 100644 index 000000000..e81ed9681 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt @@ -0,0 +1,35 @@ +package com.droiddeck.launcher.core + +import android.app.ActivityManager +import android.content.Context +import android.os.Build +import java.io.File + +/** Whether the client's memory can be paged out to compressed swap: MADV_PAGEOUT (Linux 5.4) and a zram or other swap device. */ +object ZramSupport { + private const val GIB = 1L shl 30 + + fun supported(): Boolean = kernelAtLeast(System.getProperty("os.version").orEmpty(), 5, 4) && swapTotalKb() > 0 + + fun recommended(context: Context): Boolean { + if (!supported()) return false + val memory = ActivityManager.MemoryInfo() + context.getSystemService(ActivityManager::class.java).getMemoryInfo(memory) + val advertised = if (Build.VERSION.SDK_INT >= 34) memory.advertisedMem else 0L + return belowTwelveGb(advertised, memory.totalMem) + } + + fun belowTwelveGb(advertisedBytes: Long, totalBytes: Long): Boolean = + if (advertisedBytes > 0) advertisedBytes < 11 * GIB else totalBytes < 10 * GIB + + fun kernelAtLeast(release: String, major: Int, minor: Int): Boolean { + val parts = Regex("^(\\d+)\\.(\\d+)").find(release)?.groupValues ?: return false + val ma = parts[1].toInt() + val mi = parts[2].toInt() + return ma > major || (ma == major && mi >= minor) + } + + fun swapTotalKb(meminfo: List = runCatching { File("/proc/meminfo").readLines() }.getOrDefault(emptyList())): Long = + meminfo.firstOrNull { it.startsWith("SwapTotal:") } + ?.substringAfter(':')?.trim()?.substringBefore(' ')?.toLongOrNull() ?: 0L +} diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index ff286eebc..b941508d4 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -7,6 +7,7 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.core.CpuAllocationDefaults import com.droiddeck.launcher.core.GameEnvironment import com.droiddeck.launcher.core.TextureFiltering +import com.droiddeck.launcher.core.ZramSupport import com.droiddeck.launcher.gpu.Cardboard import com.droiddeck.launcher.gpu.ScreenEffects import org.json.JSONObject @@ -1057,6 +1058,16 @@ object SessionPrefs { }.apply() } + /** Steam only: the client's memory goes to the device's zram while a game runs (ZramSupport). */ + fun zramCompression(context: Context): Boolean { + val prefs = prefs(context) + return if (prefs.contains("zramCompression")) prefs.getBoolean("zramCompression", false) else ZramSupport.recommended(context) + } + + fun setZramCompression(context: Context, on: Boolean) { + prefs(context).edit().putBoolean("zramCompression", on).apply() + } + // ── Game storage ──────────────────────────────────────────────────────────────────────── /** 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 01f9112eb..f068b8f26 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -660,6 +660,8 @@ class SessionService : Service() { // gamescope's realtime Vulkan queues (the session script turns this into // GAMESCOPE_FORCE_VULKAN_REALTIME); off unless the user turns it on. guest.add("BL_GAMESCOPE_REALTIME=" + (if (SessionPrefs.gamescopeRealtime(this)) "1" else "0")) + if (SessionState.mode == MODE_STEAM && SessionPrefs.zramCompression(this) && + com.droiddeck.launcher.core.ZramSupport.supported()) guest.add("BL_ZRAM=1") guest.add("BANNER_AUDIO_DIRECT_DECAY=0") // Anything else, for a device that cannot be reached with a debugger: droiddeck-env in the // app's own external files (envSwitchFile) holds KEY=VALUE lines that go into the session's diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt index 2ec97d7a7..3a03fe305 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt @@ -70,6 +70,10 @@ class ModeSettings( val touchMode: String, val suspendPolicy: String, val steamDownloadsInBackground: Boolean = false, + /** Steam only; null hides it. */ + val zramCompression: Boolean? = null, + /** Null until checked off the main thread. */ + val zramSupported: Boolean? = null, val pipSupported: Boolean = false, val pipAutoEnter: Boolean = false, /** Steam only. */ @@ -127,6 +131,7 @@ class ModeSettingsActions( val onTouch: (String) -> Unit, val onSuspendPolicy: (String) -> Unit, val onSteamDownloadsInBackground: (Boolean) -> Unit = {}, + val onZramCompression: (Boolean) -> Unit = {}, val onPipAutoEnter: (Boolean) -> Unit = {}, val onOsc: (String) -> Unit, val onBackActionsInverted: (Boolean) -> Unit = {}, @@ -303,6 +308,12 @@ fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras: stringResource(R.string.mode_background_downloads_hint), s.steamDownloadsInBackground, onChange = a.onSteamDownloadsInBackground, ) + if (s.zramCompression != null) ToggleRow( + host, "zram", stringResource(R.string.mode_zram), + stringResource(if (s.zramSupported == false) R.string.mode_zram_unsupported else R.string.mode_zram_hint), + s.zramCompression && s.zramSupported != false, enabled = s.zramSupported == true, + onChange = a.onZramCompression, + ) } if (s.pipSupported) SettingsGroup(stringResource(R.string.pip_title)) { ToggleRow(host, "pip-auto", stringResource(R.string.pip_auto), null, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bb1765615..3c192317e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -278,6 +278,9 @@ Auto pauses and resumes when shown. Native prepares Steam for sleep first. Manual waits for Resume. Never keeps running. Keep downloads running Delay background suspension while Steam is actively downloading. + zRAM compression + While a game runs, the Steam client\'s memory is compressed into the device\'s zram, leaving more RAM for the game. Applies the next time Steam starts. + Unavailable: this device has no zram or swap, or its kernel is older than 5.4. Auto pauses in the background and resumes when shown. Manual waits for Resume. Never keeps running. Startup Start Steam with DroidDeck diff --git a/app/src/test/java/com/droiddeck/launcher/core/ZramSupportTest.kt b/app/src/test/java/com/droiddeck/launcher/core/ZramSupportTest.kt new file mode 100644 index 000000000..056e3a055 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/core/ZramSupportTest.kt @@ -0,0 +1,37 @@ +package com.droiddeck.launcher.core + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class ZramSupportTest { + @Test + fun kernelVersionsFromAndroidReleases() { + assertTrue(ZramSupport.kernelAtLeast("6.12.38-android16-5-g665eafb62659-ab14778838-4k", 5, 4)) + assertTrue(ZramSupport.kernelAtLeast("5.4.0", 5, 4)) + assertFalse(ZramSupport.kernelAtLeast("4.19.157-perf+", 5, 4)) + assertFalse(ZramSupport.kernelAtLeast("", 5, 4)) + } + + @Test + fun underTwelveGbFromAdvertisedOrTotalMemory() { + val gb = 1_000_000_000L + val gib = 1L shl 30 + assertTrue(ZramSupport.belowTwelveGb(8 * gb, 0)) + assertTrue(ZramSupport.belowTwelveGb(8 * gib, 0)) + assertFalse(ZramSupport.belowTwelveGb(12 * gb, 0)) + assertFalse(ZramSupport.belowTwelveGb(12 * gib, 0)) + assertTrue(ZramSupport.belowTwelveGb(0, 7_400_000L * 1024)) + assertTrue(ZramSupport.belowTwelveGb(0, 9_300_000L * 1024)) + assertFalse(ZramSupport.belowTwelveGb(0, 11_100_000L * 1024)) + assertFalse(ZramSupport.belowTwelveGb(0, 15_600_380L * 1024)) + } + + @Test + fun swapTotalComesFromMeminfo() { + assertEquals(12582908L, ZramSupport.swapTotalKb(listOf("MemTotal: 15600380 kB", "SwapTotal: 12582908 kB"))) + assertEquals(0L, ZramSupport.swapTotalKb(listOf("SwapTotal: 0 kB"))) + assertEquals(0L, ZramSupport.swapTotalKb(emptyList())) + } +} diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index ba25dcc2a..6eb0059e8 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -489,6 +489,60 @@ case $mode in done ) & fi + # zRAM compression (BL_ZRAM): 20 s into a game the client and its web helpers page their memory + # out to the device's zram (libblsession zram.c), again every 5 minutes, and read it back once + # the game has exited. A game is running while Steam's reaper is. + if [ "${BL_ZRAM:-0}" = 1 ]; then + printf 0 > /tmp/.bl-zram + step "zRAM compression: on" + ( + bl_zram_scan() { + _bl_game=0 _bl_pids=() + for _d in /proc/[0-9]*; do + IFS= read -r -d '' _c 2>/dev/null < "$_d/cmdline" || continue + case ${_c##*/} in + reaper) _bl_game=1 ;; + steam|steamwebhelper) _bl_pids+=("${_d#/proc/}") ;; + esac + done + } + bl_zram_usage() { + local _p _k _v _u _rss=0 _swap=0 + for _p in "${_bl_pids[@]}"; do + while read -r _k _v _u; do + case $_k in VmRSS:) _rss=$((_rss + _v)) ;; VmSwap:) _swap=$((_swap + _v)) ;; esac + done 2>/dev/null < "/proc/$_p/status" + done + echo "${#_bl_pids[@]} processes, resident $((_rss / 1024)) MB, in zram $((_swap / 1024)) MB" + } + bl_zram_send() { + step "zRAM: $2 ($(bl_zram_usage))" + printf '%s' "$1" > /tmp/.bl-zram + [ "${#_bl_pids[@]}" = 0 ] || kill -URG "${_bl_pids[@]}" 2>/dev/null + nap 5 + bl_zram_scan + step "zRAM: done ($(bl_zram_usage))" + } + _bl_since=-1 _bl_packed=-1 + while kill -0 $$ 2>/dev/null; do + bl_zram_scan + if [ "$_bl_game" = 1 ]; then + [ "$_bl_since" -ge 0 ] || _bl_since=$SECONDS + if [ $((SECONDS - _bl_since)) -ge 20 ] && [ "${#_bl_pids[@]}" -gt 0 ] && + { [ "$_bl_packed" -lt 0 ] || [ $((SECONDS - _bl_packed)) -ge 300 ]; }; then + bl_zram_send 1 "compressing the client" + _bl_packed=$SECONDS + fi + elif [ "$_bl_since" -ge 0 ]; then + [ "$_bl_packed" -lt 0 ] || bl_zram_send 0 "game closed, reading the client back" + _bl_since=-1 _bl_packed=-1 + fi + nap 5 + done + ) & + else + rm -f /tmp/.bl-zram + fi # Rockstar's launcher keeps drawing its hidden web interface for as long as a game of theirs # runs; droiddeck-launcher-cores holds it on one slow core while it is out of sight, from # gamescope's focus as the agent publishes it. BL_LAUNCHER_CORES=0 leaves launchers alone. diff --git a/tools/linuxfs/preload/zram.c b/tools/linuxfs/preload/zram.c new file mode 100644 index 000000000..058b5bd4f --- /dev/null +++ b/tools/linuxfs/preload/zram.c @@ -0,0 +1,176 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* zRAM for the Steam client while a game runs: on SIGURG the process pages its private anonymous + * memory out (ZR_CTL holds 1) or reads it back (0). Raw syscalls only, as this library wraps + * close(), openat() and syscall(). */ + +#ifndef MADV_PAGEOUT +#define MADV_PAGEOUT 21 +#endif +#ifndef ZR_CTL +#define ZR_CTL "/tmp/.bl-zram" +#endif +#define ZR_SWAPPED (1ULL << 62) +#define ZR_BATCH 512 + +static long (*zr_sys)(long, ...); +static unsigned long zr_page; +static int zr_busy; +static int zr_pending; +static char zr_maps[8192]; +static uint64_t zr_entries[ZR_BATCH]; +static struct iovec zr_iov[ZR_BATCH]; +static char zr_sink[ZR_BATCH]; + +static int zr_open(const char *path) { + return (int)zr_sys(SYS_openat, AT_FDCWD, path, O_RDONLY | O_CLOEXEC, 0); +} + +static int zr_mode(void) { + char c = 0; + int fd = zr_open(ZR_CTL); + if (fd < 0) return -1; + long n = zr_sys(SYS_read, fd, &c, 1); + zr_sys(SYS_close, fd); + return n == 1 && (c == '0' || c == '1') ? c - '0' : -1; +} + +static unsigned long zr_hex(const char **p) { + unsigned long v = 0; + for (;; (*p)++) { + char c = **p; + if (c >= '0' && c <= '9') v = v * 16 + (unsigned long)(c - '0'); + else if (c >= 'a' && c <= 'f') v = v * 16 + (unsigned long)(c - 'a' + 10); + else return v; + } +} + +/* Paged out: private writable anonymous memory (the heap, unnamed and [anon:name] mappings). + * Read back: every private readable mapping, as the kernel may have swapped others meanwhile. */ +static int zr_eligible(const char *line, int compress, unsigned long *start, unsigned long *end) { + const char *p = line; + *start = zr_hex(&p); + if (*p++ != '-') return 0; + *end = zr_hex(&p); + if (*p++ != ' ' || p[0] != 'r' || p[3] != 'p') return 0; + if (!compress) return 1; + if (p[1] != 'w') return 0; + for (int field = 0; field < 4; field++) { + while (*p && *p != ' ') p++; + while (*p == ' ') p++; + } + return *p == 0 || strncmp(p, "[heap]", 6) == 0 || strncmp(p, "[anon:", 6) == 0; +} + +static void zr_read_back(int pagemap, unsigned long start, unsigned long end) { + pid_t self = (pid_t)zr_sys(SYS_getpid); + struct iovec local = {zr_sink, 0}; + for (unsigned long addr = start; addr < end;) { + unsigned long count = (end - addr) / zr_page; + if (count > ZR_BATCH) count = ZR_BATCH; + if (zr_sys(SYS_lseek, pagemap, (long)(addr / zr_page * sizeof(uint64_t)), SEEK_SET) < 0) return; + long got = zr_sys(SYS_read, pagemap, zr_entries, count * sizeof(uint64_t)); + if (got < (long)sizeof(uint64_t)) return; + unsigned long n = 0; + for (unsigned long i = 0; i < (unsigned long)got / sizeof(uint64_t); i++) { + if (!(zr_entries[i] & ZR_SWAPPED)) continue; + zr_iov[n].iov_base = (void *)(addr + i * zr_page); + zr_iov[n++].iov_len = 1; + } + for (unsigned long done = 0; done < n;) { + local.iov_len = n - done; + long r = zr_sys(SYS_process_vm_readv, self, &local, 1UL, zr_iov + done, n - done, 0UL); + done += r > 0 ? (unsigned long)r : 1; + } + addr += (unsigned long)got / sizeof(uint64_t) * zr_page; + } +} + +static void zr_walk(int compress) { + int maps = zr_open("/proc/self/maps"); + if (maps < 0) return; + int pagemap = compress ? -1 : zr_open("/proc/self/pagemap"); + if (!compress && pagemap < 0) { + zr_sys(SYS_close, maps); + return; + } + size_t have = 0; + for (;;) { + long n = zr_sys(SYS_read, maps, zr_maps + have, sizeof(zr_maps) - 1 - have); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) break; + have += (size_t)n; + zr_maps[have] = 0; + char *line = zr_maps; + char *nl; + while ((nl = memchr(line, '\n', have - (size_t)(line - zr_maps))) != NULL) { + *nl = 0; + unsigned long s, e; + if (zr_eligible(line, compress, &s, &e) && e > s) { + if (compress) zr_sys(SYS_madvise, s, e - s, MADV_PAGEOUT); + else zr_read_back(pagemap, s, e); + } + line = nl + 1; + } + have -= (size_t)(line - zr_maps); + memmove(zr_maps, line, have); + if (have == sizeof(zr_maps) - 1) have = 0; + } + if (pagemap >= 0) zr_sys(SYS_close, pagemap); + zr_sys(SYS_close, maps); +} + +static void zr_handler(int sig, siginfo_t *info, void *context) { + (void)sig; + (void)context; + if (info == NULL || info->si_code != SI_USER) return; + int saved = errno; + __atomic_store_n(&zr_pending, 1, __ATOMIC_SEQ_CST); + while (__atomic_load_n(&zr_pending, __ATOMIC_SEQ_CST) + && !__atomic_exchange_n(&zr_busy, 1, __ATOMIC_SEQ_CST)) { + __atomic_store_n(&zr_pending, 0, __ATOMIC_SEQ_CST); + int mode = zr_mode(); + if (mode >= 0) zr_walk(mode); + __atomic_store_n(&zr_busy, 0, __ATOMIC_SEQ_CST); + } + errno = saved; +} + +static void zr_child(void) { + __atomic_store_n(&zr_busy, 0, __ATOMIC_SEQ_CST); + __atomic_store_n(&zr_pending, 0, __ATOMIC_SEQ_CST); +} + +__attribute__((constructor)) static void zr_init(void) { + const char *name = program_invocation_short_name; + if (strcmp(name, "steam") != 0 && strcmp(name, "steamwebhelper") != 0) return; + void *libc = dlopen("libc.so.6", RTLD_LAZY | RTLD_NOLOAD); + if (libc == NULL) return; + zr_sys = (long (*)(long, ...))dlsym(libc, "syscall"); + if (zr_sys == NULL) return; + int fd = zr_open(ZR_CTL); + if (fd < 0) return; + zr_sys(SYS_close, fd); + long page = sysconf(_SC_PAGESIZE); + zr_page = page > 0 ? (unsigned long)page : 4096UL; + struct sigaction old; + if (sigaction(SIGURG, NULL, &old) != 0) return; + if ((old.sa_flags & SA_SIGINFO) || old.sa_handler != SIG_DFL) return; + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = zr_handler; + sa.sa_flags = SA_SIGINFO | SA_RESTART; + sigemptyset(&sa.sa_mask); + if (sigaction(SIGURG, &sa, NULL) == 0) pthread_atfork(NULL, NULL, zr_child); +} diff --git a/tools/tests/test_zram_preload.py b/tools/tests/test_zram_preload.py new file mode 100644 index 000000000..b0967cf62 --- /dev/null +++ b/tools/tests/test_zram_preload.py @@ -0,0 +1,176 @@ +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload" + +PROGRAM = r""" +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *ctl; +static size_t size, page; +static volatile int stop; +static unsigned char *shared; + +static long status(const char *key) { + FILE *f = fopen("/proc/self/status", "r"); + char line[256]; + long v = -1; + while (f && fgets(line, sizeof(line), f)) + if (!strncmp(line, key, strlen(key))) v = atol(line + strlen(key)); + if (f) fclose(f); + return v; +} + +static void request(char mode) { + int fd = open(ctl, O_WRONLY | O_TRUNC); + if (fd < 0 || write(fd, &mode, 1) != 1) exit(3); + close(fd); + kill(getpid(), SIGURG); +} + +static uint64_t sum(const unsigned char *p, size_t n) { + uint64_t s = 0; + for (size_t i = 0; i < n; i += 64) s = s * 31 + p[i]; + return s; +} + +/* Rewrites its pages with a round number and checks every page it wrote last round. */ +static void *writer(void *arg) { + unsigned char *mine = shared + (size_t)(uintptr_t)arg * (size / 4); + for (unsigned char round = 1; !stop; round++) { + for (size_t i = 0; i < size / 4; i += page) { + if (round > 1 && mine[i] != (unsigned char)(round - 1)) { + printf("corrupt\n"); + exit(4); + } + mine[i] = round; + } + } + return NULL; +} + +int main(int argc, char **argv) { + ctl = argv[1]; + size = strtoul(argv[2], NULL, 0) << 20; + int cycles = atoi(argv[3]); + page = (size_t)sysconf(_SC_PAGESIZE); + unsigned char *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + for (size_t i = 0; i < size; i++) data[i] = (unsigned char)(i * 7 + i / 4096); + uint64_t before = sum(data, size); + shared = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + memset(shared, 0, size); + pthread_t t[2]; + for (uintptr_t i = 0; i < 2; i++) pthread_create(&t[i], NULL, writer, (void *)i); + struct sigaction sa; + sigaction(SIGURG, NULL, &sa); + printf("handler %d\n", (sa.sa_flags & SA_SIGINFO) ? 1 : 0); + if (argc > 4) { + /* Read-only memory the process never pages out itself, swapped as the kernel would. */ + unsigned char *ro = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + memset(ro, 0x5a, size); + mprotect(ro, size, PROT_READ); + madvise(ro, size, 21); + printf("elsewhere %ld\n", status("VmSwap:")); + request('0'); + printf("back %ld\n", status("VmSwap:")); + } + for (int c = 0; c < cycles; c++) { + request('1'); + printf("out %ld\n", status("VmSwap:")); + request('0'); + printf("back %ld\n", status("VmSwap:")); + } + stop = 1; + for (int i = 0; i < 2; i++) pthread_join(t[i], NULL); + printf("intact %d\n", sum(data, size) == before); + return 0; +} +""" + + +def swap_total_kb(): + for line in Path("/proc/meminfo").read_text().splitlines(): + if line.startswith("SwapTotal:"): + return int(line.split()[1]) + return 0 + + +@unittest.skipUnless(shutil.which("cc"), "needs a C compiler") +class ZramPreloadTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.dir = Path(tempfile.mkdtemp()) + cls.ctl = cls.dir / "ctl" + build = cls.dir / "build" + build.mkdir() + (build / "program.c").write_text(PROGRAM) + cls.lib = build / "libblsession.so" + subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-Wall", "-pthread", "-DZR_CTL=\"%s\"" % cls.ctl, + "-o", str(cls.lib), *map(str, sorted(PRELOAD.glob("*.c"))), "-ldl"], check=True) + cls.steam = cls.dir / "steam" + subprocess.run(["cc", "-O2", "-pthread", "-o", str(cls.steam), str(build / "program.c")], check=True) + cls.game = cls.dir / "game" + shutil.copy(cls.steam, cls.game) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.dir, True) + + def setUp(self): + self.ctl.write_text("0") + + def run_program(self, program=None, mb=48, cycles=1, *extra): + environment = dict(os.environ, LD_PRELOAD=str(self.lib)) + result = subprocess.run([str(program or self.steam), str(self.ctl), str(mb), str(cycles), *extra], env=environment, + capture_output=True, text=True, check=True, timeout=120) + out = {} + for line in result.stdout.splitlines(): + key, value = line.split() + out.setdefault(key, []).append(int(value)) + return out + + def test_only_the_client_takes_the_signal(self): + self.assertEqual([1], self.run_program()["handler"]) + self.assertEqual([0], self.run_program(self.game)["handler"]) + + def test_nothing_is_installed_without_the_control_file(self): + self.ctl.unlink() + try: + self.assertEqual([0], self.run_program(cycles=0)["handler"]) + finally: + self.ctl.write_text("0") + + @unittest.skipUnless(swap_total_kb() > 256 * 1024, "needs swap") + def test_pages_out_and_reads_back(self): + out = self.run_program(mb=48) + self.assertGreater(out["out"][0], 32 * 1024) + self.assertLess(out["back"][0], out["out"][0] // 4) + self.assertEqual([1], out["intact"]) + + @unittest.skipUnless(swap_total_kb() > 256 * 1024, "needs swap") + def test_reads_back_what_the_kernel_swapped_elsewhere(self): + out = self.run_program(None, 32, 0, "read-only") + self.assertGreater(out["elsewhere"][0], 16 * 1024) + self.assertLess(out["back"][0], out["elsewhere"][0] // 4) + + def test_memory_stays_intact_under_writers(self): + out = self.run_program(mb=32, cycles=6) + self.assertEqual([1], out["intact"]) + self.assertEqual(6, len(out["out"])) + + +if __name__ == "__main__": + unittest.main() From cc5ae4ff0b1c883c189981d40f5752ec55791962 Mon Sep 17 00:00:00 2001 From: MaxsTechReview Date: Tue, 13 Oct 2026 13:34:00 -0400 Subject: [PATCH 03/14] Add translations for 23 more languages and fill missing strings (#669) * Add translations for 23 more languages and fill missing strings * Translate the strings added in main into every language Cardboard VR and the session drawer's component downloads in all 31 languages, the game details facts and HowLongToBeat labels in the 23 new ones, and the reworded Components page note there. --- .../droiddeck/launcher/core/AppLanguage.kt | 43 +- .../launcher/input/SteamTouchControls.kt | 100 +- .../launcher/input/SteamTouchIconPicker.kt | 16 +- .../launcher/stores/amazon/AmazonBackend.kt | 6 +- .../launcher/stores/amazon/AmazonLibrary.kt | 19 +- .../launcher/stores/epic/EpicBackend.kt | 6 +- .../launcher/stores/epic/EpicLibrary.kt | 11 +- .../launcher/stores/gog/GogBackend.kt | 4 +- .../launcher/stores/gog/GogLibrary.kt | 13 +- app/src/main/res/values-ar/strings.xml | 2335 +++++++++++++++++ app/src/main/res/values-b+es+419/strings.xml | 93 + app/src/main/res/values-bg/strings.xml | 2235 ++++++++++++++++ app/src/main/res/values-cs/strings.xml | 2285 ++++++++++++++++ app/src/main/res/values-da/strings.xml | 2235 ++++++++++++++++ app/src/main/res/values-de/strings.xml | 2235 ++++++++++++++++ app/src/main/res/values-el/strings.xml | 2235 ++++++++++++++++ app/src/main/res/values-es/strings.xml | 579 ++-- app/src/main/res/values-fi/strings.xml | 2235 ++++++++++++++++ app/src/main/res/values-fr/strings.xml | 1306 ++++++++- app/src/main/res/values-hi/strings.xml | 2235 ++++++++++++++++ app/src/main/res/values-hu/strings.xml | 2235 ++++++++++++++++ app/src/main/res/values-in/strings.xml | 2210 ++++++++++++++++ app/src/main/res/values-it/strings.xml | 2260 ++++++++++++++++ app/src/main/res/values-ja/strings.xml | 573 ++-- app/src/main/res/values-ko/strings.xml | 573 ++-- app/src/main/res/values-ms/strings.xml | 2210 ++++++++++++++++ app/src/main/res/values-nb/strings.xml | 2235 ++++++++++++++++ app/src/main/res/values-nl/strings.xml | 2235 ++++++++++++++++ app/src/main/res/values-pl/strings.xml | 2285 ++++++++++++++++ app/src/main/res/values-pt-rPT/strings.xml | 2260 ++++++++++++++++ app/src/main/res/values-pt/strings.xml | 2260 ++++++++++++++++ app/src/main/res/values-ro/strings.xml | 2260 ++++++++++++++++ app/src/main/res/values-ru/strings.xml | 1220 ++++++++- app/src/main/res/values-sv/strings.xml | 2235 ++++++++++++++++ app/src/main/res/values-th/strings.xml | 2210 ++++++++++++++++ app/src/main/res/values-tr/strings.xml | 2235 ++++++++++++++++ app/src/main/res/values-uk/strings.xml | 2285 ++++++++++++++++ app/src/main/res/values-vi/strings.xml | 2210 ++++++++++++++++ app/src/main/res/values-zh-rCN/strings.xml | 573 ++-- app/src/main/res/values-zh-rHK/strings.xml | 573 ++-- app/src/main/res/values-zh-rTW/strings.xml | 573 ++-- app/src/main/res/values/strings.xml | 68 + app/src/main/res/xml/locales_config.xml | 24 + .../launcher/core/AppLanguageTest.kt | 57 + .../launcher/session/SteamLanguageTest.kt | 12 + 45 files changed, 57044 insertions(+), 1053 deletions(-) create mode 100644 app/src/main/res/values-ar/strings.xml create mode 100644 app/src/main/res/values-b+es+419/strings.xml create mode 100644 app/src/main/res/values-bg/strings.xml create mode 100644 app/src/main/res/values-cs/strings.xml create mode 100644 app/src/main/res/values-da/strings.xml create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-el/strings.xml create mode 100644 app/src/main/res/values-fi/strings.xml create mode 100644 app/src/main/res/values-hi/strings.xml create mode 100644 app/src/main/res/values-hu/strings.xml create mode 100644 app/src/main/res/values-in/strings.xml create mode 100644 app/src/main/res/values-it/strings.xml create mode 100644 app/src/main/res/values-ms/strings.xml create mode 100644 app/src/main/res/values-nb/strings.xml create mode 100644 app/src/main/res/values-nl/strings.xml create mode 100644 app/src/main/res/values-pl/strings.xml create mode 100644 app/src/main/res/values-pt-rPT/strings.xml create mode 100644 app/src/main/res/values-pt/strings.xml create mode 100644 app/src/main/res/values-ro/strings.xml create mode 100644 app/src/main/res/values-sv/strings.xml create mode 100644 app/src/main/res/values-th/strings.xml create mode 100644 app/src/main/res/values-tr/strings.xml create mode 100644 app/src/main/res/values-uk/strings.xml create mode 100644 app/src/main/res/values-vi/strings.xml create mode 100644 app/src/test/java/com/droiddeck/launcher/core/AppLanguageTest.kt diff --git a/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt b/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt index 4021e1804..46a164f1a 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt @@ -22,7 +22,11 @@ object AppLanguage { const val SYSTEM = "" /** The languages the app ships, as BCP 47 tags, in the order the picker lists them. */ - val supported: List = listOf("en", "es", "fr", "ru", "ja", "ko", "zh-CN", "zh-TW", "zh-HK") + val supported: List = listOf( + "en", "id", "ms", "cs", "da", "de", "es", "es-419", "fr", "it", "hu", "nl", "nb", "pl", + "pt-BR", "pt-PT", "ro", "fi", "sv", "vi", "tr", "el", "bg", "ru", "uk", "ar", "hi", "th", + "ja", "ko", "zh-CN", "zh-TW", "zh-HK", + ) private const val PREFS = "language" private const val KEY = "app_language" @@ -97,9 +101,18 @@ object AppLanguage { // 0 leaves it undefined, so only the locale is changed. private fun override(locale: Locale) = Configuration().apply { fontScale = 0f; setLocales(LocaleList(locale)) } + private val latinAmerica = setOf( + "419", "AR", "BO", "BR", "BZ", "CL", "CO", "CR", "CU", "DO", "EC", "GT", "HN", "MX", "NI", "PA", + "PE", "PR", "PY", "SV", "US", "UY", "VE", + ) + /** Which of the app's languages [locale] reads as, or null when the app does not ship it. */ fun supportedTag(locale: Locale): String? = when (locale.language) { - "en", "es", "fr", "ru", "ja", "ko" -> locale.language + "en", "fr", "ru", "ja", "ko", "ms", "cs", "da", "de", "it", "hu", "nl", "nb", "pl", "ro", "fi", + "sv", "vi", "tr", "el", "bg", "uk", "ar", "hi", "th" -> locale.language + "in", "id" -> "id" + "es" -> if (locale.country in latinAmerica) "es-419" else "es" + "pt" -> if (locale.country.isEmpty() || locale.country == "BR") "pt-BR" else "pt-PT" "zh" -> when { locale.script == "Hans" -> "zh-CN" locale.country == "HK" || locale.country == "MO" -> "zh-HK" @@ -116,9 +129,33 @@ object AppLanguage { /** A language's name in that language, for the picker: what a reader of it looks for. */ fun nativeName(tag: String): String = when (tag) { "en" -> "English" - "es" -> "Español" + "id" -> "Bahasa Indonesia" + "ms" -> "Bahasa Melayu" + "cs" -> "Čeština" + "da" -> "Dansk" + "de" -> "Deutsch" + "es" -> "Español (España)" + "es-419" -> "Español (Latinoamérica)" "fr" -> "Français" + "it" -> "Italiano" + "hu" -> "Magyar" + "nl" -> "Nederlands" + "nb" -> "Norsk bokmål" + "pl" -> "Polski" + "pt-BR" -> "Português (Brasil)" + "pt-PT" -> "Português (Portugal)" + "ro" -> "Română" + "fi" -> "Suomi" + "sv" -> "Svenska" + "vi" -> "Tiếng Việt" + "tr" -> "Türkçe" + "el" -> "Ελληνικά" + "bg" -> "Български" "ru" -> "Русский" + "uk" -> "Українська" + "ar" -> "العربية" + "hi" -> "हिन्दी" + "th" -> "ไทย" "ja" -> "日本語" "ko" -> "한국어" "zh-CN" -> "简体中文" diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt index 350a61934..2995896d1 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt @@ -13,6 +13,7 @@ import android.util.Log import android.view.MotionEvent import android.view.View import android.widget.Toast +import com.droiddeck.launcher.R import com.droiddeck.launcher.input.SteamTouchConfig.Element import java.util.concurrent.Executors import kotlin.math.abs @@ -335,7 +336,7 @@ class SteamTouchControls( if (editing) { text.textSize = r * 0.3f text.color = Color.argb(alpha / 2, 255, 255, 255) - canvas.drawText("Trackpad", x, y - (text.descent() + text.ascent()) / 2, text) + canvas.drawText(context.getString(R.string.deck_surface_trackpad), x, y - (text.descent() + text.ascent()) / 2, text) } } SteamTouchConfig.STEAM -> { @@ -579,7 +580,7 @@ class SteamTouchControls( SteamTouchConfig.STEAM -> "STEAM" SteamTouchConfig.THUMB -> "…" SteamTouchConfig.KEYBOARD -> "⌨" - SteamTouchConfig.PASTE -> "Paste" + SteamTouchConfig.PASTE -> context.getString(R.string.touch_paste) SteamTouchConfig.SELECT -> "⧉" SteamTouchConfig.START -> "☰" SteamTouchConfig.BUMPER_LEFT -> "LB" @@ -880,15 +881,15 @@ class SteamTouchControls( } private fun controlName(type: Int) = when (type) { - SteamTouchConfig.DPAD -> "D-pad" - SteamTouchConfig.JOYSTICK_LEFT -> "Left stick" - SteamTouchConfig.JOYSTICK_RIGHT -> "Right stick" - SteamTouchConfig.TRACKPAD_LEFT -> "Left trackpad" - SteamTouchConfig.TRACKPAD_RIGHT -> "Right trackpad" - SteamTouchConfig.TRACKPAD_CENTER -> "Trackpad" + SteamTouchConfig.DPAD -> context.getString(R.string.touch_dpad) + SteamTouchConfig.JOYSTICK_LEFT -> context.getString(R.string.touch_left_stick) + SteamTouchConfig.JOYSTICK_RIGHT -> context.getString(R.string.touch_right_stick) + SteamTouchConfig.TRACKPAD_LEFT -> context.getString(R.string.deck_left_trackpad) + SteamTouchConfig.TRACKPAD_RIGHT -> context.getString(R.string.deck_right_trackpad) + SteamTouchConfig.TRACKPAD_CENTER -> context.getString(R.string.deck_surface_trackpad) SteamTouchConfig.STEAM -> "Steam" - SteamTouchConfig.THUMB -> "Menu" - SteamTouchConfig.KEYBOARD -> "Keyboard" + SteamTouchConfig.THUMB -> context.getString(R.string.touch_menu) + SteamTouchConfig.KEYBOARD -> context.getString(R.string.touch_keyboard) else -> label(type) } @@ -899,22 +900,22 @@ class SteamTouchControls( val list = android.widget.LinearLayout(context).apply { orientation = android.widget.LinearLayout.VERTICAL; setPadding((12 * density).toInt(), 0, (12 * density).toInt(), 0) } var close: () -> Unit = {} val copied = prefs.getString("copiedLayout", null) - fun item(name: String, enabled: Boolean = true, action: () -> Unit) = list.addView(android.widget.Button(context).apply { - text = name + fun item(name: Int, enabled: Boolean = true, action: () -> Unit) = list.addView(android.widget.Button(context).apply { + setText(name) isAllCaps = false isEnabled = enabled setOnClickListener { close(); action(); invalidate() } }) - item("Colour and opacity…") { pickColor() } - item("Copy this layout") { + item(R.string.touch_color_opacity) { pickColor() } + item(R.string.touch_copy_layout) { prefs.edit().putString("copiedLayout", (editElements + hidden).joinToString(";") { "${it.type},${it.visible},${it.x},${it.y},${it.xScale},${it.yScale}" }).apply() - Toast.makeText(context, "Layout copied; paste it into any game's touch layout", Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.touch_layout_copied, Toast.LENGTH_SHORT).show() } - item("Paste copied layout", copied != null) { pasteLayout(copied ?: "") } - item("Reset to Steam's default") { resetEditSet() } - close = SteamTouchIconPicker.showPanel(host, "Layout", list, listOf("Close" to {})) + item(R.string.touch_paste_layout, copied != null) { pasteLayout(copied ?: "") } + item(R.string.touch_reset_layout) { resetEditSet() } + close = SteamTouchIconPicker.showPanel(host, context.getString(R.string.ctrl_layout), list, listOf(context.getString(R.string.common_close) to {})) } /** A copied layout onto this action set: each control the set has takes the copied place. */ @@ -985,11 +986,12 @@ class SteamTouchControls( RectF(frame.left + l * s, frame.top + t * s, frame.left + r * s, frame.top + b * s) private fun text(canvas: Canvas, str: String, x: Float, y: Float, size: Float, color: Int = Color.WHITE, bold: Boolean = true, - align: Paint.Align = Paint.Align.CENTER) { + align: Paint.Align = Paint.Align.CENTER, maxWidth: Float = 0f) { val p = Paint(Paint.ANTI_ALIAS_FLAG).apply { this.color = color; textSize = size; textAlign = align typeface = if (bold) android.graphics.Typeface.DEFAULT_BOLD else android.graphics.Typeface.DEFAULT } + if (maxWidth > 0f) p.measureText(str).let { if (it > maxWidth) p.textSize = size * maxWidth / it } canvas.drawText(str, x, y - (p.descent() + p.ascent()) / 2, p) } @@ -1004,7 +1006,7 @@ class SteamTouchControls( canvas.drawRoundRect(r, radius, radius, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = line2; style = Paint.Style.STROKE; strokeWidth = 1.2f * s }) } val ink = when { !enabled -> Color.argb(110, 242, 244, 247); primary -> onAccent; else -> onSurface } - text(canvas, text, r.centerX(), r.centerY(), 14f * s, ink) + text(canvas, text, r.centerX(), r.centerY(), 14f * s, ink, maxWidth = r.width() - 16f * s) if (enabled) hots += Hot(r, onTap) } @@ -1044,7 +1046,7 @@ class SteamTouchControls( BIG_PICTURE -> "Steam" else -> gameNames[appId] ?: run { val app = appId - gameNames[app] = "app $app" + gameNames[app] = context.getString(R.string.touch_app_id, app) loader.execute { val name = SteamTouchConfig.gameName(context, app) ?: return@execute handler.post { gameNames[app] = name; invalidate() } @@ -1061,7 +1063,7 @@ class SteamTouchControls( // Header: power (DroidDeck's own menu: stop, settings), the Steam mark, what is running, close. iconButton(canvas, ref(f, s, 494f, 18f, 531f, 55f), false, { menuOpen = false; onMenu() }) { c, r, col -> drawPower(c, r, col) } drawMark(canvas, f.left + 104f * s, f.top + 37f * s, 13f * s) - text(canvas, "Touch controls", f.left + 124f * s, f.top + 30f * s, 14f * s, onSurface, align = Paint.Align.LEFT) + text(canvas, context.getString(R.string.touch_title), f.left + 124f * s, f.top + 30f * s, 14f * s, onSurface, align = Paint.Align.LEFT) text(canvas, gameName(), f.left + 124f * s, f.top + 46f * s, 11f * s, muted, bold = false, align = Paint.Align.LEFT) closeButton(canvas, f, s) { menuOpen = false } val controller = options.inputMode != SteamTouchConfig.INPUT_MOUSE @@ -1071,10 +1073,10 @@ class SteamTouchControls( m -> SteamTouchConfig.INPUT_MOUSE else -> SteamTouchConfig.INPUT_CONTROLLER })) - tile(canvas, ref(f, s, 45f, 81f, 249f, 271f), s, "Touch Controller", controller, { setModes(!controller, mouse || controller) }) { c, r, col -> + tile(canvas, ref(f, s, 45f, 81f, 249f, 271f), s, context.getString(R.string.touch_controller), controller, { setModes(!controller, mouse || controller) }) { c, r, col -> drawPhonePad(c, r, col) } - tile(canvas, ref(f, s, 271f, 81f, 475f, 271f), s, "Mouse", mouse, { setModes(controller || mouse, !mouse) }) { c, r, col -> + tile(canvas, ref(f, s, 271f, 81f, 475f, 271f), s, context.getString(R.string.touch_mouse), mouse, { setModes(controller || mouse, !mouse) }) { c, r, col -> drawMouse(c, r, col, crossed = false) } // Under the controller: gyroscope, fade, vibrate on touch. @@ -1102,10 +1104,13 @@ class SteamTouchControls( val speed = speeds.indexOfFirst { abs(it - options.trackpadSensitivity) < 0.2f }.coerceAtLeast(1) iconButton(canvas, ref(f, s, 412f, 278f, 475f, 318f), mouse && relative, { setOptions(options.copy(trackpadSensitivity = speeds[(speed + 1) % speeds.size])) - }) { c, r, col -> text(c, listOf("Slow", "Normal", "Fast")[speed], r.centerX(), r.centerY(), r.height() * 0.32f, col) } + }) { c, r, col -> + val name = context.getString(listOf(R.string.touch_speed_slow, R.string.touch_speed_normal, R.string.touch_speed_fast)[speed]) + text(c, name, r.centerX(), r.centerY(), r.height() * 0.32f, col, maxWidth = r.width() - 6f * s) + } drawVolume(canvas, f, s) - blueButton(canvas, ref(f, s, 45f, 329f, 249f, 372f), "Edit layout", s, primary = true) { menuOpen = false; startEditing() } - blueButton(canvas, ref(f, s, 271f, 329f, 475f, 372f), "DroidDeck menu", s) { menuOpen = false; onMenu() } + blueButton(canvas, ref(f, s, 45f, 329f, 249f, 372f), context.getString(R.string.touch_edit_layout), s, primary = true) { menuOpen = false; startEditing() } + blueButton(canvas, ref(f, s, 271f, 329f, 475f, 372f), context.getString(R.string.touch_droiddeck_menu), s) { menuOpen = false; onMenu() } } private val audio by lazy { context.getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager } @@ -1173,7 +1178,7 @@ class SteamTouchControls( canvas.drawRoundRect(r, radius, radius, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = if (on) accent else line; style = Paint.Style.STROKE; strokeWidth = (if (on) 2f else 1.2f) * s }) - text(canvas, title, r.left + 16f * s, r.top + 22f * s, 14f * s, onSurface, align = Paint.Align.LEFT) + text(canvas, title, r.left + 16f * s, r.top + 22f * s, 14f * s, onSurface, align = Paint.Align.LEFT, maxWidth = r.width() - 72f * s) // A switch, top right. val sw = RectF(r.right - 50f * s, r.top + 12f * s, r.right - 14f * s, r.top + 32f * s) canvas.drawRoundRect(sw, sw.height() / 2, sw.height() / 2, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = if (on) accent else line2 }) @@ -1181,7 +1186,7 @@ class SteamTouchControls( Paint(Paint.ANTI_ALIAS_FLAG).apply { color = if (on) onAccent else muted }) val artRect = RectF(r.centerX() - 58f * s, r.top + 62f * s, r.centerX() + 58f * s, r.top + 140f * s) art(canvas, artRect, if (on) onSurface else muted) - text(canvas, if (on) "On" else "Off", r.left + 16f * s, r.bottom - 18f * s, 12f * s, if (on) accent else muted, bold = false, align = Paint.Align.LEFT) + text(canvas, context.getString(if (on) R.string.touch_on else R.string.touch_off), r.left + 16f * s, r.bottom - 18f * s, 12f * s, if (on) accent else muted, bold = false, align = Paint.Align.LEFT) hots += Hot(r, toggle) } @@ -1194,9 +1199,9 @@ class SteamTouchControls( closeButton(canvas, f, s) { save() } val sets = SteamTouchConfig.actionSets(config) val setName = sets.firstOrNull { it.first == editSet }?.second - val title = if (sets.size > 1 && setName != null) "${gameName()} · $setName" else "${gameName()} ${config.title ?: "Layout"}" - text(canvas, title, f.centerX(), f.top + 30f * s, 14f * s, onSurface) - text(canvas, "Drag controls onto the screen", f.centerX(), f.top + 47f * s, 11f * s, muted, bold = false) + val title = if (sets.size > 1 && setName != null) "${gameName()} · $setName" else "${gameName()} ${config.title ?: context.getString(R.string.ctrl_layout)}" + text(canvas, title, f.centerX(), f.top + 30f * s, 14f * s, onSurface, maxWidth = f.width() - 200f * s) + text(canvas, context.getString(R.string.touch_drag_hint), f.centerX(), f.top + 47f * s, 11f * s, muted, bold = false, maxWidth = f.width() - 200f * s) if (sets.size > 1) { arrowPill(canvas, ref(f, s, 120f, 26f, 150f, 44f), -1) { stepEditSet(-1) } arrowPill(canvas, ref(f, s, 474f, 26f, 504f, 44f), 1) { stepEditSet(1) } @@ -1251,8 +1256,8 @@ class SteamTouchControls( shader = android.graphics.RadialGradient(wheel.centerX(), wheel.centerY(), wheel.width() / 2f, Color.WHITE, Color.TRANSPARENT, android.graphics.Shader.TileMode.CLAMP) }) hots += Hot(wheel, { pickColor() }) - blueButton(canvas, ref(f, s, 45f, 329f, 232f, 372f), "Arrange", s, primary = true) { trayOpen = false } - blueButton(canvas, ref(f, s, 254f, 329f, 441f, 372f), "Discard", s) { stopEditing() } + blueButton(canvas, ref(f, s, 45f, 329f, 232f, 372f), context.getString(R.string.touch_arrange), s, primary = true) { trayOpen = false } + blueButton(canvas, ref(f, s, 254f, 329f, 441f, 372f), context.getString(R.string.touch_discard), s) { stopEditing() } } private fun arrowPill(canvas: Canvas, r: RectF, dir: Int, onTap: () -> Unit) { @@ -1290,7 +1295,10 @@ class SteamTouchControls( return RectF(x - r, y - r, x + r, y + r) } - private val contextLabels = listOf("Icon", "W−", "W+", "H−", "H+", "Hide") + private val contextLabels by lazy { + listOf(R.string.touch_icon, R.string.touch_narrower, R.string.touch_wider, R.string.touch_shorter, R.string.touch_taller, R.string.touch_hide) + .map(context::getString) + } private fun contextRects(): List { val e = editElements.getOrNull(selected) ?: return emptyList() @@ -1313,11 +1321,11 @@ class SteamTouchControls( canvas.drawRoundRect(m, m.width() * 0.15f, m.width() * 0.15f, p) drawDots(canvas, m.centerX(), m.centerY() - m.height() * 0.1f, m.width() * 0.5f, Color.WHITE) hots += Hot(m, { trayOpen = true }) - if (saving) text(canvas, "Saving…", width / 2f, height * 0.08f, 22f * max(unit(), 0.6f)) + if (saving) text(canvas, context.getString(R.string.touch_saving), width / 2f, height * 0.08f, 22f * max(unit(), 0.6f)) contextRects().forEachIndexed { i, r -> canvas.drawRoundRect(r, r.height() * 0.3f, r.height() * 0.3f, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = surface }) canvas.drawRoundRect(r, r.height() * 0.3f, r.height() * 0.3f, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = line2; style = Paint.Style.STROKE; strokeWidth = 1.2f }) - text(canvas, contextLabels[i], r.centerX(), r.centerY(), r.height() * 0.4f, onSurface, bold = false) + text(canvas, contextLabels[i], r.centerX(), r.centerY(), r.height() * 0.4f, onSurface, bold = false, maxWidth = r.width() - 6f) hots += Hot(r, { contextTool(i) }) } } @@ -1448,12 +1456,14 @@ class SteamTouchControls( val ref = visual?.ref val binding = visual?.binding if (ref == null || binding == null) { - Toast.makeText(context, "This control has no binding in Steam to put an icon on", Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.touch_no_binding, Toast.LENGTH_SHORT).show() return } - val name = if (e.type == SteamTouchConfig.DPAD) "D-pad " + listOf("up", "down", "left", "right")[selectedArm] else label(e.type) + val name = if (e.type == SteamTouchConfig.DPAD) { + context.getString(listOf(R.string.touch_dpad_up, R.string.touch_dpad_down, R.string.touch_dpad_left, R.string.touch_dpad_right)[selectedArm]) + } else label(e.type) val host = parent as? android.view.ViewGroup ?: return - SteamTouchIconPicker(context, host, appId, "Icon for $name", binding) { picked -> + SteamTouchIconPicker(context, host, appId, context.getString(R.string.touch_icon_for, name), binding) { picked -> pendingBindings[ref] = picked rebuildVisuals() invalidate() @@ -1480,9 +1490,9 @@ class SteamTouchControls( } }, android.widget.LinearLayout.LayoutParams(dp(30), dp(30)).apply { marginEnd = dp(6) }) } - root.addView(android.widget.TextView(context).apply { text = "Colour" }) + root.addView(android.widget.TextView(context).apply { setText(R.string.ctrl_color) }) root.addView(android.widget.HorizontalScrollView(context).apply { addView(swatchRow) }) - root.addView(android.widget.TextView(context).apply { text = "Opacity"; setPadding(0, dp(12), 0, 0) }) + root.addView(android.widget.TextView(context).apply { setText(R.string.ctrl_opacity); setPadding(0, dp(12), 0, 0) }) val opacityRow = android.widget.LinearLayout(context) listOf(20, 35, 50, 65, 80, 100).forEach { percent -> opacityRow.addView(android.widget.Button(context).apply { @@ -1497,7 +1507,7 @@ class SteamTouchControls( } root.addView(android.widget.HorizontalScrollView(context).apply { addView(opacityRow) }) val host = parent as? android.view.ViewGroup ?: return - SteamTouchIconPicker.showPanel(host, "Touch layout colour", root, listOf("Done" to {})) + SteamTouchIconPicker.showPanel(host, context.getString(R.string.touch_layout_color), root, listOf(context.getString(R.string.touch_done) to {})) } private fun save() { @@ -1528,11 +1538,11 @@ class SteamTouchControls( handler.post { saving = false if (file == null) { - Toast.makeText(context, "Could not save the touch layout", Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.touch_save_failed, Toast.LENGTH_SHORT).show() invalidate() return@post } - Toast.makeText(context, "Touch layout saved to Steam", Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.touch_saved, Toast.LENGTH_SHORT).show() onSaved() stopEditing() reloadConfig() diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt index ce7f28fd6..fdd6f423f 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt @@ -21,6 +21,7 @@ import android.widget.HorizontalScrollView import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import com.droiddeck.launcher.R import java.util.concurrent.Executors /** @@ -50,7 +51,7 @@ class SteamTouchIconPicker( val root = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL; setPadding(dp(16), dp(8), dp(16), 0) } val left = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL } val label = EditText(context).apply { - hint = "Label (optional)" + setHint(R.string.touch_label_hint) setText(current.label) inputType = InputType.TYPE_CLASS_TEXT setSingleLine() @@ -77,21 +78,22 @@ class SteamTouchIconPicker( setPadding(0, dp(8), 0, 0) addView(preview, LinearLayout.LayoutParams(dp(56), dp(56))) addView(TextView(context).apply { - text = if (names.isEmpty()) "Steam's icons were not found" else "${names.size} icons from Steam" + text = if (names.isEmpty()) context.getString(R.string.touch_icons_missing) + else context.resources.getQuantityString(R.plurals.touch_icons_found, names.size, names.size) setPadding(dp(12), 0, 0, 0) }) }) - left.addView(paletteRow("Icon colour", { foreground }) { foreground = it; adapter.notifyDataSetChanged(); updatePreview(preview) }) - left.addView(paletteRow("Button colour", { background }) { background = it; adapter.notifyDataSetChanged(); updatePreview(preview) }) + left.addView(paletteRow(context.getString(R.string.touch_icon_color), { foreground }) { foreground = it; adapter.notifyDataSetChanged(); updatePreview(preview) }) + left.addView(paletteRow(context.getString(R.string.touch_button_color), { background }) { background = it; adapter.notifyDataSetChanged(); updatePreview(preview) }) root.addView(left, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)) root.addView(grid, LinearLayout.LayoutParams(0, dp(250), 1.2f).apply { marginStart = dp(16) }) updatePreview(preview) showPanel(host, title, root, listOf( - "No icon" to { + context.getString(R.string.touch_no_icon) to { onPicked(current.copy(label = label.text.toString().replace(",", " ").trim(), icon = "", foreground = "", background = "")) }, - "Cancel" to {}, - "Use" to { + context.getString(R.string.common_cancel) to {}, + context.getString(R.string.common_use) to { onPicked(current.copy(label = label.text.toString().replace(",", " ").trim(), icon = icon, foreground = if (icon.isEmpty()) "" else foreground, background = if (icon.isEmpty()) "" else background)) }, diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt index 9f16663a5..960666133 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt @@ -51,7 +51,7 @@ object AmazonBackend : StoreBackend { val quiet = cached.isNotEmpty() && !force StoresState.post { if (cached.isNotEmpty()) publish(app, cached) - if (!quiet) StoresState.status[store] = "Fetching your library…" + if (!quiet) StoresState.status[store] = app.getString(com.droiddeck.launcher.R.string.stores_sync_library) } val result = AmazonLibrary.sync(app, force) { line -> if (!quiet) StoresState.post { StoresState.status[store] = line } } StoresState.post { @@ -94,7 +94,7 @@ object AmazonBackend : StoreBackend { private val cancelled = AtomicBoolean(false) override fun run(handle: DownloadQueue.JobHandle): String? { - handle.stage(DownloadStage.MANIFEST, "Checking sign-in…") + handle.stage(DownloadStage.MANIFEST, app.getString(com.droiddeck.launcher.R.string.stores_sync_sign_in)) val token = AmazonCredentialStore.getValidAccessToken(app) ?: throw AmazonDownloadManager.InstallException("Not signed in to Amazon Games") StoreInstalls.begin(folder, Store.AMAZON, game.productId, game.title, item.tallImageUrl ?: item.imageUrl, item.imageUrl) var downloading = false @@ -111,7 +111,7 @@ object AmazonBackend : StoreBackend { override fun onSizes(downloadBytes: Long, diskBytes: Long) { handle.diskSize(diskBytes) } }) ?: return null if (cancelled.get()) return null - handle.stage(DownloadStage.INSTALL, "Registering with Steam…") + handle.stage(DownloadStage.INSTALL, app.getString(com.droiddeck.launcher.R.string.stores_registering)) AmazonLibrary.rememberSize(app, game.productId, result.bytes) AmazonPrefs.get(app).edit().putString("version_${game.productId}", result.versionId).apply() val spec = AmazonLaunchHelper.buildLaunchSpec(folder, game.title) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt index 197d6a816..0cd3be6b0 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt @@ -3,6 +3,7 @@ package com.droiddeck.launcher.stores.amazon import android.content.Context import android.content.SharedPreferences import android.util.Log +import com.droiddeck.launcher.R import com.droiddeck.launcher.stores.CatalogItem import com.droiddeck.launcher.stores.Store import com.droiddeck.launcher.stores.StoreNet @@ -66,38 +67,38 @@ object AmazonLibrary { val p = AmazonPrefs.get(app) val cachedList = cached(app) if (!force && cachedList.isNotEmpty() && System.currentTimeMillis() - p.getLong(LAST_SYNC_KEY, 0L) < THROTTLE_MS) { - backfillPosters(p, cachedList, onStatus) + backfillPosters(app, p, cachedList, onStatus) return SyncResult.Throttled } - onStatus("Checking sign-in…") + onStatus(app.getString(R.string.stores_sync_sign_in)) val creds = AmazonCredentialStore.load(app) ?: return SyncResult.NotLoggedIn val token = AmazonCredentialStore.getValidAccessToken(app) ?: return SyncResult.Failed(StoresState.notSignedInLine(Store.AMAZON)) - onStatus("Fetching game list…") + onStatus(app.getString(R.string.stores_sync_game_list)) // Refused with the stored token (revoked, or an expiry read wrong): once more on a fresh one. val all = AmazonApiClient.getEntitlements(token, creds.deviceSerial) ?: AmazonCredentialStore.refresh(app)?.let { AmazonApiClient.getEntitlements(it, creds.deviceSerial) } - if (all == null) return if (cachedList.isEmpty()) SyncResult.Failed("Amazon Games did not answer for your library: refresh, or sign in again") else SyncResult.Ok(cachedList) - if (all.isEmpty()) return if (cachedList.isEmpty()) SyncResult.Failed("No games found in your Amazon library") else SyncResult.Ok(cachedList) + if (all == null) return if (cachedList.isEmpty()) SyncResult.Failed(app.getString(R.string.stores_sync_amazon_no_answer)) else SyncResult.Ok(cachedList) + if (all.isEmpty()) return if (cachedList.isEmpty()) SyncResult.Failed(app.getString(R.string.stores_sync_no_games, Store.AMAZON.label)) else SyncResult.Ok(cachedList) val games = all.filter { !(it.isDLC && it.parentProductId.isNotEmpty()) }.ifEmpty { all } .sortedWith { a, b -> a.title.compareTo(b.title, ignoreCase = true) } for (fresh in games) cachedList.firstOrNull { it.productId == fresh.productId }?.let { fresh.versionId = it.versionId } saveCache(p, games) p.edit().putLong(LAST_SYNC_KEY, System.currentTimeMillis()).apply() - backfillPosters(p, games, onStatus) + backfillPosters(app, p, games, onStatus) Log.i(TAG, "sync: entitlements=${all.size} games=${games.size}") return SyncResult.Ok(games) } catch (e: Exception) { Log.w(TAG, "sync failed: ${e.message}") - return SyncResult.Failed(e.message ?: "Sync failed") + return SyncResult.Failed(e.message ?: context.getString(R.string.stores_sync_failed)) } finally { syncing.set(false) } } - private fun backfillPosters(p: SharedPreferences, games: List, onStatus: (String) -> Unit) { + private fun backfillPosters(app: Context, p: SharedPreferences, games: List, onStatus: (String) -> Unit) { val missing = games.filter { it.productId.isNotEmpty() && !p.contains("vcover_${it.productId}") }.take(POSTER_BUDGET) if (missing.isEmpty()) return - onStatus("Fetching cover art…") + onStatus(app.getString(R.string.stores_sync_cover_art)) val pool = Executors.newFixedThreadPool(4) val futures = missing.map { g -> pool.submit(Callable { g.productId to StoreNet.sgdbPoster(g.title) }) } pool.shutdown() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt index 57b5c4812..97c3cd3a9 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,7 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { val quiet = cached.isNotEmpty() && !force StoresState.post { if (cached.isNotEmpty()) StoresState.library[store] = com.droiddeck.launcher.stores.mergeItems(StoresState.library[store], cached) - if (!quiet) StoresState.status[store] = "Fetching your library…" + if (!quiet) StoresState.status[store] = app.getString(com.droiddeck.launcher.R.string.stores_sync_library) } val result = EpicLibrary.sync(app, force) { line -> if (!quiet) StoresState.post { StoresState.status[store] = line } } StoresState.post { @@ -122,7 +122,7 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { private fun hasChunks(dir: File): Boolean = dir.isDirectory && (dir.list()?.isNotEmpty() == true) override fun run(handle: DownloadQueue.JobHandle): String? { - handle.stage(DownloadStage.MANIFEST, "Checking sign-in…") + handle.stage(DownloadStage.MANIFEST, app.getString(com.droiddeck.launcher.R.string.stores_sync_sign_in)) val token = EpicCredentialStore.getValidAccessToken(app) ?: throw EpicDownloadManager.InstallException("Not signed in to Epic Games") StoreInstalls.begin(folder, Store.EPIC, item.id, item.title, item.tallImageUrl ?: item.imageUrl, item.imageUrl) if (scratch == null && hasChunks(File(folder, ".chunks"))) handle.log("epic: resuming \"${item.title}\" with the chunks already beside it") @@ -151,7 +151,7 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { } }) ?: return null if (cancelled.get()) return null - handle.stage(DownloadStage.INSTALL, "Registering with Steam…") + handle.stage(DownloadStage.INSTALL, app.getString(com.droiddeck.launcher.R.string.stores_registering)) EpicLibrary.rememberSize(app, item.id, result.bytes) val deploymentId = runCatching { JSONObject(manifestJson).optString("deploymentId", "") }.getOrDefault("") val exe = StoreExe.pick(folder, result.launchExe, item.title) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt index d65b0c3e6..f9c0c04bd 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt @@ -3,6 +3,7 @@ package com.droiddeck.launcher.stores.epic import android.content.Context import android.content.SharedPreferences import android.util.Log +import com.droiddeck.launcher.R import com.droiddeck.launcher.stores.CatalogItem import com.droiddeck.launcher.stores.Store import org.json.JSONArray @@ -67,17 +68,17 @@ object EpicLibrary { val cachedList = cached(app) val lastSync = p.getLong(LAST_SYNC_KEY, 0L) if (!force && cachedList.isNotEmpty() && System.currentTimeMillis() - lastSync < THROTTLE_MS) return SyncResult.Throttled - onStatus("Checking sign-in…") + onStatus(app.getString(R.string.stores_sync_sign_in)) val token = EpicCredentialStore.getValidAccessToken(app) ?: return SyncResult.NotLoggedIn - onStatus("Fetching game list…") + onStatus(app.getString(R.string.stores_sync_game_list)) val raw = EpicApiClient.getLibraryItems(token) - if (raw.isNullOrEmpty()) return if (cachedList.isEmpty()) SyncResult.Failed("No games found in your Epic library") else SyncResult.Ok(cachedList) + if (raw.isNullOrEmpty()) return if (cachedList.isEmpty()) SyncResult.Failed(app.getString(R.string.stores_sync_no_games, Store.EPIC.label)) else SyncResult.Ok(cachedList) val total = raw.size var done = 0 for (game in raw) { EpicApiClient.enrichFromCatalog(token, game) done++ - if (done % 5 == 0) onStatus("Loading game details… ($done/$total)") + if (done % 5 == 0) onStatus(app.getString(R.string.stores_sync_details, done, total)) } val main = raw.filter { !it.isDLC } val display = (if (main.isEmpty()) raw else main).distinctBy { it.appName }.sortedWith { a, b -> a.title.compareTo(b.title, ignoreCase = true) } @@ -92,7 +93,7 @@ object EpicLibrary { return SyncResult.Ok(display) } catch (e: Exception) { Log.w(TAG, "sync failed: ${e.message}") - return SyncResult.Failed(e.message ?: "Sync failed") + return SyncResult.Failed(e.message ?: context.getString(R.string.stores_sync_failed)) } finally { syncing.set(false) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogBackend.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogBackend.kt index c2bbfa635..f167265e8 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 @@ -38,7 +38,7 @@ object GogBackend : StoreBackend { val quiet = cached.isNotEmpty() && !force StoresState.post { if (cached.isNotEmpty()) StoresState.library[store] = com.droiddeck.launcher.stores.mergeItems(StoresState.library[store], cached) - if (!quiet) StoresState.status[store] = "Fetching your library…" + if (!quiet) StoresState.status[store] = app.getString(com.droiddeck.launcher.R.string.stores_sync_library) } val result = GogLibrary.sync(app, force) { line -> if (!quiet) StoresState.post { StoresState.status[store] = line } } StoresState.post { @@ -106,7 +106,7 @@ object GogBackend : StoreBackend { } }, cancelled) ?: return null if (cancelled.get()) return null - handle.stage(DownloadStage.INSTALL, "Registering with Steam…") + handle.stage(DownloadStage.INSTALL, app.getString(com.droiddeck.launcher.R.string.stores_registering)) val sidecar = StoreGameSidecar( Store.GOG, game.gameId, game.title, exe = result.exeRelative.ifEmpty { "" }, installVersion = result.buildId, installedAt = System.currentTimeMillis(), diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt index cba199ce7..79d0abf73 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt @@ -3,6 +3,7 @@ package com.droiddeck.launcher.stores.gog import android.content.Context import android.content.SharedPreferences import android.util.Log +import com.droiddeck.launcher.R import com.droiddeck.launcher.stores.CatalogItem import com.droiddeck.launcher.stores.Store import com.droiddeck.launcher.stores.StoreNet @@ -60,9 +61,9 @@ object GogLibrary { val app = context.applicationContext val p = GogPrefs.get(app) val token = GogAuth.validToken(app) ?: return SyncResult.NotLoggedIn - onStatus("Fetching game list…") + onStatus(app.getString(R.string.stores_sync_game_list)) val gamesJson = StoreNet.get("https://embed.gog.com/user/data/games", bearer = token, userAgent = GogAuth.GALAXY_UA) - ?: return SyncResult.Failed("Couldn't reach GOG") + ?: return SyncResult.Failed(app.getString(R.string.stores_sync_gog_unreachable)) val ids = ArrayList() val parsed = runCatching { val owned = JSONObject(gamesJson).optJSONArray("owned") @@ -72,7 +73,7 @@ object GogLibrary { if (id != "1801418160" && id != "0") ids.add(id) } }.isSuccess - if (!parsed) return SyncResult.Failed("Error parsing library") + if (!parsed) return SyncResult.Failed(app.getString(R.string.stores_sync_gog_unreadable)) val ownedSet = ids.toHashSet() val cachedList = cached(app) @@ -92,7 +93,7 @@ object GogLibrary { saveCache(p, list) return SyncResult.Ok(list.sortedBy { it.title.lowercase() }, 0) } - onStatus("Syncing ${idsToFetch.size} game${if (idsToFetch.size == 1) "" else "s"}…") + onStatus(app.resources.getQuantityString(R.plurals.stores_sync_games, idsToFetch.size, idsToFetch.size)) val pool = Executors.newFixedThreadPool(5) val futures = idsToFetch.map { id -> pool.submit(Callable { fetchGame(p, id, token, skipped) }) } pool.shutdown() @@ -100,7 +101,7 @@ object GogLibrary { for ((idx, f) in futures.withIndex()) { val g = runCatching { f.get() }.getOrNull() if (g != null) { merged[g.gameId] = g; fetched++ } - if ((idx + 1) % 5 == 0) onStatus("Syncing… ${idx + 1}/${futures.size}") + if ((idx + 1) % 5 == 0) onStatus(app.getString(R.string.stores_sync_progress, idx + 1, futures.size)) } val finalList = merged.values.toList() saveCache(p, finalList) @@ -111,7 +112,7 @@ object GogLibrary { return SyncResult.Ok(finalList.sortedBy { it.title.lowercase() }, fetched) } catch (e: Exception) { Log.w(TAG, "sync failed: ${e.message}") - return SyncResult.Failed(e.message ?: "Sync failed") + return SyncResult.Failed(e.message ?: context.getString(R.string.stores_sync_failed)) } finally { syncing.set(false) } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 000000000..f02b3a580 --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,2335 @@ + + + DroidDeck + + يُنزَّل Steam عند التشغيل الأول. قد يستغرق ذلك بضع دقائق. + افتراضيًا، يؤدي الخروج من التطبيق إلى إيقاف Steam مؤقتًا، ويُستأنف من حيث توقف عند عودتك. + تظهر وحدة التحكم لديك كوحدة تحكم Xbox 360. وإلا، فاستخدم عناصر التحكم على الشاشة. + + + يُعَدّ سطح المكتب عند التشغيل الأول. قد يستغرق ذلك بضع دقائق. + افتراضيًا، يؤدي الخروج من التطبيق إلى إيقاف الجلسة مؤقتًا، وتُستأنف من حيث توقفت عند عودتك. + يعمل اللمس كلوحة لمس: اسحب لتحريك المؤشر، وانقر لتنفيذ نقرة. + تظهر وحدة التحكم لديك كوحدة تحكم Xbox 360. وإلا، فاستخدم عناصر التحكم على الشاشة. + + الجلسة + يظهر أثناء فتح جلسة Steam أو جلسة سطح المكتب + Steam قيد التشغيل + يجري تنزيل في Steam + الجلسة متوقفة مؤقتًا - انقر على «استئناف» عندما تكون مستعدًا + استئناف + تعليق الآن + إيقاف الجلسة + توليد الإطارات + اختر وضع توليد الإطارات + إيقاف + Win-FG %1$d× + LSFG %1$d× + LSFG تكيّفي %1$d + يتطلب Lossless Scaling على Steam + ثبّت Lossless Scaling في Steam أو استورد Lossless.dll + استيراد Lossless.dll… + يتم استخدام النسخة الموجودة في Steam + يتم استخدام نسخة مستوردة + اختر Lossless.dll + تم استيراد Lossless.dll + ملف Lossless.dll هذا قيد الاستخدام بالفعل + حساب Steam هذا لا يملك Lossless Scaling + هذا الملف ليس Lossless.dll الخاص بـ Lossless Scaling + تعذّر تجهيز LSFG من ملف Lossless.dll هذا + يتطلب LSFG برنامج Lossless Scaling من Steam أو ملف Lossless.dll مستوردًا + تعذّر على LSFG تجهيز أدوات التظليل من Lossless.dll + %1$s: غير مدعوم + %1$s: فشل المحرك + %1$s: يتطلب Lossless Scaling + %1$s %2$s · جارٍ البدء + يضيف مكتبة Steam ويفحصها بحثًا عن ألعاب Windows. تستخدم الألعاب المملوكة إدخالاتها في Steam، ويتحول الباقي إلى اختصارات لألعاب من خارج Steam. + بيئة اللعبة + متغيرات البيئة لألعاب Proton. + تعديل + جميع ألعاب Proton + ملف التعريف + معرّف تطبيق Steam + فتح + اللعبة %1$s + إضافة متغير + اسم المتغير + القيمة + حفظ + إزالة + استعادة الوراثة + مسح تجاوزات البيئة + إلغاء التعيين لملف التعريف هذا + لم يتم تعيين أي متغيرات. + موروث + متغير مخصّص + تعذّر التحميل أو الحفظ. تحقق من مساحة التخزين المتاحة وحاول مرة أخرى. + استخدم اسمًا صالحًا وقيمة لا تتجاوز 8192 حرفًا. + لا يقرأ وقت تشغيل Proton القياسي هذا المتغير. + جارٍ التحميل… + تجاوز مستوى ميزات D3D12. تُبلغ القيمة 12_2 عن إمكانات DirectX 12 Ultimate، وقد تفشل الميزات غير المدعومة. اتركه دون تعيين للاكتشاف التلقائي. + تجاوز نموذج أدوات التظليل في D3D12. تتطلب القيمة 6_9 إصدارًا حديثًا بما يكفي من VKD3D وميزات GPU مطابقة. اتركه دون تعيين للاكتشاف التلقائي. + أبقِ التخزين المؤقت لأدوات التظليل في Mesa مفعّلًا لتقليل تكرار التجميع. القيمة false تفعّل التخزين المؤقت. + الحد الأقصى لحجم ذاكرة التخزين المؤقت لأدوات التظليل في Mesa، مثل 1G. تستهلك ذاكرة التخزين المؤقت الأكبر مساحة تخزين أكثر. + خيارات VKD3D لـ D3D12. يعطّل nodxr تتبع الأشعة؛ استخدمه فقط للعبة تحتاج إليه. + اضبطه على 1 للحصول على سجل تشخيص Proton. قد يقلل التسجيل من الأداء ويستهلك مساحة التخزين. + اضبطه على 1 لاستخدام WineD3D المعتمد على OpenGL بدلًا من DXVK لاختبار توافق D3D9–11. + اضبطه على 1 لإبعاد هذه اللعبة عن ntsync (droiddeck-ntsync أو برنامج تشغيل في النواة)؛ فتستخدم droiddeck-esync حيث تتوفر حزمة، وإلا droiddeck-fsync. + اضبطه على 1 لإيقاف droiddeck-esync لهذه اللعبة؛ فتستخدم droiddeck-fsync بدلًا منه، أو droiddeck-ntsync إذا كان fsync متوقفًا أيضًا. + اضبطه على 1 لإبعاد هذه اللعبة عن droiddeck-fsync؛ فتستخدم droiddeck-esync حيث تتوفر حزمة، وإلا droiddeck-ntsync. + اضبطه على 1 لتشغيل هذه اللعبة على droiddeck-fsync، أو على 0 لتقديم droiddeck-esync حيث تتوفر حزمة تناسب إصدار Proton الخاص بها. + اضبطه على 0 لتعطيل مساعد التنقل بوحدة التحكم في Proton لهذه اللعبة. + تراكب DXVK لـ D3D9–11. القيمة fps تعرض معدل الإطارات، وتركه دون تعيين يخفيه. + إعدادات DXVK مفصولة بفواصل منقوطة. تستخدم الإصدارات الحديثة dxvk.maxFrameRate لتحديد الإطارات؛ يُفضَّل استخدام محدِّد الإطارات الخاص بالجلسة للعب العادي. + حد معدل الإطارات في D3D12. القيمة 0 تلغي الحد. + تعدد مؤشرات الترابط في OpenGL ضمن Mesa. قد تساعد القيمة true ألعاب OpenGL المقيدة بأداء CPU؛ جرّبها لكل لعبة. + مزامنة Wine + طريقة إبقاء الألعاب لمؤشرات الترابط متزامنة. يُطبَّق في الجلسة التالية. + esync + ntsync + fsync + wineserver + إعداد FEX المسبق + يُطبَّق عند التشغيل التالي للعبة. + الألعاب + إعدادات FEX الافتراضية + يستخدم إعدادات FEX الافتراضية. + الثبات + الإعدادات الأكثر أمانًا والأبطأ. + التوافق + TSO كامل مع multiblock. + متوسط + TSO متوازن مع دقة x87 مخفّضة. + الأداء + TSO معطّل بالكامل؛ مع multiblock. + الأداء + TSO + إعدادات «الأداء» مع تفعيل TSO الرئيسي. + فائق + يعطّل فحوص التعليمات البرمجية ذاتية التعديل. قد تتعطل الألعاب التي تستخدم JIT وأنظمة DRM. + فائق + TSO + إعدادات «فائق» مع تفعيل TSO الرئيسي. + فائق-gn + «الأداء» مع تلميحات TSC والبيانات الوصفية. جرّبه قبل «فائق». + Denuvo + «الأداء» مع فحوص كاملة للتعليمات البرمجية وإخفاء بت hypervisor. + قيمة مخصّصة + اختر قيمة + المحاكيات والتطبيقات + إضافة + تطبيقك أو لعبتك الخاصة + نص برمجي + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + جارٍ البدء… + تمت إضافة %1$s + تمت إزالة %1$s + سطح المكتب + لم يعد هذا التطبيق موجودًا. اختره مجددًا من سطح المكتب. + فتح %1$s + إزالة + اضغط مجددًا للإزالة + أوقف الجلسة لتتمكن من الإزالة + يشغّل %1$s من مجلده، المرتبط بالجلسة في مكانه. لا تؤثر إزالة التطبيق على ذلك المجلد. + يشغّل %1$s من نسخة من مجلده داخل DroidDeck. تؤدي إزالة التطبيق إلى حذف النسخة، لا الأصل. + ثبّت بيئة تشغيل Linux أولًا + لم يعد الملف موجودًا + لا توجد مساحة كافية: يتطلب نسخ المجلد %1$s + جارٍ نسخ %1$s + تعذّرت إضافة التطبيق + جارٍ البحث عن إصدار من %1$s + تعذّرت قراءة إصدارات %1$s + لا يحتوي أي إصدار من %1$s على AppImage لـ ARM64 أو x86_64 + جارٍ تنزيل %1$s + فشل التنزيل + تعذّر تحديث التطبيق + تعذّرت قراءة هذا الرمز + تم حفظ %1$s + تعديل + البحث عن تحديثات + جارٍ البحث عن تحديثات… + التحديث إلى %1$s + محدَّث · %1$s + تم تحديث %1$s + %1$s · %2$s + إضافة تطبيق + نص برمجي + AppImage + GitHub + Flatpak + ملف ‎.sh. يعمل من مجلده الخاص، بملء الشاشة ضمن gamescope. + ملف AppImage لـ ARM64 (aarch64)، أو لـ x86_64 يعمل عبر FEX. يُفَك ضمن بيئة تشغيل Linux مرة واحدة. + يثبّت ملف AppImage لـ ARM64 من أحدث إصدار في المستودع، أو ملف x86_64 عبر FEX. + استخدام FEX + تلقائي + تشغيل + إيقاف + يشغّل FEX برامج Linux المخصصة لـ x86 على جهاز ARM64 هذا. يستخدمه الخيار «تلقائي» عندما يكون البرنامج مبنيًا لـ x86. + %1$s عبر FEX + هذا البرنامج مبني لأجهزة كمبيوتر %1$s ويعمل عبر FEX، الذي لم يتم إعداده بعد. افتح Steam مرة واحدة أثناء تسجيل الدخول: سيجعله DroidDeck يُنزّل FEX وSteam Linux Runtime اللازمة، ثم يُفتح هذا البرنامج. + هذا البرنامج + %1$s مبني لأجهزة كمبيوتر x86 ويعمل عبر FEX، الذي لم يتم تثبيته بعد. شغّل Steam مرة واحدة أثناء تسجيل الدخول ودعه يُنزّل FEX وSteam Linux Runtime، ثم شغّل هذا البرنامج مجددًا. + معرّف تطبيق على Flathub أو صفحته على Flathub. يُعَدّ Flatpak أولًا إن لم يكن مُعدًّا بعد. + الملف + لم يتم اختيار ملف + اختيار ملف + تغيير + اختر نصًا برمجيًا + اختر ملف AppImage + اختر رمزًا + المستودع + owner/repo أو رابط github.com + ليس مستودع GitHub + معرّف التطبيق + org.ppsspp.PPSSPP + ليس معرّف تطبيق على Flathub + أو ملف ‎.flatpak لتطبيق غير متوفر على Flathub. + اختر ملف ‎.flatpak + المصدر + على سطح المكتب + تطبيق جديد + الاسم + من التطبيق + الرمز + الرمز الافتراضي + اختيار رمز + مسح + من المستودع + جارٍ البحث عن رموز… + الافتراضي + تعديل %1$s + حفظ + جارٍ فحص المجلد… + مجلده مرتبط بالجلسة: %1$s + يحتوي مجلده على برامج Linux لا يمكن تشغيلها من مساحة التخزين المشتركة، لذا يُنسخ إلى DroidDeck (%1$s). + إضافة + إلغاء + ثبّت بيئة تشغيل Linux من «الإعداد» أولًا. + انتظر حتى ينتهي %1$s. + متوافق + الإصدارات التي تحمل العلامة «%1$s» في قائمة «التوافق» في Steam تتضمن إصلاحات البادئة من DroidDeck وتشغّل الألعاب هنا. الإصدار الافتراضي هو Proton Experimental ARM64 (%1$s)، أو Proton 11.0 ARM64 (%1$s) بعد تثبيت هذا الإصدار الأحدث. لا يمكن لإدخالات Proton الخاصة بـ Valve التي لا تحمل العلامة تشغيل أي لعبة على هذا الجهاز. + مثبّت %1$s · %2$s + إلغاء + تلقائي + أبدًا + دائمًا + إلغاء التثبيت + تحقق + تحديث + جارٍ التحقق… + جارٍ التنفيذ… + نسيان + إضافة… + استخدام + يُطبَّق في الجلسة التالية. + جلسة Steam + جلسة سطح المكتب + الشاشة + دقة الشاشة + حتى 720p + حتى 900p + حتى 1080p + الأصلية + %1$s (افتراضي) + مخصّصة · %1$d×%2$d + مخصّصة… + قد تجعل دقة 720p القوائم أسرع استجابة. + نسبة أبعاد الشاشة + تحددها الدقة المخصّصة. + يستخدم الخيار «تلقائي» نسبة 16:9 على الأقل. + حد الإطارات + يحدّ الجلسة بأكملها. تنتقل الشاشة إلى معدل تحديث يكون من مضاعفات الحد. + HDR + إخراج HDR10 + غير متاح: %1$s. + أعد تشغيل التطبيق لتطبيق التغيير. + برامج التشغيل + برامج تشغيل GPU + مشتركة بين Steam وسطح المكتب. يتم ضبطها في صفحة «المكوّنات». + اللمس وعناصر التحكم + اللمس + لوحة اللمس + مباشر + تلقائي: لوحة لمس على سطح المكتب، ولمس مباشر في Steam. + عناصر التحكم على الشاشة + Steam + QAM + يعرض الخيار «تلقائي» عناصر التحكم عند عدم توصيل وحدة تحكم. + لمس Steam (تجريبي) + تبدأ عناصر التحكم باللمس من Steam مع جلسة Steam التالية + تخطيط لمس Steam + انقل عناصر التحكم باللمس لهذه اللعبة أو غيّر حجمها أو أخفها؛ ويتم الحفظ في Steam + وحدة التحكم + كيف يرى Steam وحدة التحكم لديك. يُطبَّق في الجلسة التالية. + وحدة تحكم Steam Deck + Steam Controller + DualShock 4 + DualSense + وحدة تحكم Xbox 360 + Deck: زر «الوصول السريع» والجيروسكوب. Steam Controller: الشيء نفسه، مع استشعار القبضة على الأزرار الخلفية. DualShock 4 وDualSense: الجيروسكوب، ولوحتا التتبع كلوحة اللمس الخاصة بها. Xbox 360: افتح «الوصول السريع» باستخدام Guide+A. + زر الرجوع + القائمة ← الوصول السريع + الوصول السريع ← القائمة + الجلسة + السلوك في الخلفية + عند إخفاء التطبيق أو انطفاء الشاشة. + يدوي + أصلي + «تلقائي» يوقف مؤقتًا ويستأنف عند الظهور. «أصلي» يجهّز Steam للسكون أولًا. «يدوي» ينتظر الضغط على «استئناف». «أبدًا» يُبقيه قيد التشغيل. + مواصلة التنزيلات + تأخير التعليق في الخلفية أثناء قيام Steam بالتنزيل. + «تلقائي» يوقف مؤقتًا في الخلفية ويستأنف عند الظهور. «يدوي» ينتظر الضغط على «استئناف». «أبدًا» يُبقيه قيد التشغيل. + بدء التشغيل + تشغيل Steam مع DroidDeck + يفتح جلسة Steam عند تشغيل DroidDeck. + Decky + جارٍ البحث عن إصدارات… + جاهز لتثبيت %1$s. + لم يتم العثور على إصدار متوافق. حاول مرة أخرى لاحقًا. + مثبّت · %1$s + يتوفر تحديث · %1$s + محدَّث · %1$s + المُحمِّل + تثبيت أحدث إصدار + يبدأ مع Steam. أثناء تفعيله، قد تتمكن التطبيقات الأخرى على هذا الجهاز من التحكم في Steam. + متوقف. يبقى منفذ تصحيح الأخطاء عن بُعد في Steam مغلقًا. + هل تريد إلغاء تثبيت Decky Loader؟ + يزيل المُحمِّل. يتم الاحتفاظ بالمكونات الإضافية والإعدادات. + العميل + تراكب الأداء + أوقفه إذا تعطلت الألعاب أو أصبحت شاشتها سوداء في وضع Deck. يُطبَّق في الجلسة التالية. + إصلاح عميل Steam + يُنزّل عميل Steam مجددًا في المرة التالية التي يبدأ فيها Steam. استخدمه إذا بقي Steam على شاشة التحميل. يتم الاحتفاظ بالألعاب وتسجيل الدخول والإعدادات. + سيتم تنزيل عميل Steam مجددًا في المرة التالية التي يبدأ فيها Steam. + إصلاح + الصور الفنية + مفتاح API لـ SteamGridDB + مفتاحك + تعديل… + يملأ الصور الناقصة من Steam، ثم من SteamGridDB. تكون الأولوية لملفات cover.jpg أو hero.jpg أو logo.png أو icon.png الخاصة بك في مجلد اللعبة. + إزالة %1$s + إزالة؟ + إضافة لعبة + إضافة لعبة: + اختر ملف exe: + هذا الموقع غير متاح في الجلسة + فحص ملف exe هذا + مكوّنات Windows + يتنقل LB / RB بين الأقسام. + لا شيء + ملفات DLL لنظام Windows لا يتضمنها Proton، من قائمة المكوّنات. فعّل ما تحتاجه هذه اللعبة منها: توضع في مجلد Windows الخاص باللعبة، وليس في ملفاتها أبدًا، وتبقى مهما كان إصدار Proton الذي تعمل عليه. + تعذّر الوصول إلى قائمة المكوّنات. لا يزال بالإمكان تبديل المكوّنات التي تم تنزيلها بالفعل. + موصى بها لهذه اللعبة + جارٍ التحقق مما تحتاجه هذه اللعبة… + متاح عند الاتصال بالإنترنت + + جارٍ تفعيل %1$d مكوّن… + جارٍ تفعيل مكوّن واحد… + جارٍ تفعيل مكوّنين… + جارٍ تفعيل %1$d مكوّنات… + جارٍ تفعيل %1$d مكوّنًا… + جارٍ تفعيل %1$d مكوّن… + + مفعّلة لهذه اللعبة + يثبّت Steam %1$s مع %2$s (التطبيق %3$s) + من قائمة GOG لهذه اللعبة + من متطلبات Epic المسبقة لهذه اللعبة + من خطوات التثبيت في Amazon لهذه اللعبة + يثبّت Steam %1$s مع التطبيق %2$s، بالمطابقة حسب الاسم + مُدرجة أيضًا + مُدرج من %1$s؛ ليس مكوّنًا هنا + Steam + GOG + Epic Games + Amazon Games + تأتي اللعبة مع %1$s + تحتوي اللعبة على %1$s (غالبًا يعمل كما هو) + موجود بالفعل في هذه اللعبة + لا شيء في مجلد اللعبة يشير إلى مكوّن. + مثبّتة لهذه اللعبة (%1$d) + جميع المكوّنات (%1$d) + تتطلب مثبّت Windows (%1$d) + هذه برامج إعداد مستقلة لا تحتوي على حزمة يمكن تفكيكها، ولا يوجد بعد تسجيل لما تثبّته. معظمها بيئات تشغيل قديمة يحل محلها بالفعل Mono المضمّن في Proton، وتُضاف التسجيلات عند إنشائها. + إظهار + إخفاء + يشغّل برنامج الإعداد الخاص به (لا يوجد تسجيل بعد) + غير مدعوم هنا بعد + جارٍ تحميل المكوّنات… + تعذّر التثبيت: %1$s + تم التنزيل + جارٍ تنزيل %1$s + جارٍ التثبيت: %1$s + جارٍ التعطيل… + تنزيل بحجم %1$s + يسري مفعوله في المرة التالية التي تبدأ فيها اللعبة. + تمديد ألعاب 16:9 + على الشاشات الأقرب إلى الشكل المربع من 16:9، يمدّ الألعاب لملء الشاشة بدلًا من عرض أشرطة. يُطبَّق في الجلسة التالية. + الصوت + DirectAudio للألعاب + يتجاوز PulseAudio لتقليل زمن الاستجابة. + الميكروفون + للدردشة الصوتية. + الشبكة + اكتشاف Wi-Fi + عرض أسماء شبكات Wi-Fi والشبكات القريبة في Steam. + خدمات الموقع متوقفة. + لم يتم منح إذن الموقع. + فتح الإعدادات + متابعة + هل تريد تفعيل اكتشاف Wi-Fi؟ + يتطلب Android إذن الموقع لقراءة أسماء شبكات Wi-Fi واكتشاف الشبكات القريبة. يستخدم DroidDeck هذا الإذن لعرض تلك الشبكات في Steam. نحن لا نتتبع موقعك. يعمل الوصول إلى الإنترنت بدون هذا الإذن. + تخزين الألعاب + تلقائي - بطاقة SD عند توفرها، بالإضافة إلى مجلدات Games لديك + تلقائي - مجلدات Games لديك، بالإضافة إلى بطاقة SD عند توفرها (غير موجودة حاليًا) + مساحة التخزين الداخلية فقط + المجلد: %1$s + مكتبة ثانية + قد تتقطع الألعاب التي تحمّل ملفاتها باستمرار من بطاقة SD أو من مساحة التخزين المشتركة. + تشخيص التخزين + يسجّل نتائج تخصيص المساحة وعينات من توقيتات التخزين في «مشاركة السجلات». قد يبطئ عمليات التثبيت أثناء تفعيله. يُطبَّق في الجلسة التالية. + اختيار مجلد… + دقة مخصّصة + تحل محل الحد والنسبة. يُعرض الحجم غير المطابق للشاشة مع أشرطة. + العرض + الارتفاع + بين ‎320×240‎ و‎3840×2160‎. + + %1$d لعبة + لعبة واحدة + لعبتان + %1$d ألعاب + %1$d لعبة + %1$d لعبة + + حسنًا + حذف + استعادة + تنزيل + حذف %1$s + %1$.1f MB + برنامج تشغيل بيئة التشغيل + يستخدمه Steam والألعاب وسطح المكتب. يُطبَّق في الجلسة التالية. + استيراد ملف zip لـ Turnip… + برنامج تشغيل العرض + يرسم الإطارات على الشاشة في كلا الوضعين. أعد تشغيل التطبيق لتطبيق التغيير. + استيراد ملف zip لـ AdrenoTools… + لم يتم بعد + آخر فحص للإصدارات الليلية: %1$s + المكوّنات + حول المكوّنات + البحث عن برامج تشغيل جديدة + البحث عن حزم جديدة في الإصدارات الليلية + Proton + يشغّل Proton المختار هنا كل لعبة لم تختر إصدارها الخاص في Steam، ويظهر هنا أيضًا أي تغيير يتم في Steam. يستبدل Steam إصدار Proton من Valve عند تحديثه، ويُحتفظ بالملفات الأصلية لكل إصدار. + التهيئة + تهيئة ملف التعريف + ملف التعريف + ملف التعريف الافتراضي + تستخدمه تلقائيًا الألعاب التي ليست لها تجاوزات. + إعدادات محفوظة تُستخدم تلقائيًا عند تشغيل %1$s. + اختر فقط الإعدادات التي تحتاجها هذه اللعبة. يواصل كل ما عداها استخدام ملف التعريف الافتراضي. + إعداد FEX المسبق + يتم استخدام الإعداد المسبق لملف التعريف الافتراضي. + تم حفظ إعداد مسبق مخصّص لهذه اللعبة. + متغيرات FEX المخصّصة + استخدام الافتراضي + توافق اللعبة + خيارات البيئة لـ Proton وDXVK وVKD3D والمزامنة لهذه اللعبة. + يتم استخدام Proton من ملف التعريف الافتراضي. + تم حفظ Proton مخصّص لهذه اللعبة عبر تعيين التوافق في Steam. + بدون Proton + DXVK + VKD3D-Proton + يستخدم حزمة ملف التعريف الافتراضي من %1$s ما لم تحدد هذه اللعبة حزمة أخرى. + يستخدم حزمة المكوّنات من ملف التعريف الافتراضي. + يستخدم %1$s لهذه اللعبة فقط. + خيارات بيئة التشغيل + خيارات بيئة أخرى لكل لعبة لـ Proton وDXVK وVKD3D-Proton والمزامنة. + تم حفظ ملف تعريف %1$s للتشغيل التالي للعبة. + بدء اللعبة بدون Steam + لعبة تُغلق فورًا: شغّلها بدون إعداد Steam الخاص بـ Proton. تكون ميزات Steam، مثل الطبقة العلوية، معطّلة لها. + تشغيل + تم حفظ اختيار Proton لـ %1$s وهو متاح في Steam الآن. + تم حفظ اختيار Proton لـ %1$s. أعد تشغيل Steam قبل تشغيل اللعبة. + لا يمكن لـ %1$s استخدام Proton المحدد على هذا الجهاز. + تعذّر تحميل ملف تعريف %1$s: %2$s + تعذّر حفظ ملف تعريف اللعبة: %1$s + إعادة تعيين ملف تعريف اللعبة + هل تريد إعادة تعيين ملف تعريف %1$s؟ + هل تريد إزالة تجاوزات Proton وFEX وحزمة المكوّنات والبيئة وتصفية الأنسجة ومكوّنات Windows لهذه اللعبة؟ ستستخدم ملف التعريف الافتراضي. + يستخدم %1$s الآن ملف التعريف الافتراضي. + تعذّرت إعادة تعيين ملف تعريف اللعبة بالكامل: %1$s + يشغّل %1$s الآن كل لعبة لم تختر إصدار Proton الخاص بها في Steam. + سيشغّل %1$s كل لعبة لم تختر إصدار Proton الخاص بها، بدءًا من المرة التالية التي يبدأ فيها Steam. + لا يمكن لـ %1$s تشغيل الألعاب هنا، لذا لا يمكن أن يكون الافتراضي. + تعذّر حفظ Proton الافتراضي: %1$s + %1$s · تم تبديل %2$d + %1$s · الكل أصلي + %1$s · لعبة قيد التشغيل + %1$s · حزمة droiddeck-esync + %1$s · لا توجد حزمة droiddeck-esync بعد + %1$s · esync مضمّن، لا حاجة إلى حزمة + برامج تشغيل GPU + %1$s · أُعيد تطبيقه عند التشغيل %2$s + التالي: %1$s · بعد إغلاق اللعبة + %1$s قيد الاستخدام: %2$s + ينتظر حتى تُغلق اللعبة + يُطبَّق في المرة التالية التي تبدأ فيها لعبة + استيراد ملف ‎.zip + جارٍ قراءة إصدارات Proton… + لم يتم تثبيت أي إصدار من Proton بعد. شغّل Steam مرة واحدة لتنزيل Proton لـ ARM64، أو أضف GE-Proton أو CachyOS أو DroidDeck-Proton من «الإعداد». + الأصلي · %1$s + %1$s · هذا الإصدار + %1$s · إصدار سابق + مثبّتة · %1$d + لا يوجد شيء مخزّن لـ %1$s بعد. تُحفظ ملفات Proton هذا الخاصة به كنسخة أصلية. + هناك لعبة تعمل على Proton هذا، لذا سينتظر هذا حتى تُغلق. + يُطبَّق في المرة التالية التي تبدأ فيها لعبة. + هل تريد تبديل %1$s؟ + هل تريد وضع %1$s في %2$s؟ تبقى ملفاته الأصلية محفوظة. %3$s + هل تريد استعادة %1$s؟ + هل تريد إعادة النسخة الأصلية من %1$s إلى %2$s؟ %3$s + يزيل الحزمة من التطبيق. يمكنك تنزيلها مجددًا. + هل تريد حذف هذه النسخة الأصلية؟ + هذه النسخة الأصلية من %1$s تعود إلى إصدار سابق من %2$s. يتم دائمًا الاحتفاظ بالنسخة الأصلية للإصدار الحالي. + في الإصدارات الليلية · %1$d + حدّث لعرض الحزم في الإصدارات الليلية. + لا جديد منذ آخر فحص. + استيراد ملف ‎.wcp + استعادة النسخة الأصلية لـ Proton هذا + هل تريد إعادة %2$s الأصلي الخاص بـ %1$s؟ + تبديل + FEX وDXVK وVKD3D-Proton لكل إصدار من Proton يشغّل به Steam الألعاب.\n\nاختر صفًا مثبّتًا لتبديله. يُطبَّق في المرة التالية التي تبدأ فيها لعبة، أو بعد إغلاق اللعبة قيد التشغيل.\n\nيُعاد التحقق من اختياراتك قبل كل تشغيل وتُستعاد إذا غيّرها أي شيء. يُحتفظ بالملفات الأصلية لكل إصدار من Proton، لذا لا تضيع أبدًا مع تحديثات Steam. + أصلي + قيد الاستخدام + جديد + مخزّن + الأداء + أنوية CPU وإصلاحات الجلسة واسم المضيف. يُطبَّق في الجلسة التالية. + أنوية عميل Steam + تجاوز اختيار Steam للأنوية + يثبّت Steam ومساعد واجهته وgamescope على الأنوية أدناه. + أنوية العميل + الأنوية المخصّصة لـ Steam. + فعّل التجاوز لتتمكن من الاختيار. + أنوية الألعاب + أنوية الألعاب + الأنوية المخصّصة للألعاب. عند تحديدها جميعًا، يوزّع Android المهام بحرية. + واجهة العميل + GL متعدد مؤشرات الترابط + قد يجعل قوائم Steam أسرع استجابة. + Zink: الواصفات المؤجلة + موصى به لبرامج التشغيل التي لا تدعم مخازن الواصفات. + تخطي فحوص أخطاء GL + يوقف التحقق من GL عند كل استدعاء. + gamescope: طابور GPU بأولوية الوقت الفعلي + يعطي الأولوية لعمل GPU الخاص بالمُركِّب. قد يحسّن انتظام الإطارات على حساب أداء الألعاب قليلًا. + GPU + تثبيت GPU على أعلى تردد له + لـ Adreno فقط. تقطّع أقل، مع استهلاك أكبر للبطارية والحرارة. + إصلاحات الجلسة + Turnip: التصيير عبر sysmem + أسرع لـ Steam ولمعظم الألعاب. مطلوب على Adreno 710/720/722. + تجاوز مساعد xalia في Steam + droiddeck-ntsync (تجريبي) + بديل في مساحة المستخدم لبرنامج تشغيل ntsync في Linux، للاختبار. أثناء تفعيله، يستخدمه Proton بدلًا من droiddeck-esync وdroiddeck-fsync. يعمل مع Proton Experimental ARM64 وGE-Proton وproton-cachyos. يُطبَّق في الجلسة التالية. + droiddeck-esync + droiddeck-fsync أولًا + يشغّل خاصية fsync المدمجة في Proton على ملفات Proton الأصلية، مع توفير futex_waitv في مساحة المستخدم، فلا يحتاج إلى حزمة ويستمر في العمل لحظة تحديث Steam لـ Proton. أثناء تفعيله، تستخدمه الألعاب قبل droiddeck-esync، باستثناء القليل الذي يبقيه Proton معطّلًا لـ fsync. يُطبَّق في الجلسة التالية. + تنتظر مؤشرات ترابط Windows في اللعبة بعضها بعضًا عبر eventfd والذاكرة المشتركة، بدلًا من رحلات ذهاب وعودة عبر wineserver. يستخدم حزمة droiddeck-esync مُعدّة لإصدار Proton ذاك بالضبط؛ وتُجلَب الحزم تلقائيًا. وإلى أن تتوفر حزمة لإصدار Proton جديد، يحل droiddeck-fsync محله. يُطبَّق في الجلسة التالية. + يعطّل مساعد التنقّل بوحدة التحكم في Proton، الذي يستهلك CPU في كل لعبة. + تشغيل proot بلا seccomp + قد يُصلح أخطاء استدعاءات النظام (syscall) الناقصة على بعض الأنوية. قد يقلّل الأداء. + مسار proot السريع + يحتاج إلى مرشّح seccomp في proot. + عمليات بحث أسرع عن الملفات وتقطّع أقل. أوقفه إذا تعذّر على برنامج العثور على ملفاته. + هوية الجلسة + اسم المضيف + الاسم الذي يراه Steam لهذا الجهاز. اتركه خاليًا للاستعادة إلى %1$s. + أحرف وأرقام وشرطات داخلية فقط، حتى 63 حرفًا. + Android مهيَّأ لإنهاء هذه الجلسة + مثبّت + متاح للتنزيل + لا جديد منذ آخر تحقّق. + استعادة برامج التشغيل المدمجة + مدمج في التطبيق: هذا يزيل ملفاته غير المضغوطة ويخفيه. «استعادة برامج التشغيل المدمجة» يعيده. + نصف حزمة Android + Linux: تُزال كلا النصفين. يرجع برنامج التشغيل قيد الاستخدام إلى الافتراضي. + تُزال ملفاته من التطبيق. + تُزال ملفاته. إنه قيد الاستخدام، فيتولى الافتراضي مكانه. + العرض + المؤثرات + عناصر التحكم + المكوّنات + وحدة التحكم + الإعدادات + صفحة %1$s + Steam + سطح المكتب + قائمة Steam + الوصول السريع + فتح قائمة الوصول السريع + الصفحة السابقة + الصفحة التالية + واجهة الأداء + وضع التحجيم + حدة التحجيم + مؤثرات الشاشة + فوري. يستهلك وقت GPU قليلًا أثناء تفعيل أي مؤثر. + الإعدادات المسبقة + مخصّص + متقدّم + موسّع + مصغّر + الحدة (CAS) + مستوى الحدة + HDR مزيّف + إزالة التدرّج اللوني + قوة إزالة التدرّج اللوني + السطوع + التباين + غاما + التشبّع + FXAA (تنعيم الحواف) + الحدود الكرتونية + CRT + NTSC + تصفية الملمس + ألعاب DirectX 9–11، من تشغيلها التالي. + التصفية اللامتجانسة + حدة الملمس + انحياز mip سالب: ملمس أكثر حدة مع بعض التلألؤ. يطابق الخيار «تلقائي» مقدار تكبير الجلسة إلى الشاشة. + تلقائي (%1$s) + لوحة المفاتيح + لوحة مفاتيح الكمبيوتر + Android + الشاشة الثانية + الوضع + يعرض + تطبيقات Android + تشغيل تطبيق + إخفاء + إظهار + لا تطبيقات قابلة للتشغيل + سلوك الجلسة + يُطبَّق الآن وعلى الجلسات القادمة في هذا الوضع. + الجلسة التالية + الدعم + سجلات الجلسة + أرفقها بتقرير خلل + مشاركة السجلات + الخلفية + إيقاف + إيقاف الجلسة؟ + قيد التشغيل الآن + لا توجد لعبة قيد التشغيل على Proton + ملف تعريف اللعبة + تفاصيل ملف التعريف غير متاحة بعد. + تعذّر تحميل ملفات تعريف الألعاب. تتم إعادة المحاولة تلقائيًا. + تعذّر تحديث ملفات تعريف الألعاب. تُعرَض آخر بيانات متاحة؛ وتتم إعادة المحاولة تلقائيًا. + عرض وتعديل الإعدادات المحفوظة المستخدَمة في التشغيل التالي. + استخدام ملف التعريف الافتراضي + ملف تعريف مخصّص + استخدام %1$s من ملف التعريف الافتراضي. + تُحفَظ التغييرات في ملف تعريف %1$s وتُطبَّق عند تشغيل اللعبة التالي. + + لا تجاوزات للعبة + تجاوز واحد للعبة + تجاوزان للعبة + %1$d تجاوزات للعبة + %1$d تجاوزًا للعبة + %1$d تجاوز للعبة + + لم يتم تثبيت أي Proton بعد. + لعبة قيد التشغيل + أُعيد تطبيقه عند التشغيل %1$s + الأصلي + الاستيراد والحذف من صفحة «المكوّنات». + تم + يحتاج إلى انتباه + قيد التنفيذ + تحقّق من عنوان IP والمنفذ في تصحيح الأخطاء اللاسلكي، أو أعد الإقران إذا أزال Android هذا الجهاز. + تثبيت + إدارة + نظرة عامة + وحدة التحكم + الجلسة + المشغّل + الإعداد + ● الكل جاهز + %1$d من %2$d جاهز + فحص النظام + الجهاز مدعوم + GPU غير مُختبَر + GPU غير مدعوم + %1$s · %2$s + %1$s: %2$s + يحتاج Steam إلى برنامج تشغيل Adreno؛ قد يعرض %1$s شاشة سوداء + بيئة تشغيل Linux + %1$s · %2$d%% + غير مثبّت · نحو 3 GB، يُثبَّت عند أول تشغيل + مثبّت + %1$s · يتوفر تحديث + %1$s · محدَّث + حد العمليات الفرعية + مفعَّل · قد يُغلق Android تطبيق Steam. إيقافه يستغرق دقيقة. + غير مُحدَّد · قد يُغلق Android تطبيق Steam. إيقافه يستغرق دقيقة. + تعذّر التحقّق منه · إيقافه يستغرق دقيقة. + إصلاح + التفاصيل + خيارات المطوّرين + استخدام تصحيح الأخطاء اللاسلكي + التحقّق مرة أخرى + تفعيل الحد + حساب Steam + تم تسجيل الدخول باسم %1$s · وضع عدم الاتصال + تم تسجيل الدخول باسم %1$s + اضغط على «تشغيل» وسجّل الدخول إلى Steam + الأدوات + وحدة التحكم + إعدادات وحدة التحكم غير متاحة. + الجلسة + سجلات الجلسة + تُحفَظ بعد كل جلسة + أدوات تصحيح الأخطاء + أحدث سجلات الجلسة + سجلات الجلسة المحفوظة + يحتفظ بأحدث %1$d + مسح السجلات + وضع عدم الاتصال + سجّل الدخول إلى Steam أولًا + المشغّل + اللغة + يستخدمها Steam أيضًا، من المرة التالية التي يبدأ فيها. + الافتراضي للنظام (%1$s) + الافتراضي للنظام + تحجيم التطبيق + نصوص DroidDeck وعناصر التحكم فيه + %1$d%% (افتراضي) + المظهر + مظهر المشغّل + استخدام كشاشة رئيسية + يمكن أن يكون DroidDeck تطبيق الشاشة الرئيسية + لا يُعرَض DroidDeck أبدًا كتطبيق للشاشة الرئيسية + الاتجاه + أفقي تلقائي + أفقي + أفقي معكوس + ملء الشاشة + يخفي شريطي الحالة والتنقّل في Android + يعرض شريطي الحالة والتنقّل في Android + تطبيق الشاشة الرئيسية الافتراضي + اختيار تطبيق للشاشة الرئيسية + اختيار + تطبيقات Linux (تجريبي) + الملفات + تصفّح الملفات وإدارتها + إصدارات Proton + تثبيت إصدارات Proton لـ ARM64 + الأداء + تخصيص أنوية CPU + مجلد ROMs + حيث تُخزَّن ألعاب المحاكيات + المكوّنات + FEX، DXVK، VKD3D + عناصر التحكم + تعيين الأزرار + استكشاف + البحث + المثبّتة + %1$s · %2$d + المتجر + Flathub · ARM64 + ● Flatpak جاهز + ثبّت بيئة تشغيل Linux من «الإعداد» أولًا. + إعداد Flatpak + يضيف Flatpak ويصله بـ Flathub (نحو 20 MB). يُنزِّل أول تطبيق بيئة تشغيله أيضًا. + جارٍ الإعداد… + Flatpak + التحديثات + %1$s · %2$s + %1$s · %2$s · %3$d%% + جارٍ البدء… + الفئات + تعذّر الوصول إلى Flathub. تحقّق من اتصالك وحاول مرة أخرى. + إعادة المحاولة + البحث في Flathub + Firefox، ألعاب، محاكيات… + بحث + الكل + جارٍ البحث… + ابحث بالاسم أو اختر فئة. تُعرَض تطبيقات ARM64 فقط. + لا توجد تطبيقات ARM64 على Flathub تطابق ذلك. + النتائج + التحقّق من التحديثات + تحديث الكل (%1$d) + ● كل شيء محدَّث + لا تطبيقات حتى الآن. ابحث عنها ضمن «استكشاف» أو «البحث». + أعدّ Flatpak لتثبيت التطبيقات. + تحديث متاح · %1$s + فتح + التفاصيل + إزالة + اضغط مرة أخرى للإزالة + جارٍ التثبيت… + تثبيت + أعدّ Flatpak أولًا + تنزيل %1$s + ● مثبَّت + تعذّر تحميل صفحة هذا التطبيق على Flathub. + جارٍ التحميل من Flathub… + لا يوجد إصدار ARM64 على Flathub، لذا لا يمكن تثبيته هنا. + لقطات الشاشة + حول + معرّف التطبيق + الترخيص + بيئة التشغيل + حجم التثبيت + %1$s (مع بيئة التشغيل المشتركة) + الموقع الإلكتروني + موثّق + التحديثات + DroidDeck %1$s · %2$s + تم التحقّق %1$s + لم يتم التحقّق بعد + اسمح لـ DroidDeck بتحديث نفسه + في الشاشة التالية، فعِّل «السماح من هذا المصدر» ثم عُد. يستمر التحديث تلقائيًا. + فتح الإعدادات + ليس الآن + توقيع مختلف + لا يمكن التحديث مباشرة + هذه النسخة غير موقَّعة بمفتاح إصدار DroidDeck، فلا يمكن تثبيت التحديثات فوقها. أزل تثبيتها للتبديل. + تحديث متاح + لا يمكن التثبيت + جاهز للتبديل + متقدّم على المستقر + أنت متقدّم على الإصدار المستقر + هذا الإصدار أحدث من آخر إصدار مستقر. ستنتقل إلى الإصدار المستقر مع إصداره التالي. + انتهى الاختبار + انتهى هذا الاختبار + تم دمج إصلاحه أو التخلي عنه. تابع إصدار المعاينة للحصول على أحدث الإصلاحات. + محدَّث + لديك أحدث إصدار %1$s + · %1$s + %1$s… %2$d%% + %1$s… + تحديث%1$s + تثبيت%1$s + تثبيت الإصدار المستقر %1$s رغم ذلك + متابعة إصدار المعاينة + أوقف الجلسة قيد التشغيل للتحديث. + أقل + أكثر + DroidDeck %1$s + إصدار معاينة جديد + إصدار اختبار PR #%1$d + %1$d MB + قناة التحديث + المستقر + إصدارات مُختبَرة، تناسب معظم المستخدمين + %1$s · %2$s + المعاينة + أحدث الإصلاحات، قبل الإصدار المستقر + محدَّث + إصدارات الاختبار + جرّب إصلاحًا قبل إصداره رسميًا + لا شيء حاليًا + PR #%1$d + لم يُبنَ لهذه النسخة من DroidDeck + تم التحديث %1$s + اختبار PR #%1$d + منذ فترة + منذ لحظات + + لا شيء للتجربة + واحد للتجربة + اثنان للتجربة + %1$d للتجربة + %1$d للتجربة + %1$d للتجربة + + + التغييرات منذ إصدارك · غير متأخر + التغييرات منذ إصدارك · متأخر بإصدار واحد + التغييرات منذ إصدارك · متأخر بإصدارين + التغييرات منذ إصدارك · متأخر بـ%1$d إصدارات + التغييرات منذ إصدارك · متأخر بـ%1$d إصدارًا + التغييرات منذ إصدارك · متأخر بـ%1$d إصدار + + + آخر التغييرات · لا إصدارات جديدة + آخر التغييرات · إصدار واحد + آخر التغييرات · إصداران + آخر التغييرات · %1$d إصدارات + آخر التغييرات · %1$d إصدارًا + آخر التغييرات · %1$d إصدار + + إعداد واحد في Android يجب تغييره + Steam + يوقف Android التطبيقات التي تبدأ عمليات خلفية كثيرة، وSteam يبدأ عشرات منها. وإلى أن يتغيّر هذا، تُغلَق الألعاب دون أي خطأ. + إصلاحه لي + فعِّل «تعطيل تقييد العمليات الفرعية»، ثم عُد. اترك خيارات المطوّرين مفعّلة، وإلا سيُعاد تعطيل المفتاح. + فتح خيارات المطوّرين + لا يحتوي إصدار Android هذا على مفتاح لذلك، لذا يغيّره DroidDeck عبر تصحيح الأخطاء اللاسلكي. يستغرق نحو دقيقة، ولا يحتاج إلى جهاز كمبيوتر. + يحتاج تصحيح الأخطاء اللاسلكي إلى Wi-Fi. تعمل أي شبكة؛ فهو لا يحتاج إلى اتصال بالإنترنت. + يظهر رمز الإقران في إشعار. فعِّل الإشعارات لـ DroidDeck، أو استخدم «طرق أخرى». + السماح بالإشعارات + افتح الإعدادات ← خيارات المطوّرين ← تصحيح الأخطاء اللاسلكي وفعِّله + اضغط على «إقران جهاز برمز إقران» + أدخل الرمز في إشعار DroidDeck + في انتظار نافذة الإقران المنبثقة… + تم العثور على النافذة المنبثقة. أدخل رمزها في الإشعار. + الحالة + يتم التحقّق كل ثانيتين. تُغلَق هذه الصفحة عند إيقافه. + طرق أخرى + تصحيح الأخطاء اللاسلكي، عنوان يدوي، ADB من كمبيوتر + عنوان يدوي، ADB من كمبيوتر، خيارات المطوّرين + استخدام تصحيح الأخطاء اللاسلكي بدلًا من ذلك + تضيف بعض إصدارات ROM هذا المفتاح رغم ذلك. ابحث عن «العمليات الفرعية» في خيارات المطوّرين. + إذا لم ينجح الإشعار، أدخل عنوان النافذة المنبثقة ورمزها بنفسك. + إدخال العنوان يدويًا + من جهاز كمبيوتر يحتوي على ADB: + نسخ الأمر + اختر الشاشة التي ستُفتَح فيها إعدادات Android. + الشاشة الرئيسية + تم الإقران. جارٍ البحث عن منفذ تصحيح الأخطاء اللاسلكي… + تم الإقران. أدخل المنفذ من الإعدادات. + تم العثور على الجهاز. جارٍ تطبيق الإعداد… + تعذّر التطبيق تلقائيًا. تحقّق من العنوان أدناه وأعد المحاولة. %1$s + تصحيح الأخطاء اللاسلكي + تم تحديث حد العمليات الفرعية + تغيير حد العمليات الفرعية + إذا كان هذا الخيار غير متاح، فأقرن عبر تصحيح الأخطاء اللاسلكي. + تم الإقران. تحقّق من عنوان IP والمنفذ في تصحيح الأخطاء اللاسلكي. + تم تحديث حد العمليات الفرعية. + استخدم الخطوة 2 إذا لم يكن في خيارات المطوّرين إعداد للعمليات الفرعية. افتح خيارات المطوّرين ← تصحيح الأخطاء اللاسلكي ← إقران جهاز برمز إقران. + تم الإقران. استخدم العنوان أدناه، أو حدِّثه من تصحيح الأخطاء اللاسلكي ← عنوان IP والمنفذ. + تم تحديث حد العمليات الفرعية عبر تصحيح الأخطاء اللاسلكي. + أكّد Android أن الحد مفعّل. + أكّد Android أن الحد معطّل. + 2 · إقران تصحيح الأخطاء اللاسلكي + عنوان IP ومنفذ النافذة المنبثقة للإقران + رمز الإقران (PIN) + 6 أرقام + إقران + 2 · التطبيق عبر تصحيح الأخطاء اللاسلكي + عنوان IP ومنفذ الاتصال + إقران مرة أخرى + إيقاف الحد + طرق أخرى لتغييره + 1 · خيارات المطوّرين (جرّب هذا أولًا) + فعِّل «تقييد العمليات الفرعية». + عطِّل «تقييد العمليات الفرعية». + 3 · ADB من كمبيوتر (كخيار أخير) + فقط إذا كان تصحيح الأخطاء اللاسلكي غير متاح. + 3 · ADB من كمبيوتر (كخيار أخير) + متى تظهر عناصر التحكم باللمس في الجلسة + اللون + درجة لون الأزرار على الشاشة + مخصّص + العتامة + %1$d%% + حجم الأزرار + 100% هو الحجم القياسي + ضغط العصا + انقر مرتين مع الاستمرار على عصا للحصول على L3 أو R3 + عصي تكيّفية + اهتزاز اللعبة + زر Steam + زر QAM + زر لوحة المفاتيح + اضغط على Select لفتح قائمة Steam + بدلًا من View/Select. + الرسوم المتحركة + تظهر العصي حيث تلمس وتُخفى عند الرفع + التخطيط + تم حفظ مواضع مخصّصة + مهيّأ لهذه الشاشة + تعديل + إعادة ضبط + تعيين الأزرار + يرسل كل زر إدخاله الخاص + تمت إعادة تعيين %1$d من %2$d أزرار + ضبط + إعادة ضبط وحدة التحكم + يستعيد كل إعدادات وحدة التحكم والتخطيط + ما يرسله كل زر على الشاشة إلى اللعبة. يؤدي إخفاؤه إلى إزالة الزر. + أزرار الشاشة + الافتراضية + إعادة ضبط التعيين + تطبيقات Android + %1$d تطبيق + لم يتم العثور على تطبيقات Android قابلة للتشغيل. + ● سطح المكتب مثبّت + يُثبَّت عند أول فتح + متاح للتثبيت + غير مثبّت. + %1$s · غير مثبّت + فتح %1$s + الألعاب + اختر مجلد ROMs. + تصفّح إلى /root/ROMs في RetroArch. + أضف ألعاب %1$s إلى ROMs/%2$s. + تثبيت %1$s + أوقف الجلسة للتثبيت + بيئة التشغيل مطلوبة + جارٍ تحميل تفاصيل التثبيت… + تفاصيل التثبيت غير متاحة. حاول مرة أخرى لاحقًا. + ثبّت بيئة تشغيل Linux من «الإعداد» أولًا. + أوقف الجلسة قيد التشغيل لتثبيت تطبيقات سطح المكتب. + يثبّت %1$s في سطح مكتب Linux. + %1$s · %2$d%% + يؤدي إخفاؤه إلى إزالته من سطح المكتب. تبقى ملفاته في بيئة تشغيل Linux. + لم تعد هذه اللعبة موجودة في مجلد ROMs. + سطح المكتب · %1$s + تشغيل في %1$s + أيضًا في %1$s + لم تعد هذه الصفحة موجودة. اختر قسمًا من القائمة على اليسار. + فتح سطح المكتب + تثبيت وفتح سطح المكتب + إلغاء تثبيت سطح المكتب + إزالة سطح المكتب والتطبيقات المرفقة به. الاحتفاظ بالألعاب والتطبيقات المُضافة والإعدادات والمكتبات المشتركة. + جارٍ إزالة سطح المكتب + تمت إزالة سطح المكتب + جارٍ إزالة ملفات LXQt القديمة + حدِّث DroidDeck قبل إزالة إصدار سطح المكتب هذا. + سطح مكتب Linux + KDE Plasma وFirefox ومحاكياتك + الألعاب التي تثبّتها في Steam تظهر هنا بعد الجلسة. + تشغيل Steam + إعدادات التشغيل + تشغيل + ملفات اللعبة + بادئة Proton + جارٍ البحث عن الحفظ… + لم يتم العثور على مجلد حفظ بالاسم. يأخذ التصدير مجلد المستخدم بالكامل. + %1$d مجلدات حفظ + %1$s · %2$d ملفات · %3$s + %1$d KB + إدارة الحفظ + حفظ اللعبة + استيراد الحفظ… + تصدير بصيغة GameHub المضغوطة… + تصدير بصيغة Winlator المضغوطة… + فتح مجلد الحفظ + لم يتم تشغيلها أبدًا + بيئة التشغيل مشغولة · %1$d%% + بيئة التشغيل مشغولة + تُثبَّت بيئة التشغيل عند أول تشغيل + يتوفر تحديث لبيئة التشغيل + ● بيئة التشغيل جاهزة + آخر تشغيل %1$s + التخزين الداخلي + لعبة مُضافة + لعبة مخصّصة + تخزين الألعاب + أفضل زوج لهذا GPU، يُحافَظ على تحديثه + اختر زوجًا بنفسك + جارٍ البحث عن برامج التشغيل… + لا توجد برامج تشغيل مُدرَجة حتى الآن. حدِّث للبحث عنها. + إخفاء برامج التشغيل لوحدات GPU الأخرى + إظهار برامج التشغيل لوحدات GPU الأخرى + متقدّم + يُضبَط مع برنامج تشغيل العرض معًا: يحمل %1$s كلاهما. + يُضبَط مع برنامج تشغيل بيئة التشغيل معًا: يحمل %1$s كلاهما. + يُستخدَم في الألعاب وسطح المكتب. + الافتراضي لبيئة التشغيل + يرسم الإطارات على الشاشة. يُطبَّق بعد إعادة تشغيل DroidDeck. + تلقائي - يُحدَّد بحسب GPU + تنزيل أو استيراد أو حذف… + قيد الاستخدام + برامج التشغيل المدمجة في DroidDeck + يتم ضبط برنامجَي تشغيل بيئة التشغيل والعرض معًا كزوج متطابق. + المُستحسَن لهذا GPU: ‏%1$s. + حدِّث للبحث عن برامج تشغيل لهذا GPU. + مُستحسَن + غير متاح + بيئة تشغيل Linux + جارٍ تجهيز بيئة تشغيل Linux + الجلسة + بدء تشغيل العرض والصوت + عميل Steam + التحقّق من Steam وأدوات التوافق الخاصة به + جارٍ فتح Steam + في انتظار أول إطار من Steam + سطح المكتب + جارٍ تجهيز سطح المكتب + جارٍ الفتح + في انتظار الإطار الأول + جارٍ تشغيل Steam + انتهت الجلسة + الخطوة %1$d من %2$d + تم إيقاف Steam مؤقتًا + المقابض + اللوحة اليسرى + اللوحة اليمنى + كلتاهما + يد التحكّم + عصا التحكّم + لوحة اللمس + التبديل بين عصا التحكّم ولوحة اللمس + المزيد من عناصر تحكّم Deck + اللوحات + إغلاق عناصر التحكّم على الشاشة الثانية + فتح قائمة Steam + يسار · علوي + يسار · سفلي + يمين · علوي + يمين · سفلي + نقر + لوحة اللمس اليمنى + لوحة اللمس اليسرى + لوحة اللمس + إظهار لوحة المفاتيح + إخفاء لوحة المفاتيح + الوحدة الطرفية لـ Linux + جلسة Linux ليست جاهزة للوحدة الطرفية بعد. + Steam + الألعاب + سطح المكتب + المتجر + المكوّنات + التطبيقات + الإعداد + التحديثات + %1$d%% + استئناف %1$s + يظهر باسم /root/ROMs في كل جلسة. مساحة التخزين الداخلية متاحة في /root/Storage. + لم يتم اختيار مجلد + يُطبَّق في الجلسة التالية. بطاقات SD مدعومة. + اختيار مجلد + تغيير المجلد + ثبِّت إصدار Proton لمعالجات ARM64، ثم اختره لكل لعبة من Steam → الخصائص → التوافق. + أوقِف الجلسة الجارية لتثبيت أداة توافق أو إزالتها. + الإصدارات المتاحة + مثبَّت: %1$s + معلَّق من طلب سابق + غير مثبَّت + يتوفّر تحديث → %1$s + إزالة الإصدارات القديمة من %1$s + سيتم حذف %1$s. يجب تحويل الألعاب المضبوطة على هذه الإصدارات إلى إصدار Proton آخر. + يجب تحويل الألعاب المضبوطة على هذا الإصدار إلى إصدار Proton آخر. + التثبيت الآن + إلغاء الطلب المعلَّق + لوحة مفاتيح Android + إخفاء ✕ + تشغيل %1$s + اختر شاشة. الشاشة الثانوية: %1$s. + لم تعد الشاشة الثانوية متاحة. + الشاشة الأساسية + الشاشة الثانوية + علامة التبويب السابقة + علامة التبويب التالية + مفعَّل + الكل %1$d + لا شيء + %1$d من %2$d + غير مفعَّل + الشاشة السفلية + يُطبَّق عند تشغيل اللعبة في المرة التالية. ملف تعريف اللعبة له الأولوية على الملف المشترك. + لعبة أخرى حسب معرّف التطبيق… + تم + الافتراضي + غير مفعَّل + تغيير… + تغيير المتغيّر + العودة إلى الافتراضي + ألعاب Proton + نافذة ضمن النافذة + الدخول تلقائيًا إلى وضع نافذة ضمن النافذة + عناصر التحكّم على الشاشة غير المطوية + عند فتح الجهاز، تظهر اللعبة فوق لوحتَي اللمس والمقابض الخلفية بأسلوب Steam Deck. وعند طيّه، تُعرض اللعبة بملء الشاشة. + معلَّقة + تتوفّر ميزة نافذة ضمن النافذة عندما تصبح الجلسة جاهزة. + اسمح بميزة نافذة ضمن النافذة لتطبيق DroidDeck في إعدادات Android. + تعذّر على Android فتح نافذة ضمن النافذة. + اختصار + إضافة إلى الشاشة الرئيسية + اختيار لعبة + جارٍ البحث عن الألعاب المثبَّتة… + نسخ رابط التشغيل + تم نسخ رابط التشغيل + لا يمكن لتطبيق الشاشة الرئيسية هذا إضافة اختصارات. استخدِم «نسخ رابط التشغيل» بدلاً من ذلك. + رابط تشغيل اللعبة غير صالح. + لم تعد هذه اللعبة في مكتبة DroidDeck. + أوقِف الجلسة الحالية قبل تشغيل لعبة Steam. + تعذّر إرسال اللعبة إلى Steam. حاوِل تشغيلها مرة أخرى. + إعادة محاولة الإزالة + جارٍ الإزالة + لم تكتمل الإزالة + تصدير ملف اللعبة + مزامنة كل ملفات الألعاب مع مجلد + إيقاف مزامنة ملفات الألعاب + اختر مجلدًا لملفات الألعاب الخاصة بالواجهات الأمامية + تم حفظ ملفات الألعاب في %1$s + ملفات الواجهة الأمامية + + انتظر حتى تنتهي عملية بيئة التشغيل + انتظر حتى ينتهي تثبيت أداة التوافق + انتظر حتى ينتهي تثبيت تطبيق سطح المكتب + انتظر حتى ينتهي التثبيت قبل تشغيل لعبة + أغلِق جلسة Steam أولاً حتى لا تحفظ اللعبة فوق الملفات المستورَدة + الاستيراد إلى %1$s + + لم يتم استيراد أي ملف من %2$s إلى %3$s + تم استيراد ملف واحد من %2$s إلى %3$s + تم استيراد ملفين من %2$s إلى %3$s + تم استيراد %1$d ملفات من %2$s إلى %3$s + تم استيراد %1$d ملفًا من %2$s إلى %3$s + تم استيراد %1$d ملف من %2$s إلى %3$s + + + لم يتم استيراد أي ملف من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups + تم استيراد ملف واحد من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups + تم استيراد ملفين من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups + تم استيراد %1$d ملفات من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups + تم استيراد %1$d ملفًا من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups + تم استيراد %1$d ملف من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups + + اختر ملف zip لملفات حفظ %1$s + تصدير %1$s + + لم يتم تصدير أي ملف على هيئة %2$s: %3$s + تم تصدير ملف واحد على هيئة %2$s: %3$s + تم تصدير ملفين على هيئة %2$s: %3$s + تم تصدير %1$d ملفات على هيئة %2$s: %3$s + تم تصدير %1$d ملفًا على هيئة %2$s: %3$s + تم تصدير %1$d ملف على هيئة %2$s: %3$s + + اختر مكان حفظ %1$s (%2$s) + فشل %1$s: %2$s + لا توجد سجلات جلسات بعد: شغِّل جلسة أولاً. + لا توجد سجلات جلسات لمحوها. + + لم يتم محو أي مجلد لسجلات الجلسات. + تم محو مجلد واحد لسجلات الجلسات. + تم محو مجلدين لسجلات الجلسات. + تم محو %1$d مجلدات لسجلات الجلسات. + تم محو %1$d مجلدًا لسجلات الجلسات. + تم محو %1$d مجلد لسجلات الجلسات. + + فشل الإقران عبر تصحيح الأخطاء اللاسلكي + فشل أمر تصحيح الأخطاء اللاسلكي + تصحيح الأخطاء اللاسلكي غير متاح + تم نسخ أمر ADB + اختر مجلد ROMs + وحدة GPU ليست من نوع Adreno + يدعم Turnip وحدات GPU من نوع Adreno. على %1$s، قد يعرض Steam شاشة سوداء. حجم التنزيل: %2$.0f MB. + التثبيت على أي حال + إزالة بيئة تشغيل Linux + سيؤدي هذا إلى حذف بيئة التشغيل وعميل Steam بداخلها وكل لعبة مثبَّتة فيها. + تم التطبيق: %1$s + تعذّر فتح %1$s على الشاشة الأساسية + تعذّر فتح %1$s على %2$s + تعذّر فتح %1$s على الشاشة %2$d + الاستيراد + التبديل + الاستعادة + الإلغاء + الحذف + تتعذّر قراءة الملف + تم استيراد %1$s + تم إلغاء التبديل المنتظَر. + اختر حزمة مكوّن (-linux .wcp) + تعذّر الوصول إلى إصدارات Nightlies + تم تخزين %1$s + فشل التنزيل: %1$s + اختر برنامج تشغيل لبيئة Linux (ملف zip بلاحقة -Linux أو حزمة Android + Linux) + اختر برنامج تشغيل العرض (ملف zip من AdrenoTools أو حزمة Android + Linux) + اختر ملف zip لبرنامج تشغيل أو حزمة Android + Linux + %1$s: %2$s + جارٍ إزالة %1$s… + جارٍ نسيان %1$s… + اختر مجلد تخزين الألعاب + اختر ملف ZIP لإضافة Decky + غير صالح للاستخدام: %1$s + برنامج + جارٍ إزالة بيئة تشغيل Linux + تعذّر فتح الإعدادات على الشاشة السفلية، لذا فُتحت على الشاشة الأساسية. + افتح خيارات المطوّرين في الإعدادات + + تعذّر فتح إعداد «تثبيت التطبيقات غير المعروفة» في Android + تعذّر التحقّق: %1$s + لا يتوفّر لهذا الإصدار ملف تنزيل لهذه النسخة من DroidDeck + أوقِف الجلسة الجارية أولاً، ثم حدِّث + جارٍ التنزيل + جارٍ التثبيت + في انتظار تأكيدك + فشل التحديث + لم يعرض Android تثبيت التحديث + + فشل تثبيت Decky: %1$s + جارٍ بدء استيراد الإضافة… + فشل استيراد الإضافة: %1$s + تم تثبيت الإضافة. أعِد تشغيل Steam لتحميلها. + + لم يتم التحقّق بعد - انقر على «تحديث» للبحث عن برامج تشغيل جديدة + Android + Linux: يضبط برنامج تشغيل العرض معه + Android + Linux: يضبط برنامج تشغيل بيئة Linux معه + Turnip المدمج في بيئة التشغيل + Vulkan الخاص بالنظام: لا يوجد إصدار مضمَّن لوحدة GPU هذه + %1$s (مضمَّن) + يدوي + %1$s · %2$s + نُشر أحد الجزأين فقط حاليًا + %1$.0f MB للاثنين + لا توجد برامج تشغيل لضبطها: %1$s + جارٍ البحث عن أحدث برامج التشغيل… + محدَّث (تعذّر التحقّق: %1$s) + برامج التشغيل المقترحة غير منشورة حاليًا + جارٍ تنزيل %1$s %2$s… + تم التحديث إلى %1$s %2$s. + تم التحديث إلى %1$s %2$s. يُطبَّق برنامج تشغيل العرض بعد إعادة تشغيل DroidDeck. + تم التبديل إلى %1$s %2$s. + تم التبديل إلى %1$s %2$s. يُطبَّق برنامج تشغيل العرض بعد إعادة تشغيل DroidDeck. + يتم استخدام %1$s %2$s. + يتم استخدام %1$s %2$s. يُطبَّق برنامج تشغيل العرض بعد إعادة تشغيل DroidDeck. + ليس ملف zip لبرنامج تشغيل: ليس برنامج تشغيل AdrenoTools ولا Turnip بلاحقة -Linux ولا حزمة Android + Linux + فشل الاستيراد: %1$s + برنامج تشغيل مستورَد + يتم استخدام %1$s كبرنامج تشغيل لبيئة Linux وللعرض معًا. + يتم استخدام %1$s كبرنامج تشغيل لبيئة Linux وللعرض معًا. يُطبَّق برنامج تشغيل العرض بعد إعادة تشغيل DroidDeck. + تم حذف %1$s (كلا برنامجَي التشغيل) + تم حذف %1$s + الأحدث: %1$s · آخر تحقّق %2$s + الأحدث: %1$s · تعذّر الوصول إلى %2$s · آخر تحقّق %3$s + + منذ %1$d دقيقة + منذ دقيقة واحدة + منذ دقيقتين + منذ %1$d دقائق + منذ %1$d دقيقة + منذ %1$d دقيقة + + + منذ %1$d ساعة + منذ ساعة واحدة + منذ ساعتين + منذ %1$d ساعات + منذ %1$d ساعة + منذ %1$d ساعة + + + منذ %1$d يوم + منذ يوم واحد + منذ يومين + منذ %1$d أيام + منذ %1$d يومًا + منذ %1$d يوم + + جارٍ التحقّق من Banners-Turnip وWinNative وDroidDeck… + فشل التحقّق + تم تثبيت %1$s - اختره من أيٍّ من القائمتين لضبط برنامجَي التشغيل معًا + تم تثبيت %1$s - اختره من القائمة + + انتظر حتى تنتهي إزالة بيئة التشغيل + لعبة + لوحة اللمس + مباشر + فشل التبديل: %1$s + فشل حفظ ملف التعريف: %1$s + تم إيقاف سطح المكتب مؤقتًا + تم إيقاف %1$s مؤقتًا + تم إيقاف البرنامج مؤقتًا + جارٍ تشغيل سطح المكتب + جارٍ تشغيل %1$s + جارٍ تشغيل البرنامج + لا توجد سجلات لهذه الجلسة. + تعذّر إنشاء أرشيف سجلات الجلسة. + تعذّرت مشاركة سجلات الجلسة. + جارٍ بدء الجلسة… + جارٍ تنزيل بيئة تشغيل Linux + جارٍ تنزيل بيئة تشغيل Linux · %1$d من %2$d MB + جارٍ فحص بيئة تشغيل Linux + جارٍ فك ضغط بيئة تشغيل Linux + جارٍ تنزيل سطح المكتب + جارٍ تنزيل سطح المكتب · %1$d من %2$d MB + جارٍ فحص سطح المكتب + جارٍ فك ضغط سطح المكتب + جارٍ تنزيل Proton Experimental (ARM64) + جارٍ تنزيل Proton Experimental (ARM64) · %1$d من %2$d MB + جارٍ فحص Proton Experimental (ARM64) + جارٍ فك ضغط Proton Experimental (ARM64) + تعذّر الوصول إلى كتالوج بيئة التشغيل. تحقَّق من الاتصال واضغط على «تشغيل» مرة أخرى. + لم يتم تثبيت بيئة تشغيل Linux. تحقَّق من بيئة التشغيل في «الإعداد» واضغط على «تشغيل» مرة أخرى. + لم يتم تثبيت سطح المكتب (%1$s). تحقَّق من الاتصال واضغط على «سطح المكتب» مرة أخرى. + توقّف %1$s بشكل غير متوقَّع. شارِك السجلات مع بلاغ عن الخطأ، أو حاوِل مرة أخرى. + توقّف البرنامج بشكل غير متوقَّع. شارِك السجلات مع بلاغ عن الخطأ، أو حاوِل مرة أخرى. + توقّف سطح المكتب بشكل غير متوقَّع. شارِك السجلات مع بلاغ عن الخطأ، أو حاوِل مرة أخرى. + حالة الخروج %1$d · %2$s + لا يوجد سجل + + يُظهر السجل %1$d خطأ من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد. + يُظهر السجل خطأً واحدًا من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد. + يُظهر السجل خطأين من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد. + يُظهر السجل %1$d أخطاء من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد. + يُظهر السجل %1$d خطأً من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد. + يُظهر السجل %1$d خطأ من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد. + + + الإضافات + ثبِّت ملف ZIP لإضافة في جلسة Steam التالية. + جارٍ التنزيل… + التثبيت من ملف ZIP + الألعاب + الصوت والفيديو + الرسومات + الإنترنت + الأدوات المكتبية + التطوير + التعليم + العلوم + النظام + الأدوات المساعدة + الشائعة + الرائجة + الجديدة + المحدَّثة + جارٍ التحميل… + إعداد Flatpak + %1$s: %2$s + تعذّر البحث عن تحديثات على Flathub + مثبَّتة + مجلد + غرافيت + أسطح بلون الغرافيت، مع أزرق كرة الشعار لكل عنصر محدَّد أو مُركَّز عليه أو مضغوط. + ورق على أسود + الأبيض هو اللون الأساسي، وأزرق الأيقونة هو لون الإشارة للخطوط والحلقات والنقاط. + أزرق الأيقونة + خلفية سوداء ونص أبيض، والأزرق لكل عنصر محدَّد أو مُركَّز عليه أو مضغوط. + كحلي كهربائي + أزرق أكثر سطوعًا، على أسطح تميل إلى الكحلي بدلاً من الرمادي. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + مسافة + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + ثبِّت بيئة التشغيل أولاً + مكتبة Steam غير متاحة + مكتبة SD السابقة غير متاحة + مساحة تخزين الألعاب غير متاحة + مجلد الألعاب المضافة غير متاح + ملفات البيان في Steam غير متاحة + أنظمة متعددة + التخزين الداخلي + نظام Linux + بطاقة SD + التخزين + تحديد + الخصائص + إضافة إلى المفضّلة + إزالة من المفضّلة + إعادة التسمية + نسخ + قص + الإجراءات + المفضّلة + تثبيت المجلد الحالي + لا توجد عناصر مفضّلة بعد - ثبِّت مجلدًا من قائمته ⋮ للعودة إليه بسرعة. + إزالة من المفضّلة + 0 بايت + بايت + الموقع + النوع + الحجم + تاريخ التعديل + مجلد + ملف + ملف %1$s + للقراءة فقط + يمنع الألعاب (وWine) من استبدال هذا الملف أو حذفه. + تعذّر تغيير «للقراءة فقط» + مخفي + لا تدعم وحدة التخزين هذه علامة الإخفاء. + يجعل الملف مخفيًا في Windows (سمة الإخفاء في Wine). + تعذّر تغيير «مخفي» على وحدة التخزين هذه + تم + جارٍ الحذف... + فشل الحذف + جارٍ النقل... + جارٍ النسخ... + جارٍ النقل - %1$s + جارٍ النسخ - %1$s + جارٍ النقل %1$d/%2$d - %3$s + جارٍ النسخ %1$d/%2$d - %3$s + اكتمل %1$d، وفشل %2$d + اكتمل %1$d، وتم تخطّي %2$d + + لم يتم نقل أي عنصر + تم نقل عنصر واحد + تم نقل عنصرين + تم نقل %1$d عناصر + تم نقل %1$d عنصرًا + تم نقل %1$d عنصر + + + لم يتم نسخ أي عنصر + تم نسخ عنصر واحد + تم نسخ عنصرين + تم نسخ %1$d عناصر + تم نسخ %1$d عنصرًا + تم نسخ %1$d عنصر + + «%1$s» موجود بالفعل + جارٍ إعادة التسمية... + فشلت إعادة التسمية + جارٍ إنشاء المجلد... + تعذّر إنشاء المجلد + مجلد جديد + اسم المجلد + إنشاء + الاسم الجديد + هل تريد الحذف؟ + هل تريد حذف «%1$s» نهائيًا؟ + + هل تريد حذف %1$d عنصر؟ + هل تريد حذف عنصر واحد؟ + هل تريد حذف عنصرين؟ + هل تريد حذف %1$d عناصر؟ + هل تريد حذف %1$d عنصرًا؟ + هل تريد حذف %1$d عنصر؟ + + لا يمكن التراجع عن هذا الإجراء. + + …و%1$d عنصر آخر + …وعنصر آخر + …وعنصران آخران + …و%1$d عناصر أخرى + …و%1$d عنصرًا آخر + …و%1$d عنصر آخر + + جارٍ الحذف %1$d/%2$d - %3$s + + تعذّر حذف %1$d عنصر + تعذّر حذف عنصر واحد + تعذّر حذف عنصرين + تعذّر حذف %1$d عناصر + تعذّر حذف %1$d عنصرًا + تعذّر حذف %1$d عنصر + + «دمج» يضيف الملفات ويستبدلها داخل المجلد الموجود. + «استبدال» يحلّ محلّ الملف الموجود. + التطبيق على كل التعارضات + دمج + استبدال + الاحتفاظ بالاثنين + تخطّي + اختيار هذا المجلد + رجوع + تم تركيب %1$s لكن تتعذّر قراءته حاليًا + عرض القائمة + عرض الشبكة + بحث + ترتيب + الاسم + تاريخ التعديل + صفوف واسعة + صفوف مضغوطة + إخفاء الملفات المخفية + إظهار الملفات المخفية + إخفاء المفضّلة + إظهار المفضّلة + تصفية هذا المجلد + %1$s متاحة + تم تحديد %1$d + لا شيء + الكل + لصق + + لا عناصر + عنصر واحد + عنصران + %1$d عناصر + %1$d عنصرًا + %1$d عنصر + + لصق %1$s هنا + لصق %1$s هنا (نقل) + الملفات + التخزين + وصول سريع + المفضّلة + التنزيلات + تنزيلات + ROMs + سجلات الجلسات + الصور + تمت إضافة «%1$s» إلى المفضّلة + تمت إزالة «%1$s» من المفضّلة + المجلد فارغ + الانتقال لمستوى أعلى + اختر مجلدًا + اختر ملفًا + أزرق Steam + سماوي + أبيض + بنفسجي + أخضر + كهرماني + أحمر + وردي + الزر A + الزر B + الزر X + الزر Y + الزر الكتفي الأيسر + الزر الكتفي الأيمن + الزناد الأيسر + الزناد الأيمن + زر العرض + زر القائمة + العرض + القائمة + لوحة الاتجاهات لأعلى + لوحة الاتجاهات لأسفل + لوحة الاتجاهات لليسار + لوحة الاتجاهات لليمين + مخفي + اسحب للنقل + محاذاة + حفظ + لا شيء + لوحة المفاتيح + لوحة اللمس + الوحدة الطرفية + مقابض Deck + لوحتا اللمس + + جارٍ إزالة بيئة تشغيل Linux + + جارٍ إزالة بيئة تشغيل Linux · %1$d عنصر + جارٍ إزالة بيئة تشغيل Linux · عنصر واحد + جارٍ إزالة بيئة تشغيل Linux · عنصران + جارٍ إزالة بيئة تشغيل Linux · %1$d عناصر + جارٍ إزالة بيئة تشغيل Linux · %1$d عنصرًا + جارٍ إزالة بيئة تشغيل Linux · %1$d عنصر + + تمت إزالة بيئة تشغيل Linux + تعذّر إكمال إزالة بيئة تشغيل Linux. أعِد محاولة الإزالة في «الإعداد». + جارٍ إزالة بقايا بيئة التشغيل + جارٍ التنزيل + جارٍ التحقّق + جارٍ الاستخراج + يظهر أثناء تثبيت بيئة تشغيل Linux أو إزالتها + جارٍ تثبيت بيئة تشغيل Linux + جارٍ إزالة بيئة تشغيل Linux + + بيئة تشغيل Linux غير مثبَّتة + لا يتضمّن الكتالوج مجموعًا اختباريًا لـ %1$s + المجموع الاختباري غير مطابق - لم يتم تغيير أي شيء + جارٍ تثبيت %1$s + تعذّر وضع ملف AppImage في مكانه + فشل الاستخراج + فشل التثبيت + + فشل إعداد Flatpak (رمز الخروج %1$d) + لم يكتمل إعداد Flatpak + هناك مهمة أخرى للمتجر قيد التشغيل + أعِدّ Flatpak أولاً + جارٍ البدء + خرج Flatpak بالحالة %1$d + جارٍ تثبيت %1$s + جارٍ تحديث %1$s + جارٍ إزالة %1$s + جارٍ تثبيت %1$s (%2$d/%3$d) + جارٍ تحديث %1$s (%2$d/%3$d) + جارٍ إزالة %1$s (%2$d/%3$d) + الترجمات + برامج تشغيل الرسومات + برامج ترميز الوسائط + بيئة تشغيل %1$s + بيئة تشغيل %1$s %2$s + + الملف أصغر من أن يكون AppImage + تتعذّر قراءة الملف: %1$s + هذا ليس AppImage (ليس برنامج Linux) + ملف AppImage هذا غير مُعَدّ لمعالجات ARM64 (aarch64) ولا لأجهزة الكمبيوتر x86 + هذا برنامج Linux لكنه ليس AppImage + لا يمكن استيراد سوى ملفات AppImage من النوع 2 (هذا الملف من النوع %1$d) + لا توجد مساحة خالية كافية: يتطلّب الاستيراد نحو %1$s + لا توجد مساحة خالية كافية: يتطلّب التحديث نحو %1$s + فشل الاستيراد + لم يعد ملف AppImage هذا موجودًا + فشل التحديث + جارٍ استخراج %1$s + تعذّر استخراج ملف AppImage + تعذّر استخراج ملف AppImage: %1$s + + أوقِف الجلسة النشطة قبل تثبيت Decky Loader + فشل تنزيل Decky Loader + حجم Decky Loader غير مطابق لبيانات الإصدار الوصفية + تعذّر تنزيل المجموع الاختباري للإصدار + المجموع الاختباري للإصدار غير متوفّر + المجموع الاختباري للإصدار غير صالح + عدم تطابق المجموع الاختباري لـ Decky Loader + ملف الإصدار ليس ملفًا تنفيذيًا متوافقًا بنظام Linux 64 بت + بدأت جلسة خلال التنزيل، أوقفها قبل تثبيت Decky Loader + تعذّر وضع علامة قابل للتنفيذ على PluginLoader + تعذّر تفعيل تصحيح الأخطاء عن بُعد لـ Steam CEF + تعذّر تجهيز PluginLoader + تعذّر الحفاظ على PluginLoader الحالي + تعذّر تفعيل PluginLoader الجديد + أوقِف الجلسة النشطة قبل تثبيت إضافة + ثبِّت Decky Loader قبل استيراد إضافة + تعذّرت قراءة ملف ZIP المحدَّد + تعذّر إنشاء مجلد إضافات Decky + جارٍ استخراج ملف ZIP للإضافة + يجب أن يحتوي ملف ZIP للإضافة على مجلد إضافة واحد في المستوى الأعلى + ملف ZIP للإضافة يفتقد plugin.json أو package.json أو dist/index.js + اسم الإضافة غير متوفّر + بدأت جلسة خلال التثبيت، أوقفها قبل تثبيت إضافة + تم تثبيت الإضافة · أعِد تشغيل Steam لتحميلها + فشل تثبيت ملف ZIP للإضافة + يحتوي ملف ZIP للإضافة على عدد كبير جدًا من العناصر + يحتوي ملف ZIP للإضافة على مسار غير صالح + يحتوي ملف ZIP للإضافة على مسارات مكرَّرة + يحتوي ملف ZIP للإضافة على مسار غير آمن + تعذّر إنشاء مجلد الإضافة + يتجاوز ملف ZIP للإضافة بعد الاستخراج الحدّ البالغ 1 GiB + تعذّر وضع علامة قابل للتنفيذ على الملف الثنائي المرفَق للإضافة + عنصر remote_binary غير صالح + اسم الملف الثنائي البعيد غير صالح + يجب أن يستخدم رابط الملف الثنائي البعيد بروتوكول HTTPS + تجزئة SHA-256 غير صالحة للملف الثنائي البعيد %1$s + تعذّر إنشاء مجلد bin للإضافة + جارٍ تنزيل الملف الثنائي للإضافة · %1$s + تعذّر تنزيل الملف الثنائي للإضافة %1$s + عدم تطابق المجموع الاختباري للملف الثنائي للإضافة · %1$s + تعذّر وضع علامة قابل للتنفيذ على الملف الثنائي للإضافة · %1$s + تعذّر استبدال الملف الثنائي للإضافة · %1$s + تعذّر تثبيت الملف الثنائي للإضافة · %1$s + مجلد الإضافة يخصّ إضافة أخرى + تعذّر الحفاظ على الإضافة الحالية + تعذّر تفعيل الإضافة المستورَدة + تعذّر تحديث إعدادات إضافات Decky + تعذّر استعادة الإضافة الحالية + + تعذّر حفظ هوية تصحيح الأخطاء اللاسلكي + فشل أمر ADB (%1$d) + لم يؤكّد Android تغيير حدّ العمليات الفرعية: %1$s + لا يوجد إقران محفوظ لتصحيح الأخطاء اللاسلكي + أدخِل عنوان IP الحالي ورقم المنفذ لتصحيح الأخطاء اللاسلكي + + انتهت مهلة انتظار النافذة المنبثقة للإقران. حاوِل مرة أخرى من DroidDeck. + تعذّر البحث عن النافذة المنبثقة للإقران (%1$d). تحقَّق من تفعيل Wi-Fi. + تعذّر البحث عن النافذة المنبثقة للإقران: %1$s + أدخِل الأرقام الستة كاملةً للرمز + جارٍ الإقران… + لم ينجح هذا الرمز. افتح النافذة المنبثقة للإقران مرة أخرى وأدخِل رمزها الجديد. + تم الإقران. جارٍ الاتصال… + تم الإقران، لكن لم يتم العثور على منفذ تصحيح الأخطاء اللاسلكي. اترك تصحيح الأخطاء اللاسلكي مفعَّلاً وحاوِل مرة أخرى. + جارٍ تطبيق الإعداد… + فشل أمر تصحيح الأخطاء اللاسلكي + إعداد تصحيح الأخطاء اللاسلكي + يطلب رمز إقران تصحيح الأخطاء اللاسلكي أثناء فتح الإعدادات + إقران تصحيح الأخطاء اللاسلكي + فعِّل تصحيح الأخطاء اللاسلكي، ثم اضغط على «إقران الجهاز برمز الإقران». + فعِّل تصحيح الأخطاء اللاسلكي، ثم اضغط على «إقران الجهاز برمز الإقران». ابقَ في الإعدادات: يُكتب الرمز في هذا الإشعار. + رمز الإقران المكوَّن من 6 أرقام + اضغط على «إدخال الرمز» وأدخِل الأرقام الستة الظاهرة في الإعدادات. + أدخِل رمز إقران Wi-Fi + أعِد تجربة الرمز + إدخال الرمز + جارٍ إعداد Steam + Steam جاهز + اضغط للعودة إلى DroidDeck. يمكنك إيقاف تصحيح الأخطاء اللاسلكي الآن. + توقَّف إعداد تصحيح الأخطاء اللاسلكي + + استجاب التنزيل بـ HTTP %1$d + حجم التنزيل غير مطابق لملف APK المنشور، وتم تجاهله - حاوِل مرة أخرى + التنزيل غير مطابق لمجموعه الاختباري، وتم تجاهله - حاوِل مرة أخرى + التحديث مخصَّص لحزمة Android %1$s، لا %2$s + بيانات التحديث الوصفية لا تذكر مفتاح توقيع إصدار DroidDeck + تعذّر على Android قراءة ملف APK الذي تم تنزيله + ملف APK الذي تم تنزيله مخصَّص لـ %1$s، لا %2$s + يشير ملف APK الذي تم تنزيله إلى versionCode %1$d، لا %2$d المنشور + لن يثبِّت Android versionCode %1$d فوق %2$d المثبَّت + ملف APK الذي تم تنزيله لا يحمل معلومات توقيع + ملف APK الذي تم تنزيله غير موقَّع بمفتاح إصدار DroidDeck + استجاب كتالوج التحديثات بـ HTTP %1$d + + جارٍ بدء الجلسة… + %1$d:%2$02d منقضية · لا تزال جارية + جارٍ تنزيل تحديث عميل Steam · %1$d%% + جارٍ تنزيل عميل Steam · %1$s + + يوجد تثبيت لأداة توافق قيد التنفيذ بالفعل + أوقِف الجلسة النشطة قبل تثبيت أدوات التوافق + تعذّر العثور على إصدار ARM64 لـ %1$s + يلزم توفُّر 4 GB على الأقل بالإضافة إلى حجم التنزيل لتثبيت %1$s + جارٍ تنزيل %1$s %2$s + جارٍ إعادة محاولة التنزيل · المحاولة %1$d من %2$d + انقطع التنزيل + فشل التنزيل؛ تم الاحتفاظ بالملف الجزئي لإتاحة استئنافه عند إعادة المحاولة + تجاوز التنزيل حجم الإصدار وتم حذفه + لم يكتمل التنزيل؛ تم الاحتفاظ بالملف الجزئي لإعادة محاولة قابلة للاستئناف + جارٍ التحقّق من التنزيل + تعذّرت قراءة المجموع الاختباري للإصدار؛ حاوِل مرة أخرى + لا ينشر %1$s %2$s مجموعًا اختباريًا؛ لم يتم تثبيت أي شيء + عدم تطابق المجموع الاختباري؛ تم حذف التنزيل + بدأت جلسة خلال التنزيل، أوقفها قبل تثبيت أدوات التوافق + جارٍ تثبيت %1$s + فشل تثبيت %1$s (حالة الخروج %2$d) + تعذّر تثبيت %1$s + جارٍ جلب حزمة droiddeck-esync + + مع أدوات Linux المساعدة + ملفات DLL فقط + الأصلي (تم تحديث Proton منذ آخر تبديل) + تم تغييره خارج المكوّنات + غير موجود + الأصلي %1$s + الأصلي من %1$s + سينتقل %1$s %2$s إلى %3$s عند إغلاق اللعبة التي تعمل عليه. + أصبح %1$s %2$s الآن في %3$s. يُطبَّق عند تشغيل لعبة في المرة التالية. + تلك الحزمة الأصلية غير مخزَّنة + سيعود %1$s في %2$s إلى أصله عند إغلاق اللعبة التي تعمل عليه. + تمت استعادة %1$s في %2$s من أصله في %3$s. + %1$s قيد الاستخدام في %2$s. بدِّل ذلك الإصدار من Proton إلى شيء آخر أولاً. + %1$s غير مخزَّن + تم حذف %1$s. + تم الاحتفاظ بأصل النسخة المثبَّتة: فهو طريق العودة. + غير مخزَّن + تم حذف أصل %2$s من %1$s. + التنزيلات تأتي فقط من إصدارات Nightlies + قائمة الحزم هذه أقدم من إضافة المجاميع الاختبارية - حدِّثها وحاوِل مرة أخرى + عدم تطابق المجموع الاختباري + + خطي + الأقرب + Snapdragon GSR (جودة) + زيادة الحدة فقط + تلقائي (يطابق التكبير) + + لا يُطبَّق أي شيء. + وضوح اللعبة + زيادة حدّة مع لمسة من التباين - الخيار اليومي. + زاهي + ألوان مُعزَّزة بقوة، مع تفعيل إزالة التدرّج الشريطي لمنع ظهور خطوط في السماء. + سينمائي + تباين أعمق، ألوان أبرد، وحواف أكثر نعومة. + تنافسي + ظلال مرفوعة وزيادة حدّة قوية - لرؤية الأشخاص أولاً. + زيادة حدّة متكيّفة + زيادة حدّة فقط، دون تغيير الألوان. + فيلمي + منحنى يشبه الفيلم السينمائي، بألوان خافتة قليلاً. + أركيد + صاخب وقوي التأثير. + CRT الكلاسيكي + تأثير أنبوب CRT والإشارة التناظرية معًا. + تكبير حاد + للتشغيل بدقة أقل من دقة الشاشة. + بكسل نقي + بكسلات واضحة للألعاب الثنائية الأبعاد والألعاب القديمة. + حافة الأنيمي + حدود كرتونية مع زيادة الحدّة. + + وحدة GPU ليست من نوع Adreno + مدعوم + تجريبي: برامج تشغيله إصدارات اختبارية + أقل من العتاد المُختبَر (Adreno 725 وأحدث): قد لا يعمل + غير مدعوم: يحتاج DroidDeck إلى وحدة GPU من نوع Adreno (Snapdragon) + + تم الوصول إلى حدّ الطلبات في GitHub - حاوِل مرة أخرى لاحقًا + استجاب GitHub بـ HTTP %1$d + استجاب التنزيل بـ HTTP %1$d + قائمة برامج التشغيل هذه أقدم من إضافة المجاميع الاختبارية - حدِّثها وحاوِل مرة أخرى + عدم تطابق المجموع الاختباري - تم تجاهل التنزيل + ليس برنامج تشغيل AdrenoTools: %1$s + لا يوجد meta.json + meta.json غير قابل للقراءة (%1$s) + هذا برنامج تشغيل بيئة Linux (استورده ضمن «برنامج تشغيل بيئة Linux») + هذا برنامج تشغيل ألعاب لـ Wayland، ولا يستخدمه هذا التطبيق + لا يذكر meta.json أي libraryName، فلم يكن بالإمكان تسليم أي شيء إلى AdrenoTools + يذكر meta.json اسم %1$s، وهو غير موجود في ملف zip + %1$s ليست مكتبة مشتركة ELF من نوع AArch64 64 بت + %1$s يرتبط بـ glibc - وهو إصدار Turnip بامتداد «-Linux»، الذي يندرج تحت «برنامج تشغيل بيئة Linux» + لا يوجد libvulkan_freedreno*.so في ملف zip هذا. ملف Turnip من نوع Android (AdrenoTools) أو -Wayland ليس برنامج تشغيل بيئة Linux. + %1$s ليست مكتبة مشتركة ELF من نوع AArch64 64 بت. + %1$s ليس برنامج تشغيل glibc - فهو يرتبط بمكتبة libc الخاصة بـ Android. تحتاج بيئة تشغيل Linux إلى ملف zip بامتداد «-Linux»؛ ولا يمكن لعميل Steam تحميل إصدار Turnip العادي أو بامتداد «-Wayland» على الإطلاق. + ليست حزمة برامج تشغيل: لا يوجد manifest.json يذكر برنامج تشغيل لـ Android وآخر لـ Linux + ليست حزمة برامج تشغيل: %1$s يشير إلى خارجها + ليست حزمة برامج تشغيل: تتجاوز بعد الاستخراج %1$d MB + + بطاقة SD + مجلد + + مشاركة سجلات الجلسة + + يحتاج إلى Android 10 أو أحدث (طبقات عرض بمساحة ألوان) + لا تُعلن هذه الشاشة عن دعم HDR + تدعم هذه الشاشة %1$s، وليس HDR10 + + وحدة GPU هذه + حدّ العمليات الفرعية مفعَّل + حدّ العمليات الفرعية غير محدَّد + تعذّر التحقّق من حدّ العمليات الفرعية + حدّ العمليات الفرعية غير مفعَّل + حدّ العمليات الفرعية غير مطلوب + %1$s إذا لم يكن ذلك ممكنًا، وصِّل هذا الجهاز بكمبيوتر يستخدم ADB ونفِّذ الأمر أدناه. عُد إلى هنا وتحقَّق مرة أخرى. + يمكن بدء جلسات Steam. + لا يستخدم إصدار Android هذا هذا الحدّ. + فعِّل «تعطيل قيود العمليات الفرعية» في خيارات المطوّرين، واترك خيارات المطوّرين مفعَّلة بعد ذلك. + لا يحتوي إصدار Android هذا على مفتاح لذلك. استخدِم تصحيح الأخطاء اللاسلكي لتغييره من هذا الجهاز. + لا يتوفّر تنزيل لهذا الإصدار الخاص بهذه النسخة من DroidDeck. + لن يثبِّته Android فوق هذا الإصدار لأن versionCode %1$d أقل من %2$d المثبَّت. + لن يثبِّت Android إصدارًا أقدم فوق إصدار أحدث. ابقَ على هذا الإصدار إلى أن تلحق به قناة التحديثات. + هذه النسخة من DroidDeck موقَّعة بشكل مختلف (إصدار محلي؟)، فلن يحدِّثها Android في مكانها. + لا تتوفّر مساحة خالية كافية للتحديث. + تم إلغاء التحديث. + لم يتم تثبيت التحديث: %1$s + لم يتم تثبيت التحديث. + ملف zip لـ %1$s + ملف zip + لم يتم تشغيل %1$s بعد + لم يتم العثور على ملفات حفظ لـ %1$s + لا يحتوي %1$s على مجلد drive_c، فهو ليس ملف حفظ من GameHub أو Winlator + تعذّر إنشاء مجلدات في %1$s + تعذّرت الكتابة في %1$s + تعذّرت الكتابة في %1$s (%2$s) + المجلد غير متاح + يستخدم ملف آخر %1$s بالفعل + مجلد الصور الفنية غير متاح + يحتوي المجلد على قائمة مزامنة غير ذات صلة + تعذّرت إزالة %1$s + الملف يخرج عن مجلد التصدير + FEX (إصدار بدون اسم نسخة) + لا يوجد profile.json في %1$s + %1$s ليس حزمة مكوّن لـ Linux (النوع %2$s) + %1$s · %2$s متاحة + تلقائي + فشل كتالوج التحديثات في اختباراته (%1$s) + إعداد العمليات الفرعية لـ DroidDeck + تم تسجيل الدخول · وضع عدم الاتصال + تم تسجيل الدخول + مرفق + منزَّل + مستورَد + Android + Linux + %1$s. %2$s\n\n%3$s + شاشة الجلسة + الدقة + ملاءمة اللعبة لنسبة 16:9 + كيفية ملاءمة ألعاب 16:9 لهذه الشاشة. يُطبَّق في الجلسة التالية. + اختر «مطابقة الشاشة» أو حجمًا مخصَّصًا أضيق من 16:9. + الحفاظ على النسب + تمديد لملء الشاشة + تكبير الصورة + مرشِّح التكبير + مشترك بين Steam وسطح المكتب. + يُطبَّق فوريًا. مشترك بين Steam وسطح المكتب. + تُطبَّق FSR و GSR و NIS عند تكبير صورة الجلسة. ويعمل خيار «زيادة الحدة فقط» أيضًا دون تكبير. + حدّة المرشِّح + معدّل الإطارات + %1$s يضبط أسلوب التكبير على %2$s. + مطابقة الشاشة (%1$d×%2$d) + اتباع الشاشة (يُغيَّر الحجم عند الطي أو الفتح) + Steam + إظهار تفاصيل الجهاز + إخفاء تفاصيل الجهاز + جارٍ قراءة بيانات الجهاز… + نسخ + انسخ هذه التفاصيل في بلاغ عن الخطأ. + تم النسخ. + فرض SSBS + يحافظ على إعداد سرعة لوحدة المعالجة (CPU) يستمر Wine في إيقافه. قد يجعل ألعاب Windows أسرع على Snapdragon 8 Elite؛ مع تغيّر طفيف على الشرائح الأقدم. يُطبَّق عند تشغيل اللعبة في المرة التالية. + + المتاجر + Steam + مخصَّص + المتاجر + فتح متجر على + فئة سرعة التنزيل + محرك المتاجر + ● المحرك جاهز + غير مبنيّ + متوازن + سريع + أقصى + المتاجر + إعدادات المتاجر + الإعدادات + (لم يتم تسجيل الدخول) + المتجر + المثبَّتة + المكتبة + الكل + التنزيلات + %1$s (%2$d) + تسجيل الدخول + %1$s · تسجيل الخروج + تسجيل الخروج من %1$s؟ + تبقى الألعاب المثبَّتة. تُجلَب المكتبة مرة أخرى عند تسجيل الدخول مجددًا. + البقاء مسجَّل الدخول + تسجيل الخروج + البحث في كتالوج %1$s… + البحث في الألعاب المثبَّتة… + البحث في مكتبتك… + البحث + لا شيء مثبَّت + جارٍ التحميل + لا توجد ألعاب + جارٍ تحميل المتجر… + لا يوجد كتالوج + الجديد + العروض + ألعاب مجانية + الرائج + مكتبتك + تشغيل + تثبيت + استئناف التثبيت + منقطع + تثبيت · %1$s + احصل عليها مجانًا + عرض على %1$s + شراء · %1$s + مثبَّتة + في المكتبة + مجاني + استئناف + تحديث + إظهار المحتوى للبالغين + Epic + EOS + غير متصل + غير مفعَّل + تسجيل الدخول إلى Epic (EOS) + التشغيل دون اتصال + إصلاح تسجيل الدخول إلى Epic + ملفات الحفظ السحابية + الرفع إلى السحابة + التنزيل من السحابة + لا توجد ملفات حفظ سحابية + تعارض سحابي + تعارض سحابي · %1$d ملفات + … + الاحتفاظ بالسحابة + الاحتفاظ بالمحلي + جارٍ التحقّق من السحابة… + آخر مزامنة %1$s + لم تتم المزامنة بعد + %1$d ملفات + مثبَّتة + في مكتبتك + متجر + الإنجازات + %1$d من %2$d مفتوحة + %1$d / %2$d + مفتوحة · %1$d + مقفلة · %1$d + مخفية · %1$d + إنجاز مخفي + يتم عرض التفاصيل بعد فتحه. + جميع الإنجازات مفتوحة + جارٍ التحميل… + سجّل الدخول إلى %1$s لرؤية إنجازاتك. + تعذّر الوصول إلى %1$s. تحقّق مرة أخرى عند الاتصال بالإنترنت. + لا يسرد %1$s أي إنجازات لهذه اللعبة. + إظهار الكل (%1$d) + الإنجازات · %1$s + ✓ في Steam + إلغاء التثبيت + جارٍ إلغاء التثبيت… + إلغاء تثبيت %1$s؟ + يُفرِغ %1$s من المساحة ويزيل اللعبة من Steam. + يزيل ملفاتها واختصارها في Steam. + يكتشف Steam اللعبة عند بدء تشغيله التالي؛ شغِّلها من هناك، أو شغِّل Steam الآن. + Proton + اختيار Steam لكل لعبة + الصوت + DirectAudio + المكوّنات + FEX، DXVK، VKD3D + المحرك + %1$s · أصلي + %1$s · مدمَج + كيف تعمل + تم الجلب من + %1$s، بحسابك + مثبَّتة في + تخزين الألعاب (Games/%1$s/%2$s) + تُشغَّل باستخدام + Steam وProton، بنفس مسار ألعاب Steam + Steam وProton · تسجيل الدخول إلى Epic عند كل تشغيل، وتراكب EOS معطَّل + Steam وProton · تم ضبط متغيّرات Amazon Games Services + Steam + تُضاف إلى Steam عند التثبيت (واجهة Deck + Steam Input) + لا توجد تنزيلات + متوقِّف مؤقتًا + قيد الانتظار + قيد الانتظار · #%1$d + ملغى + فشل + فشل · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + إيقاف مؤقت + استئناف + إلغاء + إعادة المحاولة + إزالة + إزالة المنتهية + الفهرس + التنزيل + التحقّق + التثبيت + تم + جارٍ الفحص + جارٍ التنزيل + حذف التنزيل؟ + فشل · تم الاحتفاظ بـ %1$s + جارٍ التحقّق + جارٍ التثبيت + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d ملفات + مدير التنزيلات + عدد التنزيلات المتزامنة + التثبيت في + تثبيت %1$s: + التخزين الداخلي + %1$s متاحة + تنزيلات المتجر + يعرض تقدّم تنزيل الألعاب ويحافظ على استمرار التنزيلات في الخلفية + جارٍ التنزيل + %1$d تنزيلات + جارٍ التحضير… + تنزيلات المتجر المكتملة + تم تثبيت %1$s + فشل تنزيل %1$s + حذف + الاحتفاظ + تسجيل الدخول إلى %1$s + تسجيل الدخول عبر مزوّدك + إعادة التحميل + إغلاق + حاوِل مرة أخرى + جارٍ إنهاء تسجيل الدخول… + تعذّر تحميل صفحة تسجيل الدخول: %1$s + يرفض هذا المزوّد تسجيل الدخول من صفحة مضمَّنة. سجِّل الدخول باستخدام كلمة مرور المتجر بدلاً من ذلك. + استجابت صفحة تسجيل الدخول بخطأ (HTTP %1$d). + الاتصال بصفحة تسجيل الدخول غير آمن. + تعذّر التحقّق من تسجيل الدخول. حاوِل مرة أخرى. + فشل تسجيل الدخول. حاوِل مرة أخرى. + إغلاق + رجوع + تعديل + إضافة كل الألعاب في هذا المجلد + لا شيء جديد في هذا المجلد + الاسم + الهدف + بدء التشغيل في + خيارات التشغيل + الصور الفنية + ملف exe آخر… + إزالة + الغلاف + الخلفية + الشعار + الأيقونة + مجلد اللعبة + Steam + SteamGridDB + ملفك + من ملف exe + تلقائي + اختيار صورة… + اختيار صورة: %1$s + إعادة الضبط إلى تلقائي + + لم تتم إضافة أي لعبة + تمت إضافة لعبة واحدة + تمت إضافة لعبتين + تمت إضافة %1$d ألعاب + تمت إضافة %1$d لعبةً + تمت إضافة %1$d لعبة + + + لا توجد أي لعبة ضمن الألعاب بالفعل + لعبة واحدة ضمن الألعاب بالفعل + لعبتان ضمن الألعاب بالفعل + %1$d ألعاب ضمن الألعاب بالفعل + %1$d لعبةً ضمن الألعاب بالفعل + %1$d لعبة ضمن الألعاب بالفعل + + B · العودة إلى %1$s + اللعبة + إعدادات التشغيل + برامج تشغيل الرسوميات + هذا الإصدار من Proton + كل إصدار من Proton + لكل إصدار من Proton + متاح · %1$d + آخر تحقّق %1$s + التحقّق من وجود مكوّنات وبرامج تشغيل جديدة + جلب + %1$s · DXVK وVKD3D-Proton وFEX أدناه خاصة بهذا الإصدار من Proton + %1$s · تم اختباره + %1$s · لم يتم اختباره + %1$s · غير مدعوم + يحافظ على الزوج الموصى به محدَّثًا + تلقائي · %1$s + برنامجا تشغيل بيئة التشغيل والعرض بشكل منفصل + استخدام %1$s؟ + إزالة %1$s؟ + التشخيص + جاهز للّعب + غير جاهز للّعب بعد + بيئة التشغيل %1$s + حدّ العمليات الفرعية غير مفعَّل + تم تسجيل الدخول باسم %1$s + عندما يحتاج الأمر إلى تدخّلك + المظهر + هذا الجهاز + صور المكتبة الفنية + تطبيقات Linux + Flathub + تطبيقات سطح المكتب لسطح مكتب Linux، تُفتح من سطح المكتب. + إعداد Flathub + إزالة %1$s؟ + تُحفَظ إعداداته وملفاته. + الاحتفاظ + الحجم على القرص + إلغاء تثبيت سطح المكتب؟ + الكل + إنجازات GOG + مزامنة الإنجازات للألعاب المتوافقة من GOG. يُطبَّق عند التشغيل التالي. + الإنجازات متوقفة + الإنجازات مفعّلة + سجّل الدخول إلى GOG مرة أخرى + تم رفع الإنجازات + رفع الإنجاز معلّق + فعّل إنجازات GOG في إعدادات المتاجر + الإنجازات غير متوفرة؛ تحقّق من سجلات الجلسة + تم فتح الإنجاز + لصق + لوحة الاتجاهات + العصا اليسرى + العصا اليمنى + القائمة + لوحة المفاتيح + لوحة الاتجاهات للأعلى + لوحة الاتجاهات للأسفل + لوحة الاتجاهات لليسار + لوحة الاتجاهات لليمين + أدوات التحكم باللمس + وحدة تحكم باللمس + الفأرة + تشغيل + إيقاف + بطيئة + عادية + سريعة + تعديل التخطيط + قائمة DroidDeck + اسحب أدوات التحكم إلى الشاشة + ترتيب + تجاهل + جارٍ الحفظ… + الأيقونة + ع− + ع+ + ط− + ط+ + إخفاء + اللون والعتامة… + نسخ هذا التخطيط + تم نسخ التخطيط؛ يمكنك لصقه في تخطيط اللمس لأي لعبة + لصق التخطيط المنسوخ + إعادة الضبط إلى تخطيط Steam الافتراضي + لا يوجد ارتباط لهذا العنصر في Steam لإعطائه أيقونة + أيقونة %1$s + لون تخطيط اللمس + تم + تعذّر حفظ تخطيط اللمس + تم حفظ تخطيط اللمس في Steam + التطبيق %1$d + التسمية (اختياري) + لم يتم العثور على أيقونات Steam + + لا توجد أيقونات من Steam + أيقونة واحدة من Steam + أيقونتان من Steam + %1$d أيقونات من Steam + %1$d أيقونة من Steam + %1$d أيقونة من Steam + + لون الأيقونة + لون الزر + بلا أيقونة + + جارٍ التحقق من تسجيل الدخول… + جارٍ جلب مكتبتك… + جارٍ جلب قائمة الألعاب… + جارٍ تحميل تفاصيل الألعاب… (%1$d/%2$d) + + لا مزامنة لألعاب + جارٍ مزامنة لعبة واحدة… + جارٍ مزامنة لعبتين… + جارٍ مزامنة %1$d ألعاب… + جارٍ مزامنة %1$d لعبة… + جارٍ مزامنة %1$d لعبة… + + جارٍ المزامنة… %1$d/%2$d + جارٍ جلب أغلفة الألعاب… + فشلت المزامنة + لم يتم العثور على ألعاب في مكتبة %1$s الخاصة بك + لم يستجب Amazon Games بخصوص مكتبتك: حدّث الصفحة، أو سجّل الدخول من جديد + تعذّر الاتصال بـ GOG + تعذّر قراءة مكتبتك على GOG + جارٍ التسجيل في Steam… + Cardboard VR + وضع Cardboard + عرض للعين اليسرى وآخر لليمنى لنظارة واقع افتراضي للهاتف. + الصورة + شاشة افتراضية + ثلاثي الأبعاد جنبًا إلى جنب + النظارة + Cardboard + Cardboard (2014) + بلا عدسة + المسافة بين العينين + عرض الشاشة + بُعد الشاشة + تتبّع الرأس + لا يحتوي هذا الجهاز على مستشعر دوران. + إعادة توسيط العرض + إعادة التوسيط + SteamVR (تجريبي) + تثبيت + ينزّل SteamVR لمعمارية ARM64 من Valve ‏(571 ميغابايت). + جارٍ تنزيل SteamVR وفك ضغطه… + بدء + يشغّل SteamVR مع استخدام رأسك كنظارة؛ وتتحول الصورة إلى ثلاثي الأبعاد جنبًا إلى جنب. + إيقاف + SteamVR قيد التشغيل. + غير متوفر في هذه الجلسة. + لم يستجب SteamVR؛ تحقّق من سجل الجلسة. + تنزيل المكوّنات + التنزيلات مشتركة مع صفحة «المكوّنات». اختر نسخة منزّلة أعلاه لاستخدامها عند التشغيل التالي للعبة. + اختر نسخة لتنزيلها + المطوّر + الناشر + الإصدار + القصة الرئيسية + + الإضافات + الإكمال التام + %1$s س + %1$d د + diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml new file mode 100644 index 000000000..982623fd1 --- /dev/null +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -0,0 +1,93 @@ + + + + La primera vez se descarga Steam. Puede tardar unos minutos. + De forma predeterminada, al salir de la app Steam se pausa y continúa donde lo dejaste cuando vuelves. + Tu control aparece como un control de Xbox 360. Si no, usa los controles en pantalla. + + + La primera vez se prepara el escritorio. Puede tardar unos minutos. + De forma predeterminada, al salir de la app la sesión se pausa y continúa donde la dejaste cuando vuelves. + El tacto funciona como un touchpad: arrastra para mover el puntero y toca para hacer clic. + Tu control aparece como un control de Xbox 360. Si no, usa los controles en pantalla. + + Agrega una biblioteca de Steam y busca juegos de Windows en ella. Los juegos que tienes usan sus entradas de Steam; el resto se convierten en accesos directos ajenos a Steam. + Agregar variable + Pon 0 para desactivar el asistente de navegación con control de Proton en este juego. + Agregar + %1$s agregada + Presiona de nuevo para quitar + No se pudo agregar la app + Agregar una app + Agregar + Agregar… + Auto muestra los controles en pantalla cuando no hay ningún control conectado. + Control + Cómo ve Steam tu control. Se aplica en la próxima sesión. + Control de Steam Deck + Control de Xbox 360 + Agregar juego + Agregar un juego: + Los juegos que cargan recursos desde la SD o el almacenamiento compartido pueden tener microcortes. + Aún no hay ningún Proton instalado. Inicia Steam una vez para descargar su Proton ARM64, o agrega GE-Proton o CachyOS desde Configuración. + Solo Adreno. Menos microcortes, más consumo de batería y calor. + Desactiva el asistente de navegación con control de Proton, que consume CPU en cada juego. + Búsquedas de archivos más rápidas y menos microcortes. Desactívalo si un programa no encuentra sus archivos. + Control + Administrar + Control + Presiona Jugar en Steam e inicia sesión + Control + Los ajustes del control no están disponibles. + Explorar y administrar archivos + Agrega Flatpak y lo conecta a Flathub (unos 20 MB). La primera app también descarga su runtime. + Presiona de nuevo para quitar + Esta versión de Android no tiene un interruptor para ello, así que DroidDeck lo cambia mediante la Depuración inalámbrica. Tarda un minuto y no necesitas una computadora. + Ventana encontrada. Ingresa su código en la notificación. + Depuración inalámbrica, dirección manual, ADB desde una computadora + Dirección manual, ADB desde una computadora, Opciones de desarrollador + Algunas ROM agregan el interruptor de todos modos. Busca “procesos secundarios” en Opciones de desarrollador. + Si la notificación no funciona, ingresa manualmente la dirección y el código de la ventana. + Ingresar dirección manualmente + Desde una computadora con ADB: + Vinculado. Ingresa el puerto de Ajustes. + 3 · ADB desde una computadora (último recurso) + 3 · ADB desde una computadora (último recurso) + Presiona Select para abrir el menú de Steam + Restablecer control + Restaura todos los ajustes del control y la disposición + Agrega juegos de %1$s a ROMs/%2$s. + Elimina el escritorio y sus apps incluidas. Se conservan los juegos, las apps agregadas, los ajustes y las bibliotecas compartidas. + Administrar partidas guardadas + Juego agregado + Control + Agregar a la pantalla de inicio + Este launcher no puede agregar accesos directos. Usa Copiar enlace de inicio. + No se pudo acceder al catálogo del runtime. Revisa la conexión y vuelve a presionar Jugar. + El runtime de Linux no se instaló. Revisa el runtime en Configuración y vuelve a presionar Jugar. + El escritorio no se instaló (%1$s). Revisa la conexión y presiona Escritorio de nuevo. + Audio y video + Superficies grafito, con el azul del orbe del logotipo para todo lo seleccionado, enfocado o presionado. + Fondo negro, texto blanco y el azul para todo lo seleccionado, enfocado o presionado. + Enter ⏎ + La carpeta de juegos agregados no está disponible + Agregar a Favoritos + Combinar agrega y reemplaza archivos dentro de la carpeta existente. + Se agregó \"%1$s\" a Favoritos + Ingresa la dirección IP y el puerto actuales de la Depuración inalámbrica + Ingresa los 6 dígitos del código + Ese código no funcionó. Vuelve a abrir la ventana de vinculación e ingresa su nuevo código. + Activa la Depuración inalámbrica y toca “Vincular dispositivo con código de vinculación”. Quédate en Ajustes: el código se ingresa en esta notificación. + Toca Ingresar código y escribe los 6 dígitos que muestra Ajustes. + Ingresa el código de vinculación Wi-Fi + Ingresar código + %1$s Si no es posible, conecta este dispositivo a una computadora con ADB y ejecuta el comando de abajo. Después vuelve aquí y comprueba de nuevo. + se agrega a Steam al instalar (interfaz Deck + Steam Input) + Administrador de descargas + Agregar todos los juegos de esta carpeta + + %1$d juego agregado + %1$d juegos agregados + %1$d juegos agregados + + diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 000000000..ec2f9ff6c --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,2235 @@ + + + DroidDeck + + Първото стартиране изтегля Steam. Това може да отнеме няколко минути. + По подразбиране излизането от приложението поставя Steam на пауза; при връщане продължава от същото място. + Контролерът ви се разпознава като контролер Xbox 360. В противен случай използвайте екранните контроли. + + + Първото стартиране подготвя работния плот. Това може да отнеме няколко минути. + По подразбиране излизането от приложението поставя сесията на пауза; при връщане тя продължава от същото място. + Докосването действа като тъчпад: плъзгане премества показалеца, докосване щраква. + Контролерът ви се разпознава като контролер Xbox 360. В противен случай използвайте екранните контроли. + + Сесия + Показва се, докато има отворена сесия на Steam или работния плот + Steam работи + Активно изтегляне в Steam + Сесията е на пауза – докоснете „Продължи“, когато сте готови + Продължи + Приспи сега + Спри сесията + Генериране на кадри + Изберете режима на генериране на кадри + Изключено + Win-FG %1$d× + LSFG %1$d× + LSFG Adaptive %1$d + Изисква Lossless Scaling в Steam + Инсталирайте Lossless Scaling в Steam или импортирайте Lossless.dll + Импортиране на Lossless.dll… + Използва се копието от Steam + Използва се импортирано копие + Изберете Lossless.dll + Lossless.dll е импортиран + Този Lossless.dll вече се използва + Lossless Scaling не е закупен за този акаунт в Steam + Този файл не е Lossless.dll на Lossless Scaling + LSFG не можа да се подготви от този Lossless.dll + LSFG изисква Lossless Scaling от Steam или импортиран Lossless.dll + LSFG не можа да подготви шейдърите си от Lossless.dll + %1$s: не се поддържа + %1$s: грешка в двигателя + %1$s: изисква Lossless Scaling + %1$s %2$s · стартиране + Добавя библиотека на Steam и я сканира за игри за Windows. Притежаваните игри използват записите си в Steam; останалите стават преки пътища извън Steam. + Среда на играта + Променливи на средата за игри с Proton. + Редактиране + Всички игри с Proton + Профил + ID на приложението в Steam + Отвори + Игра %1$s + Добавяне на променлива + Име на променливата + Стойност + Запиши + Премахни + Наследи отново + Изчистване на промените в средата + Премахни за този профил + Няма зададени променливи. + Наследена + Персонализирана променлива + Не можа да се зареди или запише. Проверете свободното място в хранилището и опитайте пак. + Използвайте валидно име и стойност до 8192 знака. + Стандартната среда на Proton не отчита тази променлива. + Зареждане… + Замяна на нивото на възможности на D3D12. 12_2 показва възможности на DirectX 12 Ultimate; неподдържаните функции може да дадат грешка. Оставете незададено за автоматично откриване. + Замяна на модела на шейдъри на D3D12. 6_9 изисква достатъчно нов VKD3D и съответстващи възможности на GPU. Оставете незададено за автоматично откриване. + Дръжте кеширането на шейдъри на Mesa включено, за да намалите повтарящото се компилиране. false разрешава кеширането. + Максимален размер на кеша за шейдъри на Mesa, например 1G. По-големите кешове използват повече място в хранилището. + Настройки на VKD3D за D3D12. nodxr изключва трасирането на лъчи; използвайте само за игра, която го изисква. + Задайте 1 за диагностичен дневник на Proton. Записването на дневник може да намали производителността и да използва място в хранилището. + Задайте 1, за да използвате WineD3D, базиран на OpenGL, вместо DXVK за тестване на съвместимост с D3D9–11. + Задайте 1, за да изключите ntsync (droiddeck-ntsync или драйвер на ядрото) за тази игра; тогава се използва droiddeck-esync, ако има пакет, или droiddeck-fsync в противен случай. + Задайте 1, за да изключите droiddeck-esync за тази игра; вместо това се използва droiddeck-fsync, или droiddeck-ntsync, ако fsync също е изключен. + Задайте 1, за да изключите droiddeck-fsync за тази игра; тогава се използва droiddeck-esync, ако има пакет, или droiddeck-ntsync в противен случай. + Задайте 1, за да стартирате тази игра с droiddeck-fsync, или 0, за да поставите droiddeck-esync на първо място, ако има пакет, подходящ за нейния Proton. + Задайте 0, за да изключите помощника на Proton за навигация с контролер за тази игра. + Насложен слой на DXVK за D3D9–11. fps показва честотата на кадрите; незададено го скрива. + Настройки на DXVK, разделени с точка и запетая. По-новите версии използват dxvk.maxFrameRate за ограничаване на кадрите; за обикновена игра предпочитайте ограничителя на кадри на сесията. + Ограничение на честотата на кадрите за D3D12. 0 изключва ограничението. + Многонишковост на Mesa OpenGL. true може да помогне при игри с OpenGL, ограничени от процесора; тествайте за всяка игра. + Синхронизация на Wine + Как игрите поддържат нишките си синхронизирани. Прилага се от следващата сесия. + esync + ntsync + fsync + wineserver + Предварителна настройка на FEX + Прилага се при следващото стартиране на играта. + Игри + Стандартни настройки на FEX + Използва стандартните настройки на FEX. + Стабилност + Най-безопасните и най-бавните настройки. + Съвместимост + Пълен TSO с multiblock. + Средно + Балансиран TSO с намалена точност на x87. + Производителност + Целият TSO е изключен; multiblock. + Производителност + TSO + Настройки за производителност с включен основен TSO. + Екстремно + Изключва проверките за самопроменящ се код. Игри с JIT и DRM може да спрат да работят. + Екстремно + TSO + Екстремни настройки с включен основен TSO. + Extreme-gn + Производителност с TSC и подсказки за метаданни. Пробвайте преди Екстремно. + Denuvo + Производителност с пълни проверки на кода и скрит hypervisor бит. + Персонализирана стойност + Избор на стойност + Емулатори и приложения + Добави + Ваше собствено приложение или игра + Скрипт + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Стартиране… + Добавено: %1$s + Премахнато: %1$s + Работен плот + Това приложение вече не съществува. Изберете го отново от работния плот. + Отвори %1$s + Премахни + Натиснете отново, за да премахнете + Спрете сесията, за да премахнете + Стартира %1$s от папката му, свързана към сесията там, където се намира. Премахването на приложението не докосва тази папка. + Стартира %1$s от копие на папката му в DroidDeck. Премахването на приложението изтрива копието, а не оригинала. + Първо инсталирайте средата на Linux + Файлът вече не съществува + Няма достатъчно свободно място: копирането на папката изисква %1$s + Копиране на %1$s + Добавянето на приложението не бе успешно + Търсене на издание на %1$s + Изданията на %1$s не можаха да се прочетат + Никое издание на %1$s няма AppImage за ARM64 или x86_64 + Изтегляне на %1$s + Изтеглянето не бе успешно + Актуализирането на приложението не бе успешно + Тази икона не можа да бъде прочетена + Запазено: %1$s + Редактиране + Проверка за актуализации + Проверка за актуализации… + Актуализиране до %1$s + Актуално · %1$s + Актуализирано: %1$s + %1$s · %2$s + Добавяне на приложение + Скрипт + AppImage + GitHub + Flatpak + Файл .sh. Стартира от собствената си папка, на цял екран под gamescope. + AppImage за ARM64 (aarch64) или за x86_64, който се стартира през FEX. Разопакова се в средата на Linux само веднъж. + Инсталира AppImage за ARM64 от най-новото издание на хранилището, или вариант за x86_64 през FEX. + Използване на FEX + Автоматично + Включено + Изключено + FEX стартира x86 Linux програми на това ARM64 устройство. Автоматично го използва, когато програмата е компилирана за x86. + %1$s през FEX + Тази програма е компилирана за %1$s компютри и се стартира през FEX, който още не е настроен. Отворете Steam веднъж, докато сте влезли: DroidDeck ще изтегли FEX и необходимата Steam Linux Runtime, след което тя ще се отвори. + Тази програма + %1$s е компилирана за x86 компютри и се стартира през FEX, който още не е инсталиран. Стартирайте Steam веднъж, докато сте влезли, и го оставете да изтегли FEX и Steam Linux Runtime, след което стартирайте отново. + ID на приложение в Flathub или страницата му в Flathub. Flatpak се настройва първо, ако още не е. + Файл + Не е избран файл + Избор на файл + Промяна + Изберете скрипт + Изберете AppImage + Изберете икона + Хранилище + owner/repo или връзка към github.com + Това не е хранилище в GitHub + ID на приложението + org.ppsspp.PPSSPP + Това не е ID на приложение в Flathub + Или файл .flatpak за приложение, което не е в Flathub. + Изберете файл .flatpak + Източник + На вашия работен плот + Ново приложение + Име + От приложението + Икона + Стандартна икона + Избор на икона + Изчистване + От хранилището + Търсене на икони… + Стандартна + Редактиране на %1$s + Запиши + Проверка на папката… + Папката му е свързана към сесията: %1$s + Папката му съдържа Linux програми, които общото хранилище не може да стартира, затова се копира в DroidDeck (%1$s). + Добави + Отказ + Първо инсталирайте средата на Linux от Настройка. + Изчакайте %1$s да завърши. + Съвместим + Версиите, отбелязани с „%1$s“ в менюто за съвместимост на Steam, имат поправки на DroidDeck за префикса и стартират игри тук. По подразбиране е Proton Experimental ARM64 (%1$s), или Proton 11.0 ARM64 (%1$s), след като се инсталира тази по-нова версия. Собствените записи на Proton от Valve без тази отметка не могат да стартират игра на това устройство. + Инсталирано %1$s · %2$s + Отказ + Автоматично + Никога + Винаги + Деинсталиране + Проверка + Актуализация + Проверка… + Обработва се… + Забрави + Добави… + Използвай + Прилага се от следващата сесия. + Сесия на Steam + Сесия на работния плот + Екран + Резолюция + До 720p + До 900p + До 1080p + Естествена + %1$s (по подразбиране) + Персонализирана · %1$d×%2$d + Персонализирана… + 720p може да направи менютата по-отзивчиви. + Пропорции на екрана + Определени от персонализираната резолюция. + Автоматично използва поне 16:9. + Ограничение на кадрите + Ограничава цялата сесия. Екранът преминава към честота на опресняване, която се дели точно на ограничението. + HDR + HDR10 изход + Не е налично: %1$s. + Рестартирайте приложението, за да се приложи. + Драйвери + GPU драйвери + Общи за Steam и работния плот. Задават се в страницата Компоненти. + Докосване и контроли + Докосване + Тъчпад + Директно + Автоматично: тъчпад на работния плот, директно в Steam. + Екранни контроли + Steam + QAM + Автоматично показва контролите, когато няма свързан контролер. + Докосване в Steam (бета) + Сензорните контроли на Steam ще стартират от следващата сесия на Steam + Разположение на докосването в Steam + Премествайте, преоразмерявайте или скривайте сензорните контроли на тази игра; записва се в Steam + Контролер + Как Steam разпознава контролера ви. Прилага се от следващата сесия. + Контролер на Steam Deck + Steam Controller + DualShock 4 + DualSense + Контролер Xbox 360 + Deck: бутон Quick Access и гироскоп. Steam Controller: същото, с усещане за захват на задните бутони. DualShock 4 и DualSense: гироскоп, а тракпадовете служат като тъчпад. Xbox 360: отворете Quick Access с Guide+A. + Back + Меню → QAM + QAM → Меню + Сесия + Поведение във фонов режим + Когато приложението е скрито или екранът се изключи. + Ръчно + Системно + Автоматично поставя на пауза и продължава, когато се покаже отново. Системно подготвя Steam за заспиване първо. Ръчно изчаква „Продължи“. Никога продължава да работи. + Продължаване на изтеглянията + Отлага фоновото спиране, докато Steam активно изтегля. + Автоматично поставя на пауза във фонов режим и продължава при показване. Ръчно изчаква „Продължи“. Никога продължава да работи. + Стартиране + Стартиране на Steam заедно с DroidDeck + Отваря сесията на Steam при стартиране на DroidDeck. + Decky + Проверка за издания… + Готово за инсталиране на %1$s. + Не е намерена съвместима версия. Опитайте по-късно. + Инсталирано · %1$s + Налична актуализация · %1$s + Актуално · %1$s + Loader + Инсталиране на последната версия + Стартира заедно със Steam. Докато е включено, други приложения на това устройство може да могат да контролират Steam. + Изключено. Портът за отдалечено дебъгване на Steam остава затворен. + Деинсталиране на Decky Loader? + Премахва зареждащия модул. Плъгините и настройките ви се запазват. + Клиент + Насложен слой за производителност + Изключете, ако игрите забиват или екранът става черен в режим Deck. Прилага се от следващата сесия. + Поправка на клиента на Steam + Изтегля отново клиента на Steam при следващото стартиране на Steam. Използвайте, ако Steam остава на екрана за зареждане. Игрите, влизането в профила и настройките се запазват. + Клиентът на Steam ще бъде изтеглен отново при следващото стартиране на Steam. + Поправка + Изображения + API ключ за SteamGridDB + Вашият ключ + Редактиране… + Попълва липсващите изображения от Steam, а след това от SteamGridDB. Собствените ви файлове cover.jpg, hero.jpg, logo.png или icon.png в папката на играта имат предимство. + Премахване на %1$s + Премахване? + Добавяне на игра + Добавяне на игра: + Изберете .exe файла: + Това местоположение не е достъпно в сесията + Проверка на този .exe файл + Компоненти на Windows + LB / RB преминават между разделите. + Няма + DLL файлове за Windows, които Proton не включва, от списъка с компоненти. Включете тези, от които тази игра се нуждае: те отиват в собствената Windows папка на играта, никога в нейните файлове, и остават независимо на кой Proton се изпълнява тя. + Списъкът с компоненти не можа да се достигне. Вече изтеглените компоненти все още могат да се превключват. + Препоръчани за тази игра + Проверка какво изисква тази игра… + Достъпно, когато сте онлайн + + Включване на %1$d компонент… + Включване на %1$d компонента… + + Включени за тази игра + Steam инсталира %1$s с %2$s (приложение %3$s) + От списъка на GOG за тази игра + От изискванията на Epic за тази игра + От стъпките за инсталиране на Amazon за тази игра + Steam инсталира %1$s с приложение %2$s, съпоставено по име + Също в списъка + Посочено от %1$s; не е компонент тук + Steam + GOG + Epic Games + Amazon Games + Играта включва %1$s + Играта вече има %1$s (често работи така) + Вече в тази игра + Нищо в папката на играта не насочва към компонент. + Инсталирани за тази игра (%1$d) + Всички компоненти (%1$d) + Нуждаят се от инсталатор за Windows (%1$d) + Това са самостоятелни инсталационни програми без пакет вътре за разглобяване и все още без запис на какво инсталират. Повечето са стари среди за изпълнение, за които Mono в Proton вече служи като заместник; записи се добавят с течение на времето. + Показване + Скриване + Изпълнява собствена инсталационна програма (все още без запис) + Все още не се поддържа тук + Зареждане на компонентите… + Инсталирането не бе успешно: %1$s + Изтеглено + Изтегляне на %1$s + Инсталиране: %1$s + Изключване… + Изтегляне %1$s + Влиза в сила при следващото стартиране на играта. + Разтягане на игри в 16:9 + На екрани, по-квадратни от 16:9, разтяга игрите, за да запълнят екрана, вместо да показва ленти. Прилага се от следващата сесия. + Звук + DirectAudio за игри + Пропуска PulseAudio за по-ниска латентност. + Микрофон + За гласов чат. + Мрежа + Откриване на Wi-Fi + Показва имената на Wi-Fi и близките мрежи в Steam. + Услугите за местоположение са изключени. + Разрешението за местоположение не е предоставено. + Отваряне на настройките + Продължи + Включване на откриването на Wi-Fi? + Android изисква разрешение за местоположение, за да прочете имената на Wi-Fi и да открие близките мрежи. DroidDeck използва този достъп, за да показва тези мрежи в Steam. Не следим местоположението ви. Достъпът до интернет работи без това разрешение. + Хранилище за игри + Автоматично – SD картата, когато е налична, плюс папките ви Games + Автоматично – папките ви Games, плюс SD картата, когато е налична (в момента няма) + Само вътрешна памет + Папка: %1$s + Втора библиотека + Игри, които стриймват ресурси от SD картата или общото хранилище, може да заекват. + Диагностика на хранилището + Записва резултатите от заделянето и извадкови времена на хранилището в „Споделяне на дневници“. Може да забави инсталациите, докато е включено. Прилага се от следващата сесия. + Изберете папка… + Персонализирана резолюция + Заменя ограничението и пропорцията. Размер, който не съвпада с екрана, получава ленти. + Широчина + Височина + Между 320×240 и 3840×2160. + + %1$d игра + %1$d игри + + ОК + Изтрий + Възстанови + Изтегли + Изтриване на %1$s + %1$.1f MB + Драйвер за изпълнение + Използва се от Steam, игрите и работния плот. Прилага се от следващата сесия. + Импортиране на Turnip zip… + Драйвер за дисплея + Изобразява кадрите на екрана в двата режима. Рестартирайте приложението, за да се приложи. + Импортиране на AdrenoTools zip… + никога + Nightlies проверени %1$s + Компоненти + За Компоненти + Проверка за нови драйвери + Проверка на Nightlies за нови пакети + Proton + Избраният тук Proton стартира всяка игра, която не е избрала свой собствен в Steam, а промяна, направена в Steam, се показва и тук. Steam заменя Proton на Valve при актуализация; оригиналите се запазват за всяка версия. + Конфигурация + Конфигуриране на профил + Профил + Профил по подразбиране + Използва се автоматично от игри без промени. + Записани настройки, използвани автоматично при стартиране на %1$s. + Изберете само настройките, от които тази игра се нуждае. Всичко останало продължава да използва профила по подразбиране. + Предварителна настройка на FEX + Използва настройката на профила по подразбиране. + Персонализирана настройка, запазена за тази игра. + Персонализирани променливи на FEX + Използване на профила по подразбиране + Съвместимост на играта + Настройки на средата за Proton, DXVK, VKD3D и синхронизация за тази игра. + Използва Proton от профила по подразбиране. + Персонализиран Proton, запазен за тази игра чрез таблицата за съвместимост на Steam. + Без Proton + DXVK + VKD3D-Proton + Използва пакета от профила по подразбиране от %1$s, освен ако тази игра избере друг пакет. + Използва пакета с компоненти от профила по подразбиране. + Използва %1$s само за тази игра. + Настройки за изпълнение + Други настройки на средата за Proton, DXVK, VKD3D-Proton и синхронизация за отделни игри. + Профилът на %1$s е запазен за следващото стартиране на играта. + Старт без Steam + За игра, която се затваря веднага: стартира я без настройката на Steam в Proton. Функции на Steam, като оверлея, са изключени за нея. + Включено + Изборът на Proton за %1$s е запазен и вече е достъпен в Steam. + Изборът на Proton за %1$s е запазен. Рестартирайте Steam, преди да стартирате играта. + %1$s не може да използва избрания Proton на това устройство. + Профилът на %1$s не можа да се зареди: %2$s + Профилът на играта не можа да се запази: %1$s + Нулиране на профила на играта + Нулиране на профила на %1$s? + Премахване на Proton, FEX, пакета с компоненти, средата, филтрирането на текстури и промените в компонентите на Windows за тази игра? Тя ще използва профила по подразбиране. + %1$s вече използва профила по подразбиране. + Профилът на играта не можа да се нулира напълно: %1$s + %1$s вече стартира всяка игра, която не е избрала свой собствен Proton в Steam. + %1$s ще стартира всяка игра, която не е избрала свой собствен Proton, от следващото стартиране на Steam. + %1$s не може да стартира игри тук, затова не може да бъде по подразбиране. + Proton по подразбиране не можа да се запази: %1$s + %1$s · %2$d заменени + %1$s · всички оригинални + %1$s · играта работи + %1$s · пакет droiddeck-esync + %1$s · все още няма пакет droiddeck-esync + %1$s · esync вграден, не е нужен пакет + GPU драйвери + %1$s · приложен отново при стартиране %2$s + Следващо: %1$s · след затваряне на играта + %1$s в употреба: %2$s + Изчаква затварянето на играта + Прилага се при следващото стартиране на игра + Импортиране на .zip + Четене на Proton версиите… + Още няма инсталиран Proton. Стартирайте Steam веднъж, за да изтегли своя ARM64 Proton, или добавете GE-Proton, CachyOS или DroidDeck-Proton от Настройка. + Оригинал · %1$s + %1$s · тази версия + %1$s · по-стара версия + Инсталирани · %1$d + Все още няма запазено за %1$s. Собствените файлове на този Proton се запазват като негов оригинал. + Игра работи на този Proton, затова това изчаква затварянето й. + Прилага се при следващото стартиране на игра. + Замяна на %1$s? + Поставяне на %1$s в %2$s? Оригиналните му файлове остават запазени. %3$s + Възстановяване на %1$s? + Поставяне на оригинала на %1$s обратно в %2$s? %3$s + Премахва пакета от приложението. Можете да го изтеглите отново. + Изтриване на този оригинал? + Този оригинал на %1$s е от по-стара версия на %2$s. Оригиналът на текущата версия винаги се запазва. + В Nightlies · %1$d + Обновете, за да видите пакетите в Nightlies. + Нищо ново от последната проверка. + Импортиране на .wcp + Възстановяване на оригинала на този Proton + Поставяне обратно на оригиналния %2$s на %1$s? + Замяна + FEX, DXVK и VKD3D-Proton за всеки Proton, с който Steam стартира игри.\n\nИзберете инсталиран ред, за да го замените. Прилага се при следващото стартиране на игра или след затваряне на текущо работещата игра.\n\nИзборите ви се проверяват повторно преди всяко стартиране и се възстановяват, ако нещо ги е променило. Оригиналните файлове на всеки Proton се запазват, така че актуализациите на Steam никога не ги губят. + Оригинал + В употреба + Ново + Запазено + Производителност + CPU ядра, корекции на сесията и името на хоста. Прилага се от следващата сесия. + Ядра за клиента на Steam + Замяна на избора на ядра на Steam + Фиксира Steam, помощника за интерфейса му и gamescope към ядрата по-долу. + Ядра на клиента + Ядра за Steam. + Включете замяната, за да изберете. + Ядра за игри + Ядра за игри + Ядра за игри. Когато всички са избрани, Android ги разпределя свободно. + Интерфейс на клиента + Многонишков GL + Може да направи менютата на Steam по-отзивчиви. + Zink: лениви дескриптори + Препоръчва се за драйвери без буфери за дескриптори. + Без проверки за GL грешки + Изключва проверката на GL за всяко извикване. + gamescope: GPU опашка в реално време + Дава приоритет на GPU работата на компоситора. Може да изглади темпото на кадрите с известна цена за игрите. + GPU + Задръжте GPU на максималната му честота + Само за Adreno. По-малко засечки, по-голямо използване на батерията и повече топлина. + Корекции на сесията + Turnip: рендиране sysmem + По-бързо за Steam и повечето игри. Задължително на Adreno 710/720/722. + Пропускане на помощника xalia на Steam + droiddeck-ntsync (експериментално) + Заместител на потребителско ниво за драйвера ntsync на Linux, за тестване. Докато е включен, Proton го използва вместо droiddeck-esync и droiddeck-fsync. Работи с Proton Experimental ARM64, GE-Proton и proton-cachyos. Прилага се от следващата сесия. + droiddeck-esync + droiddeck-fsync първо + Стартира собствения fsync на Proton върху оригиналните файлове на Proton, с futex_waitv, подаден от потребителско ниво, затова не изисква пакет и продължава да работи веднага след актуализация на Proton от Steam. Докато е включен, игрите го използват преди droiddeck-esync, освен малкото, които Proton държи извън fsync. Прилага се от следващата сесия. + Windows нишките на игрите се изчакват взаимно чрез eventfd и обща памет, вместо през обиколки през wineserver. Използва пакет droiddeck-esync, направен точно за тази версия на Proton; пакетите се извличат автоматично. Докато не съществува такъв за нова версия на Proton, droiddeck-fsync го замества. Прилага се от следващата сесия. + Изключва помощника на Proton за навигация с контролер, който използва процесорно време във всяка игра. + Стартиране на proot без seccomp + Може да отстрани грешки с липсващи системни извиквания на някои ядра. Може да намали производителността. + Бърз път на proot + Изисква филтъра seccomp на proot. + По-бързо търсене на файлове и по-малко заекване. Изключете, ако програма не може да намери файловете си. + Идентичност на сесията + Име на хоста + Името, което Steam вижда за това устройство. Оставено празно възстановява %1$s. + Само букви, цифри и тирета по средата, до 63 знака. + Android е настроен да прекратява тази сесия + Инсталиран + Налично за изтегляне + Нищо ново от последната проверка. + Възстановяване на вградените драйвери + Вграден в приложението: това премахва разопакованите му файлове и го скрива. „Възстановяване на вградените драйвери“ го връща обратно. + Половина от пакет Android + Linux: премахват се и двете половини. Драйвер в употреба се връща към стандартния. + Файловете му се премахват от приложението. + Файловете му се премахват. В употреба е, затова стандартният драйвер поема. + Екран + Ефекти + Контроли + Компоненти + Контролер + Настройки + Страница %1$s + Steam + Работен плот + Меню на Steam + Бърз достъп + Отваряне на менюто за бърз достъп + Предишна страница + Следваща страница + HUD за производителност + Режим на мащабиране + Острота на мащабиране + Ефекти на екрана + На живо. Използва малко GPU време, докато има включен ефект. + Предварителни настройки + Персонализирано + Разширени + Разгънато + Свито + Изостряне (CAS) + Ниво на изостряне + Имитация на HDR + Премахване на цветови ленти + Сила на премахването на цветови ленти + Яркост + Контраст + Гама + Наситеност + FXAA (изглаждане на ръбове) + Карикатурни контури + CRT + NTSC + Филтриране на текстури + Игри с DirectX 9–11, от следващото им стартиране. + Анизотропно филтриране + Острота на текстурите + Отрицателно отместване на mip нивото: по-остри текстури, леко трептене. Автоматично съответства на степента, до която сесията е увеличена спрямо екрана. + Автоматично (%1$s) + Клавиатура + Клавиатура за компютър + Android + Втори екран + Режим + Показва + Приложения за Android + Стартиране на приложение + Скриване + Показване + Няма приложения за стартиране + Поведение на сесията + Прилага се сега и за бъдещи сесии в този режим. + Следваща сесия + Поддръжка + Дневници на сесията + Прикачете към доклад за грешка + Споделяне на дневници + Фонов режим + Спри + Спиране на сесията? + В момента работи + Няма игра, работеща на Proton + Профил на играта + Все още няма данни за профила. + Профилите на играта не можаха да се заредят. Автоматично се прави нов опит. + Профилите на играта не можаха да се обновят. Показват се последните налични данни; автоматично се прави нов опит. + Преглед и редактиране на запазените настройки, използвани при следващото стартиране. + Използва се профилът по подразбиране + Персонализиран профил + Използва се %1$s от профила по подразбиране. + Промените се запазват в профила на %1$s и се прилагат при следващото стартиране на играта. + + %1$d промяна за играта + %1$d промени за играта + + Все още няма инсталиран Proton. + играта работи + приложено отново при стартиране %1$s + Оригинал + Импортиране и изтриване в страницата Компоненти. + Готово + Нужно е внимание + Обработва се + Проверете IP адреса и порта за безжично отстраняване на грешки или сдвоете отново, ако Android е премахнал това устройство. + Инсталиране + Управление + Общ преглед + Контролер + Сесия + Лаунчър + Настройка + ● Всичко е готово + %1$d от %2$d готови + Проверка на системата + Устройството се поддържа + Непроверен GPU + GPU не се поддържа + %1$s · %2$s + %1$s: %2$s + Steam изисква драйвер за Adreno; %1$s може да покаже черен екран + Linux среда за изпълнение + %1$s · %2$d%% + Не е инсталирано · около 3 GB, инсталира се при първото Play + Инсталирано + %1$s · налична актуализация + %1$s · актуално + Ограничение на дъщерните процеси + Включено · Android може да затвори Steam. Изключването отнема минута. + Не е зададено · Android може да затвори Steam. Изключването отнема минута. + Не можа да се провери · изключването отнема минута. + Поправи + Подробности + Опции за програмисти + Използване на безжично отстраняване на грешки + Проверка отново + Включване на ограничението + Акаунт в Steam + Влезли сте като %1$s · офлайн режим + Влезли сте като %1$s + Натиснете Play и влезте в Steam + Инструменти + Контролер + Настройките на контролера не са достъпни. + Сесия + Дневници на сесията + Запазват се след всяка сесия + Инструменти за отстраняване на грешки + Последни дневници на сесията + Запазени дневници на сесията + Запазва най-новите %1$d + Изчистване на дневниците + Офлайн режим + Първо влезте в Steam + Лаунчър + Език + Steam също го използва, от следващото му стартиране. + По подразбиране от системата (%1$s) + По подразбиране от системата + Мащаб на приложението + Текст и контроли на DroidDeck + %1$d%% (по подразбиране) + Тема + Облик на лаунчъра + Използване като начален екран + DroidDeck може да бъде приложението за начален екран + DroidDeck никога не се предлага като приложение за начален екран + Ориентация + Автоматичен хоризонтален + Хоризонтална + Обърната хоризонтална + Цял екран + Скрива лентите за състояние и навигация на Android + Показва лентите за състояние и навигация на Android + Приложение за начален екран по подразбиране + Изберете приложение за начален екран + Избор + Приложения за Linux (бета) + Файлове + Преглед и управление на файлове + Версии на Proton + Инсталиране на версии на Proton за ARM64 + Производителност + Разпределение на CPU ядрата + Папка за ROM файлове + Къде се съхраняват игрите за емулатори + Компоненти + FEX, DXVK, VKD3D + Контроли + Разпределение на бутоните + Откриване + Търсене + Инсталирани + %1$s · %2$d + Магазин + Flathub · ARM64 + ● Flatpak е готов + Първо инсталирайте средата на Linux от Настройка. + Настройване на Flatpak + Добавя Flatpak и го свързва с Flathub (около 20 MB). Първото приложение също изтегля своята среда за изпълнение. + Настройване… + Flatpak + Актуализации + %1$s · %2$s + %1$s · %2$s · %3$d%% + Стартиране… + Категории + Flathub не можа да бъде достигнат. Проверете връзката си и опитайте отново. + Опитайте отново + Търсене във Flathub + Firefox, игри, емулатори… + Търсене + Всички + Търсене… + Търсене по име или избор на категория. Показват се само приложения за ARM64. + Няма приложения за ARM64 във Flathub, съответстващи на това. + Резултати + Проверка за актуализации + Актуализиране на всички (%1$d) + ● Всичко е актуално + Все още няма приложения. Намерете такива в Откриване или Търсене. + Настройте Flatpak, за да инсталирате приложения. + Налична актуализация · %1$s + Отвори + Подробности + Премахни + Натиснете отново, за да премахнете + Инсталиране… + Инсталиране + Първо настройте Flatpak + Изтегляне %1$s + ● Инсталирано + Страницата на това приложение във Flathub не можа да се зареди. + Зареждане от Flathub… + Няма версия за ARM64 във Flathub, затова не може да се инсталира тук. + Екранни снимки + За приложението + ID на приложението + Лиценз + Среда за изпълнение + Инсталиран размер + %1$s (плюс споделената му среда за изпълнение) + Уебсайт + Потвърдено + Актуализации + DroidDeck %1$s · %2$s + Проверено %1$s + Още не е проверено + Разрешаване на DroidDeck да се актуализира самостоятелно + На следващия екран включете „Разрешаване от този източник“, след което се върнете. Актуализацията продължава сама. + Отваряне на настройките + Не сега + Различно подписано + Не може да се актуализира директно + Това копие не е подписано с ключа за издания на DroidDeck, затова актуализациите не могат да се инсталират върху него. Деинсталирайте го, за да превключите. + Налична актуализация + Не може да се инсталира + Готово за превключване + Пред Stable + Вие сте пред Stable + Тази версия е по-нова от последната Stable. Ще преминете към Stable с нейното следващо издание. + Тестът приключи + Този тест приключи + Поправката му е обединена или изоставена. Следвайте Preview за най-новите поправки. + Актуално + Имате най-новата версия %1$s + · %1$s + %1$s… %2$d%% + %1$s… + Актуализиране%1$s + Инсталиране%1$s + Инсталиране на Stable %1$s все пак + Следване на Preview + Спрете работещата сесия, за да актуализирате. + По-малко + Повече + DroidDeck %1$s + Нова версия на Preview + Тестова версия на PR #%1$d + %1$d MB + КАНАЛ ЗА АКТУАЛИЗАЦИИ + Stable + Тествани издания, за повечето хора + %1$s · %2$s + Preview + Най-новите поправки, преди Stable + Актуално + Тестови версии + Изпробвайте поправка, преди да бъде издадена + Няма в момента + PR #%1$d + Не е компилирана за това копие на DroidDeck + Актуализирано %1$s + теста на PR #%1$d + преди време + току-що + + %1$d за изпробване + %1$d за изпробване + + + Промени от вашата версия · %1$d версия назад + Промени от вашата версия · %1$d версии назад + + + Скорошни промени · %1$d версия + Скорошни промени · %1$d версии + + Една настройка на Android за промяна + Steam + Android спира приложения, които стартират много фонови процеси, а Steam стартира десетки. Докато това не се промени, игрите се затварят без грешка. + Поправи го вместо мен + Включете „Disable child process restrictions“, след което се върнете. Оставете Опции за програмисти включени, иначе превключвателят се изключва отново. + Отваряне на Опции за програмисти + Тази версия на Android няма превключвател за това, затова DroidDeck го променя чрез безжично отстраняване на грешки. Отнема около минута, не е нужен компютър. + Безжичното отстраняване на грешки изисква Wi-Fi. Работи с всяка мрежа; не е необходим интернет. + Кодът за сдвояване се показва в известие. Разрешете известията за DroidDeck или използвайте „Други начини“. + Разрешаване на известията + Отворете Настройки → Опции за програмисти → Безжично отстраняване на грешки и го включете + Докоснете „Сдвояване на устройство с код за сдвояване“ + Въведете кода в известието на DroidDeck + Изчакване на изскачащия прозорец за сдвояване… + Изскачащият прозорец е намерен. Въведете кода му в известието. + Състояние + Проверява се на всеки 2 секунди. Тази страница се затваря, когато е изключено. + Други начини + Безжично отстраняване на грешки, ръчен адрес, ADB от компютър + Ръчен адрес, ADB от компютър, Опции за програмисти + Използване на безжично отстраняване на грешки вместо това + Някои ROM-и добавят превключвателя въпреки това. Потърсете „child process“ в Опции за програмисти. + Ако известието не работи, въведете сами адреса и кода от изскачащия прозорец. + Ръчно въвеждане на адреса + От компютър с ADB: + Копиране на командата + Изберете на кой екран да се отворят настройките на Android. + Основен екран + Сдвоено. Търсене на порта за безжично отстраняване на грешки… + Сдвоено. Въведете порта от Настройки. + Устройството е намерено. Прилагане на настройката… + Не можа да се приложи автоматично. Проверете адреса по-долу и опитайте отново. %1$s + Безжично отстраняване на грешки + Ограничението на дъщерните процеси е актуализирано + Промяна на ограничението на дъщерните процеси + Ако опцията липсва, сдвоете чрез безжично отстраняване на грешки. + Сдвоено. Проверете IP адреса и порта в безжичното отстраняване на грешки. + Ограничението на дъщерните процеси беше актуализирано. + Използвайте стъпка 2, ако Опции за програмисти нямат настройка за дъщерни процеси. Отворете Опции за програмисти → Безжично отстраняване на грешки → Сдвояване на устройство с код за сдвояване. + Сдвоено. Използвайте адреса по-долу или го обновете от Безжично отстраняване на грешки → IP адрес и порт. + Ограничението на дъщерните процеси беше актуализирано чрез безжично отстраняване на грешки. + Android потвърди, че ограничението е включено. + Android потвърди, че ограничението е изключено. + 2 · Сдвояване на безжично отстраняване на грешки + IP адрес и порт на изскачащия прозорец за сдвояване + Код за сдвояване (PIN) + 6 цифри + Сдвояване + 2 · Прилагане чрез безжично отстраняване на грешки + IP адрес и порт за връзка + Сдвояване отново + Изключване на ограничението + Други начини за промяна + 1 · Опции за програмисти (пробвайте първо) + Включете „Restrict child processes“. + Изключете „Restrict child processes“. + 3 · ADB от компютър (последна възможност) + Само ако безжичното отстраняване на грешки не е достъпно. + 3 · ADB от компютър (последна възможност) + Кога се появяват екранните контроли в сесия + Цвят + Оттенък на екранните бутони + Персонализиран + Прозрачност + %1$d%% + Размер на бутоните + 100% е стандартният размер + Натискане на стика + Докоснете двукратно и задръжте стик за L3 или R3 + Адаптивни стикове + Вибрация от играта + Бутон Steam + Бутон QAM + Бутон за клавиатура + Натиснете Select, за да отворите менюто на Steam + Вместо View/Select. + Анимации + Стиковете се появяват там, където докоснете, и се скриват при отпускане + Разположение + Запазени персонализирани позиции + Нагласено за този екран + Редактиране + Нулиране + Разпределение на бутоните + Всеки бутон изпраща собствения си сигнал + %1$d от %2$d бутона преназначени + Конфигуриране + Нулиране на контролера + Възстановява всички настройки на контролера и разположението + Какво изпраща всеки екранен бутон към играта. „Скрит“ премахва бутона. + Екранни бутони + Стандартни настройки + Нулиране на разпределението + Приложения за Android + %1$d приложения + Не са намерени приложения за Android за стартиране. + ● Работният плот е инсталиран + Инсталира се при първото отваряне + Налично за инсталиране + Не е инсталирано. + %1$s · не е инсталирано + Отвори %1$s + Игри + Изберете папка за ROM файлове. + Отидете на /root/ROMs в RetroArch. + Добавете игри за %1$s в ROMs/%2$s. + Инсталиране на %1$s + Спрете сесията, за да инсталирате + Изисква се среда за изпълнение + Зареждане на подробности за инсталирането… + Подробностите за инсталирането не са достъпни. Опитайте по-късно. + Първо инсталирайте средата на Linux от Настройка. + Спрете работещата сесия, за да инсталирате приложения за работния плот. + Инсталира %1$s в работния плот на Linux. + %1$s · %2$d%% + Скриването го премахва от работния плот. Файловете му остават в средата на Linux. + Тази игра вече не е в папката за ROM файлове. + Работен плот · %1$s + Стартиране в %1$s + Също в %1$s + Тази страница вече не съществува. Изберете раздел от лявата страна. + Отваряне на работния плот + Инсталиране и отваряне на работния плот + Деинсталиране на работния плот + Премахва работния плот и включените в него приложения. Запазва игрите, добавените приложения, настройките и споделените библиотеки. + Премахване на работния плот + Работният плот е премахнат + Премахване на старите файлове на LXQt + Актуализирайте DroidDeck, преди да премахнете тази версия на работния плот. + Работен плот на Linux + KDE Plasma, Firefox и вашите емулатори + Игрите, които инсталирате в Steam, се показват тук след сесията. + Пускане на Steam + Настройки за стартиране + Стартиране + Файлове на играта + Префикс на Proton + Търсене на записи… + Не е намерена папка за записи по име. Експортирането взема цялата потребителска папка. + %1$d папки за записи + %1$s · %2$d файла · %3$s + %1$d KB + Управление на записите + Записи на играта + Импортиране на записи… + Експортиране като GameHub zip… + Експортиране като Winlator zip… + Отваряне на папката за записи + Никога не е играна + Средата за изпълнение е заета · %1$d%% + Средата за изпълнение е заета + Средата за изпълнение се инсталира при първото Play + Налична актуализация на средата за изпълнение + ● Средата за изпълнение е готова + Последна игра %1$s + Вътрешна памет + Добавена игра + Персонализирана игра + Хранилище за игри + Най-добрата двойка за този GPU, поддържана актуална + Изберете двойка сами + Проверка за драйвери… + Все още няма изброени драйвери. Обновете, за да ги потърсите. + Скриване на драйверите за други GPU + Показване на драйверите за други GPU + Разширени + Задава се заедно с драйвера за дисплея: %1$s включва и двете. + Задава се заедно с драйвера за изпълнение: %1$s включва и двете. + Използва се от игрите и работния плот. + Стандартен за изпълнение + Изобразява кадрите на екрана. Прилага се след рестартиране на DroidDeck. + Автоматично – избрано според GPU + Изтегляне, импортиране или изтриване… + В УПОТРЕБА + Драйверите, вградени в DroidDeck + Драйверите за изпълнение и за дисплея са зададени заедно като съответстваща двойка. + Препоръчан за този GPU: %1$s. + Обновете, за да потърсите драйвери за този GPU. + ПРЕПОРЪЧАНО + Недостъпно + Linux среда за изпълнение + Подготвяне на средата на Linux + Сесия + Стартиране на екрана и звука + Клиент на Steam + Проверка на Steam и инструментите му за съвместимост + Отваряне на Steam + Изчакване на първия кадър на Steam + Работен плот + Подготвяне на работния плот + Отваряне + Изчакване на първия кадър + Стартиране на Steam + Сесията приключи + Стъпка %1$d от %2$d + Steam е на пауза + Захвати + Ляв пад + Десен пад + Двата + Геймпад + Стик + Тракпад + Превключване между стик и тракпад + Още контроли на Deck + ПАНЕЛИ + Затваряне на контролите за втория екран + Отваряне на менюто на Steam + Ляво · горе + Ляво · долу + Дясно · горе + Дясно · долу + Клик + Десен тракпад + Ляв тракпад + Тракпад + Показване на клавиатурата + Скриване на клавиатурата + Терминал на Linux + Сесията на Linux все още не е готова за терминал. + Steam + Игри + Desktop + Магазин + Компоненти + Приложения + Настройка + Актуализации + %1$d%% + Продължаване на %1$s + Показва се като /root/ROMs във всяка сесия. Вътрешната памет е на /root/Storage. + Няма избрана папка + Прилага се от следващата сесия. SD картите работят. + Избор на папка + Смяна на папка + Инсталирай ARM64 версия на Proton, после я избери за всяка игра в Steam → Properties → Compatibility. + Спри текущата сесия, за да инсталираш или премахнеш инструмент за съвместимост. + Налични версии + Инсталирано: %1$s + Чака от по-ранна заявка + Не е инсталирано + Налична актуализация → %1$s + Премахване на старите версии на %1$s + Премахва %1$s. Игрите, зададени на тези версии, трябва да бъдат превключени към друг Proton. + Игрите, зададени на тази версия, трябва да бъдат превключени към друг Proton. + Инсталиране сега + Отказ на изчакването + Клавиатура на Android + Скриване ✕ + Пускане на %1$s + Избери екран. Втори: %1$s. + Вторият екран вече не е достъпен. + Основен екран + Втори екран + Предишен раздел + Следващ раздел + Вкл. + Всички %1$d + Нищо + %1$d от %2$d + Изкл. + Долен екран + Прилага се при следващото пускане на играта. Профилът на играта има превес над общия. + Друга игра по ID на приложението… + Готово + По подразбиране + Изкл. + Смяна… + Промяна на променлива + Връщане по подразбиране + Proton игри + Картина в картината + Автоматично влизане в „Картина в картината“ + Контроли на разгънатия екран + Разгънат, играта стои над тракпадите и задните захвати на Steam Deck. Сгънат, играта е на цял екран. + Прекъсната + Картина в картината е достъпна само когато сесията е готова. + Разреши картина в картината за DroidDeck в настройките на Android. + Android не можа да отвори картина в картината. + Пряк път + Добавяне към началния екран + Избор на игра + Търсене на инсталирани игри… + Копиране на връзката за пускане + Връзката за пускане е копирана + Текущият начален екран не поддържа преки пътища. Използвай „Копиране на връзката за пускане“ вместо това. + Невалидна връзка за пускане на игра. + Тази игра вече не е в библиотеката ти в DroidDeck. + Спри текущата сесия, преди да пуснеш игра в Steam. + Играта не можа да се изпрати в Steam. Опитай да я пуснеш отново. + Нов опит за премахване + Премахване + Премахването не е завършено + Експортиране на файл на играта + Синхронизиране на всички файлове на игрите в папка + Спиране на синхронизирането на файловете на игрите + Избор на папка за файловете на фронтенда на игрите + Файловете на игрите са запазени в %1$s + Файлове на фронтенда + + Изчакай операцията със средата да приключи + Изчакай инсталирането на инструмента за съвместимост да приключи + Изчакай инсталирането на работния плот да приключи + Изчакай инсталирането да приключи, преди да пуснеш игра. + Затвори сесията на Steam, за да играта не презапише импортираните файлове. + Импортиране в %1$s + + Импортиран %1$d файл от %2$s в %3$s + Импортирани %1$d файла от %2$s в %3$s + + + Импортиран %1$d файл от %2$s в %3$s. Старите запазвания са архивирани в Download/DroidDeck/Saves/backups + Импортирани %1$d файла от %2$s в %3$s. Старите запазвания са архивирани в Download/DroidDeck/Saves/backups + + Избор на ZIP файл със запазвания за %1$s + Експортиране на %1$s + + Експортиран %1$d файл като %2$s: %3$s + Експортирани %1$d файла като %2$s: %3$s + + Избор на папка за запазване на %1$s (%2$s) + %1$s се провали: %2$s + Все още няма логове от сесии: пусни сесия първо. + Няма логове от сесии за изчистване. + + Изчистена %1$d папка с логове от сесия. + Изчистени %1$d папки с логове от сесии. + + Чифтосването за безжично отстраняване на грешки се провали + Командата за безжично отстраняване на грешки се провали + Безжичното отстраняване на грешки не е достъпно + Командата за ADB е копирана + Избор на папката ROMs + Не е Adreno GPU + Turnip поддържа Adreno GPU-та. На %1$s Steam може да покаже черен екран. Изтегляне: %2$.0f MB. + Инсталиране въпреки това + Премахване на средата на Linux + Това изтрива средата, клиента на Steam в нея и всяка инсталирана в нея игра. + Приложено: %1$s + %1$s не можа да се отвори на основния екран + %1$s не можа да се отвори на %2$s + %1$s не можа да се отвори на екран %2$d + Импортиране + Размяна + Възстановяване + Отказване + Изтриване + не може да се прочете файлът + Импортирано: %1$s + Изчакващата размяна беше отказана. + Избор на пакет с компоненти (-linux .wcp) + Nightlies не можаха да бъдат достигнати + Запазено: %1$s + Изтеглянето се провали: %1$s + Избор на драйвер за средата на Linux (-Linux ZIP или пакет Android + Linux) + Избор на драйвер за дисплея (AdrenoTools ZIP или пакет Android + Linux) + Избор на ZIP с драйвер или пакет Android + Linux + %1$s: %2$s + Премахване на %1$s… + Забравяне на %1$s… + Избор на папката за съхранение на игри + Избор на ZIP с плъгин за Decky + Не може да се използва: %1$s + Програма + Премахване на средата на Linux + Настройките не можаха да се отворят на долния екран; отворени са на основния екран. + Отваряне на „Опции за програмисти“ в настройките + + Настройката на Android „Инсталиране на неизвестни приложения“ не можа да се отвори + Проверката не можа да се извърши: %1$s + Тази версия няма изтегляне за това копие на DroidDeck + Спри текущата сесия първо, после актуализирай + Изтегляне + Инсталиране + Изчакване на потвърждение от теб + Актуализацията се провали + Android не предложи инсталирането на актуализацията + + Инсталирането на Decky се провали: %1$s + Стартиране на импортирането на плъгина… + Импортирането на плъгина се провали: %1$s + Плъгинът е инсталиран. Рестартирай Steam, за да се зареди. + + Все още не е проверено - докосни опресняване, за да провериш за нови драйвери + Android + Linux: задава драйвера за дисплея заедно с него + Android + Linux: задава драйвера за средата заедно с него + вграденият в средата Turnip + системен Vulkan: няма вградена версия за този GPU + %1$s (включен) + Ръчно + %1$s · %2$s + В момента е публикувана само една от двете части + %1$.0f MB за двете + Няма драйвери за задаване: %1$s + Проверка за най-новите драйвери… + Актуално (проверката не успя: %1$s) + Препоръчаните драйвери не са публикувани в момента + Изтегляне на %1$s %2$s… + Актуализирано до %1$s %2$s. + Актуализирано до %1$s %2$s. Драйверът за дисплея се прилага след рестартиране на DroidDeck. + Превключено към %1$s %2$s. + Превключено към %1$s %2$s. Драйверът за дисплея се прилага след рестартиране на DroidDeck. + Използва се %1$s %2$s. + Използва се %1$s %2$s. Драйверът за дисплея се прилага след рестартиране на DroidDeck. + Не е ZIP с драйвер: нито драйвер на AdrenoTools, нито -Linux Turnip, нито пакет Android + Linux + Импортирането се провали: %1$s + Импортиран драйвер + Използва се %1$s като драйвер и за средата, и за дисплея. + Използва се %1$s като драйвер и за средата, и за дисплея. Драйверът за дисплея се прилага след рестартиране на DroidDeck. + Изтрито: %1$s (двата драйвера) + Изтрито: %1$s + Най-нови: %1$s · проверено %2$s + Най-нови: %1$s · %2$s недостъпни · проверено %3$s + + преди %1$d мин + преди %1$d мин + + + преди %1$d ч + преди %1$d ч + + + преди %1$d ден + преди %1$d дни + + Проверка на Banners-Turnip, WinNative и DroidDeck… + проверката се провали + Инсталирано: %1$s - избери го в който и да е от списъците, за да зададеш и двата драйвера + Инсталирано: %1$s - избери го от менюто + + Изчакай премахването на средата да приключи + Игра + тракпад + директно + Размяната се провали: %1$s + Запазването на профила се провали: %1$s + Работният плот е на пауза + %1$s е на пауза + Програмата е на пауза + Стартиране на работния плот + Стартиране на %1$s + Стартиране на програмата + Няма логове за тази сесия. + Архивът на логовете от сесията не можа да се създаде. + Логовете от сесията не можаха да се споделят. + Стартиране на сесията… + изтегляне на средата на Linux + изтегляне на средата на Linux · %1$d от %2$d MB + проверка на средата на Linux + разопаковане на средата на Linux + изтегляне на работния плот + изтегляне на работния плот · %1$d от %2$d MB + проверка на работния плот + разопаковане на работния плот + изтегляне на Proton Experimental (ARM64) + изтегляне на Proton Experimental (ARM64) · %1$d от %2$d MB + проверка на Proton Experimental (ARM64) + разопаковане на Proton Experimental (ARM64) + Каталогът на средата не можа да бъде достигнат. Провери връзката и натисни „Пускане“ отново. + Средата на Linux не се инсталира. Провери средата в „Настройка“ и натисни „Пускане“ отново. + Работният плот не се инсталира (%1$s). Провери връзката и натисни „Desktop“ отново. + %1$s спря неочаквано. Сподели логовете в отчет за грешка или опитай отново. + Програмата спря неочаквано. Сподели логовете в отчет за грешка или опитай отново. + Работният плот спря неочаквано. Сподели логовете в отчет за грешка или опитай отново. + Код на изход %1$d · %2$s + няма лог + + Логът показва %1$d грешка „Function not implemented“: ускорението на proot чрез seccomp се проваля при помощен процес на това устройство. Пробвай %2$s → „%3$s“ (или „%4$s“) и стартирай отново. + Логът показва %1$d грешки „Function not implemented“: ускорението на proot чрез seccomp се проваля при помощен процес на това устройство. Пробвай %2$s → „%3$s“ (или „%4$s“) и стартирай отново. + + + Плъгини + Инсталирай ZIP с плъгин за следващата сесия на Steam. + Изтегляне… + Инсталиране от ZIP + Игри + Аудио и видео + Графика + Интернет + Офис + Разработка + Образование + Наука + Система + Инструменти + Популярни + Тенденции + Нови + Актуализирани + зареждане… + Настройка на Flatpak + %1$s: %2$s + Flathub не можа да бъде проверен за актуализации + инсталирана + папка + Графит + Графитени повърхности, със собственото синьо на сферата от логото за всичко избрано, на фокус или натиснато. + Хартия на черно + Бялото е основният цвят, синьото от иконата е сигналът за линии, кръгове и точки. + Синя икона + Черен фон, бял текст, синьото за всичко избрано, на фокус или натиснато. + Яркосиньо + По-яркото синьо, върху повърхности, по-близки до тъмносиньо, вместо сиво. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Инсталирай средата първо + Библиотеката на Steam не е достъпна + Предишната SD библиотека не е достъпна + Хранилището за игри не е достъпно + Папката с добавени игри не е достъпна + Манифестите на Steam не са достъпни + Много системи + Вътрешна + Система на Linux + SD карта + Хранилище + Избор + Свойства + Добавяне към Любими + Премахване от Любими + Преименуване + Копиране + Изрязване + Действия + Любими + Закачане на текущата папка + Още няма любими - закачи папка от менюто ѝ ⋮, за да се връщаш тук бързо. + Премахване от любими + 0 байта + байта + Местоположение + Тип + Размер + Последна промяна + Папка + Файл + %1$s файл + Само за четене + Пречи на игрите (и Wine) да презаписват или изтриват този файл. + Статусът „Само за четене“ не можа да се промени + Скрит + Това хранилище не може да съхранява флага за скрит файл. + Отбелязва файла като скрит в Windows (атрибутът за скрити файлове на Wine). + Статусът „Скрит“ не можа да се промени за това хранилище + Готово + Изтриване... + Изтриването се провали + Преместване... + Копиране... + Преместване - %1$s + Копиране - %1$s + Преместване %1$d/%2$d - %3$s + Копиране %1$d/%2$d - %3$s + %1$d успешни, %2$d неуспешни + %1$d успешни, %2$d пропуснати + + %1$d елемент преместен + %1$d елемента преместени + + + %1$d елемент копиран + %1$d елемента копирани + + „%1$s“ вече съществува + Преименуване... + Преименуването се провали + Създаване на папка... + Папката не можа да се създаде + Нова папка + Име на папката + Създаване + Ново име + Изтриване? + Изтриване на „%1$s“ завинаги? + + Изтриване на %1$d елемент? + Изтриване на %1$d елемента? + + Това не може да бъде отменено. + + …и още %1$d + …и още %1$d + + Изтриване %1$d/%2$d - %3$s + + %1$d не можа да бъде изтрит + %1$d не можаха да бъдат изтрити + + Сливането добавя и заменя файлове в съществуващата папка. + Презаписването заменя съществуващия файл. + Приложи за всички конфликти + Сливане + Презаписване + Запазване на двете + Пропускане + Избор на тази папка + Назад + %1$s е монтирано, но в момента не може да се прочете + Изглед като списък + Изглед като мрежа + Търсене + Сортиране + Име + Дата на промяна + Удобни редове + Компактни редове + Скриване на скритите файлове + Показване на скритите файлове + Скриване на любимите + Показване на любимите + Филтриране на тази папка + %1$s свободни + %1$d избрани + Нищо + Всички + Поставяне + + %1$d елемент + %1$d елемента + + Поставяне на %1$s тук + Поставяне на %1$s (премести) тук + Файлове + ХРАНИЛИЩЕ + БЪРЗИ + ЛЮБИМИ + Изтеглени + Изтегл. + ROMs + Логове от сесии + Снимки + „%1$s“ е добавена към Любими + „%1$s“ е премахната от Любими + Празна папка + Едно ниво нагоре + Избор на папка + Избор на файл + Синьо на Steam + Небесно + Бяло + Виолетово + Зелено + Кехлибарено + Червено + Розово + Бутон A + Бутон B + Бутон X + Бутон Y + Ляв бутон (LB) + Десен бутон (RB) + Ляв спусък (LT) + Десен спусък (RT) + Бутон View + Бутон Menu + View + Menu + D-pad нагоре + D-pad надолу + D-pad наляво + D-pad надясно + Скрит + Плъзни, за да преместиш + Прилепване + Запазване + Нищо + Клавиатура + тракпад + Терминал + Захвати + тракпади на Deck + + Премахване на средата на Linux + + Премахване на средата на Linux · %1$d запис + Премахване на средата на Linux · %1$d записа + + Средата на Linux е премахната + Премахването на средата на Linux не можа да завърши. Опитай отново от „Настройка“. + Премахване на остатъците от средата + Изтегляне + Проверка + Разопаковане + Показва се, докато средата на Linux се инсталира или премахва + Инсталиране на средата на Linux + Премахване на средата на Linux + + Средата на Linux не е инсталирана + Каталогът няма контролна сума за %1$s + Несъответствие в контролната сума - нищо не е променено + Инсталиране на %1$s + AppImage не можа да бъде поставен + Разопаковането се провали + Инсталирането се провали + + Настройката на Flatpak се провали (изход %1$d) + Настройката на Flatpak не завърши + Друга задача на магазина вече се изпълнява + Настрой Flatpak първо + Стартиране + Flatpak приключи със код %1$d + Инсталиране на %1$s + Актуализиране на %1$s + Премахване на %1$s + Инсталиране на %1$s (%2$d/%3$d) + Актуализиране на %1$s (%2$d/%3$d) + Премахване на %1$s (%2$d/%3$d) + преводи + драйвери за графика + медийни кодеци + средата %1$s + средата %1$s %2$s + + Файлът е твърде малък, за да бъде AppImage + Файлът не може да бъде прочетен: %1$s + Това не е AppImage (не е Linux програма) + Този AppImage не е компилиран нито за ARM64 (aarch64), нито за x86 компютри + Това е Linux програма, но не е AppImage + Могат да се импортират само AppImage от тип 2 (този е от тип %1$d) + Няма достатъчно свободно място: импортирането изисква около %1$s + Няма достатъчно свободно място: актуализирането изисква около %1$s + Импортирането се провали + Този AppImage вече не съществува + Актуализирането се провали + Разопаковане на %1$s + AppImage не можа да бъде разопакован + AppImage не можа да бъде разопакован: %1$s + + Спри активната сесия, преди да инсталираш Decky Loader + Изтеглянето на Decky Loader се провали + Размерът на Decky Loader не съответства на метаданните на версията + Контролната сума на версията не можа да бъде изтеглена + Липсва контролна сума на версията + Контролната сума на версията е невалидна + Несъответствие в контролната сума на Decky Loader + Файлът на версията не е съвместим 64-битов изпълним файл за Linux + По време на изтеглянето започна сесия - спри я, преди да инсталираш Decky Loader + PluginLoader не можа да бъде означен като изпълним + Отстраняването на грешки от разстояние на Steam CEF не можа да бъде включено + PluginLoader не можа да бъде подготвен + Съществуващият PluginLoader не можа да бъде запазен + Новият PluginLoader не можа да бъде активиран + Спри активната сесия, преди да инсталираш плъгин + Инсталирай Decky Loader, преди да импортираш плъгин + Избраният ZIP файл не можа да бъде прочетен + Директорията с плъгини на Decky не можа да бъде създадена + Разопаковане на ZIP на плъгина + ZIP на плъгина трябва да съдържа една папка с плъгина на най-горно ниво + В ZIP на плъгина липсва plugin.json, package.json или dist/index.js + Липсва име на плъгина + По време на инсталацията започна сесия - спри я, преди да инсталираш плъгин + Плъгинът е инсталиран · рестартирай Steam, за да се зареди + Инсталирането на ZIP на плъгина се провали + ZIP на плъгина съдържа твърде много записи + ZIP на плъгина съдържа невалиден път + ZIP на плъгина съдържа повтарящи се пътища + ZIP на плъгина съдържа опасен път + Папката на плъгина не можа да бъде създадена + ZIP на плъгина надвишава ограничението от 1 GiB при разопаковане + Вграденият изпълним файл на плъгина не можа да бъде означен като изпълним + Невалиден запис remote_binary + Невалидно име на отдалечен изпълним файл + URL адресът на отдалечения изпълним файл трябва да използва HTTPS + Отдалеченият изпълним файл %1$s има невалиден SHA-256 хеш + Директорията bin на плъгина не можа да бъде създадена + Изтегляне на изпълним файл на плъгина · %1$s + Изпълнимият файл на плъгина %1$s не можа да бъде изтеглен + Несъответствие в контролната сума на изпълнимия файл на плъгина · %1$s + Изпълнимият файл на плъгина не можа да бъде означен като изпълним · %1$s + Изпълнимият файл на плъгина не можа да бъде заменен · %1$s + Изпълнимият файл на плъгина не можа да бъде инсталиран · %1$s + Папката на плъгина принадлежи на друг плъгин + Съществуващият плъгин не можа да бъде запазен + Импортираният плъгин не можа да бъде активиран + Настройките на плъгина на Decky не можаха да бъдат обновени + Съществуващият плъгин не можа да бъде възстановен + + Идентичността на безжичното отстраняване на грешки не можа да бъде запазена + Командата на ADB се провали (%1$d) + Android не потвърди промяната на ограничението за дъщерни процеси: %1$s + Няма запазено чифтосване за безжично отстраняване на грешки + Въведи текущия IP адрес и порт за безжично отстраняване на грешки + + Изтече времето за изчакване на изскачащия прозорец за чифтосване. Опитай отново от DroidDeck. + Не можа да се търси изскачащият прозорец за чифтосване (%1$d). Проверете дали Wi-Fi е включен. + Не можа да се търси изскачащият прозорец за чифтосване: %1$s + Въведи всичките 6 цифри на кода + Чифтосване… + Този код не работи. Отвори отново изскачащия прозорец за чифтосване и въведи новия му код. + Чифтосано. Свързване… + Чифтосано, но портът за безжично отстраняване на грешки не беше намерен. Остави безжичното отстраняване на грешки включено и опитай отново. + Прилагане на настройката… + Командата за безжично отстраняване на грешки се провали + Настройка на безжично отстраняване на грешки + Пита за кода за чифтосване за безжично отстраняване на грешки, докато Настройки е отворено + Чифтосване на безжично отстраняване на грешки + Включи безжичното отстраняване на грешки, после докосни „Чифтосване на устройство с код за чифтосване“. + Включи безжичното отстраняване на грешки, после докосни „Чифтосване на устройство с код за чифтосване“. Остани в Настройки: кодът ще се появи в това известие. + 6-цифрен код за чифтосване + Докосни „Въвеждане на код“ и напиши 6-те цифри, показани в Настройки. + Въведи кода за чифтосване по Wi-Fi + Опитай кода отново + Въвеждане на код + Настройка на Steam + Steam е готов + Докосни, за да се върнеш в DroidDeck. Вече можеш да изключиш безжичното отстраняване на грешки. + Настройката на безжичното отстраняване на грешки спря + + Изтеглянето отговори с HTTP %1$d + Размерът на изтеглянето не съответстваше на публикуваното APK и беше отхвърлен - опитай отново + Изтеглянето не съответстваше на контролната си сума и беше отхвърлено - опитай отново + Актуализацията е за Android пакета %1$s, а не за %2$s + Метаданните на актуализацията не посочват ключа за подписване на версиите на DroidDeck + Android не можа да прочете изтегленото APK + Изтегленото APK е за %1$s, а не за %2$s + Изтегленото APK сочи versionCode %1$d, а не публикуваното %2$d + Android няма да инсталира versionCode %1$d върху инсталираното %2$d + Изтегленото APK няма информация за подпис + Изтегленото APK не е подписано с ключа за версии на DroidDeck + Каталогът с актуализации отговори с HTTP %1$d + + Стартиране на сесията… + %1$d:%2$02d изминали · все още работи + Изтегляне на актуализацията на клиента на Steam · %1$d%% + Изтегляне на клиента на Steam · %1$s + + Вече тече инсталация на инструмент за съвместимост + Спри активната сесия, преди да инсталираш инструменти за съвместимост + Не беше намерена ARM64 версия на %1$s + За инсталирането на %1$s са необходими поне 4 GB свободно място плюс размера на изтеглянето + Изтегляне на %1$s %2$s + Повторен опит за изтегляне · опит %1$d от %2$d + Изтеглянето е прекъснато + Изтеглянето се провали; частичният файл е запазен, за да може да се продължи при нов опит + Изтеглянето надхвърли размера на версията и беше изтрито + Изтеглянето беше непълно; частичният файл е запазен за продължаем повторен опит + Проверка на изтеглянето + Контролната сума на версията не можа да бъде прочетена; опитай отново + %1$s %2$s не публикува контролна сума; нищо не беше инсталирано + Несъответствие в контролната сума; изтеглянето беше изтрито + По време на изтеглянето започна сесия - спри я, преди да инсталираш инструменти за съвместимост + Инсталиране на %1$s + Инсталирането на %1$s се провали (код %2$d) + %1$s не можа да бъде инсталиран + Извличане на пакета droiddeck-esync + + с помощни програми за Linux + Само DLL файлове + Оригинал (Proton е обновен след последната замяна) + Променено извън Компоненти + не е налично + Оригинал %1$s + Оригинал от %1$s + %1$s %2$s ще бъде поставен в %3$s, когато играта, която го използва, се затвори. + %1$s %2$s вече е в %3$s. Прилага се при следващото стартиране на игра. + Този оригинален пакет не е запазен + %1$s на %2$s ще се върне към оригинала си, когато играта, която го използва, се затвори. + %1$s на %2$s е възстановен от оригинала си %3$s. + %1$s се използва в %2$s. Първо замени този Proton с нещо друго. + %1$s не е запазен + %1$s е изтрит. + Оригиналът на инсталираната версия е запазен: той е пътят назад. + Не е запазен + Изтрит е оригиналът %1$s на %2$s. + Изтеглянията идват само от версиите Nightlies + Този списък с пакети е отпреди въвеждането на контролни суми - обнови го и опитай отново + Несъответствие в контролната сума + + Линейно + Най-близко + Snapdragon GSR (качество) + Само изостряне + Автоматично (съответства на мащабирането) + + Нищо не е приложено. + Яснота на играта + Изостряне с лек контраст - за всеки ден. + Наситен + Силно наситен цвят. Deband е включен, за да не се набраздява небето. + Кино + По-дълбок контраст, по-хладен цвят, заглаждане на ръбовете. + Състезателен + Повдигнати сенки и силно изостряне - за да забележиш хората първи. + Адаптивно изостряне + Само изостряне, цветът остава непроменен. + Филмов + Крива като на филм, леко притъпена. + Аркаден + Силен и ефектен. + Ретро CRT + Ефектът на CRT тръбата и аналоговия сигнал заедно. + Остро мащабиране + За работа под разделителната способност на екрана. + Чисти пиксели + Ясни пиксели за 2D и стари игри. + Аниме контури + Контури като в анимация плюс изостряне. + + Не е GPU Adreno + Поддържан + Експериментален: драйверите му са тестови версии + Под тестваните устройства (Adreno 725 и по-нови): може да не работи + Не се поддържа: DroidDeck изисква GPU Adreno (Snapdragon) + + Достигнато е ограничението на GitHub за заявки - опитай отново по-късно + GitHub отговори с HTTP %1$d + изтеглянето отговори с HTTP %1$d + Този списък с драйвери е отпреди въвеждането на контролни суми - обнови го и опитай отново + Несъответствие в контролната сума - изтеглянето беше отхвърлено + Не е драйвер за AdrenoTools: %1$s + няма meta.json + meta.json не може да се прочете (%1$s) + това е драйвер за средата на Linux (импортирай го от \"Драйвер за средата на Linux\") + това е драйвер за игри за Wayland, който това приложение не може да използва + meta.json не посочва libraryName, затова не може да се предаде нищо на AdrenoTools + meta.json посочва %1$s, който не е в zip файла + %1$s не е 64-битова AArch64 ELF споделена библиотека + %1$s се свързва с glibc - това е Turnip \"-Linux\", който принадлежи към \"Драйвер за средата на Linux\" + В този zip файл няма libvulkan_freedreno*.so. Zip на Turnip за Android (AdrenoTools) или -Wayland не е драйвер за средата на Linux. + %1$s не е 64-битова AArch64 ELF споделена библиотека. + %1$s не е glibc драйвер - той се свързва с libc на Android. Средата на Linux изисква zip \"-Linux\"; обикновен Turnip или \"-Wayland\" не може да се зареди от клиента на Steam. + Не е пакет с драйвери: няма manifest.json, посочващ драйвер за Android и за Linux + Не е пакет с драйвери: %1$s сочи извън него + Не е пакет с драйвери: при разопаковане надхвърля %1$d MB + + SD карта + Папка + + Споделяне на логовете от сесията + + изисква Android 10 или по-нова версия (слоеве на екрана с цветово пространство) + този екран не отчита поддръжка на HDR + този екран поддържа %1$s, не HDR10 + + този GPU + Ограничението за дъщерни процеси е включено + Ограничението за дъщерни процеси не е зададено + Ограничението за дъщерни процеси не можа да бъде проверено + Ограничението за дъщерни процеси е изключено + Ограничението за дъщерни процеси не е необходимо + %1$s Ако това не е възможно, свържи устройството с компютър с ADB и изпълни командата отдолу. Върни се тук и провери отново. + Сесиите на Steam могат да стартират. + Тази версия на Android не използва това ограничение. + Включи „Disable child process restrictions“ в Опции за програмисти и остави Опции за програмисти включени след това. + Тази версия на Android няма превключвател за това. Използвай безжично отстраняване на грешки, за да го промениш от това устройство. + Тази версия няма изтегляне за това копие на DroidDeck. + Android няма да я инсталира върху тази версия, защото нейният versionCode %1$d е по-нисък от инсталираното %2$d. + Android няма да инсталира по-стара версия върху по-нова. Остани на тази версия, докато каналът я настигне. + Това копие на DroidDeck е подписано по различен начин (локална версия?), затова Android не ще го актуализира на място. + Няма достатъчно свободно място за актуализацията. + Актуализацията беше прекратена. + Актуализацията не се инсталира: %1$s + Актуализацията не се инсталира. + ZIP на %1$s + zip + %1$s все още не е стартирана + Не са намерени файлове със записи за %1$s + %1$s няма папка drive_c, затова не е zip със записи от GameHub или Winlator + не могат да се създават папки в %1$s + не може да се пише в %1$s + не може да се пише в %1$s (%2$s) + Папката не е достъпна + Друг файл вече използва %1$s + Папката с визуални материали не е достъпна + Папката има несвързан инвентар за синхронизация + %1$s не можа да бъде премахнат + Файлът излиза извън папката за експортиране + FEX (версия без име) + няма profile.json в %1$s + %1$s не е пакет с компоненти за Linux (тип %2$s) + %1$s · %2$s свободни + Автоматично + Каталогът с актуализации не премина проверките (%1$s) + Настройка на DroidDeck за дъщерни процеси + Вписан · офлайн режим + Вписан + ВГРАДЕН + ИЗТЕГЛЕН + ИМПОРТИРАН + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Екран на сесията + Разделителна способност + Напасване на игра 16:9 + Как игрите с 16:9 се напасват на този екран. При следващата сесия. + Избери „Напасване на екрана“ или собствен размер, по-тесен от 16:9. + Запазване на пропорциите + Разтягане до екрана + Мащабиране на изображението + Филтър за мащабиране + Общ за Steam и Desktop. + Прилага се веднага. Общ за Steam и Desktop. + FSR, GSR и NIS се прилагат при уголемяване на изображението на сесията. „Само изостряне“ работи и без уголемяване. + Острота на филтъра + Честота на кадрите + %1$s Задава мащабиране на %2$s. + Напасване на екрана (%1$d×%2$d) + Следване на екрана (преоразмерява при сгъване или разгъване) + Steam + Покажи данните за устройството + Скрий данните за устройството + Прочитане на устройството… + Копирай + Копирай тези данни в доклад за грешка. + Копирано. + Принудително SSBS + Поддържа настройка за скоростта на процесора, която Wine непрекъснато изключва. Може да ускори Windows игрите на Snapdragon 8 Elite; при по-стари чипове промяната е малка. Прилага се при следващото стартиране на игра. + + Магазини + Steam + Персонализирана + Магазини + Отваряне на магазин на + Скорост на изтегляне + Ядро на магазините + ● Ядрото е готово + Не е компилирано + Балансирано + Бързо + Максимално + Магазини + Настройки на магазините + Настройки + (не е вписан) + Магазин + Инсталирани + Библиотека + Всички + Изтегляния + %1$s (%2$d) + Вход + %1$s · изход + Изход от %1$s? + Инсталираните игри остават. Библиотеката се изтегля отново, когато се впишеш пак. + Остани вписан + Изход + Търсене в каталога на %1$s… + Търсене в инсталираните игри… + Търсене в твоята библиотека… + Търсене + Нищо инсталирано + Зареждане + Няма игри + Зареждане на магазина… + Няма каталог + Новости + Промоции + Безплатни игри + Актуални + Твоята библиотека + Играй + Инсталирай + Продължи инсталацията + Прекъснато + Инсталация · %1$s + Вземи безплатно + Виж в %1$s + Купи · %1$s + Инсталирана + В библиотеката + Безплатна + Продължи + Обнови + Показвай съдържание за възрастни + Epic + EOS + Офлайн + Изключено + Вход в Epic (EOS) + Стартирай офлайн + Разреши входа в Epic + Облачни запазвания + Качи в облака + Изтегли от облака + Няма облачни запазвания + Конфликт в облака + Конфликт в облака · %1$d файла + … + Запази облачната + Запази локалната + Проверка на облака… + Последна синхронизация %1$s + Още не е синхронизирано + %1$d файла + инсталирана + в библиотеката ти + магазин + Постижения + %1$d от %2$d отключени + %1$d / %2$d + Отключени · %1$d + Заключени · %1$d + Скрити · %1$d + Скрито постижение + Подробностите се показват след отключването му. + Всички постижения са отключени + Зареждане… + Влезте в %1$s, за да видите постиженията си. + Неуспешна връзка с %1$s. Проверете отново, когато сте онлайн. + %1$s не показва постижения за тази игра. + Покажи всички %1$d + Постижения · %1$s + ✓ В Steam + Деинсталирай + Деинсталиране… + Деинсталиране на %1$s? + Освобождава %1$s и я премахва от Steam. + Премахва файловете ѝ и пряката ѝ пътека в Steam. + Steam ще засече играта при следващото стартиране; стартирай я оттам или отвори Steam сега. + Proton + Избор на Steam за всяка игра + Звук + DirectAudio + Компоненти + FEX, DXVK, VKD3D + Ядро + %1$s · собствено + %1$s · вградено + Как се стартира + Изтеглена от + %1$s, с твоя профил + Инсталирана в + Хранилище за игри (Games/%1$s/%2$s) + Стартирана от + Steam и Proton, по същия начин както игрите от Steam + Steam и Proton · вход в Epic при всяко стартиране, оверлеят на EOS е изключен + Steam и Proton · зададени са променливите на Amazon Games Services + Steam + добавена в Steam при инсталацията (Deck UI + Steam Input) + Няма изтегляния + На пауза + На опашка + На опашка · №%1$d + Отказано + Неуспешно + Неуспешно · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Пауза + Продължи + Откажи + Опитай отново + Изчисти + Изчисти завършените + Манифест + Изтегляне + Проверка + Инсталация + Готово + Проверка + Изтегляне + Изтриване на изтеглянето? + Неуспешно · %1$s е запазен + Проверка + Инсталиране + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d файла + Диспечер на изтеглянията + Изтегляния едновременно + Инсталирай в + Инсталация на %1$s: + Вътрешна памет + %1$s свободни + Изтегляния от магазина + Показва напредъка на изтеглянето на игри и ги държи активни на заден фон + Изтегляне + %1$d изтегляния + Подготовка… + Завършени изтегляния от магазина + %1$s е инсталирана + Изтеглянето на %1$s се провали + Изтрий + Запази + Вход в %1$s + Вход с твоя доставчик + Презареди + Затвори + Опитай отново + Завършване на входа… + Страницата за вход не можа да се зареди: %1$s + Този доставчик отказва вход от вградена страница. Впиши се с паролата си за магазина. + Страницата за вход отговори с грешка (HTTP %1$d). + Връзката със страницата за вход не е защитена. + Входът не можа да бъде потвърден. Опитай отново. + Входът се провали. Опитай отново. + Затвори + Назад + Редактирай + Добави всички игри от тази папка + Нищо ново в тази папка + Име + Цел + Старт в + Настройки за стартиране + Визия + Друг exe… + Премахни + Обложка + Фон + Лого + Икона + Папка на играта + Steam + SteamGridDB + Твой файл + От exe файла + Автоматично + Избери изображение… + Избери изображение: %1$s + Връщане към автоматично + + %1$d игра добавена + %1$d игри добавени + + + %1$d вече в Игри + %1$d вече в Игри + + B · Назад към %1$s + играта + Настройки за игра + Графични драйвери + Този Proton + Всеки Proton + За всеки Proton + Налични · %1$d + Проверено %1$s + Проверка за нови компоненти и драйвери + Вземи + %1$s · DXVK, VKD3D-Proton и FEX по-долу са на този Proton + %1$s · тестван + %1$s · нетестван + %1$s · неподдържан + Поддържа препоръчаната двойка актуална + Автоматично · %1$s + Драйверите за среда и екран поотделно + Използвай %1$s? + Премахване на %1$s? + Диагностика + Готово за игра + Още не е готово за игра + среда %1$s + ограничението за процеси е изключено + вписан като %1$s + Когато нещо изисква действие + Визия + Това устройство + Визия на библиотеката + ПРИЛОЖЕНИЯ ЗА LINUX + Flathub + Приложения за десктоп на Linux, отваряни от Desktop. + Настрой Flathub + Премахване на %1$s? + Настройките и файловете му се запазват. + Запази + Размер на диска + Деинсталиране на десктопа? + Всички + Постижения в GOG + Синхронизира постиженията за поддържаните игри от GOG. Влиза в сила при следващото стартиране. + Постиженията са изключени + Постиженията са включени + Влезте в GOG отново + Постиженията са качени + Качването на постижение е в изчакване + Включи постиженията на GOG в настройките на магазините + Постиженията не са достъпни; проверете логовете на сесията + Постижение отключено + Поставяне + Кръстат бутон + Ляв стик + Десен стик + Меню + Клавиатура + Кръстат бутон нагоре + Кръстат бутон надолу + Кръстат бутон наляво + Кръстат бутон надясно + Сензорно управление + Сензорен контролер + Мишка + Вкл. + Изкл. + Бавна + Нормална + Бърза + Редактиране на разположението + Меню на DroidDeck + Плъзнете елементите за управление върху екрана + Разположи + Отхвърли + Запазване… + Икона + Ш− + Ш+ + В− + В+ + Скрий + Цвят и прозрачност… + Копиране на това разположение + Разположението е копирано; поставете го в сензорното разположение на всяка игра + Поставяне на копираното разположение + Връщане към стандартното на Steam + Този елемент няма зададено действие в Steam, за да му се даде икона + Икона за %1$s + Цвят на сензорното разположение + Готово + Сензорното разположение не можа да се запази + Сензорното разположение е запазено в Steam + Приложение %1$d + Етикет (незадължително) + Иконите на Steam не бяха намерени + + %1$d икона от Steam + %1$d икони от Steam + + Цвят на иконата + Цвят на бутона + Без икона + + Проверка на вписването… + Извличане на библиотеката ви… + Извличане на списъка с игри… + Зареждане на подробности за игрите… (%1$d/%2$d) + + Синхронизиране на %1$d игра… + Синхронизиране на %1$d игри… + + Синхронизиране… %1$d/%2$d + Извличане на обложки… + Синхронизирането е неуспешно + В библиотеката ви в %1$s не бяха намерени игри + Amazon Games не отговори за библиотеката ви: обновете или влезте отново + Неуспешно свързване с GOG + Библиотеката ви в GOG не можа да се прочете + Регистриране в Steam… + Cardboard VR + Режим Cardboard + Изображения за лявото и дясното око за VR очила за телефон. + Картина + Виртуален екран + 3D един до друг + Очила + Cardboard + Cardboard (2014) + Без леща + Разстояние между очите + Ширина на екрана + Разстояние до екрана + Проследяване на главата + Това устройство няма сензор за въртене. + Центриране на изгледа + Центриране + SteamVR (експериментално) + Инсталиране + Изтегля SteamVR за ARM64 на Valve (571 MB). + Изтегляне и разархивиране на SteamVR… + Стартиране + Стартира SteamVR с главата ви като шлем; картината превключва на 3D един до друг. + Спиране + SteamVR работи. + Не е налично в тази сесия. + SteamVR не отговори; проверете лога на сесията. + Изтегляне на компоненти + Изтеглянията са общи със страницата Компоненти. Изберете по-горе изтеглена версия, за да се използва при следващото стартиране на играта. + Изберете версия за изтегляне + Разработчик + Издател + Издадена + Основна + + Допълнително + Пълно + %1$s ч + %1$d мин + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml new file mode 100644 index 000000000..545774482 --- /dev/null +++ b/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,2285 @@ + + + DroidDeck + + První spuštění stáhne Steam. Může to trvat několik minut. + Ve výchozím nastavení se po opuštění aplikace Steam pozastaví; po návratu pokračuje, kde skončil. + Váš ovladač se zobrazí jako ovladač Xbox 360. Jinak použijte ovládání na obrazovce. + + + První spuštění připraví Plochu. Může to trvat několik minut. + Ve výchozím nastavení se po opuštění aplikace relace pozastaví; po návratu pokračuje, kde skončila. + Dotyk funguje jako touchpad: tažením posouváte kurzor, dotykem klikáte. + Váš ovladač se zobrazí jako ovladač Xbox 360. Jinak použijte ovládání na obrazovce. + + Relace + Zobrazuje se, když je otevřená relace Steamu nebo Plochy + Steam běží + Probíhá stahování Steamu + Relace pozastavena – klepnutím na Obnovit pokračujte + Obnovit + Pozastavit teď + Ukončit relaci + Generování snímků + Vyberte režim generování snímků + Vypnuto + Win-FG %1$d× + LSFG %1$d× + LSFG Adaptive %1$d + Vyžaduje Lossless Scaling na Steamu + Nainstalujte Lossless Scaling ve Steamu nebo naimportujte Lossless.dll + Importovat Lossless.dll… + Používá se kopie ze Steamu + Používá se importovaná kopie + Vyberte Lossless.dll + Lossless.dll byl importován + Tento Lossless.dll se už používá + Tento účet Steamu nevlastní Lossless Scaling + Tento soubor není Lossless.dll aplikace Lossless Scaling + Z tohoto souboru Lossless.dll se nepodařilo připravit LSFG + LSFG vyžaduje Lossless Scaling ze Steamu nebo importovaný Lossless.dll + LSFG se nepodařilo připravit shadery z Lossless.dll + %1$s: nepodporováno + %1$s: chyba enginu + %1$s: vyžaduje Lossless Scaling + %1$s %2$s · spouští se + Přidá knihovnu Steamu a prohledá ji na hry pro Windows. Vlastněné hry použijí své položky ze Steamu; ostatní se stanou zástupci mimo Steam. + Prostředí hry + Proměnné prostředí pro hry na Protonu. + Upravit + Všechny hry na Protonu + Profil + ID aplikace Steam + Otevřít + Hra %1$s + Přidat proměnnou + Název proměnné + Hodnota + Uložit + Odebrat + Znovu zdědit + Vymazat přepsání prostředí + Zrušit pro tento profil + Nejsou nastaveny žádné proměnné. + Zděděno + Vlastní proměnná + Nepodařilo se načíst ani uložit. Zkontrolujte volné úložiště a zkuste to znovu. + Použijte platný název a hodnotu o délce až 8192 znaků. + Standardní běhové prostředí Protonu tuto proměnnou nečte. + Načítání… + Přepsání úrovně funkcí D3D12. 12_2 oznamuje schopnosti DirectX 12 Ultimate; nepodporované funkce mohou selhat. Nechte nenastavené pro automatickou detekci. + Přepsání shader modelu D3D12. 6_9 vyžaduje dostatečně novou verzi VKD3D a odpovídající funkce GPU. Nechte nenastavené pro automatickou detekci. + Ponechte zapnuté ukládání shaderů Mesa do mezipaměti, aby se omezilo opakované kompilování. false ukládání do mezipaměti zapíná. + Maximální velikost mezipaměti shaderů Mesa, například 1G. Větší mezipaměť zabírá více úložiště. + Možnosti VKD3D pro D3D12. nodxr vypíná ray tracing; použijte jen pro hru, která to vyžaduje. + Nastavte na 1 pro diagnostický log Protonu. Protokolování může snížit výkon a zabírat úložiště. + Nastavte na 1 pro použití WineD3D založeného na OpenGL místo DXVK, pro testování kompatibility D3D9–11. + Nastavte na 1, aby tato hra nepoužívala ntsync (droiddeck-ntsync nebo ovladač jádra); použije droiddeck-esync, pokud existuje balíček, jinak droiddeck-fsync. + Nastavte na 1 pro vypnutí droiddeck-esync u této hry; místo něj použije droiddeck-fsync, nebo droiddeck-ntsync, pokud je vypnutý i fsync. + Nastavte na 1, aby tato hra nepoužívala droiddeck-fsync; použije droiddeck-esync, pokud existuje balíček, jinak droiddeck-ntsync. + Nastavte na 1 pro spuštění této hry na droiddeck-fsync, nebo na 0, aby měl droiddeck-esync přednost, pokud balíček odpovídá jejímu Protonu. + Nastavte na 0 pro vypnutí pomocníka Protonu pro navigaci gamepadem u této hry. + Překryvná vrstva DXVK pro D3D9–11. fps zobrazuje snímkovou frekvenci; bez nastavení se skryje. + Nastavení DXVK oddělená středníky. Novější verze používají pro omezení snímků dxvk.maxFrameRate; pro běžné hraní dejte přednost omezovači snímků relace. + Omezení snímkové frekvence D3D12. 0 omezení vypíná. + Vícevláknové OpenGL Mesa. true může pomoci hrám OpenGL omezeným CPU; vyzkoušejte u každé hry zvlášť. + Synchronizace Wine + Jak hry synchronizují svá vlákna. Použije se od příští relace. + esync + ntsync + fsync + wineserver + Předvolba FEX + Použije se při dalším spuštění hry. + Hry + Výchozí FEX + Používá výchozí nastavení FEX. + Stabilita + Nejbezpečnější, nejpomalejší nastavení. + Kompatibilita + Plné TSO s multiblokem. + Střední + Vyvážené TSO se sníženou přesností x87. + Výkon + Veškeré TSO vypnuto; multiblok. + Výkon + TSO + Nastavení výkonu se zapnutým hlavním TSO. + Extrémní + Vypíná kontroly samo-modifikujícího se kódu. Hry s JIT a DRM mohou přestat fungovat. + Extrémní + TSO + Extrémní nastavení se zapnutým hlavním TSO. + Extrémní-gn + Výkon s nápovědami TSC a metadat. Zkuste před Extrémním. + Denuvo + Výkon s plnými kontrolami kódu a skrytým bitem hypervizoru. + Vlastní hodnota + Vybrat hodnotu + Emulátory a aplikace + Přidat + Vaše vlastní aplikace nebo hra + Skript + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d %% + Spouští se… + %1$s přidáno + %1$s odebráno + Plocha + Tato aplikace zmizela. Vyberte ji znovu z Plochy. + Otevřít %1$s + Odebrat + Pro odebrání stiskněte znovu + Pro odebrání ukončete relaci + Spouští %1$s ze své složky, propojené do relace tam, kde se nachází. Odebráním aplikace zůstane tato složka nedotčena. + Spouští %1$s z kopie své složky uvnitř DroidDeck. Odebráním aplikace se smaže kopie, ne originál. + Nejprve nainstalujte běhové prostředí Linuxu + Soubor zmizel + Nedostatek volného místa: zkopírování složky vyžaduje %1$s + Kopíruje se %1$s + Přidání aplikace se nezdařilo + Hledá se vydání %1$s + Nepodařilo se načíst vydání %1$s + Žádné vydání %1$s neobsahuje AppImage pro ARM64 ani x86_64 + Stahuje se %1$s + Stahování se nezdařilo + Aktualizace aplikace se nezdařila + Tuto ikonu se nepodařilo načíst + %1$s uloženo + Upravit + Zkontrolovat aktualizace + Kontrola aktualizací… + Aktualizovat na %1$s + Aktuální · %1$s + %1$s aktualizováno + %1$s · %2$s + Přidat aplikaci + Skript + AppImage + GitHub + Flatpak + Soubor .sh. Spouští se ze své vlastní složky, na celou obrazovku pod gamescope. + AppImage pro ARM64 (aarch64), nebo pro x86_64, který běží přes FEX. Jednou se rozbalí do běhového prostředí Linuxu. + Nainstaluje AppImage pro ARM64 z nejnovějšího vydání repozitáře, nebo jeho verzi pro x86_64 přes FEX. + Použít FEX + Automaticky + Zapnuto + Vypnuto + FEX spouští programy pro x86 Linux na tomto zařízení ARM64. Automaticky ho použije, když je program sestaven pro x86. + %1$s přes FEX + Tento program je sestaven pro PC %1$s a běží přes FEX, který ještě není nastaven. Jednou spusťte přihlášený Steam: DroidDeck tím stáhne FEX a Steam Linux Runtime, který potřebuje, a pak se tento program otevře. + Tento program + %1$s je sestaven pro PC x86 a běží přes FEX, který ještě není nainstalován. Jednou spusťte přihlášený Steam a nechte ho stáhnout FEX a Steam Linux Runtime, pak to spusťte znovu. + ID aplikace Flathub nebo její stránka na Flathub. Flatpak se nejprve nastaví, pokud ještě není. + Soubor + Nevybrán žádný soubor + Vybrat soubor + Změnit + Vybrat skript + Vybrat AppImage + Vybrat ikonu + Repozitář + owner/repo nebo odkaz na github.com + Není to repozitář GitHubu + ID aplikace + org.ppsspp.PPSSPP + Není to ID aplikace Flathub + Nebo soubor .flatpak pro aplikaci, která není na Flathub. + Vybrat soubor .flatpak + Zdroj + Na Ploše + Nová aplikace + Název + Z aplikace + Ikona + Výchozí ikona + Vybrat ikonu + Vymazat + Z repozitáře + Vyhledávání ikon… + Výchozí + Upravit %1$s + Uložit + Kontrola složky… + Její složka je propojena do relace: %1$s + Její složka obsahuje programy pro Linux, které sdílené úložiště nemůže spustit, proto se zkopíruje do DroidDeck (%1$s). + Přidat + Zrušit + Nejprve nainstalujte běhové prostředí Linuxu v Nastavení. + Počkejte, až %1$s skončí. + Kompatibilní + Sestavení označená „%1$s“ v nabídce Kompatibilita Steamu mají opravy prefixu od DroidDeck a hry z nich zde spustíte. Výchozí je Proton Experimental ARM64 (%1$s), nebo Proton 11.0 ARM64 (%1$s), jakmile se nainstaluje tato novější verze. Vlastní položky Protonu od Valve bez tohoto označení nemohou na tomto zařízení spustit hru. + Nainstalováno %1$s · %2$s + Zrušit + Automaticky + Nikdy + Vždy + Odinstalovat + Zkontrolovat + Aktualizovat + Kontrola… + Zpracovává se… + Zapomenout + Přidat… + Použít + Použije se od příští relace. + Relace Steamu + Relace Plochy + Displej + Rozlišení + Až 720p + Až 900p + Až 1080p + Nativní + %1$s (výchozí) + Vlastní · %1$d×%2$d + Vlastní… + 720p může zrychlit odezvu nabídek. + Poměr stran obrazovky + Určen vlastním rozlišením. + Automaticky použije alespoň 16:9. + Omezení snímků + Omezuje celou relaci. Displej přejde na obnovovací frekvenci, kterou lze tímto limitem dělit beze zbytku. + HDR + Výstup HDR10 + Není k dispozici: %1$s. + Pro použití restartujte aplikaci. + Ovladače + Ovladače GPU + Společné pro Steam a Plochu. Nastavuje se na stránce Komponenty. + Dotyk a ovládání + Dotyk + Touchpad + Přímé + Automaticky: touchpad na Ploše, přímo ve Steamu. + Ovládání na obrazovce + Steam + QAM + Automaticky zobrazí ovládání, pokud není připojen žádný ovladač. + Dotykové ovládání Steamu (beta) + Dotykové ovládání Steamu se spustí od příští relace Steamu + Rozvržení dotykového ovládání Steamu + Přesuňte, změňte velikost nebo skryjte dotykové ovládání této hry; uloží se do Steamu + Ovladač + Jak Steam vidí váš ovladač. Použije se od příští relace. + Ovladač Steam Deck + Steam Controller + DualShock 4 + DualSense + Ovladač Xbox 360 + Deck: tlačítko Quick Access a gyroskop. Steam Controller: totéž, navíc snímání úchopu na zadních tlačítkách. DualShock 4 a DualSense: gyroskop, trackpad slouží jako touchpad. Xbox 360: Quick Access otevřete kombinací Guide+A. + Zpět + Menu → QAM + QAM → Menu + Relace + Chování na pozadí + Když je aplikace skrytá nebo se vypne obrazovka. + Manuální + Nativní + Automaticky pozastaví relaci a obnoví ji při zobrazení. Nativně nejprve připraví Steam na spánek. Manuálně čeká na Obnovit. Nikdy běh nezastavuje. + Nechat stahování běžet + Oddálí pozastavení na pozadí, pokud Steam aktivně stahuje. + Automaticky pozastaví na pozadí a obnoví při zobrazení. Manuálně čeká na Obnovit. Nikdy běh nezastavuje. + Spouštění + Spustit Steam s DroidDeck + Otevře relaci Steamu při spuštění DroidDeck. + Decky + Hledání vydání… + Připraveno k instalaci %1$s. + Nenalezeno žádné kompatibilní sestavení. Zkuste to později. + Nainstalováno · %1$s + Dostupná aktualizace · %1$s + Aktuální · %1$s + Loader + Nainstalovat nejnovější + Spouští se se Steamem. Když je zapnuto, jiné aplikace na tomto zařízení mohou mít možnost ovládat Steam. + Vypnuto. Port pro vzdálené ladění Steamu zůstává zavřený. + Odinstalovat Decky Loader? + Odstraní loader. Vaše pluginy a nastavení zůstanou zachovány. + Klient + Výkonový overlay + Vypněte, pokud hry v režimu Deck padají nebo zčernají. Použije se od příští relace. + Opravit klienta Steam + Při dalším spuštění znovu stáhne klienta Steam. Použijte, pokud Steam zůstává na načítací obrazovce. Hry, přihlášení a nastavení zůstanou zachovány. + Klient Steam se znovu stáhne při dalším spuštění Steamu. + Opravit + Grafika + Klíč API SteamGridDB + Váš klíč + Upravit… + Doplní chybějící grafiku ze Steamu, poté ze SteamGridDB. Vlastní soubory cover.jpg, hero.jpg, logo.png nebo icon.png ve složce hry mají přednost. + Odebrat %1$s + Odebrat? + Přidat hru + Přidat hru: + Vyberte exe: + Toto umístění není v relaci dostupné + Zkontrolovat tento exe + Komponenty Windows + LB / RB přeskakují mezi sekcemi. + Žádné + DLL knihovny Windows, které Proton neobsahuje, ze seznamu komponent. Zapněte ty, které tato hra potřebuje: uloží se do vlastní složky Windows hry, nikdy do jejích souborů, a zůstanou bez ohledu na to, s jakým Protonem hra běží. + Nepodařilo se načíst seznam komponent. Již stažené komponenty lze stále přepínat. + Doporučeno pro tuto hru + Zjišťuje se, co tato hra potřebuje… + Dostupné online + + Zapíná se %1$d komponenta… + Zapínají se %1$d komponenty… + Zapínají se %1$d komponenty… + Zapíná se %1$d komponent… + + Zapnuto pro tuto hru + Steam instaluje %1$s s %2$s (aplikace %3$s) + Ze seznamu GOG pro tuto hru + Z požadavků Epic pro tuto hru + Z instalačních kroků Amazon pro tuto hru + Steam instaluje %1$s s aplikací %2$s, spárováno podle názvu + Také uvedeno + Uvádí ho %1$s; zde to není komponenta + Steam + GOG + Epic Games + Amazon Games + Hra obsahuje %1$s + Hra má %1$s (často funguje i bez úprav) + Již v této hře + Nic ve složce hry neodkazuje na komponentu. + Nainstalováno pro tuto hru (%1$d) + Všechny komponenty (%1$d) + Potřebují instalátor Windows (%1$d) + Jde o vlastní instalační programy, uvnitř nemají balíček k rozebrání a zatím není zaznamenáno, co instalují. Většina z nich jsou staré runtimy, které už nahrazuje Mono uvnitř Protonu; záznamy se doplňují postupně. + Zobrazit + Skrýt + Spouští vlastní instalační program (zatím bez záznamu) + Zde ještě nepodporováno + Načítání komponent… + Nepodařilo se nainstalovat: %1$s + Staženo + Stahuje se %1$s + Instaluje se: %1$s + Vypínání… + %1$s ke stažení + Použije se při dalším spuštění hry. + Roztáhnout hry 16:9 + Na obrazovkách bližších čtverci než 16:9 roztáhne hry na celou plochu místo pruhů. Použije se od příští relace. + Zvuk + DirectAudio pro hry + Obchází PulseAudio pro nižší latenci. + Mikrofon + Pro hlasový chat. + Síť + Vyhledávání Wi-Fi + Zobrazí názvy Wi-Fi a okolních sítí ve Steamu. + Služby určování polohy jsou vypnuté. + Oprávnění k poloze nebylo uděleno. + Otevřít nastavení + Pokračovat + Povolit vyhledávání Wi-Fi? + Android vyžaduje oprávnění k poloze pro čtení názvů Wi-Fi a vyhledávání okolních sítí. DroidDeck tento přístup využívá k zobrazení těchto sítí ve Steamu. Vaši polohu nesledujeme. Přístup k internetu funguje i bez tohoto oprávnění. + Úložiště her + Automaticky – karta SD, pokud je k dispozici, a vaše složky Games + Automaticky – vaše složky Games a karta SD, pokud je k dispozici (nyní žádná) + Pouze interní + Složka: %1$s + Druhá knihovna + Hry, které streamují data z karty SD nebo sdíleného úložiště, mohou trhat. + Diagnostika úložiště + Zaznamenává výsledky alokace a vzorky časování úložiště do sdílených protokolů. Když je zapnuto, může zpomalit instalace. Použije se od příští relace. + Vybrat složku… + Vlastní rozlišení + Nahradí limit i poměr stran. Velikost, která neodpovídá obrazovce, získá pruhy. + Šířka + Výška + Mezi 320×240 a 3840×2160. + + %1$d hra + %1$d hry + %1$d hry + %1$d her + + OK + Smazat + Obnovit + Stáhnout + Smazat %1$s + %1$.1f MB + Ovladač runtime + Používá Steam, hry i Plocha. Použije se od příští relace. + Importovat zip Turnip… + Ovladač displeje + Vykresluje snímky na obrazovku v obou režimech. Pro použití restartujte aplikaci. + Importovat zip AdrenoTools… + nikdy + Nightlies zkontrolovány %1$s + Komponenty + O komponentách + Zkontrolovat nové ovladače + Zkontrolovat Nightlies na nové balíčky + Proton + Proton vybraný zde spouští každou hru, která si ve Steamu nevybrala svůj vlastní, a změna provedená ve Steamu se zobrazí i tady. Steam při aktualizaci nahrazuje Proton od Valve; originály jsou zachovány pro každé sestavení. + Konfigurace + Konfigurovat profil + Profil + Výchozí profil + Automaticky používán hrami bez vlastního přepsání. + Uložené nastavení se automaticky použije při spuštění %1$s. + Vyberte jen nastavení, která tato hra potřebuje. Vše ostatní dál používá Výchozí profil. + Předvolba FEX + Používá se předvolba Výchozího profilu. + Pro tuto hru je uložena vlastní předvolba. + Vlastní proměnné FEX + Použít Výchozí + Kompatibilita hry + Možnosti prostředí Proton, DXVK, VKD3D a synchronizace pro tuto hru. + Používá se Proton Výchozího profilu. + Pro tuto hru je přes mapování kompatibility Steamu uložen vlastní Proton. + Žádný Proton + DXVK + VKD3D-Proton + Používá balíček Výchozího profilu z %1$s, pokud tato hra nevybere jiný balíček. + Používá balíček komponent Výchozího profilu. + Pro tuto hru používá jen %1$s. + Možnosti runtime + Další možnosti prostředí Proton, DXVK, VKD3D-Proton a synchronizace pro jednotlivé hry. + Profil %1$s je uložen pro další spuštění hry. + Spustit bez Steamu + Pro hru, která se hned zavře: spustí ji bez nastavení Steamu v Protonu. Funkce Steamu, jako je překryvná vrstva, jsou pro ni vypnuté. + Zapnuto + Výběr Protonu pro %1$s je uložen a hned dostupný ve Steamu. + Výběr Protonu pro %1$s je uložen. Před spuštěním hry restartujte Steam. + %1$s nemůže na tomto zařízení použít vybraný Proton. + Nepodařilo se načíst profil %1$s: %2$s + Profil hry se nepodařilo uložit: %1$s + Resetovat profil hry + Resetovat profil %1$s? + Odebrat Proton, FEX, balíček komponent, prostředí, filtrování textur a přepsání komponent Windows u této hry? Bude používat Výchozí profil. + %1$s nyní používá Výchozí profil. + Profil hry se nepodařilo úplně resetovat: %1$s + %1$s nyní spouští každou hru, která si ve Steamu nevybrala svůj vlastní Proton. + %1$s bude od dalšího spuštění Steamu spouštět každou hru, která si nevybrala svůj vlastní Proton. + %1$s zde nemůže spouštět hry, takže nemůže být výchozí. + Výchozí Proton se nepodařilo uložit: %1$s + %1$s · %2$d nahrazeno + %1$s · vše originální + %1$s · hra běží + %1$s · balíček droiddeck-esync + %1$s · zatím bez balíčku droiddeck-esync + %1$s · esync je vestavěný, balíček nepotřeba + Ovladače GPU + %1$s · znovu použito při spuštění %2$s + Další: %1$s · po zavření hry + %1$s v použití: %2$s + Čeká na zavření hry + Použije se při dalším spuštění hry + Importovat .zip + Čtení Protonů… + Zatím není nainstalován žádný Proton. Jednou spusťte Steam a stáhněte jeho Proton pro ARM64, nebo přidejte GE-Proton, CachyOS nebo DroidDeck-Proton v Nastavení. + Originál · %1$s + %1$s · toto sestavení + %1$s · dřívější sestavení + Nainstalováno · %1$d + Pro %1$s zatím nic není uloženo. Vlastní soubory tohoto Protonu jsou uloženy jako jeho originál. + Na tomto Protonu běží hra, takže se čeká na její zavření. + Použije se při dalším spuštění hry. + Nahradit %1$s? + Vložit %1$s do %2$s? Jeho původní soubory zůstanou uložené. %3$s + Obnovit %1$s? + Vrátit originál %1$s zpět do %2$s? %3$s + Odstraní balíček z aplikace. Můžete ho znovu stáhnout. + Smazat tento originál? + Tento originál %1$s je z dřívějšího sestavení %2$s. Originál aktuálního sestavení je vždy zachován. + Na Nightlies · %1$d + Obnovte pro zobrazení balíčků na Nightlies. + Od poslední kontroly nic nového. + Importovat .wcp + Obnovit originál tohoto Protonu + Vrátit zpět originální %2$s od %1$s? + Nahradit + FEX, DXVK a VKD3D-Proton pro každý Proton, se kterým Steam spouští hry.\n\nVyberte nainstalovaný řádek pro jeho nahrazení. Použije se při dalším spuštění hry, nebo po zavření běžící hry.\n\nVaše volby se před každým spuštěním znovu zkontrolují a vrátí zpět, pokud je něco změnilo. Původní soubory každého Protonu jsou zachovány, takže je aktualizace Steamu nikdy neztratí. + Originál + Používá se + Nové + Uloženo + Výkon + Jádra CPU, opravy relace a název hostitele. Použije se od příští relace. + Jádra pro klienta Steam + Přepsat výběr jader Steamu + Připne Steam, jeho pomocníka rozhraní a gamescope na jádra níže. + Jádra klienta + Jádra pro Steam. + Pro výběr zapněte přepsání. + Jádra pro hru + Jádra pro hru + Jádra pro hry. Se všemi vybranými plánuje Android volně. + Rozhraní klienta + Vícevláknové GL + Může zrychlit odezvu nabídek Steamu. + Zink: líné deskriptory + Doporučeno pro ovladače bez descriptor bufferů. + Vynechat kontroly chyb GL + Vypne ověřování GL při každém volání. + gamescope: fronta GPU v reálném čase + Dává prioritu práci GPU kompozitoru. Může vyhladit tempo snímků na úkor her. + GPU + Udržet GPU na nejvyšších taktech + Jen Adreno. Méně zadrhávání, více spotřeby baterie a tepla. + Opravy relace + Turnip: vykreslování sysmem + Rychlejší pro Steam a většinu her. Vyžadováno na Adreno 710/720/722. + Vynechat Steamova pomocníka xalia + droiddeck-ntsync (experimentální) + Uživatelská náhrada linuxového ovladače ntsync, pro testování. Když je zapnuto, Proton ho používá místo droiddeck-esync a droiddeck-fsync. Funguje s Proton Experimental ARM64, GE-Proton a proton-cachyos. Použije se od příští relace. + droiddeck-esync + droiddeck-fsync jako první + Spouští vlastní fsync Protonu na původních souborech Protonu, s futex_waitv dodaným v uživatelském prostoru, takže nepotřebuje balíček a funguje i hned po aktualizaci Protonu Steamem. Když je zapnuto, hry ho používají před droiddeck-esync, kromě těch pár, které Proton u fsync nechává vypnuté. Použije se od příští relace. + Vlákna Windows her na sebe čekají přes eventfd a sdílenou paměť místo zpráv přes wineserver. Používá balíček droiddeck-esync vytvořený přesně pro dané sestavení Protonu; balíčky se stahují automaticky. Dokud pro nové sestavení Protonu žádný neexistuje, zastupuje ho droiddeck-fsync. Použije se od příští relace. + Vypne pomocníka Protonu pro navigaci gamepadem, který stojí výkon CPU v každé hře. + Spustit proot bez seccomp + Může opravit chyby s chybějícími systémovými voláními na některých jádrech. Může snížit výkon. + Rychlá cesta proot + Vyžaduje filtr seccomp od proot. + Rychlejší vyhledávání souborů a méně zadrhávání. Vypněte, pokud program nemůže najít své soubory. + Identita relace + Název hostitele + Název, který Steam vidí pro toto zařízení. Prázdné obnoví %1$s. + Pouze písmena, číslice a vnitřní pomlčky, až 63 znaků. + Android je nastaven tak, aby tuto relaci ukončil + Nainstalováno + K dispozici ke stažení + Od poslední kontroly nic nového. + Obnovit vestavěné ovladače + Vestavěné v aplikaci: toto odstraní jeho rozbalené soubory a skryje ho. Obnovit vestavěné ovladače ho vrátí zpět. + Polovina balíčku Android + Linux: odstraní se obě poloviny. Používaný ovladač se vrátí na výchozí. + Jeho soubory se odstraní z aplikace. + Jeho soubory se odstraní. Používá se, takže převezme výchozí. + Displej + Efekty + Ovládání + Komponenty + Ovladač + Nastavení + Stránka %1$s + Steam + Plocha + Nabídka Steam + Rychlý přístup + Otevřít nabídku Rychlý přístup + Předchozí stránka + Další stránka + HUD výkonu + Režim škálování + Ostrost škálování + Efekty obrazovky + Živě. Stojí trochu výkonu GPU, když je zapnutý jakýkoli efekt. + Předvolby + Vlastní + Pokročilé + Rozbaleno + Sbaleno + Zaostření (CAS) + Úroveň zaostření + Falešné HDR + Odstranění pruhování + Síla odstranění pruhování + Jas + Kontrast + Gama + Sytost + FXAA (vyhlazení hran) + Kreslené obrysy + CRT + NTSC + Filtrování textur + Hry na DirectX 9–11, od jejich dalšího spuštění. + Anizotropní filtrování + Ostrost textur + Záporný mip bias: ostřejší textury, trochu mihotání. Automaticky odpovídá tomu, jak moc je relace zvětšena na obrazovku. + Automaticky (%1$s) + Klávesnice + Klávesnice PC + Android + Druhá obrazovka + Režim + Zobrazuje + Aplikace Android + Spustit aplikaci + Skrýt + Zobrazit + Žádné spustitelné aplikace + Chování relace + Použije se nyní i pro budoucí relace v tomto režimu. + Příští relace + Podpora + Protokoly relace + Přiložit k hlášení chyby + Sdílet protokoly + Pozadí + Ukončit + Ukončit relaci? + Právě běží + Na Protonu neběží žádná hra + Profil hry + Podrobnosti profilu zatím nejsou k dispozici. + Nepodařilo se načíst profily her. Automaticky se to zkouší znovu. + Nepodařilo se obnovit profily her. Zobrazují se poslední dostupná data; automaticky se to zkouší znovu. + Zobrazit a upravit uložené nastavení použité při dalším spuštění. + Používá se Výchozí profil + Vlastní profil + Používá se %1$s z Výchozího profilu. + Změny se uloží do profilu %1$s a použijí se při dalším spuštění hry. + + %1$d přepsání hry + %1$d přepsání hry + %1$d přepsání hry + %1$d přepsání hry + + Zatím není nainstalován žádný Proton. + hra běží + znovu použito při spuštění %1$s + Originál + Import a mazání na stránce Komponenty. + Hotovo + Vyžaduje pozornost + Zpracovává se + Zkontrolujte IP adresu a port pro Bezdrátové ladění, nebo zařízení znovu spárujte, pokud ho Android odstranil. + Instalovat + Správa + Přehled + Ovladač + Relace + Spouštěč + Příprava + ● Vše připraveno + %1$d z %2$d připraveno + Kontrola systému + Zařízení podporováno + Neověřené GPU + GPU nepodporováno + %1$s · %2$s + %1$s: %2$s + Steam vyžaduje ovladač Adreno; %1$s může zobrazit černou obrazovku + Běhové prostředí Linuxu + %1$s · %2$d %% + Nenainstalováno · přibližně 3 GB, instaluje se při prvním Play + Nainstalováno + %1$s · dostupná aktualizace + %1$s · aktuální + Limit podprocesů + Zapnuto · Android může Steam zavřít. Vypnutí trvá minutu. + Nenastaveno · Android může Steam zavřít. Vypnutí trvá minutu. + Nelze zkontrolovat · vypnutí trvá minutu. + Opravit + Podrobnosti + Možnosti pro vývojáře + Použít Bezdrátové ladění + Zkontrolovat znovu + Zapnout limit + Účet Steam + Přihlášen jako %1$s · režim offline + Přihlášen jako %1$s + Stiskněte Play a přihlaste se ke Steamu + Nástroje + Ovladač + Nastavení ovladače není k dispozici. + Relace + Protokoly relace + Uloženo po každé relaci + Nástroje pro ladění + Protokoly poslední relace + Uložené protokoly relací + Uchovává nejnovějších %1$d + Vymazat protokoly + Režim offline + Nejprve se přihlaste ke Steamu + Spouštěč + Jazyk + Použije ho i Steam, od dalšího spuštění. + Výchozí systémový (%1$s) + Výchozí systémový + Měřítko aplikace + Text a ovládání DroidDeck + %1$d %% (výchozí) + Téma + Vzhled spouštěče + Použít jako domovskou obrazovku + DroidDeck může být domovská aplikace + DroidDeck se nikdy nenabízí jako domovská aplikace + Orientace + Automaticky na šířku + Na šířku + Na šířku obráceně + Celá obrazovka + Skryje stavový a navigační panel Androidu + Zobrazí stavový a navigační panel Androidu + Výchozí domovská aplikace + Vybrat domovskou aplikaci + Vybrat + Aplikace Linuxu (beta) + Soubory + Procházení a správa souborů + Verze Protonu + Instalace sestavení Protonu pro ARM64 + Výkon + Přiřazení jader CPU + Složka ROM + Kde jsou uloženy hry pro emulátory + Komponenty + FEX, DXVK, VKD3D + Ovládání + Mapování tlačítek + Objevit + Hledat + Nainstalováno + %1$s · %2$d + Obchod + Flathub · ARM64 + ● Flatpak připraven + Nejprve nainstalujte běhové prostředí Linuxu v Přípravě. + Nastavit Flatpak + Přidá Flatpak a připojí ho k Flathub (přibližně 20 MB). První aplikace si také stáhne svůj runtime. + Nastavování… + Flatpak + Aktualizace + %1$s · %2$s + %1$s · %2$s · %3$d %% + Spouští se… + Kategorie + Nepodařilo se spojit s Flathub. Zkontrolujte připojení a zkuste to znovu. + Zkusit znovu + Hledat na Flathub + Firefox, hry, emulátory… + Hledat + Vše + Hledání… + Hledejte podle názvu nebo vyberte kategorii. Uvedeny jsou jen aplikace pro ARM64. + Tomu na Flathub neodpovídá žádná aplikace pro ARM64. + Výsledky + Zkontrolovat aktualizace + Aktualizovat vše (%1$d) + ● Vše je aktuální + Zatím žádné aplikace. Najděte nějaké v Objevit nebo Hledat. + Pro instalaci aplikací nastavte Flatpak. + Dostupná aktualizace · %1$s + Otevřít + Podrobnosti + Odebrat + Pro odebrání stiskněte znovu + Instaluje se… + Instalovat + Nejprve nastavte Flatpak + %1$s ke stažení + ● Nainstalováno + Nepodařilo se načíst stránku této aplikace na Flathub. + Načítání z Flathub… + Na Flathub není sestavení pro ARM64, takže zde nelze nainstalovat. + Snímky obrazovky + O aplikaci + ID aplikace + Licence + Runtime + Velikost po instalaci + %1$s (plus sdílený runtime) + Web + Ověřeno + Aktualizace + DroidDeck %1$s · %2$s + Zkontrolováno %1$s + Zatím nezkontrolováno + Povolit DroidDecku aktualizovat se sám + Na další obrazovce zapněte Povolit z tohoto zdroje a vraťte se zpět. Aktualizace pak pokračuje sama. + Otevřít nastavení + Teď ne + Jinak podepsáno + Nelze aktualizovat na místě + Tato kopie není podepsána vydávacím klíčem DroidDecku, proto se přes ni aktualizace nedají nainstalovat. Odinstalujte ji, abyste mohli přejít. + Aktualizace k dispozici + Nelze instalovat + Připraveno k přechodu + Před Stable + Jste před verzí Stable + Tato sestava je novější než aktuální Stable. Na Stable přejdete s jejím dalším vydáním. + Test skončil + Tento test skončil + Jeho oprava byla sloučena nebo zrušena. Nejnovější opravy sledujte v Preview. + Aktuální + Máte nejnovější %1$s + · %1$s + %1$s… %2$d%% + %1$s… + Aktualizovat%1$s + Instalovat%1$s + Přesto instalovat Stable %1$s + Sledovat Preview + Pro aktualizaci zastavte běžící relaci. + Méně + Více + DroidDeck %1$s + Nová sestava Preview + Testovací sestava PR #%1$d + %1$d MB + AKTUALIZAČNÍ KANÁL + Stable + Testovaná vydání, pro většinu lidí + %1$s · %2$s + Preview + Nejnovější opravy, dříve než ve Stable + Aktuální + Testovací sestavy + Vyzkoušejte opravu před jejím vydáním + Momentálně žádné + PR #%1$d + Pro tuto kopii DroidDecku není sestavena + Aktualizováno %1$s + test PR #%1$d + před chvílí + právě teď + + %1$d k vyzkoušení + %1$d k vyzkoušení + %1$d k vyzkoušení + %1$d k vyzkoušení + + + Změny od vaší sestavy · o %1$d sestavu pozadu + Změny od vaší sestavy · o %1$d sestavy pozadu + Změny od vaší sestavy · o %1$d sestav pozadu + Změny od vaší sestavy · o %1$d sestav pozadu + + + Nedávné změny · %1$d sestava + Nedávné změny · %1$d sestavy + Nedávné změny · %1$d sestav + Nedávné změny · %1$d sestav + + Je třeba změnit jedno nastavení Androidu + Steam + Android zastavuje aplikace, které spouštějí mnoho procesů na pozadí, a Steam jich spouští desítky. Dokud se to nezmění, hry se zavírají bez chybové zprávy. + Oprav to za mě + Zapněte „Disable child process restrictions“ a vraťte se zpět. Nechte Možnosti pro vývojáře zapnuté, jinak se přepínač znovu vypne. + Otevřít Možnosti pro vývojáře + Tato verze Androidu pro to nemá přepínač, proto to DroidDeck změní přes Bezdrátové ladění. Zabere to asi minutu, počítač není potřeba. + Bezdrátové ladění potřebuje Wi-Fi. Funguje jakákoli síť, internet není nutný. + Párovací kód se zobrazí v oznámení. Povolte oznámení pro DroidDeck, nebo použijte Jiné způsoby. + Povolit oznámení + Otevřete Nastavení → Možnosti pro vývojáře → Bezdrátové ladění a zapněte ho + Klepněte na „Spárovat zařízení pomocí párovacího kódu“ + Zadejte kód do oznámení DroidDecku + Čekání na párovací okno… + Okno nalezeno. Zadejte jeho kód do oznámení. + Stav + Kontroluje se každé 2 sekundy. Tato stránka se zavře, až bude vypnuto. + Jiné způsoby + Bezdrátové ladění, ruční adresa, ADB z počítače + Ruční adresa, ADB z počítače, Možnosti pro vývojáře + Použít místo toho Bezdrátové ladění + Některé ROM přepínač přidávají i tak. Hledejte v Možnostech pro vývojáře „child process“. + Pokud oznámení nefunguje, zadejte adresu a kód z okna sami. + Zadat adresu ručně + Z počítače s ADB: + Kopírovat příkaz + Vyberte, na kterém displeji se má otevřít Nastavení Androidu. + Hlavní obrazovka + Spárováno. Hledání portu Bezdrátového ladění… + Spárováno. Zadejte port z Nastavení. + Zařízení nalezeno. Používá se nastavení… + Nepodařilo se použít automaticky. Zkontrolujte adresu níže a zkuste to znovu. %1$s + Bezdrátové ladění + Omezení pro dětské procesy aktualizováno + Změnit omezení pro dětské procesy + Pokud volba chybí, spárujte přes Bezdrátové ladění. + Spárováno. Zkontrolujte IP adresu a port v Bezdrátovém ladění. + Omezení pro dětské procesy bylo aktualizováno. + Krok 2 použijte, pokud Možnosti pro vývojáře nemají nastavení pro dětské procesy. Otevřete Možnosti pro vývojáře → Bezdrátové ladění → Spárovat zařízení pomocí párovacího kódu. + Spárováno. Použijte adresu níže, nebo ji aktualizujte z Bezdrátové ladění → IP adresa a port. + Omezení pro dětské procesy bylo aktualizováno přes Bezdrátové ladění. + Android potvrdil, že omezení je zapnuto. + Android potvrdil, že omezení je vypnuto. + 2 · Spárovat Bezdrátové ladění + IP adresa a port párovacího okna + Párovací kód (PIN) + 6 číslic + Spárovat + 2 · Použít přes Bezdrátové ladění + IP adresa a port pro připojení + Spárovat znovu + Vypnout omezení + Jiné způsoby změny + 1 · Možnosti pro vývojáře (zkusit nejdřív) + Zapněte „Restrict child processes“. + Vypněte „Restrict child processes“. + 3 · ADB z počítače (poslední možnost) + Jen pokud Bezdrátové ladění není k dispozici. + 3 · ADB z počítače (poslední možnost) + Kdy se dotykové ovládání zobrazí v relaci + Barva + Odstín tlačítek na obrazovce + Vlastní + Průhlednost + %1$d%% + Velikost tlačítek + 100 % je standardní velikost + Kliknutí páčkou + Dvojitým dotykem a podržením páčky aktivujete L3 nebo R3 + Adaptivní páčky + Vibrace ve hře + Tlačítko Steam + Tlačítko QAM + Tlačítko klávesnice + Otevřít nabídku Steam stisknutím Select + Místo View/Select. + Animace + Páčky se zobrazí tam, kde se dotknete, a skryjí se po uvolnění + Rozložení + Uloženy vlastní pozice + Přizpůsobeno této obrazovce + Upravit + Obnovit + Mapování tlačítek + Každé tlačítko odesílá svůj vlastní vstup + %1$d z %2$d tlačítek přemapováno + Konfigurovat + Obnovit ovladač + Obnoví všechna nastavení ovladače a rozložení + Co každé tlačítko na obrazovce odesílá hře. Skryté tlačítko odstraní. + Tlačítka na obrazovce + Výchozí + Obnovit mapování + Aplikace Android + %1$d aplikací + Nebyly nalezeny žádné spustitelné aplikace Android. + ● Desktop nainstalován + Nainstaluje se při prvním otevření + K instalaci + Nenainstalováno. + %1$s · nenainstalováno + Otevřít %1$s + Hry + Vyberte složku ROMs. + Přejděte v RetroArchu na /root/ROMs. + Přidejte hry %1$s do ROMs/%2$s. + Instalovat %1$s + Instalace vyžaduje zastavení relace + Vyžadován runtime + Načítání podrobností instalace… + Podrobnosti instalace nejsou k dispozici. Zkuste to později. + Nejprve nainstalujte Linuxový runtime z Přípravy. + Pro instalaci desktopových aplikací zastavte běžící relaci. + Instaluje %1$s do Linuxového desktopu. + %1$s · %2$d%% + Skrytím se odstraní z Desktopu. Soubory zůstanou v Linuxovém runtime. + Tato hra už není ve složce ROMs. + Desktop · %1$s + Spustit v %1$s + Také v %1$s + Tato stránka už neexistuje. Vyberte sekci vlevo. + Otevřít desktop + Instalovat a otevřít desktop + Odinstalovat desktop? + Odstraní desktop a jeho vestavěné aplikace. Hry, přidané aplikace, nastavení a sdílené knihovny zůstanou. + Odstraňování desktopu + Desktop odstraněn + Odstraňování starých souborů LXQt + Před odstraněním této verze desktopu aktualizujte DroidDeck. + Linuxový desktop + KDE Plasma, Firefox a vaše emulátory + Hry, které nainstalujete ve Steamu, se zde zobrazí po relaci. + Hrát ve Steamu + Nastavení spuštění + Spustit + Soubory hry + Prefix Protonu + Hledání uložených pozic… + Podle názvu nebyla nalezena žádná složka s uloženými pozicemi. Export zabere celou uživatelskou složku. + %1$d složek s uloženými pozicemi + %1$s · %2$d souborů · %3$s + %1$d KB + Správa uložených pozic + Uložené pozice hry + Importovat uložené pozice… + Exportovat jako GameHub zip… + Exportovat jako Winlator zip… + Otevřít složku s uloženými pozicemi + Nikdy nehráno + Runtime zaneprázdněn · %1$d%% + Runtime zaneprázdněn + Runtime se nainstaluje při prvním spuštění hry + K dispozici aktualizace runtime + ● Runtime připraven + Poslední hraní %1$s + Interní úložiště + Přidaná hra + Vlastní hra + Úložiště her + Nejlepší dvojice pro tento GPU, udržovaná aktuální + Vyberte dvojici sami + Hledání ovladačů… + Zatím žádné ovladače. Obnovte, aby se vyhledaly. + Skrýt ovladače pro jiné GPU + Zobrazit ovladače pro jiné GPU + Pokročilé + Nastaveno společně s ovladačem displeje: %1$s obsahuje oba. + Nastaveno společně s ovladačem runtime: %1$s obsahuje oba. + Používají hry a desktop. + Výchozí runtime + Vykresluje snímky na obrazovku. Použije se po restartu DroidDecku. + Automaticky - podle GPU + Stáhnout, importovat nebo smazat… + POUŽÍVÁ SE + Ovladače vestavěné v DroidDecku + Ovladače runtime a displeje nastaveny společně jako sladěná dvojice. + Doporučeno pro tento GPU: %1$s. + Obnovte, aby se vyhledaly ovladače pro tento GPU. + DOPORUČENO + Není k dispozici + Linuxový runtime + Příprava Linuxového runtime + Relace + Spouštění obrazu a zvuku + Klient Steam + Kontrola Steamu a jeho nástrojů kompatibility + Otevírání Steamu + Čekání na první snímek Steamu + Desktop + Příprava desktopu + Otevírání + Čekání na první snímek + Spouštění Steamu + Relace skončila + Krok %1$d z %2$d + Steam je na pauze + Rukojeti + Levý panel + Pravý panel + Oba + Gamepad + Páčka + Trackpad + Přepnout mezi páčkou a trackpadem + Další ovládání Deck + PANELY + Zavřít ovládání druhé obrazovky + Otevřít nabídku Steamu + Levý · horní + Levý · dolní + Pravý · horní + Pravý · dolní + Kliknutí + Pravý trackpad + Levý trackpad + Trackpad + Zobrazit klávesnici + Skrýt klávesnici + Terminál Linuxu + Relace Linuxu ještě není připravena na terminál. + Steam + Hry + Plocha + Obchod + Komponenty + Aplikace + Příprava + Aktualizace + %1$d%% + Obnovit %1$s + V každé relaci se zobrazí jako /root/ROMs. Vnitřní úložiště je v /root/Storage. + Nevybrána žádná složka + Použije se v další relaci. SD karty fungují. + Vybrat složku + Změnit složku + Nainstalujte sestavení Protonu pro ARM64 a pak ho vyberte pro konkrétní hru v Steamu → Vlastnosti → Kompatibilita. + Pro instalaci nebo odstranění nástroje kompatibility zastavte běžící relaci. + Dostupná sestavení + Nainstalováno %1$s + Čeká z dřívějšího požadavku + Není nainstalováno + Dostupná aktualizace → %1$s + Odstranit staré verze %1$s + Odstraní %1$s. Hry nastavené na tyto verze je nutné přepnout na jiný Proton. + Hry nastavené na tuto verzi je nutné přepnout na jiný Proton. + Instalovat nyní + Zrušit čekající + Klávesnice Androidu + Skrýt ✕ + Spustit %1$s + Vyberte obrazovku. Druhá: %1$s. + Druhá obrazovka už není dostupná. + Hlavní obrazovka + Druhá obrazovka + Předchozí karta + Další karta + Zapnuto + Vše %1$d + Žádné + %1$d z %2$d + Vypnuto + Spodní obrazovka + Použije se při dalším spuštění hry. Profil hry má přednost před společným. + Jiná hra podle ID aplikace… + Hotovo + Výchozí + Vypnuto + Změnit… + Změnit proměnnou + Zpět na výchozí + Hry přes Proton + Obraz v obraze + Automaticky přejít do PiP + Ovládání na rozložené obrazovce + Po rozložení je hra zobrazena nad trackpady a zadními rukojeťmi Steam Decku. Po složení je hra na celou obrazovku. + Pozastaveno + Obraz v obraze bude dostupný, jakmile bude relace připravena. + Povolte obraz v obraze pro DroidDeck v nastavení Androidu. + Android nemohl otevřít obraz v obraze. + Zkratka + Přidat na domovskou obrazovku + Vybrat hru + Vyhledávání nainstalovaných her… + Kopírovat odkaz pro spuštění + Odkaz pro spuštění zkopírován + Tento spouštěč neumí přidávat zkratky. Použijte místo toho Kopírovat odkaz pro spuštění. + Neplatný odkaz pro spuštění hry. + Tato hra už není ve vaší knihovně DroidDeck. + Před spuštěním hry ze Steamu zastavte aktuální relaci. + Hru se nepodařilo odeslat do Steamu. Zkuste ji spustit znovu. + Zkusit odstranění znovu + Odstraňování + Odstranění nedokončeno + Exportovat soubor hry + Synchronizovat všechny soubory her do složky + Zastavit synchronizaci souborů her + Vybrat složku pro soubory her frontendu + Soubory hry uloženy do %1$s + Soubory frontendu + + Počkejte, až skončí operace s runtime + Počkejte, až skončí instalace nástroje kompatibility + Počkejte, až skončí instalace plochy + Před spuštěním hry počkejte, až skončí instalace + Nejprve zavřete relaci Steamu, aby hra svým uložením nepřepsala import. + Importování do %1$s + + Importován %1$d soubor z %2$s do %3$s + Importovány %1$d soubory z %2$s do %3$s + Importováno %1$d souborů z %2$s do %3$s + Importováno %1$d souborů z %2$s do %3$s + + + Importován %1$d soubor z %2$s do %3$s. Staré uložené pozice byly zálohovány do Download/DroidDeck/Saves/backups + Importovány %1$d soubory z %2$s do %3$s. Staré uložené pozice byly zálohovány do Download/DroidDeck/Saves/backups + Importováno %1$d souborů z %2$s do %3$s. Staré uložené pozice byly zálohovány do Download/DroidDeck/Saves/backups + Importováno %1$d souborů z %2$s do %3$s. Staré uložené pozice byly zálohovány do Download/DroidDeck/Saves/backups + + Vyberte ZIP s uloženou pozicí pro %1$s + Exportování %1$s + + Exportován %1$d soubor jako %2$s: %3$s + Exportovány %1$d soubory jako %2$s: %3$s + Exportováno %1$d souborů jako %2$s: %3$s + Exportováno %1$d souborů jako %2$s: %3$s + + Vyberte, kam uložit %1$s (%2$s) + %1$s se nezdařilo: %2$s + Zatím žádné logy relace: nejprve spusťte relaci. + Žádné logy relace k vymazání. + + Vyčištěna %1$d složka s logy relace. + Vyčištěny %1$d složky s logy relace. + Vyčištěno %1$d složek s logy relace. + Vyčištěno %1$d složek s logy relace. + + Párování bezdrátového ladění se nezdařilo + Příkaz bezdrátového ladění se nezdařil + Bezdrátové ladění není k dispozici + Příkaz ADB zkopírován + Vybrat složku ROMs + Není GPU Adreno + Turnip podporuje GPU Adreno. Na %1$s může Steam zobrazit černou obrazovku. Stažení: %2$.0f MB. + Přesto instalovat + Odstranit runtime Linuxu + Tím se odstraní runtime, klient Steam v něm a všechny hry v něm nainstalované. + Použito: %1$s + Nepodařilo se otevřít %1$s na hlavní obrazovce + Nepodařilo se otevřít %1$s na %2$s + Nepodařilo se otevřít %1$s na obrazovce %2$d + Importování + Přepínání + Obnovování + Rušení + Mazání + soubor nelze přečíst + Importováno %1$s + Čekající přepnutí bylo zrušeno. + Vybrat balíček komponenty (-linux .wcp) + Nepodařilo se spojit s Nightlies + Uloženo %1$s + Stažení se nezdařilo: %1$s + Vybrat ovladač runtime Linuxu (ZIP -Linux nebo balíček Android + Linux) + Vybrat ovladač zobrazení (ZIP AdrenoTools nebo balíček Android + Linux) + Vybrat ZIP s ovladačem nebo balíček Android + Linux + %1$s: %2$s + Odstraňování %1$s… + Zapomínání %1$s… + Vybrat složku úložiště her + Vybrat ZIP s pluginem Decky + Nepoužitelné: %1$s + Program + Odstraňování runtime Linuxu + Nepodařilo se otevřít Nastavení na spodní obrazovce; otevřelo se na hlavní obrazovce. + Otevřít Možnosti pro vývojáře v Nastavení + + Nepodařilo se otevřít nastavení Androidu „Instalace neznámých aplikací“ + Nepodařilo se zkontrolovat: %1$s + Toto sestavení nemá stažení pro tuto kopii DroidDecku + Nejprve zastavte běžící relaci, pak aktualizujte + Stahování + Instalování + Čeká na vaše potvrzení + Aktualizace se nezdařila + Android nenabídl instalaci aktualizace + + Instalace Decky se nezdařila: %1$s + Spouštění importu pluginu… + Import pluginu se nezdařil: %1$s + Plugin nainstalován. Restartujte Steam, aby se načetl. + + Zatím nezkontrolováno - klepnutím na obnovit vyhledáte nové ovladače + Android + Linux: zároveň nastaví ovladač zobrazení + Android + Linux: zároveň nastaví ovladač runtime + Turnip vestavěný v runtime + systémový Vulkan: pro toto GPU není přibalené sestavení + %1$s (přibalený) + Manuální + %1$s · %2$s + Právě je zveřejněna jen jedna polovina + %1$.0f MB za oba + Žádné ovladače k nastavení: %1$s + Vyhledávání nejnovějších ovladačů… + Aktuální (nepodařilo se zkontrolovat: %1$s) + Doporučené ovladače nejsou právě zveřejněné + Stahování %1$s %2$s… + Aktualizováno na %1$s %2$s. + Aktualizováno na %1$s %2$s. Ovladač zobrazení se použije po restartu DroidDecku. + Přepnuto na %1$s %2$s. + Přepnuto na %1$s %2$s. Ovladač zobrazení se použije po restartu DroidDecku. + Používá se %1$s %2$s. + Používá se %1$s %2$s. Ovladač zobrazení se použije po restartu DroidDecku. + Toto není ZIP s ovladačem: ani ovladač AdrenoTools, ani Turnip -Linux, ani balíček Android + Linux + Import se nezdařil: %1$s + Importovaný ovladač + %1$s se používá jako ovladač runtime i zobrazení. + %1$s se používá jako ovladač runtime i zobrazení. Ovladač zobrazení se použije po restartu DroidDecku. + Odstraněno %1$s (oba ovladače) + Odstraněno %1$s + Nejnovější: %1$s · zkontrolováno %2$s + Nejnovější: %1$s · %2$s nedostupné · zkontrolováno %3$s + + před %1$d min + před %1$d min + před %1$d min + před %1$d min + + + před %1$d h + před %1$d h + před %1$d h + před %1$d h + + + před %1$d dnem + před %1$d dny + před %1$d dny + před %1$d dny + + Kontrola Banners-Turnip, WinNative a DroidDeck… + kontrola se nezdařila + Nainstalováno %1$s - vyberte ho v kterémkoli seznamu pro nastavení obou ovladačů + Nainstalováno %1$s - vyberte ho v nabídce + + Počkejte, až skončí odstranění runtime + Hra + touchpad + přímý + Přepnutí se nezdařilo: %1$s + Uložení profilu se nezdařilo: %1$s + Plocha je na pauze + %1$s je na pauze + Program je na pauze + Spouštění plochy + Spouštění %1$s + Spouštění programu + Pro tuto relaci nejsou žádné logy. + Nepodařilo se vytvořit archiv logů relace. + Nepodařilo se sdílet logy relace. + Spouštění relace… + stahování runtime Linuxu + stahování runtime Linuxu · %1$d z %2$d MB + kontrola runtime Linuxu + rozbalování runtime Linuxu + stahování plochy + stahování plochy · %1$d z %2$d MB + kontrola plochy + rozbalování plochy + stahování Proton Experimental (ARM64) + stahování Proton Experimental (ARM64) · %1$d z %2$d MB + kontrola Proton Experimental (ARM64) + rozbalování Proton Experimental (ARM64) + Nepodařilo se spojit s katalogem runtime. Zkontrolujte připojení a znovu stiskněte Hrát. + Runtime Linuxu se nenainstaloval. Zkontrolujte runtime v Přípravě a znovu stiskněte Hrát. + Plocha se nenainstalovala (%1$s). Zkontrolujte připojení a znovu stiskněte Plocha. + Došlo k neočekávanému zastavení %1$s. Přiložte logy k hlášení chyby, nebo zkuste znovu. + Program neočekávaně skončil. Přiložte logy k hlášení chyby, nebo zkuste znovu. + Plocha neočekávaně skončila. Přiložte logy k hlášení chyby, nebo zkuste znovu. + Návratový kód %1$d · %2$s + žádný log + + V logu je %1$d chyba „Function not implemented“: akcelerace seccomp v proot zde selhává u pomocné funkce na tomto zařízení. Zkuste %2$s → „%3$s“ (nebo „%4$s“) a spusťte to znovu. + V logu jsou %1$d chyby „Function not implemented“: akcelerace seccomp v proot zde selhává u pomocné funkce na tomto zařízení. Zkuste %2$s → „%3$s“ (nebo „%4$s“) a spusťte to znovu. + V logu je %1$d chyb „Function not implemented“: akcelerace seccomp v proot zde selhává u pomocné funkce na tomto zařízení. Zkuste %2$s → „%3$s“ (nebo „%4$s“) a spusťte to znovu. + V logu je %1$d chyb „Function not implemented“: akcelerace seccomp v proot zde selhává u pomocné funkce na tomto zařízení. Zkuste %2$s → „%3$s“ (nebo „%4$s“) a spusťte to znovu. + + + Pluginy + Nainstalujte ZIP s pluginem pro další relaci Steamu. + Stahování… + Instalovat ze ZIP + Hry + Zvuk a video + Grafika + Internet + Kancelář + Vývoj + Vzdělávání + Věda + Systém + Nástroje + Populární + Trendy + Novinky + Aktualizováno + načítání… + Nastavení Flatpaku + %1$s: %2$s + Nepodařilo se zkontrolovat aktualizace na Flathubu + nainstalováno + složka + Grafit + Grafitové povrchy, s modrou barvou kuličky z loga pro vše vybrané, zaměřené nebo stisknuté. + Papír na černém + Hlavní je bílá, modrá z ikony značí linky, kroužky a body. + Modrá z ikony + Černé pozadí, bílé písmo, modrá pro vše vybrané, zaměřené nebo stisknuté. + Elektrická modř + Jasnější modrá na povrchách, které jsou spíše tmavomodré než šedé. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Nejprve nainstalujte runtime + Knihovna Steamu není dostupná + Předchozí knihovna na SD kartě není dostupná + Úložiště her není dostupné + Složka přidaných her není dostupná + Manifesty Steamu nejsou dostupné + Mnoho systémů + Interní + Systém Linuxu + SD karta + Úložiště + Vybrat + Vlastnosti + Přidat do oblíbených + Odebrat z oblíbených + Přejmenovat + Kopírovat + Vyjmout + Akce + Oblíbené + Připnout aktuální složku + Zatím žádné oblíbené - připněte složku pomocí jejího menu ⋮ a rychle se sem vrátíte. + Odebrat z oblíbených + 0 bajtů + bajtů + Umístění + Typ + Velikost + Změněno + Složka + Soubor + Soubor %1$s + Jen pro čtení + Zabrání hrám (a Wine) přepsat nebo smazat tento soubor. + Nepodařilo se změnit Jen pro čtení + Skryté + Toto úložiště neumí uložit příznak skrytí. + Označí soubor jako skrytý ve Windows (atribut skrytí Wine). + Nepodařilo se změnit Skryté na tomto úložišti + Hotovo + Mazání... + Smazání se nezdařilo + Přesouvání... + Kopírování... + Přesouvání - %1$s + Kopírování - %1$s + Přesouvání %1$d/%2$d - %3$s + Kopírování %1$d/%2$d - %3$s + %1$d hotovo, %2$d se nezdařilo + %1$d hotovo, %2$d vynecháno + + Přesunuta %1$d položka + Přesunuty %1$d položky + Přesunuto %1$d položek + Přesunuto %1$d položek + + + Zkopírována %1$d položka + Zkopírovány %1$d položky + Zkopírováno %1$d položek + Zkopírováno %1$d položek + + „%1$s“ už existuje + Přejmenovávání... + Přejmenování se nezdařilo + Vytváření složky... + Nepodařilo se vytvořit složku + Nová složka + Název složky + Vytvořit + Nový název + Smazat? + Trvale smazat „%1$s“? + + Smazat %1$d položku? + Smazat %1$d položky? + Smazat %1$d položek? + Smazat %1$d položek? + + Tuto akci nelze vzít zpět. + + …a %1$d další + …a %1$d další + …a %1$d dalších + …a %1$d dalších + + Mazání %1$d/%2$d - %3$s + + Nepodařilo se smazat %1$d položku + Nepodařilo se smazat %1$d položky + Nepodařilo se smazat %1$d položek + Nepodařilo se smazat %1$d položek + + Sloučení přidá a nahradí soubory uvnitř stávající složky. + Přepsání nahradí existující soubor. + Použít pro všechny konflikty + Sloučit + Přepsat + Zachovat obojí + Přeskočit + Vybrat tuto složku + Zpět + %1$s je připojeno, ale momentálně není čitelné + Zobrazení seznamu + Zobrazení mřížky + Hledat + Seřadit + Název + Datum změny + Pohodlné řádky + Kompaktní řádky + Skrýt skryté soubory + Zobrazit skryté soubory + Skrýt oblíbené + Zobrazit oblíbené + Filtrovat tuto složku + Volné %1$s + %1$d vybráno + Žádné + Vše + Vložit + + %1$d položka + %1$d položky + %1$d položek + %1$d položek + + Vložit %1$s sem + Vložit %1$s (přesunout) sem + Soubory + ÚLOŽIŠTĚ + RYCHLÉ + OBLÍBENÉ + Stažené + Stažené + ROMs + Logy relace + Obrázky + „%1$s“ přidáno do oblíbených + „%1$s“ odebráno z oblíbených + Prázdný adresář + O úroveň výš + Vybrat složku + Vybrat soubor + Steam modrá + Nebeská + Bílá + Fialová + Zelená + Ambrová + Červená + Růžová + Tlačítko A + Tlačítko B + Tlačítko X + Tlačítko Y + Levý bumper + Pravý bumper + Levý trigger + Pravý trigger + Tlačítko View + Tlačítko Menu + View + Menu + D-pad nahoru + D-pad dolů + D-pad vlevo + D-pad vpravo + Skryté + Přetažením přesunete + Přichytit + Uložit + Žádný + Klávesnice + trackpad + Terminál + Rukojeti + trackpady Deck + + Odstraňování runtime Linuxu + + Odstraňování runtime Linuxu · %1$d položka + Odstraňování runtime Linuxu · %1$d položky + Odstraňování runtime Linuxu · %1$d položek + Odstraňování runtime Linuxu · %1$d položek + + Runtime Linuxu odstraněn + Nepodařilo se dokončit odstranění runtime Linuxu. Zkuste odstranění znovu v Přípravě. + Odstraňování zbytků runtime + Stahování + Ověřování + Rozbalování + Zobrazuje se při instalaci nebo odstraňování runtime Linuxu + Instalace runtime Linuxu + Odstraňování runtime Linuxu + + Runtime Linuxu není nainstalován + Katalog neobsahuje kontrolní součet pro %1$s + Neshoda kontrolního součtu - nic se nezměnilo + Instalování %1$s + Nepodařilo se umístit AppImage + Rozbalení se nezdařilo + Instalace se nezdařila + + Nastavení Flatpaku se nezdařilo (návratový kód %1$d) + Nastavení Flatpaku se nedokončilo + Běží jiná úloha obchodu + Nejprve nastavte Flatpak + Spouštění + Flatpak skončil s návratovým kódem %1$d + Instalování %1$s + Aktualizování %1$s + Odstraňování %1$s + Instalování %1$s (%2$d/%3$d) + Aktualizování %1$s (%2$d/%3$d) + Odstraňování %1$s (%2$d/%3$d) + překlady + grafické ovladače + multimediální kodeky + runtime %1$s + runtime %1$s %2$s + + Soubor je příliš malý na to, aby byl AppImage + Soubor nelze přečíst: %1$s + Toto není AppImage (nejde o program pro Linux) + Tento AppImage není sestaven ani pro ARM64 (aarch64), ani pro PC s x86 + Toto je program pro Linux, ale ne AppImage + Importovat lze jen AppImage typu 2 (tento je typu %1$d) + Nedostatek volného místa: import potřebuje přibližně %1$s + Nedostatek volného místa: aktualizace potřebuje přibližně %1$s + Import se nezdařil + Tento AppImage už neexistuje + Aktualizace se nezdařila + Rozbalování %1$s + AppImage se nepodařilo rozbalit + AppImage se nepodařilo rozbalit: %1$s + + Před instalací Decky Loader zastavte aktivní relaci + Stažení Decky Loader se nezdařilo + Velikost Decky Loader neodpovídala metadatům vydání + Nepodařilo se stáhnout kontrolní součet vydání + Chybí kontrolní součet vydání + Kontrolní součet vydání je neplatný + Neshoda kontrolního součtu Decky Loader + Soubor vydání není kompatibilní 64bitový spustitelný soubor pro Linux + Během stahování se spustila relace; před instalací Decky Loader ji zastavte + Nepodařilo se označit PluginLoader jako spustitelný + Nepodařilo se povolit vzdálené ladění Steam CEF + Nepodařilo se připravit PluginLoader + Nepodařilo se zachovat stávající PluginLoader + Nepodařilo se aktivovat nový PluginLoader + Před instalací pluginu zastavte aktivní relaci + Před importem pluginu nainstalujte Decky Loader + Nepodařilo se přečíst vybraný soubor ZIP + Nepodařilo se vytvořit adresář pluginů Decky + Rozbalování ZIP s pluginem + ZIP s pluginem musí obsahovat jednu složku pluginu na nejvyšší úrovni + V ZIP s pluginem chybí plugin.json, package.json nebo dist/index.js + Chybí název pluginu + Během instalace se spustila relace; před instalací pluginu ji zastavte + Plugin nainstalován · restartujte Steam, aby se načetl + Instalace ZIP s pluginem se nezdařila + ZIP s pluginem obsahuje příliš mnoho položek + ZIP s pluginem obsahuje neplatnou cestu + ZIP s pluginem obsahuje duplicitní cesty + ZIP s pluginem obsahuje nebezpečnou cestu + Nepodařilo se vytvořit složku pluginu + ZIP s pluginem po rozbalení přesáhne limit 1 GiB + Nepodařilo se označit přibalený binární soubor pluginu jako spustitelný + Neplatná položka remote_binary + Neplatný název vzdáleného binárního souboru + URL vzdáleného binárního souboru musí používat HTTPS + Vzdálený binární soubor %1$s má neplatný hash SHA-256 + Nepodařilo se vytvořit adresář bin pluginu + Stahování binárního souboru pluginu · %1$s + Nepodařilo se stáhnout binární soubor pluginu %1$s + Neshoda kontrolního součtu binárního souboru pluginu · %1$s + Nepodařilo se označit binární soubor pluginu jako spustitelný · %1$s + Nepodařilo se nahradit binární soubor pluginu · %1$s + Nepodařilo se nainstalovat binární soubor pluginu · %1$s + Složka pluginu patří jinému pluginu + Nepodařilo se zachovat stávající plugin + Nepodařilo se aktivovat importovaný plugin + Nepodařilo se aktualizovat nastavení pluginu Decky + Nepodařilo se obnovit stávající plugin + + Nepodařilo se uložit identitu bezdrátového ladění + Příkaz ADB se nezdařil (%1$d) + Android nepotvrdil změnu limitu procesů potomků: %1$s + Není uloženo žádné párování bezdrátového ladění + Zadejte aktuální IP adresu a port bezdrátového ladění + + Čekání na vyskakovací okno pro párování vypršelo. Zkuste to znovu z DroidDecku. + Nepodařilo se vyhledat vyskakovací okno pro párování (%1$d). Zkontrolujte, že je zapnutá Wi-Fi. + Nepodařilo se vyhledat vyskakovací okno pro párování: %1$s + Zadejte všech 6 číslic kódu + Párování… + Tento kód nefungoval. Otevřete znovu vyskakovací okno pro párování a zadejte jeho nový kód. + Zpárováno. Připojování… + Zpárováno, ale port bezdrátového ladění nebyl nalezen. Ponechte bezdrátové ladění zapnuté a zkuste to znovu. + Použití nastavení… + Příkaz bezdrátového ladění se nezdařil + Nastavení bezdrátového ladění + Vyžádá kód pro párování bezdrátového ladění, dokud je otevřené Nastavení + Zpárovat bezdrátové ladění + Zapněte bezdrátové ladění a pak klepněte na „Párování zařízení pomocí párovacího kódu“. + Zapněte bezdrátové ladění a pak klepněte na „Párování zařízení pomocí párovacího kódu“. Zůstaňte v Nastavení: kód se zobrazí v tomto oznámení. + 6místný párovací kód + Klepněte na Zadat kód a napište 6 číslic zobrazených v Nastavení. + Zadejte párovací kód Wi-Fi + Zkusit kód znovu + Zadat kód + Nastavování Steamu + Steam je připraven + Klepnutím se vrátíte do DroidDecku. Bezdrátové ladění teď můžete vypnout. + Nastavení bezdrátového ladění bylo zastaveno + + Stahování odpovědělo HTTP %1$d + Velikost stahování neodpovídala zveřejněnému APK a bylo zahozeno - zkuste to znovu + Stahování neodpovídalo svému kontrolnímu součtu a bylo zahozeno - zkuste to znovu + Aktualizace je pro balíček Androidu %1$s, ne %2$s + Metadata aktualizace neuvádějí podpisový klíč vydání DroidDecku + Android nemohl přečíst stažené APK + Stažené APK je pro %1$s, ne %2$s + Stažené APK uvádí versionCode %1$d, ne zveřejněný %2$d + Android nenainstaluje versionCode %1$d přes nainstalovaný %2$d + Stažené APK nemá žádné informace o podpisu + Stažené APK není podepsáno vydávacím klíčem DroidDecku + Katalog aktualizací odpověděl HTTP %1$d + + Spouštění relace… + %1$d:%2$02d uplynulo · stále pracuje + Stahování aktualizace klienta Steam · %1$d%% + Stahování klienta Steam · %1$s + + Instalace nástroje kompatibility už běží + Před instalací nástrojů kompatibility zastavte aktivní relaci + Nepodařilo se najít vydání ARM64 pro %1$s + Pro instalaci %1$s je potřeba alespoň 4 GB volného místa navíc k velikosti stahování + Stahování %1$s %2$s + Opakování stahování · pokus %1$d z %2$d + Stahování přerušeno + Stahování se nezdařilo; částečný soubor je zachován, aby mohl další pokus navázat + Stahování překročilo velikost vydání a bylo odstraněno + Stahování nebylo dokončeno; částečný soubor je zachován pro navazující opakování + Ověřování stahování + Nepodařilo se přečíst kontrolní součet vydání; zkuste to znovu + %1$s %2$s nezveřejňuje kontrolní součet; nic se nenainstalovalo + Neshoda kontrolního součtu; stahování bylo odstraněno + Během stahování se spustila relace; před instalací nástrojů kompatibility ji zastavte + Instalování %1$s + Instalace %1$s se nezdařila (návratový kód %2$d) + Nepodařilo se nainstalovat %1$s + Stahování balíčku droiddeck-esync + + s pomocnými nástroji pro Linux + Jen DLL + Původní (Proton byl od posledního přepnutí aktualizován) + Změněno mimo Komponenty + není přítomno + Původní %1$s + Původní z %1$s + %1$s %2$s se přenese do %3$s, jakmile se zavře hra, která na něm běží. + %1$s %2$s je teď v %3$s. Použije se při dalším spuštění hry. + Tento původní balíček není uložen + %1$s z %2$s se vrátí na původní verzi, jakmile se zavře hra, která na něm běží. + %1$s z %2$s bylo obnoveno z jeho původní verze %3$s. + %1$s se používá v %2$s. Nejprve ten Proton přepněte na něco jiného. + %1$s není uloženo + Odstraněno %1$s. + Původní verze nainstalovaného sestavení je zachována: je to cesta zpět. + Není uloženo + Odstraněna původní verze %2$s z %1$s. + Stahování pochází jen z vydání Nightlies + Tento seznam balíčků předchází kontrolním součtům - obnovte ho a zkuste to znovu + Neshoda kontrolního součtu + + Lineární + Nejbližší + Snapdragon GSR (kvalita) + Jen zaostření + Automaticky (podle škálování) + + Nic není použito. + Herní jasnost + Zaostření s trochou kontrastu - ten každodenní. + Sytý + Barvy vyhnané na maximum. Deband zapnutý, aby nebe nepruhovalo. + Kinematografický + Hlubší kontrast, chladnější barvy, vyhlazené hrany. + Soutěžní + Zvednuté stíny a silné zaostření - všimněte si lidí první. + Adaptivní zaostření + Jen zaostření, barvy nedotčené. + Filmový + Křivka připomínající film, lehce ztlumená. + Arkáda + Křiklavé a výrazné. + Retro CRT + Efekt CRT trubice a analogového signálu zároveň. + Ostré zvětšení + Pro běh v rozlišení nižším než má displej. + Čisté pixely + Ostré pixely pro 2D a staré hry. + Anime hrany + Kreslené obrysy plus zaostření. + + Není GPU Adreno + Podporováno + Experimentální: jeho ovladače jsou testovací sestavení + Pod testovaným hardwarem (Adreno 725 a novější): nemusí fungovat + Není podporováno: DroidDeck potřebuje GPU Adreno (Snapdragon) + + Byl dosažen limit požadavků GitHubu - zkuste to později + GitHub odpověděl HTTP %1$d + stahování odpovědělo HTTP %1$d + Tento seznam ovladačů předchází kontrolním součtům - obnovte ho a zkuste to znovu + Neshoda kontrolního součtu - stahování bylo zahozeno + Není ovladač AdrenoTools: %1$s + chybí meta.json + meta.json nelze přečíst (%1$s) + toto je ovladač runtime Linuxu (importujte ho v „Ovladač runtime Linuxu“) + toto je herní ovladač pro Wayland, který tato aplikace nepotřebuje + meta.json neuvádí libraryName, takže nic nemohlo být předáno AdrenoTools + meta.json uvádí %1$s, který není v zipu + %1$s není 64bitová sdílená knihovna ELF pro AArch64 + %1$s odkazuje na glibc - jde o Turnip „-Linux“, který patří do „Ovladač runtime Linuxu“ + V tomto zipu není žádný libvulkan_freedreno*.so. ZIP Android (AdrenoTools) nebo -Wayland Turnip není ovladač runtime Linuxu. + %1$s není 64bitová sdílená knihovna ELF pro AArch64. + %1$s není ovladač glibc - odkazuje na libc Androidu. Runtime Linuxu potřebuje zip „-Linux“; běžný nebo „-Wayland“ Turnip nemůže klient Steam vůbec načíst. + Není balíček ovladačů: chybí manifest.json s názvy ovladače pro Android a Linux + Není balíček ovladačů: %1$s odkazuje mimo něj + Není balíček ovladačů: po rozbalení přesáhne %1$d MB + + SD karta + Složka + + Sdílet logy relace + + vyžaduje Android 10 nebo novější (vrstvy displeje s barevným prostorem) + tento displej neuvádí podporu HDR + tento displej podporuje %1$s, ne HDR10 + + toto GPU + Limit procesů potomků je zapnutý + Limit procesů potomků není nastavený + Limit procesů potomků se nepodařilo zkontrolovat + Limit procesů potomků je vypnutý + Limit procesů potomků není vyžadován + %1$s Pokud to není možné, připojte toto zařízení k počítači s ADB a spusťte příkaz níže. Vraťte se sem a zkontrolujte to znovu. + Relace Steamu se mohou spouštět. + Tato verze Androidu tento limit nepoužívá. + Zapněte „Zakázat omezení procesů potomků“ v Možnostech pro vývojáře a pak je nechte zapnuté. + Tato verze Androidu pro to nemá přepínač. Použijte bezdrátové ladění a změňte to z tohoto zařízení. + Toto sestavení nemá stažení pro tuto kopii DroidDecku. + Android to nenainstaluje přes toto sestavení, protože jeho versionCode %1$d je nižší než nainstalovaný %2$d. + Android nenainstaluje starší verzi přes novější. Zůstaňte na tomto sestavení, dokud kanál nedohoní. + Tato kopie DroidDecku je podepsána jinak (lokální sestavení?), takže ji Android nemůže aktualizovat na místě. + Na aktualizaci není dostatek volného místa. + Aktualizace byla zrušena. + Aktualizace se nenainstalovala: %1$s + Aktualizace se nenainstalovala. + %1$s zip + zip + %1$s ještě nebylo spuštěno + Pro %1$s nebyly nalezeny žádné soubory uložených pozic + %1$s neobsahuje složku drive_c, takže to není zip s uloženou pozicí z GameHub nebo Winlator + nelze vytvářet složky v %1$s + nelze zapisovat do %1$s + nelze zapisovat do %1$s (%2$s) + Složka není dostupná + Jiný soubor už používá %1$s + Složka s obrázky není dostupná + Složka obsahuje nesouvisející evidenci synchronizace + Nepodařilo se odstranit %1$s + Soubor opouští exportní složku + FEX (sestavení bez názvu verze) + chybí profile.json v %1$s + %1$s není balíček komponenty pro Linux (typ %2$s) + %1$s · %2$s volných + Automaticky + Katalog aktualizací neprošel kontrolou (%1$s) + Nastavení procesů potomků DroidDecku + Přihlášeno · offline režim + Přihlášeno + PŘIBALENÝ + STAŽENÝ + IMPORTOVANÝ + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Obrazovka relace + Rozlišení + Přizpůsobení hry 16:9 + Jak se hry s poměrem 16:9 přizpůsobí této obrazovce. Příští relace. + Vyberte Podle obrazovky nebo vlastní velikost užší než 16:9. + Zachovat proporce + Roztáhnout na obrazovku + Škálování obrazu + Filtr škálování + Sdíleno Steamem a Plochou. + Použije se okamžitě. Sdíleno Steamem a Plochou. + FSR, GSR a NIS se použijí při zvětšování obrazu relace. Jen zaostření funguje i bez zvětšení. + Ostrost filtru + Snímková frekvence + %1$s Nastaví škálování na %2$s. + Podle obrazovky (%1$d×%2$d) + Sledovat obrazovku (změní velikost při složení nebo rozložení) + Steam + Zobrazit podrobnosti o zařízení + Skrýt podrobnosti o zařízení + Čtení zařízení… + Kopírovat + Vlož tyto údaje do hlášení o chybě. + Zkopírováno. + Vynutit SSBS + Udržuje zapnuté nastavení rychlosti CPU, které Wine stále vypíná. Může zrychlit Windows hry na Snapdragonu 8 Elite; na starších čipech je rozdíl malý. Použije se při příštím spuštění hry. + + Obchody + Steam + Vlastní + Obchody + Otevřít obchod na + Úroveň rychlosti stahování + Jádro obchodů + ● Jádro je připraveno + Není sestaveno + Vyvážené + Rychlé + Maximální + Obchody + Nastavení obchodů + Nastavení + (nepřihlášen) + Obchod + Nainstalované + Knihovna + Vše + Stahování + %1$s (%2$d) + Přihlásit se + %1$s · odhlásit + Odhlásit se z %1$s? + Nainstalované hry zůstanou. Knihovna se znovu stáhne po opětovném přihlášení. + Zůstat přihlášen + Odhlásit se + Hledat v katalogu %1$s… + Hledat v nainstalovaných hrách… + Hledat ve vaší knihovně… + Hledat + Nic není nainstalováno + Načítání + Žádné hry + Načítání obchodu… + Žádný katalog + Novinky + Nabídky + Hry zdarma + Populární + Vaše knihovna + Hrát + Instalovat + Pokračovat v instalaci + Přerušeno + Instalace · %1$s + Získat zdarma + Zobrazit na %1$s + Koupit · %1$s + Nainstalováno + V knihovně + Zdarma + Pokračovat + Obnovit + Zobrazovat obsah pro dospělé + Epic + EOS + Offline + Vypnuto + Přihlášení do Epic (EOS) + Spustit offline + Vyřešit přihlášení do Epic + Cloudové uložené pozice + Odeslat do cloudu + Stáhnout z cloudu + Žádné uložené pozice v cloudu + Konflikt v cloudu + Konflikt v cloudu · %1$d souborů + … + Zachovat cloud + Zachovat lokální + Kontrola cloudu… + Poslední synchronizace %1$s + Ještě nesynchronizováno + %1$d souborů + nainstalováno + ve vaší knihovně + obchod + Achievementy + %1$d z %2$d odemčeno + %1$d / %2$d + Odemčeno · %1$d + Zamčeno · %1$d + Skryto · %1$d + Skrytý achievement + Podrobnosti se zobrazí po odemčení. + Všechny achievementy odemčeny + Načítání… + Přihlaste se k %1$s a zobrazte si své achievementy. + Nepodařilo se spojit s %1$s. Zkontrolujte to znovu, až budete online. + %1$s neuvádí pro tuto hru žádné achievementy. + Zobrazit vše (%1$d) + Achievementy · %1$s + ✓ Ve Steamu + Odinstalovat + Odinstalování… + Odinstalovat %1$s? + Uvolní %1$s a odstraní ji ze Steamu. + Odstraní její soubory a zástupce ve Steamu. + Steam hru zachytí při dalším spuštění; spustit ji odtamtud, nebo otevřít Steam hned. + Proton + Volba Steamu pro každou hru + Zvuk + DirectAudio + Komponenty + FEX, DXVK, VKD3D + Jádro + %1$s · nativní + %1$s · vestavěné + Jak se spouští + Staženo z + %1$s, s vaším účtem + Nainstalováno do + Úložiště her (Games/%1$s/%2$s) + Spouští + Steam a Proton, stejnou cestou jako hry ze Steamu + Steam a Proton · přihlášení do Epic při každém spuštění, překryvná vrstva EOS vypnuta + Steam a Proton · nastaveny proměnné Amazon Games Services + Steam + přidáno do Steamu při instalaci (Deck UI + Steam Input) + Žádná stahování + Pozastaveno + V čekání + V čekání · #%1$d + Zrušeno + Nezdařilo se + Nezdařilo se · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Pozastavit + Pokračovat + Zrušit + Zkusit znovu + Vymazat + Vymazat dokončené + Manifest + Stažení + Ověření + Instalace + Hotovo + Kontrola + Stahování + Odstranit stažení? + Nezdařilo se · %1$s zachováno + Ověřování + Instalování + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d souborů + Správce stahování + Souběžných stahování + Instalovat do + Instalace %1$s: + Interní úložiště + %1$s volných + Stahování z obchodů + Zobrazuje průběh stahování her a udržuje stahování aktivní na pozadí + Stahování + %1$d stahování + Příprava… + Dokončená stahování z obchodů + %1$s nainstalována + Stažení %1$s se nezdařilo + Odstranit + Zachovat + Přihlásit se k %1$s + Přihlásit se přes poskytovatele + Znovu načíst + Zavřít + Zkusit znovu + Dokončování přihlášení… + Přihlašovací stránku se nepodařilo načíst: %1$s + Tento poskytovatel odmítá přihlášení ze vložené stránky. Přihlaste se místo toho heslem k obchodu. + Přihlašovací stránka odpověděla chybou (HTTP %1$d). + Spojení s přihlašovací stránkou není zabezpečené. + Přihlášení se nepodařilo ověřit. Zkuste to znovu. + Přihlášení se nezdařilo. Zkuste to znovu. + Zavřít + Zpět + Upravit + Přidat všechny hry z této složky + V této složce nic nového + Název + Cíl + Spustit v + Možnosti spuštění + Grafika + Jiný exe… + Odstranit + Obálka + Pozadí + Logo + Ikona + Složka hry + Steam + SteamGridDB + Váš soubor + Z exe souboru + Automaticky + Vybrat obrázek… + Vyberte obrázek: %1$s + Obnovit na automatické + + %1$d hra přidána + %1$d hry přidány + %1$d her přidáno + %1$d her přidáno + + + %1$d už je v Hrách + %1$d už jsou v Hrách + %1$d už je v Hrách + %1$d už je v Hrách + + B · Zpět na %1$s + hru + Nastavení spuštění + Grafické ovladače + Tento Proton + Každý Proton + Pro každý Proton + K dispozici · %1$d + Zkontrolováno %1$s + Zkontrolovat nové komponenty a ovladače + Získat + %1$s · DXVK, VKD3D-Proton a FEX níže patří tomuto Protonu + %1$s · testováno + %1$s · netestováno + %1$s · nepodporováno + Udržuje doporučenou dvojici aktuální + Automaticky · %1$s + Ovladače runtime a displeje samostatně + Použít %1$s? + Odstranit %1$s? + Diagnostika + Připraveno ke hraní + Ještě není připraveno ke hraní + runtime %1$s + omezení procesů vypnuto + přihlášen jako %1$s + Když je potřeba váš zásah + Vzhled + Toto zařízení + Grafika knihovny + LINUXOVÉ APLIKACE + Flathub + Desktopové aplikace pro Linuxový desktop, otevírané z Desktopu. + Nastavit Flathub + Odstranit %1$s? + Nastavení a soubory zůstanou zachovány. + Zachovat + Velikost na disku + Odinstalovat desktop? + Vše + Achievementy GOG + Synchronizuje achievementy u podporovaných her GOG. Použije se při dalším spuštění. + Achievementy vypnuty + Achievementy zapnuty + Přihlásit se ke GOG znovu + Achievementy nahrány + Nahrání achievementu čeká + Zapněte achievementy GOG v nastavení obchodů + Achievementy nejsou k dispozici; zkontrolujte protokoly relace + Achievement odemčen + Vložit + Křížový ovladač + Levá páčka + Pravá páčka + Nabídka + Klávesnice + Křížový ovladač nahoru + Křížový ovladač dolů + Křížový ovladač vlevo + Křížový ovladač vpravo + Dotykové ovládání + Dotykový ovladač + Myš + Zapnuto + Vypnuto + Pomalá + Normální + Rychlá + Upravit rozložení + Nabídka DroidDeck + Přetáhněte prvky ovládání na obrazovku + Uspořádat + Zahodit + Ukládání… + Ikona + Š− + Š+ + V− + V+ + Skrýt + Barva a průhlednost… + Kopírovat toto rozložení + Rozložení zkopírováno; vložte ho do dotykového rozložení jakékoli hry + Vložit zkopírované rozložení + Obnovit výchozí rozložení Steamu + Tento prvek nemá ve Steamu žádnou vazbu, které by se dala přiřadit ikona + Ikona pro %1$s + Barva dotykového rozložení + Hotovo + Dotykové rozložení se nepodařilo uložit + Dotykové rozložení uloženo ve Steamu + Aplikace %1$d + Popisek (nepovinné) + Ikony Steamu nebyly nalezeny + + %1$d ikona ze Steamu + %1$d ikony ze Steamu + %1$d ikony ze Steamu + %1$d ikon ze Steamu + + Barva ikony + Barva tlačítka + Bez ikony + + Ověřování přihlášení… + Načítání vaší knihovny… + Načítání seznamu her… + Načítání podrobností o hrách… (%1$d/%2$d) + + Synchronizace %1$d hry… + Synchronizace %1$d her… + Synchronizace %1$d hry… + Synchronizace %1$d her… + + Synchronizace… %1$d/%2$d + Načítání obálek… + Synchronizace se nezdařila + Ve vaší knihovně %1$s nebyly nalezeny žádné hry + Amazon Games nereagovala na vaši knihovnu: obnovte stránku nebo se přihlaste znovu + Nepodařilo se spojit s GOG + Vaši knihovnu GOG se nepodařilo načíst + Registrace ve Steamu… + Cardboard VR + Režim Cardboard + Obraz pro levé a pravé oko pro VR brýle pro telefon. + Obraz + Virtuální obrazovka + 3D vedle sebe + Brýle + Cardboard + Cardboard (2014) + Bez čočky + Vzdálenost očí + Šířka obrazovky + Vzdálenost obrazovky + Sledování hlavy + Toto zařízení nemá senzor otáčení. + Vycentrovat pohled + Vycentrovat + SteamVR (experimentální) + Instalovat + Stáhne SteamVR pro ARM64 od Valve (571 MB). + Stahování a rozbalování SteamVR… + Spustit + Spustí SteamVR s vaší hlavou jako headsetem; obraz se přepne na 3D vedle sebe. + Zastavit + SteamVR běží. + V této relaci není k dispozici. + SteamVR neodpovídá; zkontrolujte protokol relace. + Stáhnout komponenty + Stažené soubory se sdílejí se stránkou Komponenty. Výběrem stažené verze výše ji použijete při dalším spuštění hry. + Vyberte verzi ke stažení + Vývojář + Vydavatel + Vydáno + Hlavní + + Extra + Kompletní + %1$s h + %1$d min + diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml new file mode 100644 index 000000000..1a874c1cd --- /dev/null +++ b/app/src/main/res/values-da/strings.xml @@ -0,0 +1,2235 @@ + + + DroidDeck + + Den første start downloader Steam. Det kan tage nogle minutter. + Som standard sætter Steam på pause, når du forlader appen, og fortsætter, hvor du slap, når du kommer tilbage. + Din controller vises som en Xbox 360-controller. Brug ellers kontrollerne på skærmen. + + + Den første start sætter skrivebordet op. Det kan tage nogle minutter. + Som standard sætter sessionen på pause, når du forlader appen, og fortsætter, hvor du slap, når du kommer tilbage. + Berøring fungerer som et touchpad: Træk for at flytte markøren, tryk for at klikke. + Din controller vises som en Xbox 360-controller. Brug ellers kontrollerne på skærmen. + + Session + Vises, når en Steam- eller skrivebordssession er åben + Steam kører + Steam-download i gang + Session på pause – tryk på Genoptag, når du er klar + Genoptag + Sæt på pause nu + Afslut session + Billedgenerering + Vælg metode til billedgenerering + Fra + Win-FG %1$d× + LSFG %1$d× + LSFG Adaptiv %1$d + Kræver Lossless Scaling på Steam + Installer Lossless Scaling via Steam, eller importer Lossless.dll + Importer Lossless.dll… + Bruger kopien fra Steam + Bruger en importeret kopi + Vælg Lossless.dll + Lossless.dll importeret + Denne Lossless.dll er allerede i brug + Lossless Scaling ejes ikke på denne Steam-konto + Denne fil er ikke Lossless Scalings Lossless.dll + Kunne ikke forberede LSFG fra denne Lossless.dll + LSFG kræver Lossless Scaling fra Steam eller en importeret Lossless.dll + LSFG kunne ikke forberede sine shadere fra Lossless.dll + %1$s: ikke understøttet + %1$s: motoren fejlede + %1$s: kræver Lossless Scaling + %1$s %2$s · starter + Tilføjer et Steam-bibliotek og scanner det for Windows-spil. Ejede spil bruger deres Steam-poster; resten bliver genveje til spil, der ikke er fra Steam. + Spilmiljø + Miljøvariabler for Proton-spil. + Rediger + Alle Proton-spil + Profil + Steam-app-id + Åbn + Spil %1$s + Tilføj variabel + Variabelnavn + Værdi + Gem + Fjern + Nedarv igen + Ryd miljøtilsidesættelser + Fjern for denne profil + Ingen variabler angivet. + Nedarvet + Brugerdefineret variabel + Kunne ikke indlæse eller gemme. Kontroller ledig lagerplads, og prøv igen. + Brug et gyldigt navn og en værdi på op til 8192 tegn. + Den almindelige Proton-runtime læser ikke denne variabel. + Indlæser… + Tilsidesættelse af D3D12-funktionsniveau. 12_2 rapporterer DirectX 12 Ultimate-funktioner; ikke-understøttede funktioner kan fejle. Lad stå tomt for automatisk registrering. + Tilsidesættelse af D3D12-shadermodel. 6_9 kræver en tilstrækkeligt ny VKD3D og matchende GPU-funktioner. Lad stå tomt for automatisk registrering. + Behold Mesa-shader-cache aktiveret for at reducere gentagen kompilering. false aktiverer caching. + Maksimal størrelse for Mesa-shader-cache, f.eks. 1G. Større cache bruger mere lagerplads. + VKD3D-indstillinger for D3D12. nodxr deaktiverer ray tracing; brug kun til et spil, der har brug for det. + Sæt til 1 for en diagnostisk Proton-log. Logning kan reducere ydelsen og bruge lagerplads. + Sæt til 1 for at bruge OpenGL-baseret WineD3D i stedet for DXVK til D3D9–11-kompatibilitetstest. + Sæt til 1 for at holde dette spil væk fra ntsync (droiddeck-ntsync eller en kerneldriver); det bruger droiddeck-esync, hvor en pack findes, ellers droiddeck-fsync. + Sæt til 1 for at slå droiddeck-esync fra for dette spil; det bruger droiddeck-fsync i stedet, eller droiddeck-ntsync hvis fsync også er fra. + Sæt til 1 for at holde dette spil væk fra droiddeck-fsync; det bruger droiddeck-esync, hvor en pack findes, ellers droiddeck-ntsync. + Sæt til 1 for at køre dette spil på droiddeck-fsync, eller 0 for at sætte droiddeck-esync først, hvor en pack passer til dets Proton. + Sæt til 0 for at deaktivere Protons gamepad-navigationshjælper for dette spil. + DXVK-overlay til D3D9–11. fps viser billedhastigheden; lad stå tomt for at skjule den. + DXVK-indstillinger adskilt af semikolon. Nyere builds bruger dxvk.maxFrameRate til at begrænse billedhastigheden; foretræk sessionens billedgrænse til almindeligt spil. + Grænse for D3D12-billedhastighed. 0 deaktiverer grænsen. + Mesa OpenGL-threading. true kan hjælpe CPU-begrænsede OpenGL-spil; test for hvert spil. + Wine-synkronisering + Hvordan spil holder deres tråde i trit. Gælder fra næste session. + esync + ntsync + fsync + wineserver + FEX-forudindstilling + Gælder fra næste spilstart. + Spil + FEX-standard + Bruger FEX-standardindstillinger. + Stabilitet + De sikreste, langsomste indstillinger. + Kompatibilitet + Fuld TSO med multiblock. + Mellemniveau + Afbalanceret TSO med reduceret x87-præcision. + Ydelse + Al TSO deaktiveret; multiblock. + Ydelse + TSO + Ydelsesindstillinger med primær TSO aktiveret. + Ekstrem + Deaktiverer kontrol af selvmodificerende kode. JIT-spil og DRM kan gå i stykker. + Ekstrem + TSO + Ekstreme indstillinger med primær TSO aktiveret. + Ekstrem-gn + Ydelse med TSC og metadata-hints. Prøv før Ekstrem. + Denuvo + Ydelse med fuld kodekontrol og hypervisor-bit skjult. + Brugerdefineret værdi + Vælg værdi + Emulatorer & apps + Tilføj + Din egen app eller spil + Script + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Starter… + %1$s tilføjet + %1$s fjernet + Skrivebord + Den app findes ikke længere. Vælg den igen fra Skrivebord. + Åbn %1$s + Fjern + Tryk igen for at fjerne + Afslut sessionen for at fjerne + Kører %1$s fra dens egen mappe, linket ind i sessionen, hvor den ligger. Fjernes appen, lades mappen urørt. + Kører %1$s fra en kopi af dens mappe inde i DroidDeck. Fjernes appen, slettes kopien, ikke originalen. + Installer Linux-runtimen først + Filen findes ikke længere + Ikke nok ledig plads: kopiering af mappen kræver %1$s + Kopierer %1$s + Det mislykkedes at tilføje appen + Søger efter en udgivelse af %1$s + Kunne ikke læse udgivelserne for %1$s + Ingen udgivelse af %1$s har en ARM64- eller x86_64-AppImage + Downloader %1$s + Download mislykkedes + Det mislykkedes at opdatere appen + Det ikon kunne ikke læses + %1$s gemt + Rediger + Søg efter opdateringer + Søger efter opdateringer… + Opdater til %1$s + Opdateret · %1$s + %1$s opdateret + %1$s · %2$s + Tilføj en app + Script + AppImage + GitHub + Flatpak + En .sh-fil. Den køres fra sin egen mappe i fuld skærm under gamescope. + En ARM64 (aarch64) AppImage, eller en x86_64-udgave, der køres via FEX. Den udpakkes i Linux-runtimen én gang. + Installerer den nyeste udgivelse af ARM64-AppImage fra repositoryet, eller x86_64-udgaven via FEX. + Brug FEX + Auto + Til + Fra + FEX kører x86-Linux-programmer på denne ARM64-enhed. Auto bruger det, når programmet er bygget til x86. + %1$s via FEX + Dette program er bygget til %1$s-pc’er og køres via FEX, som ikke er sat op endnu. Åbn Steam én gang, mens du er logget ind: DroidDeck downloader så FEX og den Steam Linux Runtime, det kræver, hvorefter dette åbner. + Dette program + %1$s er bygget til x86-pc’er og køres via FEX, som ikke er installeret endnu. Start Steam én gang, mens du er logget ind, og lad det downloade FEX og Steam Linux Runtime, og start så dette igen. + Et Flathub-app-id eller dens Flathub-side. Flatpak sættes op først, hvis det ikke allerede er. + Fil + Ingen fil valgt + Vælg fil + Skift + Vælg et script + Vælg en AppImage + Vælg et ikon + Repository + ejer/repo eller et github.com-link + Ikke et GitHub-repository + App-id + org.ppsspp.PPSSPP + Ikke et Flathub-app-id + Eller en .flatpak-fil til en app, der ikke findes på Flathub. + Vælg en .flatpak-fil + Kilde + På dit skrivebord + Ny app + Navn + Fra appen + Ikon + Standardikon + Vælg ikon + Ryd + Fra repositoryet + Søger efter ikoner… + Standard + Rediger %1$s + Gem + Kontrollerer mappen… + Dens mappe er linket ind i sessionen: %1$s + Dens mappe indeholder Linux-programmer, som delt lager ikke kan køre, så den kopieres ind i DroidDeck (%1$s). + Tilføj + Annuller + Installer Linux-runtimen fra Opsætning først. + Vent på, at %1$s er færdig. + Kompatibel + Builds markeret med “%1$s” i Steams kompatibilitetsmenu har DroidDecks prefix-fixes og kan starte spil på denne enhed. Standarden er Proton Experimental ARM64 (%1$s), eller Proton 11.0 ARM64 (%1$s), når den nyere build er installeret. Valves egne Proton-poster uden mærket kan ikke starte et spil på denne enhed. + Installeret %1$s · %2$s + Annuller + Auto + Aldrig + Altid + Afinstaller + Kontroller + Opdater + Kontrollerer… + Arbejder… + Glem + Tilføj… + Brug + Gælder fra næste session. + Steam-session + Skrivebordssession + Skærm + Opløsning + Op til 720p + Op til 900p + Op til 1080p + Native + %1$s (standard) + Brugerdefineret · %1$d×%2$d + Brugerdefineret… + 720p kan gøre menuer mere lydhøre. + Billedformat + Angivet af den brugerdefinerede opløsning. + Auto bruger mindst 16:9. + Billedgrænse + Begrænser hele sessionen. Skærmen skifter til en opdateringshastighed, grænsen går lige op med. + HDR + HDR10-output + Ikke tilgængelig: %1$s. + Genstart appen for at anvende ændringen. + Drivere + GPU-drivere + Delt mellem Steam og Skrivebord. Indstilles på siden Komponenter. + Berøring & controller + Berøring + Touchpad + Direkte + Auto: touchpad på Skrivebord, direkte i Steam. + Kontroller på skærmen + Steam + QAM + Auto viser kontrollerne, når ingen controller er tilsluttet. + Steam-berøring (beta) + Steams berøringskontroller starter med næste Steam-session + Steam-berøringslayout + Flyt, tilpas størrelsen på eller skjul dette spils berøringskontroller; gemmes til Steam + Controller + Hvordan Steam ser din controller. Gælder fra næste session. + Steam Deck-controller + Steam Controller + DualShock 4 + DualSense + Xbox 360-controller + Deck: Hurtig adgang-knap og gyro. Steam Controller: det samme, med gribefølsomme bagknapper. DualShock 4 og DualSense: gyro, og touchpaddene som deres touchpad. Xbox 360: åbn Hurtig adgang med Guide+A. + Tilbage + Menu → QAM + QAM → Menu + Session + Baggrundsopførsel + Når appen er skjult, eller skærmen slukkes. + Manuel + Native + Auto sætter på pause og genoptager, når den vises. Native forbereder Steam til dvale først. Manuel venter på Genoptag. Aldrig fortsætter med at køre. + Fortsæt downloads + Udskyder baggrundspause, mens Steam aktivt downloader. + Auto sætter på pause i baggrunden og genoptager, når den vises. Manuel venter på Genoptag. Aldrig fortsætter med at køre. + Opstart + Start Steam med DroidDeck + Åbner Steam-sessionen, når DroidDeck starter. + Decky + Søger efter udgivelser… + Klar til at installere %1$s. + Ingen kompatibel build fundet. Prøv igen senere. + Installeret · %1$s + Opdatering tilgængelig · %1$s + Opdateret · %1$s + Loader + Installer nyeste + Starter med Steam. Mens den er til, kan andre apps på denne enhed muligvis styre Steam. + Fra. Steams fjerndebugport forbliver lukket. + Afinstaller Decky Loader? + Fjerner loaderen. Dine plugins og indstillinger bevares. + Klient + Ydelsesoverlay + Slå fra, hvis spil går ned eller bliver sorte i Deck-tilstand. Gælder fra næste session. + Reparer Steam-klienten + Downloader Steam-klienten igen, næste gang Steam starter. Brug det, hvis Steam bliver hængende på sin indlæsningsskærm. Spil, login og indstillinger bevares. + Steam-klienten downloades igen, næste gang Steam starter. + Reparer + Grafik + SteamGridDB API-nøgle + Din nøgle + Rediger… + Udfylder manglende grafik fra Steam, derefter SteamGridDB. Din egen cover.jpg, hero.jpg, logo.png eller icon.png i spillets mappe har forrang. + Fjern %1$s + Fjern? + Tilføj spil + Tilføj et spil: + Vælg exe-filen: + Denne placering er ikke tilgængelig i sessionen + Kontroller denne exe-fil + Windows-komponenter + LB/RB skifter mellem afsnittene. + Ingen + Windows-dll’er, Proton ikke inkluderer, fra komponentlisten. Slå dem til, som dette spil har brug for: de lægges ind i spillets egen Windows-mappe, aldrig dets egne filer, og følger med, uanset hvilken Proton det køres på. + Kunne ikke nå komponentlisten. Komponenter, der allerede er downloadet, kan stadig skiftes. + Anbefalet til dette spil + Kontrollerer, hvad dette spil har brug for… + Tilgængelig online + + Slår %1$d komponent til… + Slår %1$d komponenter til… + + Til for dette spil + Steam installerer %1$s med %2$s (app %3$s) + Fra GOG’s liste for dette spil + Fra Epics forudsætninger for dette spil + Fra Amazons installationstrin for dette spil + Steam installerer %1$s med app %2$s, matchet efter navn + Også på listen + Angivet af %1$s; ikke en komponent her + Steam + GOG + Epic Games + Amazon Games + Spillet leveres med %1$s + Spillet har %1$s (fungerer tit som det er) + Allerede i dette spil + Intet i spillets mappe peger på en komponent. + Installeret til dette spil (%1$d) + Alle komponenter (%1$d) + Kræver et Windows-installationsprogram (%1$d) + Disse er deres egne installationsprogrammer uden en pakke indeni at tage fra hinanden, og der er endnu ikke registreret, hvad de installerer. De fleste er gamle runtimes, som Mono inde i Proton allerede dækker; registreringer tilføjes, når de bliver lavet. + Vis + Skjul + Kører sit eget installationsprogram (endnu ikke registreret) + Ikke understøttet her endnu + Indlæser komponenterne… + Kunne ikke installere: %1$s + Downloadet + Downloader %1$s + Installerer: %1$s + Slår fra… + %1$s download + Træder i kraft, næste gang spillet starter. + Stræk 16:9-spil + På skærme mere kvadratiske end 16:9 strækkes spil, så de udfylder skærmen, i stedet for at vise sorte bjælker. Gælder fra næste session. + Lyd + DirectAudio til spil + Går uden om PulseAudio for lavere forsinkelse. + Mikrofon + Til voicechat. + Netværk + Wi-Fi-søgning + Vis Wi-Fi-navne og netværk i nærheden i Steam. + Placeringstjenester er fra. + Placeringstilladelse ikke givet. + Åbn indstillinger + Fortsæt + Aktiver Wi-Fi-søgning? + Android kræver placeringstilladelse for at læse Wi-Fi-navne og finde netværk i nærheden. DroidDeck bruger denne adgang til at vise disse netværk i Steam. Vi registrerer ikke din placering. Internetadgang fungerer uden denne tilladelse. + Spillager + Automatisk – SD-kortet, når det er isat, plus dine Spil-mapper + Automatisk – dine Spil-mapper, plus SD-kortet, når det er isat (ingen nu) + Kun internt + Mappe: %1$s + Andet bibliotek + Spil, der streamer data fra SD-kort eller delt lager, kan stamme. + Lagerdiagnostik + Registrerer allokeringsresultater og lagertider i Del logs. Kan gøre installationer langsommere, mens det er aktivt. Gælder fra næste session. + Vælg en mappe… + Brugerdefineret opløsning + Erstatter grænsen og billedformatet. En størrelse, der ikke matcher skærmen, giver sorte bjælker. + Bredde + Højde + Mellem 320×240 og 3840×2160. + + %1$d spil + %1$d spil + + OK + Slet + Gendan + Download + Slet %1$s + %1$.1f MB + Runtimedriver + Bruges af Steam, spil og skrivebordet. Gælder fra næste session. + Importer Turnip-zip… + Skærmdriver + Tegner billeder til skærmen i begge tilstande. Genstart appen for at anvende. + Importer en AdrenoTools-zip… + aldrig + Nightlies kontrolleret %1$s + Komponenter + Om Komponenter + Søg efter nye drivere + Søg Nightlies for nye pakker + Proton + Den Proton, der vælges her, kører alle spil, der ikke selv har valgt en i Steam, og en ændring foretaget i Steam vises også her. Steam erstatter Valves Proton, når den opdaterer; originalerne bevares for hver build. + Konfiguration + Konfigurer profil + Profil + Standardprofil + Bruges automatisk af spil uden tilsidesættelser. + Gemte indstillinger bruges automatisk, når %1$s starter. + Vælg kun de indstillinger, dette spil har brug for. Alt andet fortsætter med at bruge standardprofilen. + FEX-forudindstilling + Bruger standardprofilens forudindstilling. + Brugerdefineret forudindstilling gemt for dette spil. + Brugerdefinerede FEX-variabler + Brug standard + Spilkompatibilitet + Miljøindstillinger for Proton, DXVK, VKD3D og synkronisering for dette spil. + Bruger standardprofilens Proton. + Brugerdefineret Proton gemt for dette spil via Steams kompatibilitetstilknytning. + Ingen Proton + DXVK + VKD3D-Proton + Bruger standardprofilens pakke fra %1$s, hvis ikke dette spil vælger en anden pakke. + Bruger standardprofilens komponentpakke. + Bruger kun %1$s til dette spil. + Runtime-indstillinger + Andre miljøindstillinger pr. spil for Proton, DXVK, VKD3D-Proton og synkronisering. + %1$s’s profil er gemt til næste spilstart. + Start uden Steam + Til et spil, der lukker med det samme: starter det uden Protons Steam-opsætning. Steam-funktioner som overlay er slået fra for det. + Til + %1$s’s Proton-valg er gemt og tilgængeligt i Steam nu. + %1$s’s Proton-valg er gemt. Genstart Steam, inden du starter spillet. + %1$s kan ikke bruge den valgte Proton på denne enhed. + Kunne ikke indlæse %1$s’s profil: %2$s + Spilprofilen kunne ikke gemmes: %1$s + Nulstil spilprofil + Nulstil %1$s’s profil? + Fjern dette spils tilsidesættelser af Proton, FEX, komponentpakke, miljø, teksturfiltrering og Windows-komponenter? Det bruger herefter standardprofilen. + %1$s bruger nu standardprofilen. + Spilprofilen kunne ikke nulstilles helt: %1$s + %1$s kører nu alle spil, der ikke selv har valgt en Proton i Steam. + %1$s kører alle spil, der ikke selv har valgt en Proton, fra næste gang Steam starter. + %1$s kan ikke starte spil her, så den kan ikke være standarden. + Standard-Protonen kunne ikke gemmes: %1$s + %1$s · %2$d udskiftet + %1$s · alt originalt + %1$s · spil kører + %1$s · droiddeck-esync-pack + %1$s · ingen droiddeck-esync-pack endnu + %1$s · esync er indbygget, ingen pack nødvendig + GPU-drivere + %1$s · genanvendt ved start %2$s + Næste: %1$s · når spillet er lukket + %1$s i brug: %2$s + Venter til spillet lukkes + Gælder, næste gang et spil starter + Importer .zip + Læser Protons… + Ingen Proton installeret endnu. Start Steam én gang for at downloade dens ARM64-Proton, eller tilføj GE-Proton, CachyOS eller DroidDeck-Proton fra Opsætning. + Original · %1$s + %1$s · denne build + %1$s · tidligere build + Installeret · %1$d + Intet gemt for %1$s endnu. Denne Protons egne filer gemmes som dens original. + Et spil kører på denne Proton, så dette venter, til det lukkes. + Gælder, næste gang et spil starter. + Udskift %1$s? + Læg %1$s ind i %2$s? De originale filer forbliver gemt. %3$s + Gendan %1$s? + Læg originalen af %1$s tilbage i %2$s? %3$s + Fjerner pakken fra appen. Du kan downloade den igen. + Slet denne original? + Denne original af %1$s er fra en tidligere build af %2$s. Den aktuelle builds original bevares altid. + På Nightlies · %1$d + Opdater for at se pakker på Nightlies. + Intet nyt siden sidste kontrol. + Importer .wcp + Gendan denne Protons original + Læg %1$s’s original %2$s tilbage? + Udskift + FEX, DXVK og VKD3D-Proton for hver Proton, Steam kører spil med.\n\nVælg en installeret række for at sætte den i. Den træder i kraft, næste gang et spil starter, eller når det kørende spil er lukket.\n\nDine valg kontrolleres igen før hver start og lægges tilbage, hvis noget har ændret dem. Hver Protons originale filer bevares, så Steam-opdateringer aldrig fjerner dem. + Original + I brug + Ny + Gemt + Ydelse + CPU-kerner, sessionsrettelser og værtsnavn. Gælder fra næste session. + Steam-klientens kerner + Tilsidesæt Steams kernevalg + Fastgør Steam, dens UI-hjælper og gamescope til kernerne nedenfor. + Klientkerner + Kerner til Steam. + Slå tilsidesættelsen til for at vælge. + Spilkerner + Spilkerner + Kerner til spil. Med alle valgt planlægger Android frit. + Klientgrænseflade + Threaded GL + Kan gøre Steam-menuer mere lydhøre. + Zink: lazy descriptors + Anbefalet til drivere uden descriptor buffers. + Spring GL-fejlkontrol over + Slår validering af GL fra for hvert kald. + gamescope: realtids-GPU-kø + Prioriterer compositorens GPU-arbejde. Kan give jævnere billedtiming på bekostning af spillene. + GPU + Hold GPU’en på dens højeste clock + Kun Adreno. Færre hak, mere batteribrug og varme. + Sessionsrettelser + Turnip: sysmem-rendering + Hurtigere til Steam og de fleste spil. Påkrævet på Adreno 710/720/722. + Spring Steams xalia-hjælper over + droiddeck-ntsync (eksperimentel) + En stand-in til ntsync-driveren i Linux-kernen, i brugerrum, til test. Mens den er til, bruger Proton den i stedet for droiddeck-esync og droiddeck-fsync. Fungerer med Proton Experimental ARM64, GE-Proton og proton-cachyos. Gælder fra næste session. + droiddeck-esync + droiddeck-fsync først + Kører Protons egen fsync på de uændrede Proton-filer, med futex_waitv leveret i brugerrum, så det kræver ingen pack og fortsætter med at virke, lige så snart Steam opdaterer Proton. Mens den er til, bruger spil den før droiddeck-esync, undtagen de få, Proton holder uden for fsync. Gælder fra næste session. + Spils Windows-tråde venter på hinanden via eventfd og delt hukommelse i stedet for ture gennem wineserver. Bruger en droiddeck-esync-pack lavet til netop denne Proton-build; packs hentes automatisk. Indtil der findes en til en ny Proton-build, dækker droiddeck-fsync i stedet. Gælder fra næste session. + Deaktiverer Protons gamepad-navigationshjælper, som koster CPU i hvert spil. + Kør proot uden seccomp + Kan afhjælpe fejl om manglende systemkald på nogle kerner. Kan reducere ydelsen. + proot fast path + Kræver prootets seccomp-filter. + Hurtigere filopslag og mindre hak. Slå fra, hvis et program ikke kan finde sine filer. + Sessionsidentitet + Værtsnavn + Navnet Steam ser for denne enhed. Tomt gendanner %1$s. + Kun bogstaver, tal og bindestreger i midten, op til 63 tegn. + Android er sat til at lukke denne session + Installeret + Tilgængelig til download + Intet nyt siden sidste kontrol. + Gendan indbyggede drivere + Indbygget i appen: dette fjerner dens udpakkede filer og skjuler den. Gendan indbyggede drivere bringer den tilbage. + Halvdelen af en Android- og Linux-bundle: begge halvdele fjernes. En driver i brug falder tilbage til standarden. + Dens filer fjernes fra appen. + Dens filer fjernes. Den er i brug, så standarden tager over. + Skærm + Effekter + Kontroller + Komponenter + Controller + Indstillinger + Siden %1$s + Steam + Skrivebord + Steam-menu + Hurtig adgang + Åbn Hurtig adgang-menuen + Forrige side + Næste side + Ydelses-HUD + Skaleringstilstand + Skaleringsskarphed + Skærmeffekter + Live. Koster lidt GPU-tid, mens en effekt er til. + Forudindstillinger + Brugerdefineret + Avanceret + Udvidet + Sammenfoldet + Skarphed (CAS) + Skarphedsniveau + Falsk HDR + Debanding + Debanding-styrke + Lysstyrke + Kontrast + Gamma + Farvemætning + FXAA (kantudjævning) + Toon-konturer + CRT + NTSC + Teksturfiltrering + DirectX 9–11-spil, fra deres næste start. + Anisotropisk filtrering + Teksturskarphed + En negativ mip-bias: skarpere teksturer, lidt flimmer. Auto følger, hvor meget sessionen forstørres til skærmen. + Auto (%1$s) + Tastatur + PC-tastatur + Android + Anden skærm + Tilstand + Viser + Android-apps + Start en app + Skjul + Vis + Ingen apps, der kan startes + Sessionsopførsel + Gælder nu og for fremtidige sessioner i denne tilstand. + Næste session + Support + Sessionslogs + Vedhæft til en fejlrapport + Del logs + Baggrund + Stop + Afslut session? + Kører nu + Intet spil kører på en Proton + Spilprofil + Profildetaljer er ikke tilgængelige endnu. + Kunne ikke indlæse spilprofiler. Forsøger automatisk igen. + Kunne ikke opdatere spilprofiler. Viser de sidst tilgængelige data; forsøger automatisk igen. + Vis og rediger de gemte indstillinger, der bruges ved næste start. + Bruger standardprofilen + Brugerdefineret profil + Bruger %1$s fra standardprofilen. + Ændringer gemmes i %1$s’s profil og gælder, næste gang spillet starter. + + %1$d spiltilsidesættelse + %1$d spiltilsidesættelser + + Ingen Proton er installeret endnu. + spil kører + genanvendt ved start %1$s + Original + Importer og slet på siden Komponenter. + Klar + Kræver handling + Arbejder + Kontroller IP-adressen og porten for Trådløs fejlretning, eller par igen, hvis Android har fjernet denne enhed. + Installer + Administrer + Oversigt + Controller + Session + Launcher + Opsætning + ● Alt er klar + %1$d af %2$d klar + Systemkontrol + Enhed understøttet + Utestet GPU + GPU ikke understøttet + %1$s · %2$s + %1$s: %2$s + Steam kræver en Adreno-driver; %1$s kan vise en sort skærm + Linux-runtime + %1$s · %2$d%% + Ikke installeret · omkring 3 GB, installeres ved første Spil + Installeret + %1$s · opdatering tilgængelig + %1$s · opdateret + Grænse for underprocesser + Til · Android kan lukke Steam. Det tager et minut at slå den fra. + Ikke angivet · Android kan lukke Steam. Det tager et minut at slå den fra. + Kunne ikke kontrolleres · det tager et minut at slå den fra. + Ret det + Detaljer + Udviklerindstillinger + Brug Trådløs fejlretning + Kontroller igen + Slå grænsen til + Steam-konto + Logget ind som %1$s · offlinetilstand + Logget ind som %1$s + Tryk på Spil, og log ind på Steam + Værktøjer + Controller + Controllerindstillinger er ikke tilgængelige. + Session + Sessionslogs + Gemt efter hver session + Fejlfindingsværktøjer + Seneste sessionslogs + Gemte sessionslogs + Gemmer de nyeste %1$d + Ryd logs + Offlinetilstand + Log ind på Steam først + Launcher + Sprog + Steam bruger det også, fra næste gang det starter. + Systemstandard (%1$s) + Systemstandard + App-skalering + Tekst og kontroller i DroidDeck + %1$d%% (standard) + Tema + Launcherens udseende + Brug som startskærm + DroidDeck kan være Home-appen + DroidDeck tilbydes aldrig som Home-app + Retning + Automatisk liggende + Liggende + Omvendt liggende + Fuldskærm + Skjuler Androids status- og navigationsbjælker + Viser Androids status- og navigationsbjælker + Standard Home-app + Vælg en Home-app + Vælg + Linux-apps (beta) + Filer + Gennemse og administrer filer + Proton-versioner + Installer ARM64-Proton-builds + Ydelse + Tildeling af CPU-kerner + ROM-mappe + Hvor emulatorspil gemmes + Komponenter + FEX, DXVK, VKD3D + Kontroller + Knapmapning + Udforsk + Søg + Installeret + %1$s · %2$d + Butik + Flathub · ARM64 + ● Flatpak klar + Installer Linux-runtimen fra Opsætning først. + Sæt Flatpak op + Tilføjer Flatpak og forbinder det til Flathub (ca. 20 MB). Den første app downloader også sin runtime. + Sætter op… + Flatpak + Opdateringer + %1$s · %2$s + %1$s · %2$s · %3$d%% + Starter… + Kategorier + Kunne ikke nå Flathub. Kontroller din forbindelse, og prøv igen. + Prøv igen + Søg på Flathub + Firefox, spil, emulatorer… + Søg + Alle + Søger… + Søg efter navn, eller vælg en kategori. Kun ARM64-apps vises. + Ingen ARM64-apps på Flathub matcher det. + Resultater + Søg efter opdateringer + Opdater alle (%1$d) + ● Alt er opdateret + Ingen apps endnu. Find nogle under Udforsk eller Søg. + Sæt Flatpak op for at installere apps. + Opdatering tilgængelig · %1$s + Åbn + Detaljer + Fjern + Tryk igen for at fjerne + Installerer… + Installer + Sæt Flatpak op først + %1$s download + ● Installeret + Kunne ikke indlæse denne apps Flathub-side. + Indlæser fra Flathub… + Ingen ARM64-build på Flathub, så den kan ikke installeres her. + Skærmbilleder + Om + App-id + Licens + Runtime + Installeret størrelse + %1$s (plus dens delte runtime) + Websted + Verificeret + Opdateringer + DroidDeck %1$s · %2$s + Kontrolleret %1$s + Ikke kontrolleret endnu + Lad DroidDeck opdatere sig selv + På næste skærm skal du slå Tillad fra denne kilde til og derefter komme tilbage. Opdateringen fortsætter selv. + Åbn indstillinger + Ikke nu + Signeret anderledes + Kan ikke opdatere på plads + Denne kopi er ikke signeret med DroidDecks udgivelsesnøgle, så opdateringer kan ikke installeres over den. Afinstaller den for at skifte. + Opdatering tilgængelig + Kan ikke installere + Klar til at skifte + Foran Stable + Du er foran Stable + Denne build er nyere end den seneste Stable. Du flyttes til Stable med dens næste udgivelse. + Test afsluttet + Denne test er afsluttet + Dens rettelse er føjet ind eller opgivet. Følg Preview for de nyeste rettelser. + Opdateret + Du har den nyeste %1$s + · %1$s + %1$s… %2$d%% + %1$s… + Opdater%1$s + Installer%1$s + Installer Stable %1$s alligevel + Følg Preview + Afslut den kørende session for at opdatere. + Mindre + Mere + DroidDeck %1$s + Ny Preview-build + PR #%1$d-testbuild + %1$d MB + OPDATERINGSKANAL + Stable + Testede udgivelser, til de fleste + %1$s · %2$s + Preview + Nyeste rettelser, før Stable + Opdateret + Testbuilds + Prøv en rettelse, før den udgives + Ingen lige nu + PR #%1$d + Ikke bygget til denne kopi af DroidDeck + Opdateret %1$s + PR #%1$d-testen + et stykke tid siden + lige nu + + %1$d at prøve + %1$d at prøve + + + Ændringer siden din build · %1$d build bagud + Ændringer siden din build · %1$d builds bagud + + + Seneste ændringer · %1$d build + Seneste ændringer · %1$d builds + + Én Android-indstilling, der skal ændres + Steam + Android lukker apps, der starter mange baggrundsprocesser, og Steam starter dusinvis. Indtil dette ændres, lukker spil uden en fejlmeddelelse. + Ret det for mig + Slå “Deaktiver begrænsninger for underprocesser” til, og kom så tilbage. Lad Udviklerindstillinger stå til, ellers slås kontakten fra igen. + Åbn Udviklerindstillinger + Denne Android-version har ingen kontakt til det, så DroidDeck ændrer det via Trådløs fejlretning. Tager omkring et minut, ingen computer nødvendig. + Trådløs fejlretning kræver Wi-Fi. Alle netværk fungerer; det kræver ikke internet. + Parringskoden kommer i en notifikation. Tillad notifikationer for DroidDeck, eller brug Andre måder. + Tillad notifikationer + Åbn Indstillinger → Udviklerindstillinger → Trådløs fejlretning, og slå den til + Tryk på “Par enhed med parringskode” + Indtast koden i DroidDeck-notifikationen + Venter på parringsvinduet… + Vindue fundet. Indtast dets kode i notifikationen. + Status + Kontrollerer hvert 2. sekund. Denne side lukker, når den er fra. + Andre måder + Trådløs fejlretning, manuel adresse, computer-ADB + Manuel adresse, computer-ADB, Udviklerindstillinger + Brug Trådløs fejlretning i stedet + Nogle ROM’er tilføjer kontakten alligevel. Kig efter “underprocesser” i Udviklerindstillinger. + Hvis notifikationen ikke virker, kan du selv indtaste vinduets adresse og kode. + Indtast adresse manuelt + Fra en computer med ADB: + Kopier kommando + Vælg, hvilken skærm Android-indstillinger skal åbnes på. + Hovedskærm + Parret. Finder porten for Trådløs fejlretning… + Parret. Indtast porten fra Indstillinger. + Enhed fundet. Anvender indstillingen… + Kunne ikke anvendes automatisk. Kontroller adressen nedenfor, og prøv igen. %1$s + Trådløs fejlretning + Grænsen for underprocesser opdateret + Ændr grænsen for underprocesser + Hvis indstillingen mangler, skal du parre via Trådløs fejlretning. + Parret. Kontroller IP-adressen og porten i Trådløs fejlretning. + Grænsen for underprocesser blev opdateret. + Brug trin 2, hvis Udviklerindstillinger ikke har en indstilling for underprocesser. Åbn Udviklerindstillinger → Trådløs fejlretning → Par enhed med parringskode. + Parret. Brug adressen nedenfor, eller opdater den fra Trådløs fejlretning → IP-adresse og port. + Grænsen for underprocesser blev opdateret via Trådløs fejlretning. + Android bekræftede, at grænsen er til. + Android bekræftede, at grænsen er fra. + 2 · Par Trådløs fejlretning + Parringsvinduets IP-adresse og port + Parringskode (PIN) + 6 cifre + Par + 2 · Anvend via Trådløs fejlretning + Forbindelsens IP-adresse og port + Par igen + Slå grænsen fra + Andre måder at ændre det + 1 · Udviklerindstillinger (prøv først) + Slå “Begræns underprocesser” til. + Slå “Begræns underprocesser” fra. + 3 · Computer-ADB (sidste udvej) + Kun hvis Trådløs fejlretning ikke er tilgængelig. + 3 · Computer-ADB (sidste udvej) + Hvornår berøringskontrollerne vises i en session + Farve + Farvetone for knapperne på skærmen + Brugerdefineret + Gennemsigtighed + %1$d%% + Knapstørrelse + 100% er standardstørrelsen + Stick-klik + Dobbelttryk og hold en stick nede for L3 eller R3 + Adaptive sticks + Spilvibration + Steam-knap + QAM-knap + Tastaturknap + Tryk på Select for at åbne Steam-menuen + I stedet for View/Select. + Animationer + Sticks vises, hvor du rører skærmen, og skjules, når du slipper + Layout + Brugerdefinerede positioner gemt + Tilpasset denne skærm + Rediger + Nulstil + Knapmapning + Hver knap sender sit eget input + %1$d af %2$d knapper ommappet + Konfigurer + Nulstil controller + Gendanner alle controllerindstillinger og layoutet + Hvad hver knap på skærmen sender til spillet. Skjult fjerner knappen. + Knapper på skærmen + Standarder + Nulstil mapning + Android-apps + %1$d apps + Ingen Android-apps, der kan startes, fundet. + ● Skrivebord installeret + Installeres ved første åbning + Tilgængelig til installation + Ikke installeret. + %1$s · ikke installeret + Åbn %1$s + Spil + Vælg en ROM-mappe. + Gå til /root/ROMs i RetroArch. + Tilføj %1$s-spil til ROMs/%2$s. + Installer %1$s + Afslut session for at installere + Runtime påkrævet + Indlæser installationsdetaljer… + Installationsdetaljer er ikke tilgængelige. Prøv igen senere. + Installer Linux-runtimen fra Opsætning først. + Afslut den kørende session for at installere skrivebordsapps. + Installerer %1$s i Linux-skrivebordet. + %1$s · %2$d%% + Hvis den skjules, fjernes den fra Skrivebord. Dens filer forbliver i Linux-runtimen. + Det spil er ikke længere i ROM-mappen. + Skrivebord · %1$s + Start i %1$s + Også i %1$s + Den side findes ikke længere. Vælg et afsnit til venstre. + Åbn skrivebord + Installer og åbn skrivebord + Afinstaller skrivebord + Fjerner skrivebordet og dets medfølgende apps. Spil, tilføjede apps, indstillinger og delte biblioteker bevares. + Fjerner skrivebord + Skrivebord fjernet + Fjerner gamle LXQt-filer + Opdater DroidDeck, før du fjerner denne skrivebordsversion. + Linux-skrivebord + KDE Plasma, Firefox og dine emulatorer + Spil, du installerer i Steam, vises her efter sessionen. + Spil Steam + Startindstillinger + Start + Spilfiler + Proton-prefix + Søger efter gemte spil… + Ingen savemappe fundet ved navn. Eksport tager hele brugermappen. + %1$d savemapper + %1$s · %2$d filer · %3$s + %1$d KB + Administrer gemte spil + Gemte spil + Importer gemte spil… + Eksporter som GameHub-zip… + Eksporter som Winlator-zip… + Åbn savemappe + Aldrig spillet + Runtime optaget · %1$d%% + Runtime optaget + Runtime installeres ved første Spil + Runtimeopdatering tilgængelig + ● Runtime klar + Sidst spillet %1$s + Internt lager + Tilføjet spil + Brugerdefineret spil + Spillager + Det bedste par til denne GPU, holdt opdateret + Vælg et par selv + Søger efter drivere… + Ingen drivere på listen endnu. Opdater for at søge efter dem. + Skjul drivere til andre GPU’er + Vis drivere til andre GPU’er + Avanceret + Angives sammen med skærmdriveren: %1$s dækker begge. + Angives sammen med runtimedriveren: %1$s dækker begge. + Bruges af spil og skrivebordet. + Runtime-standard + Tegner billeder til skærmen. Gælder, når DroidDeck genstarter. + Auto – valgt efter GPU + Download, importer eller slet… + I BRUG + Driverne indbygget i DroidDeck + Runtime- og skærmdrivere angivet sammen som et matchende par. + Anbefalet til denne GPU: %1$s. + Opdater for at søge efter drivere til denne GPU. + ANBEFALET + Ikke tilgængelig + Linux-runtime + Gør Linux-runtimen klar + Session + Starter skærm og lyd + Steam-klient + Kontrollerer Steam og dets kompatibilitetsværktøjer + Åbner Steam + Venter på Steams første billede + Skrivebord + Gør skrivebordet klar + Åbner + Venter på det første billede + Starter Steam + Sessionen blev afsluttet + Trin %1$d af %2$d + Steam er sat på pause + Greb + Venstre pad + Højre pad + Begge + Gamepad + Stick + Trackpad + Skift mellem stick og trackpad + Flere Deck-kontroller + PANELER + Luk kontrollerne til den anden skærm + Åbn Steam-menuen + Venstre · øverst + Venstre · nederst + Højre · øverst + Højre · nederst + Klik + Højre trackpad + Venstre trackpad + Trackpad + Vis tastatur + Skjul tastatur + Linux-terminal + Linux-sessionen er ikke klar til en terminal endnu. + Steam + Spil + Skrivebord + Butik + Komponenter + Apps + Opsætning + Opdateringer + %1$d%% + Genoptag %1$s + Vises som /root/ROMs i hver session. Intern lagerplads er på /root/Storage. + Ingen mappe valgt + Gælder fra næste session. SD-kort fungerer. + Vælg mappe + Skift mappe + Installer en ARM64 Proton-build, og vælg den derefter for hvert spil i Steam → Egenskaber → Kompatibilitet. + Stop den kørende session for at installere eller fjerne et kompatibilitetsværktøj. + Tilgængelige builds + Installeret %1$s + Afventer fra en tidligere anmodning + Ikke installeret + Opdatering tilgængelig → %1$s + Fjern gamle %1$s-versioner + Fjerner %1$s. Spil, der er sat til disse versioner, skal skiftes til en anden Proton. + Spil, der er sat til denne version, skal skiftes til en anden Proton. + Installer nu + Annuller afventende + Android-tastatur + Skjul ✕ + Start %1$s + Vælg en skærm. Sekundær: %1$s. + Den sekundære skærm er ikke længere tilgængelig. + Primær skærm + Sekundær skærm + Forrige fane + Næste fane + Til + Alle %1$d + Ingen + %1$d af %2$d + Fra + Nederste skærm + Gælder fra næste spilstart. Et spils profil tilsidesætter den fælles. + Et andet spil via app-id… + Udført + Standard + Fra + Skift… + Skift variabel + Tilbage til standard + Proton-spil + Billede i billede + Gå automatisk til billede i billede + Kontroller på den udfoldede skærm + Når skærmen er foldet ud, ligger spillet over Steam Decks trackpads og grebsknapper. Når den er foldet sammen, fylder spillet hele skærmen. + Sat i bero + Billede i billede er tilgængeligt, når sessionen er klar. + Tillad billede i billede for DroidDeck i Android-indstillingerne. + Android kunne ikke åbne billede i billede. + Genvej + Tilføj til hjemmeskærm + Vælg spil + Scanner installerede spil… + Kopier startlink + Startlink kopieret + Denne launcher kan ikke tilføje genveje. Brug Kopier startlink i stedet. + Ugyldigt startlink til spil. + Dette spil findes ikke længere i dit DroidDeck-bibliotek. + Stop den aktuelle session, før du starter et Steam-spil. + Kunne ikke sende spillet til Steam. Prøv at starte det igen. + Prøv fjernelse igen + Fjerner + Fjernelse ikke fuldført + Eksporter spilfil + Synkroniser alle spilfiler til mappe + Stop synkronisering af spilfiler + Vælg en mappe til frontend-spilfiler + Spilfiler gemt i %1$s + Frontend-filer + + Vent til runtime-handlingen er færdig + Vent til installationen af kompatibilitetsværktøjet er færdig + Vent til installationen af skrivebordsappen er færdig + Vent til installationen er færdig, før du starter et spil + Luk Steam-sessionen først, så spillet ikke overskriver importen + Importerer til %1$s + + Importerede %1$d fil fra %2$s til %3$s + Importerede %1$d filer fra %2$s til %3$s + + + Importerede %1$d fil fra %2$s til %3$s. Gamle gemte spil sikkerhedskopieret til Download/DroidDeck/Saves/backups + Importerede %1$d filer fra %2$s til %3$s. Gamle gemte spil sikkerhedskopieret til Download/DroidDeck/Saves/backups + + Vælg en zip med gemte spil til %1$s + Eksporterer %1$s + + Eksporterede %1$d fil som %2$s: %3$s + Eksporterede %1$d filer som %2$s: %3$s + + Vælg, hvor %1$s skal gemmes (%2$s) + %1$s mislykkedes: %2$s + Ingen sessionslogs endnu: kør en session først. + Ingen sessionslogs at rydde. + + Ryddede %1$d sessionslogmappe. + Ryddede %1$d sessionslogmapper. + + Parring til trådløs fejlretning mislykkedes + Kommando til trådløs fejlretning mislykkedes + Trådløs fejlretning er ikke tilgængelig + ADB-kommando kopieret + Vælg ROMs-mappen + Ikke en Adreno-GPU + Turnip understøtter Adreno-GPU\'er. På %1$s kan Steam vise en sort skærm. Download: %2$.0f MB. + Installer alligevel + Fjern Linux-runtime + Dette sletter runtime\'en, Steam-klienten i den og alle spil installeret der. + Anvendt: %1$s + Kunne ikke åbne %1$s på den primære skærm + Kunne ikke åbne %1$s på %2$s + Kunne ikke åbne %1$s på skærm %2$d + Importerer + Skifter + Gendanner + Annullerer + Sletter + kan ikke læse filen + Importerede %1$s + Den ventende udskiftning blev annulleret. + Vælg en komponentpakke (-linux .wcp) + Nightlies kunne ikke nås + Gemte %1$s + Download mislykkedes: %1$s + Vælg en Linux-runtime-driver (-Linux-zip eller Android + Linux-bundle) + Vælg en skærmdriver (AdrenoTools-zip eller Android + Linux-bundle) + Vælg en driver-zip eller en Android + Linux-bundle + %1$s: %2$s + Fjerner %1$s… + Glemmer %1$s… + Vælg mappen til spillagring + Vælg en Decky-plugin-ZIP + Ikke brugbar: %1$s + Program + Fjerner Linux-runtime + Kunne ikke åbne Indstillinger på den nederste skærm; åbnede den på hovedskærmen. + Åbn Indstillinger for udviklere i Indstillinger + + Kunne ikke åbne Androids indstilling ”Installer ukendte apps” + Kunne ikke kontrollere: %1$s + Denne build har ingen download til denne kopi af DroidDeck + Stop den kørende session først, og opdater derefter + Downloader + Installerer + Venter på din bekræftelse + Opdateringen mislykkedes + Android tilbød ikke at installere opdateringen + + Decky-installation mislykkedes: %1$s + Starter import af plugin… + Import af plugin mislykkedes: %1$s + Plugin installeret. Genstart Steam for at indlæse det. + + Ikke kontrolleret endnu - tryk på opdater for at søge efter nye drivere + Android + Linux: angiver skærmdriveren samtidig + Android + Linux: angiver runtime-driveren samtidig + Turnip indbygget i runtime\'en + system-Vulkan: ingen medfølgende build til denne GPU + %1$s (medfølgende) + Manuel + %1$s · %2$s + Kun den ene halvdel er udgivet lige nu + %1$.0f MB for begge + Ingen drivere at angive: %1$s + Søger efter de nyeste drivere… + Opdateret (kunne ikke kontrollere: %1$s) + De anbefalede drivere er ikke udgivet lige nu + Downloader %1$s %2$s… + Opdateret til %1$s %2$s. + Opdateret til %1$s %2$s. Skærmdriveren træder i kraft, når DroidDeck genstarter. + Skiftet til %1$s %2$s. + Skiftet til %1$s %2$s. Skærmdriveren træder i kraft, når DroidDeck genstarter. + Bruger %1$s %2$s. + Bruger %1$s %2$s. Skærmdriveren træder i kraft, når DroidDeck genstarter. + Ikke en driver-zip: hverken en AdrenoTools-driver, en -Linux-Turnip eller en Android + Linux-bundle + Import mislykkedes: %1$s + Importeret driver + Bruger %1$s som både runtime- og skærmdriver. + Bruger %1$s som både runtime- og skærmdriver. Skærmdriveren træder i kraft, når DroidDeck genstarter. + Slettede %1$s (begge drivere) + Slettede %1$s + Nyeste: %1$s · kontrolleret %2$s + Nyeste: %1$s · %2$s kunne ikke nås · kontrolleret %3$s + + %1$d min siden + %1$d min siden + + + %1$d t siden + %1$d t siden + + + %1$d dag siden + %1$d dage siden + + Kontrollerer Banners-Turnip, WinNative og DroidDeck… + kontrol mislykkedes + Installeret %1$s - vælg den i en af listerne for at angive begge drivere + Installeret %1$s - vælg den i menuen + + Vent til fjernelse af runtime er færdig + Spil + touchpad + direkte + Udskiftning mislykkedes: %1$s + Lagring af profil mislykkedes: %1$s + Skrivebordet er sat på pause + %1$s er sat på pause + Programmet er sat på pause + Starter skrivebordet + Starter %1$s + Starter programmet + Ingen logs for denne session. + Kunne ikke oprette arkivet med sessionslogs. + Kunne ikke dele sessionslogs. + Starter sessionen… + downloader Linux-runtime + downloader Linux-runtime · %1$d af %2$d MB + kontrollerer Linux-runtime + udpakker Linux-runtime + downloader skrivebordet + downloader skrivebordet · %1$d af %2$d MB + kontrollerer skrivebordet + udpakker skrivebordet + downloader Proton Experimental (ARM64) + downloader Proton Experimental (ARM64) · %1$d af %2$d MB + kontrollerer Proton Experimental (ARM64) + udpakker Proton Experimental (ARM64) + Kunne ikke nå runtime-kataloget. Kontroller forbindelsen, og tryk på Spil igen. + Linux-runtime blev ikke installeret. Kontroller runtime i Opsætning, og tryk på Spil igen. + Skrivebordet blev ikke installeret (%1$s). Kontroller forbindelsen, og tryk på Skrivebord igen. + %1$s stoppede uventet. Del logs med en fejlrapport, eller prøv igen. + Programmet stoppede uventet. Del logs med en fejlrapport, eller prøv igen. + Skrivebordet stoppede uventet. Del logs med en fejlrapport, eller prøv igen. + Afslutningsstatus %1$d · %2$s + ingen log + + Loggen viser %1$d \"Function not implemented\"-fejl: proots seccomp-acceleration fejler i en hjælpeproces på denne enhed. Prøv %2$s → \"%3$s\" (eller \"%4$s\"), og start igen. + Loggen viser %1$d \"Function not implemented\"-fejl: proots seccomp-acceleration fejler i en hjælpeproces på denne enhed. Prøv %2$s → \"%3$s\" (eller \"%4$s\"), og start igen. + + + Plugins + Installer en plugin-ZIP til den næste Steam-session. + Downloader… + Installer fra ZIP + Spil + Lyd og video + Grafik + Internet + Kontor + Udvikler + Uddannelse + Videnskab + System + Hjælpeprogrammer + Populære + Trends + Nye + Opdaterede + indlæser… + Flatpak-opsætning + %1$s: %2$s + Kunne ikke søge efter opdateringer på Flathub + installeret + mappe + Grafit + Grafitoverflader, med logoets egen kugleblå farve til alt, der er valgt, fokuseret eller trykket. + Papir på sort + Hvid er primærfarven, ikonets blå er signalfarven til streger, ringe og prikker. + Ikonblå + Sort baggrund, hvid skrift, blå til alt, der er valgt, fokuseret eller trykket. + Elektrisk marineblå + Den blå er klarere, på overflader der trækker mod marineblåt i stedet for gråt. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Installer runtime først + Steam-biblioteket er ikke tilgængeligt + Det tidligere SD-bibliotek er ikke tilgængeligt + Spillagring er ikke tilgængelig + Mappen med tilføjede spil er ikke tilgængelig + Steam-manifester er ikke tilgængelige + Mange systemer + Intern + Linux-system + SD-kort + Lagerplads + Vælg + Egenskaber + Tilføj til Favoritter + Fjern fra Favoritter + Omdøb + Kopier + Klip + Handlinger + Favoritter + Fastgør aktuel mappe + Ingen favoritter endnu - fastgør en mappe via dens ⋮-menu for hurtigt at komme tilbage hertil. + Fjern fra favoritter + 0 byte + byte + Placering + Type + Størrelse + Ændret + Mappe + Fil + %1$s-fil + Skrivebeskyttet + Forhindrer spil (og Wine) i at overskrive eller slette denne fil. + Kunne ikke ændre Skrivebeskyttet + Skjult + Denne lagerplads kan ikke gemme det skjulte flag. + Markerer filen som skjult i Windows (Wines skjulte attribut). + Kunne ikke ændre Skjult på denne lagerplads + Udført + Sletter... + Sletning mislykkedes + Flytter... + Kopierer... + Flytter - %1$s + Kopierer - %1$s + Flytter %1$d/%2$d - %3$s + Kopierer %1$d/%2$d - %3$s + %1$d gennemført, %2$d mislykkedes + %1$d gennemført, %2$d sprunget over + + %1$d element flyttet + %1$d elementer flyttet + + + %1$d element kopieret + %1$d elementer kopieret + + \"%1$s\" findes allerede + Omdøber... + Omdøbning mislykkedes + Opretter mappe... + Kunne ikke oprette mappe + Ny mappe + Mappenavn + Opret + Nyt navn + Slet? + Slet \"%1$s\" permanent? + + Slet %1$d element? + Slet %1$d elementer? + + Dette kan ikke fortrydes. + + …og %1$d til + …og %1$d til + + Sletter %1$d/%2$d - %3$s + + %1$d kunne ikke slettes + %1$d kunne ikke slettes + + Sammenlægning tilføjer og udskifter filer i den eksisterende mappe. + Overskriv udskifter den eksisterende fil. + Anvend på alle konflikter + Sammenlæg + Overskriv + Behold begge + Spring over + Vælg denne mappe + Tilbage + %1$s er monteret, men kan ikke læses lige nu + Listevisning + Gittervisning + Søg + Sorter + Navn + Dato ændret + Luftige rækker + Kompakte rækker + Skjul skjulte filer + Vis skjulte filer + Skjul favoritter + Vis favoritter + Filtrer denne mappe + %1$s ledig + %1$d valgt + Ingen + Alle + Indsæt + + %1$d element + %1$d elementer + + Indsæt %1$s her + Indsæt %1$s (flyt) her + Filer + LAGERPLADS + GENVEJE + FAVORITTER + Download + Download + ROMs + Sessionslogs + Billeder + Tilføjede \"%1$s\" til Favoritter + Fjernede \"%1$s\" fra Favoritter + Tom mappe + Et niveau op + Vælg en mappe + Vælg en fil + Steam-blå + Himmelblå + Hvid + Violet + Grøn + Ravgul + Rød + Pink + A-knap + B-knap + X-knap + Y-knap + Venstre bumper + Højre bumper + Venstre trigger + Højre trigger + View-knap + Menu-knap + View + Menu + D-pad op + D-pad ned + D-pad venstre + D-pad højre + Skjult + Træk for at flytte + Fastgør + Gem + Ingen + Tastatur + trackpad + Terminal + Deck-greb + trackpads + + Fjerner Linux-runtime + + Fjerner Linux-runtime · %1$d element + Fjerner Linux-runtime · %1$d elementer + + Linux-runtime fjernet + Kunne ikke afslutte fjernelsen af Linux-runtime. Prøv fjernelse igen i Opsætning. + Fjerner rester af runtime + Downloader + Verificerer + Udpakker + Vises, mens Linux-runtime installeres eller fjernes + Installerer Linux-runtime + Fjerner Linux-runtime + + Linux-runtime er ikke installeret + Kataloget har ingen tjeksum for %1$s + Tjeksum stemmer ikke - intet blev ændret + Installerer %1$s + Kunne ikke placere AppImage + Udpakning mislykkedes + Installation mislykkedes + + Flatpak-opsætning mislykkedes (afslutningskode %1$d) + Flatpak-opsætning blev ikke afsluttet + En anden butiksopgave er i gang + Opsæt Flatpak først + Starter + Flatpak afsluttede med status %1$d + Installerer %1$s + Opdaterer %1$s + Fjerner %1$s + Installerer %1$s (%2$d/%3$d) + Opdaterer %1$s (%2$d/%3$d) + Fjerner %1$s (%2$d/%3$d) + oversættelser + grafikdrivere + mediecodecs + %1$s-runtime + %1$s-runtime %2$s + + Filen er for lille til at være en AppImage + Filen kan ikke læses: %1$s + Dette er ikke en AppImage (det er ikke et Linux-program) + Denne AppImage er bygget til hverken ARM64 (aarch64) eller x86-pc\'er + Dette er et Linux-program, men ikke en AppImage + Kun type 2-AppImages kan importeres (denne er type %1$d) + Ikke nok ledig plads: import kræver cirka %1$s + Ikke nok ledig plads: opdatering kræver cirka %1$s + Import mislykkedes + Den AppImage findes ikke længere + Opdatering mislykkedes + Udpakker %1$s + AppImage kunne ikke udpakkes + AppImage kunne ikke udpakkes: %1$s + + Stop den aktive session, før du installerer Decky Loader + Download af Decky Loader mislykkedes + Decky Loaders størrelse passede ikke med metadata for udgivelsen + Kunne ikke downloade tjeksummen for udgivelsen + Tjeksum for udgivelsen mangler + Tjeksum for udgivelsen er ugyldig + Tjeksum for Decky Loader stemmer ikke + Udgivelsesfilen er ikke en kompatibel 64-bit Linux-eksekverbar fil + En session startede under downloadet; stop den før du installerer Decky Loader + Kunne ikke markere PluginLoader som eksekverbar + Kunne ikke aktivere Steam CEF-fjernfejlretning + Kunne ikke forberede PluginLoader + Kunne ikke bevare den eksisterende PluginLoader + Kunne ikke aktivere den nye PluginLoader + Stop den aktive session, før du installerer et plugin + Installer Decky Loader, før du importerer et plugin + Kunne ikke læse den valgte ZIP-fil + Kunne ikke oprette mappen til Decky-plugins + Udpakker plugin-ZIP + Plugin-ZIP skal indeholde én plugin-mappe på øverste niveau + Plugin-ZIP mangler plugin.json, package.json eller dist/index.js + Plugin-navn mangler + En session startede under installationen; stop den før du installerer et plugin + Plugin installeret · genstart Steam for at indlæse det + Installation af plugin-ZIP mislykkedes + Plugin-ZIP indeholder for mange elementer + Plugin-ZIP indeholder en ugyldig sti + Plugin-ZIP indeholder gentagne stier + Plugin-ZIP indeholder en usikker sti + Kunne ikke oprette plugin-mappe + Plugin-ZIP udvider sig til mere end 1 GiB-grænsen + Kunne ikke markere den medfølgende plugin-binærfil som eksekverbar + Ugyldig remote_binary-post + Ugyldigt navn på fjern-binærfil + URL til fjern-binærfil skal bruge HTTPS + Fjern-binærfilen %1$s har en ugyldig SHA-256-hash + Kunne ikke oprette plugin-bin-mappen + Downloader plugin-binærfil · %1$s + Kunne ikke downloade plugin-binærfilen %1$s + Tjeksum for plugin-binærfil stemmer ikke · %1$s + Kunne ikke markere plugin-binærfil som eksekverbar · %1$s + Kunne ikke udskifte plugin-binærfil · %1$s + Kunne ikke installere plugin-binærfil · %1$s + Plugin-mappen hører til et andet plugin + Kunne ikke bevare det eksisterende plugin + Kunne ikke aktivere det importerede plugin + Kunne ikke opdatere Decky-pluginindstillinger + Kunne ikke gendanne det eksisterende plugin + + Kunne ikke gemme identitet til Trådløs fejlretning + ADB-kommando mislykkedes (%1$d) + Android bekræftede ikke, at procesgrænsen blev ændret: %1$s + Ingen parring for Trådløs fejlretning er gemt + Indtast den aktuelle IP-adresse og port for Trådløs fejlretning + + Der opstod timeout under venten på parringsvinduet. Forsøg igen fra DroidDeck. + Kunne ikke lede efter parringsvinduet (%1$d). Tjek at Wi-Fi er tændt. + Kunne ikke lede efter parringsvinduet: %1$s + Indtast alle 6 cifre i koden + Parrer… + Den kode virkede ikke. Åbn parringsvinduet igen, og indtast dens nye kode. + Parret. Forbinder… + Parret, men porten til Trådløs fejlretning blev ikke fundet. Lad Trådløs fejlretning være tændt, og forsøg igen. + Anvender indstillingen… + Kommando til Trådløs fejlretning mislykkedes + Opsætning af Trådløs fejlretning + Spørger efter parringskoden til Trådløs fejlretning, mens Indstillinger er åben + Par Trådløs fejlretning + Tænd Trådløs fejlretning, og tryk derefter på „Par enhed med parringskode“. + Tænd Trådløs fejlretning, og tryk derefter på „Par enhed med parringskode“. Bliv i Indstillinger: koden vises i denne notifikation. + 6-cifret parringskode + Tryk på Indtast kode, og skriv de 6 cifre, der vises i Indstillinger. + Indtast Wi-Fi-parringskoden + Forsøg koden igen + Indtast kode + Sætter Steam op + Steam er klar + Tryk for at gå tilbage til DroidDeck. Du kan slukke Trådløs fejlretning nu. + Opsætning af Trådløs fejlretning stoppet + + Downloadet svarede HTTP %1$d + Downloadets størrelse passede ikke med den udgivne APK og blev kasseret - forsøg igen + Downloadet matchede ikke sin tjeksum og blev kasseret - forsøg igen + Opdateringen er til Android-pakken %1$s, ikke %2$s + Opdateringens metadata angiver ikke DroidDecks udgivelsessigneringsnøgle + Android kunne ikke læse den downloadede APK + Den downloadede APK er til %1$s, ikke %2$s + Den downloadede APK siger versionCode %1$d, ikke den udgivne %2$d + Android installerer ikke versionCode %1$d over den installerede %2$d + Den downloadede APK har ingen signeringsoplysninger + Den downloadede APK er ikke signeret med DroidDecks udgivelsesnøgle + Opdateringskataloget svarede HTTP %1$d + + Starter sessionen… + %1$d:%2$02d forløbet · arbejder stadig + Downloader Steam-klientopdateringen · %1$d%% + Downloader Steam-klienten · %1$s + + En installation af et kompatibilitetsværktøj er allerede i gang + Stop den aktive session, før du installerer kompatibilitetsværktøjer + Kunne ikke finde en ARM64-udgivelse af %1$s + Mindst 4 GB ledig plads plus downloadets størrelse er nødvendig for at installere %1$s + Downloader %1$s %2$s + Forsøger download igen · forsøg %1$d af %2$d + Download afbrudt + Downloadet mislykkedes; den delvise fil beholdes, så et nyt forsøg kan genoptage + Downloadet overskred udgivelsens størrelse og blev slettet + Downloadet var ufuldstændigt; den delvise fil beholdes til et genoptageligt forsøg + Bekræfter download + Kunne ikke læse tjeksummen for udgivelsen; forsøg igen + %1$s %2$s udgiver ingen tjeksum; intet blev installeret + Tjeksum stemmer ikke; downloadet blev slettet + En session startede under downloadet; stop den før du installerer kompatibilitetsværktøjer + Installerer %1$s + Installation af %1$s mislykkedes (afslutningskode %2$d) + Kunne ikke installere %1$s + Henter droiddeck-esync-pakken + + med Linux-hjælpeprogrammer + Kun DLL\'er + Original (Proton opdateret siden sidste ombytning) + Ændret uden for Komponenter + ikke til stede + Original %1$s + Original fra %1$s + %1$s %2$s sættes ind i %3$s, når spillet der kører på det, lukker. + %1$s %2$s er nu i %3$s. Det træder i kraft næste gang et spil starter. + Den originale pakke er ikke gemt + %1$s af %2$s går tilbage til sin original, når spillet der kører på det, lukker. + %1$s af %2$s gendannet fra sin %3$s-original. + %1$s er i brug i %2$s. Byt den Proton til noget andet først. + %1$s er ikke gemt + %1$s slettet. + Originalen af den installerede version beholdes: den er vejen tilbage. + Ikke gemt + Slettede %1$s-originalen af %2$s. + Downloads kommer kun fra Nightlies-udgivelserne + Denne pakkeliste er ældre end tjeksummer - opdater den, og forsøg igen + Tjeksum stemmer ikke + + Lineær + Nærmeste + Snapdragon GSR (kvalitet) + Kun skærpning + Auto (matcher skalering) + + Intet anvendt. + Spilklarhed + Skærpning med et strejf af kontrast - den til hver dag. + Levende + Farven skubbet hårdt op. Deband slået til, så himlen ikke striber. + Filmisk + Dybere kontrast, koldere farve, udjævnede kanter. + Konkurrence + Skygger hævet og tung skærpning - se folk først. + Adaptiv skærpning + Kun skærpning, farven er uberørt. + Film + Filmagtig kurve, let dæmpet. + Arkade + Højt og slagkraftigt. + Retro CRT + CRT-rørets og det analoge signals udseende sammen. + Opskalering skarp + Til kørsel under skærmens opløsning. + Rene pixels + Skarpe pixels til 2D og gamle titler. + Anime-kant + Toon-konturer plus skærpning. + + Ikke en Adreno-GPU + Understøttet + Eksperimentel: dens drivere er testversioner + Under testet hardware (Adreno 725 og nyere): kører muligvis ikke + Ikke understøttet: DroidDeck kræver en Adreno (Snapdragon) GPU + + GitHubs hastighedsgrænse blev nået - forsøg igen senere + GitHub svarede HTTP %1$d + downloadet svarede HTTP %1$d + Denne driverliste er ældre end tjeksummer - opdater den, og forsøg igen + Tjeksum stemmer ikke - downloadet blev kasseret + Ikke en AdrenoTools-driver: %1$s + ingen meta.json + meta.json kan ikke læses (%1$s) + dette er en Linux-runtime-driver (importer den under „Linux runtime driver“) + dette er en Wayland-spildriver, som denne app ikke har brug for + meta.json angiver ikke noget libraryName, så intet kunne gives til AdrenoTools + meta.json angiver %1$s, som ikke findes i zip-filen + %1$s er ikke et 64-bit AArch64 ELF-delt bibliotek + %1$s linker glibc - en „-Linux“ Turnip, som hører under „Linux runtime driver“ + Ingen libvulkan_freedreno*.so i denne zip-fil. En Android (AdrenoTools) eller -Wayland Turnip-zip er ikke en Linux-runtime-driver. + %1$s er ikke et 64-bit AArch64 ELF-delt bibliotek. + %1$s er ikke en glibc-driver - den linker Androids libc. Linux-runtime kræver en „-Linux“-zip; en almindelig eller „-Wayland“ Turnip kan slet ikke indlæses af Steam-klienten. + Ikke en driverpakke: ingen manifest.json, der angiver en android- og en linux-driver + Ikke en driverpakke: %1$s peger uden for den + Ikke en driverpakke: den udpakkes til mere end %1$d MB + + SD-kort + Mappe + + Del sessionslogs + + kræver Android 10 eller nyere (skærmlag med et farverum) + denne skærm rapporterer ingen HDR-understøttelse + denne skærm understøtter %1$s, ikke HDR10 + + denne GPU + Procesgrænse er tændt + Procesgrænse er ikke sat + Procesgrænse kunne ikke tjekkes + Procesgrænse er slået fra + Procesgrænse er ikke nødvendig + %1$s Hvis det ikke er muligt, så forbind enheden til en computer med ADB, og køre kommandoen nedenfor. Kom tilbage hertil, og tjek igen. + Steam-sessioner kan starte. + Denne Android-version bruger ikke denne grænse. + Tænd „Disable child process restrictions“ i Udvikleremuligheder, og lad Udvikleremuligheder være tændt bagefter. + Denne Android-version har ingen kontakt til det. Brug Trådløs fejlretning til at ændre det fra denne enhed. + Denne version har ingen download til denne kopi af DroidDeck. + Android installerer den ikke over denne version, fordi dens versionCode %1$d er under den installerede %2$d. + Android installerer ikke en ældre version over en nyere. Bliv på denne version, indtil kanalen følger efter. + Denne kopi af DroidDeck er signeret anderledes (en lokal version?), så Android opdaterer den ikke direkte. + Der er ikke nok ledig plads til opdateringen. + Opdateringen blev annulleret. + Opdateringen blev ikke installeret: %1$s + Opdateringen blev ikke installeret. + %1$s-zip + zip + %1$s er ikke startet endnu + Ingen gemte filer fundet for %1$s + %1$s har ingen drive_c-mappe, så det er ikke en gemt-zip fra GameHub eller Winlator + kan ikke oprette mapper i %1$s + kan ikke skrive i %1$s + kan ikke skrive i %1$s (%2$s) + Mappen er ikke tilgængelig + En anden fil bruger allerede %1$s + Grafikmappen er ikke tilgængelig + Mappen har en urelateret synkroniseringsoversigt + Kunne ikke fjerne %1$s + Filen forlader eksportmappen + FEX (version uden versionsnavn) + ingen profile.json i %1$s + %1$s er ikke en Linux-komponentpakke (type %2$s) + %1$s · %2$s fri + Automatisk + Opdateringskataloget bestod ikke sine kontroller (%1$s) + DroidDeck-indstilling for procesgrænse + Logget ind · offline-tilstand + Logget ind + INDBYGGET + DOWNLOADET + IMPORTERET + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Sessionsskærm + Opløsning + 16:9-spiltilpasning + Hvordan 16:9-spil tilpasses denne skærm. Fra næste session. + Vælg Match skærm eller en brugerdefineret størrelse, der er smallere end 16:9. + Bevar proportioner + Stræk til skærm + Billedskalering + Skaleringsfilter + Deles af Steam og Desktop. + Træder i kraft øjeblikkeligt. Deles af Steam og Desktop. + FSR, GSR og NIS anvendes ved forstørrelse af sessionsbilledet. Kun skærpning virker også uden forstørrelse. + Filterskarphed + Billedfrekvens + %1$s Indstiller skalering til %2$s. + Match skærm (%1$d×%2$d) + Følg skærm (ændrer størrelse ved fold/udfold) + Steam + Vis enhedsoplysninger + Skjul enhedsoplysninger + Læser enheden… + Kopier + Indsæt disse oplysninger i en fejlrapport. + Kopieret. + Tving SSBS + Bevarer en CPU-hastighedsindstilling, som Wine hele tiden slår fra. Kan gøre Windows-spil hurtigere på Snapdragon 8 Elite; lille ændring set på ældre chips. Træder i kraft ved næste spilstart. + + Butikker + Steam + Brugerdefineret + Butikker + Åbn en butik på + Downloadhastighed + Butiksmotor + ● Motor klar + Ikke bygget + Afbalanceret + Hurtig + Maks + Butikker + Butiksindstillinger + Indstillinger + (ikke logget ind) + Butik + Installeret + Bibliotek + Alle + Downloads + %1$s (%2$d) + Log ind + %1$s · log ud + Log ud af %1$s? + Installerede spil forbliver. Biblioteket hentes igen, når du logger ind igen. + Bliv logget ind + Log ud + Søg i %1$s-kataloget… + Søg i installerede spil… + Søg i dit bibliotek… + Søg + Intet installeret + Indlæser + Ingen spil + Indlæser butikken… + Intet katalog + Nyheder + Tilbud + Gratis spil + Populært + Dit bibliotek + Spil + Installer + Genoptag installation + Afbrudt + Installation · %1$s + Få gratis + Se på %1$s + Køb · %1$s + Installeret + I biblioteket + Gratis + Genoptag + Opdater + Vis indhold for voksne + Epic + EOS + Offline + Fra + Epic-login (EOS) + Start offline + Løs Epic-login + Cloud-gemte spil + Upload til cloud + Download fra cloud + Ingen cloud-gemte spil + Cloud-konflikt + Cloud-konflikt · %1$d filer + … + Behold cloud + Behold lokal + Tjekker cloud… + Senest synkroniseret %1$s + Ikke synkroniseret endnu + %1$d filer + installeret + i dit bibliotek + butik + Bedrifter + %1$d af %2$d låst op + %1$d / %2$d + Låst op · %1$d + Låst · %1$d + Skjult · %1$d + Skjult bedrift + Detaljer vises, når den er låst op. + Alle bedrifter låst op + Indlæser… + Log ind på %1$s for at se dine bedrifter. + Kunne ikke oprette forbindelse til %1$s. Prøv igen, når du er online. + %1$s har ingen bedrifter for dette spil. + Vis alle %1$d + Bedrifter · %1$s + ✓ I Steam + Afinstaller + Afinstallerer… + Afinstaller %1$s? + Frigør %1$s og fjerner det fra Steam. + Fjerner dets filer og dets genvej i Steam. + Steam fanger spillet op næste gang det starter; start det derfra, eller åbn Steam nu. + Proton + Steams eget valg pr. spil + Lyd + DirectAudio + Komponenter + FEX, DXVK, VKD3D + Motor + %1$s · native + %1$s · indbygget + Hvordan det køres + Hentet fra + %1$s, med din konto + Installeret til + Spillagring (Games/%1$s/%2$s) + Startes af + Steam og Proton, samme vej som Steam-spil + Steam og Proton · Epic-login pr. start, EOS-overlay slået fra + Steam og Proton · Amazon Games Services-variabler er sat + Steam + tilføjet til Steam ved installation (Deck UI + Steam Input) + Ingen downloads + Pause + I kø + I kø · #%1$d + Annulleret + Mislykkedes + Mislykkedes · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Sæt på pause + Genoptag + Annuller + Forsøg igen + Ryd + Ryd gennemførte + Manifest + Download + Bekræft + Installer + Færdig + Tjekker + Downloader + Slet download? + Mislykkedes · %1$s beholdt + Bekræfter + Installerer + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d filer + Downloadadministrator + Downloads på samme tid + Installer til + Installer %1$s: + Intern + %1$s fri + Butiksdownloads + Viser fremskridt for spil-downloads og holder downloads kørende i baggrunden + Downloader + %1$d downloads + Forbereder… + Afsluttede butiksdownloads + %1$s installeret + Download af %1$s mislykkedes + Slet + Behold + Log ind på %1$s + Log ind med din udbyder + Genindlæs + Luk + Forsøg igen + Afslutter login… + Loginsiden kunne ikke indlæses: %1$s + Denne udbyder afviser login fra en indlejret side. Log i stedet ind med din butikskodeord. + Loginsiden svarede med en fejl (HTTP %1$d). + Forbindelsen til loginsiden er ikke sikker. + Login kunne ikke bekræftes. Forsøg igen. + Login mislykkedes. Forsøg igen. + Luk + Tilbage + Rediger + Tilføj alle spil i denne mappe + Intet nyt i denne mappe + Navn + Mål + Start i + Startindstillinger + Grafik + Anden exe… + Fjern + Omslag + Baggrund + Logo + Ikon + Spilmappe + Steam + SteamGridDB + Din fil + Fra exe-filen + Automatisk + Vælg billede… + Vælg et billede: %1$s + Nulstil til automatisk + + %1$d spil tilføjet + %1$d spil tilføjet + + + %1$d allerede i Spil + %1$d allerede i Spil + + B · Tilbage til %1$s + spillet + Spilindstillinger + Grafikdrivere + Denne Proton + Hver Proton + For hver Proton + Tilgængelig · %1$d + Kontrolleret %1$s + Søg efter nye komponenter og drivere + Hent + %1$s · DXVK, VKD3D-Proton og FEX nedenfor hører til denne Proton + %1$s · testet + %1$s · ikke testet + %1$s · ikke understøttet + Holder det anbefalede par ajour + Auto · %1$s + Runtime- og skærmdrivere separat + Brug %1$s? + Fjern %1$s? + Diagnostik + Klar til at spille + Ikke klar til at spille endnu + runtime %1$s + procesgrænse fra + logget ind som %1$s + Når noget kræver dig + Udseende + Denne enhed + Biblioteksgrafik + LINUX-APPS + Flathub + Desktopapps til Linux-desktoppen, åbnet fra Desktop. + Konfigurer Flathub + Fjern %1$s? + Dets indstillinger og filer bevares. + Behold + Størrelse på disk + Afinstaller desktoppen? + Alle + GOG-bedrifter + Synkroniserer bedrifter for understøttede GOG-spil. Gælder fra næste start. + Bedrifter slået fra + Bedrifter aktiveret + Log ind på GOG igen + Bedrifter uploadet + Upload af bedrift afventer + Aktivér GOG-bedrifter i Butik-indstillinger + Bedrifter ikke tilgængelige; se sessionslogs + Bedrift låst op + Indsæt + Styrekryds + Venstre stick + Højre stick + Menu + Tastatur + Styrekryds op + Styrekryds ned + Styrekryds venstre + Styrekryds højre + Berøringsstyring + Berøringscontroller + Mus + Til + Fra + Langsom + Normal + Hurtig + Rediger layout + DroidDeck-menu + Træk kontroller ud på skærmen + Arrangér + Kassér + Gemmer… + Ikon + B− + B+ + H− + H+ + Skjul + Farve og gennemsigtighed… + Kopiér dette layout + Layout kopieret; indsæt det i berøringslayoutet for et vilkårligt spil + Indsæt kopieret layout + Nulstil til Steams standard + Denne kontrol har ingen binding i Steam, der kan tildeles et ikon + Ikon for %1$s + Farve på berøringslayout + Udført + Berøringslayoutet kunne ikke gemmes + Berøringslayout gemt i Steam + App %1$d + Etiket (valgfrit) + Steams ikoner blev ikke fundet + + %1$d ikon fra Steam + %1$d ikoner fra Steam + + Ikonfarve + Knapfarve + Intet ikon + + Kontrollerer login… + Henter dit bibliotek… + Henter spilliste… + Indlæser spildetaljer… (%1$d/%2$d) + + Synkroniserer %1$d spil… + Synkroniserer %1$d spil… + + Synkroniserer… %1$d/%2$d + Henter coverbilleder… + Synkronisering mislykkedes + Der blev ikke fundet spil i dit %1$s-bibliotek + Amazon Games svarede ikke om dit bibliotek: opdater, eller log ind igen + Kunne ikke oprette forbindelse til GOG + Dit GOG-bibliotek kunne ikke læses + Registrerer hos Steam… + Cardboard VR + Cardboard-tilstand + Billeder til venstre og højre øje til en VR-brille til telefonen. + Billede + Virtuel skærm + Side-by-side-3D + Brille + Cardboard + Cardboard (2014) + Ingen linse + Øjenafstand + Skærmbredde + Skærmafstand + Hovedsporing + Denne enhed har ingen rotationssensor. + Centrer visning + Centrer + SteamVR (eksperimentel) + Installer + Downloader Valves SteamVR til ARM64 (571 MB). + Downloader og udpakker SteamVR… + Start + Kører SteamVR med dit hoved som headset; billedet skifter til side-by-side-3D. + Stop + SteamVR kører. + Ikke tilgængelig i denne session. + SteamVR svarede ikke; se sessionsloggen. + Download komponenter + Downloads deles med siden Komponenter. Vælg en downloadet version ovenfor for at bruge den fra næste spilstart. + Vælg en version at downloade + Udvikler + Udgiver + Udgivet + Hovedhistorie + + Ekstra + Komplettering + %1$s t + %1$d min + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..620972946 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,2235 @@ + + + DroidDeck + + Beim ersten Start wird Steam heruntergeladen. Das kann ein paar Minuten dauern. + Standardmäßig pausiert Steam, wenn du die App verlässt, und macht bei deiner Rückkehr dort weiter, wo es aufgehört hat. + Dein Controller erscheint als Xbox-360-Pad. Ansonsten nutze die Bildschirmsteuerung. + + + Beim ersten Start wird der Desktop eingerichtet. Das kann ein paar Minuten dauern. + Standardmäßig wird die Sitzung pausiert, wenn du die App verlässt, und macht bei deiner Rückkehr dort weiter, wo sie aufgehört hat. + Touch funktioniert wie ein Touchpad: Ziehen bewegt den Zeiger, Tippen klickt. + Dein Controller erscheint als Xbox-360-Pad. Ansonsten nutze die Bildschirmsteuerung. + + Sitzung + Wird angezeigt, solange eine Steam- oder Desktop-Sitzung geöffnet ist + Steam läuft + Steam-Download aktiv + Sitzung pausiert – tippe auf „Fortsetzen“, wenn du bereit bist + Fortsetzen + Jetzt anhalten + Sitzung beenden + Frame-Generierung + Modus der Frame-Generierung auswählen + Aus + Win-FG %1$d× + LSFG %1$d× + LSFG Adaptiv %1$d + Benötigt Lossless Scaling auf Steam + Installiere Lossless Scaling in Steam oder importiere Lossless.dll + Lossless.dll importieren… + Kopie aus Steam wird verwendet + Importierte Kopie wird verwendet + Lossless.dll auswählen + Lossless.dll importiert + Diese Lossless.dll wird bereits verwendet + Dieses Steam-Konto besitzt Lossless Scaling nicht + Diese Datei ist nicht die Lossless.dll von Lossless Scaling + LSFG konnte aus dieser Lossless.dll nicht vorbereitet werden + LSFG benötigt Lossless Scaling aus Steam oder eine importierte Lossless.dll + LSFG konnte seine Shader nicht aus Lossless.dll vorbereiten + %1$s: nicht unterstützt + %1$s: Engine-Fehler + %1$s: benötigt Lossless Scaling + %1$s %2$s · startet + Fügt eine Steam-Bibliothek hinzu und durchsucht sie nach Windows-Spielen. Eigene Spiele nutzen ihre Steam-Einträge, der Rest wird zu Nicht-Steam-Verknüpfungen. + Spielumgebung + Umgebungsvariablen für Proton-Spiele. + Bearbeiten + Alle Proton-Spiele + Profil + Steam-App-ID + Öffnen + Spiel %1$s + Variable hinzufügen + Variablenname + Wert + Speichern + Entfernen + Wieder erben + Umgebungsüberschreibungen löschen + Für dieses Profil aufheben + Keine Variablen gesetzt. + Geerbt + Eigene Variable + Laden oder Speichern fehlgeschlagen. Prüfe den freien Speicherplatz und versuch es noch einmal. + Verwende einen gültigen Namen und einen Wert mit bis zu 8192 Zeichen. + Die Standard-Proton-Laufzeit liest diese Variable nicht. + Wird geladen… + Überschreibt das D3D12-Feature-Level. 12_2 meldet DirectX-12-Ultimate-Funktionen; nicht unterstützte Funktionen können fehlschlagen. Für automatische Erkennung nicht setzen. + Überschreibt das D3D12-Shader-Model. 6_9 erfordert ein ausreichend aktuelles VKD3D und passende GPU-Funktionen. Für automatische Erkennung nicht setzen. + Lass das Mesa-Shader-Caching aktiviert, um wiederholtes Kompilieren zu vermeiden. false aktiviert das Caching. + Maximale Größe des Mesa-Shader-Caches, z. B. 1G. Größere Caches belegen mehr Speicherplatz. + VKD3D-Optionen für D3D12. nodxr deaktiviert Raytracing; nur bei Spielen verwenden, die das brauchen. + Auf 1 setzen für ein Proton-Diagnoseprotokoll. Protokollieren kann die Leistung mindern und Speicherplatz belegen. + Auf 1 setzen, um für D3D9–11-Kompatibilitätstests das OpenGL-basierte WineD3D statt DXVK zu verwenden. + Auf 1 setzen, um dieses Spiel von ntsync fernzuhalten (droiddeck-ntsync oder ein Kerneltreiber); es nutzt dann droiddeck-esync, wo ein Pack existiert, sonst droiddeck-fsync. + Auf 1 setzen, um droiddeck-esync für dieses Spiel abzuschalten; es nutzt stattdessen droiddeck-fsync oder droiddeck-ntsync, wo auch fsync aus ist. + Auf 1 setzen, um dieses Spiel von droiddeck-fsync fernzuhalten; es nutzt dann droiddeck-esync, wo ein Pack existiert, sonst droiddeck-ntsync. + Auf 1 setzen, um dieses Spiel mit droiddeck-fsync auszuführen, oder auf 0, um droiddeck-esync vorzuziehen, wo ein Pack zu seinem Proton passt. + Auf 0 setzen, um Protons Gamepad-Navigationshilfe für dieses Spiel zu deaktivieren. + DXVK-Overlay für D3D9–11. fps zeigt die Bildrate an; ohne Wert wird es ausgeblendet. + DXVK-Einstellungen, durch Semikolons getrennt. Neuere Builds nutzen dxvk.maxFrameRate zur Bildratenbegrenzung; fürs normale Spielen besser das Bildratenlimit der Sitzung verwenden. + D3D12-Bildratenlimit. 0 hebt das Limit auf. + Mesa-OpenGL-Threading. true kann CPU-limitierten OpenGL-Spielen helfen; pro Spiel testen. + Wine-Sync + Wie Spiele ihre Threads im Gleichschritt halten. Gilt ab der nächsten Sitzung. + esync + ntsync + fsync + wineserver + FEX-Voreinstellung + Gilt ab dem nächsten Spielstart. + Spiele + FEX-Standard + Verwendet die Standardwerte von FEX. + Stabilität + Sicherste, langsamste Einstellungen. + Kompatibilität + Volles TSO mit Multiblock. + Mittel + Ausgewogenes TSO mit reduzierter x87-Genauigkeit. + Leistung + TSO komplett deaktiviert; Multiblock. + Leistung + TSO + Leistungseinstellungen mit aktiviertem Haupt-TSO. + Extrem + Deaktiviert Prüfungen auf selbstmodifizierenden Code. JIT-Spiele und DRM können ausfallen. + Extrem + TSO + Extrem-Einstellungen mit aktiviertem Haupt-TSO. + Extrem-gn + Leistung mit TSC- und Metadaten-Hinweisen. Vor Extrem ausprobieren. + Denuvo + Leistung mit vollständigen Codeprüfungen und verstecktem Hypervisor-Bit. + Eigener Wert + Wert auswählen + Emulatoren & Apps + Hinzufügen + Eigene App oder eigenes Spiel + Skript + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d %% + Wird gestartet… + %1$s hinzugefügt + %1$s entfernt + Desktop + Diese App gibt es nicht mehr. Wähle sie erneut auf dem Desktop aus. + %1$s öffnen + Entfernen + Zum Entfernen erneut drücken + Zum Entfernen Sitzung beenden + Führt %1$s aus seinem Ordner aus, der an Ort und Stelle in die Sitzung eingebunden wird. Beim Entfernen der App bleibt dieser Ordner unangetastet. + Führt %1$s aus einer Kopie seines Ordners in DroidDeck aus. Beim Entfernen der App wird die Kopie gelöscht, nicht das Original. + Installiere zuerst die Linux-Laufzeit + Die Datei ist nicht mehr vorhanden + Nicht genug freier Speicherplatz: Zum Kopieren des Ordners werden %1$s benötigt + %1$s wird kopiert + Hinzufügen der App fehlgeschlagen + Suche nach einem Release von %1$s + Releases von %1$s konnten nicht gelesen werden + Kein Release von %1$s enthält ein ARM64- oder x86_64-AppImage + %1$s wird heruntergeladen + Download fehlgeschlagen + Aktualisieren der App fehlgeschlagen + Dieses Symbol konnte nicht gelesen werden + %1$s gespeichert + Bearbeiten + Nach Updates suchen + Suche nach Updates… + Auf %1$s aktualisieren + Aktuell · %1$s + %1$s aktualisiert + %1$s · %2$s + App hinzufügen + Skript + AppImage + GitHub + Flatpak + Eine .sh-Datei. Sie läuft aus ihrem eigenen Ordner, im Vollbild unter gamescope. + Ein ARM64-AppImage (aarch64) oder ein x86_64-AppImage, das über FEX läuft. Es wird einmalig in die Linux-Laufzeit entpackt. + Installiert das ARM64-AppImage aus dem neuesten Release des Repositorys oder dessen x86_64-Version über FEX. + FEX verwenden + Auto + An + Aus + FEX führt x86-Linux-Programme auf diesem ARM64-Gerät aus. Auto nutzt es, wenn das Programm für x86 gebaut ist. + %1$s über FEX + Dieses Programm ist für %1$s-PCs gebaut und läuft über FEX, das noch nicht eingerichtet ist. Öffne Steam einmal, während du angemeldet bist: DroidDeck lässt es FEX und die nötige Steam Linux Runtime herunterladen, danach lässt sich das Programm öffnen. + Dieses Programm + %1$s ist für x86-PCs gebaut und läuft über FEX, das noch nicht installiert ist. Starte Steam einmal, während du angemeldet bist, und lass es FEX und die Steam Linux Runtime herunterladen. Starte es danach erneut. + Eine Flathub-App-ID oder ihre Flathub-Seite. Flatpak wird zuerst eingerichtet, falls noch nicht geschehen. + Datei + Keine Datei ausgewählt + Datei auswählen + Ändern + Skript auswählen + AppImage auswählen + Symbol auswählen + Repository + besitzer/repo oder ein github.com-Link + Kein GitHub-Repository + App-ID + org.ppsspp.PPSSPP + Keine Flathub-App-ID + Oder eine .flatpak-Datei für eine App, die es nicht auf Flathub gibt. + .flatpak-Datei auswählen + Quelle + Auf deinem Desktop + Neue App + Name + Aus der App + Symbol + Standardsymbol + Symbol auswählen + Entfernen + Aus dem Repository + Suche nach Symbolen… + Standard + %1$s bearbeiten + Speichern + Ordner wird geprüft… + Sein Ordner wird in die Sitzung eingebunden: %1$s + Sein Ordner enthält Linux-Programme, die im gemeinsamen Speicher nicht laufen können, daher wird er in DroidDeck kopiert (%1$s). + Hinzufügen + Abbrechen + Installiere zuerst die Linux-Laufzeit unter „Einrichtung“. + Warte, bis %1$s fertig ist. + Kompatibel + Builds, die in Steams Menü „Kompatibilität“ mit „%1$s“ markiert sind, enthalten die Präfix-Korrekturen von DroidDeck und starten hier Spiele. Standard ist Proton Experimental ARM64 (%1$s) bzw. Proton 11.0 ARM64 (%1$s), sobald dieser neuere Build installiert ist. Valves eigene Proton-Einträge ohne Markierung können auf diesem Gerät kein Spiel starten. + Installiert: %1$s · %2$s + Abbrechen + Auto + Nie + Immer + Deinstallieren + Prüfen + Aktualisieren + Wird geprüft… + Wird ausgeführt… + Vergessen + Hinzufügen… + Verwenden + Gilt ab der nächsten Sitzung. + Steam-Sitzung + Desktop-Sitzung + Anzeige + Auflösung + Bis zu 720p + Bis zu 900p + Bis zu 1080p + Nativ + %1$s (Standard) + Benutzerdefiniert · %1$d×%2$d + Benutzerdefiniert… + 720p kann Menüs reaktionsschneller machen. + Seitenverhältnis + Durch die benutzerdefinierte Auflösung festgelegt. + Auto verwendet mindestens 16:9. + Bildratenlimit + Begrenzt die gesamte Sitzung. Das Display wechselt auf eine Bildwiederholrate, die ein ganzzahliges Vielfaches des Limits ist. + HDR + HDR10-Ausgabe + Nicht verfügbar: %1$s. + Starte die App neu, um dies anzuwenden. + Treiber + GPU-Treiber + Für Steam und Desktop gemeinsam. Wird auf der Seite „Komponenten“ festgelegt. + Touch & Steuerung + Touch + Touchpad + Direkt + Auto: Touchpad auf dem Desktop, direkt in Steam. + Bildschirmsteuerung + Steam + QAM + Auto zeigt die Steuerung an, wenn kein Controller verbunden ist. + Steam-Touch (Beta) + Steams Touch-Steuerung startet mit der nächsten Steam-Sitzung + Steam-Touch-Layout + Touch-Steuerung dieses Spiels verschieben, skalieren oder ausblenden; wird in Steam gespeichert + Controller + Wie Steam deinen Controller erkennt. Gilt ab der nächsten Sitzung. + Steam-Deck-Controller + Steam Controller + DualShock 4 + DualSense + Xbox-360-Controller + Deck: Schnellzugriffstaste und Gyro. Steam Controller: dasselbe, mit Griffsensor an den Rücktasten. DualShock 4 und DualSense: Gyro, die Trackpads dienen als deren Touchpad. Xbox 360: Schnellzugriff mit Guide+A öffnen. + Zurück + Menü → QAM + QAM → Menü + Sitzung + Verhalten im Hintergrund + Wenn die App ausgeblendet ist oder der Bildschirm ausgeht. + Manuell + Nativ + Auto pausiert und setzt fort, sobald die App wieder sichtbar ist. Nativ bereitet Steam zuerst auf den Ruhemodus vor. Manuell wartet auf „Fortsetzen“. Nie lässt alles weiterlaufen. + Downloads weiterlaufen lassen + Verzögert das Anhalten im Hintergrund, solange Steam aktiv herunterlädt. + Auto pausiert im Hintergrund und setzt fort, sobald die App wieder sichtbar ist. Manuell wartet auf „Fortsetzen“. Nie lässt alles weiterlaufen. + Start + Steam mit DroidDeck starten + Öffnet die Steam-Sitzung, wenn DroidDeck startet. + Decky + Suche nach Releases… + Bereit zur Installation von %1$s. + Kein kompatibler Build gefunden. Versuch es später noch einmal. + Installiert · %1$s + Update verfügbar · %1$s + Aktuell · %1$s + Loader + Neueste Version installieren + Startet mit Steam. Solange aktiv, können andere Apps auf diesem Gerät Steam möglicherweise steuern. + Aus. Der Remote-Debugging-Port von Steam bleibt geschlossen. + Decky Loader deinstallieren? + Entfernt den Loader. Deine Plugins und Einstellungen bleiben erhalten. + Client + Leistungs-Overlay + Deaktivieren, wenn Spiele im Deck-Modus abstürzen oder schwarz werden. Gilt ab der nächsten Sitzung. + Steam-Client reparieren + Lädt den Steam-Client beim nächsten Start von Steam erneut herunter. Nutze dies, wenn Steam im Ladebildschirm hängen bleibt. Spiele, Anmeldung und Einstellungen bleiben erhalten. + Der Steam-Client wird beim nächsten Start von Steam erneut heruntergeladen. + Reparieren + Grafiken + SteamGridDB-API-Schlüssel + Dein Schlüssel + Bearbeiten… + Ergänzt fehlende Grafiken aus Steam, dann aus SteamGridDB. Deine eigene cover.jpg, hero.jpg, logo.png oder icon.png im Spielordner hat Vorrang. + %1$s entfernen + Entfernen? + Spiel hinzufügen + Spiel hinzufügen: + EXE-Datei auswählen: + Dieser Ort ist in der Sitzung nicht verfügbar + Diese EXE-Datei prüfen + Windows-Komponenten + Mit LB / RB zwischen den Abschnitten wechseln. + Keine + Windows-DLLs aus der Komponentenliste, die Proton nicht enthält. Aktiviere die, die dieses Spiel braucht: Sie landen im eigenen Windows-Ordner des Spiels, nie bei seinen Dateien, und bleiben erhalten, egal mit welchem Proton es läuft. + Komponentenliste nicht erreichbar. Bereits heruntergeladene Komponenten lassen sich weiterhin umschalten. + Für dieses Spiel empfohlen + Prüfe, was dieses Spiel braucht… + Verfügbar, wenn online + + %1$d Komponente wird aktiviert… + %1$d Komponenten werden aktiviert… + + Für dieses Spiel aktiv + Steam installiert %1$s mit %2$s (App %3$s) + Aus der GOG-Liste für dieses Spiel + Aus den Epic-Voraussetzungen für dieses Spiel + Aus den Amazon-Installationsschritten für dieses Spiel + Steam installiert %1$s mit App %2$s, per Name zugeordnet + Ebenfalls aufgeführt + Aufgeführt von %1$s; hier keine Komponente + Steam + GOG + Epic Games + Amazon Games + Das Spiel liefert %1$s mit + Das Spiel hat %1$s (funktioniert oft so) + Bereits in diesem Spiel + Nichts im Spielordner deutet auf eine Komponente hin. + Für dieses Spiel installiert (%1$d) + Alle Komponenten (%1$d) + Benötigen einen Windows-Installer (%1$d) + Das sind eigenständige Setup-Programme ohne Paket, das sich zerlegen ließe, und für sie gibt es noch keine Aufzeichnung dessen, was sie installieren. Die meisten sind alte Laufzeiten, die das Mono in Proton bereits ersetzt; Aufzeichnungen kommen hinzu, sobald sie erstellt werden. + Anzeigen + Ausblenden + Führt ein eigenes Setup-Programm aus (noch keine Aufzeichnung) + Hier noch nicht unterstützt + Komponenten werden geladen… + Installation fehlgeschlagen: %1$s + Heruntergeladen + %1$s wird heruntergeladen + Wird installiert: %1$s + Wird deaktiviert… + %1$s Download + Wirkt beim nächsten Spielstart. + 16:9-Spiele strecken + Auf Bildschirmen, die quadratischer als 16:9 sind, werden Spiele bildfüllend gestreckt statt mit Balken angezeigt. Gilt ab der nächsten Sitzung. + Audio + DirectAudio für Spiele + Umgeht PulseAudio für geringere Latenz. + Mikrofon + Für Sprachchat. + Netzwerk + Wi-Fi-Erkennung + Wi-Fi-Namen und Netzwerke in der Nähe in Steam anzeigen. + Standortdienste sind deaktiviert. + Standortberechtigung nicht erteilt. + Einstellungen öffnen + Weiter + Wi-Fi-Erkennung aktivieren? + Android verlangt die Standortberechtigung, um Wi-Fi-Namen zu lesen und Netzwerke in der Nähe zu finden. DroidDeck nutzt diesen Zugriff, um diese Netzwerke in Steam anzuzeigen. Wir verfolgen deinen Standort nicht. Der Internetzugang funktioniert auch ohne diese Berechtigung. + Spielspeicher + Automatisch - die SD-Karte, falls vorhanden, plus deine Games-Ordner + Automatisch - deine Games-Ordner plus die SD-Karte, falls vorhanden (derzeit keine) + Nur intern + Ordner: %1$s + Zweite Bibliothek + Spiele, die Daten von der SD-Karte oder aus dem gemeinsamen Speicher streamen, können ruckeln. + Speicherdiagnose + Zeichnet Zuweisungsergebnisse und stichprobenartige Speicher-Timings für „Logs teilen“ auf. Kann Installationen verlangsamen, solange aktiv. Gilt ab der nächsten Sitzung. + Ordner auswählen… + Benutzerdefinierte Auflösung + Ersetzt Obergrenze und Seitenverhältnis. Eine Größe, die nicht zum Bildschirm passt, bekommt Balken. + Breite + Höhe + Zwischen 320×240 und 3840×2160. + + %1$d Spiel + %1$d Spiele + + OK + Löschen + Wiederherstellen + Herunterladen + %1$s löschen + %1$.1f MB + Laufzeittreiber + Wird von Steam, Spielen und dem Desktop genutzt. Gilt ab der nächsten Sitzung. + Turnip-ZIP importieren… + Anzeigetreiber + Bringt in beiden Modi die Frames auf den Bildschirm. Starte die App neu, um dies anzuwenden. + AdrenoTools-ZIP importieren… + nie + Nightlies geprüft: %1$s + Komponenten + Über Komponenten + Nach neuen Treibern suchen + Nightlies nach neuen Paketen durchsuchen + Proton + Das hier gewählte Proton führt jedes Spiel aus, das in Steam kein eigenes gewählt hat, und eine Änderung in Steam erscheint auch hier. Steam ersetzt Valves Proton bei Updates; die Originale werden pro Build aufbewahrt. + Konfiguration + Profil konfigurieren + Profil + Standardprofil + Wird automatisch von Spielen ohne eigene Überschreibungen verwendet. + Gespeicherte Einstellungen, die beim Start von %1$s automatisch verwendet werden. + Wähle nur die Einstellungen, die dieses Spiel braucht. Alles andere nutzt weiterhin das Standardprofil. + FEX-Voreinstellung + Voreinstellung des Standardprofils wird verwendet. + Eigene Voreinstellung für dieses Spiel gespeichert. + Eigene FEX-Variablen + Standard verwenden + Spielkompatibilität + Umgebungsoptionen für Proton, DXVK, VKD3D und Synchronisation in diesem Spiel. + Proton des Standardprofils wird verwendet. + Eigenes Proton für dieses Spiel über die Steam-Kompatibilitätszuordnung gespeichert. + Kein Proton + DXVK + VKD3D-Proton + Nutzt das Paket des Standardprofils aus %1$s, sofern dieses Spiel kein anderes Paket wählt. + Nutzt das Komponentenpaket des Standardprofils. + Nutzt %1$s nur für dieses Spiel. + Laufzeitoptionen + Weitere spielspezifische Umgebungsoptionen für Proton, DXVK, VKD3D-Proton und Synchronisation. + Das Profil von %1$s ist für den nächsten Spielstart gespeichert. + Ohne Steam starten + Für ein Spiel, das sich sofort schließt: startet es ohne Protons Steam-Einrichtung. Steam-Funktionen wie das Overlay sind dafür deaktiviert. + An + Die Proton-Auswahl für %1$s ist gespeichert und jetzt in Steam verfügbar. + Die Proton-Auswahl für %1$s ist gespeichert. Starte Steam neu, bevor du das Spiel startest. + %1$s kann das gewählte Proton auf diesem Gerät nicht verwenden. + Profil von %1$s konnte nicht geladen werden: %2$s + Das Spielprofil konnte nicht gespeichert werden: %1$s + Spielprofil zurücksetzen + Profil von %1$s zurücksetzen? + Überschreibungen dieses Spiels für Proton, FEX, Komponentenpaket, Umgebung, Texturfilterung und Windows-Komponenten entfernen? Es nutzt dann das Standardprofil. + %1$s nutzt jetzt das Standardprofil. + Das Spielprofil konnte nicht vollständig zurückgesetzt werden: %1$s + %1$s führt jetzt jedes Spiel aus, das in Steam kein eigenes Proton gewählt hat. + %1$s führt ab dem nächsten Start von Steam jedes Spiel aus, das kein eigenes Proton gewählt hat. + %1$s kann hier keine Spiele starten und daher nicht der Standard sein. + Das Standard-Proton konnte nicht gespeichert werden: %1$s + %1$s · %2$d ausgetauscht + %1$s · alles original + %1$s · Spiel läuft + %1$s · droiddeck-esync-Pack + %1$s · noch kein droiddeck-esync-Pack + %1$s · esync integriert, kein Pack nötig + GPU-Treiber + %1$s · beim Start erneut angewendet %2$s + Als Nächstes: %1$s · nach Ende des Spiels + %1$s in Verwendung: %2$s + Wartet, bis das Spiel beendet ist + Gilt ab dem nächsten Spielstart + .zip importieren + Protons werden gelesen… + Noch kein Proton installiert. Starte Steam einmal, um sein ARM64-Proton herunterzuladen, oder füge GE-Proton, CachyOS oder DroidDeck-Proton unter „Einrichtung“ hinzu. + Original · %1$s + %1$s · dieser Build + %1$s · früherer Build + Installiert · %1$d + Für %1$s ist noch nichts gespeichert. Die eigenen Dateien dieses Protons werden als sein Original gesichert. + Auf diesem Proton läuft ein Spiel, daher wartet dies, bis es beendet ist. + Gilt ab dem nächsten Spielstart. + %1$s austauschen? + %1$s in %2$s einsetzen? Die Originaldateien bleiben gesichert. %3$s + %1$s wiederherstellen? + Das Original von %1$s wieder in %2$s einsetzen? %3$s + Entfernt das Paket aus der App. Du kannst es erneut herunterladen. + Dieses Original löschen? + Dieses %1$s-Original stammt aus einem früheren Build von %2$s. Das Original des aktuellen Builds wird immer behalten. + In den Nightlies · %1$d + Aktualisiere, um die Pakete in den Nightlies aufzulisten. + Nichts Neues seit der letzten Prüfung. + .wcp importieren + Original dieses Protons wiederherstellen + Das originale %2$s von %1$s wiederherstellen? + Austauschen + FEX, DXVK und VKD3D-Proton für jedes Proton, mit dem Steam Spiele ausführt.\n\nWähle eine installierte Zeile, um sie einzusetzen. Das gilt ab dem nächsten Spielstart oder sobald das laufende Spiel beendet ist.\n\nDeine Auswahl wird vor jedem Start erneut geprüft und wiederhergestellt, falls etwas sie geändert hat. Die Originaldateien jedes Protons werden aufbewahrt, sodass Steam-Updates sie nie verlieren. + Original + In Verwendung + Neu + Gespeichert + Leistung + CPU-Kerne, Sitzungskorrekturen und der Hostname. Gilt ab der nächsten Sitzung. + Steam-Client-Kerne + Kernauswahl von Steam überschreiben + Bindet Steam, seinen UI-Helfer und gamescope an die Kerne unten. + Client-Kerne + Kerne für Steam. + Aktiviere die Überschreibung, um auszuwählen. + Spielkerne + Spielkerne + Kerne für Spiele. Sind alle ausgewählt, verteilt Android frei. + Client-Oberfläche + Threaded GL + Kann Steam-Menüs reaktionsschneller machen. + Zink: verzögerte Deskriptoren + Empfohlen für Treiber ohne Deskriptorpuffer. + GL-Fehlerprüfungen überspringen + Schaltet die GL-Validierung pro Aufruf ab. + gamescope: Echtzeit-GPU-Warteschlange + Priorisiert die GPU-Arbeit des Compositors. Kann das Frame-Pacing glätten, kostet Spiele aber etwas Leistung. + GPU + GPU auf höchstem Takt halten + Nur Adreno. Weniger Ruckler, mehr Akkuverbrauch und Wärme. + Sitzungskorrekturen + Turnip: Sysmem-Rendering + Schneller für Steam und die meisten Spiele. Erforderlich bei Adreno 710/720/722. + Steams xalia-Helfer überspringen + droiddeck-ntsync (experimentell) + Ein Userspace-Ersatz für den Linux-Treiber ntsync, zum Testen. Solange er aktiv ist, nutzt Proton ihn statt droiddeck-esync und droiddeck-fsync. Funktioniert mit Proton Experimental ARM64, GE-Proton und proton-cachyos. Gilt ab der nächsten Sitzung. + droiddeck-esync + droiddeck-fsync bevorzugen + Führt Protons eigenes fsync mit den unveränderten Proton-Dateien aus, wobei futex_waitv im Userspace bereitgestellt wird. Es braucht daher kein Pack und funktioniert weiter, sobald Steam Proton aktualisiert. Solange es aktiv ist, nutzen Spiele es vor droiddeck-esync, außer den wenigen, die Proton von fsync fernhält. Gilt ab der nächsten Sitzung. + Die Windows-Threads von Spielen warten über eventfd und gemeinsamen Speicher aufeinander statt über Umwege durch wineserver. Nutzt ein droiddeck-esync-Pack, das für genau diesen Proton-Build erstellt wurde; Packs werden automatisch geladen. Bis es eines für einen neuen Proton-Build gibt, springt droiddeck-fsync ein. Gilt ab der nächsten Sitzung. + Deaktiviert Protons Gamepad-Navigationshilfe, die in jedem Spiel CPU-Leistung kostet. + proot ohne seccomp ausführen + Kann Fehler wegen fehlender Syscalls auf manchen Kerneln beheben. Kann die Leistung mindern. + proot-Schnellpfad + Benötigt den seccomp-Filter von proot. + Schnellere Dateisuche und weniger Ruckler. Deaktivieren, wenn ein Programm seine Dateien nicht findet. + Sitzungsidentität + Hostname + Der Name, den Steam für dieses Gerät sieht. Leer lassen stellt %1$s wieder her. + Nur Buchstaben, Ziffern und innere Bindestriche, bis zu 63 Zeichen. + Android ist so eingestellt, dass es diese Sitzung beendet + Installiert + Zum Download verfügbar + Nichts Neues seit der letzten Prüfung. + Integrierte Treiber wiederherstellen + In die App integriert: Dies entfernt die entpackten Dateien und blendet ihn aus. „Integrierte Treiber wiederherstellen“ holt ihn zurück. + Hälfte eines Android- + Linux-Bundles: Beide Hälften werden entfernt. Ein verwendeter Treiber fällt auf den Standard zurück. + Seine Dateien werden aus der App entfernt. + Seine Dateien werden entfernt. Er ist in Verwendung, daher übernimmt der Standard. + Anzeige + Effekte + Steuerung + Komponenten + Controller + Einstellungen + Seite %1$s + Steam + Desktop + Steam-Menü + Schnellzugriff + Schnellzugriffsmenü öffnen + Vorherige Seite + Nächste Seite + Leistungs-HUD + Skalierungsmodus + Skalierungsschärfe + Bildschirmeffekte + Live. Kostet etwas GPU-Zeit, solange ein Effekt aktiv ist. + Voreinstellungen + Benutzerdefiniert + Erweitert + Ausgeklappt + Eingeklappt + Schärfen (CAS) + Schärfegrad + Fake-HDR + Debanding + Debanding-Stärke + Helligkeit + Kontrast + Gamma + Sättigung + FXAA (Kantenglättung) + Toon-Konturen + CRT + NTSC + Texturfilterung + DirectX-9–11-Spiele, ab ihrem nächsten Start. + Anisotrope Filterung + Texturschärfe + Ein negativer Mip-Bias: schärfere Texturen, etwas Flimmern. Auto richtet sich danach, wie stark die Sitzung auf den Bildschirm vergrößert wird. + Auto (%1$s) + Tastatur + PC-Tastatur + Android + Zweiter Bildschirm + Modus + Zeigt + Android-Apps + App starten + Ausblenden + Anzeigen + Keine startbaren Apps + Sitzungsverhalten + Gilt sofort und für künftige Sitzungen in diesem Modus. + Nächste Sitzung + Support + Sitzungslogs + An einen Fehlerbericht anhängen + Logs teilen + Hintergrund + Beenden + Sitzung beenden? + Läuft gerade + Es läuft kein Spiel auf einem Proton + Spielprofil + Profildetails sind noch nicht verfügbar. + Spielprofile konnten nicht geladen werden. Neuer Versuch erfolgt automatisch. + Spielprofile konnten nicht aktualisiert werden. Die letzten verfügbaren Daten werden angezeigt; neuer Versuch erfolgt automatisch. + Gespeicherte Einstellungen für den nächsten Start ansehen und bearbeiten. + Standardprofil wird verwendet + Eigenes Profil + %1$s aus dem Standardprofil wird verwendet. + Änderungen werden im Profil von %1$s gespeichert und gelten ab dem nächsten Spielstart. + + %1$d Spielüberschreibung + %1$d Spielüberschreibungen + + Es ist noch kein Proton installiert. + Spiel läuft + beim Start erneut angewendet %1$s + Original + Importieren und Löschen auf der Seite „Komponenten“. + Fertig + Handlungsbedarf + In Arbeit + Prüfe IP-Adresse und Port unter „Debugging über WLAN“ oder kopple erneut, falls Android dieses Gerät entfernt hat. + Installieren + Verwalten + Übersicht + Controller + Sitzung + Launcher + Einrichtung + ● Alles bereit + %1$d von %2$d bereit + Systemprüfung + Gerät unterstützt + Ungetestete GPU + GPU nicht unterstützt + %1$s · %2$s + %1$s: %2$s + Steam benötigt einen Adreno-Treiber; %1$s zeigt möglicherweise einen schwarzen Bildschirm + Linux-Laufzeit + %1$s · %2$d %% + Nicht installiert · ca. 3 GB, wird beim ersten „Spielen“ installiert + Installiert + %1$s · Update verfügbar + %1$s · aktuell + Limit für Kindprozesse + An · Android kann Steam schließen. Das Abschalten dauert eine Minute. + Nicht gesetzt · Android kann Steam schließen. Das Abschalten dauert eine Minute. + Konnte nicht geprüft werden · das Abschalten dauert eine Minute. + Beheben + Details + Entwickleroptionen + „Debugging über WLAN“ verwenden + Erneut prüfen + Limit aktivieren + Steam-Konto + Angemeldet als %1$s · Offlinemodus + Angemeldet als %1$s + Drücke „Spielen“ und melde dich bei Steam an + Tools + Controller + Controller-Einstellungen sind nicht verfügbar. + Sitzung + Sitzungslogs + Nach jeder Sitzung gespeichert + Debugging-Tools + Neueste Sitzungslogs + Gespeicherte Sitzungslogs + Behält die neuesten %1$d + Logs löschen + Offlinemodus + Melde dich zuerst bei Steam an + Launcher + Sprache + Auch Steam nutzt sie, ab seinem nächsten Start. + Systemstandard (%1$s) + Systemstandard + App-Skalierung + Text und Bedienelemente von DroidDeck + %1$d %% (Standard) + Design + Aussehen des Launchers + Als Startbildschirm verwenden + DroidDeck kann die Startbildschirm-App sein + DroidDeck wird nie als Startbildschirm-App angeboten + Ausrichtung + Automatisch Querformat + Querformat + Umgekehrtes Querformat + Vollbild + Blendet die Status- und Navigationsleiste von Android aus + Zeigt die Status- und Navigationsleiste von Android an + Standard-Startbildschirm-App + Startbildschirm-App auswählen + Auswählen + Linux-Apps (Beta) + Dateien + Dateien durchsuchen und verwalten + Proton-Versionen + ARM64-Proton-Builds installieren + Leistung + Zuweisung der CPU-Kerne + ROMs-Ordner + Speicherort für Emulatorspiele + Komponenten + FEX, DXVK, VKD3D + Steuerung + Tastenbelegung + Entdecken + Suchen + Installiert + %1$s · %2$d + Store + Flathub · ARM64 + ● Flatpak bereit + Installiere zuerst die Linux-Laufzeit unter „Einrichtung“. + Flatpak einrichten + Fügt Flatpak hinzu und verbindet es mit Flathub (ca. 20 MB). Die erste App lädt außerdem ihre Laufzeit herunter. + Wird eingerichtet… + Flatpak + Updates + %1$s · %2$s + %1$s · %2$s · %3$d %% + Wird gestartet… + Kategorien + Flathub nicht erreichbar. Prüfe deine Verbindung und versuch es noch einmal. + Erneut versuchen + Flathub durchsuchen + Firefox, Spiele, Emulatoren… + Suchen + Alle + Suche läuft… + Suche nach Namen oder wähle eine Kategorie. Nur ARM64-Apps werden aufgeführt. + Keine ARM64-Apps auf Flathub passen dazu. + Ergebnisse + Nach Updates suchen + Alle aktualisieren (%1$d) + ● Alles ist aktuell + Noch keine Apps. Finde welche unter „Entdecken“ oder „Suchen“. + Richte Flatpak ein, um Apps zu installieren. + Update verfügbar · %1$s + Öffnen + Details + Entfernen + Zum Entfernen erneut drücken + Wird installiert… + Installieren + Richte zuerst Flatpak ein + %1$s Download + ● Installiert + Die Flathub-Seite dieser App konnte nicht geladen werden. + Wird von Flathub geladen… + Kein ARM64-Build auf Flathub, daher hier nicht installierbar. + Screenshots + Über + App-ID + Lizenz + Laufzeit + Installierte Größe + %1$s (plus gemeinsame Laufzeit) + Website + Verifiziert + Updates + DroidDeck %1$s · %2$s + Geprüft: %1$s + Noch nicht geprüft + Erlaube DroidDeck, sich selbst zu aktualisieren + Aktiviere auf dem nächsten Bildschirm „Dieser Quelle vertrauen“ und kehre dann zurück. Das Update läuft von selbst weiter. + Einstellungen öffnen + Nicht jetzt + Anders signiert + Direktes Update nicht möglich + Diese Kopie ist nicht mit dem Release-Schlüssel von DroidDeck signiert, daher lassen sich Updates nicht darüber installieren. Deinstalliere sie, um zu wechseln. + Update verfügbar + Installation nicht möglich + Bereit zum Wechseln + Neuer als Stabil + Du bist dem Stabil-Kanal voraus + Dieser Build ist neuer als die aktuelle Stabil-Version. Mit dem nächsten Release wechselst du zu Stabil. + Test beendet + Dieser Test ist beendet + Sein Fix wurde übernommen oder verworfen. Folge „Vorschau“ für die neuesten Fixes. + Aktuell + Du hast die neueste Version von %1$s + · %1$s + %1$s… %2$d %% + %1$s… + Aktualisieren%1$s + Installieren%1$s + Stabil %1$s trotzdem installieren + Vorschau folgen + Beende die laufende Sitzung, um zu aktualisieren. + Weniger + Mehr + DroidDeck %1$s + Neuer Vorschau-Build + Test-Build für PR #%1$d + %1$d MB + UPDATE-KANAL + Stabil + Getestete Releases, für die meisten + %1$s · %2$s + Vorschau + Neueste Fixes, vor Stabil + Aktuell + Test-Builds + Einen Fix vor dem Release ausprobieren + Derzeit keine + PR #%1$d + Nicht für diese Kopie von DroidDeck gebaut + Aktualisiert: %1$s + Test zu PR #%1$d + vor einer Weile + gerade eben + + %1$d zum Ausprobieren + %1$d zum Ausprobieren + + + Änderungen seit deinem Build · %1$d Build zurück + Änderungen seit deinem Build · %1$d Builds zurück + + + Neueste Änderungen · %1$d Build + Neueste Änderungen · %1$d Builds + + Eine Android-Einstellung muss geändert werden + Steam + Android stoppt Apps, die viele Hintergrundprozesse starten, und Steam startet Dutzende. Solange das so bleibt, werden Spiele ohne Fehlermeldung geschlossen. + Für mich beheben + Aktiviere „Einschränkungen für untergeordnete Prozesse deaktivieren“ und kehre dann zurück. Lass die Entwickleroptionen aktiviert, sonst geht der Schalter wieder aus. + Entwickleroptionen öffnen + Diese Android-Version hat dafür keinen Schalter, deshalb ändert DroidDeck es über „Debugging über WLAN“. Dauert etwa eine Minute, kein Computer nötig. + „Debugging über WLAN“ braucht eine Wi-Fi-Verbindung. Jedes Netzwerk geht; Internet ist nicht nötig. + Der Kopplungscode wird in eine Benachrichtigung eingegeben. Erlaube Benachrichtigungen für DroidDeck oder nutze „Andere Wege“. + Benachrichtigungen erlauben + Öffne Einstellungen → Entwickleroptionen → Debugging über WLAN und aktiviere es + Tippe auf „Gerät über einen Kopplungscode koppeln“ + Gib den Code in die DroidDeck-Benachrichtigung ein + Warte auf das Kopplungs-Pop-up… + Pop-up gefunden. Gib seinen Code in der Benachrichtigung ein. + Status + Prüft alle 2 Sekunden. Diese Seite schließt sich, sobald es aus ist. + Andere Wege + Debugging über WLAN, manuelle Adresse, ADB am Computer + Manuelle Adresse, ADB am Computer, Entwickleroptionen + Stattdessen „Debugging über WLAN“ verwenden + Manche ROMs haben den Schalter trotzdem. Suche in den Entwickleroptionen nach „untergeordnete Prozesse“. + Falls die Benachrichtigung nicht funktioniert, gib Adresse und Code des Pop-ups selbst ein. + Adresse manuell eingeben + Von einem Computer mit ADB: + Befehl kopieren + Wähle, auf welchem Display die Android-Einstellungen geöffnet werden. + Hauptbildschirm + Gekoppelt. Suche den Port für Debugging über WLAN… + Gekoppelt. Gib den Port aus den Einstellungen ein. + Gerät gefunden. Einstellung wird angewendet… + Automatisches Anwenden fehlgeschlagen. Prüfe die Adresse unten und versuch es erneut. %1$s + Debugging über WLAN + Limit für untergeordnete Prozesse aktualisiert + Limit für untergeordnete Prozesse ändern + Fehlt die Option, kopple über „Debugging über WLAN“. + Gekoppelt. Prüfe IP-Adresse und Port unter „Debugging über WLAN“. + Das Limit für untergeordnete Prozesse wurde aktualisiert. + Nutze Schritt 2, wenn die Entwickleroptionen keine Einstellung für untergeordnete Prozesse haben. Öffne Entwickleroptionen → Debugging über WLAN → Gerät über einen Kopplungscode koppeln. + Gekoppelt. Nutze die Adresse unten oder aktualisiere sie über Debugging über WLAN → IP-Adresse und Port. + Das Limit für untergeordnete Prozesse wurde über „Debugging über WLAN“ aktualisiert. + Android hat bestätigt, dass das Limit aktiv ist. + Android hat bestätigt, dass das Limit aus ist. + 2 · Für Debugging über WLAN koppeln + IP-Adresse und Port aus dem Kopplungs-Pop-up + Kopplungscode (PIN) + 6 Ziffern + Koppeln + 2 · Per Debugging über WLAN anwenden + IP-Adresse und Port für die Verbindung + Erneut koppeln + Limit deaktivieren + Andere Wege, es zu ändern + 1 · Entwickleroptionen (zuerst versuchen) + Schalte „Untergeordnete Prozesse einschränken“ ein. + Schalte „Untergeordnete Prozesse einschränken“ aus. + 3 · ADB am Computer (letzter Ausweg) + Nur wenn „Debugging über WLAN“ nicht verfügbar ist. + 3 · ADB am Computer (letzter Ausweg) + Wann die Touch-Steuerung in einer Sitzung erscheint + Farbe + Farbton der Bildschirmtasten + Benutzerdefiniert + Deckkraft + %1$d %% + Tastengröße + Standardgröße: 100 % + Stick-Klick + Doppeltippen und Stick halten für L3 oder R3 + Adaptive Sticks + Spielvibration + Steam-Taste + QAM-Taste + Tastatur-Taste + Mit Select das Steam-Menü öffnen + Statt View/Select. + Animationen + Sticks erscheinen dort, wo du tippst, und verschwinden beim Loslassen + Layout + Eigene Positionen gespeichert + An diesen Bildschirm angepasst + Bearbeiten + Zurücksetzen + Tastenbelegung + Jede Taste sendet ihre eigene Eingabe + %1$d von %2$d Tasten neu belegt + Konfigurieren + Controller zurücksetzen + Stellt alle Controller-Einstellungen und das Layout wieder her + Was jede Bildschirmtaste an das Spiel sendet. „Ausgeblendet“ entfernt die Taste. + Bildschirmtasten + Standardwerte + Belegung zurücksetzen + Android-Apps + %1$d Apps + Keine startbaren Android-Apps gefunden. + ● Desktop installiert + Wird beim ersten Öffnen installiert + Zur Installation verfügbar + Nicht installiert. + %1$s · nicht installiert + %1$s öffnen + Spiele + Wähle einen ROMs-Ordner. + Navigiere in RetroArch zu /root/ROMs. + Füge %1$s-Spiele zu ROMs/%2$s hinzu. + %1$s installieren + Zum Installieren Sitzung beenden + Laufzeit erforderlich + Installationsdetails werden geladen… + Installationsdetails sind nicht verfügbar. Versuch es später noch einmal. + Installiere zuerst die Linux-Laufzeit unter „Einrichtung“. + Beende die laufende Sitzung, um Desktop-Apps zu installieren. + Installiert %1$s in den Linux-Desktop. + %1$s · %2$d %% + Ausblenden entfernt es vom Desktop. Seine Dateien bleiben in der Linux-Laufzeit. + Dieses Spiel ist nicht mehr im ROMs-Ordner. + Desktop · %1$s + In %1$s starten + Auch in %1$s + Diese Seite gibt es nicht mehr. Wähle links einen Bereich. + Desktop öffnen + Desktop installieren & öffnen + Desktop deinstallieren + Entfernt den Desktop und seine mitgelieferten Apps. Spiele, hinzugefügte Apps, Einstellungen und gemeinsame Bibliotheken bleiben erhalten. + Desktop wird entfernt + Desktop entfernt + Alte LXQt-Dateien werden entfernt + Aktualisiere DroidDeck, bevor du diese Desktop-Version entfernst. + Linux-Desktop + KDE Plasma, Firefox und deine Emulatoren + Spiele, die du in Steam installierst, erscheinen hier nach der Sitzung. + Mit Steam spielen + Starteinstellungen + Starten + Spieldateien + Proton-Präfix + Suche nach Spielständen… + Kein Spielstandordner per Name gefunden. Der Export nimmt den ganzen Benutzerordner. + %1$d Spielstandordner + %1$s · %2$d Dateien · %3$s + %1$d KB + Spielstände verwalten + Spielstände + Spielstände importieren… + Als GameHub-ZIP exportieren… + Als Winlator-ZIP exportieren… + Spielstandordner öffnen + Nie gespielt + Laufzeit beschäftigt · %1$d %% + Laufzeit beschäftigt + Laufzeit wird beim ersten „Spielen“ installiert + Laufzeit-Update verfügbar + ● Laufzeit bereit + Zuletzt gespielt: %1$s + Interner Speicher + Hinzugefügtes Spiel + Eigenes Spiel + Spielspeicher + Das beste Paar für diese GPU, stets aktuell + Wähle selbst ein Paar + Suche nach Treibern… + Noch keine Treiber aufgeführt. Aktualisiere, um danach zu suchen. + Treiber für andere GPUs ausblenden + Treiber für andere GPUs anzeigen + Erweitert + Zusammen mit dem Anzeigetreiber festgelegt: %1$s enthält beide. + Zusammen mit dem Laufzeittreiber festgelegt: %1$s enthält beide. + Wird von Spielen und dem Desktop genutzt. + Laufzeit-Standard + Bringt die Frames auf den Bildschirm. Gilt nach einem Neustart von DroidDeck. + Auto - nach GPU gewählt + Herunterladen, importieren oder löschen… + IN VERWENDUNG + Die in DroidDeck integrierten Treiber + Laufzeit- und Anzeigetreiber zusammen als passendes Paar festgelegt. + Empfohlen für diese GPU: %1$s. + Aktualisiere, um nach Treibern für diese GPU zu suchen. + EMPFOHLEN + Nicht verfügbar + Linux-Laufzeit + Linux-Laufzeit wird vorbereitet + Sitzung + Anzeige und Audio werden gestartet + Steam-Client + Steam und seine Kompatibilitätstools werden geprüft + Steam wird geöffnet + Warte auf das erste Bild von Steam + Desktop + Desktop wird vorbereitet + Wird geöffnet + Warte auf das erste Bild + Steam wird gestartet + Die Sitzung wurde beendet + Schritt %1$d von %2$d + Steam ist pausiert + Griffe + Linkes Pad + Rechtes Pad + Beide + Gamepad + Stick + Trackpad + Zwischen Stick und Trackpad wechseln + Weitere Deck-Steuerelemente + BEREICHE + Steuerung auf dem Zweitbildschirm schließen + Steam-Menü öffnen + Links · oben + Links · unten + Rechts · oben + Rechts · unten + Klick + Rechtes Trackpad + Linkes Trackpad + Trackpad + Tastatur einblenden + Tastatur ausblenden + Linux-Terminal + Die Linux-Sitzung ist noch nicht bereit für ein Terminal. + Steam + Spiele + Desktop + Store + Komponenten + Apps + Einrichtung + Updates + %1$d %% + %1$s fortsetzen + Erscheint in jeder Sitzung als /root/ROMs. Der interne Speicher liegt unter /root/Storage. + Kein Ordner ausgewählt + Gilt ab der nächsten Sitzung. SD-Karten funktionieren. + Ordner auswählen + Ordner ändern + Installiere einen ARM64-Proton-Build und wähle ihn dann pro Spiel unter Steam → Eigenschaften → Kompatibilität aus. + Beende die laufende Sitzung, um ein Kompatibilitätstool zu installieren oder zu entfernen. + Verfügbare Builds + %1$s installiert + Aus einer früheren Anfrage vorgemerkt + Nicht installiert + Update verfügbar → %1$s + Alte %1$s-Versionen entfernen + Entfernt %1$s. Spiele, die diese Versionen nutzen, müssen auf ein anderes Proton umgestellt werden. + Spiele, die diese Version nutzen, müssen auf ein anderes Proton umgestellt werden. + Jetzt installieren + Vormerkung abbrechen + Android-Tastatur + Ausblenden ✕ + %1$s starten + Wähle einen Bildschirm. Zweitbildschirm: %1$s. + Der Zweitbildschirm ist nicht mehr verfügbar. + Hauptbildschirm + Zweitbildschirm + Vorheriger Tab + Nächster Tab + An + Alle %1$d + Keine + %1$d von %2$d + Aus + Unterer Bildschirm + Gilt ab dem nächsten Spielstart. Das Profil eines Spiels hat Vorrang vor dem gemeinsamen. + Anderes Spiel per App-ID… + Fertig + Standard + Aus + Ändern… + Variable ändern + Auf Standard zurücksetzen + Proton-Spiele + Bild im Bild + Automatisch zu Bild im Bild wechseln + Steuerung auf dem aufgeklappten Bildschirm + Aufgeklappt liegt das Spiel über den Steam Deck-Trackpads und den hinteren Griffen. Zugeklappt läuft es im Vollbild. + Angehalten + Bild im Bild ist verfügbar, sobald die Sitzung bereit ist. + Erlaube Bild im Bild für DroidDeck in den Android-Einstellungen. + Android konnte Bild im Bild nicht öffnen. + Verknüpfung + Zum Startbildschirm hinzufügen + Spiel auswählen + Installierte Spiele werden gesucht… + Startlink kopieren + Startlink kopiert + Dieser Launcher kann keine Verknüpfungen hinzufügen. Nutze stattdessen „Startlink kopieren“. + Ungültiger Spiel-Startlink. + Dieses Spiel ist nicht mehr in deiner DroidDeck-Bibliothek. + Beende die aktuelle Sitzung, bevor du ein Steam-Spiel startest. + Das Spiel konnte nicht an Steam gesendet werden. Versuche erneut, es zu starten. + Entfernen wiederholen + Wird entfernt + Entfernen unvollständig + Spieldatei exportieren + Alle Spieldateien mit Ordner synchronisieren + Synchronisierung der Spieldateien beenden + Ordner für Frontend-Spieldateien auswählen + Spieldateien gespeichert unter %1$s + Frontend-Dateien + + Warte, bis der Vorgang an der Laufzeit abgeschlossen ist + Warte, bis die Installation des Kompatibilitätstools abgeschlossen ist + Warte, bis die Installation der Desktop-App abgeschlossen ist + Warte, bis die Installation abgeschlossen ist, bevor du ein Spiel startest + Schließe zuerst die Steam-Sitzung, damit das Spiel den Import nicht überschreiben kann + Import in %1$s + + %1$d Datei aus %2$s in %3$s importiert + %1$d Dateien aus %2$s in %3$s importiert + + + %1$d Datei aus %2$s in %3$s importiert. Alte Spielstände gesichert unter Download/DroidDeck/Saves/backups + %1$d Dateien aus %2$s in %3$s importiert. Alte Spielstände gesichert unter Download/DroidDeck/Saves/backups + + Spielstand-ZIP für %1$s auswählen + Export von %1$s + + %1$d Datei als %2$s exportiert: %3$s + %1$d Dateien als %2$s exportiert: %3$s + + Speicherort für %1$s auswählen (%2$s) + %1$s fehlgeschlagen: %2$s + Noch keine Sitzungsprotokolle: Starte zuerst eine Sitzung. + Keine Sitzungsprotokolle zum Löschen. + + %1$d Ordner mit Sitzungsprotokollen gelöscht. + %1$d Ordner mit Sitzungsprotokollen gelöscht. + + Kopplung für Debugging über WLAN fehlgeschlagen + Befehl für Debugging über WLAN fehlgeschlagen + Debugging über WLAN ist nicht verfügbar + ADB-Befehl kopiert + ROMs-Ordner auswählen + Keine Adreno-GPU + Turnip unterstützt Adreno-GPUs. Auf %1$s zeigt Steam eventuell einen schwarzen Bildschirm. Download: %2$.0f MB. + Trotzdem installieren + Linux-Laufzeit entfernen + Dadurch werden die Laufzeit, der Steam-Client darin und alle dort installierten Spiele gelöscht. + Angewendet: %1$s + %1$s konnte nicht auf dem Hauptbildschirm geöffnet werden + %1$s konnte nicht auf %2$s geöffnet werden + %1$s konnte nicht auf Bildschirm %2$d geöffnet werden + Importieren + Austauschen + Wiederherstellen + Abbrechen + Löschen + Datei kann nicht gelesen werden + %1$s importiert + Der wartende Austausch wurde abgebrochen. + Komponentenpaket auswählen (-linux .wcp) + Die Nightlies waren nicht erreichbar + %1$s gespeichert + Download fehlgeschlagen: %1$s + Laufzeittreiber auswählen (-Linux-ZIP oder Android + Linux-Bundle) + Anzeigetreiber auswählen (AdrenoTools-ZIP oder Android + Linux-Bundle) + Treiber-ZIP oder Android + Linux-Bundle auswählen + %1$s: %2$s + %1$s wird entfernt… + %1$s wird vergessen… + Ordner für den Spielspeicher auswählen + Decky-Plugin-ZIP auswählen + Nicht nutzbar: %1$s + Programm + Linux-Laufzeit wird entfernt + Die Einstellungen ließen sich nicht auf dem unteren Bildschirm öffnen und wurden auf dem Hauptbildschirm geöffnet. + Entwickleroptionen in den Einstellungen öffnen + + Die Android-Einstellung „Unbekannte Apps installieren“ konnte nicht geöffnet werden + Prüfung nicht möglich: %1$s + Dieser Build hat keinen Download für diese DroidDeck-Installation + Beende zuerst die laufende Sitzung und aktualisiere dann + Herunterladen + Installieren + Warte auf deine Bestätigung + Das Update ist fehlgeschlagen + Android hat die Installation des Updates nicht angeboten + + Decky-Installation fehlgeschlagen: %1$s + Plugin-Import wird gestartet… + Plugin-Import fehlgeschlagen: %1$s + Plugin installiert. Starte Steam neu, um es zu laden. + + Noch nicht geprüft - tippe auf Aktualisieren, um nach neuen Treibern zu suchen + Android + Linux: legt den Anzeigetreiber gleich mit fest + Android + Linux: legt den Laufzeittreiber gleich mit fest + das in die Laufzeit integrierte Turnip + System-Vulkan: kein mitgelieferter Build für diese GPU + %1$s (mitgeliefert) + Manuell + %1$s · %2$s + Derzeit ist nur eine Hälfte veröffentlicht + %1$.0f MB für beide + Keine Treiber zum Festlegen: %1$s + Suche nach den neuesten Treibern… + Aktuell (Prüfung nicht möglich: %1$s) + Die empfohlenen Treiber sind derzeit nicht veröffentlicht + %1$s %2$s wird heruntergeladen… + Auf %1$s %2$s aktualisiert. + Auf %1$s %2$s aktualisiert. Der Anzeigetreiber gilt nach einem Neustart von DroidDeck. + Zu %1$s %2$s gewechselt. + Zu %1$s %2$s gewechselt. Der Anzeigetreiber gilt nach einem Neustart von DroidDeck. + %1$s %2$s wird verwendet. + %1$s %2$s wird verwendet. Der Anzeigetreiber gilt nach einem Neustart von DroidDeck. + Keine Treiber-ZIP: weder ein AdrenoTools-Treiber noch ein -Linux-Turnip noch ein Android + Linux-Bundle + Import fehlgeschlagen: %1$s + Importierter Treiber + %1$s wird als Laufzeit- und Anzeigetreiber verwendet. + %1$s wird als Laufzeit- und Anzeigetreiber verwendet. Der Anzeigetreiber gilt nach einem Neustart von DroidDeck. + %1$s gelöscht (beide Treiber) + %1$s gelöscht + Neueste: %1$s · geprüft %2$s + Neueste: %1$s · %2$s nicht erreichbar · geprüft %3$s + + vor %1$d Min. + vor %1$d Min. + + + vor %1$d Std. + vor %1$d Std. + + + vor %1$d Tag + vor %1$d Tagen + + Banners-Turnip, WinNative und DroidDeck werden geprüft… + Prüfung fehlgeschlagen + %1$s installiert - wähle ihn in einer der beiden Listen, um beide Treiber festzulegen + %1$s installiert - wähle ihn im Menü aus + + Warte, bis die Laufzeit entfernt ist + Spiel + Touchpad + direkt + Austausch fehlgeschlagen: %1$s + Profil konnte nicht gespeichert werden: %1$s + Der Desktop ist pausiert + %1$s ist pausiert + Das Programm ist pausiert + Desktop wird gestartet + %1$s wird gestartet + Programm wird gestartet + Keine Protokolle für diese Sitzung. + Das Archiv der Sitzungsprotokolle konnte nicht erstellt werden. + Die Sitzungsprotokolle konnten nicht geteilt werden. + Sitzung wird gestartet… + Linux-Laufzeit wird heruntergeladen + Linux-Laufzeit wird heruntergeladen · %1$d von %2$d MB + Linux-Laufzeit wird geprüft + Linux-Laufzeit wird entpackt + Desktop wird heruntergeladen + Desktop wird heruntergeladen · %1$d von %2$d MB + Desktop wird geprüft + Desktop wird entpackt + Proton Experimental (ARM64) wird heruntergeladen + Proton Experimental (ARM64) wird heruntergeladen · %1$d von %2$d MB + Proton Experimental (ARM64) wird geprüft + Proton Experimental (ARM64) wird entpackt + Der Katalog der Laufzeiten war nicht erreichbar. Prüfe die Verbindung und tippe erneut auf Spielen. + Die Linux-Laufzeit wurde nicht installiert. Prüfe sie unter Einrichtung und tippe erneut auf Spielen. + Der Desktop wurde nicht installiert (%1$s). Prüfe die Verbindung und tippe erneut auf Desktop. + %1$s wurde unerwartet beendet. Teile die Protokolle mit einem Fehlerbericht oder versuche es erneut. + Das Programm wurde unerwartet beendet. Teile die Protokolle mit einem Fehlerbericht oder versuche es erneut. + Der Desktop wurde unerwartet beendet. Teile die Protokolle mit einem Fehlerbericht oder versuche es erneut. + Exit-Status %1$d · %2$s + kein Protokoll + + Das Protokoll zeigt %1$d Fehler „Function not implemented“: Die seccomp-Beschleunigung von proot lässt auf diesem Gerät einen Helfer scheitern. Versuche %2$s → „%3$s“ (oder „%4$s“) und starte neu. + Das Protokoll zeigt %1$d Fehler „Function not implemented“: Die seccomp-Beschleunigung von proot lässt auf diesem Gerät einen Helfer scheitern. Versuche %2$s → „%3$s“ (oder „%4$s“) und starte neu. + + + Plugins + Installiere eine Plugin-ZIP für die nächste Steam-Sitzung. + Wird heruntergeladen… + Aus ZIP installieren + Spiele + Audio & Video + Grafik + Internet + Büro + Entwicklung + Bildung + Wissenschaft + System + Dienstprogramme + Beliebt + Im Trend + Neu + Aktualisiert + wird geladen… + Flatpak-Einrichtung + %1$s: %2$s + Updates konnten nicht auf Flathub geprüft werden + installiert + Ordner + Graphit + Graphitflächen, mit dem Kugelblau des Logos für alles Ausgewählte, Fokussierte oder Gedrückte. + Papier auf Schwarz + Weiß ist die Hauptfarbe, das Blau des Symbols markiert Linien, Ringe und Punkte. + Symbolblau + Schwarzer Grund, weiße Schrift, das Blau für alles Ausgewählte, Fokussierte oder Gedrückte. + Elektrisches Marineblau + Das Blau heller, auf Flächen, die eher zu Marineblau als zu Grau tendieren. + Esc + Einfg + Entf + Pos1 + Ende + Bild↑ + Bild↓ + ⌫ Rücktaste + Tab ⇥ + Feststell + Eingabe ⏎ + ⇧ Umschalt + Strg + Super + Alt + Leertaste + AltGr + Tab + %1$s ✓ + Strg+C + Strg+D + Installiere zuerst die Laufzeit + Steam-Bibliothek ist nicht verfügbar + Vorherige SD-Bibliothek ist nicht verfügbar + Spielspeicher ist nicht verfügbar + Ordner für hinzugefügte Spiele ist nicht verfügbar + Steam-Manifeste sind nicht verfügbar + Viele Systeme + Intern + Linux-System + SD-Karte + Speicher + Auswählen + Eigenschaften + Zu Favoriten hinzufügen + Aus Favoriten entfernen + Umbenennen + Kopieren + Ausschneiden + Aktionen + Favoriten + Aktuellen Ordner anheften + Noch keine Favoriten - hefte einen Ordner über sein ⋮-Menü an, um schnell hierher zurückzukehren. + Aus Favoriten entfernen + 0 Byte + Byte + Speicherort + Typ + Größe + Geändert + Ordner + Datei + %1$s-Datei + Schreibgeschützt + Verhindert, dass Spiele (und Wine) diese Datei überschreiben oder löschen. + „Schreibgeschützt“ konnte nicht geändert werden + Versteckt + Dieser Speicher kann das Versteckt-Attribut nicht speichern. + Markiert die Datei in Windows als versteckt (Versteckt-Attribut von Wine). + „Versteckt“ konnte auf diesem Speicher nicht geändert werden + Fertig + Wird gelöscht... + Löschen fehlgeschlagen + Wird verschoben... + Wird kopiert... + Verschieben - %1$s + Kopieren - %1$s + Verschieben %1$d/%2$d - %3$s + Kopieren %1$d/%2$d - %3$s + %1$d erledigt, %2$d fehlgeschlagen + %1$d erledigt, %2$d übersprungen + + %1$d Element verschoben + %1$d Elemente verschoben + + + %1$d Element kopiert + %1$d Elemente kopiert + + „%1$s“ ist bereits vorhanden + Wird umbenannt... + Umbenennen fehlgeschlagen + Ordner wird erstellt... + Ordner konnte nicht erstellt werden + Neuer Ordner + Ordnername + Erstellen + Neuer Name + Löschen? + „%1$s“ endgültig löschen? + + %1$d Element löschen? + %1$d Elemente löschen? + + Dies kann nicht rückgängig gemacht werden. + + …und %1$d weiteres + …und %1$d weitere + + Löschen %1$d/%2$d - %3$s + + %1$d konnte nicht gelöscht werden + %1$d konnten nicht gelöscht werden + + Zusammenführen fügt Dateien im vorhandenen Ordner hinzu und ersetzt sie. + Überschreiben ersetzt die vorhandene Datei. + Auf alle Konflikte anwenden + Zusammenführen + Überschreiben + Beide behalten + Überspringen + Diesen Ordner auswählen + Zurück + %1$s ist eingebunden, aber derzeit nicht lesbar + Listenansicht + Rasteransicht + Suchen + Sortieren + Name + Änderungsdatum + Großzügige Zeilen + Kompakte Zeilen + Versteckte Dateien ausblenden + Versteckte Dateien anzeigen + Favoriten ausblenden + Favoriten anzeigen + Diesen Ordner filtern + %1$s frei + %1$d ausgewählt + Keine + Alle + Einfügen + + %1$d Element + %1$d Elemente + + %1$s hier einfügen + %1$s hier einfügen (verschieben) + Dateien + SPEICHER + SCHNELLZUGRIFF + FAVORITEN + Downloads + Download + ROMs + Sitzungsprotokolle + Bilder + „%1$s“ zu Favoriten hinzugefügt + „%1$s“ aus Favoriten entfernt + Leeres Verzeichnis + Eine Ebene nach oben + Ordner auswählen + Datei auswählen + Steam-Blau + Himmelblau + Weiß + Violett + Grün + Bernstein + Rot + Pink + A-Taste + B-Taste + X-Taste + Y-Taste + Linke Schultertaste + Rechte Schultertaste + Linker Trigger + Rechter Trigger + Ansichtstaste + Menütaste + Ansicht + Menü + Steuerkreuz oben + Steuerkreuz unten + Steuerkreuz links + Steuerkreuz rechts + Ausgeblendet + Zum Verschieben ziehen + Einrasten + Speichern + Nichts + Tastatur + Trackpad + Terminal + Deck-Griffe + Trackpads + + Linux-Laufzeit wird entfernt + + Linux-Laufzeit wird entfernt · %1$d Eintrag + Linux-Laufzeit wird entfernt · %1$d Einträge + + Linux-Laufzeit entfernt + Die Linux-Laufzeit konnte nicht vollständig entfernt werden. Wiederhole das Entfernen unter Einrichtung. + Reste der Laufzeit werden entfernt + Herunterladen + Überprüfen + Entpacken + Wird angezeigt, während die Linux-Laufzeit installiert oder entfernt wird + Linux-Laufzeit wird installiert + Linux-Laufzeit wird entfernt + + Die Linux-Laufzeit ist nicht installiert + Der Katalog hat keine Prüfsumme für %1$s + Prüfsumme stimmt nicht überein - es wurde nichts geändert + %1$s wird installiert + Das AppImage konnte nicht abgelegt werden + Entpacken fehlgeschlagen + Installation fehlgeschlagen + + Flatpak-Einrichtung fehlgeschlagen (Exit-Code %1$d) + Flatpak-Einrichtung wurde nicht abgeschlossen + Eine andere Store-Aufgabe läuft + Richte zuerst Flatpak ein + Wird gestartet + Flatpak wurde mit Status %1$d beendet + %1$s wird installiert + %1$s wird aktualisiert + %1$s wird entfernt + %1$s wird installiert (%2$d/%3$d) + %1$s wird aktualisiert (%2$d/%3$d) + %1$s wird entfernt (%2$d/%3$d) + Übersetzungen + Grafiktreiber + Mediencodecs + %1$s-Laufzeit + %1$s-Laufzeit %2$s + + Die Datei ist zu klein für ein AppImage + Die Datei kann nicht gelesen werden: %1$s + Dies ist kein AppImage (es ist kein Linux-Programm) + Dieses AppImage ist weder für ARM64 (aarch64) noch für x86-PCs gebaut + Dies ist ein Linux-Programm, aber kein AppImage + Nur AppImages vom Typ 2 können importiert werden (dieses ist Typ %1$d) + Nicht genug freier Speicherplatz: Der Import benötigt etwa %1$s + Nicht genug freier Speicherplatz: Das Update benötigt etwa %1$s + Import fehlgeschlagen + Dieses AppImage ist nicht mehr vorhanden + Update fehlgeschlagen + %1$s wird entpackt + Das AppImage konnte nicht entpackt werden + Das AppImage konnte nicht entpackt werden: %1$s + + Beende die aktive Sitzung, bevor du Decky Loader installierst + Download von Decky Loader fehlgeschlagen + Die Größe von Decky Loader stimmt nicht mit den Release-Metadaten überein + Die Release-Prüfsumme konnte nicht heruntergeladen werden + Release-Prüfsumme fehlt + Release-Prüfsumme ist ungültig + Prüfsumme von Decky Loader stimmt nicht überein + Die Release-Datei ist keine kompatible 64-Bit-Linux-Programmdatei + Während des Downloads wurde eine Sitzung gestartet; beende sie, bevor du Decky Loader installierst + PluginLoader konnte nicht als ausführbar markiert werden + Steam-CEF-Remote-Debugging konnte nicht aktiviert werden + PluginLoader konnte nicht bereitgestellt werden + Der vorhandene PluginLoader konnte nicht gesichert werden + Der neue PluginLoader konnte nicht aktiviert werden + Beende die aktive Sitzung, bevor du ein Plugin installierst + Installiere Decky Loader, bevor du ein Plugin importierst + Die ausgewählte ZIP-Datei konnte nicht gelesen werden + Das Decky-Plugin-Verzeichnis konnte nicht erstellt werden + Plugin-ZIP wird entpackt + Die Plugin-ZIP muss genau einen Plugin-Ordner auf oberster Ebene enthalten + In der Plugin-ZIP fehlt plugin.json, package.json oder dist/index.js + Plugin-Name fehlt + Während der Installation wurde eine Sitzung gestartet; beende sie, bevor du ein Plugin installierst + Plugin installiert · starte Steam neu, um es zu laden + Installation der Plugin-ZIP fehlgeschlagen + Die Plugin-ZIP enthält zu viele Einträge + Die Plugin-ZIP enthält einen ungültigen Pfad + Die Plugin-ZIP enthält doppelte Pfade + Die Plugin-ZIP enthält einen unsicheren Pfad + Plugin-Ordner konnte nicht erstellt werden + Die Plugin-ZIP überschreitet entpackt das Limit von 1 GiB + Mitgelieferte Plugin-Binärdatei konnte nicht als ausführbar markiert werden + Ungültiger remote_binary-Eintrag + Ungültiger Name der Remote-Binärdatei + Die URL der Remote-Binärdatei muss HTTPS verwenden + Remote-Binärdatei %1$s hat einen ungültigen SHA-256-Hash + bin-Verzeichnis des Plugins konnte nicht erstellt werden + Plugin-Binärdatei wird heruntergeladen · %1$s + Plugin-Binärdatei %1$s konnte nicht heruntergeladen werden + Prüfsumme der Plugin-Binärdatei stimmt nicht überein · %1$s + Plugin-Binärdatei konnte nicht als ausführbar markiert werden · %1$s + Plugin-Binärdatei konnte nicht ersetzt werden · %1$s + Plugin-Binärdatei konnte nicht installiert werden · %1$s + Der Plugin-Ordner gehört zu einem anderen Plugin + Das vorhandene Plugin konnte nicht gesichert werden + Das importierte Plugin konnte nicht aktiviert werden + Decky-Plugin-Einstellungen konnten nicht aktualisiert werden + Das vorhandene Plugin konnte nicht wiederhergestellt werden + + Identität für Debugging über WLAN konnte nicht gespeichert werden + ADB-Befehl fehlgeschlagen (%1$d) + Android hat nicht bestätigt, dass das Limit für untergeordnete Prozesse geändert wurde: %1$s + Keine Kopplung für Debugging über WLAN gespeichert + Aktuelle IP-Adresse & Port von Debugging über WLAN eingeben + + Zeitüberschreitung beim Warten auf das Kopplungs-Pop-up. Versuche es erneut über DroidDeck. + Suche nach dem Kopplungs-Pop-up nicht möglich (%1$d). Prüfe, ob WLAN aktiviert ist. + Suche nach dem Kopplungs-Pop-up nicht möglich: %1$s + Gib alle 6 Ziffern des Codes ein + Wird gekoppelt… + Dieser Code hat nicht funktioniert. Öffne das Kopplungs-Pop-up erneut und gib den neuen Code ein. + Gekoppelt. Verbindung wird hergestellt… + Gekoppelt, aber der Port für Debugging über WLAN wurde nicht gefunden. Lass Debugging über WLAN aktiviert und versuche es erneut. + Einstellung wird angewendet… + Befehl für Debugging über WLAN fehlgeschlagen + Einrichtung von Debugging über WLAN + Fragt nach dem Kopplungscode für Debugging über WLAN, während die Einstellungen geöffnet sind + Debugging über WLAN koppeln + Aktiviere Debugging über WLAN und tippe dann auf „Gerät mit Kopplungscode koppeln“. + Aktiviere Debugging über WLAN und tippe dann auf „Gerät mit Kopplungscode koppeln“. Bleib in den Einstellungen: Der Code wird in dieser Benachrichtigung eingegeben. + 6-stelliger Kopplungscode + Tippe auf „Code eingeben“ und gib die 6 Ziffern aus den Einstellungen ein. + WLAN-Kopplungscode eingeben + Code noch einmal eingeben + Code eingeben + Steam wird eingerichtet + Steam ist bereit + Tippe, um zu DroidDeck zurückzukehren. Du kannst Debugging über WLAN jetzt deaktivieren. + Einrichtung von Debugging über WLAN abgebrochen + + Der Download antwortete mit HTTP %1$d + Der Download hatte nicht die Größe der veröffentlichten APK und wurde verworfen - versuche es erneut + Der Download stimmte nicht mit seiner Prüfsumme überein und wurde verworfen - versuche es erneut + Das Update ist für das Android-Paket %1$s, nicht für %2$s + Die Update-Metadaten nennen nicht den Release-Signaturschlüssel von DroidDeck + Android konnte die heruntergeladene APK nicht lesen + Die heruntergeladene APK ist für %1$s, nicht für %2$s + Die heruntergeladene APK hat versionCode %1$d statt des veröffentlichten %2$d + Android installiert versionCode %1$d nicht über den installierten %2$d + Die heruntergeladene APK hat keine Signaturinformationen + Die heruntergeladene APK ist nicht mit dem Release-Schlüssel von DroidDeck signiert + Der Update-Katalog antwortete mit HTTP %1$d + + Sitzung wird gestartet… + %1$d:%2$02d vergangen · läuft noch + Update des Steam-Clients wird heruntergeladen · %1$d %% + Steam-Client wird heruntergeladen · %1$s + + Es läuft bereits die Installation eines Kompatibilitätstools + Beende die aktive Sitzung, bevor du Kompatibilitätstools installierst + Kein ARM64-Release für %1$s gefunden + Zum Installieren von %1$s werden mindestens 4 GB freier Speicherplatz plus die Downloadgröße benötigt + %1$s %2$s wird heruntergeladen + Download wird wiederholt · Versuch %1$d von %2$d + Download unterbrochen + Download fehlgeschlagen; die Teildatei bleibt erhalten, damit ein neuer Versuch fortsetzen kann + Der Download war größer als das Release und wurde gelöscht + Der Download war unvollständig; die Teildatei bleibt für einen fortsetzbaren neuen Versuch erhalten + Download wird überprüft + Die Release-Prüfsumme konnte nicht gelesen werden; versuche es erneut + %1$s %2$s veröffentlicht keine Prüfsumme; es wurde nichts installiert + Prüfsumme stimmt nicht überein; der Download wurde gelöscht + Während des Downloads wurde eine Sitzung gestartet; beende sie, bevor du Kompatibilitätstools installierst + %1$s wird installiert + Installation von %1$s fehlgeschlagen (Exit-Code %2$d) + %1$s konnte nicht installiert werden + droiddeck-esync-Paket wird abgerufen + + mit Linux-Helfern + nur DLLs + Original (Proton seit dem letzten Austausch aktualisiert) + Außerhalb von Komponenten geändert + nicht vorhanden + Original %1$s + Original aus %1$s + %1$s %2$s kommt in %3$s, sobald das darauf laufende Spiel beendet wird. + %1$s %2$s ist jetzt in %3$s. Es gilt ab dem nächsten Spielstart. + Dieses Original-Bundle ist nicht gespeichert + %1$s von %2$s wird auf das Original zurückgesetzt, sobald das darauf laufende Spiel beendet wird. + %1$s von %2$s aus dem Original von %3$s wiederhergestellt. + %1$s wird in %2$s verwendet. Wechsle in diesem Proton zuerst zu etwas anderem. + %1$s ist nicht gespeichert + %1$s gelöscht. + Das Original des installierten Builds bleibt erhalten: Es ist der Weg zurück. + Nicht gespeichert + Das %1$s-Original von %2$s wurde gelöscht. + Downloads kommen nur aus den Nightlies-Releases + Diese Paketliste stammt aus der Zeit vor Prüfsummen - aktualisiere sie und versuche es erneut + Prüfsumme stimmt nicht überein + + Linear + Nächster Nachbar + Snapdragon GSR (Qualität) + Nur schärfen + Auto (an Skalierung anpassen) + + Nichts angewendet. + Spielklarheit + Schärfung mit etwas Kontrast - für jeden Tag. + Lebendig + Farben kräftig verstärkt. Debanding an, damit der Himmel keine Streifen zeigt. + Kino + Tieferer Kontrast, kühlere Farben, geglättete Kanten. + Wettkampf + Aufgehellte Schatten und starke Schärfung - Gegner zuerst entdecken. + Adaptive Schärfe + Nur Schärfung, Farben unverändert. + Filmisch + Filmähnliche Kurve, leicht gedämpft. + Arcade + Laut und knackig. + Retro-Röhre + Röhrenbildschirm und analoges Signal zusammen. + Scharf hochskaliert + Für Auflösungen unterhalb der Panel-Auflösung. + Saubere Pixel + Scharfe Pixel für 2D und alte Titel. + Anime-Kanten + Cartoon-Konturen plus Schärfung. + + Keine Adreno-GPU + Unterstützt + Experimentell: Die Treiber sind Test-Builds + Unter der getesteten Hardware (Adreno 725 und neuer): läuft eventuell nicht + Nicht unterstützt: DroidDeck braucht eine Adreno-GPU (Snapdragon) + + Das Rate-Limit von GitHub wurde erreicht - versuche es später erneut + GitHub antwortete mit HTTP %1$d + Download antwortete mit HTTP %1$d + Diese Treiberliste stammt aus der Zeit vor Prüfsummen - aktualisiere sie und versuche es erneut + Prüfsumme stimmt nicht überein - der Download wurde verworfen + Kein AdrenoTools-Treiber: %1$s + keine meta.json + meta.json ist nicht lesbar (%1$s) + dies ist ein Laufzeittreiber für Linux (importiere ihn unter „Linux-Laufzeittreiber“) + dies ist ein Wayland-Spieltreiber, mit dem diese App nichts anfangen kann + meta.json nennt keinen libraryName, daher konnte nichts an AdrenoTools übergeben werden + meta.json nennt %1$s, das nicht in der ZIP enthalten ist + %1$s ist keine gemeinsam genutzte 64-Bit-AArch64-ELF-Bibliothek + %1$s linkt glibc - ein „-Linux“-Turnip, der unter „Linux-Laufzeittreiber“ gehört + Keine libvulkan_freedreno*.so in dieser ZIP. Eine Android- (AdrenoTools) oder -Wayland-Turnip-ZIP ist kein Linux-Laufzeittreiber. + %1$s ist keine gemeinsam genutzte 64-Bit-AArch64-ELF-Bibliothek. + %1$s ist kein glibc-Treiber - er linkt die libc von Android. Die Linux-Laufzeit braucht eine „-Linux“-ZIP; ein einfacher oder „-Wayland“-Turnip kann vom Steam-Client gar nicht geladen werden. + Kein Treiber-Bundle: keine manifest.json, die einen Android- und einen Linux-Treiber nennt + Kein Treiber-Bundle: %1$s zeigt nach außerhalb + Kein Treiber-Bundle: entpackt mehr als %1$d MB + + SD-Karte + Ordner + + Sitzungsprotokolle teilen + + erfordert Android 10 oder neuer (Anzeigeebenen mit Farbraum) + dieses Display meldet keine HDR-Unterstützung + dieses Display unterstützt %1$s, aber nicht HDR10 + + diese GPU + Limit für untergeordnete Prozesse ist aktiv + Limit für untergeordnete Prozesse ist nicht festgelegt + Limit für untergeordnete Prozesse konnte nicht geprüft werden + Limit für untergeordnete Prozesse ist aus + Limit für untergeordnete Prozesse nicht erforderlich + %1$s Falls das nicht möglich ist, verbinde dieses Gerät per ADB mit einem Computer und führe den Befehl unten aus. Kehre dann hierher zurück und prüfe erneut. + Steam-Sitzungen können starten. + Diese Android-Version nutzt dieses Limit nicht. + Aktiviere „Einschränkungen für untergeordnete Prozesse deaktivieren“ in den Entwickleroptionen und lass die Entwickleroptionen danach aktiviert. + Diese Android-Version hat dafür keinen Schalter. Nutze Debugging über WLAN, um es auf diesem Gerät zu ändern. + Dieser Build hat keinen Download für diese DroidDeck-Installation. + Android installiert ihn nicht über diesen Build, weil sein versionCode %1$d unter dem installierten %2$d liegt. + Android installiert keine ältere Version über eine neuere. Bleib bei diesem Build, bis der Kanal aufholt. + Diese DroidDeck-Installation ist anders signiert (ein lokaler Build?), daher kann Android sie nicht direkt aktualisieren. + Für das Update ist nicht genug freier Speicherplatz vorhanden. + Das Update wurde abgebrochen. + Das Update wurde nicht installiert: %1$s + Das Update wurde nicht installiert. + %1$s-ZIP + ZIP + %1$s wurde noch nicht gestartet + Keine Spielstände für %1$s gefunden + %1$s hat keinen drive_c-Ordner und ist daher keine GameHub- oder Winlator-Spielstand-ZIP + in %1$s können keine Ordner erstellt werden + in %1$s kann nicht geschrieben werden + in %1$s kann nicht geschrieben werden (%2$s) + Ordner ist nicht verfügbar + Eine andere Datei verwendet bereits %1$s + Grafikordner ist nicht verfügbar + Der Ordner enthält ein fremdes Sync-Inventar + %1$s konnte nicht entfernt werden + Datei liegt außerhalb des Exportordners + FEX (Build ohne Versionsnamen) + keine profile.json in %1$s + %1$s ist kein Linux-Komponentenpaket (Typ %2$s) + %1$s · %2$s frei + Automatisch + Der Update-Katalog hat seine Prüfungen nicht bestanden (%1$s) + DroidDeck-Einstellung für untergeordnete Prozesse + Angemeldet · Offlinemodus + Angemeldet + MITGELIEFERT + HERUNTERGELADEN + IMPORTIERT + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Sitzungsanzeige + Auflösung + 16:9-Spielanpassung + Wie 16:9-Spiele auf dieses Display passen. Ab der nächsten Sitzung. + Wähle „An Bildschirm anpassen“ oder eine eigene Größe schmaler als 16:9. + Seitenverhältnis beibehalten + Auf Display strecken + Bildskalierung + Skalierungsfilter + Gemeinsam für Steam und Desktop. + Wirkt sofort. Gemeinsam für Steam und Desktop. + FSR, GSR und NIS wirken beim Vergrößern des Sitzungsbilds. „Nur schärfen“ funktioniert auch ohne Vergrößerung. + Filterschärfe + Bildrate + %1$s Stellt die Skalierung auf %2$s. + An Bildschirm anpassen (%1$d×%2$d) + Bildschirm folgen (passt sich beim Auf- und Zuklappen an) + Steam + Gerätedetails anzeigen + Gerätedetails ausblenden + Gerät wird ausgelesen… + Kopieren + Kopiere diese Details in einen Fehlerbericht. + Kopiert. + SSBS erzwingen + Hält eine CPU-Geschwindigkeitseinstellung aktiv, die Wine immer wieder abschaltet. Kann Windows-Spiele auf dem Snapdragon 8 Elite beschleunigen; auf älteren Chips kaum Unterschied. Gilt ab dem nächsten Spielstart. + + Stores + Steam + Eigene + Stores + Store öffnen auf + Download-Tempo + Store-Engine + ● Engine bereit + Nicht gebaut + Ausgewogen + Schnell + Max. + Stores + Store-Einstellungen + Einstellungen + (nicht angemeldet) + Store + Installiert + Bibliothek + Alle + Downloads + %1$s (%2$d) + Anmelden + %1$s · abmelden + Von %1$s abmelden? + Installierte Spiele bleiben erhalten. Die Bibliothek wird bei der nächsten Anmeldung erneut abgerufen. + Angemeldet bleiben + Abmelden + Katalog von %1$s durchsuchen… + Installierte Spiele durchsuchen… + Deine Bibliothek durchsuchen… + Suchen + Nichts installiert + Wird geladen + Keine Spiele + Store wird geladen… + Kein Katalog + Neuheiten + Angebote + Kostenlose Spiele + Im Trend + Deine Bibliothek + Spielen + Installieren + Installation fortsetzen + Unterbrochen + Installieren · %1$s + Kostenlos erhalten + Auf %1$s ansehen + Kaufen · %1$s + Installiert + In der Bibliothek + Kostenlos + Fortsetzen + Aktualisieren + Inhalte für Erwachsene anzeigen + Epic + EOS + Offline + Aus + Epic-Anmeldung (EOS) + Offline starten + Epic-Anmeldung klären + Cloud-Spielstände + In die Cloud hochladen + Aus der Cloud herunterladen + Keine Cloud-Spielstände + Cloud-Konflikt + Cloud-Konflikt · %1$d Dateien + … + Cloud behalten + Lokal behalten + Cloud wird geprüft… + Zuletzt synchronisiert %1$s + Noch nicht synchronisiert + %1$d Dateien + installiert + in deiner Bibliothek + Store + Erfolge + %1$d von %2$d freigeschaltet + %1$d / %2$d + Freigeschaltet · %1$d + Gesperrt · %1$d + Verborgen · %1$d + Verborgener Erfolg + Details werden angezeigt, sobald er freigeschaltet ist. + Alle Erfolge freigeschaltet + Wird geladen… + Melde dich bei %1$s an, um deine Erfolge zu sehen. + %1$s konnte nicht erreicht werden. Schau später noch einmal vorbei, wenn du online bist. + %1$s listet keine Erfolge für dieses Spiel. + Alle %1$d anzeigen + Erfolge · %1$s + ✓ In Steam + Deinstallieren + Wird deinstalliert… + %1$s deinstallieren? + Gibt %1$s frei und entfernt das Spiel aus Steam. + Entfernt seine Dateien und seine Steam-Verknüpfung. + Steam übernimmt das Spiel beim nächsten Start; starte es von dort oder starte Steam jetzt. + Proton + Von Steam pro Spiel gewählt + Audio + DirectAudio + Komponenten + FEX, DXVK, VKD3D + Engine + %1$s · nativ + %1$s · integriert + So läuft es + Abgerufen von + %1$s, mit deinem Konto + Installiert in + Spielspeicher (Games/%1$s/%2$s) + Gestartet über + Steam und Proton, derselbe Weg wie bei Steam-Spielen + Steam und Proton · Epic-Anmeldung bei jedem Start, EOS-Overlay aus + Steam und Proton · Variablen für Amazon Games Services gesetzt + Steam + bei der Installation zu Steam hinzugefügt (Deck-UI + Steam Input) + Keine Downloads + Pausiert + In Warteschlange + In Warteschlange · #%1$d + Abgebrochen + Fehlgeschlagen + Fehlgeschlagen · %1$s + %1$s · %2$d %% + %1$s / %2$s · %3$d %% + Pausieren + Fortsetzen + Abbrechen + Wiederholen + Entfernen + Abgeschlossene entfernen + Manifest + Download + Prüfen + Installation + Fertig + Wird geprüft + Wird heruntergeladen + Download löschen? + Fehlgeschlagen · %1$s behalten + Wird überprüft + Wird installiert + %1$s %2$d %% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d Dateien + Download-Manager + Gleichzeitige Downloads + Installieren in + %1$s installieren: + Intern + %1$s frei + Store-Downloads + Zeigt den Fortschritt von Spiel-Downloads und lässt Downloads im Hintergrund weiterlaufen + Wird heruntergeladen + %1$d Downloads + Wird vorbereitet… + Abgeschlossene Store-Downloads + %1$s installiert + Download von %1$s fehlgeschlagen + Löschen + Behalten + Bei %1$s anmelden + Mit deinem Anbieter anmelden + Neu laden + Schließen + Erneut versuchen + Anmeldung wird abgeschlossen… + Die Anmeldeseite konnte nicht geladen werden: %1$s + Dieser Anbieter lässt keine Anmeldung über eine eingebettete Seite zu. Melde dich stattdessen mit deinem Store-Passwort an. + Die Anmeldeseite antwortete mit einem Fehler (HTTP %1$d). + Die Verbindung zur Anmeldeseite ist nicht sicher. + Die Anmeldung konnte nicht bestätigt werden. Versuche es erneut. + Anmeldung fehlgeschlagen. Versuche es erneut. + Schließen + Zurück + Bearbeiten + Alle Spiele in diesem Ordner hinzufügen + Nichts Neues in diesem Ordner + Name + Ziel + Ausführen in + Startoptionen + Grafiken + Andere EXE… + Entfernen + Cover + Hintergrund + Logo + Symbol + Spielordner + Steam + SteamGridDB + Deine Datei + Aus der EXE + Automatisch + Bild auswählen… + Bild auswählen: %1$s + Auf automatisch zurücksetzen + + %1$d Spiel hinzugefügt + %1$d Spiele hinzugefügt + + + %1$d bereits unter „Spiele“ + %1$d bereits unter „Spiele“ + + B · Zurück: %1$s + Spiel + Spieleinstellungen + Grafiktreiber + Dieses Proton + Jedes Proton + Für jedes Proton + Verfügbar · %1$d + Geprüft %1$s + Nach neuen Komponenten und Treibern suchen + Holen + %1$s · DXVK, VKD3D-Proton und FEX unten gehören zu diesem Proton + %1$s · getestet + %1$s · ungetestet + %1$s · nicht unterstützt + Hält das empfohlene Paar aktuell + Auto · %1$s + Laufzeit- und Anzeigetreiber getrennt + %1$s verwenden? + %1$s entfernen? + Diagnose + Bereit zum Spielen + Noch nicht bereit zum Spielen + Laufzeit %1$s + Prozesslimit aus + angemeldet als %1$s + Wenn etwas deine Aufmerksamkeit braucht + Aussehen + Dieses Gerät + Bibliotheksgrafiken + LINUX-APPS + Flathub + Desktop-Apps für den Linux-Desktop, geöffnet über Desktop. + Flathub einrichten + %1$s entfernen? + Einstellungen und Dateien bleiben erhalten. + Behalten + Speicherbedarf + Desktop deinstallieren? + Alle + GOG-Erfolge + Synchronisiert Erfolge für unterstützte GOG-Spiele. Gilt ab dem nächsten Start. + Erfolge aus + Erfolge aktiviert + Melde dich erneut bei GOG an + Erfolge hochgeladen + Hochladen des Erfolgs ausstehend + Aktiviere GOG-Erfolge in den Store-Einstellungen + Erfolge nicht verfügbar; Sitzungsprotokolle prüfen + Erfolg freigeschaltet + Einfügen + Richtungstasten + Linker Stick + Rechter Stick + Menü + Tastatur + Richtungstasten hoch + Richtungstasten runter + Richtungstasten links + Richtungstasten rechts + Touch-Steuerung + Touch-Controller + Maus + An + Aus + Langsam + Normal + Schnell + Layout bearbeiten + DroidDeck-Menü + Ziehe Steuerelemente auf den Bildschirm + Anordnen + Verwerfen + Wird gespeichert… + Symbol + B− + B+ + H− + H+ + Ausblenden + Farbe und Deckkraft… + Dieses Layout kopieren + Layout kopiert; füge es in das Touch-Layout eines beliebigen Spiels ein + Kopiertes Layout einfügen + Auf Steams Standard zurücksetzen + Dieses Steuerelement hat keine Zuordnung in Steam, um ihm ein Symbol zu geben + Symbol für %1$s + Touch-Layout-Farbe + Fertig + Das Touch-Layout konnte nicht gespeichert werden + Touch-Layout in Steam gespeichert + App %1$d + Bezeichnung (optional) + Steams Symbole wurden nicht gefunden + + %1$d Symbol von Steam + %1$d Symbole von Steam + + Symbolfarbe + Schaltflächenfarbe + Kein Symbol + + Anmeldung wird geprüft… + Bibliothek wird abgerufen… + Spieleliste wird abgerufen… + Spieledetails werden geladen… (%1$d/%2$d) + + %1$d Spiel wird synchronisiert… + %1$d Spiele werden synchronisiert… + + Wird synchronisiert… %1$d/%2$d + Cover werden abgerufen… + Synchronisierung fehlgeschlagen + Keine Spiele in deiner %1$s-Bibliothek gefunden + Amazon Games hat für deine Bibliothek nicht geantwortet: aktualisiere die Seite oder melde dich erneut an + GOG konnte nicht erreicht werden + Deine GOG-Bibliothek konnte nicht gelesen werden + Wird bei Steam registriert… + Cardboard VR + Cardboard-Modus + Bilder für linkes und rechtes Auge für eine Smartphone-VR-Brille. + Bild + Virtueller Bildschirm + Side-by-Side-3D + Brille + Cardboard + Cardboard (2014) + Ohne Linse + Augenabstand + Bildschirmbreite + Bildschirmabstand + Kopf-Tracking + Dieses Gerät hat keinen Drehsensor. + Ansicht zentrieren + Zentrieren + SteamVR (experimentell) + Installieren + Lädt Valves SteamVR für ARM64 herunter (571 MB). + SteamVR wird heruntergeladen und entpackt … + Starten + Startet SteamVR mit deinem Kopf als Headset; das Bild wechselt zu Side-by-Side-3D. + Beenden + SteamVR läuft. + In dieser Sitzung nicht verfügbar. + SteamVR hat nicht reagiert; Sitzungsprotokoll prüfen. + Komponenten herunterladen + Downloads werden mit der Seite „Komponenten“ geteilt. Wähle oben eine heruntergeladene Version, um sie beim nächsten Spielstart zu verwenden. + Version zum Herunterladen wählen + Entwickler + Publisher + Erschienen + Hauptspiel + + Extras + Komplettierer + %1$s Std. + %1$d Min. + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml new file mode 100644 index 000000000..efc1c59bd --- /dev/null +++ b/app/src/main/res/values-el/strings.xml @@ -0,0 +1,2235 @@ + + + DroidDeck + + Η πρώτη εκτέλεση κάνει λήψη του Steam. Αυτό μπορεί να διαρκέσει μερικά λεπτά. + Από προεπιλογή, η έξοδος από την εφαρμογή θέτει το Steam σε παύση· συνεχίζει από εκεί που είχε μείνει όταν επιστρέψετε. + Το χειριστήριό σας εμφανίζεται ως χειριστήριο Xbox 360. Διαφορετικά, χρησιμοποιήστε τα χειριστήρια στην οθόνη. + + + Η πρώτη εκτέλεση ρυθμίζει την επιφάνεια εργασίας. Αυτό μπορεί να διαρκέσει μερικά λεπτά. + Από προεπιλογή, η έξοδος από την εφαρμογή θέτει τη συνεδρία σε παύση· συνεχίζει από εκεί που είχε μείνει όταν επιστρέψετε. + Το άγγιγμα λειτουργεί ως πινακίδα αφής: σύρετε για να μετακινήσετε τον δείκτη, πατήστε για κλικ. + Το χειριστήριό σας εμφανίζεται ως χειριστήριο Xbox 360. Διαφορετικά, χρησιμοποιήστε τα χειριστήρια στην οθόνη. + + Συνεδρία + Εμφανίζεται όσο μια συνεδρία Steam ή επιφάνειας εργασίας είναι ανοιχτή + Το Steam εκτελείται + Λήψη Steam σε εξέλιξη + Η συνεδρία παύθηκε - πατήστε Συνέχιση όταν είστε έτοιμοι + Συνέχιση + Αναστολή τώρα + Διακοπή συνεδρίας + Δημιουργία καρέ + Επιλέξτε τη λειτουργία δημιουργίας καρέ + Ανενεργό + Win-FG %1$d× + LSFG %1$d× + LSFG προσαρμοστικό %1$d + Απαιτεί το Lossless Scaling στο Steam + Εγκαταστήστε το Lossless Scaling στο Steam ή εισαγάγετε το Lossless.dll + Εισαγωγή Lossless.dll… + Χρήση του αντιγράφου από το Steam + Χρήση ενός εισαγμένου αντιγράφου + Επιλογή Lossless.dll + Το Lossless.dll εισήχθη + Αυτό το Lossless.dll χρησιμοποιείται ήδη + Αυτός ο λογαριασμός Steam δεν κατέχει το Lossless Scaling + Αυτό το αρχείο δεν είναι το Lossless.dll του Lossless Scaling + Δεν ήταν δυνατή η προετοιμασία του LSFG από αυτό το Lossless.dll + Το LSFG απαιτεί το Lossless Scaling από το Steam ή ένα εισαγμένο Lossless.dll + Το LSFG δεν μπόρεσε να προετοιμάσει τα shaders του από το Lossless.dll + %1$s: μη υποστηριζόμενο + %1$s: αποτυχία μηχανής + %1$s: απαιτεί το Lossless Scaling + %1$s %2$s · ξεκινά + Προσθέτει μια βιβλιοθήκη Steam και τη σαρώνει για παιχνίδια Windows. Τα παιχνίδια που κατέχετε χρησιμοποιούν τις καταχωρίσεις τους στο Steam· τα υπόλοιπα γίνονται συντομεύσεις εκτός Steam. + Περιβάλλον παιχνιδιού + Μεταβλητές περιβάλλοντος για παιχνίδια Proton. + Επεξεργασία + Όλα τα παιχνίδια Proton + Προφίλ + ID εφαρμογής Steam + Άνοιγμα + Παιχνίδι %1$s + Προσθήκη μεταβλητής + Όνομα μεταβλητής + Τιμή + Αποθήκευση + Αφαίρεση + Κληρονόμηση ξανά + Εκκαθάριση παρακάμψεων περιβάλλοντος + Κατάργηση ορισμού για αυτό το προφίλ + Δεν έχουν ορισθεί μεταβλητές. + Κληρονομημένη + Προσαρμοσμένη μεταβλητή + Δεν ήταν δυνατή η φόρτωση ή η αποθήκευση. Ελέγξτε τον ελεύθερο χώρο αποθήκευσης και δοκιμάστε ξανά. + Χρησιμοποιήστε ένα έγκυρο όνομα και μια τιμή έως 8192 χαρακτήρων. + Το τυπικό runtime του Proton δεν διαβάζει αυτή τη μεταβλητή. + Φόρτωση… + Παράκαμψη επιπέδου λειτουργιών D3D12. Το 12_2 αναφέρει δυνατότητες DirectX 12 Ultimate· μη υποστηριζόμενες λειτουργίες μπορεί να αποτύχουν. Αφήστε το χωρίς ορισμό για αυτόματη ανίχνευση. + Παράκαμψη μοντέλου shader D3D12. Το 6_9 απαιτεί αρκετά πρόσφατο VKD3D και αντίστοιχες δυνατότητες GPU. Αφήστε το χωρίς ορισμό για αυτόματη ανίχνευση. + Διατηρήστε την προσωρινή αποθήκευση shader της Mesa ενεργοποιημένη για να μειώσετε την επανειλημμένη μεταγλώττιση. Η τιμή false ενεργοποιεί την προσωρινή αποθήκευση. + Μέγιστο μέγεθος προσωρινής μνήμης shader της Mesa, όπως 1G. Μεγαλύτερες προσωρινές μνήμες χρησιμοποιούν περισσότερο χώρο αποθήκευσης. + Επιλογές VKD3D για D3D12. Το nodxr απενεργοποιεί το ray tracing· χρησιμοποιήστε το μόνο για παιχνίδι που το χρειάζεται. + Ορίστε σε 1 για διαγνωστικό αρχείο καταγραφής του Proton. Η καταγραφή μπορεί να μειώσει την απόδοση και να χρησιμοποιήσει χώρο αποθήκευσης. + Ορίστε σε 1 για χρήση του WineD3D με βάση OpenGL αντί του DXVK, για δοκιμές συμβατότητας D3D9–11. + Ορίστε σε 1 για να κρατήσετε αυτό το παιχνίδι εκτός ntsync (droiddeck-ntsync ή οδηγός πυρήνα)· χρησιμοποιεί το droiddeck-esync όπου υπάρχει πακέτο, διαφορετικά το droiddeck-fsync. + Ορίστε σε 1 για να απενεργοποιήσετε το droiddeck-esync για αυτό το παιχνίδι· χρησιμοποιεί αντ’ αυτού το droiddeck-fsync, ή το droiddeck-ntsync όταν είναι απενεργοποιημένο και το fsync. + Ορίστε σε 1 για να κρατήσετε αυτό το παιχνίδι εκτός droiddeck-fsync· χρησιμοποιεί το droiddeck-esync όπου υπάρχει πακέτο, διαφορετικά το droiddeck-ntsync. + Ορίστε σε 1 για εκτέλεση αυτού του παιχνιδιού με droiddeck-fsync, ή σε 0 για να προτάξετε το droiddeck-esync όπου ένα πακέτο ταιριάζει με το Proton του. + Ορίστε σε 0 για να απενεργοποιήσετε τον βοηθό πλοήγησης με χειριστήριο του Proton για αυτό το παιχνίδι. + Επικάλυψη DXVK για D3D9–11. Το fps εμφανίζει τον ρυθμό καρέ· χωρίς ορισμό το αποκρύπτει. + Ρυθμίσεις DXVK χωρισμένες με ερωτηματικά. Οι πρόσφατες εκδόσεις χρησιμοποιούν το dxvk.maxFrameRate για τον περιορισμό καρέ· προτιμήστε τον περιοριστή καρέ της συνεδρίας για συνήθη χρήση. + Όριο ρυθμού καρέ D3D12. Το 0 απενεργοποιεί το όριο. + Πολυνημάτωση OpenGL της Mesa. Το true μπορεί να βοηθήσει παιχνίδια OpenGL περιορισμένα από CPU· δοκιμάστε το ανά παιχνίδι. + Συγχρονισμός Wine + Πώς τα παιχνίδια κρατούν τα νήματά τους συγχρονισμένα. Εφαρμόζεται στην επόμενη συνεδρία. + esync + ntsync + fsync + wineserver + Προεπιλογή FEX + Εφαρμόζεται στην επόμενη εκκίνηση παιχνιδιού. + Παιχνίδια + Προεπιλογές FEX + Χρησιμοποιεί τις προεπιλογές του FEX. + Σταθερότητα + Οι πιο ασφαλείς, πιο αργές ρυθμίσεις. + Συμβατότητα + Πλήρες TSO με multiblock. + Ενδιάμεσο + Ισορροπημένο TSO με μειωμένη ακρίβεια x87. + Απόδοση + Όλο το TSO απενεργοποιημένο· multiblock. + Απόδοση + TSO + Ρυθμίσεις απόδοσης με το κύριο TSO ενεργοποιημένο. + Ακραίο + Απενεργοποιεί τους ελέγχους αυτο-τροποποιούμενου κώδικα. Παιχνίδια με JIT και DRM μπορεί να πάθουν βλάβη. + Ακραίο + TSO + Ρυθμίσεις Ακραίο με το κύριο TSO ενεργοποιημένο. + Extreme-gn + Απόδοση με υποδείξεις TSC και μεταδεδομένων. Δοκιμάστε το πριν το Ακραίο. + Denuvo + Απόδοση με πλήρεις ελέγχους κώδικα και το bit υπερεπόπτη κρυμμένο. + Προσαρμοσμένη τιμή + Επιλογή τιμής + Προσομοιωτές & εφαρμογές + Προσθήκη + Η δική σας εφαρμογή ή παιχνίδι + Σενάριο + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Εκκίνηση… + Το %1$s προστέθηκε + Το %1$s αφαιρέθηκε + Επιφάνεια εργασίας + Αυτή η εφαρμογή δεν υπάρχει πια. Επιλέξτε την ξανά από την Επιφάνεια εργασίας. + Άνοιγμα %1$s + Αφαίρεση + Πατήστε ξανά για αφαίρεση + Διακόψτε τη συνεδρία για αφαίρεση + Εκτελεί το %1$s από τον φάκελό του, συνδεδεμένο στη συνεδρία όπου βρίσκεται. Η αφαίρεση της εφαρμογής αφήνει αυτόν τον φάκελο ανέγγιχτο. + Εκτελεί το %1$s από ένα αντίγραφο του φακέλου του μέσα στο DroidDeck. Η αφαίρεση της εφαρμογής διαγράφει το αντίγραφο, όχι το πρωτότυπο. + Εγκαταστήστε πρώτα το runtime Linux + Το αρχείο δεν υπάρχει πια + Δεν υπάρχει αρκετός ελεύθερος χώρος: η αντιγραφή του φακέλου απαιτεί %1$s + Αντιγραφή %1$s + Η προσθήκη της εφαρμογής απέτυχε + Αναζήτηση έκδοσης του %1$s + Δεν ήταν δυνατή η ανάγνωση των εκδόσεων του %1$s + Καμία έκδοση του %1$s δεν διαθέτει AppImage για ARM64 ή x86_64 + Λήψη %1$s + Η λήψη απέτυχε + Η ενημέρωση της εφαρμογής απέτυχε + Δεν ήταν δυνατή η ανάγνωση αυτού του εικονιδίου + Το %1$s αποθηκεύτηκε + Επεξεργασία + Έλεγχος για ενημερώσεις + Έλεγχος για ενημερώσεις… + Ενημέρωση σε %1$s + Ενημερωμένο · %1$s + Το %1$s ενημερώθηκε + %1$s · %2$s + Προσθήκη εφαρμογής + Σενάριο + AppImage + GitHub + Flatpak + Ένα αρχείο .sh. Εκτελείται από τον δικό του φάκελο, σε πλήρη οθόνη μέσω gamescope. + Ένα AppImage για ARM64 (aarch64), ή ένα για x86_64 που εκτελείται μέσω FEX. Αποσυμπιέζεται μέσα στο runtime Linux μία φορά. + Εγκαθιστά το AppImage για ARM64 από την πιο πρόσφατη έκδοση του αποθετηρίου, ή το x86_64 μέσω FEX. + Χρήση FEX + Αυτόματο + Ενεργό + Ανενεργό + Το FEX εκτελεί προγράμματα x86 Linux σε αυτή τη συσκευή ARM64. Το Αυτόματο το χρησιμοποιεί όταν το πρόγραμμα είναι χτισμένο για x86. + %1$s μέσω FEX + Αυτό το πρόγραμμα είναι χτισμένο για υπολογιστές %1$s και εκτελείται μέσω FEX, το οποίο δεν έχει ρυθμιστεί ακόμα. Ανοίξτε το Steam μία φορά ενώ είστε συνδεδεμένοι: το DroidDeck κάνει λήψη του FEX και του Steam Linux Runtime που χρειάζεται, και μετά αυτό ανοίγει. + Αυτό το πρόγραμμα + Το %1$s είναι χτισμένο για υπολογιστές x86 και εκτελείται μέσω FEX, το οποίο δεν έχει εγκατασταθεί ακόμα. Εκκινήστε το Steam μία φορά ενώ είστε συνδεδεμένοι και αφήστε το να κάνει λήψη του FEX και του Steam Linux Runtime, μετά εκκινήστε αυτό ξανά. + Ένα ID εφαρμογής Flathub ή η σελίδα της στο Flathub. Το Flatpak ρυθμίζεται πρώτα, αν δεν έχει ρυθμιστεί ακόμα. + Αρχείο + Δεν επιλέχθηκε αρχείο + Επιλογή αρχείου + Αλλαγή + Επιλογή σεναρίου + Επιλογή AppImage + Επιλογή εικονιδίου + Αποθετήριο + owner/repo ή σύνδεσμος github.com + Δεν είναι αποθετήριο GitHub + ID εφαρμογής + org.ppsspp.PPSSPP + Δεν είναι ID εφαρμογής Flathub + Ή ένα αρχείο .flatpak, για εφαρμογή που δεν υπάρχει στο Flathub. + Επιλογή αρχείου .flatpak + Πηγή + Στην Επιφάνεια εργασίας σας + Νέα εφαρμογή + Όνομα + Από την εφαρμογή + Εικονίδιο + Προεπιλεγμένο εικονίδιο + Επιλογή εικονιδίου + Εκκαθάριση + Από το αποθετήριο + Αναζήτηση εικονιδίων… + Προεπιλογή + Επεξεργασία %1$s + Αποθήκευση + Έλεγχος φακέλου… + Ο φάκελός του είναι συνδεδεμένος στη συνεδρία: %1$s + Ο φάκελός του περιέχει προγράμματα Linux, τα οποία ο κοινός χώρος αποθήκευσης δεν μπορεί να εκτελέσει, οπότε αντιγράφεται μέσα στο DroidDeck (%1$s). + Προσθήκη + Ακύρωση + Εγκαταστήστε πρώτα το runtime Linux από τη Ρύθμιση. + Περιμένετε να τελειώσει το %1$s. + Συμβατό + Τα builds που φέρουν τη σήμανση «%1$s» στο μενού Συμβατότητα του Steam έχουν τις διορθώσεις prefix του DroidDeck και εκκινούν παιχνίδια εδώ. Η προεπιλογή είναι το Proton Experimental ARM64 (%1$s), ή το Proton 11.0 ARM64 (%1$s) μόλις εγκατασταθεί αυτό το νεότερο build. Οι δικές της καταχωρίσεις Proton της Valve χωρίς τη σήμανση δεν μπορούν να εκκινήσουν παιχνίδι σε αυτή τη συσκευή. + Εγκατεστημένο %1$s · %2$s + Ακύρωση + Αυτόματο + Ποτέ + Πάντα + Κατάργηση εγκατάστασης + Έλεγχος + Ενημέρωση + Έλεγχος… + Σε εξέλιξη… + Διαγραφή + Προσθήκη… + Χρήση + Εφαρμόζεται στην επόμενη συνεδρία. + Συνεδρία Steam + Συνεδρία επιφάνειας εργασίας + Οθόνη + Ανάλυση + Έως 720p + Έως 900p + Έως 1080p + Εγγενής + %1$s (προεπιλογή) + Προσαρμοσμένη · %1$d×%2$d + Προσαρμοσμένη… + Το 720p μπορεί να κάνει τα μενού πιο αποκρίσιμα. + Αναλογία οθόνης + Καθορίζεται από την προσαρμοσμένη ανάλυση. + Το Αυτόματο χρησιμοποιεί τουλάχιστον 16:9. + Όριο καρέ + Περιορίζει ολόκληρη τη συνεδρία. Η οθόνη μεταβαίνει σε ρυθμό ανανέωσης που διαιρείται ακριβώς με το όριο. + HDR + Έξοδος HDR10 + Μη διαθέσιμο: %1$s. + Κάντε επανεκκίνηση της εφαρμογής για να εφαρμοστεί. + Προγράμματα οδήγησης + Προγράμματα οδήγησης GPU + Κοινά για το Steam και την Επιφάνεια εργασίας. Ορίζονται στη σελίδα Στοιχεία. + Αφή & χειριστήρια + Αφή + Πινακίδα αφής + Απευθείας + Αυτόματο: πινακίδα αφής στην Επιφάνεια εργασίας, απευθείας στο Steam. + Χειριστήρια στην οθόνη + Steam + QAM + Το Αυτόματο εμφανίζει τα χειριστήρια όταν δεν είναι συνδεδεμένο χειριστήριο. + Αφή Steam (beta) + Τα χειριστήρια αφής του Steam ξεκινούν από την επόμενη συνεδρία Steam + Διάταξη αφής Steam + Μετακινήστε, αλλάξτε μέγεθος ή αποκρύψτε τα χειριστήρια αφής αυτού του παιχνιδιού· αποθηκεύεται στο Steam + Χειριστήριο + Πώς βλέπει το Steam το χειριστήριό σας. Εφαρμόζεται στην επόμενη συνεδρία. + Χειριστήριο Steam Deck + Steam Controller + DualShock 4 + DualSense + Χειριστήριο Xbox 360 + Deck: κουμπί Quick Access και γυροσκόπιο. Steam Controller: το ίδιο, με αίσθηση λαβής στα πίσω κουμπιά. DualShock 4 και DualSense: γυροσκόπιο, και τα trackpad ως πινακίδα αφής τους. Xbox 360: ανοίξτε το Quick Access με Guide+A. + Πίσω + Μενού → QAM + QAM → Μενού + Συνεδρία + Συμπεριφορά παρασκηνίου + Όταν η εφαρμογή είναι κρυφή ή η οθόνη κλείνει. + Μη αυτόματο + Εγγενής + Το Αυτόματο θέτει σε παύση και συνεχίζει όταν εμφανίζεται. Το Εγγενής προετοιμάζει πρώτα το Steam για αδρανοποίηση. Το Μη αυτόματο περιμένει το Συνέχιση. Το Ποτέ συνεχίζει να εκτελείται. + Διατήρηση των λήψεων σε εξέλιξη + Καθυστερεί την αναστολή παρασκηνίου όσο το Steam κάνει ενεργά λήψη. + Το Αυτόματο θέτει σε παύση στο παρασκήνιο και συνεχίζει όταν εμφανίζεται. Το Μη αυτόματο περιμένει το Συνέχιση. Το Ποτέ συνεχίζει να εκτελείται. + Εκκίνηση + Εκκίνηση του Steam με το DroidDeck + Ανοίγει τη συνεδρία Steam όταν εκκινείται το DroidDeck. + Decky + Έλεγχος για εκδόσεις… + Έτοιμο για εγκατάσταση %1$s. + Δεν βρέθηκε συμβατό build. Δοκιμάστε ξανά αργότερα. + Εγκατεστημένο · %1$s + Διαθέσιμη ενημέρωση · %1$s + Ενημερωμένο · %1$s + Loader + Εγκατάσταση τελευταίας έκδοσης + Ξεκινά με το Steam. Όσο είναι ενεργό, άλλες εφαρμογές σε αυτή τη συσκευή μπορεί να είναι σε θέση να ελέγξουν το Steam. + Ανενεργό. Η θύρα απομακρυσμένου εντοπισμού σφαλμάτων του Steam παραμένει κλειστή. + Κατάργηση εγκατάστασης του Decky Loader; + Καταργεί τον loader. Τα plugins και οι ρυθμίσεις σας διατηρούνται. + Πελάτης + Επικάλυψη απόδοσης + Απενεργοποιήστε το αν τα παιχνίδια κολλάνε ή μαυρίζει η οθόνη σε λειτουργία Deck. Εφαρμόζεται στην επόμενη συνεδρία. + Επιδιόρθωση πελάτη Steam + Κάνει ξανά λήψη του πελάτη Steam την επόμενη φορά που θα ξεκινήσει το Steam. Χρησιμοποιήστε το αν το Steam παραμένει στην οθόνη φόρτωσης. Τα παιχνίδια, η σύνδεση και οι ρυθμίσεις διατηρούνται. + Ο πελάτης Steam θα ληφθεί ξανά την επόμενη φορά που θα ξεκινήσει το Steam. + Επιδιόρθωση + Εικαστικά + Κλειδί API SteamGridDB + Το κλειδί σας + Επεξεργασία… + Συμπληρώνει τα εικαστικά που λείπουν από το Steam, έπειτα από το SteamGridDB. Τα δικά σας cover.jpg, hero.jpg, logo.png ή icon.png στον φάκελο του παιχνιδιού έχουν προτεραιότητα. + Αφαίρεση %1$s + Αφαίρεση; + Προσθήκη παιχνιδιού + Προσθήκη παιχνιδιού: + Επιλέξτε το exe: + Αυτή η τοποθεσία δεν είναι διαθέσιμη στη συνεδρία + Έλεγχος αυτού του exe + Στοιχεία Windows + Τα LB / RB μεταπηδούν ανάμεσα στις ενότητες. + Κανένα + DLL των Windows που το Proton δεν περιλαμβάνει, από τη λίστα στοιχείων. Ενεργοποιήστε όσα χρειάζεται αυτό το παιχνίδι: τοποθετούνται στον δικό του φάκελο Windows, ποτέ στα αρχεία του, και παραμένουν ανεξάρτητα από το Proton με το οποίο εκτελείται. + Δεν ήταν δυνατή η πρόσβαση στη λίστα στοιχείων. Τα στοιχεία που έχουν ληφθεί ήδη μπορούν ακόμα να αλλάξουν. + Προτείνεται για αυτό το παιχνίδι + Έλεγχος τι χρειάζεται αυτό το παιχνίδι… + Διαθέσιμο με σύνδεση στο διαδίκτυο + + Ενεργοποίηση %1$d στοιχείου… + Ενεργοποίηση %1$d στοιχείων… + + Ενεργό για αυτό το παιχνίδι + Το Steam εγκαθιστά το %1$s με το %2$s (εφαρμογή %3$s) + Από τη λίστα του GOG για αυτό το παιχνίδι + Από τις προαπαιτήσεις του Epic για αυτό το παιχνίδι + Από τα βήματα εγκατάστασης του Amazon για αυτό το παιχνίδι + Το Steam εγκαθιστά το %1$s με την εφαρμογή %2$s, με αντιστοίχιση ονόματος + Αναφέρονται επίσης + Αναφέρεται από %1$s· δεν είναι στοιχείο εδώ + Steam + GOG + Epic Games + Amazon Games + Το παιχνίδι διανέμει το %1$s + Το παιχνίδι διαθέτει το %1$s (συχνά λειτουργεί ως έχει) + Υπάρχει ήδη σε αυτό το παιχνίδι + Τίποτα στον φάκελο του παιχνιδιού δεν παραπέμπει σε στοιχείο. + Εγκατεστημένα για αυτό το παιχνίδι (%1$d) + Όλα τα στοιχεία (%1$d) + Απαιτούν πρόγραμμα εγκατάστασης Windows (%1$d) + Αυτά είναι δικά τους προγράμματα εγκατάστασης χωρίς πακέτο μέσα για αποσυμπίεση, και χωρίς καταγραφή ακόμα για ό,τι εγκαθιστούν. Τα περισσότερα είναι παλιά runtime που το Mono μέσα στο Proton ήδη αντικαθιστά· καταγραφές προστίθενται όσο γίνονται. + Εμφάνιση + Απόκρυψη + Εκτελεί το δικό του πρόγραμμα εγκατάστασης (χωρίς καταγραφή ακόμα) + Δεν υποστηρίζεται ακόμα εδώ + Φόρτωση των στοιχείων… + Δεν ήταν δυνατή η εγκατάσταση: %1$s + Λήφθηκε + Λήψη %1$s + Εγκατάσταση: %1$s + Απενεργοποίηση… + Λήψη %1$s + Εφαρμόζεται την επόμενη φορά που θα ξεκινήσει το παιχνίδι. + Τέντωμα παιχνιδιών 16:9 + Σε οθόνες πιο τετράγωνες από 16:9, τεντώνει τα παιχνίδια για να καλύψουν την οθόνη αντί να εμφανίζονται μαύρες λωρίδες. Εφαρμόζεται στην επόμενη συνεδρία. + Ήχος + DirectAudio για παιχνίδια + Παρακάμπτει το PulseAudio για μικρότερη καθυστέρηση. + Μικρόφωνο + Για φωνητική συνομιλία. + Δίκτυο + Εντοπισμός Wi-Fi + Εμφανίζει ονόματα Wi-Fi και κοντινά δίκτυα στο Steam. + Οι υπηρεσίες τοποθεσίας είναι απενεργοποιημένες. + Δεν έχει δοθεί άδεια τοποθεσίας. + Άνοιγμα ρυθμίσεων + Συνέχεια + Ενεργοποίηση εντοπισμού Wi-Fi; + Το Android απαιτεί άδεια τοποθεσίας για να διαβάσει ονόματα Wi-Fi και να εντοπίσει κοντινά δίκτυα. Το DroidDeck χρησιμοποιεί αυτή την πρόσβαση για να εμφανίσει αυτά τα δίκτυα στο Steam. Δεν παρακολουθούμε την τοποθεσία σας. Η πρόσβαση στο διαδίκτυο λειτουργεί χωρίς αυτή την άδεια. + Αποθηκευτικός χώρος παιχνιδιών + Αυτόματο - η κάρτα SD όταν υπάρχει, καθώς και οι φάκελοι Games σας + Αυτόματο - οι φάκελοι Games σας, καθώς και η κάρτα SD όταν υπάρχει (καμία τώρα) + Μόνο εσωτερική αποθήκευση + Φάκελος: %1$s + Δεύτερη βιβλιοθήκη + Παιχνίδια που μεταδίδουν δεδομένα από κάρτα SD ή κοινόχρηστο χώρο αποθήκευσης μπορεί να κολλάνε. + Διαγνωστικά αποθηκευτικού χώρου + Καταγράφει αποτελέσματα κατανομής και δειγματοληπτικούς χρόνους αποθήκευσης στην Κοινή χρήση αρχείων καταγραφής. Μπορεί να επιβραδύνει τις εγκαταστάσεις όσο είναι ενεργό. Εφαρμόζεται στην επόμενη συνεδρία. + Επιλογή φακέλου… + Προσαρμοσμένη ανάλυση + Αντικαθιστά το όριο και την αναλογία. Ένα μέγεθος που δεν ταιριάζει με την οθόνη εμφανίζει μαύρες λωρίδες. + Πλάτος + Ύψος + Μεταξύ 320×240 και 3840×2160. + + %1$d παιχνίδι + %1$d παιχνίδια + + OK + Διαγραφή + Επαναφορά + Λήψη + Διαγραφή %1$s + %1$.1f MB + Πρόγραμμα οδήγησης runtime + Χρησιμοποιείται από το Steam, τα παιχνίδια και την επιφάνεια εργασίας. Εφαρμόζεται στην επόμενη συνεδρία. + Εισαγωγή zip Turnip… + Πρόγραμμα οδήγησης οθόνης + Σχεδιάζει τα καρέ στην οθόνη και στις δύο λειτουργίες. Κάντε επανεκκίνηση της εφαρμογής για εφαρμογή. + Εισαγωγή zip AdrenoTools… + ποτέ + Έλεγχος Nightlies %1$s + Στοιχεία + Σχετικά με τα Στοιχεία + Έλεγχος για νέα προγράμματα οδήγησης + Έλεγχος των Nightlies για νέα πακέτα + Proton + Το Proton που επιλέγεται εδώ εκτελεί κάθε παιχνίδι που δεν έχει επιλέξει το δικό του στο Steam, και μια αλλαγή που γίνεται στο Steam εμφανίζεται και εδώ. Το Steam αντικαθιστά το Proton της Valve όταν ενημερώνεται· τα πρωτότυπα διατηρούνται ανά build. + Διαμόρφωση + Διαμόρφωση προφίλ + Προφίλ + Προεπιλεγμένο προφίλ + Χρησιμοποιείται αυτόματα από παιχνίδια χωρίς παρακάμψεις. + Αποθηκευμένες ρυθμίσεις που χρησιμοποιούνται αυτόματα όταν εκκινείται το %1$s. + Επιλέξτε μόνο τις ρυθμίσεις που χρειάζεται αυτό το παιχνίδι. Όλα τα υπόλοιπα συνεχίζουν να χρησιμοποιούν το Προεπιλεγμένο προφίλ. + Προεπιλογή FEX + Χρησιμοποιεί την προεπιλογή του Προεπιλεγμένου προφίλ. + Αποθηκεύτηκε προσαρμοσμένη προεπιλογή για αυτό το παιχνίδι. + Προσαρμοσμένες μεταβλητές FEX + Χρήση Προεπιλεγμένου + Συμβατότητα παιχνιδιού + Επιλογές περιβάλλοντος Proton, DXVK, VKD3D και συγχρονισμού για αυτό το παιχνίδι. + Χρησιμοποιεί το Proton του Προεπιλεγμένου προφίλ. + Αποθηκεύτηκε προσαρμοσμένο Proton για αυτό το παιχνίδι μέσω της αντιστοίχισης συμβατότητας του Steam. + Χωρίς Proton + DXVK + VKD3D-Proton + Χρησιμοποιεί το πακέτο του Προεπιλεγμένου προφίλ από %1$s, εκτός αν αυτό το παιχνίδι επιλέξει άλλο πακέτο. + Χρησιμοποιεί το πακέτο στοιχείων του Προεπιλεγμένου προφίλ. + Χρησιμοποιεί το %1$s μόνο για αυτό το παιχνίδι. + Επιλογές runtime + Άλλες επιλογές περιβάλλοντος Proton, DXVK, VKD3D-Proton και συγχρονισμού ανά παιχνίδι. + Το προφίλ του %1$s αποθηκεύτηκε για την επόμενη εκκίνηση του παιχνιδιού. + Εκκίνηση χωρίς το Steam + Για παιχνίδι που κλείνει αμέσως: το εκκινεί χωρίς τη ρύθμιση του Steam στο Proton. Λειτουργίες του Steam, όπως το overlay, είναι απενεργοποιημένες για αυτό. + Ενεργό + Η επιλογή Proton του %1$s αποθηκεύτηκε και είναι διαθέσιμη στο Steam τώρα. + Η επιλογή Proton του %1$s αποθηκεύτηκε. Κάντε επανεκκίνηση του Steam πριν εκκινήσετε το παιχνίδι. + Το %1$s δεν μπορεί να χρησιμοποιήσει το επιλεγμένο Proton σε αυτή τη συσκευή. + Δεν ήταν δυνατή η φόρτωση του προφίλ του %1$s: %2$s + Δεν ήταν δυνατή η αποθήκευση του προφίλ παιχνιδιού: %1$s + Επαναφορά προφίλ παιχνιδιού + Επαναφορά του προφίλ του %1$s; + Αφαίρεση του Proton, του FEX, του πακέτου στοιχείων, του περιβάλλοντος, του φιλτραρίσματος υφών και των παρακάμψεων στοιχείων Windows αυτού του παιχνιδιού; Θα χρησιμοποιήσει το Προεπιλεγμένο προφίλ. + Το %1$s χρησιμοποιεί τώρα το Προεπιλεγμένο προφίλ. + Δεν ήταν δυνατή η πλήρης επαναφορά του προφίλ παιχνιδιού: %1$s + Το %1$s εκτελεί τώρα κάθε παιχνίδι που δεν έχει επιλέξει το δικό του Proton στο Steam. + Το %1$s θα εκτελεί κάθε παιχνίδι που δεν έχει επιλέξει το δικό του Proton, από την επόμενη φορά που θα ξεκινήσει το Steam. + Το %1$s δεν μπορεί να εκκινήσει παιχνίδια εδώ, οπότε δεν μπορεί να είναι η προεπιλογή. + Δεν ήταν δυνατή η αποθήκευση του προεπιλεγμένου Proton: %1$s + %1$s · %2$d αντικαταστάθηκαν + %1$s · όλα πρωτότυπα + %1$s · το παιχνίδι εκτελείται + %1$s · πακέτο droiddeck-esync + %1$s · χωρίς πακέτο droiddeck-esync ακόμα + %1$s · ενσωματωμένο esync, δεν χρειάζεται πακέτο + Προγράμματα οδήγησης GPU + %1$s · επαναφέρθηκε στην εκκίνηση %2$s + Επόμενο: %1$s · μετά το κλείσιμο του παιχνιδιού + %1$s σε χρήση: %2$s + Περιμένει μέχρι να κλείσει το παιχνίδι + Εφαρμόζεται την επόμενη φορά που θα ξεκινήσει ένα παιχνίδι + Εισαγωγή .zip + Ανάγνωση Proton… + Δεν έχει εγκατασταθεί Proton ακόμα. Εκκινήστε το Steam μία φορά για λήψη του ARM64 Proton του, ή προσθέστε GE-Proton, CachyOS ή DroidDeck-Proton από τη Ρύθμιση. + Πρωτότυπο · %1$s + %1$s · αυτό το build + %1$s · προηγούμενο build + Εγκατεστημένα · %1$d + Δεν έχει αποθηκευτεί τίποτα για το %1$s ακόμα. Τα δικά του αρχεία αυτού του Proton αποθηκεύονται ως το πρωτότυπό του. + Ένα παιχνίδι εκτελείται σε αυτό το Proton, οπότε αυτό περιμένει μέχρι να κλείσει. + Εφαρμόζεται την επόμενη φορά που θα ξεκινήσει ένα παιχνίδι. + Αντικατάσταση %1$s; + Τοποθέτηση του %1$s στο %2$s; Τα πρωτότυπα αρχεία του παραμένουν αποθηκευμένα. %3$s + Επαναφορά %1$s; + Επαναφορά του πρωτότυπου %1$s στο %2$s; %3$s + Αφαιρεί το πακέτο από την εφαρμογή. Μπορείτε να το λάβετε ξανά. + Διαγραφή αυτού του πρωτοτύπου; + Αυτό το πρωτότυπο %1$s είναι από προηγούμενο build του %2$s. Το πρωτότυπο του τρέχοντος build διατηρείται πάντα. + Στα Nightlies · %1$d + Ανανεώστε για να εμφανιστούν τα πακέτα στα Nightlies. + Τίποτα νέο από τον τελευταίο έλεγχο. + Εισαγωγή .wcp + Επαναφορά του πρωτοτύπου αυτού του Proton + Επαναφορά του πρωτότυπου %2$s του %1$s; + Αντικατάσταση + FEX, DXVK και VKD3D-Proton για κάθε Proton με το οποίο το Steam εκτελεί παιχνίδια.\n\nΕπιλέξτε μια εγκατεστημένη γραμμή για να την αντικαταστήσετε. Εφαρμόζεται την επόμενη φορά που θα ξεκινήσει ένα παιχνίδι, ή μετά το κλείσιμο του τρέχοντος παιχνιδιού.\n\nΟι επιλογές σας επανελέγχονται πριν από κάθε εκκίνηση και επανέρχονται αν κάτι τις άλλαξε. Τα πρωτότυπα αρχεία κάθε Proton διατηρούνται, έτσι οι ενημερώσεις του Steam δεν τα χάνουν ποτέ. + Πρωτότυπο + Σε χρήση + Νέο + Αποθηκευμένο + Απόδοση + Πυρήνες CPU, διορθώσεις συνεδρίας και το όνομα κεντρικού υπολογιστή. Εφαρμόζεται στην επόμενη συνεδρία. + Πυρήνες πελάτη Steam + Παράκαμψη της επιλογής πυρήνων του Steam + Καθηλώνει το Steam, τον βοηθό διεπαφής του και το gamescope στους παρακάτω πυρήνες. + Πυρήνες πελάτη + Πυρήνες για το Steam. + Ενεργοποιήστε την παράκαμψη για να επιλέξετε. + Πυρήνες παιχνιδιού + Πυρήνες παιχνιδιού + Πυρήνες για παιχνίδια. Με όλους επιλεγμένους, το Android τους προγραμματίζει ελεύθερα. + Διεπαφή πελάτη + Νηματοποιημένο GL + Μπορεί να κάνει τα μενού του Steam πιο αποκρίσιμα. + Zink: lazy descriptors + Προτείνεται για προγράμματα οδήγησης χωρίς descriptor buffers. + Παράβλεψη ελέγχων σφαλμάτων GL + Απενεργοποιεί την επικύρωση GL ανά κλήση. + gamescope: ουρά GPU πραγματικού χρόνου + Δίνει προτεραιότητα στην εργασία GPU του compositor. Μπορεί να εξομαλύνει τον ρυθμό καρέ με κάποιο κόστος για τα παιχνίδια. + GPU + Διατήρηση της GPU στη μέγιστη συχνότητά της + Μόνο Adreno. Λιγότερα κολλήματα, μεγαλύτερη χρήση μπαταρίας και θερμότητα. + Διορθώσεις συνεδρίας + Turnip: απόδοση sysmem + Ταχύτερο για το Steam και τα περισσότερα παιχνίδια. Απαιτείται σε Adreno 710/720/722. + Παράβλεψη του βοηθού xalia του Steam + droiddeck-ntsync (πειραματικό) + Ένα υποκατάστατο σε επίπεδο χρήστη για το πρόγραμμα οδήγησης ntsync του Linux, για δοκιμές. Όσο είναι ενεργό, το Proton το χρησιμοποιεί αντί για το droiddeck-esync και το droiddeck-fsync. Λειτουργεί με Proton Experimental ARM64, GE-Proton και proton-cachyos. Εφαρμόζεται στην επόμενη συνεδρία. + droiddeck-esync + droiddeck-fsync πρώτα + Εκτελεί το δικό του fsync του Proton στα απλά αρχεία Proton, με το futex_waitv παρεχόμενο σε επίπεδο χρήστη, οπότε δεν χρειάζεται πακέτο και συνεχίζει να λειτουργεί τη στιγμή που το Steam ενημερώνει το Proton. Όσο είναι ενεργό, τα παιχνίδια το χρησιμοποιούν πριν από το droiddeck-esync, εκτός από τα λίγα που το Proton κρατά εκτός fsync. Εφαρμόζεται στην επόμενη συνεδρία. + Τα νήματα Windows των παιχνιδιών περιμένουν το ένα το άλλο μέσω eventfd και κοινόχρηστης μνήμης αντί για διαδρομές μέσω wineserver. Χρησιμοποιεί ένα πακέτο droiddeck-esync φτιαγμένο ειδικά για αυτό το build του Proton· τα πακέτα λαμβάνονται αυτόματα. Μέχρι να υπάρξει ένα για νέο build του Proton, το droiddeck-fsync το αναπληρώνει. Εφαρμόζεται στην επόμενη συνεδρία. + Απενεργοποιεί τον βοηθό πλοήγησης με χειριστήριο του Proton, που κοστίζει CPU σε κάθε παιχνίδι. + Εκτέλεση του proot χωρίς seccomp + Μπορεί να διορθώσει σφάλματα απόντων syscall σε ορισμένους πυρήνες. Μπορεί να μειώσει την απόδοση. + Ταχεία διαδρομή proot + Απαιτεί το φίλτρο seccomp του proot. + Ταχύτερη αναζήτηση αρχείων και λιγότερα κολλήματα. Απενεργοποιήστε το αν ένα πρόγραμμα δεν μπορεί να βρει τα αρχεία του. + Ταυτότητα συνεδρίας + Όνομα κεντρικού υπολογιστή + Το όνομα που βλέπει το Steam για αυτή τη συσκευή. Αν το αφήσετε κενό, επανέρχεται σε %1$s. + Μόνο γράμματα, ψηφία και εσωτερικές παύλες, έως 63 χαρακτήρες. + Το Android είναι ρυθμισμένο να διακόπτει αυτή τη συνεδρία + Εγκατεστημένο + Διαθέσιμο για λήψη + Τίποτα νέο από τον τελευταίο έλεγχο. + Επαναφορά ενσωματωμένων προγραμμάτων οδήγησης + Ενσωματωμένο στην εφαρμογή: αυτό αφαιρεί τα αποσυμπιεσμένα αρχεία του και το αποκρύπτει. Η Επαναφορά ενσωματωμένων προγραμμάτων οδήγησης το επαναφέρει. + Το ένα μισό ενός πακέτου Android + Linux: αφαιρούνται και τα δύο μισά. Ένα πρόγραμμα οδήγησης σε χρήση επανέρχεται στο προεπιλεγμένο. + Τα αρχεία του αφαιρούνται από την εφαρμογή. + Τα αρχεία του αφαιρούνται. Είναι σε χρήση, οπότε το προεπιλεγμένο παίρνει τη θέση του. + Οθόνη + Εφέ + Χειριστήρια + Στοιχεία + Χειριστήριο + Ρυθμίσεις + Σελίδα %1$s + Steam + Επιφάνεια εργασίας + Μενού Steam + Quick access + Άνοιγμα Quick Access Menu + Προηγούμενη σελίδα + Επόμενη σελίδα + HUD απόδοσης + Λειτουργία κλιμάκωσης + Οξύτητα κλιμάκωσης + Εφέ οθόνης + Σε πραγματικό χρόνο. Κοστίζει λίγο χρόνο GPU όσο κάποιο εφέ είναι ενεργό. + Προεπιλογές + Προσαρμοσμένο + Για προχωρημένους + Αναπτυγμένο + Συμπτυγμένο + Όξυνση (CAS) + Επίπεδο όξυνσης + Ψεύτικο HDR + Αφαίρεση λωρίδων + Ένταση αφαίρεσης λωρίδων + Φωτεινότητα + Αντίθεση + Γάμα + Κορεσμός + FXAA (εξομάλυνση ακμών) + Περιγράμματα Toon + CRT + NTSC + Φιλτράρισμα υφών + Παιχνίδια DirectX 9–11, από την επόμενη εκκίνησή τους. + Ανισοτροπικό φιλτράρισμα + Οξύτητα υφών + Μια αρνητική απόκλιση mip: πιο έντονες υφές, κάποιο τρεμόπαιγμα. Το Αυτόματο ταιριάζει με το πόσο μεγεθύνεται η συνεδρία στην οθόνη. + Αυτόματο (%1$s) + Πληκτρολόγιο + Πληκτρολόγιο PC + Android + Δεύτερη οθόνη + Λειτουργία + Εμφανίζει + Εφαρμογές Android + Εκκίνηση εφαρμογής + Απόκρυψη + Εμφάνιση + Δεν υπάρχουν εφαρμογές για εκκίνηση + Συμπεριφορά συνεδρίας + Εφαρμόζεται τώρα και σε επόμενες συνεδρίες σε αυτή τη λειτουργία. + Επόμενη συνεδρία + Υποστήριξη + Αρχεία καταγραφής συνεδρίας + Επισυνάψτε σε αναφορά σφάλματος + Κοινή χρήση αρχείων καταγραφής + Παρασκήνιο + Διακοπή + Διακοπή συνεδρίας; + Εκτελείται τώρα + Κανένα παιχνίδι δεν εκτελείται σε Proton + Προφίλ παιχνιδιού + Οι λεπτομέρειες του προφίλ δεν είναι διαθέσιμες ακόμα. + Δεν ήταν δυνατή η φόρτωση προφίλ παιχνιδιών. Νέα προσπάθεια αυτόματα. + Δεν ήταν δυνατή η ανανέωση προφίλ παιχνιδιών. Εμφανίζονται τα τελευταία διαθέσιμα δεδομένα· νέα προσπάθεια αυτόματα. + Προβολή και επεξεργασία των αποθηκευμένων ρυθμίσεων που θα χρησιμοποιηθούν στην επόμενη εκκίνηση. + Χρήση Προεπιλεγμένου προφίλ + Προσαρμοσμένο προφίλ + Χρήση %1$s από το Προεπιλεγμένο προφίλ. + Οι αλλαγές αποθηκεύονται στο προφίλ του %1$s και εφαρμόζονται την επόμενη φορά που θα εκκινήσει το παιχνίδι. + + %1$d παράκαμψη παιχνιδιού + %1$d παρακάμψεις παιχνιδιού + + Δεν έχει εγκατασταθεί Proton ακόμα. + παιχνίδι σε εκτέλεση + επαναφέρθηκε στην εκκίνηση %1$s + Πρωτότυπο + Εισαγωγή και διαγραφή στη σελίδα Στοιχεία. + Έγινε + Χρειάζεται προσοχή + Σε εξέλιξη + Ελέγξτε τη διεύθυνση IP και τη θύρα του Ασύρματου εντοπισμού σφαλμάτων, ή συζεύξτε ξανά αν το Android αφαίρεσε αυτή τη συσκευή. + Εγκατάσταση + Διαχείριση + Επισκόπηση + Χειριστήριο + Συνεδρία + Εκκινητής + Ρύθμιση + ● Όλα έτοιμα + %1$d από %2$d έτοιμα + Έλεγχος συστήματος + Η συσκευή υποστηρίζεται + Μη δοκιμασμένη GPU + Η GPU δεν υποστηρίζεται + %1$s · %2$s + %1$s: %2$s + Το Steam απαιτεί πρόγραμμα οδήγησης Adreno· το %1$s μπορεί να εμφανίσει μαύρη οθόνη + Runtime Linux + %1$s · %2$d%% + Δεν έχει εγκατασταθεί · περίπου 3 GB, εγκαθίσταται στο πρώτο Play + Εγκατεστημένο + %1$s · διαθέσιμη ενημέρωση + %1$s · ενημερωμένο + Όριο θυγατρικών διαδικασιών + Ενεργό · Το Android μπορεί να κλείσει το Steam. Η απενεργοποίησή του διαρκεί ένα λεπτό. + Δεν έχει ορισθεί · Το Android μπορεί να κλείσει το Steam. Η απενεργοποίησή του διαρκεί ένα λεπτό. + Δεν ήταν δυνατός ο έλεγχος · η απενεργοποίηση διαρκεί ένα λεπτό. + Διόρθωση + Λεπτομέρειες + Επιλογές προγραμματιστή + Χρήση Ασύρματου εντοπισμού σφαλμάτων + Έλεγχος ξανά + Ενεργοποίηση ορίου + Λογαριασμός Steam + Συνδεδεμένος ως %1$s · λειτουργία εκτός σύνδεσης + Συνδεδεμένος ως %1$s + Πατήστε Play και συνδεθείτε στο Steam + Εργαλεία + Χειριστήριο + Οι ρυθμίσεις χειριστηρίου δεν είναι διαθέσιμες. + Συνεδρία + Αρχεία καταγραφής συνεδρίας + Αποθηκεύονται μετά από κάθε συνεδρία + Εργαλεία εντοπισμού σφαλμάτων + Πιο πρόσφατα αρχεία καταγραφής συνεδρίας + Αποθηκευμένα αρχεία καταγραφής συνεδρίας + Διατηρεί τα %1$d πιο πρόσφατα + Εκκαθάριση αρχείων καταγραφής + Λειτουργία εκτός σύνδεσης + Συνδεθείτε πρώτα στο Steam + Εκκινητής + Γλώσσα + Το χρησιμοποιεί και το Steam, από την επόμενη φορά που θα ξεκινήσει. + Προεπιλογή συστήματος (%1$s) + Προεπιλογή συστήματος + Κλίμακα εφαρμογής + Κείμενο και χειριστήρια του DroidDeck + %1$d%% (Προεπιλογή) + Θέμα + Εμφάνιση εκκινητή + Χρήση ως αρχική οθόνη + Το DroidDeck μπορεί να είναι η εφαρμογή αρχικής οθόνης + Το DroidDeck δεν προτείνεται ποτέ ως εφαρμογή αρχικής οθόνης + Προσανατολισμός + Αυτόματος οριζόντιος προσανατολισμός + Οριζόντιος προσανατολισμός + Αντίστροφος οριζόντιος προσανατολισμός + Πλήρης οθόνη + Αποκρύπτει τις γραμμές κατάστασης και πλοήγησης του Android + Εμφανίζει τις γραμμές κατάστασης και πλοήγησης του Android + Προεπιλεγμένη εφαρμογή αρχικής οθόνης + Επιλογή εφαρμογής αρχικής οθόνης + Επιλογή + Εφαρμογές Linux (beta) + Αρχεία + Περιήγηση και διαχείριση αρχείων + Εκδόσεις Proton + Εγκατάσταση builds Proton για ARM64 + Απόδοση + Ανάθεση πυρήνων CPU + Φάκελος ROM + Πού αποθηκεύονται τα παιχνίδια προσομοιωτών + Στοιχεία + FEX, DXVK, VKD3D + Χειριστήρια + Αντιστοίχιση κουμπιών + Ανακάλυψη + Αναζήτηση + Εγκατεστημένα + %1$s · %2$d + Κατάστημα + Flathub · ARM64 + ● Το Flatpak είναι έτοιμο + Εγκαταστήστε πρώτα το runtime Linux από τη Ρύθμιση. + Ρύθμιση του Flatpak + Προσθέτει το Flatpak και το συνδέει με το Flathub (περίπου 20 MB). Η πρώτη εφαρμογή κάνει επίσης λήψη του runtime της. + Ρύθμιση… + Flatpak + Ενημερώσεις + %1$s · %2$s + %1$s · %2$s · %3$d%% + Εκκίνηση… + Κατηγορίες + Δεν ήταν δυνατή η πρόσβαση στο Flathub. Ελέγξτε τη σύνδεσή σας και δοκιμάστε ξανά. + Δοκιμάστε ξανά + Αναζήτηση στο Flathub + Firefox, παιχνίδια, προσομοιωτές… + Αναζήτηση + Όλα + Αναζήτηση… + Αναζητήστε με όνομα ή επιλέξτε κατηγορία. Αναφέρονται μόνο εφαρμογές ARM64. + Καμία εφαρμογή ARM64 στο Flathub δεν ταιριάζει με αυτό. + Αποτελέσματα + Έλεγχος για ενημερώσεις + Ενημέρωση όλων (%1$d) + ● Όλα είναι ενημερωμένα + Δεν υπάρχουν εφαρμογές ακόμα. Βρείτε κάποιες στην Ανακάλυψη ή την Αναζήτηση. + Ρυθμίστε το Flatpak για να εγκαταστήσετε εφαρμογές. + Διαθέσιμη ενημέρωση · %1$s + Άνοιγμα + Λεπτομέρειες + Αφαίρεση + Πατήστε ξανά για αφαίρεση + Εγκατάσταση… + Εγκατάσταση + Ρυθμίστε πρώτα το Flatpak + Λήψη %1$s + ● Εγκατεστημένο + Δεν ήταν δυνατή η φόρτωση της σελίδας Flathub αυτής της εφαρμογής. + Φόρτωση από το Flathub… + Δεν υπάρχει build ARM64 στο Flathub, οπότε δεν μπορεί να εγκατασταθεί εδώ. + Στιγμιότυπα οθόνης + Σχετικά + ID εφαρμογής + Άδεια χρήσης + Runtime + Μέγεθος εγκατάστασης + %1$s (συν το κοινό runtime του) + Ιστότοπος + Επιβεβαιωμένο + Ενημερώσεις + DroidDeck %1$s · %2$s + Ελέγχθηκε %1$s + Δεν έχει ελεγχθεί ακόμα + Επιτρέψτε στο DroidDeck να ενημερωθεί μόνο του + Στην επόμενη οθόνη, ενεργοποιήστε την Αποδοχή από αυτή την πηγή και επιστρέψτε εδώ. Η ενημέρωση συνεχίζεται μόνη της. + Άνοιγμα ρυθμίσεων + Όχι τώρα + Υπογεγραμμένο διαφορετικά + Δεν γίνεται ενημέρωση επί τόπου + Αυτή η έκδοση δεν είναι υπογεγραμμένη με το κλειδί κυκλοφορίας του DroidDeck, οπότε οι ενημερώσεις δεν μπορούν να εγκατασταθούν πάνω της. Απεγκαταστήστε την για να αλλάξετε. + Υπάρχει ενημέρωση + Δεν γίνεται εγκατάσταση + Έτοιμο για αλλαγή + Μπροστά από το Stable + Είστε μπροστά από το Stable + Αυτό το build είναι νεότερο από το τελευταίο Stable. Θα μεταβείτε στο Stable με την επόμενη κυκλοφορία του. + Η δοκιμή τελείωσε + Αυτή η δοκιμή έχει τελειώσει + Η διόρθωσή της συγχωνεύτηκε ή εγκαταλείφθηκε. Ακολουθήστε το Preview για τις νεότερες διορθώσεις. + Ενημερωμένο + Έχετε την πιο πρόσφατη έκδοση %1$s + · %1$s + %1$s… %2$d%% + %1$s… + Ενημέρωση%1$s + Εγκατάσταση%1$s + Εγκατάσταση Stable %1$s παρόλα αυτά + Ακολούθηση Preview + Διακόψτε την τρέχουσα συνεδρία για ενημέρωση. + Λιγότερα + Περισσότερα + DroidDeck %1$s + Νέο build Preview + Δοκιμαστικό build PR #%1$d + %1$d MB + ΚΑΝΑΛΙ ΕΝΗΜΕΡΩΣΗΣ + Stable + Δοκιμασμένες εκδόσεις, για τους περισσότερους + %1$s · %2$s + Preview + Νεότερες διορθώσεις, πριν το Stable + Ενημερωμένο + Δοκιμαστικά builds + Δοκιμάστε μια διόρθωση πριν κυκλοφορήσει + Κανένα προς το παρόν + PR #%1$d + Δεν έχει κατασκευαστεί για αυτή την έκδοση του DroidDeck + Ενημερώθηκε %1$s + η δοκιμή PR #%1$d + πριν από λίγο + μόλις τώρα + + %1$d προς δοκιμή + %1$d προς δοκιμή + + + Αλλαγές από το build σας · %1$d build πίσω + Αλλαγές από το build σας · %1$d builds πίσω + + + Πρόσφατες αλλαγές · %1$d build + Πρόσφατες αλλαγές · %1$d builds + + Μία ρύθμιση Android για αλλαγή + Steam + Το Android διακόπτει εφαρμογές που ξεκινούν πολλές διαδικασίες παρασκηνίου, και το Steam ξεκινά δεκάδες. Μέχρι να αλλάξει αυτό, τα παιχνίδια κλείνουν χωρίς σφάλμα. + Διόρθωσέ το για εμένα + Ενεργοποιήστε «Απενεργοποίηση περιορισμών θυγατρικών διαδικασιών» και επιστρέψτε εδώ. Αφήστε τις Επιλογές προγραμματιστή ενεργές, διαφορετικά ο διακόπτης απενεργοποιείται ξανά. + Άνοιγμα Επιλογών προγραμματιστή + Αυτή η έκδοση Android δεν έχει διακόπτη για αυτό, οπότε το DroidDeck το αλλάζει μέσω Ασύρματου εντοπισμού σφαλμάτων. Διαρκεί περίπου ένα λεπτό, χωρίς υπολογιστή. + Ο Ασύρματος εντοπισμός σφαλμάτων απαιτεί Wi-Fi. Οποιοδήποτε δίκτυο λειτουργεί· δεν απαιτείται σύνδεση στο διαδίκτυο. + Ο κωδικός σύζευξης εμφανίζεται σε ειδοποίηση. Επιτρέψτε ειδοποιήσεις για το DroidDeck, ή χρησιμοποιήστε Άλλοι τρόποι. + Αποδοχή ειδοποιήσεων + Ανοίξτε Ρυθμίσεις → Επιλογές προγραμματιστή → Ασύρματος εντοπισμός σφαλμάτων και ενεργοποιήστε τον + Πατήστε «Σύζευξη συσκευής με κωδικό σύζευξης» + Πληκτρολογήστε τον κωδικό στην ειδοποίηση του DroidDeck + Αναμονή για το αναδυόμενο παράθυρο σύζευξης… + Βρέθηκε αναδυόμενο παράθυρο. Εισαγάγετε τον κωδικό του στην ειδοποίηση. + Κατάσταση + Έλεγχος κάθε 2 δευτερόλεπτα. Αυτή η σελίδα κλείνει όταν είναι απενεργοποιημένο. + Άλλοι τρόποι + Ασύρματος εντοπισμός σφαλμάτων, μη αυτόματη διεύθυνση, ADB υπολογιστή + Μη αυτόματη διεύθυνση, ADB υπολογιστή, Επιλογές προγραμματιστή + Χρήση Ασύρματου εντοπισμού σφαλμάτων αντ\' αυτού + Ορισμένες ROM προσθέτουν τον διακόπτη ούτως ή άλλως. Αναζητήστε «θυγατρική διαδικασία» στις Επιλογές προγραμματιστή. + Αν η ειδοποίηση δεν λειτουργεί, εισαγάγετε τη διεύθυνση και τον κωδικό του αναδυόμενου παραθύρου εσείς. + Μη αυτόματη εισαγωγή διεύθυνσης + Από υπολογιστή με ADB: + Αντιγραφή εντολής + Επιλέξτε σε ποια οθόνη θα ανοίξουν οι Ρυθμίσεις Android. + Κύρια οθόνη + Έγινε σύζευξη. Αναζήτηση θύρας Ασύρματου εντοπισμού σφαλμάτων… + Έγινε σύζευξη. Εισαγάγετε τη θύρα από τις Ρυθμίσεις. + Η συσκευή βρέθηκε. Εφαρμογή της ρύθμισης… + Δεν ήταν δυνατή η αυτόματη εφαρμογή. Ελέγξτε την παρακάτω διεύθυνση και δοκιμάστε ξανά. %1$s + Ασύρματος εντοπισμός σφαλμάτων + Το όριο θυγατρικών διαδικασιών ενημερώθηκε + Αλλαγή του ορίου θυγατρικών διαδικασιών + Αν η επιλογή λείπει, κάντε σύζευξη μέσω Ασύρματου εντοπισμού σφαλμάτων. + Έγινε σύζευξη. Ελέγξτε τη διεύθυνση IP και τη θύρα στον Ασύρματο εντοπισμό σφαλμάτων. + Το όριο θυγατρικών διαδικασιών ενημερώθηκε. + Χρησιμοποιήστε το βήμα 2 αν οι Επιλογές προγραμματιστή δεν έχουν ρύθμιση θυγατρικών διαδικασιών. Ανοίξτε Επιλογές προγραμματιστή → Ασύρματος εντοπισμός σφαλμάτων → Σύζευξη συσκευής με κωδικό σύζευξης. + Έγινε σύζευξη. Χρησιμοποιήστε την παρακάτω διεύθυνση, ή ενημερώστε την από Ασύρματος εντοπισμός σφαλμάτων → Διεύθυνση IP & θύρα. + Το όριο θυγατρικών διαδικασιών ενημερώθηκε μέσω Ασύρματου εντοπισμού σφαλμάτων. + Το Android επιβεβαίωσε ότι το όριο είναι ενεργό. + Το Android επιβεβαίωσε ότι το όριο είναι ανενεργό. + 2 · Σύζευξη Ασύρματου εντοπισμού σφαλμάτων + Διεύθυνση IP & θύρα αναδυόμενου παραθύρου σύζευξης + Κωδικός σύζευξης (PIN) + 6 ψηφία + Σύζευξη + 2 · Εφαρμογή μέσω Ασύρματου εντοπισμού σφαλμάτων + Διεύθυνση IP & θύρα σύνδεσης + Σύζευξη ξανά + Απενεργοποίηση ορίου + Άλλοι τρόποι αλλαγής + 1 · Επιλογές προγραμματιστή (δοκιμάστε πρώτα) + Ενεργοποιήστε «Περιορισμός θυγατρικών διαδικασιών». + Απενεργοποιήστε «Περιορισμός θυγατρικών διαδικασιών». + 3 · ADB υπολογιστή (τελευταία λύση) + Μόνο αν ο Ασύρματος εντοπισμός σφαλμάτων δεν είναι διαθέσιμος. + 3 · ADB υπολογιστή (τελευταία λύση) + Όταν εμφανίζονται τα χειριστήρια αφής σε μια συνεδρία + Χρώμα + Απόχρωση των κουμπιών οθόνης + Προσαρμοσμένο + Διαφάνεια + %1$d%% + Μέγεθος κουμπιών + Το 100% είναι το κανονικό μέγεθος + Κλικ μοχλού + Πατήστε δύο φορές και κρατήστε έναν μοχλό για L3 ή R3 + Προσαρμοστικοί μοχλοί + Κραδασμός παιχνιδιού + Κουμπί Steam + Κουμπί QAM + Κουμπί πληκτρολογίου + Πατήστε Select για να ανοίξετε το μενού Steam + Αντί για View/Select. + Κινήσεις + Οι μοχλοί εμφανίζονται όπου αγγίζετε και εξαφανίζονται όταν αφήνετε + Διάταξη + Αποθηκεύτηκαν προσαρμοσμένες θέσεις + Προσαρμοσμένο σε αυτή την οθόνη + Επεξεργασία + Επαναφορά + Αντιστοίχιση κουμπιών + Κάθε κουμπί στέλνει τη δική του είσοδο + %1$d από %2$d κουμπιά επαναντιστοιχίστηκαν + Διαμόρφωση + Επαναφορά χειριστηρίου + Επαναφέρει όλες τις ρυθμίσεις χειριστηρίου και τη διάταξη + Τι στέλνει κάθε κουμπί οθόνης στο παιχνίδι. Το Κρυμμένο αφαιρεί το κουμπί. + Κουμπιά οθόνης + Προεπιλογές + Επαναφορά αντιστοίχισης + Εφαρμογές Android + %1$d εφαρμογές + Δεν βρέθηκαν εφαρμογές Android για εκκίνηση. + ● Επιφάνεια εργασίας εγκατεστημένη + Εγκαθίσταται στο πρώτο άνοιγμα + Διαθέσιμο για εγκατάσταση + Δεν έχει εγκατασταθεί. + %1$s · δεν έχει εγκατασταθεί + Άνοιγμα %1$s + Παιχνίδια + Επιλέξτε έναν φάκελο ROM. + Μεταβείτε στο /root/ROMs στο RetroArch. + Προσθέστε παιχνίδια %1$s στο ROMs/%2$s. + Εγκατάσταση %1$s + Διακοπή συνεδρίας για εγκατάσταση + Απαιτείται runtime + Φόρτωση λεπτομερειών εγκατάστασης… + Οι λεπτομέρειες εγκατάστασης δεν είναι διαθέσιμες. Δοκιμάστε ξανά αργότερα. + Εγκαταστήστε πρώτα το runtime Linux από τη Ρύθμιση. + Διακόψτε την τρέχουσα συνεδρία για να εγκαταστήσετε εφαρμογές επιφάνειας εργασίας. + Εγκαθιστά το %1$s στην επιφάνεια εργασίας Linux. + %1$s · %2$d%% + Η απόκρυψη το αφαιρεί από την Επιφάνεια εργασίας. Τα αρχεία του παραμένουν στο runtime Linux. + Αυτό το παιχνίδι δεν βρίσκεται πλέον στον φάκελο ROM. + Επιφάνεια εργασίας · %1$s + Εκκίνηση σε %1$s + Επίσης σε %1$s + Αυτή η σελίδα δεν υπάρχει πια. Επιλέξτε μια ενότητα αριστερά. + Άνοιγμα επιφάνειας εργασίας + Εγκατάσταση & άνοιγμα επιφάνειας εργασίας + Απεγκατάσταση επιφάνειας εργασίας + Αφαιρεί την επιφάνεια εργασίας και τις ενσωματωμένες εφαρμογές της. Διατηρεί παιχνίδια, προστιθέμενες εφαρμογές, ρυθμίσεις και κοινές βιβλιοθήκες. + Αφαίρεση επιφάνειας εργασίας + Η επιφάνεια εργασίας αφαιρέθηκε + Αφαίρεση παλιών αρχείων LXQt + Ενημερώστε το DroidDeck πριν αφαιρέσετε αυτή την έκδοση επιφάνειας εργασίας. + Επιφάνεια εργασίας Linux + KDE Plasma, Firefox και οι προσομοιωτές σας + Τα παιχνίδια που εγκαθιστάτε στο Steam εμφανίζονται εδώ μετά τη συνεδρία. + Αναπαραγωγή Steam + Ρυθμίσεις εκκίνησης + Εκκίνηση + Αρχεία παιχνιδιού + Prefix Proton + Αναζήτηση αποθηκευμένων αρχείων… + Δεν βρέθηκε φάκελος αποθήκευσης με όνομα. Η εξαγωγή λαμβάνει ολόκληρο τον φάκελο χρήστη. + %1$d φάκελοι αποθήκευσης + %1$s · %2$d αρχεία · %3$s + %1$d KB + Διαχείριση αποθηκευμένων + Αποθηκευμένα παιχνιδιού + Εισαγωγή αποθηκευμένων… + Εξαγωγή ως zip GameHub… + Εξαγωγή ως zip Winlator… + Άνοιγμα φακέλου αποθήκευσης + Δεν έχει παιχτεί ποτέ + Το runtime είναι απασχολημένο · %1$d%% + Το runtime είναι απασχολημένο + Το runtime εγκαθίσταται στο πρώτο Play + Υπάρχει ενημέρωση runtime + ● Το runtime είναι έτοιμο + Παίχτηκε τελευταία φορά %1$s + Εσωτερική αποθήκευση + Προστέθηκε παιχνίδι + Προσαρμοσμένο παιχνίδι + Αποθήκευση παιχνιδιών + Το καλύτερο ζεύγος για αυτή τη GPU, ενημερωμένο + Επιλέξτε ένα ζεύγος εσείς + Έλεγχος για προγράμματα οδήγησης… + Δεν υπάρχουν ακόμα προγράμματα οδήγησης. Ανανεώστε για να τα αναζητήσετε. + Απόκρυψη προγραμμάτων οδήγησης για άλλες GPU + Εμφάνιση προγραμμάτων οδήγησης για άλλες GPU + Για προχωρημένους + Ορίζεται μαζί με το πρόγραμμα οδήγησης οθόνης: το %1$s φέρει και τα δύο. + Ορίζεται μαζί με το πρόγραμμα οδήγησης runtime: το %1$s φέρει και τα δύο. + Χρησιμοποιείται από παιχνίδια και την επιφάνεια εργασίας. + Προεπιλογή runtime + Σχεδιάζει καρέ στην οθόνη. Εφαρμόζεται μετά την επανεκκίνηση του DroidDeck. + Αυτόματο - επιλέγεται από τη GPU + Λήψη, εισαγωγή ή διαγραφή… + ΣΕ ΧΡΗΣΗ + Τα προγράμματα οδήγησης ενσωματωμένα στο DroidDeck + Τα προγράμματα οδήγησης runtime και οθόνης ορίζονται μαζί ως αντιστοιχισμένο ζεύγος. + Προτείνεται για αυτή τη GPU: %1$s. + Ανανεώστε για να αναζητήσετε προγράμματα οδήγησης για αυτή τη GPU. + ΠΡΟΤΕΙΝΕΤΑΙ + Μη διαθέσιμο + Runtime Linux + Προετοιμασία του runtime Linux + Συνεδρία + Εκκίνηση οθόνης και ήχου + Steam client + Έλεγχος του Steam και των εργαλείων συμβατότητάς του + Άνοιγμα Steam + Αναμονή για το πρώτο καρέ του Steam + Επιφάνεια εργασίας + Προετοιμασία της επιφάνειας εργασίας + Άνοιγμα + Αναμονή για το πρώτο καρέ + Εκκίνηση του Steam + Η συνεδρία έληξε + Βήμα %1$d από %2$d + Το Steam είναι σε παύση + Λαβές + Αριστερή επιφάνεια + Δεξιά επιφάνεια + Και τα δύο + Χειριστήριο + Μοχλός + Πινακίδα αφής + Εναλλαγή μεταξύ μοχλού και πινακίδας αφής + Περισσότερα χειριστήρια Deck + ΠΙΝΑΚΕΣ + Κλείσιμο χειριστηρίων δεύτερης οθόνης + Άνοιγμα μενού Steam + Αριστερά · πάνω + Αριστερά · κάτω + Δεξιά · πάνω + Δεξιά · κάτω + Κλικ + Δεξιά πινακίδα αφής + Αριστερή πινακίδα αφής + Πινακίδα αφής + Εμφάνιση πληκτρολογίου + Απόκρυψη πληκτρολογίου + Τερματικό Linux + Η συνεδρία Linux δεν είναι ακόμα έτοιμη για τερματικό. + Steam + Παιχνίδια + Επιφάνεια + Κατάστημα + Στοιχεία + Εφαρμογές + Ρύθμιση + Ενημερώσεις + %1$d%% + Συνέχιση %1$s + Εμφανίζεται ως /root/ROMs σε κάθε συνεδρία. Ο εσωτερικός χώρος αποθήκευσης βρίσκεται στο /root/Storage. + Δεν έχει επιλεγεί φάκελος + Ισχύει από την επόμενη συνεδρία. Οι κάρτες SD λειτουργούν. + Επιλογή φακέλου + Αλλαγή φακέλου + Εγκαταστήστε μια έκδοση Proton για ARM64, και επιλέξτε την ανά παιχνίδι από το Steam → Ιδιότητες → Συμβατότητα. + Διακόψτε την τρέχουσα συνεδρία για να εγκαταστήσετε ή να καταργήσετε ένα εργαλείο συμβατότητας. + Διαθέσιμες εκδόσεις + Εγκατεστημένο %1$s + Σε εκκρεμότητα από προηγούμενο αίτημα + Δεν είναι εγκατεστημένο + Διαθέσιμη ενημέρωση → %1$s + Κατάργηση παλιών εκδόσεων %1$s + Καταργεί %1$s. Τα παιχνίδια που έχουν ρυθμιστεί σε αυτές τις εκδόσεις πρέπει να αλλάξουν σε άλλο Proton. + Τα παιχνίδια που έχουν ρυθμιστεί σε αυτή την έκδοση πρέπει να αλλάξουν σε άλλο Proton. + Εγκατάσταση τώρα + Ακύρωση σε εκκρεμότητα + Πληκτρολόγιο Android + Απόκρυψη ✕ + Εκκίνηση %1$s + Επιλέξτε οθόνη. Δευτερεύουσα: %1$s. + Η δευτερεύουσα οθόνη δεν είναι πλέον διαθέσιμη. + Κύρια οθόνη + Δευτερεύουσα οθόνη + Προηγούμενη καρτέλα + Επόμενη καρτέλα + Ενεργό + Όλα %1$d + Κανένα + %1$d από %2$d + Ανενεργό + Κάτω οθόνη + Ισχύει από την επόμενη εκκίνηση παιχνιδιού. Το προφίλ ενός παιχνιδιού υπερισχύει του κοινού. + Άλλο παιχνίδι με αναγνωριστικό εφαρμογής… + Τέλος + Προεπιλογή + Ανενεργό + Αλλαγή… + Αλλαγή μεταβλητής + Επαναφορά στην προεπιλογή + Παιχνίδια Proton + Εικόνα εντός εικόνας + Αυτόματη είσοδος σε λειτουργία εικόνας εντός εικόνας + Χειριστήρια στην ξεδιπλωμένη οθόνη + Ξεδιπλωμένη, το παιχνίδι βρίσκεται πάνω από τις πινακίδες αφής και τις πίσω λαβές του Steam Deck. Διπλωμένη, το παιχνίδι καταλαμβάνει όλη την οθόνη. + Σε αναστολή + Η λειτουργία εικόνας εντός εικόνας είναι διαθέσιμη μόλις η συνεδρία είναι έτοιμη. + Επιτρέψτε την εικόνα εντός εικόνας για το DroidDeck στις ρυθμίσεις Android. + Το Android δεν μπόρεσε να ανοίξει τη λειτουργία εικόνας εντός εικόνας. + Συντόμευση + Προσθήκη στην αρχική οθόνη + Επιλογή παιχνιδιού + Σάρωση εγκατεστημένων παιχνιδιών… + Αντιγραφή συνδέσμου εκκίνησης + Ο σύνδεσμος εκκίνησης αντιγράφηκε + Αυτό το πρόγραμμα εκκίνησης δεν μπορεί να προσθέσει συντομεύσεις. Χρησιμοποιήστε την Αντιγραφή συνδέσμου εκκίνησης. + Μη έγκυρος σύνδεσμος εκκίνησης παιχνιδιού. + Αυτό το παιχνίδι δεν βρίσκεται πλέον στη βιβλιοθήκη DroidDeck σας. + Διακόψτε την τρέχουσα συνεδρία πριν εκκινήσετε ένα παιχνίδι Steam. + Δεν ήταν δυνατή η αποστολή του παιχνιδιού στο Steam. Δοκιμάστε να το εκκινήσετε ξανά. + Επανάληψη κατάργησης + Κατάργηση + Η κατάργηση δεν ολοκληρώθηκε + Εξαγωγή αρχείου παιχνιδιού + Συγχρονισμός όλων των αρχείων παιχνιδιού σε φάκελο + Διακοπή συγχρονισμού αρχείων παιχνιδιού + Επιλέξτε φάκελο για τα αρχεία παιχνιδιού της διεπαφής + Τα αρχεία παιχνιδιού αποθηκεύτηκαν στο %1$s + Αρχεία διεπαφής + + Περιμένετε να ολοκληρωθεί η λειτουργία του runtime + Περιμένετε να ολοκληρωθεί η εγκατάσταση του εργαλείου συμβατότητας + Περιμένετε να ολοκληρωθεί η εγκατάσταση της εφαρμογής επιφάνειας εργασίας + Περιμένετε να ολοκληρωθεί η εγκατάσταση πριν εκκινήσετε ένα παιχνίδι + Κλείστε πρώτα τη συνεδρία Steam, ώστε το παιχνίδι να μην αποθηκεύσει πάνω από την εισαγωγή + Εισαγωγή στο %1$s + + Έγινε εισαγωγή %1$d αρχείου από το %2$s στο %3$s + Έγινε εισαγωγή %1$d αρχείων από το %2$s στο %3$s + + + Έγινε εισαγωγή %1$d αρχείου από το %2$s στο %3$s. Τα παλιά αρχεία αποθήκευσης κρατήθηκαν ως αντίγραφο ασφαλείας στο Download/DroidDeck/Saves/backups + Έγινε εισαγωγή %1$d αρχείων από το %2$s στο %3$s. Τα παλιά αρχεία αποθήκευσης κρατήθηκαν ως αντίγραφο ασφαλείας στο Download/DroidDeck/Saves/backups + + Επιλέξτε ένα zip αποθηκευμένων αρχείων για το %1$s + Εξαγωγή %1$s + + Έγινε εξαγωγή %1$d αρχείου ως %2$s: %3$s + Έγινε εξαγωγή %1$d αρχείων ως %2$s: %3$s + + Επιλέξτε πού θα αποθηκευτεί το %1$s (%2$s) + Αποτυχία %1$s: %2$s + Δεν υπάρχουν ακόμα αρχεία καταγραφής συνεδρίας: εκτελέστε πρώτα μια συνεδρία. + Δεν υπάρχουν αρχεία καταγραφής συνεδρίας για εκκαθάριση. + + Έγινε εκκαθάριση %1$d φακέλου αρχείων καταγραφής συνεδρίας. + Έγινε εκκαθάριση %1$d φακέλων αρχείων καταγραφής συνεδρίας. + + Η σύζευξη ασύρματου εντοπισμού σφαλμάτων απέτυχε + Η εντολή ασύρματου εντοπισμού σφαλμάτων απέτυχε + Ο ασύρματος εντοπισμός σφαλμάτων δεν είναι διαθέσιμος + Η εντολή ADB αντιγράφηκε + Επιλέξτε τον φάκελο ROMs + Δεν είναι GPU Adreno + Το Turnip υποστηρίζει GPU Adreno. Σε %1$s, το Steam ίσως εμφανίσει μαύρη οθόνη. Λήψη: %2$.0f MB. + Εγκατάσταση παρόλα αυτά + Κατάργηση του Linux runtime + Αυτό διαγράφει το runtime, τον πελάτη Steam μέσα σε αυτό και κάθε παιχνίδι που είναι εγκατεστημένο εκεί. + Εφαρμόστηκε: %1$s + Δεν ήταν δυνατό το άνοιγμα του %1$s στην κύρια οθόνη + Δεν ήταν δυνατό το άνοιγμα του %1$s στο %2$s + Δεν ήταν δυνατό το άνοιγμα του %1$s στην οθόνη %2$d + Εισαγωγή + Εναλλαγή + Επαναφορά + Ακύρωση + Διαγραφή + δεν είναι δυνατή η ανάγνωση του αρχείου + Έγινε εισαγωγή %1$s + Η εκκρεμής εναλλαγή ακυρώθηκε. + Επιλέξτε ένα πακέτο στοιχείων (-linux .wcp) + Δεν ήταν δυνατή η πρόσβαση στα Nightlies + Αποθηκεύτηκε %1$s + Η λήψη απέτυχε: %1$s + Επιλέξτε πρόγραμμα οδήγησης runtime Linux (-Linux zip ή πακέτο Android + Linux) + Επιλέξτε πρόγραμμα οδήγησης οθόνης (AdrenoTools zip ή πακέτο Android + Linux) + Επιλέξτε zip προγράμματος οδήγησης ή πακέτο Android + Linux + %1$s: %2$s + Κατάργηση %1$s… + Διαγραφή εγγραφής %1$s… + Επιλέξτε τον φάκελο αποθήκευσης παιχνιδιών + Επιλέξτε αρχείο ZIP προσθέτου Decky + Δεν είναι χρησιμοποιήσιμο: %1$s + Πρόγραμμα + Κατάργηση του Linux runtime + Δεν ήταν δυνατό το άνοιγμα των Ρυθμίσεων στην κάτω οθόνη· άνοιξαν στην κύρια οθόνη. + Άνοιγμα Επιλογών προγραμματιστή στις Ρυθμίσεις + + Δεν ήταν δυνατό το άνοιγμα της ρύθμισης του Android «Εγκατάσταση άγνωστων εφαρμογών» + Δεν ήταν δυνατός ο έλεγχος: %1$s + Αυτή η έκδοση δεν έχει λήψη για αυτό το αντίγραφο του DroidDeck + Διακόψτε πρώτα την τρέχουσα συνεδρία και μετά ενημερώστε + Λήψη + Εγκατάσταση + Περιμένει να επιβεβαιώσετε + Η ενημέρωση απέτυχε + Το Android δεν πρότεινε την εγκατάσταση της ενημέρωσης + + Η εγκατάσταση του Decky απέτυχε: %1$s + Έναρξη εισαγωγής προσθέτου… + Η εισαγωγή προσθέτου απέτυχε: %1$s + Το πρόσθετο εγκαταστάθηκε. Κάντε επανεκκίνηση του Steam για να φορτωθεί. + + Δεν έχει ελεγχθεί ακόμα - πατήστε ανανέωση για αναζήτηση νέων προγραμμάτων οδήγησης + Android + Linux: ορίζει το πρόγραμμα οδήγησης οθόνης μαζί με αυτό + Android + Linux: ορίζει το πρόγραμμα οδήγησης runtime μαζί με αυτό + το Turnip που είναι ενσωματωμένο στο runtime + Vulkan συστήματος: δεν υπάρχει πακέτο έκδοσης για αυτό το GPU + %1$s (σε πακέτο) + Μη αυτόματο + %1$s · %2$s + Αυτή τη στιγμή έχει δημοσιευτεί μόνο το ένα μισό + %1$.0f MB για τα δύο + Δεν υπάρχουν προγράμματα οδήγησης για ορισμό: %1$s + Έλεγχος για τα πιο πρόσφατα προγράμματα οδήγησης… + Ενημερωμένο (δεν ήταν δυνατός ο έλεγχος: %1$s) + Τα προτεινόμενα προγράμματα οδήγησης δεν είναι δημοσιευμένα αυτή τη στιγμή + Λήψη %1$s %2$s… + Ενημερώθηκε σε %1$s %2$s. + Ενημερώθηκε σε %1$s %2$s. Το πρόγραμμα οδήγησης οθόνης εφαρμόζεται μετά την επανεκκίνηση του DroidDeck. + Έγινε αλλαγή σε %1$s %2$s. + Έγινε αλλαγή σε %1$s %2$s. Το πρόγραμμα οδήγησης οθόνης εφαρμόζεται μετά την επανεκκίνηση του DroidDeck. + Χρήση %1$s %2$s. + Χρήση %1$s %2$s. Το πρόγραμμα οδήγησης οθόνης εφαρμόζεται μετά την επανεκκίνηση του DroidDeck. + Δεν είναι zip προγράμματος οδήγησης: ούτε πρόγραμμα οδήγησης AdrenoTools, ούτε -Linux Turnip, ούτε πακέτο Android + Linux + Η εισαγωγή απέτυχε: %1$s + Έγινε εισαγωγή προγράμματος οδήγησης + Χρήση %1$s ως πρόγραμμα οδήγησης runtime και οθόνης. + Χρήση %1$s ως πρόγραμμα οδήγησης runtime και οθόνης. Το πρόγραμμα οδήγησης οθόνης εφαρμόζεται μετά την επανεκκίνηση του DroidDeck. + Διαγράφηκε %1$s (και τα δύο προγράμματα οδήγησης) + Διαγράφηκε %1$s + Πιο πρόσφατο: %1$s · έλεγχος %2$s + Πιο πρόσφατο: %1$s · %2$s μη προσβάσιμο · έλεγχος %3$s + + %1$d λεπτό πριν + %1$d λεπτά πριν + + + %1$d ώρα πριν + %1$d ώρες πριν + + + %1$d ημέρα πριν + %1$d ημέρες πριν + + Έλεγχος Banners-Turnip, WinNative και DroidDeck… + ο έλεγχος απέτυχε + Εγκατεστημένο %1$s - επιλέξτε το σε οποιαδήποτε λίστα για να ορίσετε και τα δύο προγράμματα οδήγησης + Εγκατεστημένο %1$s - επιλέξτε το στο μενού + + Περιμένετε να ολοκληρωθεί η κατάργηση του runtime + Παιχνίδι + πινακίδα αφής + απευθείας + Η εναλλαγή απέτυχε: %1$s + Η αποθήκευση προφίλ απέτυχε: %1$s + Η επιφάνεια εργασίας είναι σε παύση + %1$s είναι σε παύση + Το πρόγραμμα είναι σε παύση + Εκκίνηση της επιφάνειας εργασίας + Εκκίνηση %1$s + Εκκίνηση του προγράμματος + Δεν υπάρχουν αρχεία καταγραφής για αυτή τη συνεδρία. + Δεν ήταν δυνατή η δημιουργία της αρχειοθήκης καταγραφών συνεδρίας. + Δεν ήταν δυνατή η κοινοποίηση των αρχείων καταγραφής συνεδρίας. + Εκκίνηση της συνεδρίας… + λήψη του Linux runtime + λήψη του Linux runtime · %1$d από %2$d MB + έλεγχος του Linux runtime + αποσυμπίεση του Linux runtime + λήψη της επιφάνειας εργασίας + λήψη της επιφάνειας εργασίας · %1$d από %2$d MB + έλεγχος της επιφάνειας εργασίας + αποσυμπίεση της επιφάνειας εργασίας + λήψη του Proton Experimental (ARM64) + λήψη του Proton Experimental (ARM64) · %1$d από %2$d MB + έλεγχος του Proton Experimental (ARM64) + αποσυμπίεση του Proton Experimental (ARM64) + Δεν ήταν δυνατή η πρόσβαση στον κατάλογο runtime. Ελέγξτε τη σύνδεση και πατήστε ξανά Αναπαραγωγή. + Το Linux runtime δεν εγκαταστάθηκε. Ελέγξτε το runtime στη Ρύθμιση και πατήστε ξανά Αναπαραγωγή. + Η επιφάνεια εργασίας δεν εγκαταστάθηκε (%1$s). Ελέγξτε τη σύνδεση και πατήστε ξανά Επιφάνεια εργασίας. + %1$s διακόπηκε αναπάντεχα. Κοινοποιήστε τα αρχεία καταγραφής με μια αναφορά σφάλματος, ή δοκιμάστε ξανά. + Το πρόγραμμα διακόπηκε αναπάντεχα. Κοινοποιήστε τα αρχεία καταγραφής με μια αναφορά σφάλματος, ή δοκιμάστε ξανά. + Η επιφάνεια εργασίας διακόπηκε αναπάντεχα. Κοινοποιήστε τα αρχεία καταγραφής με μια αναφορά σφάλματος, ή δοκιμάστε ξανά. + Κατάσταση εξόδου %1$d · %2$s + χωρίς αρχείο καταγραφής + + Το αρχείο καταγραφής δείχνει %1$d σφάλμα «Function not implemented»: η επιτάχυνση seccomp του proot αποτυγχάνει σε έναν βοηθητικό μηχανισμό σε αυτή τη συσκευή. Δοκιμάστε %2$s → «%3$s» (ή «%4$s») και ξεκινήστε ξανά. + Το αρχείο καταγραφής δείχνει %1$d σφάλματα «Function not implemented»: η επιτάχυνση seccomp του proot αποτυγχάνει σε έναν βοηθητικό μηχανισμό σε αυτή τη συσκευή. Δοκιμάστε %2$s → «%3$s» (ή «%4$s») και ξεκινήστε ξανά. + + + Πρόσθετα + Εγκαταστήστε ένα ZIP προσθέτου για την επόμενη συνεδρία Steam. + Γίνεται λήψη… + Εγκατάσταση από ZIP + Παιχνίδια + Ήχος & βίντεο + Γραφικά + Διαδίκτυο + Γραφείο + Προγραμματιστής + Εκπαίδευση + Επιστήμη + Σύστημα + Βοηθητικά + Δημοφιλή + Σε άνοδο + Νέα + Ενημερωμένα + γίνεται φόρτωση… + Ρύθμιση Flatpak + %1$s: %2$s + Δεν ήταν δυνατός ο έλεγχος του Flathub για ενημερώσεις + εγκατεστημένο + φάκελος + Γραφίτης + Επιφάνειες από γραφίτη, με το μπλε της σφαίρας του λογότυπου για ό,τι είναι επιλεγμένο, εστιασμένο ή πατημένο. + Χαρτί σε μαύρο + Το λευκό είναι το κύριο χρώμα, το μπλε του εικονιδίου είναι το σήμα για γραμμές, δαχτυλίδια και κουκκίδες. + Μπλε εικονιδίου + Μαύρο φόντο, λευκά γράμματα, το μπλε για ό,τι είναι επιλεγμένο, εστιασμένο ή πατημένο. + Ηλεκτρικό ναυτικό μπλε + Το μπλε πιο έντονο, σε επιφάνειες που τείνουν προς το ναυτικό μπλε αντί για γκρι. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Εγκαταστήστε πρώτα το runtime + Η βιβλιοθήκη Steam δεν είναι διαθέσιμη + Η προηγούμενη βιβλιοθήκη SD δεν είναι διαθέσιμη + Ο χώρος αποθήκευσης παιχνιδιών δεν είναι διαθέσιμος + Ο φάκελος προστιθέμενων παιχνιδιών δεν είναι διαθέσιμος + Τα manifest του Steam δεν είναι διαθέσιμα + Πολλά συστήματα + Εσωτερική + Σύστημα Linux + Κάρτα SD + Αποθηκευτικός χώρος + Επιλογή + Ιδιότητες + Προσθήκη στα αγαπημένα + Κατάργηση από τα αγαπημένα + Μετονομασία + Αντιγραφή + Αποκοπή + Ενέργειες + Αγαπημένα + Καρφίτσωμα τρέχοντος φακέλου + Δεν υπάρχουν ακόμα αγαπημένα - καρφιτσώστε έναν φάκελο με το μενού ⋮ του για να έρχεστε εδώ γρήγορα. + Κατάργηση από τα αγαπημένα + 0 bytes + bytes + Τοποθεσία + Τύπος + Μέγεθος + Τροποποιήθηκε + Φάκελος + Αρχείο + Αρχείο %1$s + Μόνο για ανάγνωση + Αποτρέπει τα παιχνίδια (και το Wine) από την αντικατάσταση ή διαγραφή αυτού του αρχείου. + Δεν ήταν δυνατή η αλλαγή του Μόνο για ανάγνωση + Κρυφό + Αυτός ο αποθηκευτικός χώρος δεν μπορεί να αποθηκεύσει τη σημαία απόκρυψης. + Σημαδεύει το αρχείο ως κρυφό στα Windows (η ιδιότητα κρυφού του Wine). + Δεν ήταν δυνατή η αλλαγή του Κρυφό σε αυτόν τον αποθηκευτικό χώρο + Τέλος + Γίνεται διαγραφή... + Η διαγραφή απέτυχε + Γίνεται μετακίνηση... + Γίνεται αντιγραφή... + Μετακίνηση - %1$s + Αντιγραφή - %1$s + Μετακίνηση %1$d/%2$d - %3$s + Αντιγραφή %1$d/%2$d - %3$s + %1$d ολοκληρώθηκαν, %2$d απέτυχαν + %1$d ολοκληρώθηκαν, %2$d παραλείφθηκαν + + %1$d στοιχείο μετακινήθηκε + %1$d στοιχεία μετακινήθηκαν + + + %1$d στοιχείο αντιγράφηκε + %1$d στοιχεία αντιγράφηκαν + + Το «%1$s» υπάρχει ήδη + Γίνεται μετονομασία... + Η μετονομασία απέτυχε + Γίνεται δημιουργία φακέλου... + Δεν ήταν δυνατή η δημιουργία φακέλου + Νέος φάκελος + Όνομα φακέλου + Δημιουργία + Νέο όνομα + Διαγραφή; + Διαγραφή του «%1$s» οριστικά; + + Διαγραφή %1$d στοιχείου; + Διαγραφή %1$d στοιχείων; + + Αυτό δεν μπορεί να αναιρεθεί. + + …και %1$d ακόμα + …και %1$d ακόμα + + Διαγραφή %1$d/%2$d - %3$s + + %1$d δεν ήταν δυνατό να διαγραφεί + %1$d δεν ήταν δυνατό να διαγραφούν + + Η συγχώνευση προσθέτει και αντικαθιστά αρχεία μέσα στον υπάρχοντα φάκελο. + Η αντικατάσταση αντικαθιστά το υπάρχον αρχείο. + Εφαρμογή σε όλες τις συγκρούσεις + Συγχώνευση + Αντικατάσταση + Διατήρηση και των δύο + Παράλειψη + Επιλογή αυτού του φακέλου + Πίσω + Το %1$s είναι προσαρτημένο αλλά δεν είναι αναγνώσιμο αυτή τη στιγμή + Προβολή λίστας + Προβολή πλέγματος + Αναζήτηση + Ταξινόμηση + Όνομα + Ημερομηνία τροποποίησης + Άνετες γραμμές + Συμπαγείς γραμμές + Απόκρυψη κρυφών αρχείων + Εμφάνιση κρυφών αρχείων + Απόκρυψη αγαπημένων + Εμφάνιση αγαπημένων + Φιλτράρισμα αυτού του φακέλου + %1$s ελεύθερα + %1$d επιλεγμένα + Κανένα + Όλα + Επικόλληση + + %1$d στοιχείο + %1$d στοιχεία + + Επικόλληση %1$s εδώ + Επικόλληση %1$s (μετακίνηση) εδώ + Αρχεία + ΑΠΟΘΗΚΕΥΤΙΚΟΣ ΧΩΡΟΣ + ΓΡΗΓΟΡΑ + ΑΓΑΠΗΜΕΝΑ + Λήψεις + Λήψεις + ROMs + Καταγραφές συνεδρίας + Εικόνες + Το «%1$s» προστέθηκε στα αγαπημένα + Το «%1$s» καταργήθηκε από τα αγαπημένα + Άδειος κατάλογος + Ένα επίπεδο πάνω + Επιλέξτε φάκελο + Επιλέξτε αρχείο + Μπλε Steam + Γαλάζιο + Λευκό + Βιολετί + Πράσινο + Κεχριμπαρί + Κόκκινο + Ροζ + Κουμπί A + Κουμπί B + Κουμπί X + Κουμπί Y + Αριστερό bumper + Δεξί bumper + Αριστερή σκανδάλη + Δεξιά σκανδάλη + Κουμπί View + Κουμπί Menu + View + Menu + D-pad πάνω + D-pad κάτω + D-pad αριστερά + D-pad δεξιά + Κρυφό + Σύρετε για μετακίνηση + Προσκόλληση + Αποθήκευση + Κανένα + Πληκτρολόγιο + πινακίδα αφής + Τερματικό + Λαβές Deck + πινακίδες αφής + + Κατάργηση του Linux runtime + + Κατάργηση του Linux runtime · %1$d εγγραφή + Κατάργηση του Linux runtime · %1$d εγγραφές + + Το Linux runtime καταργήθηκε + Δεν ήταν δυνατή η ολοκλήρωση της κατάργησης του Linux runtime. Επαναλάβετε την κατάργηση στη Ρύθμιση. + Κατάργηση υπολειμμάτων του runtime + Γίνεται λήψη + Γίνεται επαλήθευση + Γίνεται εξαγωγή + Εμφανίζεται όσο το Linux runtime εγκαθίσταται ή καταργείται + Εγκατάσταση του Linux runtime + Κατάργηση του Linux runtime + + Το Linux runtime δεν είναι εγκατεστημένο + Ο κατάλογος δεν έχει checksum για το %1$s + Αναντιστοιχία checksum - τίποτα δεν άλλαξε + Εγκατάσταση %1$s + Δεν ήταν δυνατή η τοποθέτηση του AppImage + Η εξαγωγή απέτυχε + Η εγκατάσταση απέτυχε + + Η ρύθμιση του Flatpak απέτυχε (έξοδος %1$d) + Η ρύθμιση του Flatpak δεν ολοκληρώθηκε + Μια άλλη εργασία καταστήματος εκτελείται ήδη + Ρυθμίστε πρώτα το Flatpak + Γίνεται εκκίνηση + Το Flatpak έκλεισε με κατάσταση %1$d + Εγκατάσταση %1$s + Ενημέρωση %1$s + Κατάργηση %1$s + Εγκατάσταση %1$s (%2$d/%3$d) + Ενημέρωση %1$s (%2$d/%3$d) + Κατάργηση %1$s (%2$d/%3$d) + μεταφράσεις + προγράμματα οδήγησης γραφικών + codecs πολυμέσων + runtime %1$s + runtime %1$s %2$s + + Το αρχείο είναι πολύ μικρό για να είναι AppImage + Το αρχείο δεν μπορεί να αναγνωστεί: %1$s + Αυτό δεν είναι AppImage (δεν είναι πρόγραμμα Linux) + Αυτό το AppImage δεν είναι χτισμένο ούτε για ARM64 (aarch64) ούτε για PC x86 + Αυτό είναι πρόγραμμα Linux αλλά όχι AppImage + Μόνο AppImage τύπου 2 μπορούν να εισαχθούν (αυτό είναι τύπου %1$d) + Δεν υπάρχει αρκετός ελεύθερος χώρος: η εισαγωγή απαιτεί περίπου %1$s + Δεν υπάρχει αρκετός ελεύθερος χώρος: η ενημέρωση απαιτεί περίπου %1$s + Η εισαγωγή απέτυχε + Αυτό το AppImage δεν υπάρχει πλέον + Η ενημέρωση απέτυχε + Εξαγωγή %1$s + Δεν ήταν δυνατή η εξαγωγή του AppImage + Δεν ήταν δυνατή η εξαγωγή του AppImage: %1$s + + Διακόψτε την ενεργή συνεδρία πριν εγκαταστήσετε το Decky Loader + Η λήψη του Decky Loader απέτυχε + Το μέγεθος του Decky Loader δεν ταίριαζε με τα μεταδεδομένα της κυκλοφορίας + Δεν ήταν δυνατή η λήψη του αθροίσματος ελέγχου της κυκλοφορίας + Λείπει το άθροισμα ελέγχου της κυκλοφορίας + Το άθροισμα ελέγχου της κυκλοφορίας δεν είναι έγκυρο + Αναντιστοιχία αθροίσματος ελέγχου του Decky Loader + Το αρχείο της κυκλοφορίας δεν είναι συμβατό εκτελέσιμο Linux 64-bit + Μια συνεδρία ξεκίνησε κατά τη λήψη· διακόψτε την πριν εγκαταστήσετε το Decky Loader + Δεν ήταν δυνατό να οριστεί το PluginLoader ως εκτελέσιμο + Δεν ήταν δυνατή η ενεργοποίηση του απομακρυσμένου εντοπισμού σφαλμάτων CEF του Steam + Δεν ήταν δυνατή η προετοιμασία του PluginLoader + Δεν ήταν δυνατή η διατήρηση του υπάρχοντος PluginLoader + Δεν ήταν δυνατή η ενεργοποίηση του νέου PluginLoader + Διακόψτε την ενεργή συνεδρία πριν εγκαταστήσετε ένα πρόσθετο + Εγκαταστήστε το Decky Loader πριν εισαγάγετε ένα πρόσθετο + Δεν ήταν δυνατή η ανάγνωση του επιλεγμένου αρχείου ZIP + Δεν ήταν δυνατή η δημιουργία του καταλόγου προσθέτων Decky + Εξαγωγή ZIP προσθέτου + Το ZIP προσθέτου πρέπει να περιέχει έναν φάκελο προσθέτου στο ανώτερο επίπεδο + Από το ZIP προσθέτου λείπουν τα plugin.json, package.json ή dist/index.js + Λείπει το όνομα του προσθέτου + Μια συνεδρία ξεκίνησε κατά την εγκατάσταση· διακόψτε την πριν εγκαταστήσετε ένα πρόσθετο + Το πρόσθετο εγκαταστάθηκε · κάντε επανεκκίνηση του Steam για να φορτωθεί + Η εγκατάσταση του ZIP προσθέτου απέτυχε + Το ZIP προσθέτου περιέχει πολύ μεγάλο αριθμό εγγραφών + Το ZIP προσθέτου περιέχει μη έγκυρη διαδρομή + Το ZIP προσθέτου περιέχει διπλές διαδρομές + Το ZIP προσθέτου περιέχει μη ασφαλή διαδρομή + Δεν ήταν δυνατή η δημιουργία φακέλου προσθέτου + Το ZIP προσθέτου επεκτείνεται πέρα από το όριο του 1 GiB + Δεν ήταν δυνατό να οριστεί το ενσωματωμένο εκτελέσιμο του προσθέτου ως εκτελέσιμο + Μη έγκυρη εγγραφή remote_binary + Μη έγκυρο όνομα απομακρυσμένου εκτελέσιμου + Το URL του απομακρυσμένου εκτελέσιμου πρέπει να χρησιμοποιεί HTTPS + Το απομακρυσμένο εκτελέσιμο %1$s έχει μη έγκυρο κατακερματισμό SHA-256 + Δεν ήταν δυνατή η δημιουργία καταλόγου bin του προσθέτου + Λήψη εκτελέσιμου προσθέτου · %1$s + Δεν ήταν δυνατή η λήψη του εκτελέσιμου προσθέτου %1$s + Αναντιστοιχία αθροίσματος ελέγχου εκτελέσιμου προσθέτου · %1$s + Δεν ήταν δυνατό να οριστεί το εκτελέσιμο του προσθέτου ως εκτελέσιμο · %1$s + Δεν ήταν δυνατή η αντικατάσταση του εκτελέσιμου του προσθέτου · %1$s + Δεν ήταν δυνατή η εγκατάσταση του εκτελέσιμου του προσθέτου · %1$s + Ο φάκελος προσθέτου ανήκει σε άλλο πρόσθετο + Δεν ήταν δυνατή η διατήρηση του υπάρχοντος προσθέτου + Δεν ήταν δυνατή η ενεργοποίηση του εισαγόμενου προσθέτου + Δεν ήταν δυνατή η ενημέρωση των ρυθμίσεων προσθέτων Decky + Δεν ήταν δυνατή η επαναφορά του υπάρχοντος προσθέτου + + Δεν ήταν δυνατή η αποθήκευση της ταυτότητας ασύρματου εντοπισμού σφαλμάτων + Η εντολή ADB απέτυχε (%1$d) + Το Android δεν επιβεβαίωσε ότι το όριο θυγατρικών διαδικασιών άλλαξε: %1$s + Δεν υπάρχει αποθηκευμένη σύζευξη ασύρματου εντοπισμού σφαλμάτων + Εισαγάγετε την τρέχουσα διεύθυνση IP & θύρα του ασύρματου εντοπισμού σφαλμάτων + + Λήξη χρόνου αναμονής για το αναδυόμενο παράθυρο σύζευξης. Δοκιμάστε ξανά από το DroidDeck. + Δεν ήταν δυνατή η αναζήτηση του αναδυόμενου παραθύρου σύζευξης (%1$d). Ελέγξτε ότι το Wi-Fi είναι ενεργό. + Δεν ήταν δυνατή η αναζήτηση του αναδυόμενου παραθύρου σύζευξης: %1$s + Εισαγάγετε όλα τα 6 ψηφία του κωδικού + Γίνεται σύζευξη… + Αυτός ο κωδικός δεν λειτούργησε. Ανοίξτε ξανά το αναδυόμενο παράθυρο σύζευξης και εισαγάγετε τον νέο κωδικό του. + Έγινε σύζευξη. Γίνεται σύνδεση… + Έγινε σύζευξη, αλλά δεν βρέθηκε η θύρα του ασύρματου εντοπισμού σφαλμάτων. Κρατήστε τον ασύρματο εντοπισμό σφαλμάτων ενεργό και δοκιμάστε ξανά. + Εφαρμογή της ρύθμισης… + Η εντολή ασύρματου εντοπισμού σφαλμάτων απέτυχε + Ρύθμιση ασύρματου εντοπισμού σφαλμάτων + Ζητά τον κωδικό σύζευξης ασύρματου εντοπισμού σφαλμάτων όσο οι Ρυθμίσεις είναι ανοιχτές + Σύζευξη ασύρματου εντοπισμού σφαλμάτων + Ενεργοποιήστε τον ασύρματο εντοπισμό σφαλμάτων και πατήστε «Σύζευξη συσκευής με κωδικό σύζευξης». + Ενεργοποιήστε τον ασύρματο εντοπισμό σφαλμάτων και πατήστε «Σύζευξη συσκευής με κωδικό σύζευξης». Παραμείνετε στις Ρυθμίσεις: ο κωδικός εμφανίζεται σε αυτή την ειδοποίηση. + Εξαψήφιος κωδικός σύζευξης + Πατήστε Εισαγωγή κωδικού και πληκτρολογήστε τα 6 ψηφία που εμφανίζονται στις Ρυθμίσεις. + Εισαγάγετε τον κωδικό σύζευξης Wi-Fi + Δοκιμάστε ξανά τον κωδικό + Εισαγωγή κωδικού + Ρύθμιση του Steam + Το Steam είναι έτοιμο + Πατήστε για να επιστρέψετε στο DroidDeck. Μπορείτε να απενεργοποιήσετε τον ασύρματο εντοπισμό σφαλμάτων τώρα. + Η ρύθμιση ασύρματου εντοπισμού σφαλμάτων διακόπηκε + + Η λήψη απάντησε με HTTP %1$d + Το μέγεθος της λήψης δεν ταίριαζε με το δημοσιευμένο APK και απορρίφθηκε - δοκιμάστε ξανά + Η λήψη δεν ταίριαζε με το άθροισμα ελέγχου της και απορρίφθηκε - δοκιμάστε ξανά + Η ενημέρωση είναι για το πακέτο Android %1$s, όχι για το %2$s + Τα μεταδεδομένα της ενημέρωσης δεν αναφέρουν το κλειδί υπογραφής κυκλοφορίας του DroidDeck + Το Android δεν μπόρεσε να διαβάσει το ληφθέν APK + Το ληφθέν APK είναι για το %1$s, όχι για το %2$s + Το ληφθέν APK αναφέρει versionCode %1$d, όχι το δημοσιευμένο %2$d + Το Android δεν θα εγκαταστήσει το versionCode %1$d πάνω από το εγκατεστημένο %2$d + Το ληφθέν APK δεν έχει πληροφορίες υπογραφής + Το ληφθέν APK δεν είναι υπογεγραμμένο με το κλειδί κυκλοφορίας του DroidDeck + Ο κατάλογος ενημερώσεων απάντησε με HTTP %1$d + + Εκκίνηση της συνεδρίας… + %1$d:%2$02d πέρασαν · ακόμα σε εξέλιξη + Λήψη της ενημέρωσης του πελάτη Steam · %1$d%% + Λήψη του πελάτη Steam · %1$s + + Μια εγκατάσταση εργαλείου συμβατότητας εκτελείται ήδη + Διακόψτε την ενεργή συνεδρία πριν εγκαταστήσετε εργαλεία συμβατότητας + Δεν βρέθηκε κυκλοφορία ARM64 για το %1$s + Απαιτούνται τουλάχιστον 4 GB ελεύθερα συν το μέγεθος της λήψης για να εγκατασταθεί το %1$s + Λήψη %1$s %2$s + Επανάληψη λήψης · προσπάθεια %1$d από %2$d + Η λήψη διακόπηκε + Η λήψη απέτυχε· το μερικό αρχείο διατηρείται ώστε μια νέα προσπάθεια να μπορεί να συνεχίσει + Η λήψη υπερέβη το μέγεθος της κυκλοφορίας και διαγράφηκε + Η λήψη ήταν ημιτελής· το μερικό αρχείο διατηρείται για επανάληψη με συνέχιση + Επαλήθευση λήψης + Δεν ήταν δυνατή η ανάγνωση του αθροίσματος ελέγχου της κυκλοφορίας· δοκιμάστε ξανά + Το %1$s %2$s δεν δημοσιεύει άθροισμα ελέγχου· δεν εγκαταστάθηκε τίποτα + Αναντιστοιχία αθροίσματος ελέγχου· η λήψη διαγράφηκε + Μια συνεδρία ξεκίνησε κατά τη λήψη· διακόψτε την πριν εγκαταστήσετε εργαλεία συμβατότητας + Εγκατάσταση %1$s + Η εγκατάσταση του %1$s απέτυχε (έξοδος %2$d) + Δεν ήταν δυνατή η εγκατάσταση του %1$s + Λήψη πακέτου droiddeck-esync + + με βοηθητικά Linux + Μόνο DLL + Αρχικό (το Proton ενημερώθηκε από την τελευταία εναλλαγή) + Άλλαξε εκτός Στοιχείων + δεν υπάρχει + Αρχικό %1$s + Αρχικό από %1$s + Το %1$s %2$s θα μπει στο %3$s όταν κλείσει το παιχνίδι που εκτελείται σε αυτό. + Το %1$s %2$s βρίσκεται τώρα στο %3$s. Εφαρμόζεται την επόμενη φορά που θα ξεκινήσει ένα παιχνίδι. + Αυτό το αρχικό πακέτο δεν είναι αποθηκευμένο + Το %1$s του %2$s θα επανέλθει στο αρχικό του όταν κλείσει το παιχνίδι που εκτελείται σε αυτό. + Το %1$s του %2$s επανήλθε από το αρχικό του %3$s. + Το %1$s χρησιμοποιείται στο %2$s. Αλλάξτε πρώτα αυτό το Proton σε κάτι άλλο. + Το %1$s δεν είναι αποθηκευμένο + Διαγράφηκε %1$s. + Το αρχικό της εγκατεστημένης έκδοσης διατηρείται: είναι ο δρόμος της επιστροφής. + Δεν είναι αποθηκευμένο + Διαγράφηκε το αρχικό %1$s του %2$s. + Οι λήψεις προέρχονται μόνο από τις κυκλοφορίες Nightlies + Αυτή η λίστα πακέτων προηγείται των αθροισμάτων ελέγχου - ανανεώστε την και δοκιμάστε ξανά + Αναντιστοιχία αθροίσματος ελέγχου + + Γραμμικό + Πλησιέστερο + Snapdragon GSR (ποιότητα) + Μόνο όξυνση + Αυτόματο (αντιστοιχία κλιμάκωσης) + + Δεν εφαρμόζεται τίποτα. + Σαφήνεια παιχνιδιού + Όξυνση με μια πινελιά αντίθεσης - το καθημερινό. + Έντονο + Έντονα ενισχυμένο χρώμα. Το Deband ενεργό ώστε οι ουρανοί να μη γραμμώνονται. + Κινηματογραφικό + Βαθύτερη αντίθεση, πιο ψυχρό χρώμα, ομαλές ακμές. + Ανταγωνιστικό + Ανασηκωμένες σκιές και έντονη όξυνση - εντοπίστε πρώτα τους παίκτες. + Προσαρμοστική όξυνση + Μόνο όξυνση, το χρώμα παραμένει ανεπηρέαστο. + Φιλμικό + Καμπύλη σε στιλ φιλμ, ελαφρώς απαλή. + Arcade + Έντονο και δυναμικό. + Ρετρό CRT + Ο σωλήνας CRT και τα αναλογικά περάσματα σήματος μαζί. + Έντονη μεγέθυνση + Για λειτουργία κάτω από την ανάλυση της οθόνης. + Καθαρά Pixel + Ευκρινή pixel για 2D και παλιά παιχνίδια. + Περίγραμμα Anime + Περιγράμματα τύπου toon συν όξυνση. + + Δεν είναι GPU Adreno + Υποστηρίζεται + Πειραματικό: τα προγράμματα οδήγησής του είναι δοκιμαστικές εκδόσεις + Κάτω από το δοκιμασμένο υλικό (Adreno 725 και νεότερα): ίσως να μην εκτελεστεί + Δεν υποστηρίζεται: το DroidDeck χρειάζεται GPU Adreno (Snapdragon) + + Το όριο ρυθμού του GitHub ξεπεράστηκε - δοκιμάστε ξανά αργότερα + Το GitHub απάντησε με HTTP %1$d + η λήψη απάντησε με HTTP %1$d + Αυτή η λίστα προγραμμάτων οδήγησης προηγείται των αθροισμάτων ελέγχου - ανανεώστε την και δοκιμάστε ξανά + Αναντιστοιχία αθροίσματος ελέγχου - η λήψη απορρίφθηκε + Δεν είναι πρόγραμμα οδήγησης AdrenoTools: %1$s + χωρίς meta.json + Το meta.json δεν είναι αναγνώσιμο (%1$s) + αυτό είναι πρόγραμμα οδήγησης runtime Linux (εισαγάγετέ το από το «Πρόγραμμα οδήγησης runtime Linux») + αυτό είναι πρόγραμμα οδήγησης παιχνιδιών Wayland, το οποίο αυτή η εφαρμογή δεν χρησιμοποιεί + Το meta.json δεν ονομάζει κανένα libraryName, οπότε δεν ήταν δυνατό να δοθεί τίποτα στο AdrenoTools + Το meta.json ονομάζει το %1$s, το οποίο δεν βρίσκεται στο zip + Το %1$s δεν είναι κοινόχρηστη βιβλιοθήκη ELF 64-bit AArch64 + Το %1$s συνδέεται με glibc - ένα Turnip «-Linux», που ανήκει στο «Πρόγραμμα οδήγησης runtime Linux» + Δεν υπάρχει libvulkan_freedreno*.so σε αυτό το zip. Ένα zip Android (AdrenoTools) ή -Wayland Turnip δεν είναι πρόγραμμα οδήγησης runtime Linux. + Το %1$s δεν είναι κοινόχρηστη βιβλιοθήκη ELF 64-bit AArch64. + Το %1$s δεν είναι πρόγραμμα οδήγησης glibc - συνδέεται με τη libc του Android. Το Linux runtime χρειάζεται ένα zip «-Linux»· ένα απλό ή «-Wayland» Turnip δεν μπορεί να φορτωθεί καθόλου από τον πελάτη Steam. + Δεν είναι πακέτο προγραμμάτων οδήγησης: δεν υπάρχει manifest.json που να ονομάζει πρόγραμμα οδήγησης android και linux + Δεν είναι πακέτο προγραμμάτων οδήγησης: το %1$s δείχνει έξω από αυτό + Δεν είναι πακέτο προγραμμάτων οδήγησης: αποσυμπιέζεται σε περισσότερα από %1$d MB + + Κάρτα SD + Φάκελος + + Κοινοποίηση αρχείων καταγραφής συνεδρίας + + απαιτεί Android 10 ή νεότερο (επίπεδα οθόνης με χρωματικό χώρο) + αυτή η οθόνη δεν αναφέρει υποστήριξη HDR + αυτή η οθόνη υποστηρίζει %1$s, όχι HDR10 + + αυτό το GPU + Το όριο θυγατρικών διαδικασιών είναι ενεργό + Το όριο θυγατρικών διαδικασιών δεν έχει οριστεί + Δεν ήταν δυνατός ο έλεγχος του ορίου θυγατρικών διαδικασιών + Το όριο θυγατρικών διαδικασιών είναι ανενεργό + Το όριο θυγατρικών διαδικασιών δεν απαιτείται + %1$s Αν αυτό δεν είναι δυνατό, συνδέστε αυτή τη συσκευή σε έναν υπολογιστή με ADB και εκτελέστε την παρακάτω εντολή. Επιστρέψτε εδώ και ελέγξτε ξανά. + Οι συνεδρίες Steam μπορούν να ξεκινήσουν. + Αυτή η έκδοση Android δεν χρησιμοποιεί αυτό το όριο. + Ενεργοποιήστε την «Απενεργοποίηση περιορισμών θυγατρικών διαδικασιών» στις Επιλογές προγραμματιστή και αφήστε τις Επιλογές προγραμματιστή ενεργές μετά. + Αυτή η έκδοση Android δεν έχει διακόπτη για αυτό. Χρησιμοποιήστε τον ασύρματο εντοπισμό σφαλμάτων για να το αλλάξετε από αυτή τη συσκευή. + Αυτή η έκδοση δεν έχει λήψη για αυτό το αντίγραφο του DroidDeck. + Το Android δεν θα το εγκαταστήσει πάνω από αυτήν την έκδοση επειδή το versionCode %1$d είναι μικρότερο από το εγκατεστημένο %2$d. + Το Android δεν θα εγκαταστήσει μια παλιότερη έκδοση πάνω από μια νεότερη. Παραμείνετε σε αυτή την έκδοση μέχρι να την προφτάσει το κανάλι. + Αυτό το αντίγραφο του DroidDeck είναι υπογεγραμμένο διαφορετικά (τοπική έκδοση;), οπότε το Android δεν θα το ενημερώσει επί τόπου. + Δεν υπάρχει αρκετός ελεύθερος χώρος για την ενημέρωση. + Η ενημέρωση ακυρώθηκε. + Η ενημέρωση δεν εγκαταστάθηκε: %1$s + Η ενημέρωση δεν εγκαταστάθηκε. + zip %1$s + zip + Το %1$s δεν έχει εκκινηθεί ακόμα + Δεν βρέθηκαν αρχεία αποθήκευσης για το %1$s + Το %1$s δεν έχει φάκελο drive_c, οπότε δεν είναι zip αποθήκευσης GameHub ή Winlator + δεν είναι δυνατή η δημιουργία φακέλων στο %1$s + δεν είναι δυνατή η εγγραφή στο %1$s + δεν είναι δυνατή η εγγραφή στο %1$s (%2$s) + Ο φάκελος δεν είναι διαθέσιμος + Ένα διαφορετικό αρχείο χρησιμοποιεί ήδη το %1$s + Ο φάκελος γραφικών δεν είναι διαθέσιμος + Ο φάκελος έχει άσχετο ευρετήριο συγχρονισμού + Δεν ήταν δυνατή η κατάργηση του %1$s + Το αρχείο βγαίνει εκτός του φακέλου εξαγωγής + FEX (έκδοση χωρίς όνομα έκδοσης) + χωρίς profile.json στο %1$s + Το %1$s δεν είναι πακέτο στοιχείου Linux (τύπος %2$s) + %1$s · %2$s ελεύθερα + Αυτόματο + Ο κατάλογος ενημερώσεων απέτυχε στους ελέγχους του (%1$s) + Ρύθμιση θυγατρικών διαδικασιών DroidDeck + Συνδεδεμένος · λειτουργία εκτός σύνδεσης + Συνδεδεμένος + ΣΕ ΠΑΚΕΤΟ + ΛΗΦΘΗΚΕ + ΕΙΣΗΧΘΗΚΕ + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Οθόνη συνεδρίας + Ανάλυση + Προσαρμογή παιχνιδιού 16:9 + Πώς προσαρμόζονται τα παιχνίδια 16:9 σε αυτή την οθόνη. Από την επόμενη συνεδρία. + Επιλέξτε Αντιστοίχιση οθόνης ή ένα προσαρμοσμένο μέγεθος πιο στενό από 16:9. + Διατήρηση αναλογιών + Διάταση στην οθόνη + Κλιμάκωση εικόνας + Φίλτρο κλιμάκωσης + Κοινό για το Steam και την Επιφάνεια εργασίας. + Εφαρμόζεται σε πραγματικό χρόνο. Κοινό για το Steam και την Επιφάνεια εργασίας. + Τα FSR, GSR και NIS εφαρμόζονται κατά τη μεγέθυνση της εικόνας συνεδρίας. Το Μόνο όξυνση λειτουργεί και χωρίς μεγέθυνση. + Οξύτητα φίλτρου + Καρέ ανά δευτερόλεπτο + %1$s Ορίζει την κλιμάκωση σε %2$s. + Αντιστοίχιση οθόνης (%1$d×%2$d) + Ακολούθηση οθόνης (αλλάζει μέγεθος όταν διπλώνει ή ξεδιπλώνει) + Steam + Εμφάνιση στοιχείων συσκευής + Απόκρυψη στοιχείων συσκευής + Ανάγνωση της συσκευής… + Αντιγραφή + Αντιγράψτε αυτά τα στοιχεία σε μια αναφορά σφάλματος. + Αντιγράφηκε. + Εξαναγκασμός SSBS + Διατηρεί μια ρύθμιση ταχύτητας CPU που το Wine απενεργοποιεί συνεχώς. Μπορεί να κάνει τα παιχνίδια Windows πιο γρήγορα σε Snapdragon 8 Elite· μικρή διαφορά σε παλαιότερα chip. Ισχύει από την επόμενη εκκίνηση παιχνιδιού. + + Καταστήματα + Steam + Προσαρμοσμένο + Καταστήματα + Άνοιγμα καταστήματος σε + Επίπεδο ταχύτητας λήψης + Μηχανή καταστημάτων + ● Η μηχανή είναι έτοιμη + Δεν έχει χτιστεί + Ισορροπημένο + Ταχύ + Μέγιστο + Καταστήματα + Ρυθμίσεις καταστημάτων + Ρυθμίσεις + (μη συνδεδεμένος) + Κατάστημα + Εγκατεστημένα + Βιβλιοθήκη + Όλα + Λήψεις + %1$s (%2$d) + Σύνδεση + %1$s · αποσύνδεση + Αποσύνδεση από %1$s; + Τα εγκατεστημένα παιχνίδια παραμένουν. Η βιβλιοθήκη ανακτάται ξανά όταν συνδεθείτε πάλι. + Παραμονή συνδεδεμένου + Αποσύνδεση + Αναζήτηση στον κατάλογο %1$s… + Αναζήτηση εγκατεστημένων παιχνιδιών… + Αναζήτηση στη βιβλιοθήκη σας… + Αναζήτηση + Τίποτα εγκατεστημένο + Φόρτωση + Δεν υπάρχουν παιχνίδια + Φόρτωση του καταστήματος… + Δεν υπάρχει κατάλογος + Τι νέο υπάρχει + Προσφορές + Δωρεάν παιχνίδια + Σε άνοδο + Η βιβλιοθήκη σας + Αναπαραγωγή + Εγκατάσταση + Συνέχιση εγκατάστασης + Διακόπηκε + Εγκατάσταση · %1$s + Απόκτηση δωρεάν + Προβολή στο %1$s + Αγορά · %1$s + Εγκατεστημένο + Στη βιβλιοθήκη + Δωρεάν + Συνέχιση + Ανανέωση + Εμφάνιση περιεχομένου για ενήλικες + Epic + EOS + Εκτός σύνδεσης + Ανενεργό + Σύνδεση Epic (EOS) + Εκκίνηση εκτός σύνδεσης + Επίλυση σύνδεσης Epic + Αποθηκεύσεις στο cloud + Μεταφόρτωση στο cloud + Λήψη από το cloud + Καμία αποθήκευση στο cloud + Σύγκρουση cloud + Σύγκρουση cloud · %1$d αρχεία + … + Διατήρηση του cloud + Διατήρηση του τοπικού + Έλεγχος cloud… + Τελευταίος συγχρονισμός %1$s + Δεν έχει συγχρονιστεί ακόμα + %1$d αρχεία + εγκατεστημένο + στη βιβλιοθήκη σας + κατάστημα + Επιτεύγματα + %1$d από %2$d ξεκλειδώθηκαν + %1$d / %2$d + Ξεκλειδωμένα · %1$d + Κλειδωμένα · %1$d + Κρυφά · %1$d + Κρυφό επίτευγμα + Οι λεπτομέρειες εμφανίζονται μόλις ξεκλειδωθεί. + Όλα τα επιτεύγματα ξεκλειδώθηκαν + Φόρτωση… + Συνδεθείτε στο %1$s για να δείτε τα επιτεύγματά σας. + Δεν ήταν δυνατή η σύνδεση με το %1$s. Δοκιμάστε ξανά όταν είστε συνδεδεμένοι. + Το %1$s δεν αναφέρει επιτεύγματα για αυτό το παιχνίδι. + Εμφάνιση όλων (%1$d) + Επιτεύγματα · %1$s + ✓ Στο Steam + Κατάργηση εγκατάστασης + Γίνεται κατάργηση εγκατάστασης… + Κατάργηση εγκατάστασης του %1$s; + Ελευθερώνει %1$s και το αφαιρεί από το Steam. + Αφαιρεί τα αρχεία του και τη συντόμευσή του στο Steam. + Το Steam το εντοπίζει την επόμενη φορά που θα ξεκινήσει· εκκινήστε το από εκεί, ή ξεκινήστε το Steam τώρα. + Proton + Επιλογή του Steam ανά παιχνίδι + Ήχος + DirectAudio + Στοιχεία + FEX, DXVK, VKD3D + Μηχανή + %1$s · εγγενής + %1$s · ενσωματωμένη + Πώς εκτελείται + Προέρχεται από + %1$s, με τον λογαριασμό σας + Εγκαταστάθηκε σε + Αποθηκευτικός χώρος παιχνιδιών (Games/%1$s/%2$s) + Εκκινείται από + Steam και Proton, η ίδια διαδρομή με τα παιχνίδια Steam + Steam και Proton · σύνδεση Epic ανά εκκίνηση, επικάλυψη EOS ανενεργή + Steam και Proton · ορισμένες μεταβλητές Amazon Games Services + Steam + προστέθηκε στο Steam κατά την εγκατάσταση (Deck UI + Steam Input) + Καμία λήψη + Σε παύση + Σε αναμονή + Σε αναμονή · #%1$d + Ακυρώθηκε + Αποτυχία + Αποτυχία · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Παύση + Συνέχιση + Ακύρωση + Επανάληψη + Εκκαθάριση + Εκκαθάριση ολοκληρωμένων + Manifest + Λήψη + Επαλήθευση + Εγκατάσταση + Τέλος + Έλεγχος + Γίνεται λήψη + Διαγραφή λήψης; + Αποτυχία · το %1$s διατηρήθηκε + Γίνεται επαλήθευση + Γίνεται εγκατάσταση + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d αρχεία + Διαχείριση λήψεων + Ταυτόχρονες λήψεις + Εγκατάσταση σε + Εγκατάσταση %1$s: + Εσωτερική + %1$s ελεύθερα + Λήψεις καταστήματος + Εμφανίζει την πρόοδο λήψης παιχνιδιών και κρατά τις λήψεις ενεργές στο παρασκήνιο + Γίνεται λήψη + %1$d λήψεις + Προετοιμασία… + Ολοκληρωμένες λήψεις καταστήματος + Το %1$s εγκαταστάθηκε + Η λήψη του %1$s απέτυχε + Διαγραφή + Διατήρηση + Σύνδεση στο %1$s + Συνδεθείτε με τον πάροχό σας + Επαναφόρτωση + Κλείσιμο + Δοκιμάστε ξανά + Ολοκλήρωση σύνδεσης… + Δεν ήταν δυνατή η φόρτωση της σελίδας σύνδεσης: %1$s + Αυτός ο πάροχος αρνείται να συνδεθεί από μια ενσωματωμένη σελίδα. Συνδεθείτε με τον κωδικό πρόσβασης του καταστήματός σας αντ\' αυτού. + Η σελίδα σύνδεσης απάντησε με σφάλμα (HTTP %1$d). + Η σύνδεση με τη σελίδα σύνδεσης δεν είναι ασφαλής. + Δεν ήταν δυνατή η επαλήθευση της σύνδεσης. Δοκιμάστε ξανά. + Η σύνδεση απέτυχε. Δοκιμάστε ξανά. + Κλείσιμο + Πίσω + Επεξεργασία + Προσθήκη όλων των παιχνιδιών σε αυτόν τον φάκελο + Τίποτα νέο σε αυτόν τον φάκελο + Όνομα + Προορισμός + Εκκίνηση σε + Επιλογές εκκίνησης + Γραφικά + Άλλο exe… + Κατάργηση + Εξώφυλλο + Φόντο + Λογότυπο + Εικονίδιο + Φάκελος παιχνιδιού + Steam + SteamGridDB + Το αρχείο σας + Από το exe + Αυτόματο + Επιλογή εικόνας… + Επιλέξτε μια εικόνα: %1$s + Επαναφορά στο αυτόματο + + %1$d παιχνίδι προστέθηκε + %1$d παιχνίδια προστέθηκαν + + + %1$d ήδη στα Παιχνίδια + %1$d ήδη στα Παιχνίδια + + B · Πίσω στο %1$s + το παιχνίδι + Ρυθμίσεις αναπαραγωγής + Προγράμματα οδήγησης γραφικών + Αυτό το Proton + Κάθε Proton + Για κάθε Proton + Διαθέσιμα · %1$d + Ελέγχθηκε %1$s + Έλεγχος για νέα στοιχεία και προγράμματα οδήγησης + Λήψη + %1$s · τα DXVK, VKD3D-Proton και FEX παρακάτω ανήκουν σε αυτό το Proton + %1$s · δοκιμασμένο + %1$s · μη δοκιμασμένο + %1$s · μη υποστηριζόμενο + Κρατά το προτεινόμενο ζεύγος ενημερωμένο + Αυτόματο · %1$s + Προγράμματα οδήγησης runtime και οθόνης ξεχωριστά + Χρήση %1$s; + Κατάργηση %1$s; + Διαγνωστικά + Έτοιμο για παιχνίδι + Δεν είναι ακόμα έτοιμο για παιχνίδι + runtime %1$s + όριο διαδικασιών ανενεργό + συνδεδεμένος ως %1$s + Όταν κάτι χρειάζεται εσάς + Εμφάνιση + Αυτή η συσκευή + Γραφικά βιβλιοθήκης + ΕΦΑΡΜΟΓΕΣ LINUX + Flathub + Εφαρμογές επιφάνειας εργασίας για την επιφάνεια εργασίας Linux, ανοίγουν από την Επιφάνεια εργασίας. + Ρύθμιση του Flathub + Κατάργηση %1$s; + Οι ρυθμίσεις και τα αρχεία του διατηρούνται. + Διατήρηση + Μέγεθος στον δίσκο + Κατάργηση εγκατάστασης της επιφάνειας εργασίας; + Όλα + Επιτεύγματα GOG + Συγχρονίζει επιτεύγματα για υποστηριζόμενα παιχνίδια GOG. Ισχύει από την επόμενη εκκίνηση. + Τα επιτεύγματα είναι απενεργοποιημένα + Τα επιτεύγματα είναι ενεργοποιημένα + Συνδεθείτε ξανά στο GOG + Τα επιτεύγματα ανέβηκαν + Η μεταφόρτωση επιτεύγματος εκκρεμεί + Ενεργοποιήστε τα επιτεύγματα GOG στις ρυθμίσεις καταστημάτων + Τα επιτεύγματα δεν είναι διαθέσιμα· ελέγξτε τα αρχεία καταγραφής της συνεδρίας + Το επίτευγμα ξεκλειδώθηκε + Επικόλληση + Σταυρός κατεύθυνσης + Αριστερό στικ + Δεξί στικ + Μενού + Πληκτρολόγιο + Σταυρός κατεύθυνσης πάνω + Σταυρός κατεύθυνσης κάτω + Σταυρός κατεύθυνσης αριστερά + Σταυρός κατεύθυνσης δεξιά + Χειριστήρια αφής + Χειριστήριο αφής + Ποντίκι + Ενεργό + Ανενεργό + Αργή + Κανονική + Ταχεία + Επεξεργασία διάταξης + Μενού DroidDeck + Σύρετε τα χειριστήρια στην οθόνη + Διάταξη + Απόρριψη + Αποθήκευση… + Εικονίδιο + Π− + Π+ + Υ− + Υ+ + Απόκρυψη + Χρώμα και διαφάνεια… + Αντιγραφή αυτής της διάταξης + Η διάταξη αντιγράφηκε· επικολλήστε την στη διάταξη αφής οποιουδήποτε παιχνιδιού + Επικόλληση αντιγραμμένης διάταξης + Επαναφορά στην προεπιλογή του Steam + Αυτό το χειριστήριο δεν έχει αντιστοίχιση στο Steam για να του δοθεί εικονίδιο + Εικονίδιο για %1$s + Χρώμα διάταξης αφής + Τέλος + Δεν ήταν δυνατή η αποθήκευση της διάταξης αφής + Η διάταξη αφής αποθηκεύτηκε στο Steam + Εφαρμογή %1$d + Ετικέτα (προαιρετικό) + Τα εικονίδια του Steam δεν βρέθηκαν + + %1$d εικονίδιο από το Steam + %1$d εικονίδια από το Steam + + Χρώμα εικονιδίου + Χρώμα κουμπιού + Χωρίς εικονίδιο + + Έλεγχος σύνδεσης… + Λήψη της βιβλιοθήκης σας… + Λήψη λίστας παιχνιδιών… + Φόρτωση στοιχείων παιχνιδιών… (%1$d/%2$d) + + Συγχρονισμός %1$d παιχνιδιού… + Συγχρονισμός %1$d παιχνιδιών… + + Συγχρονισμός… %1$d/%2$d + Λήψη εξωφύλλων… + Ο συγχρονισμός απέτυχε + Δεν βρέθηκαν παιχνίδια στη βιβλιοθήκη σας στο %1$s + Το Amazon Games δεν απάντησε για τη βιβλιοθήκη σας: κάντε ανανέωση ή συνδεθείτε ξανά + Δεν ήταν δυνατή η σύνδεση με το GOG + Δεν ήταν δυνατή η ανάγνωση της βιβλιοθήκης σας στο GOG + Εγγραφή στο Steam… + Cardboard VR + Λειτουργία Cardboard + Εικόνες για το αριστερό και το δεξί μάτι, για γυαλιά VR κινητού. + Εικόνα + Εικονική οθόνη + 3D δίπλα-δίπλα + Γυαλιά + Cardboard + Cardboard (2014) + Χωρίς φακό + Απόσταση ματιών + Πλάτος οθόνης + Απόσταση οθόνης + Παρακολούθηση κεφαλιού + Αυτή η συσκευή δεν έχει αισθητήρα περιστροφής. + Επανακεντράρισμα προβολής + Επανακεντράρισμα + SteamVR (πειραματικό) + Εγκατάσταση + Κατεβάζει το SteamVR ARM64 της Valve (571 MB). + Λήψη και αποσυμπίεση του SteamVR… + Έναρξη + Εκτελεί το SteamVR με το κεφάλι σας ως headset· η εικόνα αλλάζει σε 3D δίπλα-δίπλα. + Διακοπή + Το SteamVR εκτελείται. + Δεν είναι διαθέσιμο σε αυτή τη συνεδρία. + Το SteamVR δεν αποκρίθηκε· ελέγξτε το αρχείο καταγραφής της συνεδρίας. + Λήψη στοιχείων + Οι λήψεις είναι κοινές με τη σελίδα Στοιχεία. Επιλέξτε παραπάνω μια έκδοση που έχει ληφθεί για να χρησιμοποιηθεί στην επόμενη εκκίνηση παιχνιδιού. + Επιλέξτε έκδοση για λήψη + Δημιουργός + Εκδότης + Κυκλοφορία + Κύρια ιστορία + + Επιπλέον + Ολοκλήρωση + %1$s ώ. + %1$d λεπ. + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4fe9ef611..a259331d0 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -15,8 +15,10 @@ Sesión Se muestra mientras hay una sesión de Steam o de escritorio abierta Steam se está ejecutando + Descarga de Steam en curso Sesión en pausa - toca Reanudar cuando quieras Reanudar + Suspender ahora Detener sesión Generación de fotogramas Elige el modo de generación de fotogramas @@ -182,6 +184,8 @@ ID de app org.ppsspp.PPSSPP No es un ID de app de Flathub + O un archivo .flatpak, para una app que no esté en Flathub. + Elige un archivo .flatpak Origen En tu Escritorio Nueva app @@ -251,6 +255,10 @@ Controles en pantalla Steam + QAM Auto muestra los controles cuando no hay ningún mando conectado. + Táctil de Steam (beta) + Los controles táctiles de Steam se activan en la próxima sesión de Steam + Disposición táctil de Steam + Mueve, cambia de tamaño u oculta los controles táctiles de este juego; se guarda en Steam Mando Cómo ve Steam tu mando. Se aplica en la próxima sesión. Mando de Steam Deck @@ -268,6 +276,8 @@ Manual Nativo Auto pausa y reanuda al mostrarse. Nativo prepara Steam para la suspensión primero. Manual espera a Reanudar. Nunca sigue ejecutándose. + Mantener las descargas activas + Retrasa la suspensión en segundo plano mientras Steam esté descargando. Auto pausa en segundo plano y reanuda al mostrarse. Manual espera a Reanudar. Nunca sigue ejecutándose. Inicio Iniciar Steam con DroidDeck @@ -333,6 +343,7 @@ El juego tiene %1$s (suele funcionar tal cual) Ya está en este juego Nada en la carpeta del juego apunta a un componente. + Instalados para este juego (%1$d) Todos los componentes (%1$d) Necesitan un instalador de Windows (%1$d) Estos ejecutan un instalador real dentro del prefijo (.NET, PhysX, XNA, los instaladores completos de Visual C++…). DroidDeck aún no puede hacerlo; es lo siguiente. @@ -343,6 +354,9 @@ Cargando los componentes… No se pudo instalar: %1$s Descargado + Descargando %1$s + Instalando: %1$s + Desactivando… Descarga de %1$s Se aplica la próxima vez que se inicie el juego. Estirar juegos 16:9 @@ -401,6 +415,44 @@ Buscar paquetes nuevos en las Nightlies Proton El Proton elegido aquí ejecuta todos los juegos que no tengan uno propio en Steam, y un cambio hecho en Steam también se ve aquí. Steam reemplaza el Proton de Valve al actualizarse; los originales se conservan por versión. + Configuración + Configurar perfil + Perfil + Perfil predeterminado + Lo usan automáticamente los juegos sin ajustes propios. + Ajustes guardados que se usan automáticamente al iniciar %1$s. + Elige solo los ajustes que necesita este juego. Todo lo demás sigue usando el perfil predeterminado. + Preajuste de FEX + Usa el preajuste del perfil predeterminado. + Preajuste personalizado guardado para este juego. + Variables de FEX personalizadas + Usar predeterminado + Compatibilidad del juego + Opciones de entorno de Proton, DXVK, VKD3D y sincronización para este juego. + Usa el Proton del perfil predeterminado. + Proton personalizado guardado para este juego mediante la asignación de compatibilidad de Steam. + Sin Proton + DXVK + VKD3D-Proton + Usa el paquete del perfil predeterminado de %1$s, salvo que este juego elija otro paquete. + Usa el paquete de componentes del perfil predeterminado. + Usa %1$s solo para este juego. + Opciones del runtime + Otras opciones de entorno de Proton, DXVK, VKD3D-Proton y sincronización para cada juego. + El perfil de %1$s se ha guardado para el próximo inicio del juego. + Iniciar sin Steam + Para un juego que se cierra enseguida: lo inicia sin la configuración de Steam de Proton. Las funciones de Steam, como la superposición, están desactivadas para él. + Activado + La elección de Proton de %1$s se ha guardado y ya está disponible en Steam. + La elección de Proton de %1$s se ha guardado. Reinicia Steam antes de iniciar el juego. + %1$s no puede usar el Proton elegido en este dispositivo. + No se pudo cargar el perfil de %1$s: %2$s + No se pudo guardar el perfil del juego: %1$s + Restablecer perfil del juego + ¿Restablecer el perfil de %1$s? + ¿Quitar los ajustes propios de este juego para Proton, FEX, paquete de componentes, entorno, filtrado de texturas y componentes de Windows? Usará el perfil predeterminado. + %1$s ahora usa el perfil predeterminado. + No se pudo restablecer del todo el perfil del juego: %1$s %1$s ahora ejecuta todos los juegos que no tengan su propio Proton en Steam. %1$s ejecutará todos los juegos que no tengan su propio Proton a partir del próximo inicio de Steam. %1$s no puede iniciar juegos aquí, así que no puede ser el predeterminado. @@ -564,6 +616,20 @@ ¿Detener la sesión? En ejecución Ningún juego se ejecuta con un Proton + Perfil del juego + Los detalles del perfil aún no están disponibles. + No se pudieron cargar los perfiles de juego. Se reintentará automáticamente. + No se pudieron actualizar los perfiles de juego. Se muestran los últimos datos disponibles; se reintentará automáticamente. + Consulta y edita los ajustes guardados que se usarán en el próximo inicio. + Usa el perfil predeterminado + Perfil personalizado + Usa %1$s del perfil predeterminado. + Los cambios se guardan en el perfil de %1$s y se aplican la próxima vez que se inicie el juego. + + %1$d ajuste propio del juego + %1$d ajustes propios del juego + %1$d ajustes propios del juego + Aún no hay ningún Proton instalado. juego en ejecución reaplicado al iniciar %1$s @@ -635,6 +701,10 @@ Usar como pantalla de inicio DroidDeck puede ser la app de inicio DroidDeck nunca se ofrece como app de inicio + Orientación + Horizontal automática + Horizontal + Horizontal invertida Pantalla completa Oculta las barras de estado y navegación de Android Muestra las barras de estado y navegación de Android @@ -898,6 +968,12 @@ Esa página ya no existe. Elige una sección a la izquierda. Abrir escritorio Instalar y abrir escritorio + Desinstalar escritorio + Elimina el escritorio y sus apps incluidas. Se conservan los juegos, las apps añadidas, los ajustes y las bibliotecas compartidas. + Eliminando el escritorio + Escritorio eliminado + Eliminando archivos antiguos de LXQt + Actualiza DroidDeck antes de eliminar esta versión del escritorio. Escritorio de Linux KDE Plasma, Firefox y tus emuladores Los juegos que instales en Steam aparecen aquí después de la sesión. @@ -926,6 +1002,8 @@ Última partida %1$s Almacenamiento interno Juego añadido + Juego personalizado + Almacenamiento de juegos El mejor par para esta GPU, siempre actualizado Elige un par manualmente Buscando controladores… @@ -967,6 +1045,12 @@ Pad izquierdo Pad derecho Ambos + Mando + Stick + Trackpad + Cambiar entre stick y trackpad + Más controles de Deck + PANELES Cerrar controles de la segunda pantalla Abrir menú de Steam Izquierdo · superior @@ -1002,6 +1086,10 @@ Instalada %1$s Pendiente de una solicitud anterior No instalada + Actualización disponible → %1$s + Quitar versiones antiguas de %1$s + Elimina %1$s. Los juegos configurados con estas versiones deberán cambiarse a otro Proton. + Los juegos configurados con esta versión deberán cambiarse a otro Proton. Instalar ahora Cancelar en cola Teclado de Android @@ -1030,6 +1118,8 @@ Juegos de Proton Imagen en imagen Entrar en PiP automáticamente + Controles con la pantalla desplegada + Desplegado, el juego queda encima de los trackpads y los agarres traseros de Steam Deck. Plegado, el juego ocupa toda la pantalla. Suspendida Imagen en imagen estará disponible cuando la sesión esté lista. Permite imagen en imagen para DroidDeck en los ajustes de Android. @@ -1056,17 +1146,14 @@ Archivos para frontends - Espera a que termine la operación del runtime Espera a que termine la instalación de la herramienta de compatibilidad Espera a que termine la instalación de la app de escritorio Espera a que termine la instalación antes de iniciar un juego Cierra primero la sesión de Steam para que el juego no guarde encima de lo importado - Importando en %1$s - Se importó %1$d archivo del %2$s en %3$s @@ -1079,20 +1166,16 @@ Se importaron %1$d archivos del %2$s en %3$s. Copia de las partidas anteriores en Download/DroidDeck/Saves/backups Elige un zip de partidas guardadas para %1$s - Exportando %1$s - Se exportó %1$d archivo como %2$s: %3$s Se exportaron %1$d archivos como %2$s: %3$s Se exportaron %1$d archivos como %2$s: %3$s - Elige dónde guardar %1$s (%2$s) - %1$s falló: %2$s Aún no hay registros de sesión: inicia una sesión primero. @@ -1108,23 +1191,18 @@ Comando ADB copiado Elige la carpeta de ROMs La GPU no es Adreno - Turnip es compatible con GPU Adreno. En %1$s, Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB. Instalar de todos modos Eliminar el runtime de Linux Esto elimina el runtime, el cliente de Steam que contiene y todos los juegos instalados en él. - Aplicado: %1$s No se pudo abrir %1$s en la pantalla principal - No se pudo abrir %1$s en %2$s - No se pudo abrir %1$s en la pantalla %2$d - Importando Reemplazando @@ -1132,20 +1210,17 @@ Cancelando Eliminando no se puede leer el archivo - %1$s importado Se canceló el reemplazo pendiente. Elige un paquete de componentes (-linux .wcp) No se pudo acceder a las Nightlies - %1$s guardado La descarga falló: %1$s Elige un controlador de runtime de Linux (zip -Linux o paquete Android + Linux) Elige un controlador de pantalla (zip de AdrenoTools o paquete Android + Linux) Elige un zip de controlador o un paquete Android + Linux - %1$s: %2$s Quitando %1$s… @@ -1153,7 +1228,6 @@ Elige la carpeta de almacenamiento de juegos Elige un ZIP de plugin de Decky No se puede usar: %1$s - Programa Eliminando el runtime de Linux @@ -1165,7 +1239,6 @@ No se pudo comprobar: %1$s Esta versión no tiene descarga para esta copia de DroidDeck Detén primero la sesión en ejecución y después actualiza - Descargando Instalando @@ -1187,18 +1260,15 @@ Vulkan del sistema: no hay versión incluida para esta GPU %1$s (incluido) Manual - %1$s · %2$s Por ahora solo se ha publicado una de las mitades %1$.0f MB para ambos - No hay controladores que definir: %1$s Buscando los controladores más recientes… Al día (no se pudo comprobar: %1$s) Los controladores recomendados no están publicados ahora mismo - Descargando %1$s %2$s… Actualizado a %1$s %2$s. @@ -1214,10 +1284,8 @@ Usando %1$s como controlador de runtime y de pantalla. El controlador de pantalla se aplica al reiniciar DroidDeck. %1$s eliminado (ambos controladores) %1$s eliminado - Últimas: %1$s · comprobado %2$s - Últimas: %1$s · sin acceso a %2$s · comprobado %3$s @@ -1237,30 +1305,25 @@ Comprobando Banners-Turnip, WinNative y DroidDeck… falló la comprobación - %1$s instalado - elígelo en cualquiera de las listas para definir ambos controladores %1$s instalado - elígelo en el menú Espera a que termine la eliminación del runtime - Juego - touchpad - directo El reemplazo falló: %1$s + No se pudo guardar el perfil: %1$s El escritorio está en pausa - %1$s está en pausa El programa está en pausa Iniciando el escritorio - Iniciando %1$s Iniciando el programa @@ -1268,7 +1331,6 @@ No se pudo crear el archivo de registros de la sesión. No se pudieron compartir los registros de la sesión. Iniciando la sesión… - descargando el runtime de Linux descargando el runtime de Linux · %1$d de %2$d MB @@ -1284,19 +1346,15 @@ descomprimiendo Proton Experimental (ARM64) No se pudo acceder al catálogo del runtime. Revisa la conexión y vuelve a pulsar Jugar. El runtime de Linux no se instaló. Revisa el runtime en Configuración y vuelve a pulsar Jugar. - El escritorio no se instaló (%1$s). Revisa la conexión y pulsa Escritorio de nuevo. - %1$s se detuvo inesperadamente. Comparte los registros en un informe de error o inténtalo de nuevo. El programa se detuvo inesperadamente. Comparte los registros en un informe de error o inténtalo de nuevo. El escritorio se detuvo inesperadamente. Comparte los registros en un informe de error o inténtalo de nuevo. - Código de salida %1$d · %2$s sin registro - El registro muestra %1$d error \"Function not implemented\": la aceleración seccomp de proot hace fallar un proceso auxiliar en este dispositivo. Prueba %2$s → \"%3$s\" (o \"%4$s\") y vuelve a iniciar. @@ -1305,13 +1363,11 @@ - Plugins Instala un ZIP de plugin para la próxima sesión de Steam. Descargando… Instalar desde ZIP - Juegos Audio y vídeo @@ -1325,27 +1381,20 @@ Utilidades Populares En tendencia - Nuevas - Actualizadas cargando… Configuración de Flatpak - %1$s: %2$s No se pudieron buscar actualizaciones en Flathub - - instalado - carpeta - Grafito Superficies grafito, con el azul del orbe del logotipo para todo lo seleccionado, enfocado o pulsado. @@ -1355,7 +1404,6 @@ Fondo negro, texto blanco y el azul para todo lo seleccionado, enfocado o pulsado. Azul marino eléctrico Un azul más vivo, sobre superficies que tiran a azul marino en vez de gris. - Esc Insert @@ -1374,15 +1422,12 @@ Alt Espacio AltGr - Tab - %1$s ✓ Ctrl+C Ctrl+D - Instala primero el runtime La biblioteca de Steam no está disponible @@ -1390,10 +1435,8 @@ El almacenamiento de juegos no está disponible La carpeta de juegos añadidos no está disponible Los manifiestos de Steam no están disponibles - Varios sistemas - Interno Sistema Linux @@ -1412,7 +1455,6 @@ Aún no hay favoritos - fija una carpeta con su menú ⋮ para volver aquí rápidamente. Quitar de favoritos 0 bytes - bytes Ubicación @@ -1421,7 +1463,6 @@ Modificado Carpeta Archivo - Archivo %1$s Solo lectura @@ -1436,18 +1477,14 @@ No se pudo eliminar Moviendo... Copiando... - Moviendo - %1$s Copiando - %1$s - Moviendo %1$d/%2$d - %3$s Copiando %1$d/%2$d - %3$s - %1$d hechos, %2$d con error - %1$d hechos, %2$d omitidos @@ -1460,7 +1497,6 @@ %1$d elementos copiados %1$d elementos copiados - \"%1$s\" ya existe Renombrando... @@ -1472,7 +1508,6 @@ Crear Nuevo nombre ¿Eliminar? - ¿Eliminar \"%1$s\" de forma permanente? @@ -1481,14 +1516,12 @@ ¿Eliminar %1$d elementos? Esta acción no se puede deshacer. - …y %1$d más …y %1$d más …y %1$d más - Eliminando %1$d/%2$d - %3$s @@ -1505,7 +1538,6 @@ Omitir Seleccionar esta carpeta Atrás - %1$s está montado, pero ahora mismo no se puede leer Vista de lista @@ -1521,14 +1553,11 @@ Ocultar favoritos Mostrar favoritos Filtrar esta carpeta - %1$s libres %1$d seleccionados - Ninguno - Todo Pegar @@ -1537,36 +1566,29 @@ %1$d elementos %1$d elementos - Pegar %1$s aquí - Pegar %1$s aquí (mover) Archivos ALMACENAMIENTO - ATAJOS FAVORITOS Descargas - Descargas ROMs Registros de sesión Imágenes - Se añadió \"%1$s\" a Favoritos - Se quitó \"%1$s\" de Favoritos Carpeta vacía Subir un nivel Elige una carpeta Elige un archivo - Azul Steam Celeste @@ -1576,7 +1598,6 @@ Ámbar Rojo Rosa - Botón A Botón B @@ -1588,25 +1609,20 @@ Gatillo derecho Botón Vista Botón Menú - - Vista - Menú Cruceta arriba Cruceta abajo Cruceta izquierda Cruceta derecha - Oculto Arrastra para mover Ajustar Guardar - Nada Teclado + trackpad @@ -1614,7 +1630,6 @@ Agarres de Deck + trackpads - Eliminando el runtime de Linux @@ -1634,11 +1649,9 @@ El runtime de Linux no está instalado - El catálogo no tiene suma de verificación para %1$s La suma de verificación no coincide - no se cambió nada - Instalando %1$s No se pudo colocar el AppImage @@ -1652,51 +1665,41 @@ Configura Flatpak primero Iniciando Flatpak terminó con el código %1$d - Instalando %1$s Actualizando %1$s Quitando %1$s - Instalando %1$s (%2$d/%3$d) Actualizando %1$s (%2$d/%3$d) Quitando %1$s (%2$d/%3$d) - traducciones controladores gráficos códecs multimedia - runtime de %1$s - runtime de %1$s %2$s El archivo es demasiado pequeño para ser un AppImage - No se puede leer el archivo: %1$s Esto no es un AppImage (no es un programa de Linux) Este AppImage no está hecho ni para ARM64 (aarch64) ni para PC x86 Es un programa de Linux, pero no un AppImage Solo se pueden importar AppImages de tipo 2 (este es de tipo %1$d) - No hay espacio libre suficiente: importarlo necesita unos %1$s - No hay espacio libre suficiente: actualizarlo necesita unos %1$s La importación falló Ese AppImage ya no está La actualización falló - Extrayendo %1$s No se pudo extraer el AppImage - No se pudo extraer el AppImage: %1$s @@ -1736,26 +1739,19 @@ Entrada remote_binary no válida Nombre de binario remoto no válido La URL del binario remoto debe usar HTTPS - El binario remoto %1$s tiene un hash SHA-256 no válido No se pudo crear la carpeta bin del plugin - Descargando el binario del plugin · %1$s - No se pudo descargar el binario del plugin %1$s - La suma de verificación del binario del plugin no coincide · %1$s - No se pudo marcar como ejecutable el binario del plugin · %1$s - No se pudo reemplazar el binario del plugin · %1$s - No se pudo instalar el binario del plugin · %1$s La carpeta del plugin pertenece a otro plugin @@ -1766,10 +1762,8 @@ No se pudo guardar la identidad de la Depuración inalámbrica - Falló el comando ADB (%1$d) - Android no confirmó el cambio del límite de procesos secundarios: %1$s No hay ninguna vinculación de Depuración inalámbrica guardada @@ -1777,10 +1771,8 @@ Se agotó el tiempo de espera de la ventana de vinculación. Inténtalo de nuevo desde DroidDeck. - No se pudo buscar la ventana de vinculación (%1$d). Comprueba que el Wi-Fi esté activado. - No se pudo buscar la ventana de vinculación: %1$s Introduce los 6 dígitos del código @@ -1795,13 +1787,11 @@ Vincular Depuración inalámbrica Activa la Depuración inalámbrica y toca “Vincular dispositivo con código de vinculación”. Activa la Depuración inalámbrica y toca “Vincular dispositivo con código de vinculación”. Quédate en Ajustes: el código se introduce en esta notificación. - Código de vinculación de 6 dígitos Toca Introducir código y escribe los 6 dígitos que muestra Ajustes. Introduce el código de vinculación Wi-Fi Vuelve a probar el código - Introducir código Preparando Steam @@ -1813,12 +1803,10 @@ La descarga respondió HTTP %1$d El tamaño de la descarga no coincidía con el APK publicado y se descartó - inténtalo de nuevo La descarga no coincidía con su suma de verificación y se descartó - inténtalo de nuevo - La actualización es para el paquete de Android %1$s, no %2$s Los metadatos de la actualización no indican la clave de firma de publicación de DroidDeck Android no pudo leer el APK descargado - El APK descargado es para %1$s, no %2$s El APK descargado indica versionCode %1$d, no el %2$d publicado @@ -1828,14 +1816,11 @@ El catálogo de actualizaciones respondió HTTP %1$d - Iniciando la sesión… - %1$d:%2$02d transcurridos · sigue en marcha Descargando la actualización del cliente de Steam · %1$d%% - Descargando el cliente de Steam · %1$s @@ -1856,7 +1841,6 @@ La suma de verificación no coincide; se eliminó la descarga Se inició una sesión durante la descarga; detenla antes de instalar herramientas de compatibilidad Instalando %1$s - Falló la instalación de %1$s (salida %2$d) No se pudo instalar %1$s @@ -1867,30 +1851,24 @@ Solo DLL Original (Proton se actualizó desde el último reemplazo) Cambiado fuera de Componentes - no presente - Original %1$s Original de %1$s %1$s %2$s entrará en %3$s cuando se cierre el juego que lo está usando. %1$s %2$s ya está en %3$s. Se aplica la próxima vez que se inicie un juego. Ese paquete original no está guardado - %1$s de %2$s volverá a su original cuando se cierre el juego que lo está usando. - %1$s de %2$s restaurado desde su original de %3$s. - %1$s está en uso en %2$s. Cambia antes ese Proton a otra cosa. %1$s no está guardado %1$s eliminado. El original de la versión instalada se conserva: es el camino de vuelta. No guardado - Se eliminó el original de %1$s de %2$s. Las descargas solo proceden de las versiones Nightlies @@ -1942,7 +1920,6 @@ la descarga respondió HTTP %1$d Esta lista de controladores es anterior a las sumas de verificación - actualízala e inténtalo de nuevo La suma de verificación no coincide - se descartó la descarga - No es un controlador de AdrenoTools: %1$s no hay meta.json @@ -1950,7 +1927,6 @@ es un controlador de runtime de Linux (impórtalo en \"Controlador de runtime\") es un controlador de juegos para Wayland, que esta app no usa meta.json no indica ningún libraryName, así que no se pudo pasar nada a AdrenoTools - meta.json indica %1$s, que no está en el zip %1$s no es una biblioteca compartida ELF AArch64 de 64 bits @@ -1964,22 +1940,19 @@ Tarjeta SD - Carpeta - Compartir registros de sesión + Compartir registros de sesión requiere Android 10 o posterior (capas de pantalla con espacio de color) esta pantalla no indica compatibilidad con HDR - esta pantalla admite %1$s, no HDR10 - esta GPU El límite de procesos secundarios está activado @@ -1987,7 +1960,6 @@ No se pudo comprobar el límite de procesos secundarios El límite de procesos secundarios está desactivado El límite de procesos secundarios no es necesario - %1$s Si no es posible, conecta este dispositivo a un ordenador con ADB y ejecuta el comando de abajo. Después vuelve aquí y comprueba de nuevo. Ya se pueden iniciar sesiones de Steam. @@ -1995,81 +1967,60 @@ Activa “Desactivar restricciones de procesos secundarios” en Opciones de desarrollador y deja activadas las Opciones de desarrollador después. Esta versión de Android no tiene un interruptor para ello. Usa la Depuración inalámbrica para cambiarlo desde este dispositivo. Esta versión no tiene descarga para esta copia de DroidDeck. - Android no la instalará sobre esta versión porque su versionCode %1$d es inferior al %2$d instalado. Android no instala una versión anterior sobre una más reciente. Quédate en esta versión hasta que el canal la alcance. Esta copia de DroidDeck está firmada de otro modo (¿una compilación local?), así que Android no la actualizará sobre la instalada. No hay espacio libre suficiente para la actualización. Se canceló la actualización. - La actualización no se instaló: %1$s La actualización no se instaló. - zip de %1$s - zip - %1$s aún no se ha iniciado No se encontraron partidas guardadas de %1$s - %1$s no tiene carpeta drive_c, así que no es un zip de partidas de GameHub ni de Winlator - no se pueden crear carpetas en %1$s no se puede escribir en %1$s - no se puede escribir en %1$s (%2$s) La carpeta no está disponible - Otro archivo ya usa %1$s La carpeta de imágenes no está disponible La carpeta tiene un inventario de sincronización ajeno - No se pudo quitar %1$s El archivo sale de la carpeta de exportación - FEX (compilación sin nombre de versión) - no hay profile.json en %1$s - %1$s no es un paquete de componentes de Linux (tipo %2$s) - %1$s · %2$s libres - Automático - El catálogo de actualizaciones no superó las comprobaciones (%1$s) - Ajuste de procesos secundarios de DroidDeck - Sesión iniciada · modo sin conexión Sesión iniciada - INCLUIDO DESCARGADO IMPORTADO - ANDROID + LINUX - %1$s. %2$s\n\n%3$s Pantalla de la sesión @@ -2088,6 +2039,7 @@ Velocidad de fotogramas %1$s Cambia el escalado a %2$s. Igual a la pantalla (%1$d×%2$d) + Seguir la pantalla (cambia de tamaño al plegar o desplegar) Steam Mostrar detalles del dispositivo Ocultar detalles del dispositivo @@ -2097,6 +2049,186 @@ Copiado. Forzar SSBS Mantiene activado un ajuste de velocidad de la CPU que Wine sigue desactivando. Puede hacer más rápidos los juegos de Windows en Snapdragon 8 Elite; poco cambio en chips anteriores. Se aplica en el próximo inicio de un juego. + + Tiendas + Steam + Personalizados + Tiendas + Abrir una tienda en + Nivel de velocidad de descarga + Motor de las tiendas + ● Motor listo + Sin compilar + Equilibrado + Rápido + Máximo + Tiendas + Ajustes de las tiendas + Ajustes + (sin iniciar sesión) + Tienda + Instalados + Biblioteca + Todos + Descargas + %1$s (%2$d) + Iniciar sesión + %1$s · cerrar sesión + ¿Cerrar sesión en %1$s? + Los juegos instalados se conservan. La biblioteca se vuelve a cargar cuando inicias sesión de nuevo. + Mantener la sesión + Cerrar sesión + Buscar en el catálogo de %1$s… + Buscar juegos instalados… + Buscar en tu biblioteca… + Buscar + Nada instalado + Cargando + No hay juegos + Cargando la tienda… + Sin catálogo + Novedades + Ofertas + Juegos gratis + En tendencia + Tu biblioteca + Jugar + Instalar + Reanudar instalación + Interrumpida + Instalar · %1$s + Obtener gratis + Ver en %1$s + Comprar · %1$s + Instalado + En la biblioteca + Gratis + Reanudar + Actualizar + Mostrar contenido para adultos + Epic + EOS + Sin conexión + Desactivado + Inicio de sesión de Epic (EOS) + Iniciar sin conexión + Solucionar el inicio de sesión de Epic + Partidas en la nube + Subir a la nube + Descargar de la nube + No hay partidas en la nube + Conflicto en la nube + Conflicto en la nube · %1$d archivos + … + Conservar la de la nube + Conservar la local + Comprobando la nube… + Última sincronización %1$s + Aún sin sincronizar + %1$d archivos + instalado + en tu biblioteca + tienda + Logros + %1$d de %2$d desbloqueados + %1$d / %2$d + Desbloqueados · %1$d + Bloqueados · %1$d + Ocultos · %1$d + Logro oculto + Los detalles se muestran al desbloquearlo. + Todos los logros desbloqueados + Leyendo… + Inicia sesión en %1$s para ver tus logros. + No se pudo conectar con %1$s. Vuelve a intentarlo cuando tengas conexión. + %1$s no indica logros para este juego. + Ver los %1$d + Logros · %1$s + ✓ En Steam + Desinstalar + Desinstalando… + ¿Desinstalar %1$s? + Libera %1$s y lo quita de Steam. + Elimina sus archivos y su acceso directo de Steam. + Steam detectará el juego la próxima vez que se inicie; ábrelo desde allí o inicia Steam ahora. + Proton + Lo elige Steam para cada juego + Audio + DirectAudio + Componentes + FEX, DXVK, VKD3D + Motor + %1$s · nativo + %1$s · integrado + Cómo se ejecuta + Obtenido de + %1$s, con tu cuenta + Instalado en + Almacenamiento de juegos (Games/%1$s/%2$s) + Se inicia con + Steam y Proton, igual que los juegos de Steam + Steam y Proton · inicio de sesión de Epic en cada arranque, superposición de EOS desactivada + Steam y Proton · variables de Amazon Games Services definidas + Steam + se añade a Steam al instalar (interfaz Deck + Steam Input) + No hay descargas + En pausa + En cola + En cola · #%1$d + Cancelada + Fallida + Fallida · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Pausar + Reanudar + Cancelar + Reintentar + Quitar + Quitar terminadas + Manifiesto + Descarga + Verificación + Instalación + Hecho + Comprobando + Descargando + ¿Eliminar la descarga? + Fallida · se conservan %1$s + Verificando + Instalando + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d archivos + Gestor de descargas + Descargas simultáneas + Instalar en + Instalar %1$s: + Interno + %1$s libres + Descargas de la tienda + Muestra el progreso de las descargas de juegos y las mantiene activas en segundo plano + Descargando + %1$d descargas + Preparando… + Descargas de la tienda terminadas + %1$s instalado + Falló la descarga de %1$s + Eliminar + Conservar + Iniciar sesión en %1$s + Inicia sesión con tu proveedor + Recargar + Cerrar + Reintentar + Terminando de iniciar sesión… + No se pudo cargar la página de inicio de sesión: %1$s + Este proveedor no permite iniciar sesión desde una página integrada. Inicia sesión con la contraseña de la tienda. + La página de inicio de sesión respondió con un error (HTTP %1$d). + La conexión con la página de inicio de sesión no es segura. + No se pudo verificar el inicio de sesión. Inténtalo de nuevo. + No se pudo iniciar sesión. Inténtalo de nuevo. Cerrar Atrás Editar @@ -2132,21 +2264,126 @@ %1$d ya en Juegos %1$d ya en Juegos - Logros - %1$d de %2$d desbloqueados - %1$d / %2$d - Desbloqueados · %1$d - Bloqueados · %1$d - Ocultos · %1$d - Logro oculto - Los detalles se muestran al desbloquearlo. - Todos los logros desbloqueados - Leyendo… - Inicia sesión en %1$s para ver tus logros. - No se pudo conectar con %1$s. Vuelve a intentarlo cuando tengas conexión. - %1$s no indica logros para este juego. - Ver los %1$d - Logros · %1$s + B · Volver a %1$s + la página del juego + los ajustes de Jugar + Controladores gráficos + Este Proton + Todos los Protons + Para todos los Protons + Disponibles · %1$d + Comprobado %1$s + Buscar componentes y controladores nuevos + Obtener + %1$s · DXVK, VKD3D-Proton y FEX de abajo son de este Proton + %1$s · probado + %1$s · sin probar + %1$s · no compatible + Mantiene actualizado el par recomendado + Auto · %1$s + Controladores de runtime y de pantalla por separado + ¿Usar %1$s? + ¿Quitar %1$s? + Diagnóstico + Listo para jugar + Aún no está listo para jugar + runtime %1$s + límite de procesos desactivado + sesión iniciada como %1$s + Cuando algo requiera tu atención + Aspecto + Este dispositivo + Ilustraciones de la biblioteca + APPS DE LINUX + Flathub + Apps para el escritorio de Linux, que se abren desde Escritorio. + Configurar Flathub + ¿Quitar %1$s? + Se conservan sus ajustes y archivos. + Conservar + Tamaño en disco + ¿Desinstalar el escritorio? + Todos + Logros de GOG + Sincroniza los logros de los juegos de GOG compatibles. Se aplica en el próximo inicio. + Logros desactivados + Logros activados + Vuelve a iniciar sesión en GOG + Logros subidos + Subida de logro pendiente + Activa los logros de GOG en los ajustes de tiendas + Logros no disponibles; revisa los registros de la sesión + Logro desbloqueado + Pegar + Pad direccional + Stick izquierdo + Stick derecho + Menú + Teclado + Pad direccional arriba + Pad direccional abajo + Pad direccional izquierda + Pad direccional derecha + Controles táctiles + Mando táctil + Ratón + Activado + Desactivado + Lenta + Normal + Rápida + Editar disposición + Menú de DroidDeck + Arrastra controles a la pantalla + Organizar + Descartar + Guardando… + Icono + A− + A+ + Al− + Al+ + Ocultar + Color y opacidad… + Copiar esta disposición + Disposición copiada; pégala en la disposición táctil de cualquier juego + Pegar disposición copiada + Restablecer a la de Steam + Este control no tiene una acción asignada en Steam para ponerle un icono + Icono para %1$s + Color de la disposición táctil + Hecho + No se pudo guardar la disposición táctil + Disposición táctil guardada en Steam + App %1$d + Etiqueta (opcional) + No se encontraron los iconos de Steam + + %1$d icono de Steam + %1$d iconos de Steam + %1$d iconos de Steam + + Color del icono + Color del botón + Sin icono + + Comprobando el inicio de sesión… + Obteniendo tu biblioteca… + Obteniendo la lista de juegos… + Cargando datos de los juegos… (%1$d/%2$d) + + Sincronizando %1$d juego… + Sincronizando %1$d juegos… + Sincronizando %1$d juegos… + + Sincronizando… %1$d/%2$d + Obteniendo las carátulas… + Error al sincronizar + No se encontraron juegos en tu biblioteca de %1$s + Amazon Games no respondió al consultar tu biblioteca: actualiza o vuelve a iniciar sesión + No se pudo conectar con GOG + No se pudo leer tu biblioteca de GOG + Registrando con Steam… Desarrollador Distribuidor Lanzamiento @@ -2155,4 +2392,34 @@ Completista %1$s h %1$d min + Cardboard VR + Modo Cardboard + Vistas para el ojo izquierdo y el derecho para un visor de RV para móvil. + Imagen + Pantalla virtual + 3D lado a lado + Visor + Cardboard + Cardboard (2014) + Sin lente + Distancia entre ojos + Ancho de pantalla + Distancia de la pantalla + Seguimiento de cabeza + Este dispositivo no tiene sensor de rotación. + Recentrar vista + Recentrar + SteamVR (experimental) + Instalar + Descarga SteamVR para ARM64 de Valve (571 MB). + Descargando y descomprimiendo SteamVR… + Iniciar + Ejecuta SteamVR usando tu cabeza como visor; la imagen cambia a 3D lado a lado. + Detener + SteamVR está en ejecución. + No disponible en esta sesión. + SteamVR no respondió; revisa el registro de la sesión. + Descargar componentes + Las descargas se comparten con la página Componentes. Elige arriba una versión descargada para usarla al iniciar el próximo juego. + Elige una versión para descargar diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml new file mode 100644 index 000000000..fd0679c05 --- /dev/null +++ b/app/src/main/res/values-fi/strings.xml @@ -0,0 +1,2235 @@ + + + DroidDeck + + Ensimmäinen käynnistys lataa Steamin. Tämä voi kestää muutaman minuutin. + Oletuksena sovelluksesta poistuminen keskeyttää Steamin; se jatkuu siitä mihin jäit, kun palaat takaisin. + Ohjaimesi näkyy Xbox 360 -ohjaimena. Muussa tapauksessa käytä näytön ohjaimia. + + + Ensimmäinen käynnistys määrittää työpöydän. Tämä voi kestää muutaman minuutin. + Oletuksena sovelluksesta poistuminen keskeyttää istunnon; se jatkuu siitä mihin jäit, kun palaat takaisin. + Kosketus toimii kosketuslevynä: vedä liikuttaaksesi osoitinta, napauta napsauttaaksesi. + Ohjaimesi näkyy Xbox 360 -ohjaimena. Muussa tapauksessa käytä näytön ohjaimia. + + Istunto + Näkyy, kun Steam- tai työpöytäistunto on käynnissä + Steam on käynnissä + Steamin lataus käynnissä + Istunto keskeytetty - napauta Jatka, kun olet valmis + Jatka + Keskeytä nyt + Lopeta istunto + Kehysten generointi + Valitse kehysten generoinnin tila + Pois + Win-FG %1$d× + LSFG %1$d× + LSFG Mukautuva %1$d + Vaatii Lossless Scalingin Steamista + Asenna Lossless Scaling Steamista tai tuo Lossless.dll + Tuo Lossless.dll… + Käytössä on Steamin kopio + Käytössä on tuotu kopio + Valitse Lossless.dll + Lossless.dll tuotu + Tämä Lossless.dll on jo käytössä + Lossless Scaling ei ole tämän Steam-tilin omistuksessa + Tämä tiedosto ei ole Lossless Scalingin Lossless.dll + LSFG:n valmistelu tästä Lossless.dll-tiedostosta epäonnistui + LSFG tarvitsee Lossless Scalingin Steamista tai tuodun Lossless.dll-tiedoston + LSFG ei pystynyt valmistelemaan sävyttimiään Lossless.dll-tiedostosta + %1$s: ei tuettu + %1$s: moottori epäonnistui + %1$s: vaatii Lossless Scalingin + %1$s %2$s · käynnistyy + Lisää Steam-kirjaston ja etsii siitä Windows-pelejä. Omistetut pelit käyttävät omia Steam-merkintöjään; muista tulee Steamin ulkopuolisia pikakuvakkeita. + Pelin ympäristö + Proton-pelien ympäristömuuttujat. + Muokkaa + Kaikki Proton-pelit + Profiili + Steam-sovellustunnus + Avaa + Peli %1$s + Lisää muuttuja + Muuttujan nimi + Arvo + Tallenna + Poista + Peri uudelleen + Tyhjennä ympäristön ohitukset + Poista tästä profiilista + Ei asetettuja muuttujia. + Peritty + Mukautettu muuttuja + Lataaminen tai tallentaminen epäonnistui. Tarkista vapaa tallennustila ja yritä uudelleen. + Käytä kelvollista nimeä ja enintään 8192 merkin arvoa. + Tavallinen Proton-ajonaikainen ympäristö ei lue tätä muuttujaa. + Latautuu… + D3D12-ominaisuustason ohitus. 12_2 ilmoittaa DirectX 12 Ultimate -ominaisuudet; tukemattomat ominaisuudet voivat epäonnistua. Jätä tyhjäksi automaattista tunnistusta varten. + D3D12-sävytinmallin ohitus. 6_9 vaatii riittävän uuden VKD3D:n ja vastaavat GPU-ominaisuudet. Jätä tyhjäksi automaattista tunnistusta varten. + Pidä Mesan sävytinvälimuisti käytössä vähentääksesi toistuvaa kääntämistä. false ottaa välimuistin käyttöön. + Mesan sävytinvälimuistin enimmäiskoko, esimerkiksi 1G. Suuremmat välimuistit käyttävät enemmän tallennustilaa. + VKD3D-asetukset D3D12:lle. nodxr poistaa säteenseurannan käytöstä; käytä vain peleissä, jotka sitä tarvitsevat. + Aseta arvoon 1 Proton-diagnostiikkalokia varten. Lokitus voi heikentää suorituskykyä ja käyttää tallennustilaa. + Aseta arvoon 1 käyttääksesi OpenGL-pohjaista WineD3D:tä DXVK:n sijaan D3D9–11-yhteensopivuuden testaamiseen. + Aseta arvoon 1 pitääksesi tämän pelin poissa ntsyncistä (droiddeck-ntsync tai ydinajuri); se käyttää droiddeck-esynciä, jos paketti on olemassa, muuten droiddeck-fsynciä. + Aseta arvoon 1 poistaaksesi droiddeck-esyncin käytöstä tälle pelille; se käyttää sen sijaan droiddeck-fsynciä, tai droiddeck-ntsynciä, jos fsync on myös pois käytöstä. + Aseta arvoon 1 pitääksesi tämän pelin poissa droiddeck-fsyncistä; se käyttää droiddeck-esynciä, jos paketti on olemassa, muuten droiddeck-ntsynciä. + Aseta arvoon 1 ajaaksesi tämän pelin droiddeck-fsyncillä, tai 0 asettaaksesi droiddeck-esyncin etusijalle, jos paketti sopii sen Protoniin. + Aseta arvoon 0 poistaaksesi käytöstä Protonin ohjaimen navigointiavustimen tälle pelille. + DXVK-peittokuva D3D9–11:lle. fps näyttää ruudunpäivitysnopeuden; tyhjänä piilottaa sen. + DXVK-asetukset erotettuna puolipisteillä. Uudemmat versiot käyttävät dxvk.maxFrameRate-asetusta kuvanopeuden rajoittamiseen; käytä tavallisessa pelaamisessa ennemmin istunnon kuvanopeusrajoitinta. + D3D12-kuvanopeusraja. 0 poistaa rajan käytöstä. + Mesan OpenGL-säikeistys. true voi auttaa CPU-rajoitteisissa OpenGL-peleissä; testaa pelikohtaisesti. + Wine-synkronointi + Miten pelit pitävät säikeensä tahdissa. Tulee voimaan seuraavassa istunnossa. + esync + ntsync + fsync + wineserver + FEX-esiasetus + Tulee voimaan seuraavalla pelin käynnistyksellä. + Pelit + FEXin oletukset + Käyttää FEXin oletuksia. + Vakaus + Turvallisimmat, hitaimmat asetukset. + Yhteensopivuus + Täysi TSO ja multiblock. + Keskitaso + Tasapainotettu TSO heikennetyllä x87-tarkkuudella. + Suorituskyky + Kaikki TSO pois käytöstä; multiblock. + Suorituskyky + TSO + Suorituskykyasetukset, pääasiallinen TSO käytössä. + Äärimmäinen + Poistaa itseään muokkaavan koodin tarkistukset käytöstä. JIT-pelit ja DRM voivat hajota. + Äärimmäinen + TSO + Äärimmäiset asetukset, pääasiallinen TSO käytössä. + Extreme-gn + Suorituskyky TSC- ja metadatavihjeillä. Kokeile ennen Äärimmäistä. + Denuvo + Suorituskyky täysin koodintarkistuksin ja hypervisor-bitti piilotettuna. + Mukautettu arvo + Valitse arvo + Emulaattorit ja sovellukset + Lisää + Oma sovelluksesi tai pelisi + Komentosarja + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Käynnistyy… + %1$s lisätty + %1$s poistettu + Työpöytä + Tämä sovellus on kadonnut. Valitse se uudelleen Työpöydältä. + Avaa %1$s + Poista + Paina uudelleen poistaaksesi + Lopeta istunto poistaaksesi + Suorittaa %1$s:n omasta kansiostaan, linkitettynä istuntoon siihen paikkaan, jossa se sijaitsee. Sovelluksen poistaminen ei koske kyseistä kansiota. + Suorittaa %1$s:n sen kansion kopiosta DroidDeckin sisällä. Sovelluksen poistaminen poistaa kopion, ei alkuperäistä. + Asenna Linux-ajoympäristö ensin + Tiedosto on kadonnut + Vapaata tilaa ei ole tarpeeksi: kansion kopioiminen vaatii %1$s + Kopioidaan: %1$s + Sovelluksen lisääminen epäonnistui + Etsitään julkaisua: %1$s + Julkaisujen lukeminen epäonnistui: %1$s + Yhdessäkään %1$s:n julkaisussa ei ole ARM64- tai x86_64-AppImagea + Ladataan: %1$s + Lataus epäonnistui + Sovelluksen päivitys epäonnistui + Tätä kuvaketta ei voitu lukea + %1$s tallennettu + Muokkaa + Tarkista päivitykset + Tarkistetaan päivityksiä… + Päivitä versioon %1$s + Ajan tasalla · %1$s + %1$s päivitetty + %1$s · %2$s + Lisää sovellus + Komentosarja + AppImage + GitHub + Flatpak + .sh-tiedosto. Se käynnistyy omasta kansiostaan koko näytön tilassa gamescopen alla. + ARM64 (aarch64) -AppImage, tai x86_64-AppImage, joka suoritetaan FEXin kautta. Se puretaan Linux-ajoympäristöön kertaalleen. + Asentaa ARM64-AppImagen repositorion uusimmasta julkaisusta, tai sen x86_64-version FEXin kautta. + Käytä FEXiä + Automaattinen + Päällä + Pois + FEX suorittaa x86-Linux-ohjelmia tällä ARM64-laitteella. Automaattinen käyttää sitä, kun ohjelma on tehty x86:lle. + %1$s FEXin kautta + Tämä ohjelma on tehty %1$s-tietokoneille ja käynnistyy FEXin kautta, jota ei ole vielä määritetty. Avaa Steam kerran kirjautuneena: DroidDeck lataa sen myötä FEXin ja tarvittavan Steam Linux Runtimen, jonka jälkeen tämä avautuu. + Tämä ohjelma + %1$s on tehty x86-tietokoneille ja käynnistyy FEXin kautta, jota ei ole vielä asennettu. Käynnistä Steam kerran kirjautuneena ja anna sen ladata FEX ja Steam Linux Runtime, sitten käynnistä tämä uudelleen. + Flathub-sovellustunnus tai sen Flathub-sivu. Flatpak otetaan käyttöön ensin, jos sitä ei ole vielä otettu. + Tiedosto + Ei valittua tiedostoa + Valitse tiedosto + Vaihda + Valitse komentosarja + Valitse AppImage + Valitse kuvake + Repositorio + omistaja/repo tai github.com-linkki + Ei ole GitHub-repositorio + Sovellustunnus + org.ppsspp.PPSSPP + Ei ole Flathub-sovellustunnus + Tai .flatpak-tiedosto sovellukselle, jota ei ole Flathubissa. + Valitse .flatpak-tiedosto + Lähde + Työpöydälläsi + Uusi sovellus + Nimi + Sovelluksesta + Kuvake + Oletuskuvake + Valitse kuvake + Tyhjennä + Repositoriosta + Etsitään kuvakkeita… + Oletus + Muokkaa: %1$s + Tallenna + Tarkistetaan kansiota… + Sen kansio on linkitetty istuntoon: %1$s + Sen kansio sisältää Linux-ohjelmia, joita jaettu tallennustila ei voi suorittaa, joten se kopioidaan DroidDeckiin (%1$s). + Lisää + Peruuta + Asenna Linux-ajoympäristö ensin Käyttöönotto-sivulta. + Odota, että %1$s valmistuu. + Yhteensopiva + Steamin Yhteensopivuus-valikossa merkinnällä ”%1$s” varustetuissa versioissa on DroidDeckin etuliitekorjaukset, ja pelit käynnistyvät niillä täällä. Oletus on Proton Experimental ARM64 (%1$s), tai Proton 11.0 ARM64 (%1$s) kun tuo uudempi versio on asennettu. Valven omat Proton-merkinnät, joissa ei ole merkintää, eivät voi käynnistää peliä tällä laitteella. + Asennettu %1$s · %2$s + Peruuta + Automaattinen + Ei koskaan + Aina + Poista asennus + Tarkista + Päivitä + Tarkistetaan… + Käsitellään… + Unohda + Lisää… + Käytä + Tulee voimaan seuraavassa istunnossa. + Steam-istunto + Työpöytäistunto + Näyttö + Resoluutio + Enintään 720p + Enintään 900p + Enintään 1080p + Alkuperäinen + %1$s (oletus) + Mukautettu · %1$d×%2$d + Mukautettu… + 720p voi tehdä valikoista reagoivampia. + Näytön kuvasuhde + Määräytyy mukautetun resoluution mukaan. + Automaattinen käyttää vähintään 16:9:ää. + Kuvanopeusraja + Rajoittaa koko istuntoa. Näyttö vaihtaa virkistystaajuuteen, jonka rajoitus jakaa tasan. + HDR + HDR10-lähtö + Ei käytettävissä: %1$s. + Käynnistä sovellus uudelleen, jotta muutos tulee voimaan. + Ajurit + GPU-ajurit + Jaettu Steamin ja Työpöydän kesken. Asetetaan Komponentit-sivulla. + Kosketus ja ohjaimet + Kosketus + Kosketuslevy + Suora + Automaattinen: kosketuslevy Työpöydällä, suora Steamissa. + Näytön ohjaimet + Steam + QAM + Automaattinen näyttää ohjaimet, kun yhtään ohjainta ei ole kytketty. + Steam-kosketus (beta) + Steamin kosketusohjaimet otetaan käyttöön seuraavassa Steam-istunnossa + Steamin kosketusasettelu + Siirrä, muuta kokoa tai piilota tämän pelin kosketusohjaimet; tallennetaan Steamiin + Ohjain + Miten Steam näkee ohjaimesi. Tulee voimaan seuraavassa istunnossa. + Steam Deck -ohjain + Steam Controller + DualShock 4 + DualSense + Xbox 360 -ohjain + Deck: pikavalikkopainike ja gyroskooppi. Steam Controller: samat, ja takapainikkeissa puristustunnistus. DualShock 4 ja DualSense: gyroskooppi, ja kosketuslevyt toimivat kosketuslevynä. Xbox 360: avaa pikavalikko Guide+A:lla. + Back + Valikko → QAM + QAM → Valikko + Istunto + Taustatoiminta + Kun sovellus on piilossa tai näyttö sammuu. + Manuaalinen + Natiivi + Automaattinen keskeyttää ja jatkaa, kun sovellus tulee näkyviin. Natiivi valmistelee Steamin lepotilaan ensin. Manuaalinen odottaa Jatka-painiketta. Ei koskaan pitää sovelluksen aina käynnissä. + Pidä lataukset käynnissä + Viivästytä taustakeskeytystä, kun Steam lataa aktiivisesti. + Automaattinen keskeyttää taustalla ja jatkaa, kun sovellus tulee näkyviin. Manuaalinen odottaa Jatka-painiketta. Ei koskaan pitää sovelluksen aina käynnissä. + Käynnistys + Käynnistä Steam DroidDeckin kanssa + Avaa Steam-istunnon, kun DroidDeck käynnistyy. + Decky + Tarkistetaan julkaisuja… + Valmiina asentamaan %1$s. + Yhteensopivaa versiota ei löytynyt. Yritä myöhemmin uudelleen. + Asennettu · %1$s + Päivitys saatavilla · %1$s + Ajan tasalla · %1$s + Loader + Asenna uusin + Käynnistyy Steamin kanssa. Päällä ollessaan muut tämän laitteen sovellukset voivat pystyä ohjaamaan Steamia. + Pois. Steamin etävianmääritysportti pysyy suljettuna. + Poistetaanko Decky Loader? + Poistaa loaderin. Laajennuksesi ja asetuksesi säilyvät. + Asiakasohjelma + Suorituskyvyn peittokuva + Poista käytöstä, jos pelit kaatuvat tai näyttö mustuu Deck-tilassa. Tulee voimaan seuraavassa istunnossa. + Korjaa Steam-asiakasohjelma + Lataa Steam-asiakasohjelman uudelleen, kun Steam käynnistyy seuraavan kerran. Käytä tätä, jos Steam jää lataus­näytölle. Pelit, kirjautuminen ja asetukset säilyvät. + Steam-asiakasohjelma ladataan uudelleen, kun Steam käynnistyy seuraavan kerran. + Korjaa + Kuvitus + SteamGridDB-API-avain + Oma avaimesi + Muokkaa… + Täyttää puuttuvan kuvituksen Steamista ja sen jälkeen SteamGridDB:stä. Pelin kansiossa oleva oma cover.jpg, hero.jpg, logo.png tai icon.png menee näiden edelle. + Poista %1$s + Poistetaanko? + Lisää peli + Lisää peli: + Valitse exe-tiedosto: + Tämä sijainti ei ole käytettävissä istunnossa + Tarkista tämä exe-tiedosto + Windows-komponentit + LB/RB siirtyvät osioiden välillä. + Ei mitään + Windows-DLL-tiedostoja, joita Proton ei sisällä, komponenttiluettelosta. Ota käyttöön ne, joita tämä peli tarvitsee: ne asennetaan pelin omaan Windows-kansioon, eivät koskaan sen tiedostoihin, ja säilyvät Protonista riippumatta. + Komponenttiluetteloa ei tavoitettu. Jo ladattuja komponentteja voi silti vaihtaa. + Suositellaan tälle pelille + Tarkistetaan, mitä tämä peli tarvitsee… + Käytettävissä verkkoyhteyden kanssa + + Otetaan käyttöön %1$d komponentti… + Otetaan käyttöön %1$d komponenttia… + + Käytössä tässä pelissä + Steam asentaa %1$s:n %2$s:n mukana (sovellus %3$s) + GOGin luettelosta tälle pelille + Epicin vaatimuksista tälle pelille + Amazonin asennusvaiheista tälle pelille + Steam asentaa %1$s:n sovelluksella %2$s, nimen perusteella täsmäytettynä + Myös listalla + %1$s listaa tämän; ei komponentti tässä + Steam + GOG + Epic Games + Amazon Games + Pelissä on mukana %1$s + Pelissä on %1$s (toimii usein sellaisenaan) + Jo tässä pelissä + Mikään pelin kansiossa ei viittaa komponenttiin. + Asennettu tälle pelille (%1$d) + Kaikki komponentit (%1$d) + Vaativat Windows-asennusohjelman (%1$d) + Näillä on omat asennusohjelmansa, joiden sisältä ei voi purkaa pakettia, ja niiden asentamisesta ei ole vielä tallennettua tietoa. Useimmat ovat vanhoja ajoympäristöjä, joiden tilalla toimii jo Protonin sisäinen Mono; tallenteet lisätään sitä mukaa kun niitä tehdään. + Näytä + Piilota + Käynnistää oman asennusohjelmansa (ei tallennetta vielä) + Ei tueta tässä vielä + Ladataan komponentteja… + Asennus epäonnistui: %1$s + Ladattu + Ladataan: %1$s + Asennetaan: %1$s + Poistetaan käytöstä… + Lataus %1$s + Tulee voimaan, kun peli käynnistyy seuraavan kerran. + Venytä 16:9-pelit + Näytöillä, jotka ovat neliömäisempiä kuin 16:9, venyttää pelit täyttämään ruudun palkkien näyttämisen sijaan. Tulee voimaan seuraavassa istunnossa. + Ääni + DirectAudio peleille + Ohittaa PulseAudion pienempää viivettä varten. + Mikrofoni + Äänichattia varten. + Verkko + Wi-Fi-haku + Näytä Wi-Fi-nimet ja lähellä olevat verkot Steamissa. + Sijaintipalvelut ovat pois käytöstä. + Sijaintilupaa ei ole myönnetty. + Avaa asetukset + Jatka + Otetaanko Wi-Fi-haku käyttöön? + Android vaatii sijaintiluvan Wi-Fi-nimien lukemiseen ja lähellä olevien verkkojen löytämiseen. DroidDeck käyttää tätä oikeutta näyttääkseen nämä verkot Steamissa. Emme seuraa sijaintiasi. Internet-yhteys toimii myös ilman tätä lupaa. + Pelien tallennustila + Automaattinen - SD-kortti, kun käytettävissä, sekä Games-kansiosi + Automaattinen - Games-kansiosi, sekä SD-kortti, kun käytettävissä (ei nyt käytettävissä) + Vain sisäinen + Kansio: %1$s + Toinen kirjasto + Pelit, jotka striimaavat materiaalia SD-kortilta tai jaetulta tallennustilalta, voivat nykiä. + Tallennustilan diagnostiikka + Tallentaa varaustulokset ja otetut tallennustilan ajoitusnäytteet Jaa lokit -toimintoon. Voi hidastaa asennuksia käytössä ollessaan. Tulee voimaan seuraavassa istunnossa. + Valitse kansio… + Mukautettu resoluutio + Korvaa rajan ja kuvasuhteen. Näyttöön sopimaton koko saa palkit. + Leveys + Korkeus + Väliltä 320×240–3840×2160. + + %1$d peli + %1$d peliä + + OK + Poista + Palauta + Lataa + Poista %1$s + %1$.1f MB + Ajonaikainen ajuri + Käytössä Steamissa, peleissä ja työpöydällä. Tulee voimaan seuraavassa istunnossa. + Tuo Turnip-zip… + Näyttöajuri + Piirtää kuvat näytölle molemmissa tiloissa. Käynnistä sovellus uudelleen ottaaksesi käyttöön. + Tuo AdrenoTools-zip… + ei koskaan + Nightlies tarkistettu %1$s + Komponentit + Tietoja komponenteista + Tarkista uudet ajurit + Tarkista Nightlies uusien pakettien varalta + Proton + Tässä valittu Proton käynnistää kaikki pelit, jotka eivät ole valinneet omaa Protoniaan Steamissa, ja Steamissa tehty muutos näkyy myös tässä. Steam korvaa Valven Protonin päivittäessään; alkuperäiset säilytetään versiokohtaisesti. + Määritys + Määritä profiili + Profiili + Oletusprofiili + Käytetään automaattisesti peleissä, joilla ei ole ohituksia. + Tallennetut asetukset käytetään automaattisesti, kun %1$s käynnistyy. + Valitse vain tämän pelin tarvitsemat asetukset. Kaikki muu käyttää edelleen Oletusprofiilia. + FEX-esiasetus + Käytössä on Oletusprofiilin esiasetus. + Tälle pelille on tallennettu mukautettu esiasetus. + Mukautetut FEX-muuttujat + Käytä oletusta + Pelin yhteensopivuus + Tämän pelin Proton-, DXVK-, VKD3D- ja synkronointiympäristön asetukset. + Käytössä on Oletusprofiilin Proton. + Tälle pelille on tallennettu mukautettu Proton Steamin yhteensopivuusmäärityksen kautta. + Ei Protonia + DXVK + VKD3D-Proton + Käyttää Oletusprofiilin pakettia kohteesta %1$s, jollei tämä peli valitse toista pakettia. + Käyttää Oletusprofiilin komponenttipakettia. + Käyttää pakettia %1$s vain tässä pelissä. + Ajonaikaiset asetukset + Muut pelikohtaiset Proton-, DXVK-, VKD3D-Proton- ja synkronointiympäristön asetukset. + %1$s:n profiili on tallennettu seuraavaa pelin käynnistystä varten. + Käynnistä ilman Steamia + Pelille, joka sulkeutuu heti: käynnistää sen ilman Protonin Steam-asetuksia. Steamin ominaisuudet, kuten overlay, on pois käytöstä sille. + Päällä + %1$s:n Proton-valinta on tallennettu ja käytettävissä Steamissa nyt. + %1$s:n Proton-valinta on tallennettu. Käynnistä Steam uudelleen ennen pelin käynnistämistä. + %1$s ei voi käyttää valittua Protonia tällä laitteella. + %1$s:n profiilin lataaminen epäonnistui: %2$s + Pelin profiilia ei voitu tallentaa: %1$s + Palauta pelin profiili + Palautetaanko %1$s:n profiili? + Poistetaanko tämän pelin Proton, FEX, komponenttipaketti, ympäristö, tekstuurisuodatus ja Windows-komponenttien ohitukset? Peli käyttää tämän jälkeen Oletusprofiilia. + %1$s käyttää nyt Oletusprofiilia. + Pelin profiilia ei voitu palauttaa kokonaan: %1$s + %1$s käynnistää nyt kaikki pelit, jotka eivät ole valinneet omaa Protoniaan Steamissa. + %1$s käynnistää kaikki pelit, jotka eivät ole valinneet omaa Protoniaan, siitä lähtien kun Steam käynnistyy seuraavan kerran. + %1$s ei voi käynnistää pelejä tällä laitteella, joten se ei voi olla oletus. + Oletus-Protonia ei voitu tallentaa: %1$s + %1$s · %2$d vaihdettu + %1$s · kaikki alkuperäisiä + %1$s · peli käynnissä + %1$s · droiddeck-esync-paketti + %1$s · ei droiddeck-esync-pakettia vielä + %1$s · esync sisäänrakennettu, ei pakettia tarvita + GPU-ajurit + %1$s · otettu uudelleen käyttöön käynnistyksessä %2$s + Seuraavaksi: %1$s · pelin sulkeutumisen jälkeen + %1$s käytössä: %2$s + Odottaa, kunnes peli sulkeutuu + Tulee voimaan, kun peli käynnistyy seuraavan kerran + Tuo .zip + Luetaan Protoneja… + Protonia ei ole vielä asennettu. Käynnistä Steam kerran, jotta sen ARM64-Proton latautuu, tai lisää GE-Proton, CachyOS tai DroidDeck-Proton Käyttöönotto-sivulta. + Alkuperäinen · %1$s + %1$s · tämä versio + %1$s · aiempi versio + Asennettu · %1$d + Mitään ei ole tallennettu %1$s:lle vielä. Tämän Protonin omat tiedostot tallennetaan sen alkuperäisenä. + Tällä Protonilla on peli käynnissä, joten tämä odottaa, kunnes se sulkeutuu. + Tulee voimaan, kun peli käynnistyy seuraavan kerran. + Vaihdetaanko %1$s? + Asetetaanko %1$s kohteeseen %2$s? Sen alkuperäiset tiedostot pysyvät tallessa. %3$s + Palautetaanko %1$s? + Palautetaanko %1$s:n alkuperäinen kohteeseen %2$s? %3$s + Poistaa paketin sovelluksesta. Voit latata sen uudelleen. + Poistetaanko tämä alkuperäinen? + Tämä %1$s:n alkuperäinen on aiemmasta %2$s-versiosta. Nykyisen version alkuperäinen säilytetään aina. + Nightliesissä · %1$d + Päivitä nähdäksesi Nightliesin paketit. + Ei mitään uutta viimeisimmän tarkistuksen jälkeen. + Tuo .wcp + Palauta tämän Protonin alkuperäinen + Palautetaanko %1$s:n alkuperäinen %2$s? + Vaihda + FEX, DXVK ja VKD3D-Proton kullekin Protonille, jolla Steam käynnistää pelejä.\n\nValitse asennettu rivi ottaaksesi sen käyttöön. Se tulee voimaan, kun peli käynnistyy seuraavan kerran, tai kun käynnissä oleva peli sulkeutuu.\n\nValintasi tarkistetaan uudelleen ennen jokaista käynnistystä ja palautetaan, jos jokin on muuttanut niitä. Kunkin Protonin alkuperäiset tiedostot säilytetään, niin Steamin päivitykset eivät koskaan hävitä niitä. + Alkuperäinen + Käytössä + Uusi + Tallennettu + Suorituskyky + CPU-ytimet, istunnon korjaukset ja isäntänimi. Tulee voimaan seuraavassa istunnossa. + Steam-asiakasohjelman ytimet + Ohita Steamin ydinvalinta + Kiinnittää Steamin, sen käyttöliittymäavustajan ja gamescopen alla oleviin ytimiin. + Asiakasohjelman ytimet + Ytimet Steamille. + Ota ohitus käyttöön valitaksesi. + Pelin ytimet + Pelin ytimet + Ytimet peleille. Kun kaikki on valittuna, Android ajoittaa vapaasti. + Asiakasohjelman käyttöliittymä + Säikeistetty GL + Voi tehdä Steamin valikoista reagoivampia. + Zink: laiskat deskriptorit + Suositellaan ajureille, joissa ei ole deskriptoripuskureita. + Ohita GL-virhetarkistukset + Poistaa käytöstä kutsukohtaisen GL-validoinnin. + gamescope: reaaliaikainen GPU-jono + Priorisoi kompositorin GPU-työtä. Voi tasoittaa kuvanopeuden tahditusta pelien kustannuksella. + GPU + Pidä GPU huippukellotaajuudessaan + Vain Adreno. Vähemmän nykäyksiä, enemmän akun kulutusta ja lämpöä. + Istunnon korjaukset + Turnip: sysmem-renderöinti + Nopeampi Steamille ja useimmille peleille. Pakollinen Adreno 710/720/722:lla. + Ohita Steamin xalia-avustaja + droiddeck-ntsync (kokeellinen) + Käyttäjätilainen korvike Linuxin ntsync-ajurille, testausta varten. Kun se on päällä, Proton käyttää sitä droiddeck-esyncin ja droiddeck-fsyncin sijaan. Toimii Proton Experimental ARM64:n, GE-Protonin ja proton-cachyosin kanssa. Tulee voimaan seuraavassa istunnossa. + droiddeck-esync + droiddeck-fsync etusijalle + Käynnistää Protonin omaa fsynciä vakiotiedostoilla, futex_waitv tarjottuna käyttäjätilassa, niin se ei tarvitse pakettia ja toimii edelleen heti kun Steam päivittää Protonin. Kun se on päällä, pelit käyttävät sitä droiddeck-esyncin edellä, paitsi harvat, jotka Proton pitää pois fsyncistä. Tulee voimaan seuraavassa istunnossa. + Pelien Windows-säikeet odottavat toisiaan eventfd:n ja jaetun muistin avulla sen sijaan, että kiertäisivät wineserverin kautta. Käyttää juuri tälle Proton-versiolle tehtyä droiddeck-esync-pakettia; paketit haetaan automaattisesti. Kunnes sellainen on olemassa uudelle Proton-versiolle, droiddeck-fsync toimii sen tilalla. Tulee voimaan seuraavassa istunnossa. + Poistaa käytöstä Protonin ohjaimen navigointiavustimen, joka kuluttaa suoritintehoa jokaisessa pelissä. + Suorita proot ilman seccompia + Voi korjata puuttuvan järjestelmäkutsun virheitä joissakin ytimissä. Voi heikentää suorituskykyä. + proot-pikareitti + Vaatii prootin seccomp-suodattimen. + Nopeampi tiedostojen hakeminen ja vähemmän nykimistä. Poista käytöstä, jos ohjelma ei löydä tiedostojaan. + Istunnon identiteetti + Isäntänimi + Nimi, jonka Steam näkee tälle laitteelle. Tyhjä palauttaa arvon %1$s. + Vain kirjaimia, numeroita ja sisäisiä yhdysmerkkejä, enintään 63 merkkiä. + Android on asetettu lopettamaan tämä istunto + Asennettu + Ladattavissa + Ei mitään uutta viimeisimmän tarkistuksen jälkeen. + Palauta sisäänrakennetut ajurit + Sisäänrakennettu sovellukseen: tämä poistaa sen puretut tiedostot ja piilottaa sen. Palauta sisäänrakennetut ajurit tuo sen takaisin. + Puolikas Android + Linux -paketista: molemmat puoliskot poistetaan. Käytössä oleva ajuri palaa oletukseen. + Sen tiedostot poistetaan sovelluksesta. + Sen tiedostot poistetaan. Se on käytössä, joten oletus ottaa sen paikan. + Näyttö + Tehosteet + Ohjaimet + Komponentit + Ohjain + Asetukset + %1$s-sivu + Steam + Työpöytä + Steam-valikko + Pikavalikko + Avaa pikavalikko + Edellinen sivu + Seuraava sivu + Suorituskyky-HUD + Skaalaustila + Skaalauksen terävyys + Näyttötehosteet + Reaaliaikainen. Kuluttaa hieman GPU-aikaa, kun jokin tehoste on päällä. + Esiasetukset + Mukautettu + Lisäasetukset + Laajennettu + Supistettu + Terävöinti (CAS) + Terävöinnin taso + Keinotekoinen HDR + Raidanpoisto + Raidanpoiston voimakkuus + Kirkkaus + Kontrasti + Gamma + Kylläisyys + FXAA (reunan pehmennys) + Piirrosääriviivat + CRT + NTSC + Tekstuurisuodatus + DirectX 9–11 -pelit, niiden seuraavasta käynnistyksestä alkaen. + Anisotrooppinen suodatus + Tekstuurien terävyys + Negatiivinen mip-poikkeama: terävämmät tekstuurit, jonkin verran kimmellystä. Automaattinen vastaa sitä, kuinka paljon istunto on suurennettu näytölle. + Automaattinen (%1$s) + Näppäimistö + PC-näppäimistö + Android + Toinen näyttö + Tila + Näyttää + Android-sovellukset + Käynnistä sovellus + Piilota + Näytä + Ei käynnistettäviä sovelluksia + Istunnon toiminta + Tulee voimaan nyt ja tuleviin istuntoihin tässä tilassa. + Seuraava istunto + Tuki + Istunnon lokit + Liitä virheraporttiin + Jaa lokit + Tausta + Lopeta + Lopetetaanko istunto? + Käynnissä nyt + Mikään peli ei ole käynnissä Protonilla + Pelin profiili + Profiilin tiedot eivät ole vielä saatavilla. + Pelien profiileja ei voitu ladata. Yritetään automaattisesti uudelleen. + Pelien profiileja ei voitu päivittää. Näytetään viimeisin saatavilla oleva tieto; yritetään automaattisesti uudelleen. + Tarkastele ja muokkaa tallennettuja asetuksia, joita käytetään seuraavassa käynnistyksessä. + Käytössä on Oletusprofiili + Mukautettu profiili + Käytössä on %1$s Oletusprofiilista. + Muutokset tallennetaan %1$s:n profiiliin ja tulevat voimaan, kun peli käynnistyy seuraavan kerran. + + %1$d pelikohtainen ohitus + %1$d pelikohtaista ohitusta + + Protonia ei ole vielä asennettu. + peli käynnissä + otettu uudelleen käyttöön käynnistyksessä %1$s + Alkuperäinen + Tuo ja poista Komponentit-sivulla. + Valmis + Vaatii huomiota + Käsitellään + Tarkista Langattoman virheenkorjauksen IP-osoite ja portti, tai pariuta uudelleen, jos Android poisti tämän laitteen. + Asenna + Hallitse + Yleiskatsaus + Ohjain + Istunto + Käynnistin + Käyttöönotto + ● Kaikki valmiina + %1$d/%2$d valmiina + Järjestelmätarkistus + Laite tuettu + Testaamaton GPU + GPU ei ole tuettu + %1$s · %2$s + %1$s: %2$s + Steam tarvitsee Adreno-ajurin; %1$s saattaa näyttää mustan ruudun + Linux-ajoympäristö + %1$s · %2$d%% + Ei asennettu · noin 3 GB, asennetaan ensimmäisellä Pelaa-painalluksella + Asennettu + %1$s · päivitys saatavilla + %1$s · ajan tasalla + Lapsiprosessien raja + Päällä · Android voi sulkea Steamin. Pois ottaminen kestää minuutin. + Ei asetettu · Android voi sulkea Steamin. Pois ottaminen kestää minuutin. + Ei voitu tarkistaa · pois ottaminen kestää minuutin. + Korjaa + Tiedot + Kehittäjäasetukset + Käytä Langatonta virheenkorjausta + Tarkista uudelleen + Ota raja käyttöön + Steam-tili + Kirjautuneena nimellä %1$s · offline-tila + Kirjautuneena nimellä %1$s + Paina Pelaa ja kirjaudu Steamiin + Työkalut + Ohjain + Ohjaimen asetukset eivät ole käytettävissä. + Istunto + Istunnon lokit + Tallennetaan joka istunnon jälkeen + Vianmääritystyökalut + Viimeisimmät istunnon lokit + Tallennetut istunnon lokit + Säilyttää %1$d uusinta + Tyhjennä lokit + Offline-tila + Kirjaudu Steamiin ensin + Käynnistin + Kieli + Steam käyttää sitä myös, seuraavasta käynnistyksestä alkaen. + Järjestelmän oletus (%1$s) + Järjestelmän oletus + Sovelluksen skaalaus + DroidDeckin tekstit ja säätimet + %1$d %% (Oletus) + Teema + Käynnistimen ulkoasu + Käytä Aloitusnäyttönä + DroidDeck voi olla kotisovellus + DroidDeckiä ei koskaan tarjota kotisovellukseksi + Suunta + Automaattinen vaakasuunta + Vaakasuunta + Käänteinen vaakasuunta + Koko näyttö + Piilottaa Androidin tila- ja navigointipalkit + Näyttää Androidin tila- ja navigointipalkit + Oletuskotisovellus + Valitse kotisovellus + Valitse + Linux-sovellukset (beta) + Tiedostot + Selaa ja hallitse tiedostoja + Proton-versiot + Asenna ARM64-Proton-versioita + Suorituskyky + CPU-ydinten määritys + ROM-kansio + Mihin emulaattoripelit tallennetaan + Komponentit + FEX, DXVK, VKD3D + Ohjaimet + Painikkeiden kartoitus + Löydä + Hae + Asennetut + %1$s · %2$d + Kauppa + Flathub · ARM64 + ● Flatpak valmis + Asenna Linux-ajoympäristö ensin Käyttöönotto-sivulta. + Ota Flatpak käyttöön + Lisää Flatpakin ja yhdistää sen Flathubiin (noin 20 MB). Ensimmäinen sovellus lataa myös oman ajoympäristönsä. + Otetaan käyttöön… + Flatpak + Päivitykset + %1$s · %2$s + %1$s · %2$s · %3$d%% + Käynnistyy… + Luokat + Flathubia ei tavoitettu. Tarkista yhteytesi ja yritä uudelleen. + Yritä uudelleen + Hae Flathubista + Firefox, pelit, emulaattorit… + Hae + Kaikki + Haetaan… + Hae nimellä tai valitse luokka. Vain ARM64-sovellukset näkyvät listalla. + Flathubista ei löytynyt tätä vastaavia ARM64-sovelluksia. + Tulokset + Tarkista päivitykset + Päivitä kaikki (%1$d) + ● Kaikki on ajan tasalla + Ei sovelluksia vielä. Löydä niitä Löydä- tai Hae-välilehdeltä. + Ota Flatpak käyttöön asentaaksesi sovelluksia. + Päivitys saatavilla · %1$s + Avaa + Tiedot + Poista + Paina uudelleen poistaaksesi + Asennetaan… + Asenna + Ota Flatpak käyttöön ensin + Lataus %1$s + ● Asennettu + Tämän sovelluksen Flathub-sivua ei voitu ladata. + Ladataan Flathubista… + Flathubissa ei ole ARM64-versiota, joten sitä ei voi asentaa tässä. + Kuvakaappaukset + Tietoja + Sovellustunnus + Lisenssi + Ajoympäristö + Asennuksen koko + %1$s (lisäksi jaettu ajoympäristö) + Verkkosivu + Vahvistettu + Päivitykset + DroidDeck %1$s · %2$s + Tarkistettu %1$s + Ei tarkistettu vielä + Anna DroidDeckin päivittää itsensä + Ota seuraavalla näytöllä käyttöön Salli tästä lähteestä ja palaa takaisin. Päivitys jatkuu itsekseen. + Avaa asetukset + Ei nyt + Allekirjoitettu eri tavalla + Ei voida päivittää paikan päällä + Tätä kopiota ei ole allekirjoitettu DroidDeckin julkaisuavaimella, niin päivitykset eivät voi asentua sen päälle. Poista se, jos haluat vaihtaa. + Päivitys saatavilla + Ei voida asentaa + Valmis vaihdettavaksi + Stable-versiota edellä + Olet Stable-versiota edellä + Tämä build on uudempi kuin viimeisin Stable. Siirryt Stableen sen seuraavan julkaisun myötä. + Testi päättyi + Tämä testi on päättynyt + Sen korjaus yhdistettiin tai hylättiin. Seuraa Previewiä saadaksesi uusimmat korjaukset. + Ajan tasalla + Sinulla on uusin %1$s + · %1$s + %1$s… %2$d %% + %1$s… + Päivitä%1$s + Asenna%1$s + Asenna Stable %1$s silti + Seuraa Previewiä + Lopeta käynnissä oleva istunto päivittääksesi. + Vähemmän + Lisää + DroidDeck %1$s + Uusi Preview-build + PR #%1$d -testibuild + %1$d MB + PÄIVITYSKANAVA + Stable + Testatut julkaisut, sopii useimmille + %1$s · %2$s + Preview + Uusimmat korjaukset, ennen Stablea + Ajan tasalla + Testibuildit + Kokeile korjausta ennen sen julkaisua + Ei yhtään juuri nyt + PR #%1$d + Ei rakennettu tälle DroidDeck-kopiolle + Päivitettiin %1$s + PR #%1$d -testi + hetki sitten + juuri nyt + + %1$d kokeiltavana + %1$d kokeiltavana + + + Muutoksia buildisi jälkeen · %1$d build jäljessä + Muutoksia buildisi jälkeen · %1$d buildia jäljessä + + + Viimeisimmät muutokset · %1$d build + Viimeisimmät muutokset · %1$d buildia + + Yksi Android-asetus muutettavaksi + Steam + Android pysäyttää sovelluksia, jotka käynnistävät paljon taustaprosesseja, ja Steam käynnistää niitä kymmeniä. Kunnes tämä muuttuu, pelit sulkeutuvat ilman virheilmoitusta. + Korjaa se puolestani + Ota käyttöön ”Disable child process restrictions” ja palaa takaisin. Jätä Kehittäjäasetukset päälle, tai kytkin kytkeytyy taas pois päältä. + Avaa Kehittäjäasetukset + Tässä Android-versiossa ei ole kytkintä tälle, niin DroidDeck muuttaa sen Langattoman virheenkorjauksen kautta. Kestää noin minuutin, ei tarvita tietokonetta. + Langaton virheenkorjaus vaatii Wi-Fi-yhteyden. Mikä tahansa verkko käy; internet-yhteyttä ei tarvita. + Pariutuskoodi tulee ilmoitukseen. Salli ilmoitukset DroidDeckille, tai käytä Muita tapoja. + Salli ilmoitukset + Avaa Asetukset → Kehittäjäasetukset → Langaton virheenkorjaus ja kytke se päälle + Napauta ”Pariuta laite pariutuskoodilla” + Kirjoita koodi DroidDeckin ilmoitukseen + Odotetaan pariutuksen ponnahdusikkunaa… + Ponnahdusikkuna löytyi. Syötä sen koodi ilmoitukseen. + Tila + Tarkistetaan 2 sekunnin välein. Tämä sivu sulkeutuu, kun se on pois päältä. + Muita tapoja + Langaton virheenkorjaus, manuaalinen osoite, tietokoneen ADB + Manuaalinen osoite, tietokoneen ADB, Kehittäjäasetukset + Käytä sen sijaan Langatonta virheenkorjausta + Jotkin ROM-versiot lisäävät kytkimen joka tapauksessa. Etsi ”child process” Kehittäjäasetuksista. + Jos ilmoitus ei toimi, syötä ponnahdusikkunan osoite ja koodi itse. + Syötä osoite manuaalisesti + Tietokoneelta ADB:llä: + Kopioi komento + Valitse, millä näytöllä Android-asetukset avataan. + Päänäyttö + Pariutui. Etsitään Langattoman virheenkorjauksen porttia… + Pariutui. Syötä portti Asetuksista. + Laite löytyi. Otetaan asetus käyttöön… + Automaattinen käyttöönotto epäonnistui. Tarkista alla oleva osoite ja yritä uudelleen. %1$s + Langaton virheenkorjaus + Lapsiprosessien raja päivitetty + Muuta lapsiprosessien rajaa + Jos asetus puuttuu, pariuta Langattoman virheenkorjauksen kautta. + Pariutui. Tarkista IP-osoite ja portti Langattomasta virheenkorjauksesta. + Lapsiprosessien raja päivitettiin. + Käytä vaihetta 2, jos Kehittäjäasetuksissa ei ole lapsiprosessiasetusta. Avaa Kehittäjäasetukset → Langaton virheenkorjaus → Pariuta laite pariutuskoodilla. + Pariutui. Käytä alla olevaa osoitetta, tai päivitä se Langaton virheenkorjaus → IP-osoite ja portti -kohdasta. + Lapsiprosessien raja päivitettiin Langattoman virheenkorjauksen kautta. + Android vahvisti, että raja on päällä. + Android vahvisti, että raja on pois päältä. + 2 · Pariuta Langaton virheenkorjaus + Pariutuksen ponnahdusikkunan IP-osoite ja portti + Pariutuskoodi (PIN) + 6 numeroa + Pariuta + 2 · Ota käyttöön Langattoman virheenkorjauksen kautta + Yhteyden IP-osoite ja portti + Pariuta uudelleen + Kytke raja pois päältä + Muita tapoja muuttaa sitä + 1 · Kehittäjäasetukset (kokeile ensin) + Aseta ”Restrict child processes” päälle. + Aseta ”Restrict child processes” pois päältä. + 3 · Tietokoneen ADB (viimeinen keino) + Vain jos Langaton virheenkorjaus ei ole käytettävissä. + 3 · Tietokoneen ADB (viimeinen keino) + Kun kosketusohjaimet näkyvät istunnossa + Väri + Näytön painikkeiden sävy + Mukautettu + Peittävyys + %1$d %% + Painikkeiden koko + 100% on vakiokoko + Sauvan napsautus + Kaksoisnapauta ja pidä sauvaa pohjassa saadaksesi L3:n tai R3:n + Mukautuvat sauvat + Pelin tärinä + Steam-painike + QAM-painike + Näppäimistöpainike + Avaa Steam-valikko Select-painikkeella + View/Select-painikkeen sijaan. + Animaatiot + Sauvat tulevat näkyviin siihen, mihin kosketat, ja piiloutuvat, kun vapautat + Asettelu + Mukautetut sijainnit tallennettu + Sovitettu tähän näyttöön + Muokkaa + Palauta + Painikkeiden kartoitus + Jokainen painike lähettää oman syötteensä + %1$d/%2$d painiketta uudelleenkartoitettu + Määritä + Palauta ohjain + Palauttaa kaikki ohjaimen asetukset ja asettelun + Mitä mikä näytön painike lähettää pelille. Piilotettu poistaa painikkeen. + Näytön painikkeet + Oletukset + Palauta kartoitus + Android-sovellukset + %1$d sovellusta + Käynnistettäviä Android-sovelluksia ei löytynyt. + ● Työpöytä asennettu + Asentuu ensimmäisellä avauksella + Saatavilla asennettavaksi + Ei asennettu. + %1$s · ei asennettu + Avaa %1$s + Pelit + Valitse ROM-kansio. + Selaa kohteeseen /root/ROMs RetroArchissa. + Lisää %1$s-pelit kansioon ROMs/%2$s. + Asenna %1$s + Lopeta istunto asentaaksesi + Ajoympäristö vaaditaan + Ladataan asennustietoja… + Asennustiedot eivät ole käytettävissä. Yritä myöhemmin uudelleen. + Asenna Linux-ajoympäristö ensin Käyttöönotto-sivulta. + Lopeta käynnissä oleva istunto asentaaksesi työpöytäsovelluksia. + Asentaa %1$s Linux-työpöydälle. + %1$s · %2$d %% + Piilottaminen poistaa sen Työpöydältä. Sen tiedostot pysyvät Linux-ajoympäristössä. + Tämä peli ei ole enää ROM-kansiossa. + Työpöytä · %1$s + Käynnistä kohteessa %1$s + Myös kohteessa %1$s + Tätä sivua ei ole enää. Valitse osio vasemmalta. + Avaa työpöytä + Asenna ja avaa työpöytä + Poista työpöytä + Poistaa työpöydän ja siihen kuuluvat sovellukset. Säilyttää pelit, lisätyt sovellukset, asetukset ja jaetut kirjastot. + Poistetaan työpöytää + Työpöytä poistettu + Poistetaan vanhoja LXQt-tiedostoja + Päivitä DroidDeck ennen tämän työpöytäversion poistamista. + Linux-työpöytä + KDE Plasma, Firefox ja emulaattorisi + Steamissa asentamasi pelit näkyvät tässä istunnon jälkeen. + Pelaa Steamissa + Käynnistysasetukset + Käynnistä + Pelitiedostot + Proton-prefix + Etsitään tallennuksia… + Nimellä löydettävää tallennuskansiota ei löytynyt. Vienti ottaa koko käyttäjäkansion. + %1$d tallennuskansiota + %1$s · %2$d tiedostoa · %3$s + %1$d kt + Hallitse tallennuksia + Pelitallennukset + Tuo tallennuksia… + Vie GameHub-zipinä… + Vie Winlator-zipinä… + Avaa tallennuskansio + Ei pelattu koskaan + Ajoympäristö varattu · %1$d %% + Ajoympäristö varattu + Ajoympäristö asentuu ensimmäisellä Pelaa-painalluksella + Ajoympäristön päivitys saatavilla + ● Ajoympäristö valmis + Viimeksi pelattu %1$s + Sisäinen tallennustila + Lisätty peli + Mukautettu peli + Pelien tallennustila + Parhaiten tähän GPU:hun sopiva pari, pidetään ajan tasalla + Valitse pari itse + Tarkistetaan ajureita… + Ei ajureita listattuna vielä. Päivitä etsiäksesi niitä. + Piilota muiden GPU:iden ajurit + Näytä muiden GPU:iden ajurit + Lisäasetukset + Asetetaan yhdessä näyttöajurin kanssa: %1$s kattaa molemmat. + Asetetaan yhdessä ajoympäristöajurin kanssa: %1$s kattaa molemmat. + Käytetään peleissä ja työpöydällä. + Ajoympäristön oletus + Piirtää kuvat näytölle. Käytössä DroidDeckin uudelleenkäynnistyksen jälkeen. + Automaattinen - valittu GPU:n mukaan + Lataa, tuo tai poista… + KÄYTÖSSÄ + DroidDeckiin sisäänrakennetut ajurit + Ajoympäristö- ja näyttöajurit asetetaan yhdessä toisiinsa sopivana parina. + Suositellaan tälle GPU:lle: %1$s. + Päivitä etsiäksesi ajureita tälle GPU:lle. + SUOSITELTU + Ei saatavilla + Linux-ajoympäristö + Valmistellaan Linux-ajoympäristöä + Istunto + Käynnistetään näyttöä ja ääntä + Steam-asiakasohjelma + Tarkistetaan Steamia ja sen yhteensopivuustyökaluja + Avataan Steam + Odotetaan Steamin ensimmäistä kuvaa + Työpöytä + Valmistellaan työpöytää + Avataan + Odotetaan ensimmäistä kuvaa + Käynnistetään Steamia + Istunto päättyi + Vaihe %1$d/%2$d + Steam on keskeytetty + Kahvat + Vasen pad + Oikea pad + Molemmat + Peliohjain + Tatti + Kosketuslevy + Vaihda tatin ja kosketuslevyn välillä + Lisää Deck-ohjaimia + PANEELIT + Sulje toisen näytön ohjaimet + Avaa Steam-valikko + Vasen · ylä + Vasen · ala + Oikea · ylä + Oikea · ala + Napsautus + Oikea kosketuslevy + Vasen kosketuslevy + Kosketuslevy + Näytä näppäimistö + Piilota näppäimistö + Linux-pääte + Linux-istunto ei ole vielä valmis päätteelle. + Steam + Pelit + Työpöytä + Kauppa + Komponentit + Sovellukset + Asetukset + Päivitykset + %1$d %% + Jatka: %1$s + Näkyy sessiossa polussa /root/ROMs. Sisäinen tallennustila on /root/Storage. + Kansiota ei valittu + Vaikuttaa seuraavaan istuntoon. SD-kortit toimivat. + Valitse kansio + Vaihda kansio + Asenna ARM64-Proton-versio ja valitse se pelikohtaisesti Steamissa kohdasta Properties → Compatibility. + Pysäytä käynnissä oleva istunto, jotta voit asentaa tai poistaa yhteensopivuustyökalun. + Saatavilla olevat versiot + Asennettu: %1$s + Odottaa aiempaa pyyntöä + Ei asennettu + Päivitys saatavilla → %1$s + Poista vanhat %1$s-versiot + Poistaa %1$s. Näihin versioihin asetetut pelit on vaihdettava toiseen Protoniin. + Tähän versioon asetetut pelit on vaihdettava toiseen Protoniin. + Asenna nyt + Peruuta jonotus + Android-näppäimistö + Piilota ✕ + Käynnistä %1$s + Valitse näyttö. Toinen näyttö: %1$s. + Toinen näyttö ei ole enää käytettävissä. + Ensisijainen näyttö + Toinen näyttö + Edellinen välilehti + Seuraava välilehti + Päällä + Kaikki %1$d + Ei mitään + %1$d/%2$d + Pois + Alanäyttö + Vaikuttaa seuraavaan pelin käynnistykseen. Pelin omat asetukset ohittavat yhteiset. + Toinen peli sovellustunnuksella… + Valmis + Oletus + Pois + Muuta… + Muuta muuttujaa + Palauta oletus + Proton-pelit + Kuva kuvassa + Siirry automaattisesti kuva kuvassa -tilaan + Ohjaimet avattuna näytöllä + Avattuna peli sijaitsee Steam Deckin kosketuslevyjen ja takakahvojen yläpuolella. Taitettuna peli on koko näytön kokoinen. + Keskeytetty + Kuva kuvassa -tila on käytettävissä, kun istunto on valmis. + Salli kuva kuvassa -tila DroidDeckille Android-asetuksissa. + Android ei pystynyt avaamaan kuva kuvassa -tilaa. + Pikakuvake + Lisää aloitusnäytölle + Valitse peli + Etsitään asennettuja pelejä… + Kopioi käynnistyslinkki + Käynnistyslinkki kopioitu + Tämä käynnistin ei voi lisätä pikakuvakkeita. Käytä sen sijaan Kopioi käynnistyslinkki -toimintoa. + Virheellinen pelin käynnistyslinkki. + Tämä peli ei ole enää DroidDeck-kirjastossasi. + Pysäytä nykyinen istunto ennen Steam-pelin käynnistämistä. + Pelin lähettäminen Steamiin epäonnistui. Yritä käynnistää se uudelleen. + Yritä poistoa uudelleen + Poistetaan + Poisto kesken + Vie pelitiedosto + Synkronoi kaikki pelitiedostot kansioon + Pysäytä pelitiedostojen synkronointi + Valitse kansio käyttöliittymän pelitiedostoille + Pelitiedostot tallennettu kohteeseen %1$s + Käyttöliittymän tiedostot + + Odota, että ajoympäristötoiminto päättyy + Odota, että yhteensopivuustyökalun asennus päättyy + Odota, että työpöytäsovelluksen asennus päättyy + Odota, että asennus päättyy ennen pelin käynnistämistä + Sulje Steam-istunto ensin, jotta peli ei voi tallentaa tuonnin päälle + Tuodaan kohteeseen %1$s + + Tuotiin %1$d tiedosto %2$s-muodosta kohteeseen %3$s + Tuotiin %1$d tiedostoa %2$s-muodosta kohteeseen %3$s + + + Tuotiin %1$d tiedosto %2$s-muodosta kohteeseen %3$s. Vanhat tallennukset varmuuskopioitu kansioon Download/DroidDeck/Saves/backups + Tuotiin %1$d tiedostoa %2$s-muodosta kohteeseen %3$s. Vanhat tallennukset varmuuskopioitu kansioon Download/DroidDeck/Saves/backups + + Valitse tallennusten zip-tiedosto kohteelle %1$s + Viedään %1$s + + Vietiin %1$d tiedosto %2$s-muotoisena: %3$s + Vietiin %1$d tiedostoa %2$s-muotoisena: %3$s + + Valitse, minne %1$s (%2$s) tallennetaan + %1$s epäonnistui: %2$s + Ei istuntolokeja vielä: suorita istunto ensin. + Ei tyhjennettäviä istuntolokeja. + + Tyhjennettiin %1$d istuntolokikansio. + Tyhjennettiin %1$d istuntolokikansiota. + + Langattoman virheenkorjauksen laitepariutus epäonnistui + Langattoman virheenkorjauksen komento epäonnistui + Langaton virheenkorjaus ei ole käytettävissä + ADB-komento kopioitu + Valitse ROMs-kansio + Ei Adreno-grafiikkapiiri + Turnip tukee Adreno-grafiikkapiirejä. Laitteella %1$s Steam voi näyttää mustan ruudun. Latauksen koko: %2$.0f MB. + Asenna silti + Poista Linux-ajoympäristö + Tämä poistaa ajoympäristön, siihen asennetun Steam-asiakasohjelman ja kaikki sinne asennetut pelit. + Otettu käyttöön: %1$s + Sovelluksen %1$s avaaminen ensisijaisella näytöllä epäonnistui + Sovelluksen %1$s avaaminen näytöllä %2$s epäonnistui + Sovelluksen %1$s avaaminen näytöllä %2$d epäonnistui + Tuodaan + Vaihdetaan + Palautetaan + Perutaan + Poistetaan + tiedostoa ei voi lukea + Tuotiin %1$s + Odottava vaihto peruttiin. + Valitse komponenttipaketti (-linux .wcp) + Nightlies-julkaisuihin ei saatu yhteyttä + Tallennettu %1$s + Lataus epäonnistui: %1$s + Valitse Linux-ajoympäristön ajuri (-Linux-zip tai Android + Linux -paketti) + Valitse näyttöajuri (AdrenoTools-zip tai Android + Linux -paketti) + Valitse ajuri-zip tai Android + Linux -paketti + %1$s: %2$s + Poistetaan %1$s… + Unohdetaan %1$s… + Valitse pelien tallennustilakansio + Valitse Decky-laajennuksen ZIP-tiedosto + Ei käytettävissä: %1$s + Ohjelma + Poistetaan Linux-ajoympäristöä + Asetuksia ei voitu avata alanäytöllä; avattiin sen sijaan päänäytöllä. + Avaa Kehittäjäasetukset Asetuksissa + + Androidin \"Tuntemattomien sovellusten asentaminen\" -asetusta ei voitu avata + Tarkistus epäonnistui: %1$s + Tälle versiolle ei ole latausta tähän DroidDeck-kopioon + Pysäytä käynnissä oleva istunto ennen päivitystä + Latausta + Asennusta + Odotetaan vahvistustasi + Päivitys epäonnistui + Android ei tarjonnut päivityksen asentamista + + Deckyn asennus epäonnistui: %1$s + Aloitetaan laajennuksen tuontia… + Laajennuksen tuonti epäonnistui: %1$s + Laajennus asennettu. Käynnistä Steam uudelleen, jotta se latautuu. + + Ei tarkistettu vielä - napauta päivitä etsiäksesi uusia ajureita + Android + Linux: asettaa näyttöajurin sen mukana + Android + Linux: asettaa ajoympäristön ajurin sen mukana + ajoympäristöön sisäänrakennettu Turnip + järjestelmän Vulkan: ei paketoitua versiota tälle grafiikkapiirille + %1$s (mukana) + Manuaalinen + %1$s · %2$s + Vain toinen puolisko on julkaistu juuri nyt + %1$.0f MB molemmille + Ei asetettavia ajureita: %1$s + Tarkistetaan uusimpia ajureita… + Ajan tasalla (tarkistus epäonnistui: %1$s) + Suositeltuja ajureita ei ole julkaistu juuri nyt + Ladataan %1$s %2$s… + Päivitetty versioon %1$s %2$s. + Päivitetty versioon %1$s %2$s. Näyttöajuri otetaan käyttöön, kun DroidDeck käynnistyy uudelleen. + Vaihdettu versioon %1$s %2$s. + Vaihdettu versioon %1$s %2$s. Näyttöajuri otetaan käyttöön, kun DroidDeck käynnistyy uudelleen. + Käytössä %1$s %2$s. + Käytössä %1$s %2$s. Näyttöajuri otetaan käyttöön, kun DroidDeck käynnistyy uudelleen. + Ei ajuri-zip: ei AdrenoTools-ajuri, -Linux-Turnip eikä Android + Linux -paketti + Tuonti epäonnistui: %1$s + Ajuri tuotu + Käytetään %1$s sekä ajoympäristön että näytön ajurina. + Käytetään %1$s sekä ajoympäristön että näytön ajurina. Näyttöajuri otetaan käyttöön, kun DroidDeck käynnistyy uudelleen. + Poistettu %1$s (molemmat ajurit) + Poistettu %1$s + Uusimmat: %1$s · tarkistettu %2$s + Uusimmat: %1$s · %2$s ei tavoitettavissa · tarkistettu %3$s + + %1$d min sitten + %1$d min sitten + + + %1$d h sitten + %1$d h sitten + + + %1$d päivä sitten + %1$d päivää sitten + + Tarkistetaan Banners-Turnip, WinNative ja DroidDeck… + tarkistus epäonnistui + Asennettu %1$s - valitse se kummasta tahansa listasta asettaaksesi molemmat ajurit + Asennettu %1$s - valitse se valikosta + + Odota, että ajoympäristön poisto päättyy + Peli + kosketuslevy + suora + Vaihto epäonnistui: %1$s + Profiilin tallennus epäonnistui: %1$s + Työpöytä on keskeytetty + %1$s on keskeytetty + Ohjelma on keskeytetty + Käynnistetään työpöytää + Käynnistetään %1$s + Käynnistetään ohjelmaa + Ei lokeja tälle istunnolle. + Istuntolokien arkistoa ei voitu luoda. + Istuntolokeja ei voitu jakaa. + Käynnistetään istuntoa… + ladataan Linux-ajoympäristöä + ladataan Linux-ajoympäristöä · %1$d/%2$d MB + tarkistetaan Linux-ajoympäristöä + puretaan Linux-ajoympäristöä + ladataan työpöytää + ladataan työpöytää · %1$d/%2$d MB + tarkistetaan työpöytää + puretaan työpöytää + ladataan Proton Experimentalia (ARM64) + ladataan Proton Experimentalia (ARM64) · %1$d/%2$d MB + tarkistetaan Proton Experimentalia (ARM64) + puretaan Proton Experimentalia (ARM64) + Ajoympäristöluetteloon ei saatu yhteyttä. Tarkista yhteys ja paina Play uudelleen. + Linux-ajoympäristö ei asentunut. Tarkista ajoympäristö Asetuksissa ja paina Play uudelleen. + Työpöytä ei asentunut (%1$s). Tarkista yhteys ja paina Työpöytä uudelleen. + %1$s pysähtyi odottamatta. Jaa lokit virheraportin mukana, tai yritä uudelleen. + Ohjelma pysähtyi odottamatta. Jaa lokit virheraportin mukana, tai yritä uudelleen. + Työpöytä pysähtyi odottamatta. Jaa lokit virheraportin mukana, tai yritä uudelleen. + Poistumistila %1$d · %2$s + ei lokia + + Loki näyttää %1$d ”Function not implemented” -virheen: prootin seccomp-kiihdytys epäonnistuu laitteen apuohjelmassa. Kokeile %2$s → ”%3$s” (tai ”%4$s”) ja käynnistä uudelleen. + Loki näyttää %1$d ”Function not implemented” -virhettä: prootin seccomp-kiihdytys epäonnistuu laitteen apuohjelmassa. Kokeile %2$s → ”%3$s” (tai ”%4$s”) ja käynnistä uudelleen. + + + Laajennukset + Asenna laajennuksen ZIP-tiedosto seuraavaa Steam-istuntoa varten. + Latautuu… + Asenna ZIP-tiedostosta + Pelit + Ääni ja video + Grafiikka + Internet + Toimisto + Kehitys + Koulutus + Tiede + Järjestelmä + Apuohjelmat + Suositut + Nousussa + Uudet + Päivitetyt + latautuu… + Flatpak-asennus + %1$s: %2$s + Flathubista ei voitu tarkistaa päivityksiä + asennettu + kansio + Grafiitti + Grafiitinväriset pinnat, ja logon oma pallonsininen sävy kaikessa valitussa, kohdistetussa tai painetussa. + Paperi mustalla + Valkoinen on pääväri, kuvakkeen sininen on merkki viivoille, renkaille ja pisteille. + Kuvakkeen sininen + Musta pohja, valkoinen teksti, sininen kaikessa valitussa, kohdistetussa tai painetussa. + Sähköinen laivastonsininen + Sininen kirkkaampana pinnoilla, jotka taittuvat harmaan sijaan laivastonsiniseen. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Asenna ajoympäristö ensin + Steam-kirjasto ei ole käytettävissä + Aiempi SD-kirjasto ei ole käytettävissä + Pelien tallennustila ei ole käytettävissä + Lisättyjen pelien kansio ei ole käytettävissä + Steam-manifestit eivät ole käytettävissä + Useita järjestelmiä + Sisäinen + Linux-järjestelmä + SD-kortti + Tallennustila + Valitse + Ominaisuudet + Lisää suosikkeihin + Poista suosikeista + Nimeä uudelleen + Kopioi + Leikkaa + Toiminnot + Suosikit + Kiinnitä nykyinen kansio + Ei suosikkeja vielä - kiinnitä kansio sen ⋮-valikosta palataksesi tähän nopeasti. + Poista suosikeista + 0 tavua + tavua + Sijainti + Tyyppi + Koko + Muokattu + Kansio + Tiedosto + %1$s-tiedosto + Vain luku + Estää pelejä (ja Winea) ylikirjoittamasta tai poistamasta tätä tiedostoa. + Vain luku -tilaa ei voitu muuttaa + Piilotettu + Tämä tallennustila ei tue piilotusmääritettä. + Merkitsee tiedoston piilotetuksi Windowsissa (Winen piilotusmääritteenä). + Piilotusta ei voitu muuttaa tällä tallennustilalla + Valmis + Poistetaan... + Poisto epäonnistui + Siirretään... + Kopioidaan... + Siirretään - %1$s + Kopioidaan - %1$s + Siirretään %1$d/%2$d - %3$s + Kopioidaan %1$d/%2$d - %3$s + %1$d valmiina, %2$d epäonnistui + %1$d valmiina, %2$d ohitettu + + %1$d kohde siirretty + %1$d kohdetta siirretty + + + %1$d kohde kopioitu + %1$d kohdetta kopioitu + + ”%1$s” on jo olemassa + Nimetään uudelleen... + Uudelleennimeäminen epäonnistui + Luodaan kansiota... + Kansiota ei voitu luoda + Uusi kansio + Kansion nimi + Luo + Uusi nimi + Poistetaanko? + Poistetaanko ”%1$s” pysyvästi? + + Poistetaanko %1$d kohde? + Poistetaanko %1$d kohdetta? + + Tätä ei voi perua. + + …ja %1$d lisää + …ja %1$d lisää + + Poistetaan %1$d/%2$d - %3$s + + %1$d kohdetta ei voitu poistaa + %1$d kohdetta ei voitu poistaa + + Yhdistäminen lisää ja korvaa tiedostoja olemassa olevan kansion sisällä. + Korvaaminen korvaa olemassa olevan tiedoston. + Käytä kaikkiin ristiriitoihin + Yhdistä + Korvaa + Säilytä molemmat + Ohita + Valitse tämä kansio + Takaisin + %1$s on liitetty, mutta ei ole luettavissa juuri nyt + Luettelonäkymä + Ruudukkonäkymä + Hae + Järjestä + Nimi + Muokkauspäivä + Väljät rivit + Tiiviit rivit + Piilota piilotiedostot + Näytä piilotiedostot + Piilota suosikit + Näytä suosikit + Suodata tätä kansiota + %1$s vapaana + %1$d valittu + Ei mitään + Kaikki + Liitä + + %1$d kohde + %1$d kohdetta + + Liitä %1$s tähän + Liitä %1$s (siirrä) tähän + Tiedostot + TALLENNUSTILA + PIKAVALINNAT + SUOSIKIT + Lataukset + Latauks. + ROMs + Istuntolokit + Kuvat + ”%1$s” lisätty suosikkeihin + ”%1$s” poistettu suosikeista + Tyhjä kansio + Ylös yksi taso + Valitse kansio + Valitse tiedosto + Steam-sininen + Taivaansininen + Valkoinen + Violetti + Vihreä + Meripihka + Punainen + Pinkki + A-painike + B-painike + X-painike + Y-painike + Vasen olkapainike + Oikea olkapainike + Vasen liipaisin + Oikea liipaisin + View-painike + Menu-painike + View + Menu + D-padi ylös + D-padi alas + D-padi vasen + D-padi oikea + Piilotettu + Vedä siirtääksesi + Kohdistus + Tallenna + Ei mitään + Näppäimistö + kosketuslevy + Pääte + Deck-kahvat + kosketuslevyt + + Poistetaan Linux-ajoympäristöä + + Poistetaan Linux-ajoympäristöä · %1$d kohta + Poistetaan Linux-ajoympäristöä · %1$d kohtaa + + Linux-ajoympäristö poistettu + Linux-ajoympäristön poistoa ei voitu viimeistellä. Yritä poistoa uudelleen Asetuksissa. + Poistetaan ajoympäristön jäänteitä + Ladataan + Tarkistetaan + Puretaan + Näytetään, kun Linux-ajoympäristöä asennetaan tai poistetaan + Asennetaan Linux-ajoympäristöä + Poistetaan Linux-ajoympäristöä + + Linux-ajoympäristöä ei ole asennettu + Luettelossa ei ole tarkistussummaa kohteelle %1$s + Tarkistussumma ei täsmää - mitään ei muutettu + Asennetaan %1$s + AppImagea ei voitu sijoittaa + Purkaminen epäonnistui + Asennus epäonnistui + + Flatpak-asennus epäonnistui (poistumiskoodi %1$d) + Flatpak-asennus ei valmistunut + Toinen kauppatehtävä on käynnissä + Asenna Flatpak ensin + Käynnistetään + Flatpak sulkeutui tilakoodilla %1$d + Asennetaan %1$s + Päivitetään %1$s + Poistetaan %1$s + Asennetaan %1$s (%2$d/%3$d) + Päivitetään %1$s (%2$d/%3$d) + Poistetaan %1$s (%2$d/%3$d) + käännökset + näyttöajurit + mediakoodekit + %1$s-ajoympäristö + %1$s-ajoympäristö %2$s + + Tiedosto on liian pieni ollakseen AppImage + Tiedostoa ei voida lukea: %1$s + Tämä ei ole AppImage (se ei ole Linux-ohjelma) + Tämä AppImage ei ole tehty ARM64:lle (aarch64) eikä x86-tietokoneille + Tämä on Linux-ohjelma, mutta ei AppImage + Vain tyypin 2 AppImageja voi tuoda (tämä on tyyppiä %1$d) + Tilaa ei ole riittävästi: tuonti vaatii noin %1$s + Tilaa ei ole riittävästi: päivitys vaatii noin %1$s + Tuonti epäonnistui + Tämä AppImage on poissa + Päivitys epäonnistui + Puretaan %1$s + AppImagea ei voitu purkaa + AppImagea ei voitu purkaa: %1$s + + Pysäytä käynnissä oleva istunto ennen Decky Loaderin asentamista + Decky Loaderin lataus epäonnistui + Decky Loaderin koko ei vastannut julkaisun metatietoja + Julkaisun tarkistussummaa ei voitu latata + Julkaisun tarkistussumma puuttuu + Julkaisun tarkistussumma on virheellinen + Decky Loaderin tarkistussumma ei täsmää + Julkaisun tiedosto ei ole yhteensopiva 64-bittinen Linux-ohjelma + Istunto käynnistyi latauksen aikana; pysäytä se ennen Decky Loaderin asentamista + PluginLoaderia ei voitu merkitä suoritettavaksi + Steamin CEF-etävirheenkorjausta ei voitu ottaa käyttöön + PluginLoaderia ei voitu valmistella + Nykyistä PluginLoaderia ei voitu säilyttää + Uutta PluginLoaderia ei voitu ottaa käyttöön + Pysäytä käynnissä oleva istunto ennen laajennuksen asentamista + Asenna Decky Loader ennen laajennuksen tuomista + Valittua ZIP-tiedostoa ei voitu lukea + Deckyn laajennuskansiota ei voitu luoda + Puretaan laajennuksen ZIP-tiedostoa + Laajennuksen ZIP-tiedostossa täytyy olla yksi ylätason laajennuskansio + Laajennuksen ZIP-tiedostosta puuttuu plugin.json, package.json tai dist/index.js + Laajennuksen nimi puuttuu + Istunto käynnistyi asennuksen aikana; pysäytä se ennen laajennuksen asentamista + Laajennus asennettu · käynnistä Steam uudelleen, jotta se latautuu + Laajennuksen ZIP-tiedoston asennus epäonnistui + Laajennuksen ZIP-tiedostossa on liian monta kohdetta + Laajennuksen ZIP-tiedostossa on virheellinen polku + Laajennuksen ZIP-tiedostossa on päällekkäisiä polkuja + Laajennuksen ZIP-tiedostossa on turvaton polku + Laajennuskansiota ei voitu luoda + Laajennuksen ZIP-tiedosto purkautuu yli 1 GiB:n rajan + Mukana tulevaa laajennuksen binääriä ei voitu merkitä suoritettavaksi + Virheellinen remote_binary-merkintä + Virheellinen etäbinäärin nimi + Etäbinäärin URL-osoitteen täytyy käyttää HTTPS:ää + Etäbinäärillä %1$s on virheellinen SHA-256-tiiviste + Laajennuksen bin-kansiota ei voitu luoda + Ladataan laajennuksen binääriä · %1$s + Laajennuksen binääriä %1$s ei voitu latata + Laajennuksen binäärin tarkistussumma ei täsmää · %1$s + Laajennuksen binääriä ei voitu merkitä suoritettavaksi · %1$s + Laajennuksen binääriä ei voitu korvata · %1$s + Laajennuksen binääriä ei voitu asentaa · %1$s + Laajennuskansio kuuluu toiselle laajennukselle + Nykyistä laajennusta ei voitu säilyttää + Tuotua laajennusta ei voitu ottaa käyttöön + Deckyn laajennusasetuksia ei voitu päivittää + Nykyistä laajennusta ei voitu palauttaa + + Langattoman virheenkorjauksen tunnistetta ei voitu tallentaa + ADB-komento epäonnistui (%1$d) + Android ei vahvistanut, että lapsiprosessirajaa muutettiin: %1$s + Langattoman virheenkorjauksen laiteparitusta ei ole tallennettu + Anna langattoman virheenkorjauksen nykyinen IP-osoite ja portti + + Paritusikkunan odotus aikakatkaistiin. Yritä uudelleen DroidDeckistä. + Paritusikkunaa ei voitu etsiä (%1$d). Tarkista, että Wi-Fi on päällä. + Paritusikkunaa ei voitu etsiä: %1$s + Anna koodin kaikki 6 numeroa + Paritetaan… + Tämä koodi ei toiminut. Avaa paritusikkuna uudelleen ja syötä sen uusi koodi. + Paritettu. Yhdistetään… + Paritettu, mutta langattoman virheenkorjauksen porttia ei löytynyt. Pidä langaton virheenkorjaus päällä ja yritä uudelleen. + Otetaan asetusta käyttöön… + Langattoman virheenkorjauksen komento epäonnistui + Langattoman virheenkorjauksen käyttöönotto + Kysyy langattoman virheenkorjauksen parituskoodia Asetusten ollessa auki + Paritä langaton virheenkorjaus + Ota langaton virheenkorjaus käyttöön ja napauta ”Yhdistä laite paritunnuksella”. + Ota langaton virheenkorjaus käyttöön ja napauta ”Yhdistä laite paritunnuksella”. Pysy Asetuksissa: koodi syötetään tähän ilmoitukseen. + 6-numeroinen paritunnus + Napauta Syötä koodi ja kirjoita Asetuksissa näkyvät 6 numeroa. + Anna Wi-Fi-paritunnus + Yritä koodia uudelleen + Syötä koodi + Otetaan Steamia käyttöön + Steam on valmis + Napauta palataksesi DroidDeckiin. Voit sammuttaa langattoman virheenkorjauksen nyt. + Langattoman virheenkorjauksen käyttöönotto pysähtyi + + Lataus vastasi HTTP %1$d + Latauksen koko ei vastannut julkaistua APK-tiedostoa, ja se hylättiin - yritä uudelleen + Lataus ei vastannut tarkistussummaansa, ja se hylättiin - yritä uudelleen + Päivitys on Android-paketille %1$s, ei %2$s + Päivityksen metatiedot eivät mainitse DroidDeckin julkaisun allekirjoitusavainta + Android ei pystynyt lukemaan ladattua APK-tiedostoa + Ladattu APK on sovellukselle %1$s, ei %2$s + Ladattu APK ilmoittaa versionCode-arvon %1$d, ei julkaistua %2$d + Android ei asenna versionCode-arvoa %1$d asennetun %2$d päälle + Ladatussa APK-tiedostossa ei ole allekirjoitustietoja + Ladattu APK ei ole allekirjoitettu DroidDeckin julkaisuavaimella + Päivitysluettelo vastasi HTTP %1$d + + Käynnistetään istuntoa… + %1$d:%2$02d kulunut · yhä käynnissä + Ladataan Steam-asiakasohjelman päivitystä · %1$d %% + Ladataan Steam-asiakasohjelmaa · %1$s + + Yhteensopivuustyökalun asennus on jo käynnissä + Pysäytä käynnissä oleva istunto ennen yhteensopivuustyökalujen asentamista + ARM64-julkaisua kohteelle %1$s ei löytynyt + Kohteen %1$s asentaminen vaatii vähintään 4 GB vapaata tilaa latauksen koon lisäksi + Ladataan %1$s %2$s + Yritetään latausta uudelleen · yritys %1$d/%2$d + Lataus keskeytyi + Lataus epäonnistui; osittainen tiedosto säilytetään, jotta uusi yritys voi jatkaa siitä + Lataus ylitti julkaisun koon ja poistettiin + Lataus jäi kesken; osittainen tiedosto säilytetään jatkettavaa yritystä varten + Tarkistetaan latausta + Julkaisun tarkistussummaa ei voitu lukea; yritä uudelleen + %1$s %2$s ei julkaise tarkistussummaa; mitään ei asennettu + Tarkistussumma ei täsmää; lataus poistettiin + Istunto käynnistyi latauksen aikana; pysäytä se ennen yhteensopivuustyökalujen asentamista + Asennetaan %1$s + %1$s-asennus epäonnistui (poistumiskoodi %2$d) + Kohdetta %1$s ei voitu asentaa + Haetaan droiddeck-esync-pakettia + + Linux-apuohjelmien kanssa + Vain DLL-tiedostot + Alkuperäinen (Proton päivitetty viimeisimmän vaihdon jälkeen) + Muutettu Komponenttien ulkopuolella + puuttuu + Alkuperäinen %1$s + Alkuperäinen versiosta %1$s + %1$s %2$s siirtyy kohteeseen %3$s, kun sillä käynnissä oleva peli suljetaan. + %1$s %2$s on nyt kohteessa %3$s. Se otetaan käyttöön seuraavan kerran, kun peli käynnistyy. + Tätä alkuperäistä pakettia ei ole tallennettu + %2$s:n %1$s palautuu alkuperäiseen, kun sillä käynnissä oleva peli suljetaan. + %2$s:n %1$s palautettu %3$s-alkuperäisestä. + %1$s on käytössä kohteessa %2$s. Vaihda tämä Proton ensin joksikin toiseksi. + %1$s ei ole tallennettu + Poistettu %1$s. + Asennetun version alkuperäinen säilytetään: se on tie takaisin. + Ei tallennettu + Poistettu %2$s:n %1$s-alkuperäinen. + Lataukset tulevat vain Nightlies-julkaisuista + Tämä pakettiluettelo on tarkistussummia vanhempi - päivitä se ja yritä uudelleen + Tarkistussumma ei täsmää + + Lineaarinen + Lähin + Snapdragon GSR (laatu) + Vain terävöinti + Automaattinen (sovita skaalaukseen) + + Ei mitään käytössä. + Pelin terävyys + Terävöinti hieman kontrastilla - arkikäyttöön. + Eloisa + Väri viritetty voimakkaaksi. Deband päällä, jotta taivaat eivät raidoitu. + Elokuvamainen + Syvempi kontrasti, viileämpi väri, reunat pehmennetty. + Kilpailullinen + Varjot nostettuna ja voimakkaalla terävöinnillä - huomaa pelaajat ensin. + Mukautuva terävöinti + Vain terävöinti, väri koskematta. + Filmimäinen + Filmimäinen käyrä, hieman vaimennettu. + Arcade + Räväkkä ja iskevä. + Retro CRT + CRT-putken ja analogisen signaalin vaikutukset yhdessä. + Terävä skaalaus + Käytettäväksi, kun ajetaan näytön tarkkuutta pienemmällä resoluutiolla. + Puhtaat pikselit + Terävät pikselit 2D- ja vanhoille peleille. + Anime-reunat + Sarjakuvamaiset ääriviivat ja terävöinti. + + Ei Adreno-grafiikkapiiri + Tuettu + Kokeellinen: sen ajurit ovat testiversioita + Testatun laitteiston alapuolella (Adreno 725 ja uudemmat): ei välttämättä toimi + Ei tuettu: DroidDeck vaatii Adreno (Snapdragon) -grafiikkapiirin + + GitHubin käyttörajoitus tuli vastaan - yritä myöhemmin uudelleen + GitHub vastasi HTTP %1$d + lataus vastasi HTTP %1$d + Tämä ajuriluettelo on tarkistussummia vanhempi - päivitä se ja yritä uudelleen + Tarkistussumma ei täsmää - lataus hylättiin + Ei AdrenoTools-ajuri: %1$s + ei meta.json-tiedostoa + meta.json ei ole luettavissa (%1$s) + tämä on Linux-ajoympäristön ajuri (tuo se kohdasta ”Linux-ajoympäristön ajuri”) + tämä on Wayland-peliajuri, jolle tällä sovelluksella ei ole käyttöä + meta.json ei nimeä libraryName-arvoa, joten mitään ei voitu antaa AdrenoToolsille + meta.json nimeää %1$s, jota ei ole zip-tiedostossa + %1$s ei ole 64-bittinen AArch64 ELF-jaettu kirjasto + %1$s linkittää glibc:n - tämä on ”-Linux”-Turnip, joka kuuluu kohtaan ”Linux-ajoympäristön ajuri” + Tässä zip-tiedostossa ei ole libvulkan_freedreno*.so-tiedostoa. Android (AdrenoTools)- tai -Wayland-Turnip-zip ei ole Linux-ajoympäristön ajuri. + %1$s ei ole 64-bittinen AArch64 ELF-jaettu kirjasto. + %1$s ei ole glibc-ajuri - se linkittää Androidin libc:n. Linux-ajoympäristö tarvitsee ”-Linux”-zipin; tavallista tai ”-Wayland”-Turnipia Steam-asiakasohjelma ei voi ladata lainkaan. + Ei ajuripaketti: ei manifest.json-tiedostoa, joka nimeäisi android- ja linux-ajurin + Ei ajuripaketti: %1$s osoittaa sen ulkopuolelle + Ei ajuripaketti: se purkautuu yli %1$d MB:n kokoiseksi + + SD-kortti + Kansio + + Jaa istuntolokit + + vaatii Android 10:n tai uudemman (näyttötasot väriavaruudella) + tämä näyttö ei ilmoita HDR-tukea + tämä näyttö tukee %1$s, ei HDR10:tä + + tämä grafiikkapiiri + Lapsiprosessiraja on päällä + Lapsiprosessirajaa ei ole asetettu + Lapsiprosessirajaa ei voitu tarkistaa + Lapsiprosessiraja on pois päältä + Lapsiprosessirajaa ei vaadita + %1$s Jos se ei ole mahdollista, yhdistä tämä laite tietokoneeseen ADB:llä ja suorita alla oleva komento. Palaa tähän ja tarkista uudelleen. + Steam-istunnot voivat käynnistyä. + Tämä Android-versio ei käytä tätä rajaa. + Ota käyttöön ”Poista käytöstä lapsiprosessien rajoitukset” Kehittäjäasetuksissa, ja pidä Kehittäjäasetukset päällä sen jälkeen. + Tässä Android-versiossa ei ole kytkintä sille. Käytä langatonta virheenkorjausta muuttaaksesi sitä tältä laitteelta. + Tälle versiolle ei ole latausta tähän DroidDeck-kopioon. + Android ei asenna sitä tämän version päälle, koska sen versionCode %1$d on pienempi kuin asennettu %2$d. + Android ei asenna vanhempaa versiota uudemman päälle. Pysy tässä versiossa, kunnes kanava saavuttaa sen. + Tämä DroidDeck-kopio on allekirjoitettu eri tavalla (paikallinen käännös?), joten Android ei päivitä sitä paikan päällä. + Vapaata tilaa ei ole riittävästi päivitykselle. + Päivitys peruutettiin. + Päivitys ei asentunut: %1$s + Päivitys ei asentunut. + %1$s-zip + zip + %1$s ei ole käynnistetty vielä + Tallennustiedostoja ei löytynyt kohteelle %1$s + %1$s ei sisällä drive_c-kansiota, joten se ei ole GameHubin tai Winlatorin tallennuszip + kansioita ei voida luoda kohteeseen %1$s + kohteeseen %1$s ei voida kirjoittaa + kohteeseen %1$s ei voida kirjoittaa (%2$s) + Kansio ei ole käytettävissä + Toinen tiedosto käyttää jo nimeä %1$s + Kuvituskansio ei ole käytettävissä + Kansiossa on muuhun synkronointiin liittyvä luettelo + Kohdetta %1$s ei voitu poistaa + Tiedosto siirtyy vientikansion ulkopuolelle + FEX (versio, jolla ei ole nimeä) + ei profile.json-tiedostoa kohteessa %1$s + %1$s ei ole Linux-komponenttipaketti (tyyppi %2$s) + %1$s · %2$s vapaana + Automaattinen + Päivitysluettelo ei läpäissyt tarkistuksia (%1$s) + DroidDeckin lapsiprosessiasetus + Kirjautunut · offline-tila + Kirjautunut + MUKANA + LADATTU + TUOTU + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Istunnon näyttö + Tarkkuus + 16:9-pelin sovitus + Miten 16:9-pelit sovitetaan tähän näyttöön. Seuraava istunto. + Valitse Sovita näyttöön tai mukautettu koko, joka on 16:9:ää kapeampi. + Säilytä kuvasuhde + Venytä näyttöön + Kuvan skaalaus + Skaalaussuodatin + Yhteinen Steamille ja Työpöydälle. + Vaikuttaa välittömästi. Yhteinen Steamille ja Työpöydälle. + FSR, GSR ja NIS vaikuttavat, kun istunnon kuvaa suurennetaan. Vain terävöinti toimii myös suurentamatta. + Suodattimen terävyys + Kuvanopeus + %1$s Asettaa skaalauksen tilaan %2$s. + Sovita näyttöön (%1$d×%2$d) + Seuraa näyttöä (koko muuttuu taitettuna tai avattuna) + Steam + Näytä laitteen tiedot + Piilota laitteen tiedot + Luetaan laitetta… + Kopioi + Kopioi näitä tietoja virheraporttiin. + Kopioitu. + Pakota SSBS + Pitää päällä suoritintehoasetuksen, jonka Wine jatkuvasti sammuttaa. Voi nopeuttaa Windows-pelejä Snapdragon 8 Elite -piirillä; vanhemmilla piireillä vaikutus on pieni. Vaikuttaa seuraavasta pelin käynnistyksestä alkaen. + + Kaupat + Steam + Mukautettu + Kaupat + Avaa kauppa näytöllä + Latausnopeuden taso + Kauppojen moottori + ● Moottori valmis + Ei koottu + Tasapainotettu + Nopea + Maksimi + Kaupat + Kauppojen asetukset + Asetukset + (ei kirjautunut) + Kauppa + Asennetut + Kirjasto + Kaikki + Lataukset + %1$s (%2$d) + Kirjaudu sisään + %1$s · kirjaudu ulos + Kirjaudutaanko ulos %1$s:stä? + Asennetut pelit säilyvät. Kirjasto haetaan uudelleen, kun kirjaudut takaisin sisään. + Pysy kirjautuneena + Kirjaudu ulos + Hae %1$s-luettelosta… + Hae asennetuista peleistä… + Hae kirjastostasi… + Hae + Ei mitään asennettuna + Latautuu + Ei pelejä + Ladataan kauppaa… + Ei luetteloa + Uutuudet + Tarjoukset + Ilmaiset pelit + Nousussa + Kirjastosi + Pelaa + Asenna + Jatka asennusta + Keskeytynyt + Asenna · %1$s + Hanki ilmaiseksi + Näytä %1$s:ssä + Osta · %1$s + Asennettu + Kirjastossa + Ilmainen + Jatka + Päivitä + Näytä aikuisille tarkoitettu sisältö + Epic + EOS + Offline + Pois + Epic-kirjautuminen (EOS) + Käynnistä offline-tilassa + Ratkaise Epic-kirjautuminen + Pilvitallennukset + Lähetä pilveen + Lataa pilvestä + Ei pilvitallennuksia + Pilviristiriita + Pilviristiriita · %1$d tiedostoa + … + Säilytä pilvi + Säilytä paikallinen + Tarkistetaan pilveä… + Viimeisin synkronointi %1$s + Ei synkronoitu vielä + %1$d tiedostoa + asennettu + kirjastossasi + kauppa + Saavutukset + %1$d / %2$d avattu + %1$d / %2$d + Avattu · %1$d + Lukittu · %1$d + Piilotettu · %1$d + Piilotettu saavutus + Tiedot näytetään, kun se on avattu. + Kaikki saavutukset avattu + Ladataan… + Kirjaudu sisään %1$s-palveluun nähdäksesi saavutuksesi. + Yhteyttä %1$s-palveluun ei saatu. Tarkista uudelleen, kun olet verkossa. + %1$s ei listaa saavutuksia tälle pelille. + Näytä kaikki %1$d + Saavutukset · %1$s + ✓ Steamissa + Poista asennus + Poistetaan asennusta… + Poistetaanko %1$s:n asennus? + Vapauttaa %1$s ja poistaa sen Steamista. + Poistaa sen tiedostot ja Steam-pikakuvakkeen. + Steam huomaa pelin seuraavan käynnistyksensä yhteydessä; käynnistä se siellä, tai käynnistä Steam nyt. + Proton + Steamin valinta pelikohtaisesti + Äänet + DirectAudio + Komponentit + FEX, DXVK, VKD3D + Moottori + %1$s · natiivi + %1$s · sisäänrakennettu + Miten se toimii + Haettu + %1$s, tilillasi + Asennettu kohteeseen + Pelien tallennustila (Games/%1$s/%2$s) + Käynnistäjä + Steam ja Proton, sama reitti kuin Steam-peleillä + Steam ja Proton · Epic-kirjautuminen joka käynnistyksellä, EOS-overlay pois + Steam ja Proton · Amazon Games Services -muuttujat asetettu + Steam + lisätty Steamiin asennuksen yhteydessä (Deck-käyttöliittymä + Steam Input) + Ei latauksia + Keskeytetty + Jonossa + Jonossa · #%1$d + Peruutettu + Epäonnistui + Epäonnistui · %1$s + %1$s · %2$d %% + %1$s / %2$s · %3$d %% + Keskeytä + Jatka + Peruuta + Yritä uudelleen + Tyhjennä + Tyhjennä valmiit + Manifesti + Lataus + Tarkistus + Asennus + Valmis + Tarkistetaan + Latautuu + Poistetaanko lataus? + Epäonnistui · %1$s säilytetty + Tarkistetaan + Asennetaan + %1$s %2$d %% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d tiedostoa + Latausten hallinta + Samanaikaisia latauksia + Asenna kohteeseen + Asenna %1$s: + Sisäinen + %1$s vapaana + Kauppalataukset + Näyttää pelin latauksen etenemisen ja pitää lataukset käynnissä taustalla + Latautuu + %1$d latausta + Valmistellaan… + Valmiit kauppalataukset + %1$s asennettu + %1$s:n lataus epäonnistui + Poista + Säilytä + Kirjaudu sisään palveluun %1$s + Kirjaudu sisään palveluntarjoajallasi + Lataa uudelleen + Sulje + Yritä uudelleen + Viimeistellään kirjautumista… + Kirjautumissivua ei voitu latata: %1$s + Tämä palveluntarjoaja ei salli kirjautumista upotetulta sivulta. Kirjaudu sisään kaupan salasanalla sen sijaan. + Kirjautumissivu vastasi virheellä (HTTP %1$d). + Yhteys kirjautumissivulle ei ole suojattu. + Kirjautumista ei voitu vahvistaa. Yritä uudelleen. + Kirjautuminen epäonnistui. Yritä uudelleen. + Sulje + Takaisin + Muokkaa + Lisää kaikki tämän kansion pelit + Ei mitään uutta tässä kansiossa + Nimi + Kohde + Käynnistyskansio + Käynnistysasetukset + Kuvitus + Toinen exe… + Poista + Kansikuva + Tausta + Logo + Kuvake + Pelin kansio + Steam + SteamGridDB + Oma tiedostosi + Exe-tiedostosta + Automaattinen + Valitse kuva… + Valitse kuva: %1$s + Palauta automaattiseksi + + %1$d peli lisätty + %1$d peliä lisätty + + + %1$d on jo Peleissä + %1$d on jo Peleissä + + B · Takaisin %1$s + peliin + Käynnistysasetuksiin + Näyttöajurit + Tämä Proton + Kaikki Protonit + Kaikille Protoneille + Saatavilla · %1$d + Tarkistettu %1$s + Tarkista uudet komponentit ja ajurit + Hae + %1$s · alla olevat DXVK, VKD3D-Proton ja FEX ovat tämän Protonin + %1$s · testattu + %1$s · testaamaton + %1$s · ei tuettu + Pitää suositellun parin ajan tasalla + Automaattinen · %1$s + Ajoympäristön ja näytön ajurit erikseen + Käytetäänkö %1$s? + Poistetaanko %1$s? + Diagnostiikka + Valmis pelattavaksi + Ei vielä valmis pelattavaksi + ajoympäristö %1$s + lapsiprosessiraja pois + kirjautuneena nimellä %1$s + Kun jokin tarvitsee sinua + Ulkoasu + Tämä laite + Kirjaston kuvitus + LINUX-SOVELLUKSET + Flathub + Työpöytäsovelluksia Linux-työpöydälle, avataan Työpöydältä. + Ota Flathub käyttöön + Poistetaanko %1$s? + Sen asetukset ja tiedostot säilyvät. + Säilytä + Koko levyllä + Poistetaanko työpöydän asennus? + Kaikki + GOG-saavutukset + Synkronoi saavutukset tuetuille GOG-peleille. Tulee voimaan seuraavalla käynnistyksellä. + Saavutukset pois käytöstä + Saavutukset käytössä + Kirjaudu GOG:iin uudelleen + Saavutukset lähetetty + Saavutuksen lähetys odottaa + Ota GOG-saavutukset käyttöön kauppa-asetuksissa + Saavutukset ei käytettävissä; tarkista istunnon lokit + Saavutus avattu + Liitä + Ristiohjain + Vasen tatti + Oikea tatti + Valikko + Näppäimistö + Ristiohjain ylös + Ristiohjain alas + Ristiohjain vasen + Ristiohjain oikea + Kosketusohjaimet + Kosketusohjain + Hiiri + Päällä + Pois + Hidas + Normaali + Nopea + Muokkaa asettelua + DroidDeck-valikko + Vedä ohjaimia näytölle + Järjestä + Hylkää + Tallennetaan… + Kuvake + L− + L+ + K− + K+ + Piilota + Väri ja peittävyys… + Kopioi tämä asettelu + Asettelu kopioitu; liitä se minkä tahansa pelin kosketusasetteluun + Liitä kopioitu asettelu + Palauta Steamin oletus + Tällä ohjaimella ei ole sidosta Steamissa, jolle voisi antaa kuvakkeen + Kuvake kohteelle %1$s + Kosketusasettelun väri + Valmis + Kosketusasettelua ei voitu tallentaa + Kosketusasettelu tallennettu Steamiin + Sovellus %1$d + Nimike (valinnainen) + Steamin kuvakkeita ei löytynyt + + %1$d kuvake Steamista + %1$d kuvaketta Steamista + + Kuvakkeen väri + Painikkeen väri + Ei kuvaketta + + Tarkistetaan kirjautumista… + Haetaan kirjastoasi… + Haetaan peliluetteloa… + Ladataan pelien tietoja… (%1$d/%2$d) + + Synkronoidaan %1$d peliä… + Synkronoidaan %1$d peliä… + + Synkronoidaan… %1$d/%2$d + Haetaan kansikuvia… + Synkronointi epäonnistui + %1$s-kirjastostasi ei löytynyt pelejä + Amazon Games ei vastannut kirjastosi suhteen: päivitä sivu tai kirjaudu uudelleen + GOG:iin ei saatu yhteyttä + GOG-kirjastoasi ei voitu lukea + Rekisteröidytään Steamiin… + Cardboard VR + Cardboard-tila + Vasemman ja oikean silmän kuvat puhelimen VR-laseille. + Kuva + Virtuaalinen näyttö + Rinnakkais-3D + Lasit + Cardboard + Cardboard (2014) + Ei linssiä + Silmien etäisyys + Näytön leveys + Näytön etäisyys + Pään seuranta + Tässä laitteessa ei ole kiertoanturia. + Keskitä näkymä + Keskitä + SteamVR (kokeellinen) + Asenna + Lataa Valven ARM64-version SteamVR:stä (571 Mt). + Ladataan ja puretaan SteamVR:ää… + Käynnistä + Ajaa SteamVR:ää niin, että pääsi toimii lasina; kuva vaihtuu rinnakkais-3D:ksi. + Pysäytä + SteamVR on käynnissä. + Ei käytettävissä tässä istunnossa. + SteamVR ei vastannut; tarkista istunnon loki. + Lataa komponentteja + Lataukset jaetaan Komponentit-sivun kanssa. Valitse yltä ladattu versio, niin sitä käytetään seuraavalla pelin käynnistyksellä. + Valitse ladattava versio + Kehittäjä + Julkaisija + Julkaistu + Pääosa + + Lisät + Kaikki + %1$s t + %1$d min + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 30e576eda..52967b528 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -15,8 +15,10 @@ Session Affiché lorsqu\'une session Steam ou Bureau est ouverte Steam est en cours d\'exécution + Téléchargement Steam en cours Session en pause - Appuyez pour reprendre quand vous êtes prêt Reprendre + Mettre en pause maintenant Terminer cette session Génération d\'images Sélectionnez le mode de génération d\'images à utiliser @@ -72,11 +74,21 @@ Options VKD3D pour D3D12. nodxr désactive le ray tracing ; à utiliser uniquement si le jeu le nécessite. Définir sur 1 pour générer un journal de diagnostic Proton. La journalisation peut réduire les performances et consommer de l\'espace de stockage. Définir sur 1 pour utiliser WineD3D (basé sur OpenGL) au lieu de DXVK, pour tester la compatibilité D3D9–11. + Définir sur 1 pour que ce jeu n\'utilise pas ntsync (droiddeck-ntsync ou un pilote du noyau) ; il utilise droiddeck-esync si un pack existe, sinon droiddeck-fsync. + Définir sur 1 pour désactiver droiddeck-esync pour ce jeu ; il utilise alors droiddeck-fsync, ou droiddeck-ntsync si fsync est aussi désactivé. + Définir sur 1 pour que ce jeu n\'utilise pas droiddeck-fsync ; il utilise droiddeck-esync si un pack existe, sinon droiddeck-ntsync. + Définir sur 1 pour exécuter ce jeu avec droiddeck-fsync, ou sur 0 pour donner la priorité à droiddeck-esync si un pack correspond à son Proton. Définir sur 0 pour désactiver l\'assistant de navigation à la manette de Proton pour ce jeu. Superposition DXVK pour D3D9–11. fps affiche le taux de rafraîchissement ; annuler la définition masque la superposition. Paramètres DXVK séparés par des points-virgules. Les versions récentes utilisent dxvk.maxFrameRate pour limiter le taux de rafraîchissement ; pour un jeu quotidien, il est recommandé d\'utiliser le limiteur de taux de rafraîchissement de session. Limite de taux de rafraîchissement D3D12. 0 désactive la limite. Mesa OpenGL multithread. Pour les jeux OpenGL limités par le CPU, true peut aider ; à tester jeu par jeu. + Synchronisation Wine + Comment les jeux synchronisent leurs threads. Prendra effet à la prochaine session. + esync + ntsync + fsync + wineserver Préréglages FEX Prendra effet au prochain lancement du jeu. Jeu @@ -149,6 +161,15 @@ Un fichier .sh. Il s\'exécutera depuis son propre dossier et s\'affichera en plein écran via gamescope. Un AppImage ARM64 (aarch64). Il sera décompressé dans l\'environnement d\'exécution Linux lors de la première utilisation. Installer l\'AppImage ARM64 depuis la dernière version du dépôt. + Utiliser FEX + Auto + Activé + Désactivé + FEX exécute des programmes Linux x86 sur cet appareil ARM64. En mode Auto, il est utilisé lorsque le programme est conçu pour x86. + %1$s via FEX + Ce programme est conçu pour les PC %1$s et s\'exécute via FEX, qui n\'est pas encore configuré. Ouvrez Steam une fois en étant connecté : DroidDeck lui fait télécharger FEX et le Steam Linux Runtime nécessaires, puis ce programme s\'ouvre. + Ce programme + %1$s est conçu pour les PC x86 et s\'exécute via FEX, qui n\'est pas encore installé. Lancez Steam une fois en étant connecté et laissez-le télécharger FEX et le Steam Linux Runtime, puis relancez ce programme. ID d\'application Flathub ou sa page Flathub. Si Flatpak n\'est pas encore configuré, il le sera d\'abord. Fichier Aucun fichier sélectionné @@ -163,6 +184,8 @@ ID d\'application org.ppsspp.PPSSPP N\'est pas un ID d\'application Flathub valide + Ou un fichier .flatpak, pour une application absente de Flathub. + Choisir un fichier .flatpak Source Afficher sur le bureau Nouvelle application @@ -184,6 +207,9 @@ Annuler Veuillez d\'abord installer l\'environnement d\'exécution Linux depuis les « Paramètres ». Veuillez attendre que %1$s soit terminé. + Compatible + Les versions marquées « %1$s » dans le menu Compatibilité de Steam incluent les correctifs de préfixe de DroidDeck et lancent les jeux ici. Par défaut, c\'est Proton Experimental ARM64 (%1$s), ou Proton 11.0 ARM64 (%1$s) une fois cette version plus récente installée. Les entrées Proton de Valve sans cette mention ne peuvent pas lancer de jeu sur cet appareil. + %1$s installé · %2$s Annuler Auto Jamais @@ -229,6 +255,10 @@ Commandes à l\'écran Steam + QAM Le mode automatique affiche les commandes à l\'écran lorsqu\'aucune manette n\'est connectée. + Tactile Steam (bêta) + Les commandes tactiles de Steam s\'activeront à la prochaine session Steam + Disposition tactile Steam + Déplacez, redimensionnez ou masquez les commandes tactiles de ce jeu ; enregistré dans Steam Manette Comment Steam reconnaît votre manette. Prendra effet à la prochaine session. Manette Steam Deck @@ -238,10 +268,16 @@ Manette Xbox 360 Deck : fournit un bouton d\'accès rapide et un gyroscope. Xbox 360 : utilisez Guide+A pour ouvrir l\'accès rapide. Retour + Menu → QAM + QAM → Menu Session Comportement en arrière-plan Comportement lorsque l\'application est masquée ou que l\'écran est éteint. Manuel + Natif + Auto : met en pause, puis reprend à l\'affichage. Natif : prépare d\'abord Steam à la mise en veille. Manuel : attend « Reprendre ». Jamais : continue de fonctionner. + Poursuivre les téléchargements + Retarde la mise en pause en arrière-plan tant que Steam télécharge. Le mode automatique met en pause en arrière-plan et reprend à l\'affichage. Le mode manuel nécessite d\'appuyer sur « Reprendre ». Ne reste jamais en cours d\'exécution. Démarrage Lancer Steam à l\'ouverture de DroidDeck @@ -262,6 +298,10 @@ Client Superposition de performance Désactivez cette option si les jeux plantent ou affichent un écran noir en mode Deck. Prendra effet à la prochaine session. + Réparer le client Steam + Retélécharge le client Steam au prochain démarrage de Steam. Utile si Steam reste bloqué sur son écran de chargement. Les jeux, la connexion et les paramètres sont conservés. + Le client Steam sera retéléchargé au prochain démarrage de Steam. + Réparer Illustrations Clé API SteamGridDB Votre clé @@ -274,6 +314,51 @@ Choisir l’exe : Cet emplacement n’est pas disponible dans la session Vérifier cet exécutable + Composants Windows + LB / RB pour passer d\'une section à l\'autre. + Aucun + DLL Windows non incluses dans Proton, issues de la liste des composants. Activez celles dont ce jeu a besoin : elles sont placées dans le dossier Windows propre au jeu, jamais dans ses fichiers, et restent quel que soit le Proton utilisé. + Impossible d\'accéder à la liste des composants. Les composants déjà téléchargés peuvent toujours être activés ou désactivés. + Recommandés pour ce jeu + Analyse des besoins de ce jeu… + Disponible une fois en ligne + + Activation de %1$d composant… + Activation de %1$d composants… + Activation de %1$d composants… + + Activés pour ce jeu + Steam installe %1$s avec %2$s (application %3$s) + D\'après la liste de GOG pour ce jeu + D\'après les prérequis d\'Epic pour ce jeu + D\'après les étapes d\'installation d\'Amazon pour ce jeu + Steam installe %1$s avec l\'application %2$s, identifiée par son nom + Également listés + Listé par %1$s ; non disponible comme composant ici + Steam + GOG + Epic Games + Amazon Games + Le jeu fournit %1$s + Le jeu contient %1$s (fonctionne souvent tel quel) + Déjà présent dans ce jeu + Rien dans le dossier du jeu n\'indique un composant. + Installés pour ce jeu (%1$d) + Tous les composants (%1$d) + Installateur Windows requis (%1$d) + Ce sont des programmes d\'installation autonomes, sans paquet à extraire, et ce qu\'ils installent n\'a pas encore été enregistré. La plupart sont d\'anciens environnements d\'exécution que le Mono de Proton remplace déjà ; les enregistrements sont ajoutés au fur et à mesure. + Afficher + Masquer + Lance son propre programme d\'installation (pas encore d\'enregistrement) + Pas encore pris en charge ici + Chargement des composants… + Installation impossible : %1$s + Téléchargé + Téléchargement de %1$s + Installation : %1$s + Désactivation… + %1$s à télécharger + Prendra effet au prochain lancement du jeu. Étirer les jeux 16:9 Sur les écrans dont le rapport d\'aspect est inférieur à 16:9, étire le jeu pour remplir l\'écran au lieu d\'afficher des bandes noires. Prendra effet à la prochaine session. Audio @@ -297,6 +382,8 @@ Dossier : %1$s Bibliothèque secondaire Les jeux lisant des ressources depuis une carte SD ou un stockage partagé peuvent saccader. + Diagnostic du stockage + Enregistre les résultats d\'allocation et des mesures de temps du stockage dans « Partager les journaux ». Peut ralentir les installations lorsqu\'il est activé. Prendra effet à la prochaine session. Choisir un dossier… Résolution personnalisée Remplace la limite de résolution et le format d\'écran. Les dimensions ne correspondant pas à l\'écran afficheront des bandes noires. @@ -305,6 +392,7 @@ Plage : 320×240 à 3840×2160. %1$d jeu + %1$d jeux %1$d jeux OK @@ -327,9 +415,54 @@ Vérifier les nouveaux paquets dans Nightlies Proton Les mises à jour de Steam remplacent le Proton de Valve ; les fichiers originaux de chaque version sont conservés. + Configuration + Configurer le profil + Profil + Profil par défaut + Utilisé automatiquement par les jeux sans réglages personnalisés. + Paramètres enregistrés utilisés automatiquement au lancement de %1$s. + Choisissez uniquement les paramètres dont ce jeu a besoin. Tout le reste continue d\'utiliser le profil par défaut. + Préréglage FEX + Utilise le préréglage du profil par défaut. + Préréglage personnalisé enregistré pour ce jeu. + Variables FEX personnalisées + Utiliser par défaut + Compatibilité du jeu + Options d\'environnement Proton, DXVK, VKD3D et de synchronisation pour ce jeu. + Utilise le Proton du profil par défaut. + Proton personnalisé enregistré pour ce jeu via le mappage de compatibilité de Steam. + Aucun Proton + DXVK + VKD3D-Proton + Utilise le paquet du profil par défaut issu de %1$s, sauf si ce jeu choisit un autre paquet. + Utilise le paquet de composants du profil par défaut. + Utilise %1$s uniquement pour ce jeu. + Options d\'exécution + Autres options d\'environnement Proton, DXVK, VKD3D-Proton et de synchronisation propres à ce jeu. + Le profil de %1$s est enregistré pour le prochain lancement du jeu. + Démarrer sans Steam + Pour un jeu qui se ferme aussitôt : le démarre sans la configuration Steam de Proton. Les fonctionnalités Steam, comme la surimpression, sont désactivées pour lui. + Activé + Le choix de Proton pour %1$s est enregistré et disponible dès maintenant dans Steam. + Le choix de Proton pour %1$s est enregistré. Redémarrez Steam avant de lancer le jeu. + %1$s ne peut pas utiliser le Proton sélectionné sur cet appareil. + Impossible de charger le profil de %1$s : %2$s + Impossible d\'enregistrer le profil du jeu : %1$s + Réinitialiser le profil du jeu + Réinitialiser le profil de %1$s ? + Supprimer les réglages propres à ce jeu (Proton, FEX, paquet de composants, environnement, filtrage des textures et composants Windows) ? Il utilisera le profil par défaut. + %1$s utilise désormais le profil par défaut. + Le profil du jeu n\'a pas pu être entièrement réinitialisé : %1$s + %1$s exécute désormais tous les jeux qui n\'ont pas choisi leur propre Proton dans Steam. + %1$s exécutera tous les jeux qui n\'ont pas choisi leur propre Proton, à partir du prochain démarrage de Steam. + %1$s ne peut pas lancer de jeux ici, il ne peut donc pas être le Proton par défaut. + Impossible d\'enregistrer le Proton par défaut : %1$s %1$s · %2$d remplacé(s) %1$s · Tout est original %1$s · Jeu en cours d\'exécution + %1$s · pack droiddeck-esync + %1$s · pas encore de pack droiddeck-esync + %1$s · esync intégré, aucun pack requis Pilotes GPU %1$s · Réappliqué au lancement de %2$s Suivant : %1$s · Après la fermeture du jeu @@ -392,6 +525,12 @@ Turnip : rendu sysmem Steam et la plupart des jeux seront plus rapides. Requis pour Adreno 710/720/722. Ignorer l\'assistant xalia de Steam + droiddeck-ntsync (expérimental) + Un substitut en espace utilisateur du pilote Linux ntsync, pour les tests. Lorsqu\'il est activé, Proton l\'utilise à la place de droiddeck-esync et de droiddeck-fsync. Fonctionne avec Proton Experimental ARM64, GE-Proton et proton-cachyos. Prendra effet à la prochaine session. + droiddeck-esync + droiddeck-fsync en priorité + Exécute le fsync propre à Proton sur les fichiers Proton d\'origine, avec futex_waitv fourni en espace utilisateur : aucun pack n\'est nécessaire et il continue de fonctionner dès que Steam met à jour Proton. Lorsqu\'il est activé, les jeux l\'utilisent avant droiddeck-esync, sauf les quelques-uns que Proton exclut de fsync. Prendra effet à la prochaine session. + Les threads Windows des jeux s\'attendent mutuellement via eventfd et la mémoire partagée au lieu de faire des allers-retours par wineserver. Utilise un pack droiddeck-esync conçu pour cette version exacte de Proton ; les packs sont récupérés automatiquement. Tant qu\'il n\'en existe pas pour une nouvelle version de Proton, droiddeck-fsync prend le relais. Prendra effet à la prochaine session. Désactive l\'assistant de navigation à la manette de Proton, qui consomme du CPU dans chaque jeu. Exécuter proot sans seccomp Peut corriger certaines erreurs d\'appels système manquants sur certains noyaux, mais peut réduire les performances. @@ -412,8 +551,10 @@ Ses fichiers seront supprimés de l\'application. Ses fichiers seront supprimés. Comme il est en cours d\'utilisation, le pilote par défaut sera utilisé à la place. Affichage + Effets Contrôles Composants + Manette Paramètres Page %1$s Steam @@ -424,11 +565,39 @@ Page précédente Page suivante HUD de performance + Mode de mise à l\'échelle + Netteté de la mise à l\'échelle + Effets d\'écran + En direct. Consomme un peu de temps GPU tant qu\'un effet est actif. + Préréglages + Personnalisé + Avancé + Développé + Réduit + Netteté (CAS) + Niveau de netteté + Faux HDR + Anti-banding + Intensité de l\'anti-banding + Luminosité + Contraste + Gamma + Saturation + FXAA (lissage des contours) + Contours cartoon + CRT + NTSC + Filtrage des textures + Jeux DirectX 9–11, à partir de leur prochain lancement. + Filtrage anisotrope + Netteté des textures + Un biais de mip négatif : textures plus nettes, un peu de scintillement. Auto s\'adapte au facteur d\'agrandissement de la session à l\'écran. Auto (%1$s) Clavier Clavier PC Android Deuxième écran + Mode Afficher Applications Android Lancer l\'application @@ -447,6 +616,20 @@ Arrêter la session ? En cours d\'exécution Aucun jeu n\'utilise actuellement Proton + Profil du jeu + Les détails du profil ne sont pas encore disponibles. + Impossible de charger les profils de jeu. Nouvelle tentative automatique. + Impossible d\'actualiser les profils de jeu. Affichage des dernières données disponibles ; nouvelle tentative automatique. + Afficher et modifier les paramètres enregistrés utilisés au prochain lancement. + Utilise le profil par défaut + Profil personnalisé + Utilise %1$s du profil par défaut. + Les modifications sont enregistrées dans le profil de %1$s et prennent effet au prochain lancement du jeu. + + %1$d réglage propre au jeu + %1$d réglages propres au jeu + %1$d réglages propres au jeu + Proton n\'est pas encore installé. Jeu en cours d\'exécution Réappliqué au lancement de %1$s @@ -506,11 +689,22 @@ Mode hors ligne Veuillez d\'abord vous connecter à Steam Lanceur + Langue + Steam l\'utilise aussi, à partir de son prochain démarrage. + Langue du système (%1$s) + Langue du système + Échelle de l\'application + Texte et commandes de DroidDeck + %1$d%% (par défaut) Thème Apparence du lanceur Utiliser comme écran d\'accueil DroidDeck peut être utilisé comme application d\'écran d\'accueil DroidDeck ne sera pas proposé comme application d\'écran d\'accueil + Orientation + Paysage automatique + Paysage + Paysage inversé Plein écran Masquer la barre d\'état et la barre de navigation Android Afficher la barre d\'état et la barre de navigation Android @@ -638,14 +832,17 @@ À l\'instant %1$d disponible + %1$d disponibles %1$d disponibles Modifications depuis votre version · %1$d version de retard + Modifications depuis votre version · %1$d versions de retard Modifications depuis votre version · %1$d versions de retard Modifications récentes · %1$d version + Modifications récentes · %1$d versions Modifications récentes · %1$d versions Un paramètre Android à modifier @@ -771,6 +968,12 @@ Cette page n\'existe plus. Veuillez sélectionner une section à gauche. Ouvrir le bureau Installer et ouvrir le bureau + Désinstaller le bureau + Supprime le bureau et ses applications intégrées. Conserve les jeux, les applications ajoutées, les paramètres et les bibliothèques partagées. + Suppression du bureau + Bureau supprimé + Suppression des anciens fichiers LXQt + Mettez à jour DroidDeck avant de supprimer cette version du bureau. Bureau Linux KDE Plasma, Firefox et vos émulateurs Les jeux installés sur Steam apparaîtront ici après la fin de la session. @@ -799,6 +1002,8 @@ Dernier lancement %1$s Stockage interne Jeux ajoutés + Jeu personnalisé + Stockage des jeux Sélectionne la meilleure combinaison de pilotes pour ce GPU et les maintient à jour Choisir manuellement la combinaison de pilotes Vérification des pilotes… @@ -840,6 +1045,12 @@ Pavé tactile gauche Pavé tactile droit Les deux + Manette + Joystick + Pavé tactile + Basculer entre joystick et pavé tactile + Plus de commandes Deck + PANNEAUX Fermer les commandes du deuxième écran Ouvrir le menu Steam Gauche · Haut @@ -875,6 +1086,10 @@ %1$s installé Élément en attente d\'une demande précédente Non installé + Mise à jour disponible → %1$s + Supprimer les anciennes versions de %1$s + Supprime %1$s. Les jeux configurés sur ces versions devront passer à un autre Proton. + Les jeux configurés sur cette version devront passer à un autre Proton. Installer maintenant Annuler la file d\'attente Clavier Android @@ -903,6 +1118,8 @@ Jeux Proton Picture-in-Picture (PiP) Passer automatiquement en PiP + Commandes sur l\'écran déplié + Déplié, le jeu s\'affiche au-dessus des pavés tactiles et des poignées arrière du Steam Deck. Plié, le jeu est en plein écran. En pause Le mode PiP sera disponible une fois la session prête. Veuillez autoriser DroidDeck à utiliser le mode PiP dans les paramètres Android. @@ -927,8 +1144,926 @@ Choisir un dossier pour les fichiers du jeu frontal Fichiers du jeu enregistrés dans %1$s Fichiers frontaux - Remonter d\'un niveau + + Attendez la fin de l\'opération sur l\'environnement d\'exécution + Attendez la fin de l\'installation de l\'outil de compatibilité + Attendez la fin de l\'installation de l\'application de bureau + Attendez la fin de l\'installation avant de lancer un jeu + Fermez d\'abord la session Steam, pour que le jeu ne puisse pas écraser l\'importation + Importation dans %1$s + + %1$d fichier importé (%2$s) dans %3$s + %1$d fichiers importés (%2$s) dans %3$s + %1$d fichiers importés (%2$s) dans %3$s + + + %1$d fichier importé (%2$s) dans %3$s. Anciennes sauvegardes copiées dans Download/DroidDeck/Saves/backups + %1$d fichiers importés (%2$s) dans %3$s. Anciennes sauvegardes copiées dans Download/DroidDeck/Saves/backups + %1$d fichiers importés (%2$s) dans %3$s. Anciennes sauvegardes copiées dans Download/DroidDeck/Saves/backups + + Choisir un zip de sauvegarde pour %1$s + Exportation de %1$s + + %1$d fichier exporté (%2$s) : %3$s + %1$d fichiers exportés (%2$s) : %3$s + %1$d fichiers exportés (%2$s) : %3$s + + Choisir où enregistrer %1$s (%2$s) + %1$s : échec (%2$s) + Aucun journal de session pour l\'instant : lancez d\'abord une session. + Aucun journal de session à effacer. + + %1$d dossier de journaux de session effacé. + %1$d dossiers de journaux de session effacés. + %1$d dossiers de journaux de session effacés. + + Échec de l\'appairage du débogage sans fil + Échec de la commande de débogage sans fil + Le débogage sans fil n\'est pas disponible + Commande ADB copiée + Choisir le dossier ROM + GPU non Adreno + Turnip prend en charge les GPU Adreno. Sur %1$s, Steam peut afficher un écran noir. Téléchargement : %2$.0f Mo. + Installer quand même + Supprimer l\'environnement d\'exécution Linux + Cela supprime l\'environnement d\'exécution, le client Steam qu\'il contient et tous les jeux qui y sont installés. + Appliqué : %1$s + Impossible d\'ouvrir %1$s sur l\'écran principal + Impossible d\'ouvrir %1$s sur %2$s + Impossible d\'ouvrir %1$s sur l\'écran %2$d + Importation + Remplacement + Restauration + Annulation + Suppression + impossible de lire le fichier + %1$s importé + Le remplacement en attente a été annulé. + Choisir un paquet de composants (-linux .wcp) + Impossible d\'accéder aux Nightlies + %1$s enregistré + Échec du téléchargement : %1$s + Choisir un pilote d\'environnement d\'exécution Linux (zip -Linux ou bundle Android + Linux) + Choisir un pilote d\'affichage (zip AdrenoTools ou bundle Android + Linux) + Choisir un zip de pilote ou un bundle Android + Linux + %1$s : %2$s + Suppression de %1$s… + Oubli de %1$s… + Choisir le dossier de stockage des jeux + Choisir un ZIP de plugin Decky + Inutilisable : %1$s + Programme + Suppression de l\'environnement d\'exécution Linux + Impossible d\'ouvrir les paramètres sur l\'écran inférieur ; ils ont été ouverts sur l\'écran principal. + Ouvrez les options pour les développeurs dans les paramètres + + Impossible d\'ouvrir le paramètre Android « Installer des applis inconnues » + Vérification impossible : %1$s + Cette version ne propose aucun téléchargement pour cette copie de DroidDeck + Arrêtez d\'abord la session en cours, puis mettez à jour + Téléchargement + Installation + En attente de votre confirmation + La mise à jour a échoué + Android n\'a pas proposé d\'installer la mise à jour + + Échec de l\'installation de Decky : %1$s + Démarrage de l\'importation du plugin… + Échec de l\'importation du plugin : %1$s + Plugin installé. Redémarrez Steam pour le charger. + + Pas encore vérifié - appuyez sur Actualiser pour rechercher de nouveaux pilotes + Android + Linux : définit aussi le pilote d\'affichage + Android + Linux : définit aussi le pilote d\'exécution + le Turnip intégré à l\'environnement d\'exécution + Vulkan du système : aucune version fournie pour ce GPU + %1$s (intégré) + Manuel + %1$s · %2$s + Une seule moitié est publiée pour le moment + %1$.0f Mo pour les deux + Aucun pilote à définir : %1$s + Recherche des derniers pilotes… + À jour (vérification impossible : %1$s) + Les pilotes recommandés ne sont pas publiés pour le moment + Téléchargement de %1$s %2$s… + Mis à jour vers %1$s %2$s. + Mis à jour vers %1$s %2$s. Le pilote d\'affichage prendra effet après le redémarrage de DroidDeck. + Basculé vers %1$s %2$s. + Basculé vers %1$s %2$s. Le pilote d\'affichage prendra effet après le redémarrage de DroidDeck. + Utilise %1$s %2$s. + Utilise %1$s %2$s. Le pilote d\'affichage prendra effet après le redémarrage de DroidDeck. + Ce n\'est pas un zip de pilote : ni un pilote AdrenoTools, ni un Turnip -Linux, ni un bundle Android + Linux + Échec de l\'importation : %1$s + Pilote importé + %1$s est utilisé comme pilote d\'exécution et comme pilote d\'affichage. + %1$s est utilisé comme pilote d\'exécution et comme pilote d\'affichage. Le pilote d\'affichage prendra effet après le redémarrage de DroidDeck. + %1$s supprimé (les deux pilotes) + %1$s supprimé + Dernières versions : %1$s · vérifié %2$s + Dernières versions : %1$s · %2$s inaccessible · vérifié %3$s + + il y a %1$d min + il y a %1$d min + il y a %1$d min + + + il y a %1$d h + il y a %1$d h + il y a %1$d h + + + il y a %1$d jour + il y a %1$d jours + il y a %1$d jours + + Vérification de Banners-Turnip, WinNative et DroidDeck… + échec de la vérification + %1$s installé - choisissez-le dans l\'une ou l\'autre liste pour définir les deux pilotes + %1$s installé - choisissez-le dans le menu + + Attendez la fin de la suppression de l\'environnement d\'exécution + Jeu + pavé tactile + direct + Échec du remplacement : %1$s + Échec de l\'enregistrement du profil : %1$s + Le bureau est en pause + %1$s est en pause + Le programme est en pause + Démarrage du bureau + Démarrage de %1$s + Démarrage du programme + Aucun journal pour cette session. + Impossible de créer l\'archive des journaux de session. + Impossible de partager les journaux de session. + Démarrage de la session… + téléchargement de l\'environnement d\'exécution Linux + téléchargement de l\'environnement d\'exécution Linux · %1$d sur %2$d Mo + vérification de l\'environnement d\'exécution Linux + décompression de l\'environnement d\'exécution Linux + téléchargement du bureau + téléchargement du bureau · %1$d sur %2$d Mo + vérification du bureau + décompression du bureau + téléchargement de Proton Experimental (ARM64) + téléchargement de Proton Experimental (ARM64) · %1$d sur %2$d Mo + vérification de Proton Experimental (ARM64) + décompression de Proton Experimental (ARM64) + Impossible d\'accéder au catalogue des environnements d\'exécution. Vérifiez la connexion et appuyez à nouveau sur Play. + L\'environnement d\'exécution Linux ne s\'est pas installé. Vérifiez-le dans « Paramètres » et appuyez à nouveau sur Play. + Le bureau ne s\'est pas installé (%1$s). Vérifiez la connexion et appuyez à nouveau sur Bureau. + %1$s s\'est arrêté de manière inattendue. Partagez les journaux dans un rapport de bug, ou réessayez. + Le programme s\'est arrêté de manière inattendue. Partagez les journaux dans un rapport de bug, ou réessayez. + Le bureau s\'est arrêté de manière inattendue. Partagez les journaux dans un rapport de bug, ou réessayez. + Code de sortie %1$d · %2$s + aucun journal + + Le journal indique %1$d erreur « Function not implemented » : l\'accélération seccomp de proot fait échouer un utilitaire sur cet appareil. Essayez %2$s → « %3$s » (ou « %4$s ») et relancez. + Le journal indique %1$d erreurs « Function not implemented » : l\'accélération seccomp de proot fait échouer un utilitaire sur cet appareil. Essayez %2$s → « %3$s » (ou « %4$s ») et relancez. + Le journal indique %1$d erreurs « Function not implemented » : l\'accélération seccomp de proot fait échouer un utilitaire sur cet appareil. Essayez %2$s → « %3$s » (ou « %4$s ») et relancez. + + + Plugins + Installez un ZIP de plugin pour la prochaine session Steam. + Téléchargement… + Installer depuis un ZIP + Jeux + Audio et vidéo + Graphisme + Internet + Bureautique + Développement + Éducation + Sciences + Système + Utilitaires + Populaires + Tendances + Nouveautés + Mis à jour + chargement… + Configuration de Flatpak + %1$s : %2$s + Impossible de rechercher des mises à jour sur Flathub + installé + dossier + Graphite + Surfaces graphite, avec le bleu de l\'orbe du logo pour tout élément sélectionné, ciblé ou pressé. + Papier sur noir + Le blanc est la couleur principale ; le bleu de l\'icône signale les filets, les anneaux et les points. + Bleu icône + Fond noir, texte blanc, et le bleu pour tout élément sélectionné, ciblé ou pressé. + Marine électrique + Un bleu plus vif, sur des surfaces qui tirent vers le marine plutôt que vers le gris. + Échap + Inser + Suppr + Début + Fin + PgPréc + PgSuiv + ⌫ Retour + Tab ⇥ + Verr Maj + Entrée ⏎ + ⇧ Maj + Ctrl + Super + Alt + Espace + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Installez d\'abord l\'environnement d\'exécution + La bibliothèque Steam n\'est pas disponible + L\'ancienne bibliothèque SD n\'est pas disponible + Le stockage des jeux n\'est pas disponible + Le dossier des jeux ajoutés n\'est pas disponible + Les manifestes Steam ne sont pas disponibles + Nombreux systèmes + Interne Système Linux + Carte SD + Stockage + Sélectionner + Propriétés + Ajouter aux favoris + Retirer des favoris + Renommer + Copier + Couper + Actions + Favoris + Épingler le dossier actuel + Aucun favori pour l\'instant - épinglez un dossier via son menu ⋮ pour y revenir rapidement. + Retirer des favoris + 0 octet + octets + Emplacement + Type + Taille + Modifié + Dossier + Fichier + Fichier %1$s + Lecture seule + Empêche les jeux (et Wine) d\'écraser ou de supprimer ce fichier. + Impossible de modifier « Lecture seule » + Masqué + Ce stockage ne peut pas enregistrer l\'attribut masqué. + Marque le fichier comme masqué sous Windows (attribut masqué de Wine). + Impossible de modifier « Masqué » sur ce stockage + Terminé + Suppression... + Échec de la suppression + Déplacement... + Copie... + Déplacement - %1$s + Copie - %1$s + Déplacement %1$d/%2$d - %3$s + Copie %1$d/%2$d - %3$s + %1$d terminé(s), %2$d en échec + %1$d terminé(s), %2$d ignoré(s) + + %1$d élément déplacé + %1$d éléments déplacés + %1$d éléments déplacés + + + %1$d élément copié + %1$d éléments copiés + %1$d éléments copiés + + « %1$s » existe déjà + Renommage... + Échec du renommage + Création du dossier... + Impossible de créer le dossier + Nouveau dossier + Nom du dossier + Créer + Nouveau nom + Supprimer ? + Supprimer définitivement « %1$s » ? + + Supprimer %1$d élément ? + Supprimer %1$d éléments ? + Supprimer %1$d éléments ? + + Cette action est irréversible. + + …et %1$d autre + …et %1$d autres + …et %1$d autres + + Suppression %1$d/%2$d - %3$s + + %1$d élément n\'a pas pu être supprimé + %1$d éléments n\'ont pas pu être supprimés + %1$d éléments n\'ont pas pu être supprimés + + Fusionner ajoute et remplace des fichiers dans le dossier existant. + Écraser remplace le fichier existant. + Appliquer à tous les conflits + Fusionner + Écraser + Conserver les deux + Ignorer + Sélectionner ce dossier + Retour + %1$s est monté, mais illisible pour le moment + Vue en liste + Vue en grille + Rechercher + Trier + Nom + Date de modification + Lignes aérées + Lignes compactes + Masquer les fichiers cachés + Afficher les fichiers cachés + Masquer les favoris + Afficher les favoris + Filtrer ce dossier + %1$s libres + %1$d sélectionné(s) + Aucun + Tout + Coller + + %1$d élément + %1$d éléments + %1$d éléments + + Coller %1$s ici + Coller %1$s ici (déplacer) + Fichiers + STOCKAGE + ACCÈS RAPIDE + FAVORIS + Téléchargements + Téléch. + ROMs + Journaux de session + Images + « %1$s » ajouté aux favoris + « %1$s » retiré des favoris + Dossier vide + Remonter d\'un niveau + Choisir un dossier + Choisir un fichier + Bleu Steam + Ciel + Blanc + Violet + Vert + Ambre + Rouge + Rose + Bouton A + Bouton B + Bouton X + Bouton Y + Bouton bombé gauche + Bouton bombé droit + Gâchette gauche + Gâchette droite + Bouton Affichage + Bouton Menu + Affichage + Menu + Croix haut + Croix bas + Croix gauche + Croix droite + Masqué + Faites glisser pour déplacer + Aimanter + Enregistrer + Aucun + Clavier + pavé tactile + Terminal + Poignées Deck + pavés tactiles + + Suppression de l\'environnement d\'exécution Linux + + Suppression de l\'environnement d\'exécution Linux · %1$d élément + Suppression de l\'environnement d\'exécution Linux · %1$d éléments + Suppression de l\'environnement d\'exécution Linux · %1$d éléments + + Environnement d\'exécution Linux supprimé + Impossible de terminer la suppression de l\'environnement d\'exécution Linux. Réessayez la suppression dans « Paramètres ». + Suppression des restes de l\'environnement d\'exécution + Téléchargement + Vérification + Extraction + Affiché pendant l\'installation ou la suppression de l\'environnement d\'exécution Linux + Installation de l\'environnement d\'exécution Linux + Suppression de l\'environnement d\'exécution Linux + + L\'environnement d\'exécution Linux n\'est pas installé + Le catalogue n\'a pas de somme de contrôle pour %1$s + Somme de contrôle incorrecte - rien n\'a été modifié + Installation de %1$s + Impossible de placer l\'AppImage + Échec de l\'extraction + Échec de l\'installation + + Échec de la configuration de Flatpak (code %1$d) + La configuration de Flatpak ne s\'est pas terminée + Une autre tâche de la boutique est en cours + Configurez d\'abord Flatpak + Démarrage + Flatpak s\'est arrêté avec le code %1$d + Installation : %1$s + Mise à jour : %1$s + Suppression : %1$s + Installation : %1$s (%2$d/%3$d) + Mise à jour : %1$s (%2$d/%3$d) + Suppression : %1$s (%2$d/%3$d) + traductions + pilotes graphiques + codecs multimédias + environnement d\'exécution %1$s + environnement d\'exécution %1$s %2$s + + Le fichier est trop petit pour être un AppImage + Impossible de lire le fichier : %1$s + Ce n\'est pas un AppImage (ce n\'est pas un programme Linux) + Cet AppImage n\'est conçu ni pour ARM64 (aarch64) ni pour les PC x86 + C\'est un programme Linux, mais pas un AppImage + Seuls les AppImage de type 2 peuvent être importés (celui-ci est de type %1$d) + Espace libre insuffisant : l\'importation nécessite environ %1$s + Espace libre insuffisant : la mise à jour nécessite environ %1$s + Échec de l\'importation + Cet AppImage n\'existe plus + Échec de la mise à jour + Extraction de %1$s + Impossible d\'extraire l\'AppImage + Impossible d\'extraire l\'AppImage : %1$s + + Arrêtez la session en cours avant d\'installer Decky Loader + Échec du téléchargement de Decky Loader + La taille de Decky Loader ne correspond pas aux métadonnées de la version + Impossible de télécharger la somme de contrôle de la version + La somme de contrôle de la version est manquante + La somme de contrôle de la version n\'est pas valide + Somme de contrôle de Decky Loader incorrecte + Le fichier de la version n\'est pas un exécutable Linux 64 bits compatible + Une session a démarré pendant le téléchargement ; arrêtez-la avant d\'installer Decky Loader + Impossible de rendre PluginLoader exécutable + Impossible d\'activer le débogage distant CEF de Steam + Impossible de préparer PluginLoader + Impossible de conserver le PluginLoader existant + Impossible d\'activer le nouveau PluginLoader + Arrêtez la session en cours avant d\'installer un plugin + Installez Decky Loader avant d\'importer un plugin + Impossible de lire le fichier ZIP sélectionné + Impossible de créer le dossier des plugins Decky + Extraction du ZIP du plugin + Le ZIP du plugin doit contenir un seul dossier de plugin à la racine + Il manque plugin.json, package.json ou dist/index.js dans le ZIP du plugin + Le nom du plugin est manquant + Une session a démarré pendant l\'installation ; arrêtez-la avant d\'installer un plugin + Plugin installé · redémarrez Steam pour le charger + Échec de l\'installation du ZIP du plugin + Le ZIP du plugin contient trop d\'éléments + Le ZIP du plugin contient un chemin non valide + Le ZIP du plugin contient des chemins en double + Le ZIP du plugin contient un chemin dangereux + Impossible de créer le dossier du plugin + Une fois décompressé, le ZIP du plugin dépasse la limite de 1 Gio + Impossible de rendre exécutable le binaire fourni avec le plugin + Entrée remote_binary non valide + Nom de binaire distant non valide + L\'URL du binaire distant doit utiliser HTTPS + Le binaire distant %1$s a un hachage SHA-256 non valide + Impossible de créer le dossier bin du plugin + Téléchargement du binaire du plugin · %1$s + Impossible de télécharger le binaire du plugin %1$s + Somme de contrôle du binaire du plugin incorrecte · %1$s + Impossible de rendre exécutable le binaire du plugin · %1$s + Impossible de remplacer le binaire du plugin · %1$s + Impossible d\'installer le binaire du plugin · %1$s + Le dossier du plugin appartient à un autre plugin + Impossible de conserver le plugin existant + Impossible d\'activer le plugin importé + Impossible de mettre à jour les paramètres des plugins Decky + Impossible de restaurer le plugin existant + + Impossible d\'enregistrer l\'identité du débogage sans fil + Échec de la commande ADB (%1$d) + Android n\'a pas confirmé la modification de la limite des processus enfants : %1$s + Aucun appairage de débogage sans fil n\'est enregistré + Saisissez l\'adresse IP et le port actuels du débogage sans fil + + Délai dépassé en attendant la fenêtre d\'appairage. Réessayez depuis DroidDeck. + Impossible de rechercher la fenêtre d\'appairage (%1$d). Vérifiez que le Wi-Fi est activé. + Impossible de rechercher la fenêtre d\'appairage : %1$s + Saisissez les 6 chiffres du code + Appairage… + Ce code n\'a pas fonctionné. Rouvrez la fenêtre d\'appairage et saisissez son nouveau code. + Appairé. Connexion… + Appairé, mais le port de débogage sans fil est introuvable. Laissez le débogage sans fil activé et réessayez. + Application du paramètre… + Échec de la commande de débogage sans fil + Configuration du débogage sans fil + Demande le code d\'appairage du débogage sans fil pendant que les paramètres sont ouverts + Appairer le débogage sans fil + Activez le débogage sans fil, puis appuyez sur « Associer l\'appareil avec un code d\'appairage ». + Activez le débogage sans fil, puis appuyez sur « Associer l\'appareil avec un code d\'appairage ». Restez dans les paramètres : le code se saisit dans cette notification. + Code d\'appairage à 6 chiffres + Appuyez sur « Saisir le code » et tapez les 6 chiffres affichés dans les paramètres. + Saisissez le code d\'appairage Wi-Fi + Saisissez à nouveau le code + Saisir le code + Configuration de Steam + Steam est prêt + Appuyez pour revenir à DroidDeck. Vous pouvez maintenant désactiver le débogage sans fil. + Configuration du débogage sans fil interrompue + + Le téléchargement a renvoyé HTTP %1$d + La taille du téléchargement ne correspond pas à l\'APK publié ; il a été supprimé - réessayez + Le téléchargement ne correspond pas à sa somme de contrôle et a été supprimé - réessayez + La mise à jour concerne le paquet Android %1$s, et non %2$s + Les métadonnées de la mise à jour ne mentionnent pas la clé de signature de publication de DroidDeck + Android n\'a pas pu lire l\'APK téléchargé + L\'APK téléchargé concerne %1$s, et non %2$s + L\'APK téléchargé indique versionCode %1$d, et non le %2$d publié + Android refuse d\'installer versionCode %1$d par-dessus le %2$d installé + L\'APK téléchargé ne contient aucune information de signature + L\'APK téléchargé n\'est pas signé avec la clé de publication de DroidDeck + Le catalogue des mises à jour a renvoyé HTTP %1$d + + Démarrage de la session… + %1$d:%2$02d écoulées · toujours en cours + Téléchargement de la mise à jour du client Steam · %1$d%% + Téléchargement du client Steam · %1$s + + Une installation d\'outil de compatibilité est déjà en cours + Arrêtez la session en cours avant d\'installer des outils de compatibilité + Aucune version ARM64 trouvée pour %1$s + Au moins 4 Go libres, plus la taille du téléchargement, sont nécessaires pour installer %1$s + Téléchargement de %1$s %2$s + Nouvelle tentative de téléchargement · essai %1$d sur %2$d + Téléchargement interrompu + Échec du téléchargement ; le fichier partiel est conservé pour pouvoir reprendre + Le téléchargement dépassait la taille de la version et a été supprimé + Le téléchargement est incomplet ; le fichier partiel est conservé pour une reprise ultérieure + Vérification du téléchargement + Impossible de lire la somme de contrôle de la version ; réessayez + %1$s %2$s ne publie aucune somme de contrôle ; rien n\'a été installé + Somme de contrôle incorrecte ; le téléchargement a été supprimé + Une session a démarré pendant le téléchargement ; arrêtez-la avant d\'installer des outils de compatibilité + Installation de %1$s + Échec de l\'installation de %1$s (code %2$d) + Impossible d\'installer %1$s + Récupération du pack droiddeck-esync + + avec les utilitaires Linux + DLL uniquement + Original (Proton mis à jour depuis le dernier remplacement) + Modifié en dehors de « Composants » + absent + Original %1$s + Original issu de %1$s + %1$s %2$s sera placé dans %3$s à la fermeture du jeu qui l\'utilise. + %1$s %2$s est maintenant dans %3$s. Prendra effet au prochain lancement d\'un jeu. + Cet ensemble original n\'est pas enregistré + %1$s de %2$s reviendra à l\'original à la fermeture du jeu qui l\'utilise. + %1$s de %2$s restauré à partir de l\'original %3$s. + %1$s est utilisé dans %2$s. Remplacez-le d\'abord par autre chose dans ce Proton. + %1$s n\'est pas enregistré + %1$s supprimé. + L\'original de la version installée est conservé : c\'est le moyen d\'y revenir. + Non enregistré + Original %1$s de %2$s supprimé. + Les téléchargements proviennent uniquement des versions Nightlies + Cette liste de paquets est antérieure aux sommes de contrôle - actualisez-la et réessayez + Somme de contrôle incorrecte + + Linéaire + Plus proche voisin + Snapdragon GSR (qualité) + Netteté seule + Auto (selon la mise à l\'échelle) + + Aucun effet appliqué. + Clarté de jeu + Netteté avec une pointe de contraste - pour tous les jours. + Éclatant + Couleurs poussées à fond. Anti-banding activé pour que le ciel ne forme pas de bandes. + Cinématique + Contraste plus profond, couleurs plus froides, contours lissés. + Compétitif + Ombres éclaircies et netteté renforcée - repérez les adversaires en premier. + Netteté adaptative + Netteté uniquement, couleurs intactes. + Filmique + Courbe façon pellicule, légèrement atténuée. + Arcade + Intense et percutant. + CRT rétro + Les passes tube CRT et signal analogique combinées. + Agrandissement net + Pour une résolution inférieure à celle de l\'écran. + Pixels nets + Pixels nets pour la 2D et les vieux jeux. + Contours anime + Contours façon dessin animé et netteté accrue. + + GPU non Adreno + Pris en charge + Expérimental : ses pilotes sont des versions de test + En dessous du matériel testé (Adreno 725 et plus récent) : risque de ne pas fonctionner + Non pris en charge : DroidDeck nécessite un GPU Adreno (Snapdragon) + + Limite de requêtes GitHub atteinte - réessayez plus tard + GitHub a répondu HTTP %1$d + le téléchargement a répondu HTTP %1$d + Cette liste de pilotes est antérieure aux sommes de contrôle - actualisez-la et réessayez + Somme de contrôle incorrecte - le téléchargement a été supprimé + Ce n\'est pas un pilote AdrenoTools : %1$s + aucun meta.json + meta.json est illisible (%1$s) + c\'est un pilote d\'exécution Linux (importez-le sous « Pilote d\'exécution Linux ») + c\'est un pilote de jeu Wayland, inutile pour cette application + meta.json n\'indique aucun libraryName, rien ne peut donc être transmis à AdrenoTools + meta.json indique %1$s, qui n\'est pas dans le zip + %1$s n\'est pas une bibliothèque partagée ELF AArch64 64 bits + %1$s utilise glibc - c\'est un Turnip « -Linux », à importer sous « Pilote d\'exécution Linux » + Aucun libvulkan_freedreno*.so dans ce zip. Un zip Turnip Android (AdrenoTools) ou -Wayland n\'est pas un pilote d\'exécution Linux. + %1$s n\'est pas une bibliothèque partagée ELF AArch64 64 bits. + %1$s n\'est pas un pilote glibc - il utilise la libc d\'Android. L\'environnement d\'exécution Linux nécessite un zip « -Linux » ; un Turnip standard ou « -Wayland » ne peut pas du tout être chargé par le client Steam. + Ce n\'est pas un bundle de pilotes : aucun manifest.json désignant un pilote Android et un pilote Linux + Ce n\'est pas un bundle de pilotes : %1$s pointe en dehors de celui-ci + Ce n\'est pas un bundle de pilotes : il dépasse %1$d Mo une fois décompressé + + Carte SD + Dossier + + Partager les journaux de session + + nécessite Android 10 ou plus récent (couches d\'affichage avec espace colorimétrique) + cet écran n\'indique aucune prise en charge HDR + cet écran prend en charge %1$s, mais pas HDR10 + + ce GPU + Limite des processus enfants activée + Limite des processus enfants non définie + Impossible de vérifier la limite des processus enfants + Limite des processus enfants désactivée + Limite des processus enfants non requise + %1$s Si ce n\'est pas possible, connectez cet appareil à un ordinateur avec ADB et exécutez la commande ci-dessous. Revenez ensuite ici et vérifiez à nouveau. + Les sessions Steam peuvent démarrer. + Cette version d\'Android n\'utilise pas cette limite. + Activez « Désactiver les restrictions des processus enfants » dans les options pour les développeurs, puis laissez les options pour les développeurs activées. + Cette version d\'Android ne dispose pas de cet interrupteur. Utilisez le débogage sans fil pour le modifier depuis cet appareil. + Cette version ne propose aucun téléchargement pour cette copie de DroidDeck. + Android ne l\'installera pas par-dessus cette version, car son versionCode %1$d est inférieur à celui installé (%2$d). + Android n\'installe pas une version plus ancienne par-dessus une plus récente. Restez sur cette version jusqu\'à ce que le canal la rattrape. + Cette copie de DroidDeck est signée différemment (version compilée localement ?), Android ne peut donc pas la mettre à jour sur place. + Espace libre insuffisant pour la mise à jour. + La mise à jour a été annulée. + La mise à jour n\'a pas été installée : %1$s + La mise à jour n\'a pas été installée. + Zip %1$s + zip + %1$s n\'a pas encore été lancé + Aucune sauvegarde trouvée pour %1$s + %1$s ne contient pas de dossier drive_c : ce n\'est donc pas un zip de sauvegarde GameHub ou Winlator + impossible de créer des dossiers dans %1$s + impossible d\'écrire dans %1$s + impossible d\'écrire dans %1$s (%2$s) + Dossier indisponible + Un autre fichier utilise déjà %1$s + Dossier des illustrations indisponible + Le dossier contient un inventaire de synchronisation sans rapport + Impossible de supprimer %1$s + Le fichier sort du dossier d\'exportation + FEX (version sans nom de version) + aucun profile.json dans %1$s + %1$s n\'est pas un paquet de composants Linux (type %2$s) + %1$s · %2$s libres + Automatique + Le catalogue de mises à jour n\'a pas passé les vérifications (%1$s) + Paramètre de processus enfants DroidDeck + Connecté · Mode hors ligne + Connecté + INTÉGRÉ + TÉLÉCHARGÉ + IMPORTÉ + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Affichage de la session + Résolution + Ajustement des jeux 16:9 + Comment les jeux 16:9 s\'adaptent à cet écran. À la prochaine session. + Choisissez « Adapter à l\'écran » ou une taille personnalisée plus étroite que 16:9. + Conserver les proportions + Étirer sur l\'écran + Mise à l\'échelle de l\'image + Filtre de mise à l\'échelle + Partagé entre Steam et le bureau. + S\'applique en direct. Partagé entre Steam et le bureau. + FSR, GSR et NIS s\'appliquent lors de l\'agrandissement de l\'image de la session. « Netteté seule » fonctionne aussi sans agrandissement. + Netteté du filtre + Fréquence d\'images + %1$s Règle la mise à l\'échelle sur %2$s. + Adapter à l\'écran (%1$d×%2$d) + Suivre l\'écran (s\'adapte au pliage et au dépliage) + Steam + Afficher les détails de l\'appareil + Masquer les détails de l\'appareil + Lecture des informations de l\'appareil… + Copier + Copiez ces détails dans un rapport d\'erreur. + Copié. + Forcer SSBS + Maintient activé un réglage de vitesse du CPU que Wine désactive sans cesse. Peut accélérer les jeux Windows sur Snapdragon 8 Elite ; peu de différence constatée sur les puces plus anciennes. Prendra effet au prochain lancement du jeu. + + Boutiques + Steam + Personnalisé + Boutiques + Ouvrir une boutique sur + Niveau de vitesse de téléchargement + Moteur des boutiques + ● Moteur prêt + Non compilé + Équilibré + Rapide + Max + Boutiques + Paramètres des boutiques + Paramètres + (non connecté) + Boutique + Installés + Bibliothèque + Tous + Téléchargements + %1$s (%2$d) + Se connecter + %1$s · se déconnecter + Se déconnecter de %1$s ? + Les jeux installés sont conservés. La bibliothèque sera récupérée à nouveau lors de votre prochaine connexion. + Rester connecté + Se déconnecter + Rechercher dans le catalogue %1$s… + Rechercher dans les jeux installés… + Rechercher dans votre bibliothèque… + Rechercher + Aucun jeu installé + Chargement + Aucun jeu + Chargement de la boutique… + Aucun catalogue + Nouveautés + Promotions + Jeux gratuits + Tendances + Votre bibliothèque + Jouer + Installer + Reprendre l\'installation + Interrompu + Installer · %1$s + Obtenir gratuitement + Voir sur %1$s + Acheter · %1$s + Installé + Dans la bibliothèque + Gratuit + Reprendre + Actualiser + Afficher le contenu pour adultes + Epic + EOS + Hors ligne + Désactivé + Connexion Epic (EOS) + Lancer hors ligne + Corriger la connexion Epic + Sauvegardes cloud + Envoyer vers le cloud + Télécharger depuis le cloud + Aucune sauvegarde cloud + Conflit cloud + Conflit cloud · %1$d fichiers + … + Garder la version cloud + Garder la version locale + Vérification du cloud… + Dernière synchro : %1$s + Pas encore synchronisé + %1$d fichiers + installé + dans votre bibliothèque + boutique + Succès + %1$d sur %2$d débloqués + %1$d / %2$d + Débloqués · %1$d + Verrouillés · %1$d + Cachés · %1$d + Succès caché + Les détails s\'affichent une fois débloqué. + Tous les succès débloqués + Lecture… + Connectez-vous à %1$s pour voir vos succès. + Impossible de joindre %1$s. Réessayez une fois en ligne. + %1$s n\'indique aucun succès pour ce jeu. + Tout afficher (%1$d) + Succès · %1$s + ✓ Dans Steam + Désinstaller + Désinstallation… + Désinstaller %1$s ? + Libère %1$s et le retire de Steam. + Supprime ses fichiers et son raccourci Steam. + Steam détectera le jeu à son prochain démarrage ; lancez-le depuis Steam, ou démarrez Steam maintenant. + Proton + Choix de Steam pour chaque jeu + Audio + DirectAudio + Composants + FEX, DXVK, VKD3D + Moteur + %1$s · natif + %1$s · intégré + Fonctionnement + Récupéré depuis + %1$s, avec votre compte + Installé dans + Stockage des jeux (Games/%1$s/%2$s) + Lancé par + Steam et Proton, comme les jeux Steam + Steam et Proton · connexion Epic à chaque lancement, superposition EOS désactivée + Steam et Proton · variables Amazon Games Services définies + Steam + ajouté à Steam à l\'installation (interface Deck + Steam Input) + Aucun téléchargement + En pause + En attente + En attente · n° %1$d + Annulé + Échec + Échec · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Pause + Reprendre + Annuler + Réessayer + Effacer + Effacer les terminés + Manifeste + Téléchargement + Vérification + Installation + Terminé + Contrôle en cours + Téléchargement en cours + Supprimer le téléchargement ? + Échec · %1$s conservés + Vérification en cours + Installation en cours + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d fichiers + Gestionnaire de téléchargements + Téléchargements simultanés + Installer dans + Installer %1$s : + Interne + %1$s libres + Téléchargements des boutiques + Affiche la progression des téléchargements de jeux et les poursuit en arrière-plan + Téléchargement en cours + %1$d téléchargements + Préparation… + Téléchargements des boutiques terminés + %1$s installé + Échec du téléchargement de %1$s + Supprimer + Conserver + Se connecter à %1$s + Connectez-vous avec votre fournisseur + Recharger + Fermer + Réessayer + Finalisation de la connexion… + Impossible de charger la page de connexion : %1$s + Ce fournisseur refuse la connexion depuis une page intégrée. Connectez-vous plutôt avec le mot de passe de la boutique. + La page de connexion a renvoyé une erreur (HTTP %1$d). + La liaison avec la page de connexion n\'est pas sécurisée. + Impossible de vérifier la connexion. Réessayez. + Échec de la connexion. Réessayez. Fermer Retour Modifier @@ -956,27 +2091,134 @@ Revenir à automatique %1$d jeu ajouté + %1$d jeux ajoutés %1$d jeux ajoutés %1$d déjà dans Jeux + %1$d déjà dans Jeux %1$d déjà dans Jeux - Succès - %1$d sur %2$d débloqués - %1$d / %2$d - Débloqués · %1$d - Verrouillés · %1$d - Cachés · %1$d - Succès caché - Les détails s\'affichent une fois débloqué. - Tous les succès débloqués - Lecture… - Connectez-vous à %1$s pour voir vos succès. - Impossible de joindre %1$s. Réessayez une fois en ligne. - %1$s n\'indique aucun succès pour ce jeu. - Tout afficher (%1$d) - Succès · %1$s + B · Retour vers %1$s + le jeu + les paramètres de jeu + Pilotes graphiques + Ce Proton + Tous les Proton + Pour tous les Proton + Disponible · %1$d + Vérifié : %1$s + Rechercher de nouveaux composants et pilotes + Obtenir + %1$s · DXVK, VKD3D-Proton et FEX ci-dessous sont ceux de ce Proton + %1$s · testé + %1$s · non testé + %1$s · non pris en charge + Maintient la paire recommandée à jour + Auto · %1$s + Pilotes d\'exécution et d\'affichage séparément + Utiliser %1$s ? + Supprimer %1$s ? + Diagnostic + Prêt à jouer + Pas encore prêt à jouer + environnement d\'exécution %1$s + limite des processus désactivée + connecté en tant que %1$s + Quand une intervention est requise + Apparence + Cet appareil + Illustrations de la bibliothèque + APPLICATIONS LINUX + Flathub + Applications pour le bureau Linux, à ouvrir depuis l\'onglet Bureau. + Configurer Flathub + Supprimer %1$s ? + Ses paramètres et fichiers sont conservés. + Conserver + Taille sur le disque + Désinstaller le bureau ? + Tous + Succès GOG + Synchronise les succès des jeux GOG pris en charge. S\'applique au prochain lancement. + Succès désactivés + Succès activés + Reconnectez-vous à GOG + Succès envoyés + Envoi du succès en attente + Activez les succès GOG dans les paramètres des boutiques + Succès indisponibles ; consultez les journaux de session + Succès débloqué + Coller + Pavé directionnel + Stick gauche + Stick droit + Menu + Clavier + Pavé directionnel haut + Pavé directionnel bas + Pavé directionnel gauche + Pavé directionnel droite + Commandes tactiles + Manette tactile + Souris + Activé + Désactivé + Lente + Normale + Rapide + Modifier la disposition + Menu DroidDeck + Faites glisser des commandes sur l\'écran + Organiser + Ignorer + Enregistrement… + Icône + L− + L+ + H− + H+ + Masquer + Couleur et opacité… + Copier cette disposition + Disposition copiée ; collez-la dans la disposition tactile de n\'importe quel jeu + Coller la disposition copiée + Rétablir celle de Steam + Cette commande n\'a pas d\'action assignée dans Steam pour lui donner une icône + Icône pour %1$s + Couleur de la disposition tactile + Terminé + Impossible d\'enregistrer la disposition tactile + Disposition tactile enregistrée sur Steam + Appli %1$d + Libellé (facultatif) + Les icônes de Steam n\'ont pas été trouvées + + %1$d icône de Steam + %1$d icônes de Steam + %1$d icônes de Steam + + Couleur de l\'icône + Couleur du bouton + Aucune icône + + Vérification de la connexion… + Récupération de votre bibliothèque… + Récupération de la liste des jeux… + Chargement des détails des jeux… (%1$d/%2$d) + + Synchronisation de %1$d jeu… + Synchronisation de %1$d jeux… + Synchronisation de %1$d jeux… + + Synchronisation… %1$d/%2$d + Récupération des jaquettes… + Échec de la synchronisation + Aucun jeu trouvé dans votre bibliothèque %1$s + Amazon Games n\'a pas répondu pour votre bibliothèque : actualisez, ou reconnectez-vous + Impossible de contacter GOG + Impossible de lire votre bibliothèque GOG + Enregistrement auprès de Steam… Développeur Éditeur Sortie @@ -985,4 +2227,34 @@ Complétionniste %1$s h %1$d min - \ No newline at end of file + Cardboard VR + Mode Cardboard + Vues pour l\'œil gauche et l\'œil droit, pour un casque VR pour téléphone. + Image + Écran virtuel + 3D côte à côte + Casque + Cardboard + Cardboard (2014) + Sans lentille + Écart entre les yeux + Largeur de l\'écran + Distance de l\'écran + Suivi de la tête + Cet appareil n\'a pas de capteur de rotation. + Recentrer la vue + Recentrer + SteamVR (expérimental) + Installer + Télécharge SteamVR ARM64 de Valve (571 Mo). + Téléchargement et décompression de SteamVR… + Démarrer + Lance SteamVR avec votre tête comme casque ; l\'image passe en 3D côte à côte. + Arrêter + SteamVR est en cours d\'exécution. + Indisponible dans cette session. + SteamVR ne répond pas ; consultez le journal de session. + Télécharger des composants + Les téléchargements sont partagés avec la page « Composants ». Choisissez une version téléchargée ci-dessus pour l\'utiliser au prochain lancement du jeu. + Choisissez une version à télécharger + diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml new file mode 100644 index 000000000..b36f6fd37 --- /dev/null +++ b/app/src/main/res/values-hi/strings.xml @@ -0,0 +1,2235 @@ + + + DroidDeck + + पहली बार चलाने पर Steam डाउनलोड होता है। इसमें कुछ मिनट लग सकते हैं। + डिफ़ॉल्ट रूप से, ऐप से बाहर जाने पर Steam रुक जाता है; वापस आने पर यह वहीं से शुरू होता है जहां छोड़ा था। + आपका कंट्रोलर Xbox 360 पैड के रूप में दिखाई देता है। नहीं तो, ऑन-स्क्रीन कंट्रोल का उपयोग करें। + + + पहली बार चलाने पर डेस्कटॉप सेट हो जाता है। इसमें कुछ मिनट लग सकते हैं। + डिफ़ॉल्ट रूप से, ऐप से बाहर जाने पर सेशन रुक जाता है; वापस आने पर यह वहीं से शुरू होता है जहां छोड़ा था। + टच टचपैड की तरह काम करता है: पॉइंटर घुमाने के लिए ड्रैग करें, क्लिक के लिए टैप करें। + आपका कंट्रोलर Xbox 360 पैड के रूप में दिखाई देता है। नहीं तो, ऑन-स्क्रीन कंट्रोल का उपयोग करें। + + सेशन + Steam या डेस्कटॉप सेशन खुला होने पर दिखता है + Steam चल रहा है + Steam डाउनलोड जारी है + सेशन रुका हुआ है - तैयार होने पर फिर से शुरू करें पर टैप करें + फिर से शुरू करें + अभी सस्पेंड करें + सेशन बंद करें + फ़्रेम जनरेशन + फ़्रेम जनरेशन मोड चुनें + बंद + Win-FG %1$d× + LSFG %1$d× + LSFG एडैप्टिव %1$d + Steam पर Lossless Scaling की ज़रूरत है + Steam में Lossless Scaling इंस्टॉल करें या Lossless.dll इंपोर्ट करें + Lossless.dll इंपोर्ट करें… + Steam वाली कॉपी का उपयोग हो रहा है + इंपोर्ट की गई कॉपी का उपयोग हो रहा है + Lossless.dll चुनें + Lossless.dll इंपोर्ट हो गई + यह Lossless.dll पहले से उपयोग में है + इस Steam खाते में Lossless Scaling नहीं है + यह फ़ाइल Lossless Scaling की Lossless.dll नहीं है + इस Lossless.dll से LSFG तैयार नहीं हो सका + LSFG को Steam से Lossless Scaling या इंपोर्ट की गई Lossless.dll चाहिए + LSFG अपने शेडर Lossless.dll से तैयार नहीं कर सका + %1$s: असमर्थित + %1$s: इंजन विफल + %1$s: को Lossless Scaling चाहिए + %1$s %2$s · शुरू हो रहा है + एक Steam लाइब्रेरी जोड़ता है और उसमें Windows गेम खोजता है। जिन गेम के मालिक आप हैं वे अपनी Steam एंट्री का उपयोग करते हैं; बाकी non-Steam शॉर्टकट बन जाते हैं। + गेम एनवायरनमेंट + Proton गेम के लिए एनवायरनमेंट वेरिएबल। + संपादित करें + सभी Proton गेम + प्रोफ़ाइल + Steam ऐप ID + खोलें + गेम %1$s + वेरिएबल जोड़ें + वेरिएबल का नाम + मान + सेव करें + हटाएं + फिर से इनहेरिट करें + एनवायरनमेंट ओवरराइड हटाएं + इस प्रोफ़ाइल के लिए अनसेट करें + कोई वेरिएबल सेट नहीं है। + इनहेरिटेड + कस्टम वेरिएबल + लोड या सेव नहीं हो सका। खाली स्टोरेज जांचें और फिर कोशिश करें। + मान्य नाम और 8192 वर्णों तक का मान इस्तेमाल करें। + स्टैंडर्ड Proton रनटाइम यह वेरिएबल नहीं पढ़ता। + लोड हो रहा है… + D3D12 फ़ीचर-लेवल ओवरराइड। 12_2 DirectX 12 Ultimate क्षमताएं दिखाता है; असमर्थित फ़ीचर फेल हो सकते हैं। ऑटोमैटिक पहचान के लिए इसे अनसेट रहने दें। + D3D12 शेडर-मॉडल ओवरराइड। 6_9 के लिए पर्याप्त नया VKD3D और मिलते GPU फ़ीचर चाहिए। ऑटोमैटिक पहचान के लिए इसे अनसेट रहने दें। + बार-बार कंपाइल होने से बचने के लिए Mesa शेडर कैशिंग चालू रखें। false कैशिंग को चालू करता है। + Mesa शेडर-कैश का अधिकतम आकार, जैसे 1G। बड़े कैश ज़्यादा स्टोरेज लेते हैं। + D3D12 के लिए VKD3D विकल्प। nodxr रे ट्रेसिंग बंद करता है; इसका उपयोग सिर्फ़ उन गेम के लिए करें जिन्हें इसकी ज़रूरत हो। + Proton डायग्नोस्टिक लॉग के लिए 1 सेट करें। लॉगिंग से परफ़ॉर्मेंस घट सकता है और स्टोरेज लगता है। + D3D9–11 संगतता जांच के लिए DXVK के बजाय OpenGL-आधारित WineD3D इस्तेमाल करने के लिए 1 सेट करें। + इस गेम को ntsync (droiddeck-ntsync या कर्नेल ड्राइवर) से दूर रखने के लिए 1 सेट करें; जहां पैक मौजूद है वहां यह droiddeck-esync इस्तेमाल करता है, वरना droiddeck-fsync। + इस गेम के लिए droiddeck-esync बंद करने के लिए 1 सेट करें; इसके बदले यह droiddeck-fsync इस्तेमाल करता है, या जहां fsync भी बंद है वहां droiddeck-ntsync। + इस गेम को droiddeck-fsync से दूर रखने के लिए 1 सेट करें; जहां पैक मौजूद है वहां यह droiddeck-esync इस्तेमाल करता है, वरना droiddeck-ntsync। + इस गेम को droiddeck-fsync पर चलाने के लिए 1 सेट करें, या droiddeck-esync को पहले रखने के लिए 0 — जहां कोई पैक इसके Proton से मेल खाता हो। + इस गेम के लिए Proton के गेमपैड-नेविगेशन हेल्पर को बंद करने के लिए 0 सेट करें। + D3D9–11 के लिए DXVK ओवरले। fps फ़्रेम रेट दिखाता है; अनसेट करने पर यह छिप जाता है। + सेमीकोलन से अलग की गई DXVK सेटिंग। नए बिल्ड फ़्रेम सीमित करने के लिए dxvk.maxFrameRate इस्तेमाल करते हैं; सामान्य खेल के लिए सेशन का फ़्रेम लिमिटर इस्तेमाल करना बेहतर है। + D3D12 फ़्रेम-रेट सीमा। 0 सीमा को बंद कर देता है। + Mesa OpenGL थ्रेडिंग। true, CPU-सीमित OpenGL गेम में मदद कर सकता है; हर गेम पर जांच लें। + Wine सिंक + गेम अपने थ्रेड को किस तरह तालमेल में रखते हैं। अगले सेशन से लागू होता है। + esync + ntsync + fsync + wineserver + FEX प्रीसेट + अगली बार गेम शुरू होने पर लागू होता है। + गेम + FEX डिफ़ॉल्ट + FEX के डिफ़ॉल्ट इस्तेमाल करता है। + स्टेबिलिटी + सबसे सुरक्षित, सबसे धीमी सेटिंग। + कम्पैटिबिलिटी + मल्टीब्लॉक के साथ पूरा TSO। + इंटरमीडिएट + घटी हुई x87 प्रिसिज़न के साथ संतुलित TSO। + परफ़ॉर्मेंस + सभी TSO बंद; मल्टीब्लॉक। + परफ़ॉर्मेंस + TSO + मुख्य TSO चालू के साथ परफ़ॉर्मेंस सेटिंग। + एक्सट्रीम + सेल्फ़-मॉडिफ़ाइंग-कोड जांच बंद करता है। JIT गेम और DRM काम करना बंद कर सकते हैं। + एक्सट्रीम + TSO + मुख्य TSO चालू के साथ एक्सट्रीम सेटिंग। + एक्सट्रीम-gn + TSC और मेटाडेटा हिंट के साथ परफ़ॉर्मेंस। एक्सट्रीम से पहले इसे आज़माएं। + Denuvo + पूरी कोड जांच के साथ परफ़ॉर्मेंस, और हाइपरवाइज़र बिट छिपा हुआ। + कस्टम मान + मान चुनें + एमुलेटर और ऐप + जोड़ें + आपका अपना ऐप या गेम + स्क्रिप्ट + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + शुरू हो रहा है… + %1$s जोड़ा गया + %1$s हटाया गया + डेस्कटॉप + वह ऐप अब नहीं है। इसे डेस्कटॉप से फिर से चुनें। + %1$s खोलें + हटाएं + हटाने के लिए फिर से दबाएं + हटाने के लिए सेशन बंद करें + %1$s को उसके फ़ोल्डर से चलाता है, जो सेशन में वहीं से लिंक रहता है। ऐप हटाने पर वह फ़ोल्डर जैसा है वैसा ही रहता है। + %1$s को DroidDeck के भीतर उसके फ़ोल्डर की एक कॉपी से चलाता है। ऐप हटाने पर कॉपी मिटती है, असली फ़ोल्डर नहीं। + पहले Linux रनटाइम इंस्टॉल करें + फ़ाइल अब नहीं है + पर्याप्त खाली जगह नहीं है: फ़ोल्डर कॉपी करने के लिए %1$s चाहिए + %1$s कॉपी हो रहा है + ऐप जोड़ना विफल रहा + %1$s का रिलीज़ खोजा जा रहा है + %1$s के रिलीज़ नहीं पढ़े जा सके + %1$s के किसी भी रिलीज़ में ARM64 या x86_64 AppImage नहीं है + %1$s डाउनलोड हो रहा है + डाउनलोड विफल रहा + ऐप अपडेट करना विफल रहा + वह आइकन पढ़ा नहीं जा सका + %1$s सेव हुआ + संपादित करें + अपडेट देखें + अपडेट देखे जा रहे हैं… + %1$s में अपडेट करें + नवीनतम · %1$s + %1$s अपडेट हुआ + %1$s · %2$s + ऐप जोड़ें + स्क्रिप्ट + AppImage + GitHub + Flatpak + एक .sh फ़ाइल। यह अपने फ़ोल्डर से gamescope के अंदर फ़ुल स्क्रीन में चलती है। + एक ARM64 (aarch64) AppImage, या एक x86_64 वाली जो FEX के ज़रिए चलती है। यह Linux रनटाइम में एक बार अनपैक होती है। + रिपॉज़िटरी के सबसे नए रिलीज़ से ARM64 AppImage इंस्टॉल करता है, या FEX के ज़रिए इसकी x86_64 वाली। + FEX इस्तेमाल करें + ऑटो + चालू + बंद + FEX इस ARM64 डिवाइस पर x86 Linux प्रोग्राम चलाता है। प्रोग्राम x86 के लिए बना होने पर ऑटो इसे इस्तेमाल करता है। + FEX के ज़रिए %1$s + यह प्रोग्राम %1$s पीसी के लिए बना है और FEX के ज़रिए चलता है, जो अभी सेट नहीं है। साइन इन रहते हुए Steam एक बार खोलें: DroidDeck इससे FEX और ज़रूरी Steam Linux Runtime डाउनलोड करवा लेगा, फिर यह खुल जाएगा। + यह प्रोग्राम + %1$s x86 पीसी के लिए बना है और FEX के ज़रिए चलता है, जो अभी इंस्टॉल नहीं है। साइन इन रहते हुए Steam एक बार शुरू करें और इसे FEX और Steam Linux Runtime डाउनलोड करने दें, फिर इसे फिर से शुरू करें। + एक Flathub ऐप ID या उसका Flathub पेज। अगर Flatpak अभी सेट नहीं है, तो पहले वह सेट होगा। + फ़ाइल + कोई फ़ाइल नहीं चुनी गई + फ़ाइल चुनें + बदलें + स्क्रिप्ट चुनें + AppImage चुनें + आइकन चुनें + रिपॉज़िटरी + owner/repo या github.com का लिंक + यह GitHub रिपॉज़िटरी नहीं है + ऐप ID + org.ppsspp.PPSSPP + यह Flathub ऐप ID नहीं है + या एक .flatpak फ़ाइल, उस ऐप के लिए जो Flathub पर नहीं है। + .flatpak फ़ाइल चुनें + सोर्स + आपके डेस्कटॉप पर + नया ऐप + नाम + ऐप से + आइकन + डिफ़ॉल्ट आइकन + आइकन चुनें + हटाएं + रिपॉज़िटरी से + आइकन खोजे जा रहे हैं… + डिफ़ॉल्ट + %1$s संपादित करें + सेव करें + फ़ोल्डर जांचा जा रहा है… + इसका फ़ोल्डर सेशन में लिंक है: %1$s + इसके फ़ोल्डर में Linux प्रोग्राम हैं, जिन्हें शेयर्ड स्टोरेज चला नहीं सकता, इसलिए इसे DroidDeck में कॉपी किया जाता है (%1$s)। + जोड़ें + रद्द करें + पहले Setup से Linux रनटाइम इंस्टॉल करें। + %1$s के पूरा होने का इंतज़ार करें। + कम्पैटिबल + Steam के Compatibility मेनू में “%1$s” के निशान वाले बिल्ड में DroidDeck के प्रीफ़िक्स फ़िक्स होते हैं और यहां गेम शुरू कर पाते हैं। डिफ़ॉल्ट है Proton Experimental ARM64 (%1$s), या नया बिल्ड इंस्टॉल होने पर Proton 11.0 ARM64 (%1$s)। बिना निशान वाली Valve की खुद की Proton एंट्री इस डिवाइस पर गेम शुरू नहीं कर सकती। + इंस्टॉल है %1$s · %2$s + रद्द करें + ऑटो + कभी नहीं + हमेशा + अनइंस्टॉल करें + जांचें + अपडेट करें + जांचा जा रहा है… + काम हो रहा है… + भूलें + जोड़ें… + इस्तेमाल करें + अगले सेशन से लागू होता है। + Steam सेशन + डेस्कटॉप सेशन + डिस्प्ले + रिज़ॉल्यूशन + 720p तक + 900p तक + 1080p तक + नेटिव + %1$s (डिफ़ॉल्ट) + कस्टम · %1$d×%2$d + कस्टम… + 720p मेनू को ज़्यादा रिस्पॉन्सिव बना सकता है। + स्क्रीन रेशियो + कस्टम रिज़ॉल्यूशन से तय होता है। + ऑटो कम से कम 16:9 इस्तेमाल करता है। + फ़्रेम सीमा + पूरे सेशन को सीमित करता है। डिस्प्ले उस रिफ्रेश रेट पर चला जाता है जिसे यह सीमा बराबर बांटती है। + HDR + HDR10 आउटपुट + उपलब्ध नहीं: %1$s। + लागू करने के लिए ऐप रीस्टार्ट करें। + ड्राइवर + GPU ड्राइवर + Steam और डेस्कटॉप दोनों में साझा। Components पेज पर सेट करें। + टच और कंट्रोल + टच + टचपैड + डायरेक्ट + ऑटो: डेस्कटॉप पर टचपैड, Steam में डायरेक्ट। + ऑन-स्क्रीन कंट्रोल + Steam + QAM + कोई कंट्रोलर कनेक्ट न होने पर ऑटो कंट्रोल दिखाता है। + Steam टच (बीटा) + Steam के टच कंट्रोल अगले Steam सेशन से शुरू होंगे + Steam टच लेआउट + इस गेम के टच कंट्रोल को घुमाएं, आकार बदलें या छिपाएं; Steam में सेव होता है + कंट्रोलर + Steam आपके कंट्रोलर को किस तरह पहचानता है। अगले सेशन से लागू होता है। + Steam Deck कंट्रोलर + Steam Controller + DualShock 4 + DualSense + Xbox 360 कंट्रोलर + Deck: Quick Access बटन और gyro। Steam Controller: वही, साथ में पीछे के बटन पर ग्रिप सेंस। DualShock 4 और DualSense: gyro, और ट्रैकपैड उनके टचपैड के तौर पर। Xbox 360: Guide+A से Quick Access खोलें। + बैक + मेनू → QAM + QAM → मेनू + सेशन + बैकग्राउंड व्यवहार + ऐप छिपने या स्क्रीन बंद होने पर। + मैनुअल + नेटिव + ऑटो दिखने पर रुकता है और फिर से शुरू होता है। नेटिव पहले Steam को स्लीप के लिए तैयार करता है। मैनुअल फिर से शुरू करें का इंतज़ार करता है। कभी नहीं वाला चलता रहता है। + डाउनलोड चलते रहने दें + Steam के सक्रिय रूप से डाउनलोड करते समय बैकग्राउंड सस्पेंशन को टालें। + ऑटो बैकग्राउंड में रुकता है और दिखने पर फिर से शुरू होता है। मैनुअल फिर से शुरू करें का इंतज़ार करता है। कभी नहीं वाला चलता रहता है। + स्टार्टअप + DroidDeck के साथ Steam शुरू करें + DroidDeck लॉन्च होने पर Steam सेशन खोलता है। + Decky + रिलीज़ देखी जा रही हैं… + %1$s इंस्टॉल करने के लिए तैयार। + कोई कम्पैटिबल बिल्ड नहीं मिला। बाद में फिर कोशिश करें। + इंस्टॉल है · %1$s + अपडेट उपलब्ध · %1$s + नवीनतम · %1$s + लोडर + नवीनतम इंस्टॉल करें + Steam के साथ शुरू होता है। चालू रहते हुए, इस डिवाइस के दूसरे ऐप Steam को नियंत्रित कर सकते हैं। + बंद। Steam का रिमोट डिबगिंग पोर्ट बंद रहता है। + Decky Loader अनइंस्टॉल करें? + लोडर हटाता है। आपके प्लगइन और सेटिंग बनी रहती हैं। + क्लाइंट + परफ़ॉर्मेंस ओवरले + Deck मोड में गेम क्रैश होने या काली स्क्रीन होने पर इसे बंद करें। अगले सेशन से लागू होता है। + Steam क्लाइंट रिपेयर करें + अगली बार Steam शुरू होने पर Steam क्लाइंट फिर से डाउनलोड होता है। इसे इस्तेमाल करें जब Steam अपनी लोडिंग स्क्रीन पर अटका रह जाए। गेम, साइन-इन और सेटिंग बनी रहती हैं। + अगली बार Steam शुरू होने पर Steam क्लाइंट फिर से डाउनलोड होगा। + रिपेयर करें + आर्टवर्क + SteamGridDB API कुंजी + आपकी कुंजी + संपादित करें… + पहले Steam से, फिर SteamGridDB से छूटी हुई आर्टवर्क भरता है। गेम फ़ोल्डर में आपकी अपनी cover.jpg, hero.jpg, logo.png या icon.png को प्राथमिकता मिलती है। + %1$s हटाएं + हटाएं? + गेम जोड़ें + गेम जोड़ें: + exe चुनें: + यह जगह सेशन में उपलब्ध नहीं है + इस exe को जांचें + Windows कॉम्पोनेंट + LB / RB से सेक्शन के बीच जाएं। + कोई नहीं + कॉम्पोनेंट लिस्ट से वे Windows DLL जो Proton में शामिल नहीं हैं। जिन्हें इस गेम को ज़रूरत है उन्हें चालू करें: वे गेम के अपने Windows फ़ोल्डर में जाते हैं, उसकी फ़ाइलों में कभी नहीं, और जो भी Proton यह चलता है उसमें बने रहते हैं। + कॉम्पोनेंट लिस्ट तक नहीं पहुंच सका। पहले से डाउनलोड किए कॉम्पोनेंट अब भी बदले जा सकते हैं। + इस गेम के लिए सुझाए गए + यह गेम किसकी ज़रूरत है देखा जा रहा है… + ऑनलाइन होने पर उपलब्ध + + %1$d कॉम्पोनेंट चालू किया जा रहा है… + %1$d कॉम्पोनेंट चालू किए जा रहे हैं… + + इस गेम के लिए चालू + Steam %2$s (ऐप %3$s) के साथ %1$s इंस्टॉल करता है + इस गेम के लिए GOG की लिस्ट से + इस गेम के लिए Epic की ज़रूरी शर्तों से + इस गेम के लिए Amazon के इंस्टॉल स्टेप से + Steam नाम से मिलान करके ऐप %2$s के साथ %1$s इंस्टॉल करता है + ये भी सूचीबद्ध + %1$s द्वारा सूचीबद्ध; यहां कॉम्पोनेंट नहीं है + Steam + GOG + Epic Games + Amazon Games + गेम %1$s के साथ आता है + गेम में %1$s है (अक्सर ऐसे ही काम करता है) + इस गेम में पहले से मौजूद + गेम के फ़ोल्डर में किसी कॉम्पोनेंट का कोई संकेत नहीं है। + इस गेम के लिए इंस्टॉल (%1$d) + सभी कॉम्पोनेंट (%1$d) + Windows इंस्टॉलर की ज़रूरत (%1$d) + ये अपने खुद के सेटअप प्रोग्राम हैं, जिनके भीतर खोलने लायक कोई पैकेज नहीं है, और अभी तक यह रिकॉर्ड नहीं है कि वे क्या इंस्टॉल करते हैं। ज़्यादातर पुराने रनटाइम हैं जिनकी जगह Proton के भीतर का Mono पहले से ले लेता है; रिकॉर्ड बनते ही जोड़े जाते हैं। + दिखाएं + छिपाएं + अपना सेटअप प्रोग्राम चलाता है (अभी रिकॉर्ड नहीं) + अभी यहां समर्थित नहीं + कॉम्पोनेंट लोड हो रहे हैं… + इंस्टॉल नहीं हो सका: %1$s + डाउनलोड हो गया + %1$s डाउनलोड हो रहा है + इंस्टॉल हो रहा है: %1$s + बंद किया जा रहा है… + %1$s डाउनलोड + अगली बार गेम शुरू होने पर लागू होगा। + 16:9 गेम को स्ट्रेच करें + 16:9 से ज़्यादा चौकोर स्क्रीन पर, बार दिखाने के बजाय गेम को भरने के लिए स्ट्रेच करता है। अगले सेशन से लागू होता है। + ऑडियो + गेम के लिए DirectAudio + कम लेटेंसी के लिए PulseAudio को बायपास करता है। + माइक्रोफ़ोन + वॉइस चैट के लिए। + नेटवर्क + Wi-Fi खोज + Steam में Wi-Fi नाम और आस-पास के नेटवर्क दिखाएं। + लोकेशन सेवाएं बंद हैं। + लोकेशन अनुमति नहीं दी गई। + सेटिंग खोलें + जारी रखें + Wi-Fi खोज चालू करें? + Wi-Fi नाम पढ़ने और आस-पास के नेटवर्क खोजने के लिए Android को लोकेशन अनुमति चाहिए। DroidDeck इस एक्सेस का उपयोग Steam में वे नेटवर्क दिखाने के लिए करता है। हम आपकी लोकेशन ट्रैक नहीं करते। इंटरनेट एक्सेस इस अनुमति के बिना भी काम करता है। + गेम स्टोरेज + ऑटोमैटिक - मौजूद होने पर SD कार्ड, और आपके Games फ़ोल्डर + ऑटोमैटिक - आपके Games फ़ोल्डर, और मौजूद होने पर SD कार्ड (अभी कोई नहीं) + केवल इंटरनल + फ़ोल्डर: %1$s + दूसरी लाइब्रेरी + SD या शेयर्ड स्टोरेज से एसेट स्ट्रीम करने वाले गेम अटक सकते हैं। + स्टोरेज डायग्नोस्टिक्स + Share logs में एलोकेशन नतीजे और सैंपल किए स्टोरेज टाइमिंग रिकॉर्ड करें। चालू रहते हुए इंस्टॉल धीमे हो सकते हैं। अगले सेशन से लागू होता है। + फ़ोल्डर चुनें… + कस्टम रिज़ॉल्यूशन + सीमा और रेशियो को बदल देता है। स्क्रीन से न मिलने वाले आकार में बार दिखेंगे। + चौड़ाई + ऊंचाई + 320×240 और 3840×2160 के बीच। + + %1$d गेम + %1$d गेम + + ठीक है + हटाएं + वापस लाएं + डाउनलोड करें + %1$s हटाएं + %1$.1f MB + रनटाइम ड्राइवर + Steam, गेम और डेस्कटॉप में उपयोग होता है। अगले सेशन से लागू होता है। + Turnip zip इंपोर्ट करें… + डिस्प्ले ड्राइवर + दोनों मोड में स्क्रीन पर फ़्रेम बनाता है। लागू करने के लिए ऐप रीस्टार्ट करें। + AdrenoTools zip इंपोर्ट करें… + कभी नहीं + Nightlies की जांच %1$s को हुई + कॉम्पोनेंट + कॉम्पोनेंट के बारे में + नए ड्राइवर देखें + नए पैकेज के लिए Nightlies देखें + Proton + यहां चुना गया Proton उन सभी गेम को चलाता है जिन्होंने Steam में अपना अलग Proton नहीं चुना है, और Steam में किया गया बदलाव यहां भी दिखता है। Steam अपडेट होने पर Valve का Proton बदल देता है; हर बिल्ड के ओरिजिनल सुरक्षित रहते हैं। + कॉन्फ़िगरेशन + प्रोफ़ाइल कॉन्फ़िगर करें + प्रोफ़ाइल + डिफ़ॉल्ट प्रोफ़ाइल + बिना ओवरराइड वाले गेम इसे खुद इस्तेमाल करते हैं। + %1$s शुरू होने पर सेव की गई सेटिंग खुद इस्तेमाल होती हैं। + सिर्फ़ वे सेटिंग चुनें जिनकी इस गेम को ज़रूरत है। बाकी सब डिफ़ॉल्ट प्रोफ़ाइल का इस्तेमाल करते रहते हैं। + FEX प्रीसेट + डिफ़ॉल्ट प्रोफ़ाइल का प्रीसेट इस्तेमाल हो रहा है। + इस गेम के लिए कस्टम प्रीसेट सेव किया गया है। + कस्टम FEX वेरिएबल + डिफ़ॉल्ट इस्तेमाल करें + गेम कम्पैटिबिलिटी + इस गेम के लिए Proton, DXVK, VKD3D और सिंक्रोनाइज़ेशन एनवायरनमेंट विकल्प। + डिफ़ॉल्ट प्रोफ़ाइल का Proton इस्तेमाल हो रहा है। + Steam कम्पैटिबिलिटी मैपिंग के ज़रिए इस गेम के लिए कस्टम Proton सेव किया गया है। + कोई Proton नहीं + DXVK + VKD3D-Proton + अगर यह गेम कोई और पैकेज न चुने, तो %1$s से डिफ़ॉल्ट प्रोफ़ाइल का पैकेज इस्तेमाल होता है। + डिफ़ॉल्ट प्रोफ़ाइल का कॉम्पोनेंट पैकेज इस्तेमाल होता है। + सिर्फ़ इस गेम के लिए %1$s इस्तेमाल होता है। + रनटाइम विकल्प + हर गेम के लिए अन्य Proton, DXVK, VKD3D-Proton और सिंक्रोनाइज़ेशन एनवायरनमेंट विकल्प। + %1$s की प्रोफ़ाइल अगली बार गेम शुरू होने के लिए सेव है। + Steam के बिना शुरू करें + ऐसे गेम के लिए जो तुरंत बंद हो जाता है: इसे Proton के Steam सेटअप के बिना शुरू करता है। इसके लिए ओवरले जैसी Steam सुविधाएं बंद रहती हैं। + चालू + %1$s का Proton चुनाव सेव है और अभी Steam में उपलब्ध है। + %1$s का Proton चुनाव सेव है। गेम शुरू करने से पहले Steam रीस्टार्ट करें। + %1$s इस डिवाइस पर चुना गया Proton इस्तेमाल नहीं कर सकता। + %1$s की प्रोफ़ाइल लोड नहीं हो सकी: %2$s + गेम प्रोफ़ाइल सेव नहीं हो सकी: %1$s + गेम प्रोफ़ाइल रीसेट करें + %1$s की प्रोफ़ाइल रीसेट करें? + इस गेम का Proton, FEX, कॉम्पोनेंट पैकेज, एनवायरनमेंट, टेक्सचर फ़िल्टरिंग, और Windows कॉम्पोनेंट ओवरराइड हटाएं? यह डिफ़ॉल्ट प्रोफ़ाइल इस्तेमाल करेगा। + %1$s अब डिफ़ॉल्ट प्रोफ़ाइल इस्तेमाल करता है। + गेम प्रोफ़ाइल पूरी तरह रीसेट नहीं हो सकी: %1$s + %1$s अब उन सभी गेम को चलाता है जिन्होंने Steam में अपना Proton नहीं चुना है। + अगली बार Steam शुरू होने से, %1$s उन सभी गेम को चलाएगा जिन्होंने अपना Proton नहीं चुना है। + %1$s यहां गेम शुरू नहीं कर सकता, इसलिए यह डिफ़ॉल्ट नहीं हो सकता। + डिफ़ॉल्ट Proton सेव नहीं हो सका: %1$s + %1$s · %2$d बदला गया + %1$s · सभी ओरिजिनल + %1$s · गेम चल रहा है + %1$s · droiddeck-esync पैक + %1$s · अभी droiddeck-esync पैक नहीं है + %1$s · esync पहले से मौजूद, पैक की ज़रूरत नहीं + GPU ड्राइवर + %1$s · लॉन्च %2$s पर फिर से लागू हुआ + आगे: %1$s · गेम बंद होने के बाद + उपयोग में %1$s: %2$s + गेम बंद होने तक इंतज़ार करता है + अगली बार गेम शुरू होने पर लागू होता है + .zip इंपोर्ट करें + Proton पढ़े जा रहे हैं… + अभी कोई Proton इंस्टॉल नहीं है। इसका ARM64 Proton डाउनलोड करने के लिए Steam एक बार शुरू करें, या Setup से GE-Proton, CachyOS या DroidDeck-Proton जोड़ें। + ओरिजिनल · %1$s + %1$s · यह बिल्ड + %1$s · पहले का बिल्ड + इंस्टॉल है · %1$d + %1$s के लिए अभी कुछ सेव नहीं है। इस Proton की अपनी फ़ाइलें इसके ओरिजिनल के रूप में सेव हैं। + इस Proton पर एक गेम चल रहा है, इसलिए यह बंद होने तक इंतज़ार करता है। + अगली बार गेम शुरू होने पर लागू होता है। + %1$s बदलें? + %1$s को %2$s में डालें? इसकी ओरिजिनल फ़ाइलें सेव रहती हैं। %3$s + %1$s वापस लाएं? + %1$s का ओरिजिनल %2$s में वापस डालें? %3$s + ऐप से पैकेज हटाता है। आप इसे फिर से डाउनलोड कर सकते हैं। + यह ओरिजिनल हटाएं? + यह %1$s ओरिजिनल %2$s के पहले वाले बिल्ड से है। मौजूदा बिल्ड का ओरिजिनल हमेशा रखा जाता है। + Nightlies पर · %1$d + Nightlies के पैकेज सूचीबद्ध करने के लिए रीफ़्रेश करें। + पिछली जांच के बाद कुछ नया नहीं है। + .wcp इंपोर्ट करें + इस Proton का ओरिजिनल वापस लाएं + %1$s का ओरिजिनल %2$s वापस डालें? + बदलें + हर उस Proton के लिए FEX, DXVK और VKD3D-Proton जिससे Steam गेम चलाता है।\n\nकिसी इंस्टॉल की गई पंक्ति को चुनकर उसे बदलें। यह अगली बार गेम शुरू होने पर, या चल रहा गेम बंद होने के बाद लागू होता है।\n\nआपके चुनाव हर लॉन्च से पहले फिर से जांचे जाते हैं और कुछ बदल गया हो तो वापस रखे जाते हैं। हर Proton की ओरिजिनल फ़ाइलें रखी जाती हैं, इसलिए Steam अपडेट से वे कभी नहीं खोती। + ओरिजिनल + उपयोग में + नया + सेव है + परफ़ॉर्मेंस + CPU कोर, सेशन फ़िक्स और होस्ट नाम। अगले सेशन से लागू होता है। + Steam क्लाइंट कोर + Steam के कोर चुनाव को ओवरराइड करें + Steam, उसके UI हेल्पर और gamescope को नीचे दिए कोर पर पिन करता है। + क्लाइंट कोर + Steam के लिए कोर। + चुनने के लिए ओवरराइड चालू करें। + गेम कोर + गेम कोर + गेम के लिए कोर। सभी चुने होने पर, Android खुद शेड्यूल करता है। + क्लाइंट इंटरफ़ेस + थ्रेडेड GL + Steam मेनू को ज़्यादा रिस्पॉन्सिव बना सकता है। + Zink: लेज़ी डिस्क्रिप्टर + डिस्क्रिप्टर बफ़र न रखने वाले ड्राइवर के लिए सुझाया गया। + GL एरर जांच छोड़ें + हर कॉल पर होने वाली GL वैलिडेशन बंद करता है। + gamescope: रीयलटाइम GPU क्यू + कम्पोज़िटर के GPU काम को प्राथमिकता देता है। गेम की कुछ कीमत पर फ़्रेम पेसिंग को स्मूथ कर सकता है। + GPU + GPU को उसकी टॉप क्लॉक पर बनाए रखें + सिर्फ़ Adreno के लिए। कम हिचकोले, लेकिन ज़्यादा बैटरी खर्च और गर्मी। + सेशन फ़िक्स + Turnip: sysmem रेंडरिंग + Steam और ज़्यादातर गेम के लिए तेज़। Adreno 710/720/722 पर ज़रूरी। + Steam के xalia हेल्पर को छोड़ें + droiddeck-ntsync (एक्सपेरिमेंटल) + जांच के लिए Linux ntsync ड्राइवर की यूज़रस्पेस जगह लेने वाली चीज़। चालू रहते हुए, Proton इसे droiddeck-esync और droiddeck-fsync के बदले इस्तेमाल करता है। यह Proton Experimental ARM64, GE-Proton और proton-cachyos के साथ काम करता है। अगले सेशन से लागू होता है। + droiddeck-esync + droiddeck-fsync पहले + स्टॉक Proton फ़ाइलों पर Proton का अपना fsync चलाता है, futex_waitv यूज़रस्पेस में दिया जाता है, इसलिए इसे किसी पैक की ज़रूरत नहीं और Steam के Proton अपडेट करते ही यह काम करता रहता है। चालू रहते हुए, गेम इसे droiddeck-esync से पहले इस्तेमाल करते हैं, सिवाय उन कुछ के जिन्हें Proton fsync से दूर रखता है। अगले सेशन से लागू होता है। + गेम के Windows थ्रेड wineserver के राउंड ट्रिप के बजाय eventfd और शेयर्ड मेमोरी से एक-दूसरे का इंतज़ार करते हैं। उस खास Proton बिल्ड के लिए बने droiddeck-esync पैक का उपयोग करता है; पैक अपने आप मिल जाते हैं। किसी नए Proton बिल्ड के लिए पैक न होने तक droiddeck-fsync जगह लेता है। अगले सेशन से लागू होता है। + Proton के गेमपैड नेविगेशन हेल्पर को बंद करता है, जो हर गेम में CPU खर्च करता है। + proot को seccomp के बिना चलाएं + कुछ कर्नेल पर missing-syscall एरर ठीक कर सकता है। परफ़ॉर्मेंस घटा सकता है। + proot फ़ास्ट पाथ + proot के seccomp फ़िल्टर की ज़रूरत है। + तेज़ फ़ाइल लुकअप और कम अटकाव। अगर कोई प्रोग्राम अपनी फ़ाइलें न खोज पाए तो इसे बंद करें। + सेशन पहचान + होस्ट नाम + वह नाम जो Steam इस डिवाइस के लिए देखता है। खाली छोड़ने पर %1$s वापस आ जाता है। + सिर्फ़ अक्षर, अंक और बीच के हाइफ़न, 63 वर्णों तक। + Android इस सेशन को बंद करने के लिए सेट है + इंस्टॉल है + डाउनलोड के लिए उपलब्ध + पिछली जांच के बाद कुछ नया नहीं है। + बिल्ट-इन ड्राइवर वापस लाएं + ऐप में बिल्ट-इन: यह इसकी अनपैक की गई फ़ाइलें हटाता है और इसे छिपा देता है। बिल्ट-इन ड्राइवर वापस लाएं इसे फिर से वापस लाता है। + Android + Linux बंडल का आधा हिस्सा: दोनों हिस्से हटाए जाते हैं। उपयोग में मौजूद ड्राइवर डिफ़ॉल्ट पर वापस चला जाता है। + इसकी फ़ाइलें ऐप से हटा दी जाती हैं। + इसकी फ़ाइलें हटा दी जाती हैं। यह उपयोग में है, इसलिए डिफ़ॉल्ट इसकी जगह ले लेता है। + डिस्प्ले + इफ़ेक्ट + कंट्रोल + कॉम्पोनेंट + कंट्रोलर + सेटिंग + %1$s पेज + Steam + डेस्कटॉप + Steam मेनू + Quick Access + Quick Access Menu खोलें + पिछला पेज + अगला पेज + परफ़ॉर्मेंस HUD + स्केलिंग मोड + स्केलिंग शार्पनेस + स्क्रीन इफ़ेक्ट + लाइव। कोई इफ़ेक्ट चालू होने पर थोड़ा GPU समय लगता है। + प्रीसेट + कस्टम + एडवांस्ड + विस्तृत + संक्षिप्त + शार्पनिंग (CAS) + शार्पनिंग स्तर + फ़ेक HDR + डीबैंडिंग + डीबैंडिंग तीव्रता + ब्राइटनेस + कंट्रास्ट + गामा + सैचुरेशन + FXAA (एज स्मूदिंग) + टून आउटलाइन + CRT + NTSC + टेक्सचर फ़िल्टरिंग + DirectX 9–11 गेम, उनके अगले लॉन्च से। + एनिसोट्रॉपिक फ़िल्टरिंग + टेक्सचर शार्पनेस + एक नेगेटिव mip बायस: ज़्यादा शार्प टेक्सचर, कुछ शिमर। ऑटो उस हद से मेल खाता है जितना सेशन को स्क्रीन के लिए बड़ा किया गया है। + ऑटो (%1$s) + कीबोर्ड + पीसी कीबोर्ड + Android + दूसरी स्क्रीन + मोड + दिखाता है + Android ऐप + ऐप लॉन्च करें + छिपाएं + दिखाएं + लॉन्च करने लायक कोई ऐप नहीं + सेशन व्यवहार + अभी और इस मोड के भविष्य के सेशन पर लागू होता है। + अगला सेशन + सपोर्ट + सेशन लॉग + बग रिपोर्ट में जोड़ें + लॉग शेयर करें + बैकग्राउंड + बंद करें + सेशन बंद करें? + अभी चल रहा है + किसी Proton पर कोई गेम नहीं चल रहा + गेम प्रोफ़ाइल + प्रोफ़ाइल जानकारी अभी उपलब्ध नहीं है। + गेम प्रोफ़ाइल लोड नहीं हो सकीं। अपने आप फिर कोशिश हो रही है। + गेम प्रोफ़ाइल रीफ़्रेश नहीं हो सकीं। आख़िरी उपलब्ध डेटा दिखाया जा रहा है; अपने आप फिर कोशिश हो रही है। + अगले लॉन्च पर इस्तेमाल होने वाली सेव की गई सेटिंग देखें और बदलें। + डिफ़ॉल्ट प्रोफ़ाइल इस्तेमाल हो रही है + कस्टम प्रोफ़ाइल + डिफ़ॉल्ट प्रोफ़ाइल से %1$s इस्तेमाल हो रहा है। + बदलाव %1$s की प्रोफ़ाइल में सेव होते हैं और अगली बार गेम लॉन्च होने पर लागू होते हैं। + + %1$d गेम ओवरराइड + %1$d गेम ओवरराइड + + अभी कोई Proton इंस्टॉल नहीं है। + गेम चल रहा है + लॉन्च %1$s पर फिर से लागू हुआ + ओरिजिनल + Components पेज पर इंपोर्ट और डिलीट करें। + पूरा हुआ + ध्यान देने की ज़रूरत + काम हो रहा है + वायरलेस डीबगिंग का IP एड्रेस और पोर्ट जांचें, या अगर Android ने इस डिवाइस को हटा दिया है तो फिर से पेयर करें। + इंस्टॉल करें + मैनेज करें + ओवरव्यू + कंट्रोलर + सेशन + लॉन्चर + सेटअप + ● सब तैयार + %2$d में से %1$d तैयार + सिस्टम जांच + डिवाइस समर्थित + अनटेस्टेड GPU + GPU समर्थित नहीं + %1$s · %2$s + %1$s: %2$s + Steam को Adreno ड्राइवर चाहिए; %1$s में काली स्क्रीन दिख सकती है + Linux रनटाइम + %1$s · %2$d%% + इंस्टॉल नहीं है · लगभग 3 GB, पहली बार Play पर इंस्टॉल होगा + इंस्टॉल है + %1$s · अपडेट उपलब्ध + %1$s · नवीनतम + चाइल्ड-प्रोसेस सीमा + चालू · Android Steam बंद कर सकता है। इसे बंद करने में एक मिनट लगता है। + सेट नहीं · Android Steam बंद कर सकता है। इसे बंद करने में एक मिनट लगता है। + जांचा नहीं जा सका · इसे बंद करने में एक मिनट लगता है। + ठीक करें + जानकारी + डेवलपर विकल्प + वायरलेस डीबगिंग इस्तेमाल करें + फिर से जांचें + सीमा चालू करें + Steam खाता + %1$s के रूप में साइन इन · ऑफ़लाइन मोड + %1$s के रूप में साइन इन + Play दबाएं और Steam में साइन इन करें + टूल + कंट्रोलर + कंट्रोलर सेटिंग उपलब्ध नहीं हैं। + सेशन + सेशन लॉग + हर सेशन के बाद सेव होता है + डिबगिंग टूल + नवीनतम सेशन लॉग + सेव किए सेशन लॉग + सबसे नए %1$d रखता है + लॉग हटाएं + ऑफ़लाइन मोड + पहले Steam में साइन इन करें + लॉन्चर + भाषा + अगली बार शुरू होने पर Steam भी इसे इस्तेमाल करता है। + सिस्टम डिफ़ॉल्ट (%1$s) + सिस्टम डिफ़ॉल्ट + ऐप स्केल + DroidDeck टेक्स्ट और कंट्रोल + %1$d%% (डिफ़ॉल्ट) + थीम + लॉन्चर की दिखावट + होम स्क्रीन के तौर पर इस्तेमाल करें + DroidDeck होम ऐप हो सकता है + DroidDeck को होम ऐप के तौर पर कभी नहीं दिखाया जाता + ओरिएंटेशन + ऑटोमैटिक लैंडस्केप + लैंडस्केप + उलटा लैंडस्केप + फ़ुलस्क्रीन + Android स्टेटस और नेविगेशन बार छिपाता है + Android स्टेटस और नेविगेशन बार दिखाता है + डिफ़ॉल्ट होम ऐप + होम ऐप चुनें + चुनें + Linux ऐप (बीटा) + फ़ाइलें + फ़ाइलें ब्राउज़ और मैनेज करें + Proton वर्शन + ARM64 Proton बिल्ड इंस्टॉल करें + परफ़ॉर्मेंस + CPU कोर असाइनमेंट + ROM फ़ोल्डर + एमुलेटर गेम जहां रखे जाते हैं + कॉम्पोनेंट + FEX, DXVK, VKD3D + कंट्रोल + बटन मैपिंग + खोजें + सर्च + इंस्टॉल किए + %1$s · %2$d + स्टोर + Flathub · ARM64 + ● Flatpak तैयार + पहले सेटअप से Linux रनटाइम इंस्टॉल करें। + Flatpak सेट करें + Flatpak जोड़ता है और इसे Flathub से जोड़ता है (लगभग 20 MB)। पहला ऐप अपना रनटाइम भी डाउनलोड करता है। + सेट हो रहा है… + Flatpak + अपडेट + %1$s · %2$s + %1$s · %2$s · %3$d%% + शुरू हो रहा है… + कैटेगरी + Flathub तक नहीं पहुंच सका। अपना कनेक्शन जांचें और फिर कोशिश करें। + फिर कोशिश करें + Flathub में सर्च करें + Firefox, गेम, एमुलेटर… + सर्च + सभी + सर्च हो रहा है… + नाम से सर्च करें या कोई कैटेगरी चुनें। सिर्फ़ ARM64 ऐप सूचीबद्ध हैं। + Flathub पर इससे मेल खाने वाला कोई ARM64 ऐप नहीं है। + नतीजे + अपडेट देखें + सभी अपडेट करें (%1$d) + ● सब कुछ नवीनतम है + अभी कोई ऐप नहीं है। खोजें या सर्च में देखें। + ऐप इंस्टॉल करने के लिए Flatpak सेट करें। + अपडेट उपलब्ध · %1$s + खोलें + जानकारी + हटाएं + हटाने के लिए फिर से दबाएं + इंस्टॉल हो रहा है… + इंस्टॉल करें + पहले Flatpak सेट करें + %1$s डाउनलोड + ● इंस्टॉल है + इस ऐप का Flathub पेज लोड नहीं हो सका। + Flathub से लोड हो रहा है… + Flathub पर कोई ARM64 बिल्ड नहीं है, इसलिए इसे यहां इंस्टॉल नहीं किया जा सकता। + स्क्रीनशॉट + परिचय + ऐप ID + लाइसेंस + रनटाइम + इंस्टॉल आकार + %1$s (साथ में इसका शेयर्ड रनटाइम) + वेबसाइट + वेरिफ़ाइड + अपडेट + DroidDeck %1$s · %2$s + %1$s को जांचा गया + अभी जांचा नहीं गया + DroidDeck को खुद अपडेट होने दें + अगली स्क्रीन पर, “इस सोर्स से अनुमति दें” चालू करें, फिर वापस आएं। अपडेट खुद जारी रहेगा। + सेटिंग खोलें + अभी नहीं + अलग तरीके से साइन किया गया + यहीं अपडेट नहीं हो सकता + यह कॉपी DroidDeck की रिलीज़ कुंजी से साइन नहीं है, इसलिए अपडेट इसके ऊपर इंस्टॉल नहीं हो सकते। बदलने के लिए इसे अनइंस्टॉल करें। + अपडेट उपलब्ध + इंस्टॉल नहीं हो सकता + बदलने के लिए तैयार + स्टेबल से आगे + आप स्टेबल से आगे हैं + यह बिल्ड नवीनतम स्टेबल से नया है। इसके अगले रिलीज़ के साथ आप स्टेबल पर चले जाएंगे। + टेस्ट खत्म हुआ + यह टेस्ट खत्म हो गया है + इसका फ़िक्स मर्ज हो गया या हटा दिया गया। नवीनतम फ़िक्स के लिए प्रीव्यू फ़ॉलो करें। + नवीनतम + आपके पास नवीनतम %1$s है + · %1$s + %1$s… %2$d%% + %1$s… + अपडेट%1$s + इंस्टॉल%1$s + फिर भी स्टेबल %1$s इंस्टॉल करें + प्रीव्यू फ़ॉलो करें + अपडेट करने के लिए चल रहा सेशन बंद करें। + कम + ज़्यादा + DroidDeck %1$s + नया प्रीव्यू बिल्ड + PR #%1$d टेस्ट बिल्ड + %1$d MB + अपडेट चैनल + स्टेबल + जांचे गए रिलीज़, ज़्यादातर लोगों के लिए + %1$s · %2$s + प्रीव्यू + नवीनतम फ़िक्स, स्टेबल से पहले + नवीनतम + टेस्ट बिल्ड + रिलीज़ होने से पहले किसी फ़िक्स को आज़माएं + अभी कोई नहीं + PR #%1$d + DroidDeck की इस कॉपी के लिए नहीं बना + %1$s को अपडेट किया गया + PR #%1$d टेस्ट + कुछ समय पहले + अभी-अभी + + %1$d आज़माने के लिए + %1$d आज़माने के लिए + + + आपके बिल्ड के बाद से बदलाव · %1$d बिल्ड पीछे + आपके बिल्ड के बाद से बदलाव · %1$d बिल्ड पीछे + + + नए बदलाव · %1$d बिल्ड + नए बदलाव · %1$d बिल्ड + + बदलने के लिए एक Android सेटिंग + Steam + Android उन ऐप को रोकता है जो कई बैकग्राउंड प्रोसेस शुरू करते हैं, और Steam दर्जनों शुरू करता है। जब तक यह नहीं बदलता, गेम बिना किसी एरर के बंद हो जाते हैं। + मेरे लिए ठीक करें + “Disable child process restrictions” चालू करें, फिर वापस आएं। डेवलपर विकल्प चालू रहने दें, नहीं तो यह स्विच फिर से बंद हो जाएगा। + डेवलपर विकल्प खोलें + इस Android वर्शन में इसके लिए कोई स्विच नहीं है, इसलिए DroidDeck इसे वायरलेस डीबगिंग के ज़रिए बदलता है। इसमें लगभग एक मिनट लगता है, किसी कंप्यूटर की ज़रूरत नहीं। + वायरलेस डीबगिंग के लिए Wi-Fi चाहिए। कोई भी नेटवर्क काम करेगा; इंटरनेट की ज़रूरत नहीं है। + पेयरिंग कोड एक नोटिफ़िकेशन में आता है। DroidDeck के लिए नोटिफ़िकेशन की अनुमति दें, या अन्य तरीके इस्तेमाल करें। + नोटिफ़िकेशन की अनुमति दें + Settings → डेवलपर विकल्प → वायरलेस डीबगिंग खोलें और उसे चालू करें + “Pair device with pairing code” पर टैप करें + DroidDeck नोटिफ़िकेशन में कोड टाइप करें + पेयरिंग पॉप-अप का इंतज़ार हो रहा है… + पॉप-अप मिल गया। इसका कोड नोटिफ़िकेशन में डालें। + स्थिति + हर 2 सेकंड में जांच हो रही है। यह बंद होने पर यह पेज बंद हो जाता है। + अन्य तरीके + वायरलेस डीबगिंग, मैनुअल एड्रेस, कंप्यूटर ADB + मैनुअल एड्रेस, कंप्यूटर ADB, डेवलपर विकल्प + इसके बदले वायरलेस डीबगिंग इस्तेमाल करें + कुछ ROM फिर भी यह स्विच जोड़ देते हैं। डेवलपर विकल्प में “child process” देखें। + अगर नोटिफ़िकेशन काम न करे, तो पॉप-अप का एड्रेस और कोड खुद डालें। + एड्रेस खुद डालें + ADB वाले कंप्यूटर से: + कमांड कॉपी करें + Android Settings किस डिस्प्ले पर खोलना है चुनें। + मुख्य स्क्रीन + पेयर हो गया। वायरलेस डीबगिंग का पोर्ट खोजा जा रहा है… + पेयर हो गया। Settings से पोर्ट डालें। + डिवाइस मिल गया। सेटिंग लागू की जा रही है… + खुद लागू नहीं हो सका। नीचे दिया एड्रेस जांचें और फिर कोशिश करें। %1$s + वायरलेस डीबगिंग + चाइल्ड-प्रोसेस सीमा अपडेट हुई + चाइल्ड-प्रोसेस सीमा बदलें + अगर यह विकल्प न हो, तो वायरलेस डीबगिंग से पेयर करें। + पेयर हो गया। वायरलेस डीबगिंग में IP एड्रेस और पोर्ट जांचें। + चाइल्ड-प्रोसेस सीमा अपडेट हो गई। + अगर डेवलपर विकल्प में चाइल्ड-प्रोसेस सेटिंग न हो तो स्टेप 2 इस्तेमाल करें। डेवलपर विकल्प → वायरलेस डीबगिंग → Pair device with pairing code खोलें। + पेयर हो गया। नीचे दिया एड्रेस इस्तेमाल करें, या इसे वायरलेस डीबगिंग → IP address & Port से अपडेट करें। + चाइल्ड-प्रोसेस सीमा वायरलेस डीबगिंग के ज़रिए अपडेट हो गई। + Android ने पुष्टि की कि सीमा चालू है। + Android ने पुष्टि की कि सीमा बंद है। + 2 · वायरलेस डीबगिंग पेयर करें + पेयरिंग पॉप-अप का IP एड्रेस और पोर्ट + पेयरिंग कोड (PIN) + 6 अंक + पेयर करें + 2 · वायरलेस डीबगिंग के ज़रिए लागू करें + कनेक्शन का IP एड्रेस और पोर्ट + फिर से पेयर करें + सीमा बंद करें + इसे बदलने के अन्य तरीके + 1 · डेवलपर विकल्प (पहले आज़माएं) + “Restrict child processes” चालू करें। + “Restrict child processes” बंद करें। + 3 · कंप्यूटर ADB (आख़िरी विकल्प) + सिर्फ़ तब जब वायरलेस डीबगिंग उपलब्ध न हो। + 3 · कंप्यूटर ADB (आख़िरी विकल्प) + सेशन में टच कंट्रोल कब दिखते हैं + रंग + ऑन-स्क्रीन बटन का रंग + कस्टम + ओपैसिटी + %1$d%% + बटन का आकार + 100% मानक आकार है + स्टिक क्लिक + L3 या R3 के लिए किसी स्टिक को डबल-टैप करके दबाए रखें + एडैप्टिव स्टिक + गेम वाइब्रेशन + Steam बटन + QAM बटन + कीबोर्ड बटन + Steam मेनू खोलने के लिए Select दबाएं + View/Select के बदले। + एनिमेशन + आप जहां टच करें वहां स्टिक दिखते हैं और छोड़ने पर छिप जाते हैं + लेआउट + कस्टम पोज़िशन सेव हैं + इस स्क्रीन के हिसाब से फ़िट किया गया + संपादित करें + रीसेट करें + बटन मैपिंग + हर बटन अपना इनपुट भेजता है + %2$d में से %1$d बटन रीमैप किए गए + कॉन्फ़िगर करें + कंट्रोलर रीसेट करें + सभी कंट्रोलर सेटिंग और लेआउट वापस लाता है + हर ऑन-स्क्रीन बटन गेम को क्या भेजता है। Hidden बटन हटा देता है। + ऑन-स्क्रीन बटन + डिफ़ॉल्ट + मैपिंग रीसेट करें + Android ऐप + %1$d ऐप + लॉन्च करने लायक कोई Android ऐप नहीं मिला। + ● डेस्कटॉप इंस्टॉल है + पहली बार खोलने पर इंस्टॉल होता है + इंस्टॉल के लिए उपलब्ध + इंस्टॉल नहीं है। + %1$s · इंस्टॉल नहीं है + %1$s खोलें + गेम + ROM फ़ोल्डर चुनें। + RetroArch में /root/ROMs तक ब्राउज़ करें। + %1$s गेम ROMs/%2$s में जोड़ें। + %1$s इंस्टॉल करें + इंस्टॉल करने के लिए सेशन बंद करें + रनटाइम ज़रूरी + इंस्टॉल जानकारी लोड हो रही है… + इंस्टॉल जानकारी उपलब्ध नहीं है। बाद में फिर कोशिश करें। + पहले सेटअप से Linux रनटाइम इंस्टॉल करें। + डेस्कटॉप ऐप इंस्टॉल करने के लिए चल रहा सेशन बंद करें। + %1$s को Linux डेस्कटॉप में इंस्टॉल करता है। + %1$s · %2$d%% + छिपाने से यह डेस्कटॉप से हट जाता है। इसकी फ़ाइलें Linux रनटाइम में रहती हैं। + वह गेम अब ROM फ़ोल्डर में नहीं है। + डेस्कटॉप · %1$s + %1$s में लॉन्च करें + %1$s में भी + वह पेज अब नहीं है। बाईं तरफ़ से कोई सेक्शन चुनें। + डेस्कटॉप खोलें + डेस्कटॉप इंस्टॉल करें और खोलें + डेस्कटॉप अनइंस्टॉल करें + डेस्कटॉप और इसके साथ आए ऐप हटाएं। गेम, जोड़े गए ऐप, सेटिंग और शेयर्ड लाइब्रेरी बनी रहेंगी। + डेस्कटॉप हटाया जा रहा है + डेस्कटॉप हटा दिया गया + पुरानी LXQt फ़ाइलें हटाई जा रही हैं + इस डेस्कटॉप वर्शन को हटाने से पहले DroidDeck अपडेट करें। + Linux डेस्कटॉप + KDE Plasma, Firefox और आपके एमुलेटर + Steam में इंस्टॉल किए गेम सेशन के बाद यहां दिखते हैं। + Steam चलाएं + लॉन्च सेटिंग + लॉन्च करें + गेम फ़ाइलें + Proton प्रीफ़िक्स + सेव खोजे जा रहे हैं… + नाम से कोई सेव फ़ोल्डर नहीं मिला। एक्सपोर्ट पूरा यूज़र फ़ोल्डर लेता है। + %1$d सेव फ़ोल्डर + %1$s · %2$d फ़ाइलें · %3$s + %1$d KB + सेव मैनेज करें + गेम सेव + सेव इंपोर्ट करें… + GameHub zip के रूप में एक्सपोर्ट करें… + Winlator zip के रूप में एक्सपोर्ट करें… + सेव फ़ोल्डर खोलें + कभी नहीं खेला + रनटाइम व्यस्त · %1$d%% + रनटाइम व्यस्त + पहली बार Play पर रनटाइम इंस्टॉल होता है + रनटाइम अपडेट उपलब्ध + ● रनटाइम तैयार + आख़िरी बार %1$s को खेला + इंटरनल स्टोरेज + जोड़ा गया गेम + कस्टम गेम + गेम स्टोरेज + इस GPU के लिए सबसे बेहतर जोड़ी, हमेशा नवीनतम + खुद एक जोड़ी चुनें + ड्राइवर देखे जा रहे हैं… + अभी कोई ड्राइवर सूचीबद्ध नहीं है। उन्हें देखने के लिए रीफ़्रेश करें। + दूसरे GPU के ड्राइवर छिपाएं + दूसरे GPU के ड्राइवर दिखाएं + एडवांस्ड + डिस्प्ले ड्राइवर के साथ सेट: %1$s दोनों साथ लाता है। + रनटाइम ड्राइवर के साथ सेट: %1$s दोनों साथ लाता है। + गेम और डेस्कटॉप में उपयोग होता है। + रनटाइम डिफ़ॉल्ट + स्क्रीन पर फ़्रेम बनाता है। DroidDeck रीस्टार्ट होने के बाद लागू होता है। + ऑटो - GPU के हिसाब से चुना गया + डाउनलोड, इंपोर्ट या डिलीट करें… + उपयोग में + DroidDeck में बिल्ट-इन ड्राइवर + रनटाइम और डिस्प्ले ड्राइवर एक साथ मिलती-जुलती जोड़ी के रूप में सेट हैं। + इस GPU के लिए सुझाया गया: %1$s। + इस GPU के लिए ड्राइवर देखने के लिए रीफ़्रेश करें। + सुझाया गया + उपलब्ध नहीं + Linux रनटाइम + Linux रनटाइम तैयार किया जा रहा है + सेशन + डिस्प्ले और ऑडियो शुरू किया जा रहा है + Steam क्लाइंट + Steam और उसके कम्पैटिबिलिटी टूल जांचे जा रहे हैं + Steam खोला जा रहा है + Steam के पहले फ़्रेम का इंतज़ार हो रहा है + डेस्कटॉप + डेस्कटॉप तैयार किया जा रहा है + खोला जा रहा है + पहले फ़्रेम का इंतज़ार हो रहा है + Steam शुरू हो रहा है + सेशन खत्म हो गया + स्टेप %2$d में से %1$d + Steam रुका हुआ है + ग्रिप + बायां पैड + दायां पैड + दोनों + गेमपैड + स्टिक + ट्रैकपैड + स्टिक और ट्रैकपैड के बीच बदलें + और Deck कंट्रोल + पैनल + दूसरी स्क्रीन के कंट्रोल बंद करें + Steam मेनू खोलें + बायां · ऊपर + बायां · नीचे + दायां · ऊपर + दायां · नीचे + क्लिक + दायां ट्रैकपैड + बायां ट्रैकपैड + ट्रैकपैड + कीबोर्ड दिखाएं + कीबोर्ड छिपाएं + Linux टर्मिनल + Linux सेशन अभी टर्मिनल के लिए तैयार नहीं है। + Steam + गेम + डेस्कटॉप + स्टोर + कॉम्पोनेंट + ऐप + सेटअप + अपडेट + %1$d%% + %1$s फिर से शुरू करें + हर सेशन में /root/ROMs के रूप में दिखता है। इंटरनल स्टोरेज /root/Storage पर है। + कोई फ़ोल्डर नहीं चुना गया + अगले सेशन से लागू होता है। SD कार्ड काम करते हैं। + फ़ोल्डर चुनें + फ़ोल्डर बदलें + एक ARM64 Proton बिल्ड इंस्टॉल करें, फिर इसे Steam → Properties → Compatibility में हर गेम के लिए चुनें। + कम्पैटिबिलिटी टूल इंस्टॉल या हटाने के लिए चल रहा सेशन बंद करें। + उपलब्ध बिल्ड + इंस्टॉल है %1$s + पहले की रिक्वेस्ट से पेंडिंग + इंस्टॉल नहीं है + अपडेट उपलब्ध → %1$s + पुराने %1$s वर्शन हटाएं + %1$s हटाता है। इन वर्शन पर सेट गेम को किसी और Proton पर बदलना होगा। + इस वर्शन पर सेट गेम को किसी और Proton पर बदलना होगा। + अभी इंस्टॉल करें + पेंडिंग रद्द करें + Android कीबोर्ड + छिपाएं ✕ + %1$s लॉन्च करें + स्क्रीन चुनें। सेकेंडरी: %1$s। + सेकेंडरी डिस्प्ले अब उपलब्ध नहीं है। + प्राइमरी स्क्रीन + सेकेंडरी स्क्रीन + पिछला टैब + अगला टैब + चालू + सभी %1$d + कोई नहीं + %2$d में से %1$d + बंद + नीचे की स्क्रीन + अगली बार गेम लॉन्च होने पर लागू होता है। गेम की प्रोफ़ाइल शेयर्ड प्रोफ़ाइल से ऊपर रहती है। + ऐप ID से कोई और गेम… + हो गया + डिफ़ॉल्ट + बंद + बदलें… + वेरिएबल बदलें + डिफ़ॉल्ट पर वापस जाएं + Proton गेम + पिक्चर इन पिक्चर + अपने आप PiP में जाएं + खुली हुई स्क्रीन पर कंट्रोल + खुली होने पर, गेम Steam Deck के ट्रैकपैड और पीछे की ग्रिप के ऊपर दिखता है। बंद होने पर, गेम फ़ुल स्क्रीन में रहता है। + सस्पेंडेड + सेशन तैयार होने पर पिक्चर इन पिक्चर उपलब्ध होता है। + Android सेटिंग में DroidDeck के लिए पिक्चर इन पिक्चर की अनुमति दें। + Android पिक्चर इन पिक्चर नहीं खोल सका। + शॉर्टकट + होम स्क्रीन पर जोड़ें + गेम चुनें + इंस्टॉल किए गेम स्कैन हो रहे हैं… + लॉन्च लिंक कॉपी करें + लॉन्च लिंक कॉपी हो गया + यह लॉन्चर शॉर्टकट नहीं जोड़ सकता। इसके बदले लॉन्च लिंक कॉपी करें इस्तेमाल करें। + गेम लॉन्च लिंक मान्य नहीं है। + यह गेम अब आपकी DroidDeck लाइब्रेरी में नहीं है। + कोई Steam गेम लॉन्च करने से पहले चल रहा सेशन बंद करें। + गेम Steam को नहीं भेजा जा सका। इसे फिर से लॉन्च करने की कोशिश करें। + हटाना फिर से कोशिश करें + हटाया जा रहा है + हटाना पूरा नहीं हुआ + गेम फ़ाइल एक्सपोर्ट करें + सभी गेम फ़ाइलें फ़ोल्डर में सिंक करें + गेम फ़ाइलें सिंक करना बंद करें + फ़्रंटएंड गेम फ़ाइलों के लिए फ़ोल्डर चुनें + गेम फ़ाइलें %1$s में सेव हुईं + फ़्रंटएंड फ़ाइलें + + रनटाइम ऑपरेशन खत्म होने का इंतज़ार करें + कम्पैटिबिलिटी टूल इंस्टॉल खत्म होने का इंतज़ार करें + डेस्कटॉप ऐप इंस्टॉल खत्म होने का इंतज़ार करें + गेम लॉन्च करने से पहले इंस्टॉल खत्म होने का इंतज़ार करें + पहले Steam सेशन बंद करें, ताकि गेम इंपोर्ट के ऊपर सेव न कर सके + %1$s में इंपोर्ट हो रहा है + + %2$s से %1$d फ़ाइल %3$s में इंपोर्ट हुई + %2$s से %1$d फ़ाइलें %3$s में इंपोर्ट हुईं + + + %2$s से %1$d फ़ाइल %3$s में इंपोर्ट हुई। पुराने सेव Download/DroidDeck/Saves/backups में बैकअप हो गए + %2$s से %1$d फ़ाइलें %3$s में इंपोर्ट हुईं। पुराने सेव Download/DroidDeck/Saves/backups में बैकअप हो गए + + %1$s के लिए एक सेव zip चुनें + %1$s एक्सपोर्ट हो रहा है + + %1$d फ़ाइल %2$s के रूप में एक्सपोर्ट हुई: %3$s + %1$d फ़ाइलें %2$s के रूप में एक्सपोर्ट हुईं: %3$s + + %1$s (%2$s) को सेव करने के लिए जगह चुनें + %1$s विफल: %2$s + अभी कोई सेशन लॉग नहीं है: पहले एक सेशन चलाएं। + हटाने के लिए कोई सेशन लॉग नहीं है। + + %1$d सेशन लॉग फ़ोल्डर हटाया गया। + %1$d सेशन लॉग फ़ोल्डर हटाए गए। + + वायरलेस डीबगिंग पेयरिंग विफल रही + वायरलेस डीबगिंग कमांड विफल रहा + वायरलेस डीबगिंग उपलब्ध नहीं है + ADB कमांड कॉपी हो गया + ROM फ़ोल्डर चुनें + Adreno GPU नहीं है + Turnip Adreno GPU को सपोर्ट करता है। %1$s पर, Steam में काली स्क्रीन दिख सकती है। डाउनलोड: %2$.0f MB। + फिर भी इंस्टॉल करें + Linux रनटाइम हटाएं + यह रनटाइम, इसके भीतर का Steam क्लाइंट, और वहां इंस्टॉल हर गेम हटा देता है। + लागू हुआ: %1$s + %1$s प्राइमरी स्क्रीन पर नहीं खुल सका + %1$s %2$s पर नहीं खुल सका + %1$s डिस्प्ले %2$d पर नहीं खुल सका + इंपोर्ट हो रहा है + बदला जा रहा है + वापस लाया जा रहा है + रद्द हो रहा है + हटाया जा रहा है + फ़ाइल पढ़ी नहीं जा सकती + %1$s इंपोर्ट हुआ + पेंडिंग स्वैप रद्द कर दिया गया। + कॉम्पोनेंट पैकेज चुनें (-linux .wcp) + Nightlies तक नहीं पहुंचा जा सका + %1$s सेव हुआ + डाउनलोड विफल: %1$s + Linux रनटाइम ड्राइवर चुनें (-Linux zip या Android + Linux बंडल) + डिस्प्ले ड्राइवर चुनें (AdrenoTools zip या Android + Linux बंडल) + ड्राइवर zip या Android + Linux बंडल चुनें + %1$s: %2$s + %1$s हटाया जा रहा है… + %1$s भुलाया जा रहा है… + गेम स्टोरेज फ़ोल्डर चुनें + Decky प्लगइन ZIP चुनें + इस्तेमाल लायक नहीं: %1$s + प्रोग्राम + Linux रनटाइम हटाया जा रहा है + नीचे की स्क्रीन पर Settings नहीं खुल सकी; इसे मुख्य स्क्रीन पर खोला गया। + Settings में डेवलपर विकल्प खोलें + + Android की “Install unknown apps” सेटिंग नहीं खुल सकी + जांच नहीं हो सकी: %1$s + इस बिल्ड में DroidDeck की इस कॉपी के लिए कोई डाउनलोड नहीं है + पहले चल रहा सेशन बंद करें, फिर अपडेट करें + डाउनलोड हो रहा है + इंस्टॉल हो रहा है + आपकी पुष्टि का इंतज़ार हो रहा है + अपडेट विफल रहा + Android ने अपडेट इंस्टॉल करने का विकल्प नहीं दिया + + Decky इंस्टॉल विफल रहा: %1$s + प्लगइन इंपोर्ट शुरू हो रहा है… + प्लगइन इंपोर्ट विफल रहा: %1$s + प्लगइन इंस्टॉल हो गया। इसे लोड करने के लिए Steam रीस्टार्ट करें। + + अभी जांचा नहीं गया - नए ड्राइवर देखने के लिए रीफ़्रेश पर टैप करें + Android + Linux: इसके साथ डिस्प्ले ड्राइवर सेट करता है + Android + Linux: इसके साथ रनटाइम ड्राइवर सेट करता है + रनटाइम में बिल्ट-इन Turnip + सिस्टम Vulkan: इस GPU के लिए कोई बंडल बिल्ड नहीं + %1$s (बंडल) + मैनुअल + %1$s · %2$s + अभी सिर्फ़ एक आधा हिस्सा पब्लिश है + दोनों के लिए %1$.0f MB + सेट करने के लिए कोई ड्राइवर नहीं: %1$s + नवीनतम ड्राइवर देखे जा रहे हैं… + नवीनतम (जांच नहीं हो सकी: %1$s) + सुझाए गए ड्राइवर अभी पब्लिश नहीं हैं + %1$s %2$s डाउनलोड हो रहा है… + %1$s %2$s में अपडेट हो गया। + %1$s %2$s में अपडेट हो गया। DroidDeck रीस्टार्ट होने के बाद डिस्प्ले ड्राइवर लागू होता है। + %1$s %2$s में बदल दिया गया। + %1$s %2$s में बदल दिया गया। DroidDeck रीस्टार्ट होने के बाद डिस्प्ले ड्राइवर लागू होता है। + %1$s %2$s इस्तेमाल हो रहा है। + %1$s %2$s इस्तेमाल हो रहा है। DroidDeck रीस्टार्ट होने के बाद डिस्प्ले ड्राइवर लागू होता है। + यह ड्राइवर zip नहीं है: न AdrenoTools ड्राइवर, न -Linux Turnip, न Android + Linux बंडल + इंपोर्ट विफल: %1$s + इंपोर्ट किया गया ड्राइवर + %1$s को रनटाइम और डिस्प्ले दोनों ड्राइवर के रूप में इस्तेमाल किया जा रहा है। + %1$s को रनटाइम और डिस्प्ले दोनों ड्राइवर के रूप में इस्तेमाल किया जा रहा है। DroidDeck रीस्टार्ट होने के बाद डिस्प्ले ड्राइवर लागू होता है। + %1$s हटाया गया (दोनों ड्राइवर) + %1$s हटाया गया + नवीनतम: %1$s · %2$s को जांचा गया + नवीनतम: %1$s · %2$s तक नहीं पहुंचा जा सका · %3$s को जांचा गया + + %1$d मिनट पहले + %1$d मिनट पहले + + + %1$d घंटे पहले + %1$d घंटे पहले + + + %1$d दिन पहले + %1$d दिन पहले + + Banners-Turnip, WinNative और DroidDeck जांचे जा रहे हैं… + जांच विफल रही + इंस्टॉल है %1$s - दोनों ड्राइवर सेट करने के लिए इसे किसी भी लिस्ट में चुनें + इंस्टॉल है %1$s - इसे मेनू में चुनें + + रनटाइम हटाना खत्म होने का इंतज़ार करें + गेम + टचपैड + डायरेक्ट + स्वैप विफल: %1$s + प्रोफ़ाइल सेव विफल: %1$s + डेस्कटॉप रुका हुआ है + %1$s रुका हुआ है + प्रोग्राम रुका हुआ है + डेस्कटॉप शुरू हो रहा है + %1$s शुरू हो रहा है + प्रोग्राम शुरू हो रहा है + इस सेशन के लिए कोई लॉग नहीं है। + सेशन लॉग आर्काइव नहीं बन सका। + सेशन लॉग शेयर नहीं हो सके। + सेशन शुरू हो रहा है… + Linux रनटाइम डाउनलोड हो रहा है + Linux रनटाइम डाउनलोड हो रहा है · %2$d में से %1$d MB + Linux रनटाइम जांचा जा रहा है + Linux रनटाइम अनपैक हो रहा है + डेस्कटॉप डाउनलोड हो रहा है + डेस्कटॉप डाउनलोड हो रहा है · %2$d में से %1$d MB + डेस्कटॉप जांचा जा रहा है + डेस्कटॉप अनपैक हो रहा है + Proton Experimental (ARM64) डाउनलोड हो रहा है + Proton Experimental (ARM64) डाउनलोड हो रहा है · %2$d में से %1$d MB + Proton Experimental (ARM64) जांचा जा रहा है + Proton Experimental (ARM64) अनपैक हो रहा है + रनटाइम कैटलॉग तक नहीं पहुंचा जा सका। कनेक्शन जांचें और फिर से Play दबाएं। + Linux रनटाइम इंस्टॉल नहीं हुआ। सेटअप में रनटाइम जांचें और फिर से Play दबाएं। + डेस्कटॉप इंस्टॉल नहीं हुआ (%1$s)। कनेक्शन जांचें और फिर से Desktop दबाएं। + %1$s अचानक बंद हो गया। बग रिपोर्ट के साथ लॉग शेयर करें, या फिर कोशिश करें। + प्रोग्राम अचानक बंद हो गया। बग रिपोर्ट के साथ लॉग शेयर करें, या फिर कोशिश करें। + डेस्कटॉप अचानक बंद हो गया। बग रिपोर्ट के साथ लॉग शेयर करें, या फिर कोशिश करें। + एग्ज़िट स्टेटस %1$d · %2$s + कोई लॉग नहीं + + लॉग में %1$d “Function not implemented” एरर दिखता है: इस डिवाइस पर proot का seccomp एक्सेलरेशन एक हेल्पर को फेल कर रहा है। %2$s → “%3$s” (या “%4$s”) आज़माएं और फिर से शुरू करें। + लॉग में %1$d “Function not implemented” एरर दिखते हैं: इस डिवाइस पर proot का seccomp एक्सेलरेशन एक हेल्पर को फेल कर रहा है। %2$s → “%3$s” (या “%4$s”) आज़माएं और फिर से शुरू करें। + + + प्लगइन + अगले Steam सेशन के लिए एक प्लगइन ZIP इंस्टॉल करें। + डाउनलोड हो रहा है… + ZIP से इंस्टॉल करें + गेम + ऑडियो और वीडियो + ग्राफ़िक्स + इंटरनेट + ऑफ़िस + डेवलपर + शिक्षा + विज्ञान + सिस्टम + यूटिलिटी + पॉपुलर + ट्रेंडिंग + नया + अपडेट किए गए + लोड हो रहा है… + Flatpak सेटअप + %1$s: %2$s + Flathub पर अपडेट नहीं जांचे जा सके + इंस्टॉल है + फ़ोल्डर + ग्रेफ़ाइट + ग्रेफ़ाइट सतहें, और चुने, फ़ोकस या दबाए गए हर चीज़ के लिए लोगो के अपने ऑर्ब ब्लू रंग का इस्तेमाल। + काले पर पेपर + सफ़ेद मुख्य रंग है, आइकन का ब्लू रंग लाइन, रिंग और डॉट के लिए संकेत है। + आइकन ब्लू + काली ज़मीन, सफ़ेद टाइप, चुने, फ़ोकस या दबाए गए हर चीज़ के लिए ब्लू रंग। + इलेक्ट्रिक नेवी + ब्लू रंग ज़्यादा चमकीला, उन सतहों पर जो ग्रे के बजाय नेवी की ओर झुकती हैं। + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + पहले रनटाइम इंस्टॉल करें + Steam लाइब्रेरी उपलब्ध नहीं है + पिछली SD लाइब्रेरी उपलब्ध नहीं है + गेम स्टोरेज उपलब्ध नहीं है + जोड़े गए गेम का फ़ोल्डर उपलब्ध नहीं है + Steam मेनिफ़ेस्ट उपलब्ध नहीं हैं + कई सिस्टम + इंटरनल + Linux सिस्टम + SD कार्ड + स्टोरेज + चुनें + प्रॉपर्टी + Favorites में जोड़ें + Favorites से हटाएं + नाम बदलें + कॉपी करें + कट करें + एक्शन + Favorites + मौजूदा फ़ोल्डर पिन करें + अभी कोई favorite नहीं है - किसी फ़ोल्डर को उसके ⋮ मेनू से पिन करें ताकि यहां जल्दी वापस आ सकें। + favorites से हटाएं + 0 बाइट + बाइट + जगह + टाइप + आकार + बदला गया + फ़ोल्डर + फ़ाइल + %1$s फ़ाइल + रीड-ओनली + गेम (और Wine) को इस फ़ाइल को ओवरराइट या डिलीट करने से रोकता है। + रीड-ओनली नहीं बदला जा सका + Hidden + यह स्टोरेज hidden फ़्लैग सेव नहीं कर सकता। + फ़ाइल को Windows में hidden के तौर पर चिह्नित करता है (Wine का hidden एट्रिब्यूट)। + इस स्टोरेज पर Hidden नहीं बदला जा सका + हो गया + हटाया जा रहा है... + हटाना विफल रहा + ले जाया जा रहा है... + कॉपी हो रहा है... + ले जाया जा रहा है - %1$s + कॉपी हो रहा है - %1$s + ले जाया जा रहा है %1$d/%2$d - %3$s + कॉपी हो रहा है %1$d/%2$d - %3$s + %1$d पूरा, %2$d विफल + %1$d पूरा, %2$d छोड़ा गया + + %1$d आइटम ले जाया गया + %1$d आइटम ले जाए गए + + + %1$d आइटम कॉपी हुआ + %1$d आइटम कॉपी हुए + + “%1$s” पहले से मौजूद है + नाम बदला जा रहा है... + नाम बदलना विफल रहा + फ़ोल्डर बनाया जा रहा है... + फ़ोल्डर नहीं बन सका + नया फ़ोल्डर + फ़ोल्डर का नाम + बनाएं + नया नाम + हटाएं? + “%1$s” को स्थायी रूप से हटाएं? + + %1$d आइटम हटाएं? + %1$d आइटम हटाएं? + + यह पूर्ववत नहीं हो सकता। + + …और %1$d और + …और %1$d और + + हटाया जा रहा है %1$d/%2$d - %3$s + + %1$d नहीं हटाया जा सका + %1$d नहीं हटाए जा सके + + मर्ज मौजूदा फ़ोल्डर के भीतर फ़ाइलें जोड़ता और बदलता है। + ओवरराइट मौजूदा फ़ाइल को बदल देता है। + सभी कॉन्फ़्लिक्ट पर लागू करें + मर्ज करें + ओवरराइट करें + दोनों रखें + छोड़ें + यह फ़ोल्डर चुनें + बैक + %1$s माउंट है लेकिन अभी पढ़ा नहीं जा सकता + लिस्ट व्यू + ग्रिड व्यू + सर्च + सॉर्ट + नाम + बदलने की तारीख + आरामदायक पंक्तियां + कॉम्पैक्ट पंक्तियां + छिपी फ़ाइलें छिपाएं + छिपी फ़ाइलें दिखाएं + favorites छिपाएं + favorites दिखाएं + इस फ़ोल्डर को फ़िल्टर करें + %1$s खाली + %1$d चुने गए + कोई नहीं + सभी + पेस्ट करें + + %1$d आइटम + %1$d आइटम + + %1$s यहां पेस्ट करें + %1$s (मूव) यहां पेस्ट करें + फ़ाइलें + स्टोरेज + क्विक + FAVORITES + डाउनलोड + डाउनलोड + ROM + सेशन लॉग + पिक्चर + “%1$s” को Favorites में जोड़ा गया + “%1$s” को Favorites से हटाया गया + खाली डायरेक्टरी + एक लेवल ऊपर + फ़ोल्डर चुनें + फ़ाइल चुनें + Steam ब्लू + स्काई + सफ़ेद + वायलेट + हरा + एम्बर + लाल + गुलाबी + A बटन + B बटन + X बटन + Y बटन + लेफ़्ट बंपर + राइट बंपर + लेफ़्ट ट्रिगर + राइट ट्रिगर + View बटन + Menu बटन + View + Menu + D-pad ऊपर + D-pad नीचे + D-pad बायां + D-pad दायां + Hidden + ले जाने के लिए ड्रैग करें + स्नैप + सेव करें + कोई नहीं + कीबोर्ड + ट्रैकपैड + टर्मिनल + Deck ग्रिप + ट्रैकपैड + + Linux रनटाइम हटाया जा रहा है + + Linux रनटाइम हटाया जा रहा है · %1$d एंट्री + Linux रनटाइम हटाया जा रहा है · %1$d एंट्री + + Linux रनटाइम हटाया गया + Linux रनटाइम हटाना पूरा नहीं हो सका। सेटअप में फिर से हटाने की कोशिश करें। + रनटाइम का बाकी हिस्सा हटाया जा रहा है + डाउनलोड हो रहा है + जांचा जा रहा है + एक्सट्रैक्ट हो रहा है + Linux रनटाइम इंस्टॉल या हटाए जाते समय दिखता है + Linux रनटाइम इंस्टॉल हो रहा है + Linux रनटाइम हटाया जा रहा है + + Linux रनटाइम इंस्टॉल नहीं है + कैटलॉग में %1$s के लिए कोई चेकसम नहीं है + चेकसम मेल नहीं खाया - कुछ नहीं बदला गया + %1$s इंस्टॉल हो रहा है + AppImage को नहीं रखा जा सका + एक्सट्रैक्शन विफल रहा + इंस्टॉल विफल रहा + + Flatpak सेटअप विफल रहा (exit %1$d) + Flatpak सेटअप पूरा नहीं हुआ + कोई और स्टोर टास्क चल रहा है + पहले Flatpak सेट करें + शुरू हो रहा है + Flatpak status %1$d के साथ बंद हुआ + %1$s इंस्टॉल हो रहा है + %1$s अपडेट हो रहा है + %1$s हटाया जा रहा है + %1$s इंस्टॉल हो रहा है (%2$d/%3$d) + %1$s अपडेट हो रहा है (%2$d/%3$d) + %1$s हटाया जा रहा है (%2$d/%3$d) + अनुवाद + ग्राफ़िक्स ड्राइवर + मीडिया कोडेक + %1$s रनटाइम + %1$s रनटाइम %2$s + + यह फ़ाइल AppImage होने के लिए बहुत छोटी है + फ़ाइल पढ़ी नहीं जा सकती: %1$s + यह AppImage नहीं है (यह Linux प्रोग्राम नहीं है) + यह AppImage न ARM64 (aarch64) के लिए बना है, न x86 पीसी के लिए + यह Linux प्रोग्राम है लेकिन AppImage नहीं है + सिर्फ़ टाइप 2 AppImage इंपोर्ट किए जा सकते हैं (यह टाइप %1$d है) + पर्याप्त खाली जगह नहीं है: इंपोर्ट के लिए लगभग %1$s चाहिए + पर्याप्त खाली जगह नहीं है: अपडेट के लिए लगभग %1$s चाहिए + इंपोर्ट विफल रहा + वह AppImage अब नहीं है + अपडेट विफल रहा + %1$s एक्सट्रैक्ट हो रहा है + AppImage एक्सट्रैक्ट नहीं हो सका + AppImage एक्सट्रैक्ट नहीं हो सका: %1$s + + Decky Loader इंस्टॉल करने से पहले चल रहा सेशन बंद करें + Decky Loader डाउनलोड विफल रहा + Decky Loader का आकार रिलीज़ मेटाडेटा से मेल नहीं खाया + रिलीज़ चेकसम डाउनलोड नहीं हो सका + रिलीज़ चेकसम मौजूद नहीं है + रिलीज़ चेकसम मान्य नहीं है + Decky Loader चेकसम मेल नहीं खाया + रिलीज़ asset एक कम्पैटिबल 64-बिट Linux एग्ज़िक्यूटेबल नहीं है + डाउनलोड के दौरान एक सेशन शुरू हुआ; Decky Loader इंस्टॉल करने से पहले इसे बंद करें + PluginLoader को एग्ज़िक्यूटेबल के तौर पर चिह्नित नहीं किया जा सका + Steam CEF रिमोट डिबगिंग चालू नहीं हो सकी + PluginLoader स्टेज नहीं किया जा सका + मौजूदा PluginLoader को सुरक्षित नहीं रखा जा सका + नया PluginLoader चालू नहीं हो सका + प्लगइन इंस्टॉल करने से पहले चल रहा सेशन बंद करें + प्लगइन इंपोर्ट करने से पहले Decky Loader इंस्टॉल करें + चुनी गई ZIP फ़ाइल नहीं पढ़ी जा सकी + Decky प्लगइन डायरेक्टरी नहीं बन सकी + प्लगइन ZIP एक्सट्रैक्ट हो रहा है + प्लगइन ZIP में एक टॉप-लेवल प्लगइन फ़ोल्डर होना चाहिए + प्लगइन ZIP में plugin.json, package.json, या dist/index.js नहीं है + प्लगइन का नाम मौजूद नहीं है + इंस्टॉलेशन के दौरान एक सेशन शुरू हुआ; प्लगइन इंस्टॉल करने से पहले इसे बंद करें + प्लगइन इंस्टॉल हो गया · इसे लोड करने के लिए Steam रीस्टार्ट करें + प्लगइन ZIP इंस्टॉलेशन विफल रहा + प्लगइन ZIP में बहुत ज़्यादा एंट्री हैं + प्लगइन ZIP में एक अमान्य पाथ है + प्लगइन ZIP में डुप्लीकेट पाथ हैं + प्लगइन ZIP में एक असुरक्षित पाथ है + प्लगइन फ़ोल्डर नहीं बन सका + प्लगइन ZIP 1 GiB की सीमा से आगे बढ़ जाता है + बंडल किए प्लगइन बाइनरी को एग्ज़िक्यूटेबल के तौर पर चिह्नित नहीं किया जा सका + अमान्य remote_binary एंट्री + अमान्य रिमोट बाइनरी नाम + रिमोट बाइनरी URL को HTTPS इस्तेमाल करना चाहिए + रिमोट बाइनरी %1$s का SHA-256 हैश मान्य नहीं है + प्लगइन bin डायरेक्टरी नहीं बन सकी + प्लगइन बाइनरी डाउनलोड हो रही है · %1$s + प्लगइन बाइनरी %1$s डाउनलोड नहीं हो सकी + प्लगइन बाइनरी चेकसम मेल नहीं खाया · %1$s + प्लगइन बाइनरी को एग्ज़िक्यूटेबल के तौर पर चिह्नित नहीं किया जा सका · %1$s + प्लगइन बाइनरी बदली नहीं जा सकी · %1$s + प्लगइन बाइनरी इंस्टॉल नहीं हो सकी · %1$s + प्लगइन फ़ोल्डर किसी और प्लगइन का है + मौजूदा प्लगइन सुरक्षित नहीं रखा जा सका + इंपोर्ट किए प्लगइन को चालू नहीं किया जा सका + Decky प्लगइन सेटिंग अपडेट नहीं हो सकी + मौजूदा प्लगइन वापस नहीं लाया जा सका + + वायरलेस डीबगिंग पहचान सेव नहीं हो सकी + ADB कमांड विफल रहा (%1$d) + Android ने पुष्टि नहीं की कि चाइल्ड-प्रोसेस सीमा बदली गई: %1$s + कोई वायरलेस डीबगिंग पेयरिंग सेव नहीं है + मौजूदा वायरलेस डीबगिंग का IP एड्रेस और पोर्ट डालें + + पेयरिंग पॉप-अप का इंतज़ार करते-करते समय खत्म हो गया। DroidDeck से फिर कोशिश करें। + पेयरिंग पॉप-अप नहीं खोजा जा सका (%1$d)। जांचें कि Wi-Fi चालू है। + पेयरिंग पॉप-अप नहीं खोजा जा सका: %1$s + कोड के सभी 6 अंक डालें + पेयरिंग हो रही है… + यह कोड काम नहीं आया। पेयरिंग पॉप-अप फिर से खोलें और इसका नया कोड डालें। + पेयर हो गया। कनेक्ट हो रहा है… + पेयर हो गया, लेकिन वायरलेस डीबगिंग का पोर्ट नहीं मिला। वायरलेस डीबगिंग चालू रखें और फिर कोशिश करें। + सेटिंग लागू की जा रही है… + वायरलेस डीबगिंग कमांड विफल रहा + वायरलेस डीबगिंग सेटअप + Settings खुली रहते हुए वायरलेस डीबगिंग का पेयरिंग कोड मांगता है + वायरलेस डीबगिंग पेयर करें + वायरलेस डीबगिंग चालू करें, फिर “Pair device with pairing code” पर टैप करें। + वायरलेस डीबगिंग चालू करें, फिर “Pair device with pairing code” पर टैप करें। Settings में बने रहें: कोड इस नोटिफ़िकेशन में आता है। + 6 अंकों का पेयरिंग कोड + Enter code पर टैप करें और Settings में दिखाए गए 6 अंक टाइप करें। + Wi-Fi पेयरिंग कोड डालें + कोड फिर से आज़माएं + कोड डालें + Steam सेट किया जा रहा है + Steam तैयार है + DroidDeck पर वापस जाने के लिए टैप करें। अब आप वायरलेस डीबगिंग बंद कर सकते हैं। + वायरलेस डीबगिंग सेटअप रुक गया + + डाउनलोड ने HTTP %1$d जवाब दिया + डाउनलोड का आकार पब्लिश किए APK से मेल नहीं खाया और इसे हटा दिया गया - फिर कोशिश करें + डाउनलोड अपने चेकसम से मेल नहीं खाया और इसे हटा दिया गया - फिर कोशिश करें + यह अपडेट Android पैकेज %1$s के लिए है, %2$s के लिए नहीं + अपडेट मेटाडेटा DroidDeck की रिलीज़ साइनिंग कुंजी का नाम नहीं लेता + Android डाउनलोड किया APK नहीं पढ़ सका + डाउनलोड किया APK %1$s के लिए है, %2$s के लिए नहीं + डाउनलोड किए APK में versionCode %1$d है, पब्लिश किए %2$d नहीं + Android इंस्टॉल किए %2$d के ऊपर versionCode %1$d इंस्टॉल नहीं करेगा + डाउनलोड किए APK में कोई साइनिंग जानकारी नहीं है + डाउनलोड किया APK DroidDeck की रिलीज़ कुंजी से साइन नहीं है + अपडेट कैटलॉग ने HTTP %1$d जवाब दिया + + सेशन शुरू हो रहा है… + %1$d:%2$02d बीत गया · अभी भी काम हो रहा है + Steam क्लाइंट अपडेट डाउनलोड हो रहा है · %1$d%% + Steam क्लाइंट डाउनलोड हो रहा है · %1$s + + एक कम्पैटिबिलिटी टूल इंस्टॉल पहले से चल रहा है + कम्पैटिबिलिटी टूल इंस्टॉल करने से पहले चल रहा सेशन बंद करें + %1$s के लिए कोई ARM64 रिलीज़ नहीं मिली + %1$s इंस्टॉल करने के लिए कम से कम 4 GB खाली जगह और डाउनलोड आकार चाहिए + %1$s %2$s डाउनलोड हो रहा है + डाउनलोड फिर से कोशिश हो रहा है · %2$d में से %1$d प्रयास + डाउनलोड बाधित हुआ + डाउनलोड विफल रहा; आंशिक फ़ाइल रखी गई है ताकि फिर कोशिश करने पर यह जारी रह सके + डाउनलोड रिलीज़ के आकार से बड़ा हो गया और हटा दिया गया + डाउनलोड अधूरा रहा; आंशिक फ़ाइल को फिर से जारी रखने लायक कोशिश के लिए रखा गया है + डाउनलोड जांचा जा रहा है + रिलीज़ चेकसम नहीं पढ़ा जा सका; फिर कोशिश करें + %1$s %2$s कोई चेकसम पब्लिश नहीं करता; कुछ भी इंस्टॉल नहीं हुआ + चेकसम मेल नहीं खाया; डाउनलोड हटा दिया गया + डाउनलोड के दौरान एक सेशन शुरू हुआ; कम्पैटिबिलिटी टूल इंस्टॉल करने से पहले इसे बंद करें + %1$s इंस्टॉल हो रहा है + %1$s इंस्टॉलेशन विफल रहा (exit %2$d) + %1$s इंस्टॉल नहीं हो सका + droiddeck-esync पैक लिया जा रहा है + + Linux हेल्पर के साथ + सिर्फ़ DLL + ओरिजिनल (पिछले स्वैप के बाद Proton अपडेट हुआ) + Components के बाहर बदला गया + मौजूद नहीं + ओरिजिनल %1$s + %1$s से ओरिजिनल + जब इस पर चल रहा गेम बंद होगा, तो %1$s %2$s %3$s में चला जाएगा। + %1$s %2$s अब %3$s में है। यह अगली बार गेम शुरू होने पर लागू होता है। + वह ओरिजिनल बंडल सेव नहीं है + जब इस पर चल रहा गेम बंद होगा, तो %2$s का %1$s अपने ओरिजिनल पर वापस चला जाएगा। + %2$s का %1$s अपने %3$s ओरिजिनल से वापस लाया गया। + %1$s %2$s में उपयोग में है। पहले उस Proton को किसी और चीज़ में बदलें। + %1$s सेव नहीं है + %1$s हटाया गया। + इंस्टॉल किए बिल्ड का ओरिजिनल रखा गया है: यह वापस जाने का रास्ता है। + सेव नहीं है + %2$s का %1$s ओरिजिनल हटाया गया। + डाउनलोड सिर्फ़ Nightlies रिलीज़ से आते हैं + यह पैकेज लिस्ट चेकसम से पुरानी है - इसे रीफ़्रेश करें और फिर कोशिश करें + चेकसम मेल नहीं खाया + + लीनियर + निअरेस्ट + Snapdragon GSR (क्वालिटी) + सिर्फ़ शार्पन + ऑटो (स्केलिंग से मेल) + + कुछ लागू नहीं। + गेम क्लैरिटी + थोड़े कंट्रास्ट के साथ शार्पनिंग - रोज़ के लिए वाला। + विविड + रंग को ज़ोर से बढ़ाया गया। आसमान में पट्टियां न आएं इसलिए डीबैंड चालू। + सिनेमैटिक + गहरा कंट्रास्ट, ठंडा रंग, स्मूथ किए गए एज। + कॉम्पिटिटिव + शैडो उठाए गए और भारी शार्पनिंग - पहले लोगों को पहचानें। + एडैप्टिव शार्पन + सिर्फ़ शार्पनिंग, रंग जैसा है वैसा। + फ़िल्मिक + फ़िल्म जैसा कर्व, थोड़ा हल्का रंग। + आर्केड + तेज़ और दमदार। + रेट्रो CRT + CRT ट्यूब और एनालॉग सिग्नल का असर एक साथ। + अपस्केल शार्प + पैनल रिज़ॉल्यूशन से कम पर चलाने के लिए। + पिक्सल क्लीन + 2D और पुराने गेम के लिए साफ़ पिक्सल। + एनिमे एज + टून आउटलाइन के साथ शार्पनिंग। + + Adreno GPU नहीं है + समर्थित + एक्सपेरिमेंटल: इसके ड्राइवर टेस्ट बिल्ड हैं + जांचे गए हार्डवेयर से नीचे (Adreno 725 और नए): यह शायद न चले + समर्थित नहीं: DroidDeck को Adreno (Snapdragon) GPU चाहिए + + GitHub की रेट लिमिट पार हो गई - बाद में फिर कोशिश करें + GitHub ने HTTP %1$d जवाब दिया + डाउनलोड ने HTTP %1$d जवाब दिया + यह ड्राइवर लिस्ट चेकसम से पुरानी है - इसे रीफ़्रेश करें और फिर कोशिश करें + चेकसम मेल नहीं खाया - डाउनलोड हटा दिया गया + यह AdrenoTools ड्राइवर नहीं है: %1$s + कोई meta.json नहीं + meta.json पढ़ा नहीं जा सकता (%1$s) + यह एक Linux रनटाइम ड्राइवर है (इसे “Linux रनटाइम ड्राइवर” के अंतर्गत इंपोर्ट करें) + यह एक Wayland गेम ड्राइवर है, जिसका इस ऐप में कोई उपयोग नहीं है + meta.json में कोई libraryName नहीं है, इसलिए AdrenoTools को कुछ नहीं दिया जा सका + meta.json में %1$s का नाम है, जो zip में नहीं है + %1$s एक 64-बिट AArch64 ELF शेयर्ड लाइब्रेरी नहीं है + %1$s glibc से जुड़ा है - यह एक “-Linux” Turnip है, जो “Linux रनटाइम ड्राइवर” के अंतर्गत आता है + इस zip में कोई libvulkan_freedreno*.so नहीं है। Android (AdrenoTools) या -Wayland Turnip zip Linux रनटाइम ड्राइवर नहीं है। + %1$s एक 64-बिट AArch64 ELF शेयर्ड लाइब्रेरी नहीं है। + %1$s glibc ड्राइवर नहीं है - यह Android की libc से जुड़ा है। Linux रनटाइम को एक “-Linux” zip चाहिए; सादा या “-Wayland” Turnip Steam क्लाइंट द्वारा बिल्कुल भी लोड नहीं हो सकता। + यह ड्राइवर बंडल नहीं है: कोई manifest.json नहीं जो एक android और एक linux ड्राइवर का नाम ले + यह ड्राइवर बंडल नहीं है: %1$s इसके बाहर इशारा करता है + यह ड्राइवर बंडल नहीं है: यह %1$d MB से ज़्यादा में अनपैक होता है + + SD कार्ड + फ़ोल्डर + + सेशन लॉग शेयर करें + + Android 10 या नया चाहिए (कलर स्पेस वाली डिस्प्ले लेयर) + यह डिस्प्ले HDR सपोर्ट नहीं बताता + यह डिस्प्ले %1$s को सपोर्ट करता है, HDR10 को नहीं + + यह GPU + चाइल्ड-प्रोसेस सीमा चालू है + चाइल्ड-प्रोसेस सीमा सेट नहीं है + चाइल्ड-प्रोसेस सीमा जांची नहीं जा सकी + चाइल्ड-प्रोसेस सीमा बंद है + चाइल्ड-प्रोसेस सीमा ज़रूरी नहीं + %1$s अगर यह संभव नहीं है, तो इस डिवाइस को ADB वाले कंप्यूटर से कनेक्ट करें और नीचे दिया कमांड चलाएं। यहां वापस आएं और फिर से जांचें। + Steam सेशन शुरू हो सकते हैं। + यह Android वर्शन इस सीमा का उपयोग नहीं करता। + डेवलपर विकल्प में “Disable child process restrictions” चालू करें, और बाद में डेवलपर विकल्प चालू रहने दें। + इस Android वर्शन में इसके लिए कोई स्विच नहीं है। इसे इस डिवाइस से बदलने के लिए वायरलेस डीबगिंग इस्तेमाल करें। + इस बिल्ड में DroidDeck की इस कॉपी के लिए कोई डाउनलोड नहीं है। + Android इसे इस बिल्ड के ऊपर इंस्टॉल नहीं करेगा क्योंकि इसका versionCode %1$d इंस्टॉल किए %2$d से कम है। + Android किसी पुराने वर्शन को नए के ऊपर इंस्टॉल नहीं करेगा। चैनल के आगे आने तक इस बिल्ड पर बने रहें। + DroidDeck की यह कॉपी अलग तरीके से साइन है (क्या यह लोकल बिल्ड है?), इसलिए Android इसे यहीं अपडेट नहीं करेगा। + अपडेट के लिए पर्याप्त खाली जगह नहीं है। + अपडेट रद्द कर दिया गया। + अपडेट इंस्टॉल नहीं हुआ: %1$s + अपडेट इंस्टॉल नहीं हुआ। + %1$s zip + zip + %1$s अभी तक लॉन्च नहीं हुआ + %1$s के लिए कोई सेव फ़ाइल नहीं मिली + %1$s में कोई drive_c फ़ोल्डर नहीं है, इसलिए यह GameHub या Winlator सेव zip नहीं है + %1$s में फ़ोल्डर नहीं बनाए जा सकते + %1$s में लिखा नहीं जा सकता + %1$s में लिखा नहीं जा सकता (%2$s) + फ़ोल्डर उपलब्ध नहीं है + %1$s का उपयोग एक अलग फ़ाइल पहले से कर रही है + आर्टवर्क फ़ोल्डर उपलब्ध नहीं है + फ़ोल्डर में एक असंबंधित सिंक इन्वेंटरी है + %1$s हटाया नहीं जा सका + फ़ाइल एक्सपोर्ट फ़ोल्डर से बाहर चली जाती है + FEX (वर्शन नाम के बिना बिल्ड) + %1$s में कोई profile.json नहीं + %1$s एक Linux कॉम्पोनेंट पैकेज नहीं है (टाइप %2$s) + %1$s · %2$s खाली + ऑटोमैटिक + अपडेट कैटलॉग अपनी जांच में विफल रहा (%1$s) + DroidDeck चाइल्ड-प्रोसेस सेटिंग + साइन इन है · ऑफ़लाइन मोड + साइन इन है + बंडल + डाउनलोड किया गया + इंपोर्ट किया गया + ANDROID + LINUX + %1$s। %2$s\n\n%3$s + सेशन डिस्प्ले + रिज़ॉल्यूशन + 16:9 गेम फ़िट + 16:9 गेम इस डिस्प्ले में कैसे फ़िट होते हैं। अगले सेशन से। + Match screen चुनें या 16:9 से पतला कस्टम आकार चुनें। + अनुपात बनाए रखें + डिस्प्ले में स्ट्रेच करें + इमेज स्केलिंग + स्केलिंग फ़िल्टर + Steam और डेस्कटॉप में साझा। + लाइव लागू होता है। Steam और डेस्कटॉप में साझा। + FSR, GSR और NIS सेशन इमेज को बड़ा करने पर लागू होते हैं। सिर्फ़ शार्पन बिना बड़ा किए भी काम करता है। + फ़िल्टर शार्पनेस + फ़्रेम रेट + %1$s स्केलिंग को %2$s पर सेट करता है। + Match screen (%1$d×%2$d) + Follow screen (खुलने या बंद होने पर आकार बदलता है) + Steam + डिवाइस जानकारी दिखाएं + डिवाइस जानकारी छिपाएं + डिवाइस पढ़ा जा रहा है… + कॉपी करें + इस जानकारी को बग रिपोर्ट में कॉपी करें। + कॉपी हो गया। + SSBS फ़ोर्स करें + एक CPU स्पीड सेटिंग चालू रखता है जिसे Wine बार-बार बंद कर देता है। Snapdragon 8 Elite पर Windows गेम तेज़ हो सकते हैं; पुराने चिप पर बहुत कम फ़र्क़ दिखता है। अगली बार गेम लॉन्च होने पर लागू होता है। + + स्टोर + Steam + कस्टम + स्टोर + स्टोर खोलें + डाउनलोड स्पीड टियर + स्टोर इंजन + ● इंजन तैयार + नहीं बना + बैलेंस्ड + फ़ास्ट + मैक्स + स्टोर + स्टोर सेटिंग + सेटिंग + (साइन इन नहीं है) + स्टोर + इंस्टॉल किए + लाइब्रेरी + सभी + डाउनलोड + %1$s (%2$d) + साइन इन करें + %1$s · साइन आउट करें + %1$s से साइन आउट करें? + इंस्टॉल किए गेम बने रहते हैं। जब आप फिर से साइन इन करेंगे तो लाइब्रेरी फिर से मिलेगी। + साइन इन रहें + साइन आउट करें + %1$s के कैटलॉग में सर्च करें… + इंस्टॉल किए गेम में सर्च करें… + अपनी लाइब्रेरी में सर्च करें… + सर्च + कुछ इंस्टॉल नहीं है + लोड हो रहा है + कोई गेम नहीं + स्टोर लोड हो रहा है… + कोई कैटलॉग नहीं + नया क्या है + डील + फ़्री गेम + ट्रेंडिंग + आपकी लाइब्रेरी + Play + इंस्टॉल करें + इंस्टॉल फिर से शुरू करें + बाधित + इंस्टॉल · %1$s + मुफ़्त में पाएं + %1$s पर देखें + खरीदें · %1$s + इंस्टॉल है + लाइब्रेरी में + फ़्री + फिर से शुरू करें + रीफ़्रेश करें + वयस्क कंटेंट दिखाएं + Epic + EOS + ऑफ़लाइन + बंद + Epic साइन-इन (EOS) + ऑफ़लाइन लॉन्च करें + Epic साइन-इन हल करें + क्लाउड सेव + क्लाउड पर अपलोड करें + क्लाउड से डाउनलोड करें + कोई क्लाउड सेव नहीं + क्लाउड कॉन्फ़्लिक्ट + क्लाउड कॉन्फ़्लिक्ट · %1$d फ़ाइलें + … + क्लाउड रखें + लोकल रखें + क्लाउड जांचा जा रहा है… + आख़िरी सिंक %1$s + अभी सिंक नहीं हुआ + %1$d फ़ाइलें + इंस्टॉल है + आपकी लाइब्रेरी में + स्टोर + उपलब्धियां + %2$d में से %1$d अनलॉक + %1$d / %2$d + अनलॉक · %1$d + लॉक · %1$d + छिपी · %1$d + छिपी हुई उपलब्धि + अनलॉक होने पर विवरण दिखाए जाते हैं। + सभी उपलब्धियां अनलॉक + लोड हो रहा है… + अपनी उपलब्धियां देखने के लिए %1$s में साइन इन करें। + %1$s तक नहीं पहुंच सका। ऑनलाइन होने पर फिर से देखें। + %1$s इस गेम के लिए कोई उपलब्धि सूचीबद्ध नहीं करता। + सभी %1$d दिखाएं + उपलब्धियां · %1$s + ✓ Steam में + अनइंस्टॉल करें + अनइंस्टॉल हो रहा है… + %1$s अनइंस्टॉल करें? + %1$s खाली करता है और इसे Steam से हटाता है। + इसकी फ़ाइलें और इसका Steam शॉर्टकट हटाता है। + Steam अगली बार शुरू होने पर इस गेम को ले लेगा; इसे वहां से लॉन्च करें, या अभी Steam शुरू करें। + Proton + हर गेम के लिए Steam का चुनाव + ऑडियो + DirectAudio + कॉम्पोनेंट + FEX, DXVK, VKD3D + इंजन + %1$s · नेटिव + %1$s · बिल्ट-इन + यह कैसे चलता है + कहां से लिया गया + %1$s, आपके खाते से + कहां इंस्टॉल हुआ + गेम स्टोरेज (Games/%1$s/%2$s) + किससे लॉन्च होता है + Steam और Proton, Steam गेम वाला ही रास्ता + Steam और Proton · हर लॉन्च पर Epic साइन-इन, EOS ओवरले बंद + Steam और Proton · Amazon Games Services वेरिएबल सेट + Steam + इंस्टॉल पर Steam में जोड़ा गया (Deck UI + Steam Input) + कोई डाउनलोड नहीं + रुका हुआ + कतार में + कतार में · #%1$d + रद्द + विफल + विफल · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + रोकें + फिर से शुरू करें + रद्द करें + फिर कोशिश करें + हटाएं + पूरे हो गए हटाएं + मेनिफ़ेस्ट + डाउनलोड + जांचें + इंस्टॉल + पूरा हुआ + जांचा जा रहा है + डाउनलोड हो रहा है + डाउनलोड हटाएं? + विफल · %1$s रखा गया + जांचा जा रहा है + इंस्टॉल हो रहा है + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d फ़ाइलें + डाउनलोड मैनेजर + एक साथ डाउनलोड + कहां इंस्टॉल करें + %1$s इंस्टॉल करें: + इंटरनल + %1$s खाली + स्टोर डाउनलोड + गेम डाउनलोड की प्रोग्रेस दिखाता है और डाउनलोड को बैकग्राउंड में चलते रहने देता है + डाउनलोड हो रहा है + %1$d डाउनलोड + तैयार किया जा रहा है… + पूरे हुए स्टोर डाउनलोड + %1$s इंस्टॉल हुआ + %1$s डाउनलोड विफल रहा + हटाएं + रखें + %1$s में साइन इन करें + अपने प्रोवाइडर से साइन इन करें + फिर से लोड करें + बंद करें + फिर कोशिश करें + साइन-इन पूरा हो रहा है… + साइन-इन पेज लोड नहीं हो सका: %1$s + यह प्रोवाइडर एम्बेडेड पेज से साइन इन करने से मना करता है। इसके बदले अपने स्टोर पासवर्ड से साइन इन करें। + साइन-इन पेज ने एक एरर के साथ जवाब दिया (HTTP %1$d)। + साइन-इन पेज से कनेक्शन सुरक्षित नहीं है। + साइन-इन सत्यापित नहीं हो सका। फिर कोशिश करें। + साइन-इन विफल रहा। फिर कोशिश करें। + बंद करें + बैक + संपादित करें + इस फ़ोल्डर के सभी गेम जोड़ें + इस फ़ोल्डर में कुछ नया नहीं है + नाम + टार्गेट + यहां शुरू करें + लॉन्च विकल्प + आर्टवर्क + दूसरा exe… + हटाएं + कवर + बैकग्राउंड + लोगो + आइकन + गेम फ़ोल्डर + Steam + SteamGridDB + आपकी फ़ाइल + exe से + ऑटोमैटिक + इमेज चुनें… + एक इमेज चुनें: %1$s + ऑटोमैटिक पर रीसेट करें + + %1$d गेम जोड़ा गया + %1$d गेम जोड़े गए + + + %1$d पहले से Games में है + %1$d पहले से Games में हैं + + B · %1$s पर वापस जाएं + गेम + Play सेटिंग + ग्राफ़िक्स ड्राइवर + यह Proton + हर Proton + हर Proton के लिए + उपलब्ध · %1$d + %1$s को जांचा गया + नए कॉम्पोनेंट और ड्राइवर देखें + लें + %1$s · नीचे दिए DXVK, VKD3D-Proton और FEX इस Proton के हैं + %1$s · जांचा गया + %1$s · अनटेस्टेड + %1$s · असमर्थित + सुझाई गई जोड़ी को हमेशा नवीनतम रखता है + ऑटो · %1$s + रनटाइम और डिस्प्ले ड्राइवर अलग-अलग + %1$s इस्तेमाल करें? + %1$s हटाएं? + डायग्नोस्टिक्स + खेलने के लिए तैयार + अभी खेलने के लिए तैयार नहीं + रनटाइम %1$s + प्रोसेस सीमा बंद + %1$s के रूप में साइन इन + जब किसी चीज़ को आपकी ज़रूरत हो + लुक + यह डिवाइस + लाइब्रेरी आर्ट + LINUX ऐप + Flathub + Linux डेस्कटॉप के लिए डेस्कटॉप ऐप, डेस्कटॉप से खोले जाते हैं। + Flathub सेट करें + %1$s हटाएं? + इसकी सेटिंग और फ़ाइलें बनी रहती हैं। + रखें + डिस्क पर आकार + डेस्कटॉप अनइंस्टॉल करें? + सभी + GOG उपलब्धियां + समर्थित GOG गेम के लिए उपलब्धियां सिंक करता है। यह अगली बार लॉन्च करने पर लागू होगा। + उपलब्धियां बंद + उपलब्धियां चालू + GOG में फिर से साइन इन करें + उपलब्धियां अपलोड हुईं + उपलब्धि अपलोड होना बाकी है + Stores सेटिंग में GOG उपलब्धियां चालू करें + उपलब्धियां उपलब्ध नहीं; सेशन लॉग देखें + उपलब्धि अनलॉक हुई + पेस्ट करें + डी-पैड + बायां स्टिक + दायां स्टिक + मेन्यू + कीबोर्ड + डी-पैड ऊपर + डी-पैड नीचे + डी-पैड बायां + डी-पैड दायां + टच कंट्रोल + टच कंट्रोलर + माउस + ऑन + ऑफ + धीमी + सामान्य + तेज़ + लेआउट बदलें + DroidDeck मेन्यू + कंट्रोल को स्क्रीन पर खींचकर रखें + व्यवस्थित करें + रद्द करें + सेव हो रहा है… + आइकॉन + चौ− + चौ+ + ऊं− + ऊं+ + छिपाएं + रंग और ओपैसिटी… + यह लेआउट कॉपी करें + लेआउट कॉपी हो गया; इसे किसी भी गेम के टच लेआउट में पेस्ट करें + कॉपी किया लेआउट पेस्ट करें + Steam के डिफ़ॉल्ट पर रीसेट करें + इस कंट्रोल की Steam में कोई बाइंडिंग नहीं है जिसे आइकॉन दिया जा सके + %1$s के लिए आइकॉन + टच लेआउट का रंग + हो गया + टच लेआउट सेव नहीं हो सका + टच लेआउट Steam में सेव हो गया + ऐप %1$d + लेबल (वैकल्पिक) + Steam के आइकॉन नहीं मिले + + Steam से %1$d आइकॉन + Steam से %1$d आइकॉन + + आइकॉन का रंग + बटन का रंग + कोई आइकॉन नहीं + + साइन-इन जांचा जा रहा है… + आपकी लाइब्रेरी लाई जा रही है… + गेम लिस्ट लाई जा रही है… + गेम की जानकारी लोड हो रही है… (%1$d/%2$d) + + %1$d गेम सिंक हो रहा है… + %1$d गेम सिंक हो रहे हैं… + + सिंक हो रहा है… %1$d/%2$d + कवर इमेज लाई जा रही हैं… + सिंक नहीं हो सका + आपकी %1$s लाइब्रेरी में कोई गेम नहीं मिला + आपकी लाइब्रेरी के लिए Amazon Games ने जवाब नहीं दिया: रिफ्रेश करें, या फिर से साइन इन करें + GOG से कनेक्ट नहीं हो सका + आपकी GOG लाइब्रेरी पढ़ी नहीं जा सकी + Steam में रजिस्टर हो रहा है… + Cardboard VR + Cardboard मोड + फ़ोन VR व्यूअर के लिए बाईं और दाईं आंख के व्यू। + तस्वीर + वर्चुअल स्क्रीन + साइड-बाय-साइड 3D + व्यूअर + Cardboard + Cardboard (2014) + कोई लेंस नहीं + आंखों की दूरी + स्क्रीन की चौड़ाई + स्क्रीन की दूरी + हेड ट्रैकिंग + इस डिवाइस में रोटेशन सेंसर नहीं है। + व्यू को फिर से बीच में लाएं + बीच में लाएं + SteamVR (प्रायोगिक) + इंस्टॉल करें + Valve का ARM64 SteamVR डाउनलोड करता है (571 MB)। + SteamVR डाउनलोड और अनपैक हो रहा है… + शुरू करें + आपके सिर को हेडसेट मानकर SteamVR चलाता है; तस्वीर साइड-बाय-साइड 3D में बदल जाती है। + रोकें + SteamVR चल रहा है। + इस सेशन में उपलब्ध नहीं है। + SteamVR ने जवाब नहीं दिया; सेशन लॉग देखें। + कंपोनेंट डाउनलोड करें + डाउनलोड Components पेज के साथ साझा होते हैं। अगली बार गेम शुरू होने पर इस्तेमाल करने के लिए ऊपर डाउनलोड किया गया वर्शन चुनें। + डाउनलोड करने के लिए वर्शन चुनें + डेवलपर + पब्लिशर + रिलीज़ + मुख्य + + एक्स्ट्रा + पूरा + %1$s घं + %1$d मि + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 000000000..51f234a08 --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,2235 @@ + + + DroidDeck + + Az első indítás letölti a Steamet. Ez néhány percet vehet igénybe. + Alapértelmezés szerint az alkalmazás elhagyása szünetelteti a Steamet; visszatéréskor ott folytatja, ahol abbahagyta. + A kontroller Xbox 360 kontrollerként jelenik meg. Egyébként használja a képernyőn lévő vezérlőket. + + + Az első indítás beállítja az asztalt. Ez néhány percet vehet igénybe. + Alapértelmezés szerint az alkalmazás elhagyása szünetelteti a munkamenetet; visszatéréskor ott folytatja, ahol abbahagyta. + Az érintés érintőpadként működik: húzással mozgathatja a kurzort, koppintással kattinthat. + A kontroller Xbox 360 kontrollerként jelenik meg. Egyébként használja a képernyőn lévő vezérlőket. + + Munkamenet + Megjelenik, amíg egy Steam- vagy asztali munkamenet aktív + A Steam fut + Steam-letöltés folyamatban + A munkamenet szünetel – koppintson a Folytatásra, ha készen áll + Folytatás + Felfüggesztés most + Munkamenet leállítása + Képkocka-generálás + Válassza ki a képkocka-generálás módját + Ki + Win-FG %1$d× + LSFG %1$d× + LSFG adaptív %1$d + A Lossless Scaling szükséges a Steamen + Telepítse a Lossless Scalinget a Steamen, vagy importálja a Lossless.dll fájlt + Lossless.dll importálása… + A Steamről származó másolat használatban + Egy importált másolat használatban + Válassza ki a Lossless.dll fájlt + A Lossless.dll importálva + Ez a Lossless.dll már használatban van + A Lossless Scaling nincs a tulajdonában ezen a Steam-fiókon + Ez a fájl nem a Lossless Scaling Lossless.dll fájlja + Nem sikerült előkészíteni az LSFG-t ebből a Lossless.dll fájlból + Az LSFG-hez szükséges a Lossless Scaling a Steamről vagy egy importált Lossless.dll + Az LSFG nem tudta előkészíteni a shadereket a Lossless.dll fájlból + %1$s: nem támogatott + %1$s: a motor hibát jelzett + %1$s: a Lossless Scaling szükséges + %1$s %2$s · indítás + Hozzáad egy Steam-könyvtárat, és Windows-os játékokat keres benne. A tulajdonában lévő játékok a Steam-bejegyzéseiket használják; a többi nem Steam-parancsikonként jelenik meg. + Játékkörnyezet + Környezeti változók Proton-játékokhoz. + Szerkesztés + Összes Proton-játék + Profil + Steam alkalmazásazonosító + Megnyitás + %1$s játék + Változó hozzáadása + Változó neve + Érték + Mentés + Eltávolítás + Örökítés újra + Környezeti felülírások törlése + Törlés ebben a profilban + Nincs beállított változó. + Örökölt + Egyéni változó + Nem sikerült betölteni vagy menteni. Ellenőrizze a szabad tárhelyet, és próbálja újra. + Használjon érvényes nevet és legfeljebb 8192 karakteres értéket. + A szabványos Proton futtatókörnyezet nem olvassa ezt a változót. + Betöltés… + D3D12 feature level felülírása. A 12_2 a DirectX 12 Ultimate képességeit jelzi; a nem támogatott funkciók hibát okozhatnak. Hagyja beállítás nélkül az automatikus felismeréshez. + D3D12 shader model felülírása. A 6_9 kellően friss VKD3D-t és megfelelő GPU-funkciókat igényel. Hagyja beállítás nélkül az automatikus felismeréshez. + Tartsa bekapcsolva a Mesa shader-gyorsítótárazást az ismételt fordítás csökkentéséhez. A false érték bekapcsolja a gyorsítótárazást. + A Mesa shader-gyorsítótár legnagyobb mérete, például 1G. A nagyobb gyorsítótár több tárhelyet használ. + VKD3D-beállítások D3D12-höz. A nodxr kikapcsolja a sugárkövetést; csak olyan játéknál használja, amelynek szüksége van rá. + Állítsa 1-re a Proton diagnosztikai napló bekapcsolásához. A naplózás csökkentheti a teljesítményt, és tárhelyet használ. + Állítsa 1-re, hogy a DXVK helyett az OpenGL-alapú WineD3D-t használja D3D9–11 kompatibilitási teszteléshez. + Állítsa 1-re, hogy ez a játék ne használja az ntsyncet (droiddeck-ntsync vagy kernelillesztőprogram); ekkor a droiddeck-esynct használja, ha van csomag, egyébként a droiddeck-fsynct. + Állítsa 1-re a droiddeck-esync kikapcsolásához ennél a játéknál; helyette a droiddeck-fsynct használja, vagy a droiddeck-ntsynct, ha az fsync is ki van kapcsolva. + Állítsa 1-re, hogy ez a játék ne használja a droiddeck-fsynct; ekkor a droiddeck-esynct használja, ha van csomag, egyébként a droiddeck-ntsynct. + Állítsa 1-re, hogy ez a játék a droiddeck-fsyncen fusson, vagy 0-ra, hogy a droiddeck-esync kerüljön előre, ha van csomag a Protonjához. + Állítsa 0-ra a Proton kontroller-navigációs segédjének kikapcsolásához ennél a játéknál. + DXVK overlay D3D9–11-hez. Az fps megjeleníti a képkockasebességet; beállítás nélkül elrejti. + Pontosvesszővel elválasztott DXVK-beállítások. A legújabb buildek a dxvk.maxFrameRate beállítást használják a képkocka-korlátozáshoz; szokásos játékhoz a munkamenet képkocka-korlátozóját érdemes használni. + D3D12 képkockasebesség-korlát. A 0 kikapcsolja a korlátot. + Mesa OpenGL szálkezelés. A true érték segíthet a CPU-korlátozott OpenGL-játékoknál; tesztelje játékonként. + Wine-szinkronizálás + Hogyan tartják szinkronban a játékok a szálaikat. A következő munkamenettől érvényes. + esync + ntsync + fsync + wineserver + FEX előbeállítás + A következő játékindítástól érvényes. + Játékok + FEX alapértelmezés + A FEX alapértelmezéseit használja. + Stabilitás + A legbiztosabb, leglassabb beállítások. + Kompatibilitás + Teljes TSO multiblokkal. + Köztes + Kiegyensúlyozott TSO csökkentett x87-pontossággal. + Teljesítmény + Minden TSO kikapcsolva; multiblokk. + Teljesítmény + TSO + Teljesítmény-beállítások a fő TSO bekapcsolásával. + Extrém + Kikapcsolja az önmódosító kód ellenőrzését. A JIT-et használó játékok és a DRM-ek hibásan működhetnek. + Extrém + TSO + Extrém beállítások a fő TSO bekapcsolásával. + Extrém-gn + Teljesítmény TSC- és metaadat-utalásokkal. Próbálja ki az Extrém előtt. + Denuvo + Teljesítmény teljes kódellenőrzéssel és elrejtett hypervisor-bittel. + Egyéni érték + Érték kiválasztása + Emulátorok és alkalmazások + Hozzáadás + Saját alkalmazás vagy játék + Szkript + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Indítás… + %1$s hozzáadva + %1$s eltávolítva + Asztal + Ez az alkalmazás már nincs meg. Válassza ki újra az Asztalról. + %1$s megnyitása + Eltávolítás + Nyomja meg újra az eltávolításhoz + Állítsa le a munkamenetet az eltávolításhoz + A %1$s a saját mappájából fut, amely a helyén csatolva van a munkamenetbe. Az alkalmazás eltávolítása nem érinti azt a mappát. + A %1$s a mappájának egy, a DroidDecken belüli másolatából fut. Az alkalmazás eltávolítása törli a másolatot, az eredetit nem. + Előbb telepítse a Linux futtatókörnyezetet + A fájl már nincs meg + Nincs elég szabad hely: a mappa másolásához %1$s szükséges + %1$s másolása + Az alkalmazás hozzáadása sikertelen + %1$s kiadásának keresése + Nem sikerült beolvasni a %1$s kiadásait + A %1$s semelyik kiadása nem tartalmaz ARM64 vagy x86_64 AppImage-et + %1$s letöltése + A letöltés sikertelen + Az alkalmazás frissítése sikertelen + Ezt az ikont nem sikerült beolvasni + %1$s mentve + Szerkesztés + Frissítések keresése + Frissítések keresése… + Frissítés erre: %1$s + Naprakész · %1$s + %1$s frissítve + %1$s · %2$s + Alkalmazás hozzáadása + Szkript + AppImage + GitHub + Flatpak + Egy .sh fájl. A saját mappájából fut, teljes képernyőn, gamescope alatt. + Egy ARM64 (aarch64) AppImage, vagy egy x86_64-es, amely a FEX-en keresztül fut. Egyszer kerül kicsomagolásra a Linux futtatókörnyezetbe. + Telepíti a tároló legújabb kiadásának ARM64 AppImage-ét, vagy annak x86_64-es változatát a FEX-en keresztül. + FEX használata + Automatikus + Be + Ki + A FEX x86-os Linux-programokat futtat ezen az ARM64-es eszközön. Az Automatikus akkor használja, ha a program x86-ra készült. + %1$s a FEX-en keresztül + Ez a program %1$s PC-khez készült, és a FEX-en keresztül fut, amely még nincs beállítva. Nyissa meg egyszer a Steamet bejelentkezve: a DroidDeck ekkor letölti a FEX-et és a szükséges Steam Linux Runtime-ot, majd ez megnyílik. + Ez a program + A %1$s x86 PC-khez készült, és a FEX-en keresztül fut, amely még nincs telepítve. Indítsa el egyszer a Steamet bejelentkezve, és hagyja letölteni a FEX-et és a Steam Linux Runtime-ot, majd indítsa el ezt újra. + Egy Flathub-alkalmazásazonosító vagy a Flathub-oldala. A Flatpak előbb beállításra kerül, ha még nincs beállítva. + Fájl + Nincs kiválasztott fájl + Fájl kiválasztása + Módosítás + Válasszon szkriptet + Válasszon AppImage-et + Válasszon ikont + Tároló + tulajdonos/tároló vagy egy github.com hivatkozás + Nem egy GitHub-tároló + Alkalmazásazonosító + org.ppsspp.PPSSPP + Nem egy Flathub-alkalmazásazonosító + Vagy egy .flatpak fájl, egy Flathubon nem elérhető alkalmazáshoz. + Válasszon .flatpak fájlt + Forrás + Az Asztalon + Új alkalmazás + Név + Az alkalmazásból + Ikon + Alapértelmezett ikon + Ikon kiválasztása + Törlés + A tárolóból + Ikonok keresése… + Alapértelmezett + %1$s szerkesztése + Mentés + A mappa ellenőrzése… + A mappája a munkamenetbe van csatolva: %1$s + A mappája Linux-programokat tartalmaz, amelyeket a megosztott tárhely nem tud futtatni, ezért átkerül a DroidDeckbe (%1$s). + Hozzáadás + Mégse + Előbb telepítse a Linux futtatókörnyezetet a Beállításból. + Várja meg, amíg a(z) %1$s befejeződik. + Kompatibilis + A Steam Kompatibilitás menüjében „%1$s” jelölésű buildek tartalmazzák a DroidDeck prefixjavításait, és ezeken itt elindulnak a játékok. Az alapértelmezett a Proton Experimental ARM64 (%1$s), vagy a Proton 11.0 ARM64 (%1$s), ha az az újabb build telepítve van. A Valve saját, jelölés nélküli Proton-bejegyzései nem tudnak játékot elindítani ezen az eszközön. + %1$s telepítve · %2$s + Mégse + Automatikus + Soha + Mindig + Eltávolítás + Ellenőrzés + Frissítés + Ellenőrzés… + Feldolgozás… + Elfelejtés + Hozzáadás… + Használat + A következő munkamenettől érvényes. + Steam-munkamenet + Asztali munkamenet + Kijelző + Felbontás + Legfeljebb 720p + Legfeljebb 900p + Legfeljebb 1080p + Natív + %1$s (alapértelmezett) + Egyéni · %1$d×%2$d + Egyéni… + A 720p reszponzívabbá teheti a menüket. + Képernyőarány + Az egyéni felbontás határozza meg. + Az Automatikus legalább 16:9-et használ. + Képkocka-korlát + Korlátozza a teljes munkamenetet. A kijelző olyan frissítési frekvenciára vált, amelyet a korlát egyenlően oszt. + HDR + HDR10-kimenet + Nem elérhető: %1$s. + Indítsa újra az alkalmazást az érvényesítéshez. + Illesztőprogramok + GPU-illesztőprogramok + A Steam és az Asztal közösen használja. Az Összetevők oldalon állítható be. + Érintés és vezérlők + Érintés + Érintőpad + Közvetlen + Automatikus: érintőpad az Asztalon, közvetlen a Steamben. + Képernyőn megjelenő vezérlők + Steam + QAM + Az Automatikus megjeleníti a vezérlőket, ha nincs csatlakoztatott kontroller. + Steam érintés (béta) + A Steam érintésvezérlői a következő Steam-munkamenettől lépnek életbe + Steam érintéskiosztás + Mozgassa, méretezze vagy rejtse el a játék érintésvezérlőit; a Steamben mentve + Kontroller + Hogyan látja a Steam a kontrollerét. A következő munkamenettől érvényes. + Steam Deck kontroller + Steam Controller + DualShock 4 + DualSense + Xbox 360 kontroller + Deck: Gyors hozzáférés gomb és gyroszkóp. Steam Controller: ugyanez, a hátsó gombok fogásérzékelésével. DualShock 4 és DualSense: gyroszkóp, és az érintőpadok mint touchpad. Xbox 360: a Gyors hozzáférés megnyitása Guide+A-val. + Vissza gomb + Menü → QAM + QAM → Menü + Munkamenet + Háttérbeli viselkedés + Amikor az alkalmazás el van rejtve, vagy a kijelző kikapcsol. + Manuális + Natív + Az Automatikus szüneteltet, és megjelenéskor folytatja. A Natív előbb felkészíti a Steamet az altatásra. A Manuális megvárja a Folytatást. A Soha mindig futva tartja. + Letöltések futtatásának fenntartása + Elhalasztja a háttérbeli felfüggesztést, amíg a Steam aktívan letölt. + Az Automatikus szüneteltet a háttérben, és megjelenéskor folytatja. A Manuális megvárja a Folytatást. A Soha mindig futva tartja. + Indítás + Steam indítása a DroidDeckkel + Megnyitja a Steam-munkamenetet a DroidDeck indulásakor. + Decky + Kiadások keresése… + Készen áll a %1$s telepítésére. + Nem található kompatibilis build. Próbálja újra később. + Telepítve · %1$s + Elérhető frissítés · %1$s + Naprakész · %1$s + Betöltő + Legújabb telepítése + A Steammel együtt indul. Amíg be van kapcsolva, az eszközön lévő más alkalmazások is irányíthatják a Steamet. + Ki. A Steam távoli hibakeresési portja zárva marad. + Eltávolítja a Decky Loadert? + Eltávolítja a betöltőt. A bővítményei és beállításai megmaradnak. + Kliens + Teljesítmény-overlay + Kapcsolja ki, ha a játékok összeomlanak vagy elfeketednek Deck módban. A következő munkamenettől érvényes. + Steam-kliens javítása + A Steam kliens újbóli letöltése a Steam következő indításakor. Használja, ha a Steam a betöltő képernyőn ragad. A játékok, a bejelentkezés és a beállítások megmaradnak. + A Steam kliens újra letöltődik a Steam következő indításakor. + Javítás + Illusztrációk + SteamGridDB API-kulcs + A kulcsa + Szerkesztés… + Kitölti a hiányzó illusztrációkat a Steamről, majd a SteamGridDB-ről. A játék mappájában lévő saját cover.jpg, hero.jpg, logo.png vagy icon.png fájl elsőbbséget kap. + %1$s eltávolítása + Eltávolítja? + Játék hozzáadása + Játék hozzáadása: + Válassza ki az exe fájlt: + Ez a hely nem érhető el a munkamenetben + Ennek az exe fájlnak az ellenőrzése + Windows-összetevők + LB / RB a szakaszok közötti ugráshoz. + Nincs + A Proton által nem tartalmazott Windows DLL-ek az összetevőlistából. Kapcsolja be azokat, amelyekre ennek a játéknak szüksége van: ezek a játék saját Windows-mappájába kerülnek, soha nem a fájljaiba, és megmaradnak, függetlenül attól, melyik Protonon fut. + Nem sikerült elérni az összetevőlistát. A már letöltött összetevők továbbra is kapcsolhatók. + Ajánlott ehhez a játékhoz + Annak ellenőrzése, mire van szüksége ennek a játéknak… + Online elérhető + + %1$d összetevő bekapcsolása… + %1$d összetevő bekapcsolása… + + Bekapcsolva ehhez a játékhoz + A Steam a %2$s segítségével telepíti a %1$s-t (alkalmazás: %3$s) + A GOG listájából ehhez a játékhoz + Az Epic előfeltételeiből ehhez a játékhoz + Az Amazon telepítési lépéseiből ehhez a játékhoz + A Steam név szerint egyezve telepíti a %1$s-t a %2$s alkalmazással + Szintén szerepel a listán + A %1$s sorolja fel; itt nem összetevő + Steam + GOG + Epic Games + Amazon Games + A játék a %1$s-t tartalmazza + A játékban megtalálható a %1$s (gyakran változatlanul is működik) + Már megvan ebben a játékban + A játék mappájában semmi nem utal összetevőre. + Telepítve ehhez a játékhoz (%1$d) + Minden összetevő (%1$d) + Windows-telepítőt igényel (%1$d) + Ezek saját telepítőprogramok, amelyekben nincs szétszedhető csomag, és még nincs rögzítve, mit telepítenek. Többségük régi futtatókörnyezet, amelyet a Protonban lévő Mono már helyettesít; a rögzítések elkészülésük után kerülnek hozzáadásra. + Megjelenítés + Elrejtés + Saját telepítőprogramot futtat (még nincs rögzítve) + Itt még nem támogatott + Összetevők betöltése… + Nem sikerült telepíteni: %1$s + Letöltve + %1$s letöltése + Telepítés: %1$s + Kikapcsolás… + %1$s letöltés + A játék következő indításakor lép érvénybe. + 16:9 játékok nyújtása + A 16:9-nél négyzetesebb képernyőkön kitölti a játékkal a képernyőt sávok helyett. A következő munkamenettől érvényes. + Hang + DirectAudio a játékokhoz + Megkerüli a PulseAudiót a kisebb késleltetés érdekében. + Mikrofon + Hangos csevegéshez. + Hálózat + Wi-Fi felderítés + Wi-Fi nevek és közeli hálózatok megjelenítése a Steamben. + A helymeghatározási szolgáltatások ki vannak kapcsolva. + A helymeghatározási engedély nincs megadva. + Beállítások megnyitása + Folytatás + Bekapcsolja a Wi-Fi felderítést? + Az Android helymeghatározási engedélyt igényel a Wi-Fi nevek beolvasásához és a közeli hálózatok felderítéséhez. A DroidDeck ezt a hozzáférést arra használja, hogy megjelenítse ezeket a hálózatokat a Steamben. Nem követjük a helyadatait. Az internet-hozzáférés ezen engedély nélkül is működik. + Játéktárhely + Automatikus – az SD-kártya, ha van, és a Játékok mappái + Automatikus – a Játékok mappái, és az SD-kártya, ha van (jelenleg nincs) + Csak belső + Mappa: %1$s + Második könyvtár + Az SD-kártyáról vagy megosztott tárhelyről streamelő játékok akadhatnak. + Tárhely-diagnosztika + Rögzíti a foglalási eredményeket és a mintavételezett tárhelyidőzítéseket a megosztott naplókban. Bekapcsolva lassíthatja a telepítéseket. A következő munkamenettől érvényes. + Válasszon mappát… + Egyéni felbontás + Felülírja a korlátot és az arányt. A képernyőhöz nem illő méret sávokat eredményez. + Szélesség + Magasság + 320×240 és 3840×2160 között. + + %1$d játék + %1$d játék + + OK + Törlés + Visszaállítás + Letöltés + %1$s törlése + %1$.1f MB + Futtatókörnyezet-illesztőprogram + A Steam, a játékok és az asztal használja. A következő munkamenettől érvényes. + Turnip zip importálása… + Megjelenítő-illesztőprogram + Mindkét módban ez rajzolja a képkockákat a képernyőre. Indítsa újra az alkalmazást az érvényesítéshez. + AdrenoTools zip importálása… + soha + Nightlies ellenőrizve: %1$s + Összetevők + Az Összetevőkről + Új illesztőprogramok keresése + Új csomagok keresése a Nightliesben + Proton + Az itt kiválasztott Proton futtat minden olyan játékot, amely a Steamben nem választott saját Protont, és a Steamben végzett módosítás is megjelenik itt. A Steam frissítéskor lecseréli a Valve Protonját; az eredetik buildenként megmaradnak. + Konfiguráció + Profil beállítása + Profil + Alapértelmezett profil + A felülírás nélküli játékok automatikusan ezt használják. + A mentett beállítások automatikusan érvénybe lépnek a %1$s indításakor. + Csak azokat a beállításokat válassza ki, amelyekre ennek a játéknak szüksége van. Minden más továbbra is az Alapértelmezett profilt használja. + FEX előbeállítás + Az Alapértelmezett profil előbeállítását használja. + Egyéni előbeállítás mentve ehhez a játékhoz. + Egyéni FEX-változók + Alapértelmezett használata + Játékkompatibilitás + Proton-, DXVK-, VKD3D- és szinkronizációs környezeti beállítások ehhez a játékhoz. + Az Alapértelmezett profil Protonját használja. + Egyéni Proton mentve ehhez a játékhoz a Steam kompatibilitási hozzárendelésén keresztül. + Nincs Proton + DXVK + VKD3D-Proton + Az Alapértelmezett profil %1$s csomagját használja, hacsak ez a játék másik csomagot nem választ. + Az Alapértelmezett profil összetevőcsomagját használja. + Csak ehhez a játékhoz a %1$s-t használja. + Futtatókörnyezet-beállítások + Egyéb, játékonkénti Proton-, DXVK-, VKD3D-Proton- és szinkronizációs környezeti beállítások. + A %1$s profilja mentve a játék következő indítására. + Indítás Steam nélkül + Olyan játékhoz, amely azonnal bezáródik: elindítja a Proton Steam-beállítása nélkül. A Steam funkciói, mint az overlay, ki vannak kapcsolva hozzá. + Be + A %1$s Proton-választása mentve, és már elérhető a Steamben. + A %1$s Proton-választása mentve. Indítsa újra a Steamet a játék elindítása előtt. + A %1$s nem tudja használni a kiválasztott Protont ezen az eszközön. + Nem sikerült betölteni a %1$s profilját: %2$s + Nem sikerült menteni a játékprofilt: %1$s + Játékprofil visszaállítása + Visszaállítja a %1$s profilját? + Eltávolítja ennek a játéknak a Proton-, FEX-, összetevőcsomag-, környezeti, textúraszűrési és Windows-összetevő-felülírásait? Ezután az Alapértelmezett profilt fogja használni. + A %1$s most az Alapértelmezett profilt használja. + Nem sikerült teljesen visszaállítani a játékprofilt: %1$s + A %1$s most minden olyan játékot futtat, amely a Steamben nem választott saját Protont. + A %1$s a Steam következő indításától minden olyan játékot futtat, amely nem választott saját Protont. + A %1$s itt nem tud elindítani játékokat, ezért nem lehet alapértelmezett. + Nem sikerült menteni az alapértelmezett Protont: %1$s + %1$s · %2$d kicserélve + %1$s · mind eredeti + %1$s · a játék fut + %1$s · droiddeck-esync csomag + %1$s · még nincs droiddeck-esync csomag + %1$s · az esync be van építve, nincs szükség csomagra + GPU-illesztőprogramok + %1$s · újraalkalmazva indításkor: %2$s + Következő: %1$s · a játék bezárása után + %1$s használatban: %2$s + Megvárja, amíg a játék bezárul + A következő játékindítástól érvényes + .zip importálása + Protonok beolvasása… + Még nincs telepítve Proton. Indítsa el egyszer a Steamet az ARM64 Protonjának letöltéséhez, vagy adjon hozzá GE-Protont, CachyOS-t vagy DroidDeck-Protont a Beállításból. + Eredeti · %1$s + %1$s · ez a build + %1$s · korábbi build + Telepítve · %1$d + Még nincs tárolva semmi a %1$s számára. Ennek a Protonnak a saját fájljai eredetiként lesznek mentve. + Egy játék fut ezen a Protonon, ezért ez megvárja, amíg bezárul. + A következő játékindítástól érvényes. + Kicseréli a %1$s-t? + Beilleszti a %1$s-t a %2$s-be? Az eredeti fájljai mentve maradnak. %3$s + Visszaállítja a %1$s-t? + Visszateszi a %1$s eredetijét a %2$s-be? %3$s + Eltávolítja a csomagot az alkalmazásból. Újra letöltheti. + Törli ezt az eredetit? + Ez a %1$s eredeti a %2$s egy korábbi buildjéből származik. Az aktuális build eredetije mindig megmarad. + A Nightliesen · %1$d + Frissítsen a Nightliesen lévő csomagok listázásához. + Nincs semmi új az utolsó ellenőrzés óta. + .wcp importálása + Ennek a Protonnak az eredetijének visszaállítása + Visszateszi a %1$s eredeti %2$s-ét? + Csere + FEX, DXVK és VKD3D-Proton minden olyan Protonhoz, amellyel a Steam játékokat futtat.\n\nVálasszon ki egy telepített sort a becseréléséhez. Ez a következő játékindításkor, vagy a futó játék bezárása után lép érvénybe.\n\nA választásai minden indítás előtt újraellenőrződnek, és visszaállnak, ha valami megváltoztatta őket. Minden Proton eredeti fájljai megmaradnak, így a Steam-frissítések sosem veszítik el őket. + Eredeti + Használatban + Új + Tárolva + Teljesítmény + CPU-magok, munkamenet-javítások és a hosztnév. A következő munkamenettől érvényes. + Steam-kliens magok + A Steam magválasztásának felülírása + A Steamet, felhasználói felület segítőjét és a gamescope-ot az alábbi magokhoz rögzíti. + Kliensmagok + Magok a Steamhez. + Kapcsolja be a felülírást a kiválasztáshoz. + Játékmagok + Játékmagok + Magok a játékokhoz. Ha mind kiválasztva van, az Android szabadon ütemez. + Kliens felhasználói felület + Szálalt GL + Reszponzívabbá teheti a Steam menüit. + Zink: lusta leírók + Leírópuffer nélküli illesztőprogramokhoz ajánlott. + GL-hibaellenőrzések kihagyása + Kikapcsolja a hívásonkénti GL-ellenőrzést. + gamescope: valós idejű GPU-sor + Előnyben részesíti a kompozitor GPU-munkáját. Egyenletesebbé teheti a képkocka-ütemezést, kisebb költséggel a játékoknak. + GPU + A GPU legmagasabb órajelen tartása + Csak Adreno. Kevesebb akadás, nagyobb akkumulátorhasználat és hőtermelés. + Munkamenet-javítások + Turnip: sysmem renderelés + Gyorsabb a Steamhez és a legtöbb játékhoz. Adreno 710/720/722-n szükséges. + A Steam xalia segítőjének kihagyása + droiddeck-ntsync (kísérleti) + A Linux ntsync illesztőprogram felhasználói szintű helyettesítője, teszteléshez. Amíg be van kapcsolva, a Proton ezt használja a droiddeck-esync és a droiddeck-fsync helyett. A Proton Experimental ARM64-gyel, a GE-Protonnal és a proton-cachyosszal működik. A következő munkamenettől érvényes. + droiddeck-esync + droiddeck-fsync előre + A Proton saját fsyncjét futtatja az eredeti Proton-fájlokon, felhasználói szinten biztosított futex_waitv-vel, így nincs szükség csomagra, és azonnal működik, amint a Steam frissíti a Protont. Amíg be van kapcsolva, a játékok ezt használják a droiddeck-esync előtt, kivéve azt a néhányat, amelyet a Proton kizár az fsyncből. A következő munkamenettől érvényes. + A játékok Windows-szálai eventfd-n és megosztott memórián keresztül várnak egymásra, a wineserveren át vezető körök helyett. Egy az adott Proton buildhez készült droiddeck-esync csomagot használ; a csomagok automatikusan letöltődnek. Amíg egy új Proton buildhez nincs csomag, a droiddeck-fsync helyettesíti. A következő munkamenettől érvényes. + Kikapcsolja a Proton kontroller-navigációs segédjét, amely minden játékban CPU-t használ. + A proot futtatása seccomp nélkül + Javíthatja a hiányzó szisztémahívás-hibákat egyes kerneleken. Csökkentheti a teljesítményt. + proot gyors útvonal + A proot seccomp-szűrőjét igényli. + Gyorsabb fájlkeresés és kevesebb akadás. Kapcsolja ki, ha egy program nem találja a fájljait. + Munkamenet-azonosítás + Hosztnév + A név, amelyet a Steam ehhez az eszközhöz lát. Üresen hagyva visszaállítja: %1$s. + Csak betűk, számok és belső kötőjelek, legfeljebb 63 karakter. + Az Android be van állítva, hogy kilője ezt a munkamenetet + Telepítve + Letölthető + Nincs semmi új az utolsó ellenőrzés óta. + Beépített illesztőprogramok visszaállítása + Beépítve az alkalmazásba: ez eltávolítja a kicsomagolt fájljait, és elrejti. A Beépített illesztőprogramok visszaállítása visszahozza. + Fele egy Android + Linux csomagnak: mindkét fele eltávolításra kerül. A használatban lévő illesztőprogram visszaáll az alapértelmezettre. + A fájljai eltávolításra kerülnek az alkalmazásból. + A fájljai eltávolításra kerülnek. Használatban van, ezért az alapértelmezett veszi át a szerepét. + Kijelző + Effektek + Vezérlők + Összetevők + Kontroller + Beállítások + %1$s oldal + Steam + Asztal + Steam-menü + Gyors hozzáférés + Gyors hozzáférés menü megnyitása + Előző oldal + Következő oldal + Teljesítmény HUD + Méretezési mód + Méretezési élesség + Képernyőeffektek + Élőben. Kis GPU-időt igényel, amíg bármelyik effekt be van kapcsolva. + Előbeállítások + Egyéni + Speciális + Kibontva + Összecsukva + Élesítés (CAS) + Élesítés szintje + Hamis HDR + Csíkmentesítés + Csíkmentesítés erőssége + Fényerő + Kontraszt + Gamma + Telítettség + FXAA (élsimítás) + Rajzfilmes körvonalak + CRT + NTSC + Textúraszűrés + DirectX 9–11 játékok, a következő indításuktól. + Anizotróp szűrés + Textúraélesség + Negatív mip-eltolás: élesebb textúrák, némi csillogással. Az Automatikus illeszkedik ahhoz, mennyire van felnagyítva a munkamenet a képernyőhöz. + Automatikus (%1$s) + Billentyűzet + PC-billentyűzet + Android + Második képernyő + Mód + Megjelenít + Android-alkalmazások + Alkalmazás indítása + Elrejtés + Megjelenítés + Nincs indítható alkalmazás + Munkamenet-viselkedés + Most és a jövőbeli, ebben a módban futó munkamenetekre is érvényes. + Következő munkamenet + Támogatás + Munkamenet-naplók + Csatolja egy hibajelentéshez + Naplók megosztása + Háttér + Leállítás + Leállítja a munkamenetet? + Jelenleg fut + Nem fut játék Protonon + Játékprofil + A profil részletei még nem állnak rendelkezésre. + Nem sikerült betölteni a játékprofilokat. Automatikus újrapróbálkozás. + Nem sikerült frissíteni a játékprofilokat. Az utolsó elérhető adatok jelennek meg; automatikus újrapróbálkozás. + A következő indításkor használt mentett beállítások megtekintése és szerkesztése. + Az Alapértelmezett profil használatban + Egyéni profil + A %1$s az Alapértelmezett profilból használatban. + A módosítások a %1$s profiljába kerülnek mentésre, és a játék következő indításakor lépnek érvénybe. + + %1$d játékfelülírás + %1$d játékfelülírás + + Még nincs telepítve Proton. + játék fut + újraalkalmazva indításkor: %1$s + Eredeti + Importálás és törlés az Összetevők oldalon. + Kész + Figyelmet igényel + Feldolgozás + Ellenőrizze a vezeték nélküli hibakeresés IP-címét és portját, vagy párosítsa újra, ha az Android eltávolította ezt az eszközt. + Telepítés + Kezelés + Áttekintés + Kontroller + Munkamenet + Indítóalkalmazás + Beállítás + ● Minden rendben + %1$d/%2$d kész + Rendszerellenőrzés + Az eszköz támogatott + Nem tesztelt GPU + A GPU nem támogatott + %1$s · %2$s + %1$s: %2$s + A Steamnek Adreno-illesztőprogram szükséges; a %1$s fekete képernyőt jeleníthet meg + Linux futtatókörnyezet + %1$s · %2$d%% + Nincs telepítve · kb. 3 GB, az első Játék gombnyomáskor települ + Telepítve + %1$s · elérhető frissítés + %1$s · naprakész + Gyermekfolyamat-korlát + Be · az Android bezárhatja a Steamet. A kikapcsolása egy percet vesz igénybe. + Nincs beállítva · az Android bezárhatja a Steamet. A kikapcsolása egy percet vesz igénybe. + Nem sikerült ellenőrizni · a kikapcsolása egy percet vesz igénybe. + Javítás + Részletek + Fejlesztői beállítások + Vezeték nélküli hibakeresés használata + Ellenőrzés újra + Korlát bekapcsolása + Steam-fiók + Bejelentkezve mint %1$s · offline mód + Bejelentkezve mint %1$s + Nyomja meg a Játék gombot, és jelentkezzen be a Steambe + Eszközök + Kontroller + A kontrollerbeállítások nem érhetők el. + Munkamenet + Munkamenet-naplók + Minden munkamenet után mentve + Hibakeresési eszközök + Legutóbbi munkamenet-naplók + Mentett munkamenet-naplók + A legújabb %1$d-t tartja meg + Naplók törlése + Offline mód + Előbb jelentkezzen be a Steambe + Indítóalkalmazás + Nyelv + A Steam is ezt használja, a következő indításától. + Rendszer alapértelmezett (%1$s) + Rendszer alapértelmezett + Alkalmazásméret + A DroidDeck szövege és vezérlői + %1$d%% (Alapértelmezett) + Téma + Az indítóalkalmazás megjelenése + Használat kezdőképernyőként + A DroidDeck lehet a kezdőképernyő alkalmazás + A DroidDeck soha nem jelenik meg kezdőképernyő alkalmazásként + Tájolás + Automatikus fekvő + Fekvő + Fordított fekvő + Teljes képernyő + Elrejti az Android állapot- és navigációs sávokat + Megjeleníti az Android állapot- és navigációs sávokat + Alapértelmezett kezdőképernyő alkalmazás + Válasszon kezdőképernyő alkalmazást + Kiválasztás + Linux-alkalmazások (béta) + Fájlok + Fájlok tallózása és kezelése + Proton-verziók + ARM64 Proton-buildek telepítése + Teljesítmény + CPU-mag hozzárendelés + ROMs mappa + Hol vannak tárolva az emulátoros játékok + Összetevők + FEX, DXVK, VKD3D + Vezérlők + Gombkiosztás + Felfedezés + Keresés + Telepítve + %1$s · %2$d + Áruház + Flathub · ARM64 + ● A Flatpak kész + Előbb telepítse a Linux futtatókörnyezetet a Beállításból. + Flatpak beállítása + Hozzáadja a Flatpakot, és csatlakoztatja a Flathubhoz (kb. 20 MB). Az első alkalmazás letölti a saját futtatókörnyezetét is. + Beállítás… + Flatpak + Frissítések + %1$s · %2$s + %1$s · %2$s · %3$d%% + Indítás… + Kategóriák + Nem sikerült elérni a Flathubot. Ellenőrizze a kapcsolatot, és próbálja újra. + Próbálja újra + Keresés a Flathubon + Firefox, játékok, emulátorok… + Keresés + Összes + Keresés… + Keressen név szerint, vagy válasszon kategóriát. Csak ARM64-es alkalmazások jelennek meg. + Nincs ennek megfelelő ARM64-es alkalmazás a Flathubon. + Eredmények + Frissítések keresése + Összes frissítése (%1$d) + ● Minden naprakész + Még nincsenek alkalmazások. Keressen a Felfedezés vagy a Keresés alatt. + Állítsa be a Flatpakot az alkalmazások telepítéséhez. + Elérhető frissítés · %1$s + Megnyitás + Részletek + Eltávolítás + Nyomja meg újra az eltávolításhoz + Telepítés… + Telepítés + Előbb állítsa be a Flatpakot + %1$s letöltés + ● Telepítve + Nem sikerült betölteni ennek az alkalmazásnak a Flathub-oldalát. + Betöltés a Flathubról… + Nincs ARM64-es build a Flathubon, így itt nem telepíthető. + Képernyőképek + Névjegy + Alkalmazásazonosító + Licenc + Futtatókörnyezet + Telepített méret + %1$s (plusz a megosztott futtatókörnyezete) + Weboldal + Ellenőrzött + Frissítések + DroidDeck %1$s · %2$s + Ellenőrizve: %1$s + Még nincs ellenőrizve + Engedélyezze, hogy a DroidDeck magát frissítse + A következő képernyőn kapcsolja be az „Engedélyezés ebből a forrásból” lehetőséget, majd térjen vissza. A frissítés önműködően folytatódik. + Beállítások megnyitása + Most nem + Másként aláírva + Nem frissíthető a helyén + Ez a másolat nincs a DroidDeck kiadási kulcsával aláírva, így a frissítések nem telepíthetők rá. Távolítsa el az átváltáshoz. + Elérhető frissítés + Nem telepíthető + Kész az átváltásra + A Stable előtt + A Stable előtt áll + Ez a build újabb, mint a legújabb Stable. A Stable következő kiadásával arra fog átváltani. + A teszt lezárult + Ez a teszt lezárult + A javítása beolvasztva vagy elvetve lett. Kövesse a Previewt a legújabb javításokért. + Naprakész + A legújabb %1$s van telepítve + · %1$s + %1$s… %2$d%% + %1$s… + Frissítés%1$s + Telepítés%1$s + A Stable %1$s telepítése mindenképpen + Preview követése + Állítsa le a futó munkamenetet a frissítéshez. + Kevesebb + Több + DroidDeck %1$s + Új Preview build + PR #%1$d teszt build + %1$d MB + FRISSÍTÉSI CSATORNA + Stable + Tesztelt kiadások, a legtöbb felhasználónak + %1$s · %2$s + Preview + A legújabb javítások, a Stable előtt + Naprakész + Teszt buildek + Próbáljon ki egy javítást a kiadása előtt + Jelenleg nincs + PR #%1$d + Nincs buildelve ehhez a DroidDeck-másolathoz + Frissítve: %1$s + a PR #%1$d teszt + egy ideje + épp most + + %1$d kipróbálható + %1$d kipróbálható + + + Változások a buildje óta · %1$d build lemaradás + Változások a buildje óta · %1$d build lemaradás + + + Legutóbbi változások · %1$d build + Legutóbbi változások · %1$d build + + Egy Android-beállítást kell módosítani + Steam + Az Android leállítja azokat az alkalmazásokat, amelyek sok háttérfolyamatot indítanak, a Steam pedig tucatnyit indít. Amíg ez nem változik, a játékok hibaüzenet nélkül bezáródnak. + Javítsa ezt helyettem + Kapcsolja be a „Gyermekfolyamat-korlátozások letiltása” kapcsolót, majd térjen vissza. Hagyja bekapcsolva a Fejlesztői beállításokat, különben a kapcsoló visszakapcsol. + Fejlesztői beállítások megnyitása + Ehhez az Android-verzióhoz nincs kapcsoló, ezért a DroidDeck a vezeték nélküli hibakeresésen keresztül módosítja. Körülbelül egy percet vesz igénybe, számítógép nélkül. + A vezeték nélküli hibakereséshez Wi-Fi szükséges. Bármelyik hálózat megfelel; internetkapcsolat nem szükséges. + A párosítási kód egy értesítésben jelenik meg. Engedélyezze az értesítéseket a DroidDecknek, vagy használja az Egyéb módokat. + Értesítések engedélyezése + Nyissa meg: Beállítások → Fejlesztői beállítások → Vezeték nélküli hibakeresés, és kapcsolja be + Koppintson az „Eszköz párosítása párosítási kóddal” lehetőségre + Írja be a kódot a DroidDeck értesítésébe + Várakozás a párosítási felugró ablakra… + Felugró ablak megtalálva. Írja be a kódját az értesítésbe. + Állapot + Ellenőrzés 2 másodpercenként. Ez az oldal bezárul, amikor kikapcsol. + Egyéb módok + Vezeték nélküli hibakeresés, manuális cím, számítógépes ADB + Manuális cím, számítógépes ADB, Fejlesztői beállítások + Vezeték nélküli hibakeresés használata helyette + Néhány ROM mindenképpen hozzáadja a kapcsolót. Keresse a „gyermekfolyamat” kifejezést a Fejlesztői beállításokban. + Ha az értesítés nem működik, írja be saját kezűleg a felugró ablak címét és kódját. + Cím kézi megadása + Egy ADB-vel rendelkező számítógépről: + Parancs másolása + Válassza ki, melyik kijelzőn nyíljon meg az Android Beállítások. + Fő képernyő + Párosítva. A vezeték nélküli hibakeresés portjának keresése… + Párosítva. Adja meg a portot a Beállításokból. + Eszköz megtalálva. A beállítás alkalmazása… + Nem sikerült automatikusan alkalmazni. Ellenőrizze az alábbi címet, és próbálja újra. %1$s + Vezeték nélküli hibakeresés + A gyermekfolyamat-korlát frissítve + A gyermekfolyamat-korlát módosítása + Ha a beállítás hiányzik, párosítson a vezeték nélküli hibakeresésen keresztül. + Párosítva. Ellenőrizze az IP-címet és a portot a vezeték nélküli hibakeresésben. + A gyermekfolyamat-korlát frissítve. + Használja a 2. lépést, ha a Fejlesztői beállításokban nincs gyermekfolyamat-beállítás. Nyissa meg: Fejlesztői beállítások → Vezeték nélküli hibakeresés → Eszköz párosítása párosítási kóddal. + Párosítva. Használja az alábbi címet, vagy frissítse a Vezeték nélküli hibakeresés → IP-cím és port menüből. + A gyermekfolyamat-korlát frissítve a vezeték nélküli hibakeresésen keresztül. + Az Android megerősítette, hogy a korlát be van kapcsolva. + Az Android megerősítette, hogy a korlát ki van kapcsolva. + 2 · Vezeték nélküli hibakeresés párosítása + A párosítási felugró ablak IP-címe és portja + Párosítási kód (PIN) + 6 számjegy + Párosítás + 2 · Alkalmazás vezeték nélküli hibakeresésen keresztül + Kapcsolat IP-címe és portja + Párosítás újra + Korlát kikapcsolása + Egyéb módok a módosítására + 1 · Fejlesztői beállítások (próbálja ezt először) + Kapcsolja be a „Gyermekfolyamatok korlátozása” lehetőséget. + Kapcsolja ki a „Gyermekfolyamatok korlátozása” lehetőséget. + 3 · Számítógépes ADB (végső megoldás) + Csak ha a vezeték nélküli hibakeresés nem elérhető. + 3 · Számítógépes ADB (végső megoldás) + Mikor jelennek meg az érintésvezérlők egy munkamenetben + Szín + A képernyőn megjelenő gombok árnyalata + Egyéni + Átlátszatlanság + %1$d%% + Gombméret + A 100%% az alapértelmezett méret + Kar kattintása + Koppintson duplán, és tartsa lenyomva a kart az L3 vagy R3 érdekében + Adaptív karok + Játékvibráció + Steam gomb + QAM gomb + Billentyűzet gomb + Nyomja meg a Select gombot a Steam-menü megnyitásához + A View/Select helyett. + Animációk + A karok ott jelennek meg, ahol megérinti őket, és elengedéskor elrejtőznek + Elrendezés + Egyéni pozíciók mentve + Ehhez a képernyőhöz illesztve + Szerkesztés + Visszaállítás + Gombkiosztás + Minden gomb a saját bemenetét küldi + %1$d/%2$d gomb átkiosztva + Beállítás + Kontroller visszaállítása + Visszaállítja az összes kontrollerbeállítást és az elrendezést + Mit küld a játéknak az egyes képernyőgombok. A Rejtett eltávolítja a gombot. + Képernyőgombok + Alapértelmezések + Kiosztás visszaállítása + Android-alkalmazások + %1$d alkalmazás + Nem található indítható Android-alkalmazás. + ● Az Asztal telepítve + Az első megnyitáskor települ + Telepíthető + Nincs telepítve. + %1$s · nincs telepítve + %1$s megnyitása + Játékok + Válasszon ROMs mappát. + Tallózzon a /root/ROMs mappához a RetroArchban. + Adjon hozzá %1$s játékokat ide: ROMs/%2$s. + %1$s telepítése + Állítsa le a munkamenetet a telepítéshez + Futtatókörnyezet szükséges + Telepítési részletek betöltése… + A telepítési részletek nem érhetők el. Próbálja újra később. + Előbb telepítse a Linux futtatókörnyezetet a Beállításból. + Állítsa le a futó munkamenetet az asztali alkalmazások telepítéséhez. + A %1$s-t telepíti a Linux-asztalba. + %1$s · %2$d%% + Az elrejtés eltávolítja az Asztalról. A fájljai megmaradnak a Linux futtatókörnyezetben. + Ez a játék már nincs a ROMs mappában. + Asztal · %1$s + Indítás ebben: %1$s + Ebben is: %1$s + Ez az oldal már nincs meg. Válasszon egy szakaszt a bal oldalon. + Asztal megnyitása + Asztal telepítése és megnyitása + Asztal eltávolítása + Eltávolítja az asztalt és a hozzá tartozó alkalmazásokat. A játékok, a hozzáadott alkalmazások, a beállítások és a megosztott könyvtárak megmaradnak. + Asztal eltávolítása + Asztal eltávolítva + Régi LXQt-fájlok eltávolítása + Frissítse a DroidDecket, mielőtt eltávolítja ezt az asztali verziót. + Linux-asztal + KDE Plasma, Firefox és az emulátorai + A Steamben telepített játékok a munkamenet után itt jelennek meg. + Steam elindítása + Indítási beállítások + Indítás + Játékfájlok + Proton prefix + Mentések keresése… + Nem található mentésmappa név szerint. Az exportálás a teljes felhasználói mappát veszi. + %1$d mentésmappa + %1$s · %2$d fájl · %3$s + %1$d KB + Mentések kezelése + Játékmentések + Mentések importálása… + Exportálás GameHub zipként… + Exportálás Winlator zipként… + Mentésmappa megnyitása + Még nem játszott + Futtatókörnyezet dolgozik · %1$d%% + Futtatókörnyezet dolgozik + A futtatókörnyezet az első Játék gombnyomáskor települ + Elérhető futtatókörnyezet-frissítés + ● A futtatókörnyezet kész + Utoljára játszva: %1$s + Belső tárhely + Hozzáadott játék + Egyéni játék + Játéktárhely + A legjobb páros ehhez a GPU-hoz, naprakészen tartva + Válasszon párost saját kezűleg + Illesztőprogramok keresése… + Még nincs felsorolt illesztőprogram. Frissítsen a kereséshez. + Más GPU-k illesztőprogramjainak elrejtése + Más GPU-k illesztőprogramjainak megjelenítése + Speciális + A megjelenítő-illesztőprogrammal együtt beállítva: a %1$s mindkettőt tartalmazza. + A futtatókörnyezet-illesztőprogrammal együtt beállítva: a %1$s mindkettőt tartalmazza. + A játékok és az asztal használja. + Futtatókörnyezet alapértelmezése + A képkockákat rajzolja a képernyőre. A DroidDeck újraindítása után lép érvénybe. + Automatikus – GPU szerint kiválasztva + Letöltés, importálás vagy törlés… + HASZNÁLATBAN + A DroidDeckbe épített illesztőprogramok + A futtatókörnyezet- és megjelenítő-illesztőprogramok együtt, párként beállítva. + Ehhez a GPU-hoz ajánlott: %1$s. + Frissítsen az ehhez a GPU-hoz tartozó illesztőprogramok kereséséhez. + AJÁNLOTT + Nem elérhető + Linux futtatókörnyezet + A Linux futtatókörnyezet előkészítése + Munkamenet + Kijelző és hang indítása + Steam-kliens + A Steam és kompatibilitási eszközeinek ellenőrzése + Steam megnyitása + Várakozás a Steam első képkockájára + Asztal + Az asztal előkészítése + Megnyitás + Várakozás az első képkockára + Steam indítása + A munkamenet véget ért + %1$d. lépés a %2$d-ből + A Steam szünetel + Fogók + Bal panel + Jobb panel + Mindkettő + Kontroller + Kar + Érintőpad + Váltás a kar és az érintőpad között + További Deck-vezérlők + PANELEK + Második képernyős vezérlők bezárása + Steam-menü megnyitása + Bal · felső + Bal · alsó + Jobb · felső + Jobb · alsó + Kattintás + Jobb érintőpad + Bal érintőpad + Érintőpad + Billentyűzet megjelenítése + Billentyűzet elrejtése + Linux terminál + A Linux-munkamenet még nem áll készen a terminálra. + Steam + Játékok + Asztal + Áruház + Összetevők + Alkalmazások + Beállítás + Frissítések + %1$d%% + %1$s folytatása + Minden munkamenetben /root/ROMs néven jelenik meg. A belső tárhely itt található: /root/Storage. + Nincs mappa kiválasztva + A következő munkamenettől érvényes. SD-kártyák is működnek. + Mappa kiválasztása + Mappa módosítása + Telepítsen egy ARM64-es Proton-buildet, majd válassza ki játékonként itt: Steam → Tulajdonságok → Kompatibilitás. + Állítsa le a futó munkamenetet egy kompatibilitási eszköz telepítéséhez vagy eltávolításához. + Elérhető buildek + %1$s telepítve + Egy korábbi kérésből függőben + Nincs telepítve + Elérhető frissítés → %1$s + Régi %1$s verziók eltávolítása + Eltávolítja: %1$s. Az ezekre a verziókra beállított játékokat másik Protonra kell állítani. + Az erre a verzióra beállított játékokat másik Protonra kell állítani. + Telepítés most + Várakozó megszakítása + Android billentyűzet + Elrejtés ✕ + %1$s indítása + Válasszon képernyőt. Másodlagos: %1$s. + A másodlagos kijelző már nem elérhető. + Elsődleges képernyő + Másodlagos képernyő + Előző lap + Következő lap + Be + Összes (%1$d) + Nincs + %1$d/%2$d + Ki + Alsó képernyő + A következő játékindítástól érvényes. A játék profilja felülírja a közöset. + Másik játék alkalmazásazonosító szerint… + Kész + Alapértelmezett + Ki + Módosítás… + Változó módosítása + Visszaállítás alapértelmezettre + Proton-játékok + Kép a képben + Automatikus belépés a kép a képben módba + Vezérlők a kinyitott képernyőn + Kinyitva a játék a Steam Deck érintőpadjai és hátsó fogói fölött jelenik meg. Összecsukva a játék teljes képernyős. + Felfüggesztve + A kép a képben mód csak a munkamenet elkészülése után érhető el. + Engedélyezze a kép a képben módot a DroidDeck számára az Android beállításaiban. + Az Android nem tudta megnyitni a kép a képben módot. + Parancsikon + Hozzáadás a kezdőképernyőhöz + Játék kiválasztása + Telepített játékok keresése… + Indítási link másolása + Indítási link másolva + Ez az indítóalkalmazás nem tud parancsikont hozzáadni. Használja helyette az „Indítási link másolása” lehetőséget. + Érvénytelen játékindítási link. + Ez a játék már nincs a DroidDeck könyvtárában. + Állítsa le a jelenlegi munkamenetet, mielőtt elindít egy Steam-játékot. + Nem sikerült elküldeni a játékot a Steamnek. Próbálja újra elindítani. + Eltávolítás újra + Eltávolítás + Az eltávolítás nem teljes + Játékfájl exportálása + Minden játékfájl szinkronizálása mappába + Játékfájlok szinkronizálásának leállítása + Mappa kiválasztása a frontend játékfájlokhoz + A játékfájlok mentve: %1$s + Frontend-fájlok + + Várja meg a futtatókörnyezet-művelet befejezését + Várja meg a kompatibilitási eszköz telepítésének befejezését + Várja meg az asztali alkalmazás telepítésének befejezését + Várja meg a telepítés befejezését, mielőtt elindít egy játékot + Előbb zárja be a Steam-munkamenetet, hogy a játék ne írhassa felül az importált mentést + Importálás ide: %1$s + + %1$d fájl importálva innen: %2$s, ide: %3$s + %1$d fájl importálva innen: %2$s, ide: %3$s + + + %1$d fájl importálva innen: %2$s, ide: %3$s. A régi mentések mentve itt: Download/DroidDeck/Saves/backups + %1$d fájl importálva innen: %2$s, ide: %3$s. A régi mentések mentve itt: Download/DroidDeck/Saves/backups + + Válasszon mentés zip fájlt ehhez: %1$s + %1$s exportálása + + %1$d fájl exportálva %2$s formátumban: %3$s + %1$d fájl exportálva %2$s formátumban: %3$s + + Válassza ki, hová mentse: %1$s (%2$s) + %1$s sikertelen: %2$s + Még nincs munkamenet-napló: indítson el előbb egy munkamenetet. + Nincs törölhető munkamenet-napló. + + %1$d munkamenet-naplómappa törölve. + %1$d munkamenet-naplómappa törölve. + + A vezeték nélküli hibakeresés párosítása sikertelen + A vezeték nélküli hibakeresés parancsa sikertelen + A vezeték nélküli hibakeresés nem áll rendelkezésre + ADB-parancs másolva + ROMs mappa kiválasztása + Nem Adreno GPU + A Turnip Adreno GPU-kat támogat. %1$s esetén a Steam fekete képernyőt jeleníthet meg. Letöltés: %2$.0f MB. + Telepítés mindenképpen + Linux futtatókörnyezet eltávolítása + Ez törli a futtatókörnyezetet, a benne lévő Steam-klienst és az összes ott telepített játékot. + Alkalmazva: %1$s + Nem sikerült megnyitni a %1$s alkalmazást az elsődleges képernyőn + Nem sikerült megnyitni a %1$s alkalmazást itt: %2$s + Nem sikerült megnyitni a %1$s alkalmazást a %2$d. kijelzőn + Importálás + Csere + Visszaállítás + Megszakítás + Törlés + nem lehet beolvasni a fájlt + %1$s importálva + A várakozó csere megszakítva. + Válasszon összetevőcsomagot (-linux .wcp) + A Nightlies nem volt elérhető + %1$s tárolva + Letöltés sikertelen: %1$s + Válasszon Linux futtatókörnyezet-illesztőprogramot (-Linux zip vagy Android + Linux csomag) + Válasszon megjelenítő-illesztőprogramot (AdrenoTools zip vagy Android + Linux csomag) + Válasszon illesztőprogram zip fájlt vagy Android + Linux csomagot + %1$s: %2$s + %1$s eltávolítása… + %1$s elfelejtése… + Játéktárhely-mappa kiválasztása + Válasszon Decky-bővítmény ZIP fájlt + Nem használható: %1$s + Program + Linux futtatókörnyezet eltávolítása + Nem sikerült megnyitni a Beállításokat az alsó képernyőn; a fő képernyőn nyílt meg. + Fejlesztői beállítások megnyitása a Beállításokban + + Nem sikerült megnyitni az Android „Ismeretlen alkalmazások telepítése” beállítását + Nem sikerült ellenőrizni: %1$s + Ez a build nem rendelkezik letöltéssel a DroidDeck ezen másolatához + Előbb állítsa le a futó munkamenetet, majd frissítsen + Letöltés + Telepítés + Megerősítésre várakozás + A frissítés sikertelen + Az Android nem ajánlotta fel a frissítés telepítését + + A Decky telepítése sikertelen: %1$s + Bővítmény importálásának indítása… + A bővítmény importálása sikertelen: %1$s + A bővítmény telepítve. Indítsa újra a Steamet a betöltéséhez. + + Még nincs ellenőrizve – koppintson a frissítésre az új illesztőprogramok kereséséhez + Android + Linux: ezzel együtt beállítja a megjelenítő-illesztőprogramot is + Android + Linux: ezzel együtt beállítja a futtatókörnyezet illesztőprogramját is + a futtatókörnyezetbe épített Turnip + rendszer Vulkan: nincs csomagolt build ehhez a GPU-hoz + %1$s (csomagolt) + Manuális + %1$s · %2$s + Jelenleg csak az egyik fele van kiadva + %1$.0f MB mindkettőhöz + Nincs beállítható illesztőprogram: %1$s + A legújabb illesztőprogramok keresése… + Naprakész (nem sikerült ellenőrizni: %1$s) + Az ajánlott illesztőprogramok jelenleg nincsenek kiadva + %1$s %2$s letöltése… + Frissítve erre: %1$s %2$s. + Frissítve erre: %1$s %2$s. A megjelenítő-illesztőprogram a DroidDeck újraindítása után lép érvénybe. + Átváltva erre: %1$s %2$s. + Átváltva erre: %1$s %2$s. A megjelenítő-illesztőprogram a DroidDeck újraindítása után lép érvénybe. + %1$s %2$s használatban. + %1$s %2$s használatban. A megjelenítő-illesztőprogram a DroidDeck újraindítása után lép érvénybe. + Nem illesztőprogram zip: nem AdrenoTools-illesztőprogram, nem -Linux Turnip és nem Android + Linux csomag + Importálás sikertelen: %1$s + Illesztőprogram importálva + %1$s használatban mind futtatókörnyezet-, mind megjelenítő-illesztőprogramként. + %1$s használatban mind futtatókörnyezet-, mind megjelenítő-illesztőprogramként. A megjelenítő-illesztőprogram a DroidDeck újraindítása után lép érvénybe. + %1$s törölve (mindkét illesztőprogram) + %1$s törölve + Legújabb: %1$s · ellenőrizve: %2$s + Legújabb: %1$s · %2$s nem elérhető · ellenőrizve: %3$s + + %1$d perce + %1$d perce + + + %1$d órája + %1$d órája + + + %1$d napja + %1$d napja + + A Banners-Turnip, a WinNative és a DroidDeck ellenőrzése… + ellenőrzés sikertelen + %1$s telepítve – válassza ki bármelyik listában mindkét illesztőprogram beállításához + %1$s telepítve – válassza ki a menüben + + Várja meg a futtatókörnyezet eltávolításának befejezését + Játék + érintőpad + közvetlen + A csere sikertelen: %1$s + A profil mentése sikertelen: %1$s + Az asztal szünetel + %1$s szünetel + A program szünetel + Az asztal indítása + %1$s indítása + A program indítása + Nincs napló ehhez a munkamenethez. + Nem sikerült létrehozni a munkamenet-napló archívumot. + Nem sikerült megosztani a munkamenet-naplókat. + A munkamenet indítása… + a Linux futtatókörnyezet letöltése + a Linux futtatókörnyezet letöltése · %1$d/%2$d MB + a Linux futtatókörnyezet ellenőrzése + a Linux futtatókörnyezet kicsomagolása + az asztal letöltése + az asztal letöltése · %1$d/%2$d MB + az asztal ellenőrzése + az asztal kicsomagolása + a Proton Experimental (ARM64) letöltése + a Proton Experimental (ARM64) letöltése · %1$d/%2$d MB + a Proton Experimental (ARM64) ellenőrzése + a Proton Experimental (ARM64) kicsomagolása + Nem sikerült elérni a futtatókörnyezet-katalógust. Ellenőrizze a kapcsolatot, és nyomja meg újra a Játék gombot. + A Linux futtatókörnyezet telepítése sikertelen. Ellenőrizze a futtatókörnyezetet a Beállításban, és nyomja meg újra a Játék gombot. + Az asztal telepítése sikertelen (%1$s). Ellenőrizze a kapcsolatot, és nyomja meg újra az Asztal gombot. + %1$s váratlanul leállt. Ossza meg a naplókat egy hibajelentésben, vagy próbálja újra. + A program váratlanul leállt. Ossza meg a naplókat egy hibajelentésben, vagy próbálja újra. + Az asztal váratlanul leállt. Ossza meg a naplókat egy hibajelentésben, vagy próbálja újra. + Kilépési állapot: %1$d · %2$s + nincs napló + + A napló %1$d „Function not implemented” hibát tartalmaz: a proot seccomp-gyorsítása elutasít egy segédfolyamatot ezen az eszközön. Próbálja ki ezt: %2$s → „%3$s” (vagy „%4$s”), majd indítsa el újra. + A napló %1$d „Function not implemented” hibát tartalmaz: a proot seccomp-gyorsítása elutasít egy segédfolyamatot ezen az eszközön. Próbálja ki ezt: %2$s → „%3$s” (vagy „%4$s”), majd indítsa el újra. + + + Bővítmények + Telepítsen egy bővítmény ZIP-et a következő Steam-munkamenethez. + Letöltés… + Telepítés ZIP-ből + Játékok + Hang és videó + Grafika + Internet + Iroda + Fejlesztés + Oktatás + Tudomány + Rendszer + Segédprogramok + Népszerű + Felkapott + Új + Frissített + betöltés… + Flatpak beállítása + %1$s: %2$s + Nem sikerült ellenőrizni a frissítéseket a Flathubon + telepítve + mappa + Grafit + Grafitszínű felületek, a logó saját gömbkékjével minden kijelölt, fókuszált vagy lenyomott elemhez. + Papír feketén + A fehér az elsődleges, az ikon kékje jelzi a vonalakat, köröket és pontokat. + Ikonkék + Fekete alap, fehér betűk, a kék minden kijelölt, fókuszált vagy lenyomott elemhez. + Elektromos tengerészkék + Fényesebb kék, szürke helyett tengerészkék felületeken. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Előbb telepítse a futtatókörnyezetet + A Steam-könyvtár nem elérhető + A korábbi SD-könyvtár nem elérhető + A játéktárhely nem elérhető + A hozzáadott játékok mappája nem elérhető + A Steam-manifesztek nem elérhetők + Sok rendszer + Belső + Linux-rendszer + SD-kártya + Tárhely + Kiválasztás + Tulajdonságok + Hozzáadás a Kedvencekhez + Eltávolítás a Kedvencekből + Átnevezés + Másolás + Kivágás + Műveletek + Kedvencek + Jelenlegi mappa rögzítése + Még nincs kedvenc – rögzítsen egy mappát a ⋮ menüjével, hogy gyorsan visszaugorhasson ide. + Eltávolítás a kedvencekből + 0 bájt + bájt + Hely + Típus + Méret + Módosítva + Mappa + Fájl + %1$s fájl + Csak olvasható + Megakadályozza, hogy a játékok (és a Wine) felülírják vagy töröljék ezt a fájlt. + Nem sikerült módosítani a Csak olvasható állapotot + Rejtett + Ez a tárhely nem tudja tárolni a rejtett jelzőt. + Rejtettként jelöli meg a fájlt Windows alatt (a Wine rejtett attribútuma). + Nem sikerült módosítani a Rejtett állapotot ezen a tárhelyen + Kész + Törlés… + A törlés sikertelen + Áthelyezés… + Másolás… + Áthelyezés – %1$s + Másolás – %1$s + Áthelyezés %1$d/%2$d – %3$s + Másolás %1$d/%2$d – %3$s + %1$d kész, %2$d sikertelen + %1$d kész, %2$d kihagyva + + %1$d elem áthelyezve + %1$d elem áthelyezve + + + %1$d elem másolva + %1$d elem másolva + + „%1$s” már létezik + Átnevezés… + Az átnevezés sikertelen + Mappa létrehozása… + Nem sikerült létrehozni a mappát + Új mappa + Mappa neve + Létrehozás + Új név + Törli? + Véglegesen törli: „%1$s”? + + Törli %1$d elemet? + Törli %1$d elemet? + + Ez nem vonható vissza. + + …és még %1$d + …és még %1$d + + Törlés %1$d/%2$d – %3$s + + %1$d elemet nem sikerült törölni + %1$d elemet nem sikerült törölni + + Az Összefésülés hozzáadja és lecseréli a fájlokat a meglévő mappában. + A Felülírás lecseréli a meglévő fájlt. + Alkalmazás minden ütközésre + Összefésülés + Felülírás + Mindkettő megtartása + Kihagyás + Ennek a mappának a kiválasztása + Vissza + A %1$s csatolva van, de jelenleg nem olvasható + Listanézet + Rácsnézet + Keresés + Rendezés + Név + Módosítás dátuma + Kényelmes sorok + Kompakt sorok + Rejtett fájlok elrejtése + Rejtett fájlok megjelenítése + Kedvencek elrejtése + Kedvencek megjelenítése + Ennek a mappának a szűrése + %1$s szabad + %1$d kiválasztva + Nincs + Összes + Beillesztés + + %1$d elem + %1$d elem + + %1$s beillesztése ide + %1$s beillesztése (áthelyezés) ide + Fájlok + TÁRHELY + GYORS + KEDVENCEK + Letöltések + Letöltés + ROMs + Munkamenet-naplók + Képek + „%1$s” hozzáadva a Kedvencekhez + „%1$s” eltávolítva a Kedvencekből + Üres mappa + Egy szinttel feljebb + Válasszon mappát + Válasszon fájlt + Steam kék + Égszínkék + Fehér + Lila + Zöld + Borostyán + Piros + Rózsaszín + A gomb + B gomb + X gomb + Y gomb + Bal bumper + Jobb bumper + Bal ravasz + Jobb ravasz + View gomb + Menu gomb + View + Menu + D-pad fel + D-pad le + D-pad balra + D-pad jobbra + Rejtett + Húzza a mozgatáshoz + Rögzítés + Mentés + Nincs + Billentyűzet + érintőpad + Terminál + Deck fogók + érintőpadok + + Linux futtatókörnyezet eltávolítása + + Linux futtatókörnyezet eltávolítása · %1$d bejegyzés + Linux futtatókörnyezet eltávolítása · %1$d bejegyzés + + Linux futtatókörnyezet eltávolítva + Nem sikerült befejezni a Linux futtatókörnyezet eltávolítását. Próbálja újra a Beállításban. + Futtatókörnyezet-maradványok eltávolítása + Letöltés + Ellenőrzés + Kicsomagolás + Megjelenik, amíg a Linux futtatókörnyezet telepítése vagy eltávolítása zajlik + A Linux futtatókörnyezet telepítése + A Linux futtatókörnyezet eltávolítása + + A Linux futtatókörnyezet nincs telepítve + A katalógusban nincs ellenőrzőösszeg a %1$s-hez + Az ellenőrzőösszeg nem egyezik – semmi nem változott + %1$s telepítése + Nem sikerült elhelyezni az AppImage-et + A kicsomagolás sikertelen + A telepítés sikertelen + + A Flatpak beállítása sikertelen (kilépési kód: %1$d) + A Flatpak beállítása nem fejeződött be + Egy másik áruházi feladat fut + Előbb állítsa be a Flatpakot + Indítás + A Flatpak %1$d állapotkóddal lépett ki + %1$s telepítése + %1$s frissítése + %1$s eltávolítása + %1$s telepítése (%2$d/%3$d) + %1$s frissítése (%2$d/%3$d) + %1$s eltávolítása (%2$d/%3$d) + fordítások + grafikai illesztőprogramok + médiakodekek + %1$s futtatókörnyezet + %1$s futtatókörnyezet %2$s + + A fájl túl kicsi ahhoz, hogy AppImage legyen + A fájl nem olvasható: %1$s + Ez nem egy AppImage (nem Linux-program) + Ez az AppImage sem ARM64-hez (aarch64), sem x86 PC-khez nincs készítve + Ez egy Linux-program, de nem AppImage + Csak 2-es típusú AppImage-ek importálhatók (ez %1$d típusú) + Nincs elég szabad hely: az importáláshoz körülbelül %1$s szükséges + Nincs elég szabad hely: a frissítéshez körülbelül %1$s szükséges + Az importálás sikertelen + Ez az AppImage már nincs meg + A frissítés sikertelen + %1$s kicsomagolása + Az AppImage-et nem sikerült kicsomagolni + Az AppImage-et nem sikerült kicsomagolni: %1$s + + Állítsa le az aktív munkamenetet a Decky Loader telepítése előtt + A Decky Loader letöltése sikertelen + A Decky Loader mérete nem egyezett a kiadás metaadataival + Nem sikerült letölteni a kiadás ellenőrzőösszegét + A kiadás ellenőrzőösszege hiányzik + A kiadás ellenőrzőösszege érvénytelen + A Decky Loader ellenőrzőösszege nem egyezik + A kiadás eszköze nem kompatibilis 64 bites Linux-futtatható + Egy munkamenet elindult a letöltés közben; állítsa le a Decky Loader telepítése előtt + Nem sikerült futtathatóvá tenni a PluginLoadert + Nem sikerült bekapcsolni a Steam CEF távoli hibakeresését + Nem sikerült előkészíteni a PluginLoadert + Nem sikerült megőrizni a meglévő PluginLoadert + Nem sikerült aktiválni az új PluginLoadert + Állítsa le az aktív munkamenetet egy bővítmény telepítése előtt + Telepítse a Decky Loadert egy bővítmény importálása előtt + Nem sikerült beolvasni a kiválasztott ZIP fájlt + Nem sikerült létrehozni a Decky bővítménymappát + Bővítmény ZIP kicsomagolása + A bővítmény ZIP-nek egy legfelső szintű bővítménymappát kell tartalmaznia + A bővítmény ZIP-ből hiányzik a plugin.json, a package.json vagy a dist/index.js + A bővítmény neve hiányzik + Egy munkamenet elindult a telepítés közben; állítsa le egy bővítmény telepítése előtt + A bővítmény telepítve · indítsa újra a Steamet a betöltéséhez + A bővítmény ZIP telepítése sikertelen + A bővítmény ZIP túl sok bejegyzést tartalmaz + A bővítmény ZIP érvénytelen elérési utat tartalmaz + A bővítmény ZIP duplikált elérési utakat tartalmaz + A bővítmény ZIP nem biztonságos elérési utat tartalmaz + Nem sikerült létrehozni a bővítménymappát + A bővítmény ZIP kicsomagolva túllépi az 1 GiB-os korlátot + Nem sikerült futtathatóvá tenni a csomagolt bővítménybinárist + Érvénytelen remote_binary bejegyzés + Érvénytelen távoli bináris neve + A távoli bináris URL-jének HTTPS-t kell használnia + A %1$s távoli bináris SHA-256 hash-e érvénytelen + Nem sikerült létrehozni a bővítmény bin mappáját + Bővítménybináris letöltése · %1$s + Nem sikerült letölteni a %1$s bővítménybinárist + A bővítménybináris ellenőrzőösszege nem egyezik · %1$s + Nem sikerült futtathatóvá tenni a bővítménybinárist · %1$s + Nem sikerült lecserélni a bővítménybinárist · %1$s + Nem sikerült telepíteni a bővítménybinárist · %1$s + A bővítménymappa egy másik bővítményhez tartozik + Nem sikerült megőrizni a meglévő bővítményt + Nem sikerült aktiválni az importált bővítményt + Nem sikerült frissíteni a Decky bővítménybeállításokat + Nem sikerült visszaállítani a meglévő bővítményt + + Nem sikerült menteni a vezeték nélküli hibakeresés azonosítóját + Az ADB-parancs sikertelen (%1$d) + Az Android nem erősítette meg a gyermekfolyamat-korlát módosítását: %1$s + Nincs mentett vezeték nélküli hibakeresés-párosítás + Adja meg a vezeték nélküli hibakeresés jelenlegi IP-címét és portját + + Időtúllépés a párosítási felugró ablakra várva. Próbálja újra a DroidDeckből. + Nem sikerült megkeresni a párosítási felugró ablakot (%1$d). Ellenőrizze, hogy a Wi-Fi be van kapcsolva. + Nem sikerült megkeresni a párosítási felugró ablakot: %1$s + Írja be a kód mind a 6 számjegyét + Párosítás… + Ez a kód nem működött. Nyissa meg újra a párosítási felugró ablakot, és írja be az új kódját. + Párosítva. Csatlakozás… + Párosítva, de a vezeték nélküli hibakeresés portja nem található. Hagyja bekapcsolva a vezeték nélküli hibakeresést, és próbálja újra. + A beállítás alkalmazása… + A vezeték nélküli hibakeresés parancsa sikertelen + Vezeték nélküli hibakeresés beállítása + A vezeték nélküli hibakeresés párosítási kódját kéri, amíg a Beállítások meg van nyitva + Vezeték nélküli hibakeresés párosítása + Kapcsolja be a vezeték nélküli hibakeresést, majd koppintson az „Eszköz párosítása párosítási kóddal” lehetőségre. + Kapcsolja be a vezeték nélküli hibakeresést, majd koppintson az „Eszköz párosítása párosítási kóddal” lehetőségre. Maradjon a Beállításokban: a kód ebbe az értesítésbe kerül. + 6 számjegyű párosítási kód + Koppintson a Kód megadására, és írja be a Beállításokban megjelenő 6 számjegyet. + Adja meg a Wi-Fi párosítási kódot + Próbálja meg újra a kódot + Kód megadása + A Steam beállítása + A Steam készen áll + Koppintson a DroidDeckhez való visszatéréshez. A vezeték nélküli hibakeresést most kikapcsolhatja. + A vezeték nélküli hibakeresés beállítása megállt + + A letöltés HTTP %1$d választ adott + A letöltés mérete nem egyezett a közzétett APK-val, és elvetésre került – próbálja újra + A letöltés nem egyezett az ellenőrzőösszegével, és elvetésre került – próbálja újra + A frissítés a %1$s Android-csomaghoz tartozik, nem a %2$s-hez + A frissítés metaadatai nem nevezik meg a DroidDeck kiadási aláírókulcsát + Az Android nem tudta beolvasni a letöltött APK-t + A letöltött APK a %1$s-hez tartozik, nem a %2$s-hez + A letöltött APK %1$d versionCode-ot ad meg, nem a közzétett %2$d-t + Az Android nem telepíti a %1$d versionCode-ot a telepített %2$d fölé + A letöltött APK nem tartalmaz aláírási információt + A letöltött APK nincs aláírva a DroidDeck kiadási kulcsával + A frissítési katalógus HTTP %1$d választ adott + + A munkamenet indítása… + %1$d:%2$02d eltelt · még dolgozik + A Steam-kliens frissítésének letöltése · %1$d%% + A Steam-kliens letöltése · %1$s + + Már fut egy kompatibilitási eszköz telepítése + Állítsa le az aktív munkamenetet a kompatibilitási eszközök telepítése előtt + Nem található ARM64-es kiadás a %1$s-hez + Legalább 4 GB szabad hely, plusz a letöltés mérete szükséges a %1$s telepítéséhez + %1$s %2$s letöltése + A letöltés újrapróbálása · %1$d. próbálkozás a %2$d-ből + A letöltés megszakadt + A letöltés sikertelen; a részleges fájl megmarad, hogy egy újrapróbálkozás folytathassa + A letöltés túllépte a kiadás méretét, és törlésre került + A letöltés befejezetlen volt; a részleges fájl megmarad egy folytatható újrapróbálkozáshoz + A letöltés ellenőrzése + Nem sikerült beolvasni a kiadás ellenőrzőösszegét; próbálja újra + A %1$s %2$s nem közöl ellenőrzőösszeget; semmi nem lett telepítve + Az ellenőrzőösszeg nem egyezik; a letöltés törlésre került + Egy munkamenet elindult a letöltés közben; állítsa le a kompatibilitási eszközök telepítése előtt + %1$s telepítése + A %1$s telepítése sikertelen (kilépési kód: %2$d) + Nem sikerült telepíteni a %1$s-t + droiddeck-esync csomag letöltése + + Linux segédprogramokkal + Csak DLL-ek + Eredeti (a Proton frissült az utolsó csere óta) + Az Összetevőkön kívül módosítva + nincs jelen + Eredeti %1$s + Eredeti innen: %1$s + A %1$s %2$s a %3$s-be kerül, amint a rajta futó játék bezárul. + A %1$s %2$s most a %3$s-ben van. A következő játékindítástól érvényes. + Az az eredeti csomag nincs tárolva + A %2$s %1$s-e visszaáll az eredetire, amint a rajta futó játék bezárul. + A %2$s %1$s-e visszaállítva a %3$s eredetijéből. + A %1$s használatban van itt: %2$s. Előbb cserélje ki azt a Protont valami másra. + A %1$s nincs tárolva + %1$s törölve. + A telepített build eredetije megmarad: ez a visszaút. + Nincs tárolva + A %2$s %1$s eredetije törölve. + A letöltések csak a Nightlies kiadásokból érkeznek + Ez a csomaglista régebbi, mint az ellenőrzőösszegek – frissítse, és próbálja újra + Az ellenőrzőösszeg nem egyezik + + Lineáris + Legközelebbi + Snapdragon GSR (minőség) + Csak élesítés + Automatikus (illeszkedik a méretezéshez) + + Semmi nincs alkalmazva. + Játéktisztaság + Élesítés egy csipetnyi kontraszttal – a mindennapi. + Élénk + Erősen fokozott szín. Csíkmentesítés bekapcsolva, hogy az égbolt ne csíkozódjon. + Filmszerű + Mélyebb kontraszt, hidegebb szín, simított élek. + Versenyzős + Felemelt árnyékok és erős élesítés – vegye észre először az embereket. + Adaptív élesítés + Csak élesítés, a színek érintetlenek. + Filmes + Filmszerű görbe, enyhén tompított. + Arcade + Hangos és ütős. + Retro CRT + A CRT-képernyő és az analóg jel átmenetei együtt. + Felskálázó élesítés + A panel felbontása alatti futtatáshoz. + Pixeltiszta + Éles pixelek 2D-s és régi játékokhoz. + Anime élek + Rajzfilmes körvonalak plusz élesítés. + + Nem Adreno GPU + Támogatott + Kísérleti: az illesztőprogramjai teszt buildek + A tesztelt hardver alatt (Adreno 725 és újabb): lehet, hogy nem fut + Nem támogatott: a DroidDecknek Adreno (Snapdragon) GPU szükséges + + A GitHub korlátja elérve – próbálja újra később + A GitHub HTTP %1$d választ adott + a letöltés HTTP %1$d választ adott + Ez az illesztőprogram-lista régebbi, mint az ellenőrzőösszegek – frissítse, és próbálja újra + Az ellenőrzőösszeg nem egyezik – a letöltés elvetve + Nem egy AdrenoTools illesztőprogram: %1$s + nincs meta.json + a meta.json nem olvasható (%1$s) + ez egy Linux futtatókörnyezet-illesztőprogram (importálja a „Linux futtatókörnyezet-illesztőprogram” alatt) + ez egy Wayland játékillesztőprogram, amelyet ez az alkalmazás nem tud használni + a meta.json nem nevez meg libraryName-et, így semmit nem lehetett átadni az AdrenoToolsnak + a meta.json a %1$s-t nevezi meg, amely nincs a zipben + a %1$s nem 64 bites AArch64 ELF megosztott könyvtár + a %1$s a glibc-hez linkel – egy „-Linux” Turnip, amely a „Linux futtatókörnyezet-illesztőprogram” alá tartozik + Nincs libvulkan_freedreno*.so ebben a zipben. Egy Android (AdrenoTools) vagy -Wayland Turnip zip nem Linux futtatókörnyezet-illesztőprogram. + A %1$s nem 64 bites AArch64 ELF megosztott könyvtár. + A %1$s nem glibc illesztőprogram – az Android libc-jéhez linkel. A Linux futtatókörnyezethez „-Linux” zip szükséges; egy egyszerű vagy „-Wayland” Turnipet a Steam-kliens egyáltalán nem tudja betölteni. + Nem illesztőprogram-csomag: nincs manifest.json, amely android és linux illesztőprogramot nevez meg + Nem illesztőprogram-csomag: a %1$s rajta kívülre mutat + Nem illesztőprogram-csomag: kicsomagolva több mint %1$d MB + + SD-kártya + Mappa + + Munkamenet-naplók megosztása + + Android 10 vagy újabb szükséges (színtérrel rendelkező megjelenítési rétegek) + ez a kijelző nem jelez HDR-támogatást + ez a kijelző a %1$s-t támogatja, nem a HDR10-et + + ez a GPU + A gyermekfolyamat-korlát be van kapcsolva + A gyermekfolyamat-korlát nincs beállítva + A gyermekfolyamat-korlátot nem sikerült ellenőrizni + A gyermekfolyamat-korlát ki van kapcsolva + A gyermekfolyamat-korlát nem szükséges + %1$s Ha ez nem lehetséges, csatlakoztassa ezt az eszközt egy ADB-vel rendelkező számítógéphez, és futtassa az alábbi parancsot. Térjen vissza ide, és ellenőrizze újra. + A Steam-munkamenetek elindulhatnak. + Ez az Android-verzió nem használja ezt a korlátot. + Kapcsolja be a „Gyermekfolyamat-korlátozások letiltása” lehetőséget a Fejlesztői beállításokban, és utána hagyja bekapcsolva a Fejlesztői beállításokat. + Ehhez az Android-verzióhoz nincs kapcsoló. Használja a vezeték nélküli hibakeresést a módosításához erről az eszközről. + Ehhez a buildhez nincs letöltés a DroidDeck ezen másolatához. + Az Android nem telepíti ezt a buildet fölé, mert a %1$d versionCode-ja a telepített %2$d alatt van. + Az Android nem telepít régebbi verziót egy újabb fölé. Maradjon ezen a buildon, amíg a csatorna felzárkózik. + Ez a DroidDeck-másolat másként van aláírva (talán egy helyi build?), így az Android nem frissíti a helyén. + Nincs elég szabad hely a frissítéshez. + A frissítés megszakítva. + A frissítés nem települt: %1$s + A frissítés nem települt. + %1$s zip + zip + A %1$s még nem lett elindítva + Nem található mentésfájl a %1$s-hez + A %1$s-ben nincs drive_c mappa, így nem egy GameHub vagy Winlator mentés zip + nem lehet mappákat létrehozni itt: %1$s + nem lehet írni ide: %1$s + nem lehet írni ide: %1$s (%2$s) + A mappa nem elérhető + Egy másik fájl már használja a %1$s nevet + Az illusztrációk mappája nem elérhető + A mappában egy nem kapcsolódó szinkronizációs leltár van + Nem sikerült eltávolítani a %1$s-t + A fájl kilép az exportmappából + FEX (verziónév nélküli build) + nincs profile.json a %1$s-ben + A %1$s nem Linux-összetevőcsomag (típus: %2$s) + %1$s · %2$s szabad + Automatikus + A frissítési katalógus nem ment át az ellenőrzéseken (%1$s) + DroidDeck gyermekfolyamat-beállítás + Bejelentkezve · offline mód + Bejelentkezve + CSOMAGOLT + LETÖLTÖTT + IMPORTÁLT + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Munkamenet kijelzője + Felbontás + 16:9 játék illesztése + Hogyan illeszkednek a 16:9 játékok ehhez a kijelzőhöz. A következő munkamenettől. + Válassza a Képernyőhöz illesztést, vagy egy 16:9-nél szűkebb egyéni méretet. + Arányok megtartása + Nyújtás a kijelzőhöz + Képméretezés + Méretezési szűrő + A Steam és az Asztal közösen használja. + Élőben érvénybe lép. A Steam és az Asztal közösen használja. + Az FSR, a GSR és a NIS a munkamenet képének felnagyításakor lép érvénybe. A Csak élesítés nagyítás nélkül is működik. + Szűrőélesség + Képkockasebesség + %1$s A méretezést erre állítja: %2$s. + Képernyőhöz illesztés (%1$d×%2$d) + Képernyő követése (átméreteződik össze- vagy kinyitáskor) + Steam + Eszközrészletek megjelenítése + Eszközrészletek elrejtése + Az eszköz beolvasása… + Másolás + Másolja ezeket a részleteket egy hibajelentésbe. + Másolva. + SSBS kényszerítése + Bekapcsolva tartja azt a CPU-sebesség-beállítást, amelyet a Wine folyton kikapcsol. Gyorsíthatja a Windows-os játékokat Snapdragon 8 Elite-n; régebbi chipeken alig van hatása. A következő játékindítástól érvényes. + + Áruházak + Steam + Egyéni + Áruházak + Áruház megnyitása itt + Letöltési sebességi szint + Áruházmotor + ● A motor kész + Nincs buildelve + Kiegyensúlyozott + Gyors + Max + Áruházak + Áruházbeállítások + Beállítások + (nincs bejelentkezve) + Áruház + Telepítve + Könyvtár + Összes + Letöltések + %1$s (%2$d) + Bejelentkezés + %1$s · kijelentkezés + Kijelentkezik a %1$s-ből? + A telepített játékok megmaradnak. A könyvtár újra lekérésre kerül, amikor újra bejelentkezik. + Bejelentkezve marad + Kijelentkezés + Keresés a %1$s katalógusban… + Keresés a telepített játékok között… + Keresés a könyvtárában… + Keresés + Nincs telepítve semmi + Betöltés + Nincs játék + Az áruház betöltése… + Nincs katalógus + Újdonságok + Akciók + Ingyenes játékok + Felkapott + A könyvtára + Játék + Telepítés + Telepítés folytatása + Megszakítva + Telepítés · %1$s + Ingyenes beszerzés + Megtekintés itt: %1$s + Megvétel · %1$s + Telepítve + A könyvtárban + Ingyenes + Folytatás + Frissítés + Felnőtt tartalom megjelenítése + Epic + EOS + Offline + Ki + Epic bejelentkezés (EOS) + Indítás offline + Epic bejelentkezés megoldása + Felhőmentések + Feltöltés a felhőbe + Letöltés a felhőből + Nincs felhőmentés + Felhőütközés + Felhőütközés · %1$d fájl + … + Felhő megtartása + Helyi megtartása + Felhő ellenőrzése… + Utolsó szinkronizálás: %1$s + Még nincs szinkronizálva + %1$d fájl + telepítve + a könyvtárában + áruház + Eredmények + %1$d / %2$d feloldva + %1$d / %2$d + Feloldva · %1$d + Zárolva · %1$d + Rejtett · %1$d + Rejtett eredmény + A részletek a feloldás után jelennek meg. + Minden eredmény feloldva + Betöltés… + Jelentkezz be a %1$s fiókodba az eredményeid megtekintéséhez. + Nem sikerült elérni a %1$s szolgáltatást. Nézd meg újra, ha online vagy. + A %1$s nem sorol fel eredményt ehhez a játékhoz. + Mind a %1$d megjelenítése + Eredmények · %1$s + ✓ A Steamben + Eltávolítás + Eltávolítás… + Eltávolítja a %1$s-t? + %1$s szabadít fel, és eltávolítja a Steamből. + Eltávolítja a fájljait és a Steam-parancsikonját. + A Steam a következő indításakor felveszi a játékot; indítsa el onnan, vagy indítsa el most a Steamet. + Proton + A Steam választása játékonként + Hang + DirectAudio + Összetevők + FEX, DXVK, VKD3D + Motor + %1$s · natív + %1$s · beépített + Hogyan fut + Lekérve innen + %1$s, az Ön fiókjával + Telepítve ide + Játéktárhely (Games/%1$s/%2$s) + Indítja + A Steam és a Proton, ugyanúgy, mint a Steam-játékoknál + A Steam és a Proton · Epic bejelentkezés minden indításnál, EOS overlay kikapcsolva + A Steam és a Proton · Amazon Games Services változók beállítva + Steam + a Steamhez adva telepítéskor (Deck felhasználói felület + Steam Input) + Nincs letöltés + Szüneteltetve + Sorban + Sorban · #%1$d + Megszakítva + Sikertelen + Sikertelen · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Szüneteltetés + Folytatás + Megszakítás + Újra + Törlés + Befejezettek törlése + Manifeszt + Letöltés + Ellenőrzés + Telepítés + Kész + Ellenőrzés + Letöltés + Törli a letöltést? + Sikertelen · %1$s megtartva + Ellenőrzés + Telepítés + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d fájl + Letöltéskezelő + Egyszerre futó letöltések + Telepítés ide + %1$s telepítése: + Belső + %1$s szabad + Áruházletöltések + Megjeleníti a játékletöltés folyamatát, és futva tartja a letöltéseket a háttérben + Letöltés + %1$d letöltés + Előkészítés… + Befejezett áruházletöltések + %1$s telepítve + A %1$s letöltése sikertelen + Törlés + Megtartás + Bejelentkezés ide: %1$s + Bejelentkezés a szolgáltatójával + Újratöltés + Bezárás + Próbálja újra + A bejelentkezés befejezése… + Nem sikerült betölteni a bejelentkezési oldalt: %1$s + Ez a szolgáltató nem engedi a bejelentkezést egy beágyazott oldalról. Jelentkezzen be helyette az áruház jelszavával. + A bejelentkezési oldal hibával válaszolt (HTTP %1$d). + A kapcsolat a bejelentkezési oldalhoz nem biztonságos. + A bejelentkezést nem sikerült ellenőrizni. Próbálja újra. + A bejelentkezés sikertelen. Próbálja újra. + Bezárás + Vissza + Szerkesztés + Minden játék hozzáadása ebből a mappából + Nincs semmi új ebben a mappában + Név + Cél + Indítás innen + Indítási beállítások + Illusztráció + Másik exe… + Eltávolítás + Borító + Háttér + Logó + Ikon + Játékmappa + Steam + SteamGridDB + Saját fájl + Az exe-ből + Automatikus + Kép kiválasztása… + Kép kiválasztása: %1$s + Visszaállítás automatikusra + + %1$d játék hozzáadva + %1$d játék hozzáadva + + + %1$d már a Játékokban van + %1$d már a Játékokban van + + B · Vissza: %1$s + a játék + Játék beállítások + Grafikai illesztőprogramok + Ez a Proton + Minden Proton + Minden Protonhoz + Elérhető · %1$d + Ellenőrizve: %1$s + Új összetevők és illesztőprogramok keresése + Beszerzés + %1$s · a lenti DXVK, VKD3D-Proton és FEX ehhez a Protonhoz tartozik + %1$s · tesztelt + %1$s · nem tesztelt + %1$s · nem támogatott + Naprakészen tartja az ajánlott párost + Automatikus · %1$s + Futtatókörnyezet- és megjelenítő-illesztőprogramok külön + Használja a %1$s-t? + Eltávolítja a %1$s-t? + Diagnosztika + Készen áll a játékra + Még nem áll készen a játékra + futtatókörnyezet: %1$s + folyamatkorlát kikapcsolva + bejelentkezve mint %1$s + Amikor szükség van rá + Megjelenés + Ez az eszköz + Könyvtár illusztrációi + LINUX-ALKALMAZÁSOK + Flathub + Asztali alkalmazások a Linux-asztalhoz, az Asztalról megnyitva. + Flathub beállítása + Eltávolítja a %1$s-t? + A beállításai és fájljai megmaradnak. + Megtartás + Méret a tárhelyen + Eltávolítja az asztalt? + Összes + GOG-eredmények + Szinkronizálja az eredményeket a támogatott GOG-játékokhoz. A következő indítástól lép életbe. + Eredmények kikapcsolva + Eredmények bekapcsolva + Jelentkezz be újra a GOG-ba + Eredmények feltöltve + Az eredmény feltöltése függőben + Kapcsold be a GOG-eredményeket a Store-beállításokban + Eredmények nem elérhetők; nézd meg a munkamenet-naplókat + Eredmény feloldva + Beillesztés + Iránykereszt + Bal kar + Jobb kar + Menü + Billentyűzet + Iránykereszt fel + Iránykereszt le + Iránykereszt balra + Iránykereszt jobbra + Érintővezérlők + Érintőkontroller + Egér + Be + Ki + Lassú + Normál + Gyors + Elrendezés szerkesztése + DroidDeck menü + Húzd a vezérlőket a képernyőre + Elrendezés + Elvetés + Mentés… + Ikon + Sz− + Sz+ + M− + M+ + Elrejtés + Szín és átlátszatlanság… + Elrendezés másolása + Elrendezés másolva; illeszd be bármelyik játék érintőelrendezésébe + Másolt elrendezés beillesztése + Visszaállítás a Steam alapértelmezésére + Ehhez a vezérlőhöz nincs hozzárendelés a Steamben, amihez ikont lehetne adni + Ikon ehhez: %1$s + Érintőelrendezés színe + Kész + Az érintőelrendezést nem sikerült elmenteni + Az érintőelrendezés elmentve a Steambe + %1$d. alkalmazás + Felirat (nem kötelező) + A Steam ikonjai nem találhatók + + %1$d ikon a Steamről + %1$d ikon a Steamről + + Ikon színe + Gomb színe + Nincs ikon + + Bejelentkezés ellenőrzése… + Könyvtárad lekérése… + Játéklista lekérése… + Játékadatok betöltése… (%1$d/%2$d) + + %1$d játék szinkronizálása… + %1$d játék szinkronizálása… + + Szinkronizálás… %1$d/%2$d + Borítóképek lekérése… + A szinkronizálás sikertelen + Nem található játék a %1$s könyvtáradban + Az Amazon Games nem válaszolt a könyvtáradra vonatkozóan: frissítsd, vagy jelentkezz be újra + Nem sikerült csatlakozni a GOG-hoz + A GOG-könyvtáradat nem sikerült beolvasni + Regisztráció a Steamnél… + Cardboard VR + Cardboard mód + Bal és jobb szem képe telefonos VR-szemüveghez. + Kép + Virtuális képernyő + Egymás melletti 3D + Szemüveg + Cardboard + Cardboard (2014) + Lencse nélkül + Szemtávolság + Képernyő szélessége + Képernyő távolsága + Fejkövetés + Ezen az eszközön nincs forgásérzékelő. + Nézet középre igazítása + Középre + SteamVR (kísérleti) + Telepítés + Letölti a Valve ARM64-es SteamVR-jét (571 MB). + SteamVR letöltése és kicsomagolása… + Indítás + A SteamVR-t a fejeddel mint headsettel futtatja; a kép egymás melletti 3D-re vált. + Leállítás + A SteamVR fut. + Ebben a munkamenetben nem érhető el. + A SteamVR nem válaszolt; nézd meg a munkamenet naplóját. + Összetevők letöltése + A letöltések közösek az Összetevők oldallal. Fent válassz egy letöltött verziót, hogy a következő játékindítástól azt használd. + Válassz letöltendő verziót + Fejlesztő + Kiadó + Megjelenés + Fő történet + + Extrák + Teljes + %1$s óra + %1$d perc + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml new file mode 100644 index 000000000..7e4049f3c --- /dev/null +++ b/app/src/main/res/values-in/strings.xml @@ -0,0 +1,2210 @@ + + + DroidDeck + + Proses pertama mengunduh Steam. Ini mungkin memerlukan beberapa menit. + Secara default, keluar dari aplikasi akan menjeda Steam; sesi dilanjutkan dari posisi terakhir saat Anda kembali. + Pengontrol Anda akan terlihat sebagai gamepad Xbox 360. Jika tidak, gunakan kontrol di layar. + + + Proses pertama menyiapkan desktop. Ini mungkin memerlukan beberapa menit. + Secara default, keluar dari aplikasi akan menjeda sesi; sesi dilanjutkan dari posisi terakhir saat Anda kembali. + Sentuhan berfungsi seperti touchpad: geser untuk menggerakkan penunjuk, ketuk untuk mengeklik. + Pengontrol Anda akan terlihat sebagai gamepad Xbox 360. Jika tidak, gunakan kontrol di layar. + + Sesi + Ditampilkan saat sesi Steam atau desktop berjalan + Steam berjalan + Unduhan Steam aktif + Sesi dijeda - ketuk Lanjutkan saat siap + Lanjutkan + Jeda sekarang + Hentikan sesi + Pembuatan frame + Pilih mode pembuatan frame + Nonaktif + Win-FG %1$d× + LSFG %1$d× + LSFG Adaptif %1$d + Memerlukan Lossless Scaling di Steam + Instal Lossless Scaling di Steam atau impor Lossless.dll + Impor Lossless.dll… + Menggunakan salinan dari Steam + Menggunakan salinan yang diimpor + Pilih Lossless.dll + Lossless.dll diimpor + Lossless.dll ini sudah digunakan + Lossless Scaling tidak dimiliki di akun Steam ini + File ini bukan Lossless.dll milik Lossless Scaling + Tidak dapat menyiapkan LSFG dari Lossless.dll ini + LSFG memerlukan Lossless Scaling dari Steam atau Lossless.dll yang diimpor + LSFG tidak dapat menyiapkan shader-nya dari Lossless.dll + %1$s: tidak didukung + %1$s: mesin gagal + %1$s: memerlukan Lossless Scaling + %1$s %2$s · memulai + Menambahkan pustaka Steam dan memindainya untuk gim Windows. Gim yang sudah dimiliki akan menggunakan entri Steam-nya; sisanya menjadi pintasan non-Steam. + Lingkungan gim + Variabel lingkungan untuk gim Proton. + Edit + Semua gim Proton + Profil + ID aplikasi Steam + Buka + Gim %1$s + Tambah variabel + Nama variabel + Nilai + Simpan + Hapus + Warisi kembali + Hapus semua penggantian lingkungan + Nonaktifkan untuk profil ini + Belum ada variabel yang diatur. + Diwariskan + Variabel khusus + Tidak dapat memuat atau menyimpan. Periksa penyimpanan kosong dan coba lagi. + Gunakan nama yang valid dan nilai hingga 8192 karakter. + Runtime Proton standar tidak membaca variabel ini. + Memuat… + Override level fitur D3D12. 12_2 melaporkan kemampuan DirectX 12 Ultimate; fitur yang tidak didukung dapat gagal. Biarkan kosong untuk deteksi otomatis. + Override model shader D3D12. 6_9 memerlukan VKD3D yang cukup baru dan fitur GPU yang sesuai. Biarkan kosong untuk deteksi otomatis. + Pertahankan cache shader Mesa tetap aktif untuk mengurangi kompilasi berulang. false mengaktifkan cache. + Ukuran maksimum cache shader Mesa, misalnya 1G. Cache yang lebih besar menggunakan lebih banyak penyimpanan. + Opsi VKD3D untuk D3D12. nodxr menonaktifkan ray tracing; gunakan hanya untuk gim yang membutuhkannya. + Atur ke 1 untuk log diagnostik Proton. Pencatatan log dapat mengurangi performa dan menggunakan penyimpanan. + Atur ke 1 untuk menggunakan WineD3D berbasis OpenGL sebagai pengganti DXVK untuk pengujian kompatibilitas D3D9–11. + Atur ke 1 untuk menjaga gim ini di luar ntsync (droiddeck-ntsync atau driver kernel); gim ini menggunakan droiddeck-esync jika ada paket yang tersedia, jika tidak droiddeck-fsync. + Atur ke 1 untuk menonaktifkan droiddeck-esync bagi gim ini; gim ini menggunakan droiddeck-fsync, atau droiddeck-ntsync jika fsync juga nonaktif. + Atur ke 1 untuk menjaga gim ini di luar droiddeck-fsync; gim ini menggunakan droiddeck-esync jika ada paket yang tersedia, jika tidak droiddeck-ntsync. + Atur ke 1 untuk menjalankan gim ini pada droiddeck-fsync, atau 0 untuk mengutamakan droiddeck-esync jika ada paket yang cocok dengan Proton-nya. + Atur ke 0 untuk menonaktifkan fitur bantu navigasi gamepad milik Proton untuk gim ini. + Overlay DXVK untuk D3D9–11. fps menampilkan frame rate; kosongkan untuk menyembunyikannya. + Setelan DXVK dipisahkan dengan titik koma. Versi terbaru menggunakan dxvk.maxFrameRate untuk membatasi frame rate; gunakan pembatas frame rate sesi untuk permainan biasa. + Batas frame rate D3D12. 0 menonaktifkan batas. + Threading OpenGL Mesa. true dapat membantu gim OpenGL yang terbatas CPU; uji per gim. + Sinkronisasi Wine + Cara gim menjaga thread-nya tetap selaras. Berlaku pada sesi berikutnya. + esync + ntsync + fsync + wineserver + Preset FEX + Berlaku pada peluncuran gim berikutnya. + Gim + Default FEX + Menggunakan default FEX. + Stabilitas + Setelan paling aman dan paling lambat. + Kompatibilitas + TSO penuh dengan multiblock. + Menengah + TSO seimbang dengan presisi x87 yang dikurangi. + Performa + Semua TSO dinonaktifkan; multiblock. + Performa + TSO + Setelan performa dengan TSO utama diaktifkan. + Ekstrem + Menonaktifkan pemeriksaan self-modifying-code. Gim JIT dan DRM dapat rusak. + Ekstrem + TSO + Setelan ekstrem dengan TSO utama diaktifkan. + Extreme-gn + Performa dengan TSC dan metadata hints. Coba sebelum Ekstrem. + Denuvo + Performa dengan pemeriksaan kode penuh dan bit hypervisor disembunyikan. + Nilai khusus + Pilih nilai + Emulator & aplikasi + Tambah + Aplikasi atau gim Anda sendiri + Skrip + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Memulai… + %1$s ditambahkan + %1$s dihapus + Desktop + Aplikasi itu sudah tidak ada. Pilih lagi dari Desktop. + Buka %1$s + Hapus + Tekan lagi untuk menghapus + Hentikan sesi untuk menghapus + Menjalankan %1$s dari foldernya, ditautkan ke dalam sesi tempatnya berada. Menghapus aplikasi ini tidak memengaruhi folder tersebut. + Menjalankan %1$s dari salinan foldernya di dalam DroidDeck. Menghapus aplikasi ini menghapus salinannya, bukan aslinya. + Instal runtime Linux terlebih dahulu + File tersebut sudah tidak ada + Penyimpanan tidak cukup: menyalin folder memerlukan %1$s + Menyalin %1$s + Gagal menambahkan aplikasi + Mencari rilis %1$s + Tidak dapat membaca rilis %1$s + Tidak ada rilis %1$s yang memiliki AppImage ARM64 atau x86_64 + Mengunduh %1$s + Unduhan gagal + Gagal memperbarui aplikasi + Ikon tersebut tidak dapat dibaca + %1$s disimpan + Edit + Periksa pembaruan + Memeriksa pembaruan… + Perbarui ke %1$s + Terbaru · %1$s + %1$s diperbarui + %1$s · %2$s + Tambahkan aplikasi + Skrip + AppImage + GitHub + Flatpak + File .sh. Berjalan dari foldernya sendiri, layar penuh di bawah gamescope. + AppImage ARM64 (aarch64), atau AppImage x86_64 yang berjalan melalui FEX. Dibongkar ke dalam runtime Linux sekali saja. + Menginstal AppImage ARM64 dari rilis terbaru repositori, atau AppImage x86_64-nya melalui FEX. + Gunakan FEX + Otomatis + Aktif + Nonaktif + FEX menjalankan program Linux x86 di perangkat ARM64 ini. Otomatis menggunakannya saat program dibuat untuk x86. + %1$s melalui FEX + Program ini dibuat untuk PC %1$s dan berjalan melalui FEX, yang belum disiapkan. Buka Steam sekali saat masuk: DroidDeck akan mengunduh FEX dan Steam Linux Runtime yang diperlukan, lalu ini akan terbuka. + Program ini + %1$s dibuat untuk PC x86 dan berjalan melalui FEX, yang belum terinstal. Mulai Steam sekali saat masuk dan biarkan FEX serta Steam Linux Runtime diunduh, lalu mulai lagi. + ID aplikasi Flathub atau halaman Flathub-nya. Flatpak disiapkan dahulu jika belum. + File + Belum ada file yang dipilih + Pilih file + Ubah + Pilih skrip + Pilih AppImage + Pilih ikon + Repositori + owner/repo atau tautan github.com + Bukan repositori GitHub + ID aplikasi + org.ppsspp.PPSSPP + Bukan ID aplikasi Flathub + Atau file .flatpak, untuk aplikasi yang tidak ada di Flathub. + Pilih file .flatpak + Sumber + Di Desktop Anda + Aplikasi baru + Nama + Dari aplikasi + Ikon + Ikon default + Pilih ikon + Hapus + Dari repositori + Mencari ikon… + Default + Edit %1$s + Simpan + Memeriksa folder… + Foldernya ditautkan ke dalam sesi: %1$s + Foldernya berisi program Linux, yang tidak dapat dijalankan dari penyimpanan bersama, sehingga disalin ke dalam DroidDeck (%1$s). + Tambah + Batal + Instal runtime Linux dari Penyiapan terlebih dahulu. + Tunggu %1$s selesai. + Kompatibel + Build yang ditandai “%1$s” di menu Kompatibilitas Steam memiliki perbaikan prefix dari DroidDeck dan dapat menjalankan gim di sini. Defaultnya adalah Proton Experimental ARM64 (%1$s), atau Proton 11.0 ARM64 (%1$s) setelah build yang lebih baru itu terinstal. Entri Proton asli milik Valve tanpa tanda ini tidak dapat menjalankan gim di perangkat ini. + Terinstal %1$s · %2$s + Batal + Otomatis + Tidak pernah + Selalu + Hapus instalasi + Periksa + Perbarui + Memeriksa… + Memproses… + Lupakan + Tambah… + Gunakan + Berlaku pada sesi berikutnya. + Sesi Steam + Sesi Desktop + Tampilan + Resolusi + Hingga 720p + Hingga 900p + Hingga 1080p + Asli + %1$s (default) + Khusus · %1$d×%2$d + Khusus… + 720p dapat membuat menu lebih responsif. + Rasio layar + Ditentukan oleh resolusi khusus. + Otomatis menggunakan setidaknya 16:9. + Batas frame rate + Membatasi seluruh sesi. Tampilan beralih ke refresh rate yang habis dibagi oleh batas ini. + HDR + Output HDR10 + Tidak tersedia: %1$s. + Mulai ulang aplikasi untuk menerapkannya. + Driver + Driver GPU + Digunakan bersama oleh Steam dan Desktop. Diatur di halaman Komponen. + Sentuhan & kontrol + Sentuhan + Touchpad + Langsung + Otomatis: touchpad di Desktop, langsung di Steam. + Kontrol di layar + Steam + QAM + Otomatis menampilkan kontrol saat tidak ada pengontrol yang terhubung. + Sentuhan Steam (beta) + Kontrol sentuh Steam dimulai pada sesi Steam berikutnya + Tata letak sentuh Steam + Pindahkan, ubah ukuran, atau sembunyikan kontrol sentuh gim ini; disimpan ke Steam + Pengontrol + Cara Steam mendeteksi pengontrol Anda. Berlaku pada sesi berikutnya. + Pengontrol Steam Deck + Steam Controller + DualShock 4 + DualSense + Pengontrol Xbox 360 + Deck: tombol Quick Access dan giroskop. Steam Controller: sama, dengan grip sense pada tombol belakang. DualShock 4 dan DualSense: giroskop, dan trackpad sebagai touchpad-nya. Xbox 360: buka Quick Access dengan Guide+A. + Kembali + Menu → QAM + QAM → Menu + Sesi + Perilaku latar belakang + Saat aplikasi disembunyikan atau layar mati. + Manual + Native + Otomatis menjeda dan melanjutkan saat ditampilkan. Native menyiapkan Steam untuk tidur terlebih dahulu. Manual menunggu Lanjutkan. Tidak pernah terus berjalan. + Pertahankan unduhan tetap berjalan + Menunda penjedaan latar belakang saat Steam sedang aktif mengunduh. + Otomatis menjeda di latar belakang dan melanjutkan saat ditampilkan. Manual menunggu Lanjutkan. Tidak pernah terus berjalan. + Mulai + Mulai Steam bersama DroidDeck + Membuka sesi Steam saat DroidDeck diluncurkan. + Decky + Memeriksa rilis… + Siap menginstal %1$s. + Tidak ditemukan build yang kompatibel. Coba lagi nanti. + Terinstal · %1$s + Pembaruan tersedia · %1$s + Terbaru · %1$s + Loader + Instal versi terbaru + Dimulai bersama Steam. Saat aktif, aplikasi lain di perangkat ini mungkin dapat mengendalikan Steam. + Nonaktif. Port debugging jarak jauh Steam tetap tertutup. + Hapus instalasi Decky Loader? + Menghapus loader. Plugin dan setelan Anda tetap disimpan. + Klien + Overlay performa + Nonaktifkan jika gim crash atau layar menjadi hitam di mode Deck. Berlaku pada sesi berikutnya. + Perbaiki klien Steam + Mengunduh ulang klien Steam saat Steam dimulai lagi. Gunakan ini jika Steam tetap berada di layar pemuatannya. Gim, status masuk, dan setelan tetap disimpan. + Klien Steam akan diunduh ulang saat Steam dimulai lagi. + Perbaiki + Karya seni + Kunci API SteamGridDB + Kunci Anda + Edit… + Melengkapi karya seni yang belum ada dari Steam, lalu SteamGridDB. File cover.jpg, hero.jpg, logo.png, atau icon.png Anda sendiri di folder gim lebih diutamakan. + Hapus %1$s + Hapus? + Tambah gim + Tambahkan gim: + Pilih exe: + Lokasi ini tidak tersedia dalam sesi + Periksa exe ini + Komponen Windows + LB / RB berpindah antar bagian. + Tidak ada + DLL Windows yang tidak disertakan Proton, dari daftar komponen. Aktifkan yang diperlukan gim ini: DLL tersebut masuk ke folder Windows milik gim sendiri, bukan ke file gimnya, dan tetap ada pada Proton apa pun yang menjalankannya. + Tidak dapat menjangkau daftar komponen. Komponen yang sudah diunduh masih dapat dialihkan. + Direkomendasikan untuk gim ini + Memeriksa kebutuhan gim ini… + Tersedia saat online + + Mengaktifkan %1$d komponen… + + Aktif untuk gim ini + Steam menginstal %1$s dengan %2$s (app %3$s) + Dari daftar GOG untuk gim ini + Dari prasyarat Epic untuk gim ini + Dari langkah instalasi Amazon untuk gim ini + Steam menginstal %1$s dengan app %2$s, dicocokkan berdasarkan nama + Juga tercantum + Tercantum oleh %1$s; bukan komponen di sini + Steam + GOG + Epic Games + Amazon Games + Gim ini menyertakan %1$s + Gim ini memiliki %1$s (sering berfungsi tanpa perubahan) + Sudah ada di gim ini + Tidak ada yang di folder gim mengarah ke komponen. + Terinstal untuk gim ini (%1$d) + Semua komponen (%1$d) + Memerlukan installer Windows (%1$d) + Ini adalah program penyiapan sendiri tanpa paket di dalamnya yang dapat dibongkar, dan belum ada catatan tentang apa yang diinstalnya. Kebanyakan adalah runtime lama yang sudah digantikan oleh Mono di dalam Proton; catatan ditambahkan seiring dibuatnya. + Tampilkan + Sembunyikan + Menjalankan program penyiapan sendiri (belum tercatat) + Belum didukung di sini + Memuat komponen… + Tidak dapat menginstal: %1$s + Terunduh + Mengunduh %1$s + Menginstal: %1$s + Menonaktifkan… + Unduhan %1$s + Berlaku saat gim dimulai lagi. + Rentangkan gim 16:9 + Pada layar yang lebih persegi dari 16:9, merentangkan gim agar memenuhi layar, bukan menampilkan batang hitam. Berlaku pada sesi berikutnya. + Audio + DirectAudio untuk gim + Melewati PulseAudio untuk latensi yang lebih rendah. + Mikrofon + Untuk obrolan suara. + Jaringan + Pencarian Wi-Fi + Menampilkan nama Wi-Fi dan jaringan di sekitar di Steam. + Layanan lokasi tidak aktif. + Izin lokasi tidak diberikan. + Buka setelan + Lanjutkan + Aktifkan pencarian Wi-Fi? + Android memerlukan izin Lokasi untuk membaca nama Wi-Fi dan menemukan jaringan di sekitar. DroidDeck menggunakan akses ini untuk menampilkan jaringan tersebut di Steam. Kami tidak melacak lokasi Anda. Akses internet tetap berfungsi tanpa izin ini. + Penyimpanan gim + Otomatis - kartu SD jika ada, ditambah folder Games Anda + Otomatis - folder Games Anda, ditambah kartu SD jika ada (tidak ada sekarang) + Hanya internal + Folder: %1$s + Pustaka kedua + Gim yang melakukan streaming aset dari SD atau penyimpanan bersama dapat tersendat. + Diagnostik penyimpanan + Mencatat hasil alokasi dan waktu penyimpanan yang disampel di Bagikan log. Dapat memperlambat instalasi saat diaktifkan. Berlaku pada sesi berikutnya. + Pilih folder… + Resolusi khusus + Menggantikan batas dan rasio. Ukuran yang tidak sesuai dengan layar akan menampilkan batang. + Lebar + Tinggi + Antara 320×240 dan 3840×2160. + + %1$d gim + + OK + Hapus + Pulihkan + Unduh + Hapus %1$s + %1$.1f MB + Driver runtime + Digunakan oleh Steam, gim, dan desktop. Berlaku pada sesi berikutnya. + Impor zip Turnip… + Driver tampilan + Menggambar frame ke layar di kedua mode. Mulai ulang aplikasi untuk menerapkannya. + Impor zip AdrenoTools… + tidak pernah + Nightlies diperiksa %1$s + Komponen + Tentang Komponen + Periksa driver baru + Periksa Nightlies untuk paket baru + Proton + Proton yang dipilih di sini menjalankan setiap gim yang belum memilih Proton sendiri di Steam, dan perubahan yang dibuat di Steam juga ditampilkan di sini. Steam menggantikan Proton milik Valve saat memperbarui; aslinya tetap disimpan per build. + Konfigurasi + Konfigurasikan profil + Profil + Profil default + Digunakan secara otomatis oleh gim tanpa penggantian. + Setelan tersimpan yang digunakan secara otomatis saat %1$s dijalankan. + Pilih hanya setelan yang diperlukan gim ini. Semua yang lain tetap menggunakan profil Default. + Preset FEX + Menggunakan preset profil Default. + Preset khusus tersimpan untuk gim ini. + Variabel FEX khusus + Gunakan Default + Kompatibilitas gim + Opsi lingkungan Proton, DXVK, VKD3D, dan sinkronisasi untuk gim ini. + Menggunakan Proton profil Default. + Proton khusus tersimpan untuk gim ini melalui pemetaan kompatibilitas Steam. + Tanpa Proton + DXVK + VKD3D-Proton + Menggunakan paket profil Default dari %1$s kecuali gim ini memilih paket lain. + Menggunakan paket komponen profil Default. + Menggunakan %1$s hanya untuk gim ini. + Opsi runtime + Opsi lingkungan Proton, DXVK, VKD3D-Proton, dan sinkronisasi lain per gim. + Profil %1$s tersimpan untuk peluncuran gim berikutnya. + Mulai tanpa Steam + Untuk game yang langsung tertutup: menjalankannya tanpa pengaturan Steam dari Proton. Fitur Steam seperti overlay dimatikan untuknya. + Aktif + Pilihan Proton %1$s tersimpan dan sudah tersedia di Steam sekarang. + Pilihan Proton %1$s tersimpan. Mulai ulang Steam sebelum meluncurkan gim. + %1$s tidak dapat menggunakan Proton yang dipilih di perangkat ini. + Tidak dapat memuat profil %1$s: %2$s + Profil gim tidak dapat disimpan: %1$s + Atur ulang profil gim + Atur ulang profil %1$s? + Hapus Proton, FEX, paket komponen, lingkungan, pemfilteran tekstur, dan penggantian komponen Windows gim ini? Gim ini akan menggunakan profil Default. + %1$s sekarang menggunakan profil Default. + Profil gim tidak dapat diatur ulang secara lengkap: %1$s + %1$s sekarang menjalankan setiap gim yang belum memilih Proton sendiri di Steam. + %1$s akan menjalankan setiap gim yang belum memilih Proton sendiri, mulai dari saat Steam berikutnya dijalankan. + %1$s tidak dapat memulai gim di sini, sehingga tidak dapat menjadi default. + Proton default tidak dapat disimpan: %1$s + %1$s · %2$d ditukar + %1$s · semua asli + %1$s · gim berjalan + %1$s · paket droiddeck-esync + %1$s · belum ada paket droiddeck-esync + %1$s · esync sudah terpasang, tidak perlu paket + Driver GPU + %1$s · diterapkan ulang saat peluncuran %2$s + Berikutnya: %1$s · setelah gim ditutup + %1$s digunakan: %2$s + Menunggu sampai gim ditutup + Berlaku saat gim berikutnya dimulai + Impor .zip + Membaca Proton… + Belum ada Proton yang terinstal. Mulai Steam sekali untuk mengunduh Proton ARM64-nya, atau tambahkan GE-Proton, CachyOS, atau DroidDeck-Proton dari Penyiapan. + Asli · %1$s + %1$s · build ini + %1$s · build sebelumnya + Terinstal · %1$d + Belum ada yang tersimpan untuk %1$s. File asli Proton ini disimpan sebagai aslinya. + Sebuah gim sedang berjalan pada Proton ini, sehingga ini menunggu sampai gim ditutup. + Berlaku saat gim berikutnya dimulai. + Tukar %1$s? + Masukkan %1$s ke %2$s? File aslinya tetap tersimpan. %3$s + Pulihkan %1$s? + Kembalikan %1$s asli ke %2$s? %3$s + Menghapus paket dari aplikasi. Anda dapat mengunduhnya lagi. + Hapus yang asli ini? + %1$s asli ini berasal dari build %2$s yang lebih lama. Asli build saat ini selalu dipertahankan. + Di Nightlies · %1$d + Segarkan untuk melihat daftar paket di Nightlies. + Tidak ada yang baru sejak pemeriksaan terakhir. + Impor .wcp + Pulihkan yang asli dari Proton ini + Kembalikan %1$s asli %2$s? + Tukar + FEX, DXVK, dan VKD3D-Proton untuk setiap Proton yang digunakan Steam menjalankan gim.\n\nPilih baris yang terinstal untuk menukarnya. Berlaku saat gim berikutnya dimulai, atau setelah gim yang sedang berjalan ditutup.\n\nPilihan Anda diperiksa ulang sebelum setiap peluncuran dan dikembalikan jika ada yang mengubahnya. File asli setiap Proton tetap disimpan, sehingga pembaruan Steam tidak akan pernah menghilangkannya. + Asli + Digunakan + Baru + Tersimpan + Performa + Core CPU, perbaikan sesi, dan nama host. Berlaku pada sesi berikutnya. + Core klien Steam + Timpa pilihan core Steam + Mengikat Steam, pembantu UI-nya, dan gamescope ke core di bawah. + Core klien + Core untuk Steam. + Aktifkan penggantian untuk memilih. + Core gim + Core gim + Core untuk gim. Dengan semua dipilih, Android menjadwalkan secara bebas. + Antarmuka klien + GL berulir (threaded) + Dapat membuat menu Steam lebih responsif. + Zink: deskriptor lazy + Direkomendasikan untuk driver tanpa descriptor buffer. + Lewati pemeriksaan error GL + Menonaktifkan validasi GL per panggilan. + gamescope: antrean GPU realtime + Memprioritaskan kerja GPU compositor. Dapat menghaluskan pacing frame dengan sedikit mengorbankan gim. + GPU + Pertahankan GPU pada clock tertingginya + Hanya Adreno. Lebih sedikit tersendat, lebih banyak penggunaan baterai dan panas. + Perbaikan sesi + Turnip: rendering sysmem + Lebih cepat untuk Steam dan sebagian besar gim. Diperlukan pada Adreno 710/720/722. + Lewati bantuan xalia milik Steam + droiddeck-ntsync (eksperimental) + Pengganti userspace untuk driver ntsync Linux, untuk pengujian. Saat aktif, Proton menggunakannya sebagai pengganti droiddeck-esync dan droiddeck-fsync. Berfungsi dengan Proton Experimental ARM64, GE-Proton, dan proton-cachyos. Berlaku pada sesi berikutnya. + droiddeck-esync + droiddeck-fsync lebih dulu + Menjalankan fsync milik Proton sendiri pada file Proton standar, dengan futex_waitv yang disediakan di userspace, sehingga tidak memerlukan paket dan tetap berfungsi begitu Steam memperbarui Proton. Saat aktif, gim menggunakannya sebelum droiddeck-esync, kecuali beberapa gim yang oleh Proton dijaga tetap di luar fsync. Berlaku pada sesi berikutnya. + Thread Windows milik gim saling menunggu melalui eventfd dan memori bersama, bukan melalui bolak-balik ke wineserver. Menggunakan paket droiddeck-esync yang dibuat khusus untuk build Proton tersebut; paket diambil secara otomatis. Sampai paket untuk build Proton baru tersedia, droiddeck-fsync menjadi penggantinya. Berlaku pada sesi berikutnya. + Menonaktifkan bantuan navigasi gamepad milik Proton, yang menggunakan CPU di setiap gim. + Jalankan proot tanpa seccomp + Dapat memperbaiki galat syscall yang hilang pada beberapa kernel. Dapat mengurangi performa. + Jalur cepat proot + Memerlukan filter seccomp milik proot. + Pencarian file lebih cepat dan lebih sedikit tersendat. Nonaktifkan jika program tidak dapat menemukan filenya. + Identitas sesi + Nama host + Nama yang dilihat Steam untuk perangkat ini. Kosongkan untuk memulihkan %1$s. + Hanya huruf, angka, dan tanda hubung di tengah, hingga 63 karakter. + Android diatur untuk menghentikan sesi ini + Terinstal + Tersedia untuk diunduh + Tidak ada yang baru sejak pemeriksaan terakhir. + Pulihkan driver bawaan + Bawaan aplikasi: ini menghapus file yang telah dibongkar dan menyembunyikannya. Pulihkan driver bawaan akan mengembalikannya. + Separuh dari paket Android + Linux: kedua separuhnya dihapus. Driver yang sedang digunakan akan beralih kembali ke default. + Filenya dihapus dari aplikasi. + Filenya dihapus. Driver ini sedang digunakan, jadi default akan mengambil alih. + Tampilan + Efek + Kontrol + Komponen + Pengontrol + Setelan + Halaman %1$s + Steam + Desktop + Menu Steam + Akses cepat + Buka Menu Akses Cepat + Halaman sebelumnya + Halaman berikutnya + HUD performa + Mode penskalaan + Ketajaman penskalaan + Efek layar + Langsung. Menggunakan sedikit waktu GPU saat ada efek yang aktif. + Preset + Khusus + Lanjutan + Diperluas + Dilipat + Penajaman (CAS) + Tingkat penajaman + HDR palsu + Debanding + Kekuatan debanding + Kecerahan + Kontras + Gamma + Saturasi + FXAA (penghalusan tepi) + Garis tepi toon + CRT + NTSC + Pemfilteran tekstur + Gim DirectX 9–11, mulai dari peluncuran berikutnya. + Pemfilteran anisotropik + Ketajaman tekstur + Bias mip negatif: tekstur lebih tajam, sedikit kilauan. Otomatis menyesuaikan seberapa besar sesi diperbesar ke layar. + Otomatis (%1$s) + Keyboard + Keyboard PC + Android + Layar kedua + Mode + Menampilkan + Aplikasi Android + Luncurkan aplikasi + Sembunyikan + Tampilkan + Tidak ada aplikasi yang dapat diluncurkan + Perilaku sesi + Berlaku sekarang dan untuk sesi mendatang dalam mode ini. + Sesi berikutnya + Dukungan + Log sesi + Lampirkan ke laporan bug + Bagikan log + Latar belakang + Hentikan + Hentikan sesi? + Sedang berjalan + Tidak ada gim yang berjalan pada Proton + Profil gim + Detail profil belum tersedia. + Tidak dapat memuat profil gim. Mencoba lagi secara otomatis. + Tidak dapat menyegarkan profil gim. Menampilkan data terakhir yang tersedia; mencoba lagi secara otomatis. + Lihat dan edit setelan tersimpan yang digunakan pada peluncuran berikutnya. + Menggunakan profil Default + Profil khusus + Menggunakan %1$s dari profil Default. + Perubahan disimpan ke profil %1$s dan berlaku saat gim diluncurkan berikutnya. + + %1$d penggantian gim + + Belum ada Proton yang terinstal. + gim berjalan + diterapkan ulang pada peluncuran %1$s + Asli + Impor dan hapus di halaman Komponen. + Selesai + Perlu perhatian + Memproses + Periksa alamat IP dan port Debugging nirkabel, atau sandingkan lagi jika Android telah menghapus perangkat ini. + Instal + Kelola + Ringkasan + Pengontrol + Sesi + Peluncur + Penyiapan + ● Semua siap + %1$d dari %2$d siap + Pemeriksaan sistem + Perangkat didukung + GPU belum diuji + GPU tidak didukung + %1$s · %2$s + %1$s: %2$s + Steam memerlukan driver Adreno; %1$s mungkin menampilkan layar hitam + Runtime Linux + %1$s · %2$d%% + Belum terinstal · sekitar 3 GB, terinstal saat Play pertama kali + Terinstal + %1$s · pembaruan tersedia + %1$s · terbaru + Batas proses turunan + Aktif · Android dapat menutup Steam. Menonaktifkannya memerlukan waktu sekitar satu menit. + Belum diatur · Android dapat menutup Steam. Menonaktifkannya memerlukan waktu sekitar satu menit. + Tidak dapat diperiksa · menonaktifkannya memerlukan waktu sekitar satu menit. + Perbaiki + Detail + Opsi developer + Gunakan Debugging nirkabel + Periksa lagi + Aktifkan batas + Akun Steam + Masuk sebagai %1$s · mode luring + Masuk sebagai %1$s + Tekan Play dan masuk ke Steam + Alat + Pengontrol + Setelan pengontrol tidak tersedia. + Sesi + Log sesi + Disimpan setelah setiap sesi + Alat debug + Log sesi terbaru + Log sesi tersimpan + Menyimpan %1$d terbaru + Hapus log + Mode luring + Masuk ke Steam terlebih dahulu + Peluncur + Bahasa + Steam juga menggunakannya, mulai dari saat berikutnya dimulai. + Default sistem (%1$s) + Default sistem + Skala aplikasi + Teks dan kontrol DroidDeck + %1$d%% (Default) + Tema + Tampilan peluncur + Gunakan sebagai Layar Utama + DroidDeck dapat menjadi aplikasi Utama + DroidDeck tidak pernah ditawarkan sebagai aplikasi Utama + Orientasi + Lanskap otomatis + Lanskap + Lanskap terbalik + Layar penuh + Menyembunyikan bilah status dan navigasi Android + Menampilkan bilah status dan navigasi Android + Aplikasi Utama default + Pilih aplikasi Utama + Pilih + Aplikasi Linux (beta) + File + Jelajahi dan kelola file + Versi Proton + Instal build Proton ARM64 + Performa + Penugasan inti CPU + Folder ROM + Tempat gim emulator disimpan + Komponen + FEX, DXVK, VKD3D + Kontrol + Pemetaan tombol + Temukan + Cari + Terinstal + %1$s · %2$d + Toko + Flathub · ARM64 + ● Flatpak siap + Instal runtime Linux dari Penyiapan terlebih dahulu. + Siapkan Flatpak + Menambahkan Flatpak dan menghubungkannya ke Flathub (sekitar 20 MB). Aplikasi pertama juga mengunduh runtime-nya. + Menyiapkan… + Flatpak + Pembaruan + %1$s · %2$s + %1$s · %2$s · %3$d%% + Memulai… + Kategori + Tidak dapat menjangkau Flathub. Periksa koneksi Anda dan coba lagi. + Coba lagi + Cari di Flathub + Firefox, gim, emulator… + Cari + Semua + Mencari… + Cari berdasarkan nama atau pilih kategori. Hanya aplikasi ARM64 yang ditampilkan. + Tidak ada aplikasi ARM64 di Flathub yang cocok. + Hasil + Periksa pembaruan + Perbarui semua (%1$d) + ● Semuanya sudah terbaru + Belum ada aplikasi. Temukan di tab Temukan atau Cari. + Siapkan Flatpak untuk menginstal aplikasi. + Pembaruan tersedia · %1$s + Buka + Detail + Hapus + Tekan lagi untuk menghapus + Menginstal… + Instal + Siapkan Flatpak terlebih dahulu + Unduhan %1$s + ● Terinstal + Tidak dapat memuat halaman Flathub aplikasi ini. + Memuat dari Flathub… + Tidak ada build ARM64 di Flathub, sehingga tidak dapat diinstal di sini. + Tangkapan layar + Tentang + ID aplikasi + Lisensi + Runtime + Ukuran terinstal + %1$s (ditambah runtime bersamanya) + Situs web + Terverifikasi + Pembaruan + DroidDeck %1$s · %2$s + Diperiksa %1$s + Belum diperiksa + Izinkan DroidDeck memperbarui dirinya sendiri + Pada layar berikutnya, aktifkan Izinkan dari sumber ini, lalu kembali. Pembaruan akan berlanjut secara otomatis. + Buka setelan + Nanti saja + Ditandatangani berbeda + Tidak dapat memperbarui di tempat + Salinan ini tidak ditandatangani dengan kunci rilis DroidDeck, sehingga pembaruan tidak dapat diinstal menimpanya. Hapus instalasinya untuk beralih. + Pembaruan tersedia + Tidak dapat diinstal + Siap untuk beralih + Lebih baru dari Stable + Anda lebih baru dari Stable + Build ini lebih baru daripada Stable terbaru. Anda akan beralih ke Stable pada rilis berikutnya. + Pengujian berakhir + Pengujian ini telah berakhir + Perbaikannya sudah digabungkan atau dibatalkan. Ikuti Preview untuk perbaikan terbaru. + Terbaru + Anda memiliki %1$s terbaru + · %1$s + %1$s… %2$d%% + %1$s… + Perbarui%1$s + Instal%1$s + Tetap instal Stable %1$s + Ikuti Preview + Hentikan sesi yang berjalan untuk memperbarui. + Lebih sedikit + Lebih banyak + DroidDeck %1$s + Build Preview baru + Build pengujian PR #%1$d + %1$d MB + SALURAN PEMBARUAN + Stable + Rilis yang telah diuji, untuk kebanyakan orang + %1$s · %2$s + Preview + Perbaikan terbaru, sebelum Stable + Terbaru + Build pengujian + Coba perbaikan sebelum dirilis + Tidak ada saat ini + PR #%1$d + Tidak dibuat untuk salinan DroidDeck ini + Diperbarui %1$s + pengujian PR #%1$d + beberapa waktu lalu + baru saja + + %1$d untuk dicoba + + + Perubahan sejak build Anda · %1$d build di belakang + + + Perubahan terbaru · %1$d build + + Satu setelan Android yang harus diubah + Steam + Android menghentikan aplikasi yang memulai banyak proses latar belakang, dan Steam memulai puluhan. Sampai ini berubah, gim akan tertutup tanpa pesan kesalahan. + Perbaiki untuk saya + Aktifkan “Disable child process restrictions”, lalu kembali. Biarkan Opsi pengembang tetap aktif, atau sakelarnya akan nonaktif lagi. + Buka Opsi pengembang + Versi Android ini tidak memiliki sakelar untuk itu, sehingga DroidDeck mengubahnya melalui Debugging nirkabel. Membutuhkan waktu sekitar satu menit, tanpa komputer. + Debugging nirkabel memerlukan Wi-Fi. Jaringan apa pun berfungsi; tidak perlu internet. + Kode pemasangan muncul di notifikasi. Izinkan notifikasi untuk DroidDeck, atau gunakan Cara lain. + Izinkan notifikasi + Buka Setelan → Opsi pengembang → Debugging nirkabel dan aktifkan + Ketuk “Pasangkan perangkat dengan kode pemasangan” + Masukkan kode ke notifikasi DroidDeck + Menunggu jendela pemasangan… + Jendela ditemukan. Masukkan kodenya di notifikasi. + Status + Memeriksa setiap 2 detik. Halaman ini akan tertutup saat statusnya nonaktif. + Cara lain + Debugging nirkabel, alamat manual, ADB komputer + Alamat manual, ADB komputer, Opsi pengembang + Gunakan Debugging nirkabel sebagai gantinya + Beberapa ROM menambahkan sakelar ini juga. Cari “child process” di Opsi pengembang. + Jika notifikasinya tidak berfungsi, masukkan sendiri alamat dan kode dari jendela pemasangan. + Masukkan alamat secara manual + Dari komputer dengan ADB: + Salin perintah + Pilih tampilan tempat membuka Setelan Android. + Layar utama + Berhasil dipasangkan. Mencari port Debugging nirkabel… + Berhasil dipasangkan. Masukkan port dari Setelan. + Perangkat ditemukan. Menerapkan setelan… + Tidak dapat menerapkan secara otomatis. Periksa alamat di bawah dan coba lagi. %1$s + Debugging nirkabel + Batas proses turunan diperbarui + Ubah batas proses turunan + Jika opsinya tidak ada, pasangkan melalui Debugging nirkabel. + Berhasil dipasangkan. Periksa alamat IP dan port di Debugging nirkabel. + Batas proses turunan telah diperbarui. + Gunakan langkah 2 jika Opsi pengembang tidak memiliki setelan proses turunan. Buka Opsi pengembang → Debugging nirkabel → Pasangkan perangkat dengan kode pemasangan. + Berhasil dipasangkan. Gunakan alamat di bawah, atau perbarui dari Debugging nirkabel → Alamat IP & Port. + Batas proses turunan telah diperbarui melalui Debugging nirkabel. + Android mengonfirmasi batas ini aktif. + Android mengonfirmasi batas ini nonaktif. + 2 · Pasangkan Debugging nirkabel + Alamat IP & port jendela pemasangan + Kode pemasangan (PIN) + 6 digit + Pasangkan + 2 · Terapkan melalui Debugging nirkabel + Alamat IP & port koneksi + Pasangkan lagi + Nonaktifkan batas + Cara lain untuk mengubahnya + 1 · Opsi pengembang (coba dahulu) + Aktifkan “Restrict child processes”. + Nonaktifkan “Restrict child processes”. + 3 · ADB komputer (upaya terakhir) + Hanya jika Debugging nirkabel tidak tersedia. + 3 · ADB komputer (upaya terakhir) + Saat kontrol sentuh muncul dalam sesi + Warna + Warna tombol di layar + Khusus + Opasitas + %1$d%% + Ukuran tombol + 100% adalah ukuran standar + Klik stik + Ketuk dua kali dan tahan sebuah stik untuk L3 atau R3 + Stik adaptif + Getaran gim + Tombol Steam + Tombol QAM + Tombol keyboard + Tekan Select untuk membuka menu Steam + Bukan View/Select. + Animasi + Stik muncul di tempat Anda menyentuh dan tersembunyi saat dilepas + Tata letak + Posisi khusus disimpan + Disesuaikan dengan layar ini + Edit + Reset + Pemetaan tombol + Setiap tombol mengirim input-nya sendiri + %1$d dari %2$d tombol dipetakan ulang + Konfigurasi + Reset pengontrol + Mengembalikan semua setelan pengontrol dan tata letak + Apa yang dikirim setiap tombol di layar ke gim. Tersembunyi menghapus tombol tersebut. + Tombol di layar + Default + Reset pemetaan + Aplikasi Android + %1$d aplikasi + Tidak ada aplikasi Android yang dapat dijalankan. + ● Desktop terinstal + Terinstal saat pertama kali dibuka + Tersedia untuk diinstal + Belum terinstal. + %1$s · belum terinstal + Buka %1$s + Gim + Pilih folder ROM. + Jelajahi ke /root/ROMs di RetroArch. + Tambahkan gim %1$s ke ROMs/%2$s. + Instal %1$s + Hentikan sesi untuk menginstal + Runtime diperlukan + Memuat detail instalasi… + Detail instalasi tidak tersedia. Coba lagi nanti. + Instal runtime Linux dari Penyiapan terlebih dahulu. + Hentikan sesi yang berjalan untuk menginstal aplikasi desktop. + Menginstal %1$s ke desktop Linux. + %1$s · %2$d%% + Menyembunyikan menghapusnya dari Desktop. Filenya tetap berada di runtime Linux. + Gim tersebut sudah tidak ada di folder ROMs. + Desktop · %1$s + Luncurkan di %1$s + Juga di %1$s + Halaman tersebut sudah tidak ada. Pilih bagian di sebelah kiri. + Buka desktop + Instal & buka desktop + Hapus instalasi desktop + Hapus desktop dan aplikasi bawaannya. Simpan gim, aplikasi yang ditambahkan, setelan, dan pustaka bersama. + Menghapus desktop + Desktop dihapus + Menghapus file LXQt lama + Perbarui DroidDeck sebelum menghapus versi desktop ini. + Desktop Linux + KDE Plasma, Firefox, dan emulator Anda + Gim yang Anda instal di Steam akan muncul di sini setelah sesi. + Mainkan Steam + Setelan peluncuran + Luncurkan + File gim + Prefix Proton + Mencari data simpanan… + Tidak ditemukan folder simpanan berdasarkan nama. Ekspor mengambil seluruh folder pengguna. + %1$d folder simpanan + %1$s · %2$d file · %3$s + %1$d KB + Kelola data simpanan + Data simpanan gim + Impor data simpanan… + Ekspor sebagai zip GameHub… + Ekspor sebagai zip Winlator… + Buka folder simpanan + Belum pernah dimainkan + Runtime sibuk · %1$d%% + Runtime sibuk + Runtime terinstal saat Main pertama kali + Pembaruan runtime tersedia + ● Runtime siap + Terakhir dimainkan %1$s + Penyimpanan internal + Gim ditambahkan + Gim khusus + Penyimpanan gim + Pasangan terbaik untuk GPU ini, selalu diperbarui + Pilih pasangan sendiri + Memeriksa driver… + Belum ada driver yang terdaftar. Segarkan untuk mencarinya. + Sembunyikan driver untuk GPU lain + Tampilkan driver untuk GPU lain + Lanjutan + Diatur bersama driver tampilan: %1$s membawa keduanya. + Diatur bersama driver runtime: %1$s membawa keduanya. + Digunakan oleh gim dan desktop. + Default runtime + Menggambar frame ke layar. Berlaku setelah DroidDeck dimulai ulang. + Otomatis - dipilih berdasarkan GPU + Unduh, impor, atau hapus… + DIGUNAKAN + Driver bawaan DroidDeck + Driver runtime dan tampilan diatur bersama sebagai pasangan yang cocok. + Direkomendasikan untuk GPU ini: %1$s. + Segarkan untuk mencari driver bagi GPU ini. + DIREKOMENDASIKAN + Tidak tersedia + Runtime Linux + Menyiapkan runtime Linux + Sesi + Memulai tampilan dan audio + Klien Steam + Memeriksa Steam dan alat kompatibilitasnya + Membuka Steam + Menunggu frame pertama Steam + Desktop + Menyiapkan desktop + Membuka + Menunggu frame pertama + Memulai Steam + Sesi berakhir + Langkah %1$d dari %2$d + Steam dijeda + Pegangan + Pad kiri + Pad kanan + Keduanya + Gamepad + Stik + Trackpad + Beralih antara stik dan trackpad + Kontrol Deck lainnya + PANEL + Tutup kontrol layar kedua + Buka menu Steam + Kiri · atas + Kiri · bawah + Kanan · atas + Kanan · bawah + Klik + Trackpad kanan + Trackpad kiri + Trackpad + Tampilkan keyboard + Sembunyikan keyboard + Terminal Linux + Sesi Linux belum siap untuk terminal. + Steam + Game + Desktop + Toko + Komponen + Aplikasi + Penyiapan + Pembaruan + %1$d%% + Lanjutkan %1$s + Muncul sebagai /root/ROMs di setiap sesi. Penyimpanan internal berada di /root/Storage. + Belum ada folder yang dipilih + Berlaku pada sesi berikutnya. Kartu SD juga bisa. + Pilih folder + Ubah folder + Instal build Proton ARM64, lalu pilih per game di Steam → Properti → Kompatibilitas. + Hentikan sesi yang berjalan untuk menginstal atau menghapus alat kompatibilitas. + Build yang tersedia + Terinstal %1$s + Tertunda dari permintaan sebelumnya + Belum terinstal + Pembaruan tersedia → %1$s + Hapus versi %1$s yang lama + Menghapus %1$s. Game yang diatur ke versi ini harus dialihkan ke Proton lain. + Game yang diatur ke versi ini harus dialihkan ke Proton lain. + Instal sekarang + Batalkan antrean + Keyboard Android + Sembunyikan ✕ + Luncurkan %1$s + Pilih layar. Sekunder: %1$s. + Layar sekunder tidak lagi tersedia. + Layar utama + Layar sekunder + Tab sebelumnya + Tab selanjutnya + Aktif + Semua %1$d + Tidak ada + %1$d dari %2$d + Nonaktif + Layar bawah + Berlaku pada peluncuran game berikutnya. Profil khusus game ini menimpa profil bersama. + Game lain berdasarkan app ID… + Selesai + Default + Nonaktif + Ubah… + Ubah variabel + Kembali ke default + Game Proton + Gambar dalam gambar + Otomatis masuk ke mode PiP + Kontrol di layar saat dibuka + Saat dibuka, game berada di atas trackpad dan grip belakang Steam Deck. Saat dilipat, game tampil layar penuh. + Ditangguhkan + Gambar dalam gambar tersedia setelah sesi siap. + Izinkan gambar dalam gambar untuk DroidDeck di setelan Android. + Android tidak dapat membuka gambar dalam gambar. + Pintasan + Tambahkan ke layar utama + Pilih game + Memindai game yang terinstal… + Salin tautan peluncuran + Tautan peluncuran disalin + Launcher ini tidak dapat menambahkan pintasan. Gunakan Salin tautan peluncuran. + Tautan peluncuran game tidak valid. + Game ini tidak lagi ada di pustaka DroidDeck Anda. + Hentikan sesi saat ini sebelum meluncurkan game Steam. + Tidak dapat mengirim game ke Steam. Coba luncurkan lagi. + Ulangi penghapusan + Menghapus + Penghapusan belum selesai + Ekspor file game + Sinkronkan semua file game ke folder + Hentikan sinkronisasi file game + Pilih folder untuk file game frontend + File game disimpan ke %1$s + File frontend + + Tunggu operasi runtime selesai + Tunggu instalasi alat kompatibilitas selesai + Tunggu instalasi aplikasi desktop selesai + Tunggu instalasi selesai sebelum meluncurkan game + Tutup sesi Steam dulu, agar game tidak menyimpan dan menimpa hasil impor + Mengimpor ke %1$s + + %1$d file diimpor dari %2$s ke %3$s + + + %1$d file diimpor dari %2$s ke %3$s. Save lama dicadangkan ke Download/DroidDeck/Saves/backups + + Pilih zip save untuk %1$s + Mengekspor %1$s + + %1$d file diekspor sebagai %2$s: %3$s + + Pilih tempat menyimpan %1$s (%2$s) + %1$s gagal: %2$s + Belum ada log sesi: jalankan sesi dulu. + Tidak ada log sesi untuk dihapus. + + %1$d folder log sesi dihapus. + + Penyambungan debugging nirkabel gagal + Perintah debugging nirkabel gagal + Debugging nirkabel tidak tersedia + Perintah ADB disalin + Pilih folder ROMs + Bukan GPU Adreno + Turnip mendukung GPU Adreno. Di %1$s, Steam mungkin menampilkan layar hitam. Unduhan: %2$.0f MB. + Tetap instal + Hapus runtime Linux + Ini menghapus runtime, klien Steam di dalamnya, dan setiap game yang terinstal di sana. + Diterapkan: %1$s + Tidak dapat membuka %1$s di layar utama + Tidak dapat membuka %1$s di %2$s + Tidak dapat membuka %1$s di layar %2$d + Mengimpor + Mengganti + Memulihkan + Membatalkan + Menghapus + tidak dapat membaca file + Diimpor %1$s + Pergantian yang tertunda dibatalkan. + Pilih paket komponen (-linux .wcp) + Nightlies tidak dapat dihubungi + Disimpan %1$s + Unduhan gagal: %1$s + Pilih driver runtime Linux (zip -Linux atau bundle Android + Linux) + Pilih driver tampilan (zip AdrenoTools atau bundle Android + Linux) + Pilih zip driver atau bundle Android + Linux + %1$s: %2$s + Menghapus %1$s… + Melupakan %1$s… + Pilih folder penyimpanan game + Pilih ZIP plugin Decky + Tidak dapat digunakan: %1$s + Program + Menghapus runtime Linux + Tidak dapat membuka Setelan di layar bawah; dibuka di layar utama. + Buka Opsi Pengembang di Setelan + + Tidak dapat membuka setelan Android “Instal aplikasi tidak dikenal” + Tidak dapat memeriksa: %1$s + Build ini tidak memiliki unduhan untuk salinan DroidDeck ini + Hentikan sesi yang berjalan dulu, baru perbarui + Mengunduh + Menginstal + Menunggu konfirmasi Anda + Pembaruan gagal + Android tidak menawarkan untuk menginstal pembaruan + + Instalasi Decky gagal: %1$s + Memulai impor plugin… + Impor plugin gagal: %1$s + Plugin terinstal. Mulai ulang Steam untuk memuatnya. + + Belum diperiksa - ketuk refresh untuk mencari driver baru + Android + Linux: mengatur driver tampilan dengannya + Android + Linux: mengatur driver runtime dengannya + Turnip bawaan runtime + Vulkan sistem: tidak ada build bawaan untuk GPU ini + %1$s (bawaan) + Manual + %1$s · %2$s + Hanya satu bagian yang dirilis saat ini + %1$.0f MB untuk keduanya + Tidak ada driver untuk diatur: %1$s + Memeriksa driver terbaru… + Sudah terbaru (tidak dapat memeriksa: %1$s) + Driver yang direkomendasikan belum dirilis saat ini + Mengunduh %1$s %2$s… + Diperbarui ke %1$s %2$s. + Diperbarui ke %1$s %2$s. Driver tampilan berlaku setelah DroidDeck dimulai ulang. + Beralih ke %1$s %2$s. + Beralih ke %1$s %2$s. Driver tampilan berlaku setelah DroidDeck dimulai ulang. + Menggunakan %1$s %2$s. + Menggunakan %1$s %2$s. Driver tampilan berlaku setelah DroidDeck dimulai ulang. + Bukan zip driver: bukan driver AdrenoTools, Turnip -Linux, maupun bundle Android + Linux + Impor gagal: %1$s + Driver diimpor + Menggunakan %1$s sebagai driver runtime dan tampilan. + Menggunakan %1$s sebagai driver runtime dan tampilan. Driver tampilan berlaku setelah DroidDeck dimulai ulang. + Dihapus %1$s (kedua driver) + Dihapus %1$s + Terbaru: %1$s · diperiksa %2$s + Terbaru: %1$s · %2$s tidak dapat dihubungi · diperiksa %3$s + + %1$d mnt lalu + + + %1$d jam lalu + + + %1$d hari lalu + + Memeriksa Banners-Turnip, WinNative, dan DroidDeck… + pemeriksaan gagal + Terinstal %1$s - pilih di salah satu daftar untuk mengatur kedua driver + Terinstal %1$s - pilih di menu + + Tunggu penghapusan runtime selesai + Game + touchpad + langsung + Pergantian gagal: %1$s + Penyimpanan profil gagal: %1$s + Desktop dijeda + %1$s dijeda + Program dijeda + Memulai desktop + Memulai %1$s + Memulai program + Tidak ada log untuk sesi ini. + Tidak dapat membuat arsip log sesi. + Tidak dapat membagikan log sesi. + Memulai sesi… + mengunduh runtime Linux + mengunduh runtime Linux · %1$d dari %2$d MB + memeriksa runtime Linux + mengekstrak runtime Linux + mengunduh desktop + mengunduh desktop · %1$d dari %2$d MB + memeriksa desktop + mengekstrak desktop + mengunduh Proton Experimental (ARM64) + mengunduh Proton Experimental (ARM64) · %1$d dari %2$d MB + memeriksa Proton Experimental (ARM64) + mengekstrak Proton Experimental (ARM64) + Tidak dapat menghubungi katalog runtime. Periksa koneksi dan tekan Mainkan lagi. + Runtime Linux tidak terinstal. Periksa runtime di Penyiapan dan tekan Mainkan lagi. + Desktop tidak terinstal (%1$s). Periksa koneksi dan tekan Desktop lagi. + %1$s berhenti secara tak terduga. Bagikan log bersama laporan bug, atau coba lagi. + Program berhenti secara tak terduga. Bagikan log bersama laporan bug, atau coba lagi. + Desktop berhenti secara tak terduga. Bagikan log bersama laporan bug, atau coba lagi. + Status keluar %1$d · %2$s + tidak ada log + + Log menunjukkan %1$d error “Function not implemented”: akselerasi seccomp proot gagal pada salah satu helper di perangkat ini. Coba %2$s → “%3$s” (atau “%4$s”) lalu mulai lagi. + + + Plugin + Instal file ZIP plugin untuk sesi Steam berikutnya. + Mengunduh… + Instal dari ZIP + Gim + Audio & Video + Grafis + Internet + Perkantoran + Pengembang + Pendidikan + Sains + Sistem + Utilitas + Populer + Tren + Baru + Diperbarui + memuat… + Penyiapan Flatpak + %1$s: %2$s + Tidak dapat memeriksa pembaruan di Flathub + terinstal + folder + Graphite + Permukaan graphite, dengan biru orb khas logo untuk semua yang dipilih, difokuskan, atau ditekan. + Kertas di atas hitam + Putih adalah warna utama, biru pada ikon menjadi sinyal untuk garis, cincin, dan titik. + Biru ikon + Dasar hitam, tipe putih, biru untuk semua yang dipilih, difokuskan, atau ditekan. + Navy elektrik + Biru yang lebih cerah, pada permukaan yang condong ke navy, bukan abu-abu. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Instal runtime terlebih dahulu + Pustaka Steam tidak tersedia + Pustaka SD sebelumnya tidak tersedia + Penyimpanan gim tidak tersedia + Folder gim yang ditambahkan tidak tersedia + Manifes Steam tidak tersedia + Banyak sistem + Internal + Sistem Linux + Kartu SD + Penyimpanan + Pilih + Properti + Tambahkan ke Favorit + Hapus dari Favorit + Ganti nama + Salin + Potong + Tindakan + Favorit + Sematkan folder saat ini + Belum ada favorit - sematkan folder dengan menu ⋮-nya untuk kembali ke sini dengan cepat. + Hapus dari favorit + 0 byte + byte + Lokasi + Jenis + Ukuran + Diubah + Folder + File + File %1$s + Hanya-baca + Mencegah gim (dan Wine) menimpa atau menghapus file ini. + Tidak dapat mengubah Hanya-baca + Tersembunyi + Penyimpanan ini tidak dapat menyimpan tanda tersembunyi. + Menandai file tersembunyi di Windows (atribut tersembunyi milik Wine). + Tidak dapat mengubah Tersembunyi pada penyimpanan ini + Selesai + Menghapus... + Penghapusan gagal + Memindahkan... + Menyalin... + Memindahkan - %1$s + Menyalin - %1$s + Memindahkan %1$d/%2$d - %3$s + Menyalin %1$d/%2$d - %3$s + %1$d selesai, %2$d gagal + %1$d selesai, %2$d dilewati + + %1$d item dipindahkan + + + %1$d item disalin + + \"%1$s\" sudah ada + Mengganti nama... + Gagal mengganti nama + Membuat folder... + Tidak dapat membuat folder + Folder Baru + Nama folder + Buat + Nama baru + Hapus? + Hapus \"%1$s\" secara permanen? + + Hapus %1$d item? + + Ini tidak dapat dibatalkan. + + …dan %1$d lainnya + + Menghapus %1$d/%2$d - %3$s + + %1$d tidak dapat dihapus + + Gabungkan menambahkan dan mengganti file di dalam folder yang sudah ada. + Timpa mengganti file yang sudah ada. + Terapkan ke semua konflik + Gabungkan + Timpa + Simpan keduanya + Lewati + Pilih folder ini + Kembali + %1$s terpasang tetapi tidak dapat dibaca saat ini + Tampilan daftar + Tampilan kisi + Cari + Urutkan + Nama + Tanggal diubah + Baris nyaman + Baris ringkas + Sembunyikan file tersembunyi + Tampilkan file tersembunyi + Sembunyikan favorit + Tampilkan favorit + Filter folder ini + %1$s bebas + %1$d dipilih + Tidak Ada + Semua + Tempel + + %1$d item + + Tempel %1$s di sini + Tempel %1$s (pindahkan) di sini + File + PENYIMPANAN + CEPAT + FAVORIT + Unduhan + Unduhan + ROM + Log sesi + Gambar + \"%1$s\" ditambahkan ke Favorit + \"%1$s\" dihapus dari Favorit + Direktori kosong + Naik satu tingkat + Pilih folder + Pilih file + Biru Steam + Langit + Putih + Violet + Hijau + Amber + Merah + Merah muda + Tombol A + Tombol B + Tombol X + Tombol Y + Bumper kiri + Bumper kanan + Trigger kiri + Trigger kanan + Tombol View + Tombol Menu + View + Menu + D-pad atas + D-pad bawah + D-pad kiri + D-pad kanan + Tersembunyi + Seret untuk memindahkan + Ratakan + Simpan + Tidak ada + Keyboard + trackpad + Terminal + Pegangan Deck + trackpad + + Menghapus runtime Linux + + Menghapus runtime Linux · %1$d entri + + Runtime Linux dihapus + Tidak dapat menyelesaikan penghapusan runtime Linux. Coba hapus lagi di Penyiapan. + Menghapus sisa runtime + Mengunduh + Memverifikasi + Mengekstrak + Ditampilkan saat runtime Linux diinstal atau dihapus + Menginstal runtime Linux + Menghapus runtime Linux + + Runtime Linux tidak terinstal + Katalog tidak memiliki checksum untuk %1$s + Checksum tidak cocok - tidak ada yang diubah + Menginstal %1$s + Tidak dapat menempatkan AppImage + Ekstraksi gagal + Instalasi gagal + + Penyiapan Flatpak gagal (keluar %1$d) + Penyiapan Flatpak tidak selesai + Tugas toko lainnya sedang berjalan + Siapkan Flatpak terlebih dahulu + Memulai + Flatpak keluar dengan status %1$d + Menginstal %1$s + Memperbarui %1$s + Menghapus %1$s + Menginstal %1$s (%2$d/%3$d) + Memperbarui %1$s (%2$d/%3$d) + Menghapus %1$s (%2$d/%3$d) + terjemahan + driver grafis + kodek media + Runtime %1$s + Runtime %1$s %2$s + + File terlalu kecil untuk menjadi AppImage + File tidak dapat dibaca: %1$s + Ini bukan AppImage (bukan program Linux) + AppImage ini tidak dibuat untuk ARM64 (aarch64) maupun PC x86 + Ini adalah program Linux tetapi bukan AppImage + Hanya AppImage tipe 2 yang dapat diimpor (yang ini tipe %1$d) + Ruang kosong tidak cukup: mengimpor memerlukan sekitar %1$s + Ruang kosong tidak cukup: memperbarui memerlukan sekitar %1$s + Impor gagal + AppImage itu sudah tidak ada + Pembaruan gagal + Mengekstrak %1$s + AppImage tidak dapat diekstrak + AppImage tidak dapat diekstrak: %1$s + + Hentikan sesi aktif sebelum menginstal Decky Loader + Unduhan Decky Loader gagal + Ukuran Decky Loader tidak sesuai dengan metadata rilis + Tidak dapat mengunduh checksum rilis + Checksum rilis tidak ada + Checksum rilis tidak valid + Checksum Decky Loader tidak cocok + Aset rilis bukan file eksekusi Linux 64-bit yang kompatibel + Sesi dimulai saat pengunduhan berlangsung; hentikan sesi tersebut sebelum menginstal Decky Loader + Tidak dapat menandai PluginLoader sebagai dapat dieksekusi + Tidak dapat mengaktifkan debugging jarak jauh CEF Steam + Tidak dapat menyiapkan PluginLoader + Tidak dapat mempertahankan PluginLoader yang ada + Tidak dapat mengaktifkan PluginLoader baru + Hentikan sesi aktif sebelum menginstal plugin + Instal Decky Loader sebelum mengimpor plugin + Tidak dapat membaca file ZIP yang dipilih + Tidak dapat membuat direktori plugin Decky + Mengekstrak ZIP plugin + ZIP plugin harus berisi satu folder plugin tingkat atas + ZIP plugin tidak memiliki plugin.json, package.json, atau dist/index.js + Nama plugin tidak ada + Sesi dimulai saat instalasi berlangsung; hentikan sesi tersebut sebelum menginstal plugin + Plugin terinstal · mulai ulang Steam untuk memuatnya + Instalasi ZIP plugin gagal + ZIP plugin berisi terlalu banyak entri + ZIP plugin berisi jalur yang tidak valid + ZIP plugin berisi jalur duplikat + ZIP plugin berisi jalur yang tidak aman + Tidak dapat membuat folder plugin + ZIP plugin mengembang melebihi batas 1 GiB + Tidak dapat menandai biner plugin bawaan sebagai dapat dieksekusi + Entri remote_binary tidak valid + Nama biner jarak jauh tidak valid + URL biner jarak jauh harus menggunakan HTTPS + Biner jarak jauh %1$s memiliki hash SHA-256 yang tidak valid + Tidak dapat membuat direktori bin plugin + Mengunduh biner plugin · %1$s + Tidak dapat mengunduh biner plugin %1$s + Checksum biner plugin tidak cocok · %1$s + Tidak dapat menandai biner plugin sebagai dapat dieksekusi · %1$s + Tidak dapat mengganti biner plugin · %1$s + Tidak dapat menginstal biner plugin · %1$s + Folder plugin milik plugin lain + Tidak dapat mempertahankan plugin yang ada + Tidak dapat mengaktifkan plugin yang diimpor + Tidak dapat memperbarui setelan plugin Decky + Tidak dapat memulihkan plugin yang ada + + Tidak dapat menyimpan identitas Debugging nirkabel + Perintah ADB gagal (%1$d) + Android tidak mengonfirmasi bahwa batas proses turunan telah diubah: %1$s + Tidak ada pemasangan Debugging nirkabel yang tersimpan + Masukkan alamat IP & Port Debugging nirkabel saat ini + + Waktu tunggu munculan pemasangan habis. Coba lagi dari DroidDeck. + Tidak dapat mencari munculan pemasangan (%1$d). Pastikan Wi-Fi aktif. + Tidak dapat mencari munculan pemasangan: %1$s + Masukkan ke-6 digit kode tersebut + Memasangkan… + Kode tersebut tidak berfungsi. Buka kembali munculan pemasangan dan masukkan kode barunya. + Terpasang. Menghubungkan… + Terpasang, tetapi port Debugging nirkabel tidak ditemukan. Biarkan Debugging nirkabel tetap aktif dan coba lagi. + Menerapkan setelan… + Perintah Debugging nirkabel gagal + Penyiapan Debugging nirkabel + Meminta kode pemasangan Debugging nirkabel saat Setelan terbuka + Pasangkan Debugging nirkabel + Aktifkan Debugging nirkabel, lalu ketuk “Pasangkan perangkat dengan kode pemasangan”. + Aktifkan Debugging nirkabel, lalu ketuk “Pasangkan perangkat dengan kode pemasangan”. Tetap di Setelan: kodenya akan muncul di notifikasi ini. + Kode pemasangan 6 digit + Ketuk Masukkan kode dan ketikkan 6 digit yang ditampilkan di Setelan. + Masukkan kode pemasangan Wi-Fi + Coba kode lagi + Masukkan kode + Menyiapkan Steam + Steam siap + Ketuk untuk kembali ke DroidDeck. Anda bisa menonaktifkan Debugging nirkabel sekarang. + Penyiapan Debugging nirkabel dihentikan + + Unduhan merespons HTTP %1$d + Ukuran unduhan tidak sesuai dengan APK yang dipublikasikan dan dibuang - coba lagi + Unduhan tidak sesuai dengan checksum-nya dan dibuang - coba lagi + Pembaruan ini untuk paket Android %1$s, bukan %2$s + Metadata pembaruan tidak mencantumkan kunci penandatanganan rilis DroidDeck + Android tidak dapat membaca APK yang diunduh + APK yang diunduh untuk %1$s, bukan %2$s + APK yang diunduh mencantumkan versionCode %1$d, bukan %2$d yang dipublikasikan + Android tidak akan menginstal versionCode %1$d di atas %2$d yang terinstal + APK yang diunduh tidak memiliki informasi penandatanganan + APK yang diunduh tidak ditandatangani dengan kunci rilis DroidDeck + Katalog pembaruan merespons HTTP %1$d + + Memulai sesi… + %1$d:%2$02d berlalu · masih berjalan + Mengunduh pembaruan klien Steam · %1$d%% + Mengunduh klien Steam · %1$s + + Instalasi alat kompatibilitas sedang berjalan + Hentikan sesi aktif sebelum menginstal alat kompatibilitas + Tidak dapat menemukan rilis ARM64 untuk %1$s + Setidaknya 4 GB ruang kosong ditambah ukuran unduhan diperlukan untuk menginstal %1$s + Mengunduh %1$s %2$s + Mencoba lagi unduhan · percobaan %1$d dari %2$d + Unduhan terputus + Unduhan gagal; file sebagian disimpan agar percobaan ulang dapat dilanjutkan + Unduhan melebihi ukuran rilis dan telah dihapus + Unduhan tidak lengkap; file sebagian disimpan untuk percobaan ulang yang dapat dilanjutkan + Memverifikasi unduhan + Tidak dapat membaca checksum rilis; coba lagi + %1$s %2$s tidak mempublikasikan checksum; tidak ada yang diinstal + Checksum tidak cocok; unduhan telah dihapus + Sesi dimulai saat pengunduhan berlangsung; hentikan sesi tersebut sebelum menginstal alat kompatibilitas + Menginstal %1$s + Instalasi %1$s gagal (keluar %2$d) + Tidak dapat menginstal %1$s + Mengambil paket droiddeck-esync + + dengan helper Linux + Hanya DLL + Asli (Proton diperbarui sejak penggantian terakhir) + Diubah di luar Komponen + tidak ada + Asli %1$s + Asli dari %1$s + %1$s %2$s akan dipasang ke %3$s saat gim yang berjalan di sana ditutup. + %1$s %2$s sekarang ada di %3$s. Berlaku pada peluncuran gim berikutnya. + Paket asli tersebut tidak tersimpan + %1$s dari %2$s akan dikembalikan ke aslinya saat gim yang berjalan di sana ditutup. + %1$s dari %2$s dipulihkan dari aslinya %3$s. + %1$s sedang digunakan di %2$s. Ganti Proton tersebut ke yang lain terlebih dahulu. + %1$s tidak tersimpan + %1$s dihapus. + Yang asli dari build terinstal disimpan: itulah cara untuk kembali. + Tidak tersimpan + Menghapus yang asli %1$s dari %2$s. + Unduhan hanya berasal dari rilis Nightlies + Daftar paket ini dibuat sebelum adanya checksum - segarkan dan coba lagi + Checksum tidak cocok + + Linear + Nearest + Snapdragon GSR (kualitas) + Hanya penajaman + Otomatis (sesuaikan skala) + + Tidak ada yang diterapkan. + Kejernihan Gim + Penajaman dengan sedikit kontras - cocok untuk sehari-hari. + Vivid + Warna didorong lebih kuat. Deband aktif agar langit tidak bergaris. + Sinematik + Kontras lebih dalam, warna lebih dingin, tepi dihaluskan. + Kompetitif + Bayangan diangkat dan penajaman berat - temukan orang lebih dulu. + Penajaman Adaptif + Hanya penajaman, warna tidak diubah. + Filmic + Kurva seperti film, sedikit diredam. + Arcade + Mencolok dan berdampak kuat. + CRT Retro + Tabung CRT dan sinyal analog digabungkan bersama. + Upscale Tajam + Untuk menjalankan di bawah resolusi panel. + Piksel Bersih + Piksel tajam untuk gim 2D dan judul lawas. + Tepi Anime + Garis luar toon ditambah penajaman. + + Bukan GPU Adreno + Didukung + Eksperimental: drivernya adalah build uji + Di bawah perangkat keras yang diuji (Adreno 725 dan lebih baru): mungkin tidak dapat berjalan + Tidak didukung: DroidDeck memerlukan GPU Adreno (Snapdragon) + + Batas kecepatan GitHub tercapai - coba lagi nanti + GitHub merespons HTTP %1$d + unduhan merespons HTTP %1$d + Daftar driver ini dibuat sebelum adanya checksum - segarkan dan coba lagi + Checksum tidak cocok - unduhan dibuang + Bukan driver AdrenoTools: %1$s + tidak ada meta.json + meta.json tidak dapat dibaca (%1$s) + ini adalah driver runtime Linux (impor di bagian “Linux runtime driver”) + ini adalah driver gim Wayland, yang tidak digunakan oleh aplikasi ini + meta.json tidak menyebutkan libraryName, sehingga tidak ada yang dapat diserahkan ke AdrenoTools + meta.json menyebutkan %1$s, yang tidak ada di dalam zip + %1$s bukan library bersama ELF AArch64 64-bit + %1$s menautkan glibc - Turnip “-Linux”, yang termasuk di bagian “Linux runtime driver” + Tidak ada libvulkan_freedreno*.so dalam zip ini. Zip Turnip Android (AdrenoTools) atau -Wayland bukan driver runtime Linux. + %1$s bukan library bersama ELF AArch64 64-bit. + %1$s bukan driver glibc - ia menautkan libc milik Android. Runtime Linux memerlukan zip “-Linux”; Turnip biasa atau “-Wayland” sama sekali tidak dapat dimuat oleh klien Steam. + Bukan paket driver: tidak ada manifest.json yang menyebutkan driver android dan linux + Bukan paket driver: %1$s mengarah ke luar paket + Bukan paket driver: ukurannya setelah diekstrak lebih dari %1$d MB + + Kartu SD + Folder + + Bagikan log sesi + + memerlukan Android 10 atau yang lebih baru (lapisan tampilan dengan ruang warna) + tampilan ini melaporkan tidak ada dukungan HDR + tampilan ini mendukung %1$s, bukan HDR10 + + GPU ini + Batas proses turunan aktif + Batas proses turunan belum diatur + Batas proses turunan tidak dapat diperiksa + Batas proses turunan nonaktif + Batas proses turunan tidak diperlukan + %1$s Jika itu tidak memungkinkan, sambungkan perangkat ini ke komputer dengan ADB dan jalankan perintah di bawah. Kembali ke sini dan periksa lagi. + Sesi Steam dapat dimulai. + Versi Android ini tidak menggunakan batas ini. + Aktifkan “Disable child process restrictions” di Opsi developer, dan biarkan Opsi developer tetap aktif setelahnya. + Versi Android ini tidak memiliki sakelar untuk itu. Gunakan Debugging nirkabel untuk mengubahnya dari perangkat ini. + Build ini tidak memiliki unduhan untuk salinan DroidDeck ini. + Android tidak akan menginstalnya di atas build ini karena versionCode %1$d lebih rendah dari %2$d yang terinstal. + Android tidak akan menginstal versi yang lebih lama di atas versi yang lebih baru. Tetap gunakan build ini sampai saluran tersebut menyusul. + Salinan DroidDeck ini ditandatangani secara berbeda (build lokal?), sehingga Android tidak akan memperbaruinya di tempat. + Ruang kosong tidak cukup untuk pembaruan ini. + Pembaruan dibatalkan. + Pembaruan tidak terinstal: %1$s + Pembaruan tidak terinstal. + zip %1$s + zip + %1$s belum pernah dijalankan + Tidak ditemukan file simpanan untuk %1$s + %1$s tidak memiliki folder drive_c, sehingga bukan zip simpanan GameHub atau Winlator + tidak dapat membuat folder di %1$s + tidak dapat menulis di %1$s + tidak dapat menulis di %1$s (%2$s) + Folder tidak tersedia + File lain sudah menggunakan %1$s + Folder artwork tidak tersedia + Folder memiliki inventaris sinkronisasi yang tidak terkait + Tidak dapat menghapus %1$s + File berada di luar folder ekspor + FEX (build tanpa nama versi) + tidak ada profile.json di %1$s + %1$s bukan paket komponen Linux (tipe %2$s) + %1$s · %2$s kosong + Otomatis + Katalog pembaruan gagal dalam pemeriksaannya (%1$s) + Setelan proses turunan DroidDeck + Masuk · mode offline + Masuk + BAWAAN + DIUNDUH + DIIMPOR + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Tampilan sesi + Resolusi + Penyesuaian gim 16:9 + Cara gim 16:9 disesuaikan dengan tampilan ini. Berlaku pada sesi berikutnya. + Pilih Sesuaikan layar atau ukuran khusus yang lebih sempit dari 16:9. + Pertahankan proporsi + Bentangkan ke tampilan + Penskalaan gambar + Filter penskalaan + Digunakan bersama oleh Steam dan Desktop. + Berlaku langsung. Digunakan bersama oleh Steam dan Desktop. + FSR, GSR, dan NIS berlaku saat memperbesar gambar sesi. Hanya penajaman juga berfungsi tanpa pembesaran. + Ketajaman filter + Frame rate + %1$s Mengatur skala ke %2$s. + Sesuaikan layar (%1$d×%2$d) + Ikuti layar (ukuran berubah saat dilipat atau dibuka) + Steam + Tampilkan detail perangkat + Sembunyikan detail perangkat + Membaca perangkat… + Salin + Salin detail ini ke dalam laporan bug. + Disalin. + Paksa SSBS + Menjaga setelan kecepatan CPU yang terus dinonaktifkan oleh Wine. Dapat membuat gim Windows lebih cepat di Snapdragon 8 Elite; perubahan kecil terlihat pada chip yang lebih lama. Berlaku pada peluncuran gim berikutnya. + + Toko + Steam + Khusus + Toko + Buka toko di + Tingkat kecepatan unduhan + Mesin toko + ● Mesin siap + Belum dibuat + Seimbang + Cepat + Maks + Toko + Setelan toko + Setelan + (belum masuk) + Toko + Terinstal + Pustaka + Semua + Unduhan + %1$s (%2$d) + Masuk + %1$s · keluar + Keluar dari %1$s? + Gim yang terinstal tetap ada. Pustaka diambil lagi saat Anda masuk kembali. + Tetap masuk + Keluar + Cari katalog %1$s… + Cari gim yang terinstal… + Cari pustaka Anda… + Cari + Belum ada yang terinstal + Memuat + Tidak ada gim + Memuat toko… + Tidak ada katalog + Yang baru + Penawaran + Gim gratis + Sedang tren + Pustaka Anda + Mainkan + Instal + Lanjutkan instalasi + Terhenti + Instal · %1$s + Dapatkan gratis + Lihat di %1$s + Beli · %1$s + Terinstal + Dalam pustaka + Gratis + Lanjutkan + Segarkan + Tampilkan konten dewasa + Epic + EOS + Offline + Nonaktif + Masuk Epic (EOS) + Jalankan secara offline + Selesaikan masuk Epic + Simpanan cloud + Unggah ke cloud + Unduh dari cloud + Tidak ada simpanan cloud + Konflik cloud + Konflik cloud · %1$d file + … + Pertahankan cloud + Pertahankan lokal + Memeriksa cloud… + Sinkronisasi terakhir %1$s + Belum disinkronkan + %1$d file + terinstal + dalam pustaka Anda + toko + Pencapaian + %1$d dari %2$d terbuka + %1$d / %2$d + Terbuka · %1$d + Terkunci · %1$d + Tersembunyi · %1$d + Pencapaian tersembunyi + Detail ditampilkan setelah terbuka. + Semua pencapaian terbuka + Memuat… + Masuk ke %1$s untuk melihat pencapaianmu. + Tidak dapat menjangkau %1$s. Periksa lagi saat sudah online. + %1$s tidak mencantumkan pencapaian untuk game ini. + Tampilkan semua %1$d + Pencapaian · %1$s + ✓ Di Steam + Hapus instalasi + Menghapus instalasi… + Hapus instalasi %1$s? + Membebaskan %1$s dan menghapusnya dari Steam. + Menghapus filenya dan pintasan Steam-nya. + Steam akan mendeteksi gim ini saat berikutnya dimulai; jalankan dari sana, atau mulai Steam sekarang. + Proton + Pilihan Steam per gim + Audio + DirectAudio + Komponen + FEX, DXVK, VKD3D + Mesin + %1$s · asli + %1$s · bawaan + Cara menjalankannya + Diambil dari + %1$s, dengan akun Anda + Terinstal di + Penyimpanan gim (Games/%1$s/%2$s) + Dijalankan oleh + Steam dan Proton, jalur yang sama seperti gim Steam + Steam dan Proton · Masuk Epic setiap peluncuran, overlay EOS nonaktif + Steam dan Proton · Variabel Amazon Games Services diatur + Steam + ditambahkan ke Steam saat instalasi (UI Deck + Steam Input) + Tidak ada unduhan + Dijeda + Dalam antrean + Dalam antrean · #%1$d + Dibatalkan + Gagal + Gagal · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Jeda + Lanjutkan + Batal + Coba lagi + Hapus + Hapus yang selesai + Manifest + Unduhan + Verifikasi + Instal + Selesai + Memeriksa + Mengunduh + Hapus unduhan? + Gagal · %1$s disimpan + Memverifikasi + Menginstal + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d file + Manajer unduhan + Unduhan sekaligus + Instal ke + Instal %1$s: + Internal + %1$s tersedia + Unduhan toko + Menampilkan progres unduhan gim dan menjaga unduhan tetap berjalan di latar belakang + Mengunduh + %1$d unduhan + Menyiapkan… + Unduhan toko selesai + %1$s terinstal + Unduhan %1$s gagal + Hapus + Pertahankan + Masuk ke %1$s + Masuk dengan penyedia Anda + Muat ulang + Tutup + Coba lagi + Menyelesaikan proses masuk… + Halaman masuk tidak dapat dimuat: %1$s + Penyedia ini menolak proses masuk dari halaman tersemat. Masuk dengan kata sandi toko Anda sebagai gantinya. + Halaman masuk merespons dengan kesalahan (HTTP %1$d). + Koneksi ke halaman masuk tidak aman. + Proses masuk tidak dapat diverifikasi. Coba lagi. + Proses masuk gagal. Coba lagi. + Tutup + Kembali + Edit + Tambahkan semua gim di folder ini + Tidak ada yang baru di folder ini + Nama + Target + Mulai di + Opsi peluncuran + Artwork + Exe lain… + Hapus + Sampul + Latar belakang + Logo + Ikon + Folder gim + Steam + SteamGridDB + File Anda + Dari exe + Otomatis + Pilih gambar… + Pilih gambar: %1$s + Setel ulang ke otomatis + + %1$d gim ditambahkan + + + %1$d sudah ada di Games + + B · Kembali ke %1$s + gim ini + Setelan Main + Driver grafis + Proton ini + Setiap Proton + Untuk setiap Proton + Tersedia · %1$d + Diperiksa %1$s + Periksa komponen dan driver baru + Dapatkan + %1$s · DXVK, VKD3D-Proton, dan FEX di bawah ini milik Proton ini + %1$s · diuji + %1$s · belum diuji + %1$s · tidak didukung + Menjaga pasangan yang direkomendasikan tetap terbaru + Otomatis · %1$s + Runtime dan driver tampilan secara terpisah + Gunakan %1$s? + Hapus %1$s? + Diagnostik + Siap bermain + Belum siap bermain + runtime %1$s + batas proses nonaktif + masuk sebagai %1$s + Saat ada yang memerlukan Anda + Tampilan + Perangkat ini + Artwork pustaka + APLIKASI LINUX + Flathub + Aplikasi desktop untuk desktop Linux, dibuka dari Desktop. + Siapkan Flathub + Hapus %1$s? + Setelan dan filenya tetap disimpan. + Pertahankan + Ukuran di disk + Hapus instalasi desktop? + Semua + Pencapaian GOG + Menyinkronkan pencapaian untuk game GOG yang didukung. Berlaku saat peluncuran berikutnya. + Pencapaian nonaktif + Pencapaian aktif + Masuk lagi ke GOG + Pencapaian terunggah + Unggahan pencapaian tertunda + Aktifkan pencapaian GOG di setelan Stores + Pencapaian tidak tersedia; periksa log sesi + Pencapaian terbuka + Tempel + Tombol arah + Stik kiri + Stik kanan + Menu + Keyboard + Tombol arah atas + Tombol arah bawah + Tombol arah kiri + Tombol arah kanan + Kontrol sentuh + Kontroler sentuh + Mouse + Aktif + Nonaktif + Lambat + Normal + Cepat + Edit tata letak + Menu DroidDeck + Seret kontrol ke layar + Atur + Buang + Menyimpan… + Ikon + L− + L+ + T− + T+ + Sembunyikan + Warna dan opasitas… + Salin tata letak ini + Tata letak disalin; tempelkan ke tata letak sentuh game apa pun + Tempel tata letak yang disalin + Setel ulang ke default Steam + Kontrol ini tidak memiliki pengikatan di Steam untuk diberi ikon + Ikon untuk %1$s + Warna tata letak sentuh + Selesai + Tata letak sentuh tidak dapat disimpan + Tata letak sentuh disimpan ke Steam + App %1$d + Label (opsional) + Ikon Steam tidak ditemukan + + %1$d ikon dari Steam + + Warna ikon + Warna tombol + Tidak ada ikon + + Memeriksa status masuk… + Mengambil pustaka kamu… + Mengambil daftar game… + Memuat detail game… (%1$d/%2$d) + + Menyinkronkan %1$d game… + + Menyinkronkan… %1$d/%2$d + Mengambil gambar sampul… + Sinkronisasi gagal + Tidak ada game yang ditemukan di pustaka %1$s kamu + Amazon Games tidak merespons untuk pustaka kamu: segarkan, atau masuk lagi + Tidak dapat menghubungi GOG + Pustaka GOG kamu tidak dapat dibaca + Mendaftar ke Steam… + Cardboard VR + Mode Cardboard + Tampilan mata kiri dan kanan untuk kacamata VR ponsel. + Gambar + Layar virtual + 3D berdampingan + Kacamata + Cardboard + Cardboard (2014) + Tanpa lensa + Jarak mata + Lebar layar + Jarak layar + Pelacakan kepala + Perangkat ini tidak punya sensor rotasi. + Posisikan ulang tampilan + Posisikan ulang + SteamVR (eksperimental) + Pasang + Mengunduh SteamVR ARM64 dari Valve (571 MB). + Mengunduh dan mengekstrak SteamVR… + Mulai + Menjalankan SteamVR dengan kepala Anda sebagai headset; gambar beralih ke 3D berdampingan. + Hentikan + SteamVR sedang berjalan. + Tidak tersedia di sesi ini. + SteamVR tidak merespons; periksa log sesi. + Unduh komponen + Unduhan dipakai bersama halaman Komponen. Pilih versi yang sudah diunduh di atas untuk memakainya pada peluncuran gim berikutnya. + Pilih versi untuk diunduh + Pengembang + Penerbit + Rilis + Utama + + Ekstra + Tuntas + %1$s j + %1$d mnt + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 000000000..26e984240 --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,2260 @@ + + + DroidDeck + + Al primo avvio viene scaricato Steam. Potrebbero volerci alcuni minuti. + Per impostazione predefinita, uscire dall\'app mette in pausa Steam; al tuo ritorno riprende da dove si era interrotto. + Il tuo controller viene visto come un pad Xbox 360. In alternativa, usa i controlli su schermo. + + + Al primo avvio viene configurato il desktop. Potrebbero volerci alcuni minuti. + Per impostazione predefinita, uscire dall\'app mette in pausa la sessione; al tuo ritorno riprende da dove si era interrotta. + Il touchscreen funziona come un touchpad: trascina per spostare il puntatore, tocca per fare clic. + Il tuo controller viene visto come un pad Xbox 360. In alternativa, usa i controlli su schermo. + + Sessione + Visibile mentre è aperta una sessione Steam o desktop + Steam è in esecuzione + Download di Steam in corso + Sessione in pausa - tocca Riprendi quando sei pronto + Riprendi + Sospendi ora + Termina sessione + Generazione di frame + Seleziona la modalità di generazione di frame + Disattivata + Win-FG %1$d× + LSFG %1$d× + LSFG adattivo %1$d + Richiede Lossless Scaling su Steam + Installa Lossless Scaling in Steam o importa Lossless.dll + Importa Lossless.dll… + In uso la copia di Steam + In uso una copia importata + Scegli Lossless.dll + Lossless.dll importato + Questo Lossless.dll è già in uso + Questo account Steam non possiede Lossless Scaling + Questo file non è il Lossless.dll di Lossless Scaling + Impossibile preparare LSFG da questo Lossless.dll + LSFG richiede Lossless Scaling da Steam o un Lossless.dll importato + LSFG non è riuscito a preparare gli shader da Lossless.dll + %1$s: non supportato + %1$s: errore del motore + %1$s: richiede Lossless Scaling + %1$s %2$s · avvio + Aggiunge una libreria di Steam e la analizza alla ricerca di giochi Windows. I giochi posseduti usano le loro voci di Steam; gli altri diventano collegamenti non di Steam. + Ambiente di gioco + Variabili d\'ambiente per i giochi Proton. + Modifica + Tutti i giochi Proton + Profilo + ID app di Steam + Apri + Gioco %1$s + Aggiungi variabile + Nome variabile + Valore + Salva + Rimuovi + Eredita di nuovo + Cancella le personalizzazioni dell\'ambiente + Annulla impostazione per questo profilo + Nessuna variabile impostata. + Ereditata + Variabile personalizzata + Impossibile caricare o salvare. Controlla lo spazio libero e riprova. + Usa un nome valido e un valore di massimo 8192 caratteri. + Il runtime Proton standard non legge questa variabile. + Caricamento… + Sostituisce il feature level D3D12. 12_2 dichiara le funzionalità di DirectX 12 Ultimate; quelle non supportate possono causare errori. Lascia vuota per il rilevamento automatico. + Sostituisce lo shader model D3D12. 6_9 richiede un VKD3D abbastanza recente e funzionalità GPU adeguate. Lascia vuota per il rilevamento automatico. + Mantieni attiva la cache degli shader di Mesa per ridurre le compilazioni ripetute. false attiva la cache. + Dimensione massima della cache degli shader di Mesa, ad esempio 1G. Una cache più grande occupa più spazio di archiviazione. + Opzioni VKD3D per D3D12. nodxr disattiva il ray tracing; usalo solo per un gioco che lo richiede. + Imposta 1 per un log diagnostico di Proton. Il log può ridurre le prestazioni e occupare spazio. + Imposta 1 per usare WineD3D basato su OpenGL invece di DXVK, per test di compatibilità D3D9–11. + Imposta 1 per non usare ntsync (droiddeck-ntsync o un driver del kernel) in questo gioco; userà droiddeck-esync se esiste un pacchetto, altrimenti droiddeck-fsync. + Imposta 1 per disattivare droiddeck-esync in questo gioco; userà invece droiddeck-fsync, o droiddeck-ntsync se anche fsync è disattivato. + Imposta 1 per non usare droiddeck-fsync in questo gioco; userà droiddeck-esync se esiste un pacchetto, altrimenti droiddeck-ntsync. + Imposta 1 per eseguire questo gioco con droiddeck-fsync, o 0 per dare priorità a droiddeck-esync se c\'è un pacchetto adatto al suo Proton. + Imposta 0 per disattivare l\'assistente di navigazione con gamepad di Proton in questo gioco. + Overlay DXVK per D3D9–11. fps mostra il frame rate; se vuota, l\'overlay è nascosto. + Impostazioni DXVK separate da punto e virgola. Le build recenti usano dxvk.maxFrameRate per limitare i frame; per giocare normalmente è meglio il limitatore di frame della sessione. + Limite di frame rate D3D12. 0 disattiva il limite. + Threading OpenGL di Mesa. true può aiutare i giochi OpenGL limitati dalla CPU; provalo gioco per gioco. + Sincronizzazione Wine + Come i giochi mantengono sincronizzati i loro thread. Si applica dalla prossima sessione. + esync + ntsync + fsync + wineserver + Preset FEX + Si applica al prossimo avvio del gioco. + Giochi + Predefinito FEX + Usa le impostazioni predefinite di FEX. + Stabilità + Impostazioni più sicure e più lente. + Compatibilità + TSO completo con multiblock. + Intermedio + TSO bilanciato con precisione x87 ridotta. + Prestazioni + TSO del tutto disattivato; multiblock. + Prestazioni + TSO + Impostazioni Prestazioni con il TSO principale attivo. + Estremo + Disattiva i controlli sul codice automodificante. Giochi con JIT e DRM potrebbero non funzionare. + Estremo + TSO + Impostazioni Estremo con il TSO principale attivo. + Estremo-gn + Prestazioni con suggerimenti TSC e metadati. Provalo prima di Estremo. + Denuvo + Prestazioni con controlli completi sul codice e bit dell\'hypervisor nascosto. + Valore personalizzato + Seleziona valore + Emulatori e app + Aggiungi + La tua app o il tuo gioco + Script + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Avvio… + Hai aggiunto %1$s + Hai rimosso %1$s + Desktop + Quell\'app non c\'è più. Sceglila di nuovo dal Desktop. + Apri %1$s + Rimuovi + Premi di nuovo per rimuovere + Termina la sessione per rimuovere + Esegue %1$s dalla sua cartella, collegata alla sessione nella posizione in cui si trova. Rimuovere l\'app non tocca quella cartella. + Esegue %1$s da una copia della sua cartella dentro DroidDeck. Rimuovere l\'app elimina la copia, non l\'originale. + Installa prima il runtime Linux + Il file non esiste più + Spazio libero insufficiente: per copiare la cartella servono %1$s + Copia di %1$s + Impossibile aggiungere l\'app + Ricerca di una release di %1$s + Impossibile leggere le release di %1$s + Nessuna release di %1$s ha un AppImage ARM64 o x86_64 + Download di %1$s + Download non riuscito + Impossibile aggiornare l\'app + Impossibile leggere quell\'icona + Modifiche a %1$s salvate + Modifica + Controlla aggiornamenti + Controllo aggiornamenti… + Aggiorna a %1$s + Ultima versione · %1$s + Aggiornamento di %1$s completato + %1$s · %2$s + Aggiungi un\'app + Script + AppImage + GitHub + Flatpak + Un file .sh. Viene eseguito dalla sua cartella, a schermo intero in gamescope. + Un AppImage ARM64 (aarch64) o uno x86_64 da eseguire tramite FEX. Viene estratto nel runtime Linux una sola volta. + Installa l\'AppImage ARM64 dalla release più recente del repository, oppure quello x86_64 tramite FEX. + Usa FEX + Auto + Sì + No + FEX esegue programmi Linux x86 su questo dispositivo ARM64. Auto lo usa quando il programma è compilato per x86. + %1$s tramite FEX + Questo programma è compilato per PC %1$s e viene eseguito tramite FEX, che non è ancora configurato. Apri Steam una volta dopo aver eseguito l\'accesso: DroidDeck gli farà scaricare FEX e lo Steam Linux Runtime necessario, poi il programma si aprirà. + Questo programma + %1$s è compilato per PC x86 e viene eseguito tramite FEX, che non è ancora installato. Avvia Steam una volta dopo aver eseguito l\'accesso e lascia che scarichi FEX e lo Steam Linux Runtime, poi riavvia il programma. + Un ID app di Flathub o la sua pagina su Flathub. Se manca, Flatpak viene prima configurato. + File + Nessun file scelto + Scegli file + Cambia + Scegli uno script + Scegli un AppImage + Scegli un\'icona + Repository + proprietario/repo o un link github.com + Non è un repository GitHub + ID app + org.ppsspp.PPSSPP + Non è un ID app di Flathub + Oppure un file .flatpak, per un\'app che non è su Flathub. + Scegli un file .flatpak + Origine + Sul tuo Desktop + Nuova app + Nome + Dall\'app + Icona + Icona predefinita + Scegli icona + Cancella + Dal repository + Ricerca di icone… + Predefinita + Modifica %1$s + Salva + Controllo della cartella… + La sua cartella è collegata alla sessione: %1$s + La sua cartella contiene programmi Linux, che la memoria condivisa non può eseguire, quindi viene copiata in DroidDeck (%1$s). + Aggiungi + Annulla + Installa prima il runtime Linux da Configurazione. + Attendi che %1$s finisca. + Compatibile + Le build contrassegnate con “%1$s” nel menu Compatibilità di Steam includono le correzioni al prefisso di DroidDeck e avviano i giochi qui. Quella predefinita è Proton Experimental ARM64 (%1$s), oppure Proton 11.0 ARM64 (%1$s) quando è installata questa build più recente. Le voci Proton di Valve senza il contrassegno non possono avviare giochi su questo dispositivo. + Installato: %1$s · %2$s + Annulla + Auto + Mai + Sempre + Disinstalla + Controlla + Aggiorna + Controllo… + Operazione in corso… + Dimentica + Aggiungi… + Usa + Si applica dalla prossima sessione. + Sessione Steam + Sessione desktop + Schermo + Risoluzione + Fino a 720p + Fino a 900p + Fino a 1080p + Nativa + %1$s (predefinita) + Personalizzata · %1$d×%2$d + Personalizzata… + A 720p i menu possono essere più reattivi. + Proporzioni schermo + Definite dalla risoluzione personalizzata. + Auto usa almeno 16:9. + Limite frame + Limita l\'intera sessione. Lo schermo passa a una frequenza di aggiornamento multipla del limite. + HDR + Uscita HDR10 + Non disponibile: %1$s. + Riavvia l\'app per applicare. + Driver + Driver GPU + Condivisi da Steam e Desktop. Si impostano nella pagina Componenti. + Tocco e controlli + Tocco + Touchpad + Diretto + Auto: touchpad sul Desktop, diretto in Steam. + Controlli su schermo + Steam + QAM + Auto mostra i controlli quando non è collegato nessun controller. + Touch di Steam (beta) + I controlli touch di Steam partono dalla prossima sessione Steam + Layout touch di Steam + Sposta, ridimensiona o nascondi i controlli touch di questo gioco; vengono salvati su Steam + Controller + Come Steam vede il tuo controller. Si applica dalla prossima sessione. + Controller di Steam Deck + Steam Controller + DualShock 4 + DualSense + Controller Xbox 360 + Deck: tasto Accesso rapido e giroscopio. Steam Controller: lo stesso, con il rilevamento della presa sui tasti posteriori. DualShock 4 e DualSense: giroscopio, con i trackpad usati come touchpad. Xbox 360: apri Accesso rapido con Guide+A. + Indietro + Menu → QAM + QAM → Menu + Sessione + Comportamento in background + Quando l\'app è nascosta o lo schermo si spegne. + Manuale + Nativo + Auto mette in pausa e riprende quando l\'app torna visibile. Nativo prepara prima Steam alla sospensione. Manuale attende Riprendi. Mai lascia tutto in esecuzione. + Continua i download + Ritarda la sospensione in background mentre Steam sta scaricando. + Auto mette in pausa in background e riprende quando l\'app torna visibile. Manuale attende Riprendi. Mai lascia tutto in esecuzione. + Avvio + Avvia Steam con DroidDeck + Apre la sessione Steam all\'avvio di DroidDeck. + Decky + Ricerca di release… + Pronto per installare %1$s. + Nessuna build compatibile trovata. Riprova più tardi. + Installato · %1$s + Aggiornamento disponibile · %1$s + Ultima versione · %1$s + Loader + Installa l\'ultima versione + Si avvia con Steam. Quando è attivo, altre app su questo dispositivo potrebbero controllare Steam. + Disattivato. La porta di debug remoto di Steam resta chiusa. + Disinstallare Decky Loader? + Rimuove il loader. I tuoi plugin e le impostazioni vengono mantenuti. + Client + Overlay prestazioni + Disattivalo se i giochi vanno in crash o diventano neri in modalità Deck. Si applica dalla prossima sessione. + Ripara il client Steam + Scarica di nuovo il client Steam al prossimo avvio di Steam. Usalo se Steam resta sulla schermata di caricamento. Giochi, accesso e impostazioni vengono mantenuti. + Il client Steam verrà scaricato di nuovo al prossimo avvio di Steam. + Ripara + Grafica + Chiave API SteamGridDB + La tua chiave + Modifica… + Completa la grafica mancante da Steam, poi da SteamGridDB. I tuoi cover.jpg, hero.jpg, logo.png o icon.png nella cartella del gioco hanno la priorità. + Rimuovi %1$s + Rimuovere? + Aggiungi gioco + Aggiungi un gioco: + Scegli l\'exe: + Questa posizione non è disponibile nella sessione + Controlla questo exe + Componenti Windows + LB / RB per passare da una sezione all\'altra. + Nessuno + DLL di Windows non incluse in Proton, dall\'elenco dei componenti. Attiva quelle che servono a questo gioco: vanno nella cartella Windows del gioco, mai tra i suoi file, e restano con qualunque Proton venga usato. + Impossibile raggiungere l\'elenco dei componenti. I componenti già scaricati si possono comunque attivare o disattivare. + Consigliati per questo gioco + Verifica di cosa serve a questo gioco… + Disponibile quando sei online + + Attivazione di %1$d componente… + Attivazione di %1$d componenti… + Attivazione di %1$d componenti… + + Attivi per questo gioco + Steam installa %1$s con %2$s (app %3$s) + Dall\'elenco di GOG per questo gioco + Dai prerequisiti di Epic per questo gioco + Dai passaggi di installazione di Amazon per questo gioco + Steam installa %1$s con l\'app %2$s, abbinata per nome + Elencati anche + Elencato da %1$s; qui non è un componente + Steam + GOG + Epic Games + Amazon Games + Il gioco include %1$s + Il gioco ha %1$s (spesso funziona così com\'è) + Già presente in questo gioco + Nulla nella cartella del gioco indica un componente. + Installati per questo gioco (%1$d) + Tutti i componenti (%1$d) + Richiedono un programma di installazione Windows (%1$d) + Sono programmi di installazione a sé, senza un pacchetto da scomporre e ancora senza una registrazione di ciò che installano. Quasi tutti sono vecchi runtime già sostituiti dal Mono incluso in Proton; le registrazioni vengono aggiunte man mano che vengono create. + Mostra + Nascondi + Esegue un proprio programma di installazione (ancora nessuna registrazione) + Non ancora supportato qui + Caricamento dei componenti… + Impossibile installare: %1$s + Scaricato + Download di %1$s + Installazione: %1$s + Disattivazione… + %1$s da scaricare + Ha effetto al prossimo avvio del gioco. + Allarga i giochi 16:9 + Sugli schermi più quadrati di 16:9, allarga i giochi a tutto schermo invece di mostrare le bande. Si applica dalla prossima sessione. + Audio + DirectAudio per i giochi + Bypassa PulseAudio per ridurre la latenza. + Microfono + Per la chat vocale. + Rete + Rilevamento Wi-Fi + Mostra in Steam i nomi delle reti Wi-Fi e le reti vicine. + La geolocalizzazione è disattivata. + Autorizzazione Posizione non concessa. + Apri impostazioni + Continua + Attivare il rilevamento Wi-Fi? + Android richiede l\'autorizzazione Posizione per leggere i nomi delle reti Wi-Fi e rilevare le reti vicine. DroidDeck usa questo accesso per mostrare quelle reti in Steam. Non tracciamo la tua posizione. L\'accesso a Internet funziona anche senza questa autorizzazione. + Archiviazione giochi + Automatica - la scheda SD se presente, più le tue cartelle Games + Automatica - le tue cartelle Games, più la scheda SD se presente (ora assente) + Solo memoria interna + Cartella: %1$s + Seconda libreria + I giochi che caricano risorse in streaming dalla SD o dalla memoria condivisa potrebbero scattare. + Diagnostica archiviazione + Registra i risultati delle allocazioni e campioni dei tempi di archiviazione in Condividi log. Mentre è attiva può rallentare le installazioni. Si applica dalla prossima sessione. + Scegli una cartella… + Risoluzione personalizzata + Sostituisce il limite e le proporzioni. Una dimensione che non corrisponde allo schermo avrà delle bande. + Larghezza + Altezza + Tra 320×240 e 3840×2160. + + %1$d gioco + %1$d giochi + %1$d giochi + + OK + Elimina + Ripristina + Scarica + Elimina %1$s + %1$.1f MB + Driver di runtime + Usato da Steam, dai giochi e dal desktop. Si applica dalla prossima sessione. + Importa zip Turnip… + Driver di visualizzazione + Disegna i frame sullo schermo in entrambe le modalità. Riavvia l\'app per applicare. + Importa uno zip AdrenoTools… + mai + Nightlies controllate: %1$s + Componenti + Informazioni sui componenti + Cerca nuovi driver + Cerca nuovi pacchetti nelle Nightlies + Proton + Il Proton scelto qui esegue ogni gioco che non ne ha scelto uno proprio in Steam, e una modifica fatta in Steam appare anche qui. Steam sostituisce il Proton di Valve quando si aggiorna; gli originali vengono conservati per ogni build. + Configurazione + Configura profilo + Profilo + Profilo predefinito + Usato automaticamente dai giochi senza personalizzazioni. + Impostazioni salvate, usate automaticamente all\'avvio di %1$s. + Scegli solo le impostazioni che servono a questo gioco. Tutto il resto continua a usare il Profilo predefinito. + Preset FEX + In uso il preset del Profilo predefinito. + Preset personalizzato salvato per questo gioco. + Variabili FEX personalizzate + Usa predefinito + Compatibilità del gioco + Opzioni d\'ambiente di Proton, DXVK, VKD3D e sincronizzazione per questo gioco. + In uso il Proton del Profilo predefinito. + Proton personalizzato salvato per questo gioco tramite la mappatura di compatibilità di Steam. + Nessun Proton + DXVK + VKD3D-Proton + Usa il pacchetto del Profilo predefinito da %1$s, a meno che questo gioco non ne selezioni un altro. + Usa il pacchetto di componenti del Profilo predefinito. + Usa %1$s solo per questo gioco. + Opzioni del runtime + Altre opzioni d\'ambiente per gioco di Proton, DXVK, VKD3D-Proton e sincronizzazione. + Il profilo di %1$s è salvato per il prossimo avvio del gioco. + Avvia senza Steam + Per un gioco che si chiude subito: lo avvia senza la configurazione Steam di Proton. Le funzioni di Steam, come l\'overlay, sono disattivate per esso. + Attivo + La scelta di Proton per %1$s è salvata e già disponibile in Steam. + La scelta di Proton per %1$s è salvata. Riavvia Steam prima di avviare il gioco. + %1$s non può usare il Proton selezionato su questo dispositivo. + Impossibile caricare il profilo di %1$s: %2$s + Impossibile salvare il profilo del gioco: %1$s + Reimposta profilo del gioco + Reimpostare il profilo di %1$s? + Rimuovere le personalizzazioni di Proton, FEX, pacchetto di componenti, ambiente, filtro delle texture e componenti Windows di questo gioco? Userà il Profilo predefinito. + %1$s ora usa il Profilo predefinito. + Impossibile reimpostare completamente il profilo del gioco: %1$s + %1$s ora esegue ogni gioco che non ha scelto un proprio Proton in Steam. + %1$s eseguirà ogni gioco che non ha scelto un proprio Proton, dal prossimo avvio di Steam. + %1$s non può avviare giochi qui, quindi non può essere il predefinito. + Impossibile salvare il Proton predefinito: %1$s + %1$s · %2$d sostituiti + %1$s · tutti originali + %1$s · gioco in esecuzione + %1$s · pacchetto droiddeck-esync + %1$s · ancora nessun pacchetto droiddeck-esync + %1$s · esync integrato, nessun pacchetto necessario + Driver GPU + %1$s · riapplicato all\'avvio %2$s + Prossimo: %1$s · dopo la chiusura del gioco + %1$s in uso: %2$s + Attende la chiusura del gioco + Si applica al prossimo avvio di un gioco + Importa .zip + Lettura delle versioni di Proton… + Nessun Proton ancora installato. Avvia Steam una volta per scaricare il suo Proton ARM64, oppure aggiungi GE-Proton, CachyOS o DroidDeck-Proton da Configurazione. + Originale · %1$s + %1$s · questa build + %1$s · build precedente + Installati · %1$d + Ancora nulla di salvato per %1$s. I file di questo Proton vengono salvati come suo originale. + Un gioco è in esecuzione su questo Proton, quindi la modifica attende la sua chiusura. + Si applica al prossimo avvio di un gioco. + Sostituire %1$s? + Inserire %1$s in %2$s? I suoi file originali restano salvati. %3$s + Ripristinare %1$s? + Rimettere l\'originale di %1$s in %2$s? %3$s + Rimuove il pacchetto dall\'app. Puoi scaricarlo di nuovo. + Eliminare questo originale? + Questo originale di %1$s proviene da una build precedente di %2$s. L\'originale della build attuale viene sempre conservato. + Nelle Nightlies · %1$d + Aggiorna per elencare i pacchetti nelle Nightlies. + Niente di nuovo dall\'ultimo controllo. + Importa .wcp + Ripristina l\'originale di questo Proton + Ripristinare il %2$s originale di %1$s? + Sostituisci + FEX, DXVK e VKD3D-Proton per ogni Proton con cui Steam esegue i giochi.\n\nSeleziona una riga installata per usarla al posto di quella attuale. La modifica si applica al prossimo avvio di un gioco, o dopo la chiusura del gioco in esecuzione.\n\nLe tue scelte vengono ricontrollate prima di ogni avvio e ripristinate se qualcosa le ha modificate. I file originali di ogni Proton vengono conservati, così gli aggiornamenti di Steam non li perdono mai. + Originale + In uso + Nuovo + Salvato + Prestazioni + Core della CPU, correzioni della sessione e nome host. Si applica dalla prossima sessione. + Core del client Steam + Sostituisci la scelta dei core di Steam + Vincola Steam, il suo helper dell\'interfaccia e gamescope ai core qui sotto. + Core del client + Core per Steam. + Attiva la sostituzione per sceglierli. + Core dei giochi + Core dei giochi + Core per i giochi. Con tutti selezionati, Android li gestisce liberamente. + Interfaccia del client + GL multithread + Può rendere più reattivi i menu di Steam. + Zink: descrittori differiti + Consigliato per i driver senza descriptor buffer. + Salta i controlli degli errori GL + Disattiva la convalida GL a ogni chiamata. + gamescope: coda GPU in tempo reale + Dà priorità al lavoro GPU del compositor. Può rendere più regolare il frame pacing, a scapito dei giochi. + GPU + Mantieni la GPU alla frequenza massima + Solo Adreno. Meno scatti, più consumo di batteria e più calore. + Correzioni della sessione + Turnip: rendering sysmem + Più veloce per Steam e la maggior parte dei giochi. Necessario su Adreno 710/720/722. + Salta l\'assistente xalia di Steam + droiddeck-ntsync (sperimentale) + Un sostituto in userspace del driver ntsync di Linux, per i test. Quando è attivo, Proton lo usa al posto di droiddeck-esync e droiddeck-fsync. Funziona con Proton Experimental ARM64, GE-Proton e proton-cachyos. Si applica dalla prossima sessione. + droiddeck-esync + Prima droiddeck-fsync + Esegue il fsync di Proton sui file Proton originali, con futex_waitv fornito in userspace, quindi non richiede pacchetti e continua a funzionare appena Steam aggiorna Proton. Quando è attivo, i giochi lo usano prima di droiddeck-esync, tranne i pochi che Proton esclude da fsync. Si applica dalla prossima sessione. + I thread Windows dei giochi si attendono a vicenda tramite eventfd e memoria condivisa invece di passare ogni volta da wineserver. Usa un pacchetto droiddeck-esync creato per quella precisa build di Proton; i pacchetti vengono scaricati automaticamente. Finché non ne esiste uno per una nuova build di Proton, viene usato droiddeck-fsync. Si applica dalla prossima sessione. + Disattiva l\'assistente di navigazione con gamepad di Proton, che consuma CPU in ogni gioco. + Esegui proot senza seccomp + Può risolvere errori di syscall mancanti su alcuni kernel. Può ridurre le prestazioni. + Percorso veloce di proot + Richiede il filtro seccomp di proot. + Ricerca dei file più veloce e meno scatti. Disattivalo se un programma non trova i suoi file. + Identità della sessione + Nome host + Il nome con cui Steam vede questo dispositivo. Se vuoto, torna a %1$s. + Solo lettere, cifre e trattini interni, fino a 63 caratteri. + Android è impostato per terminare questa sessione + Installati + Disponibili per il download + Niente di nuovo dall\'ultimo controllo. + Ripristina driver integrati + Integrato nell\'app: questa azione rimuove i suoi file estratti e lo nasconde. Ripristina driver integrati lo fa ricomparire. + Metà di un pacchetto Android + Linux: vengono rimosse entrambe le metà. Un driver in uso torna a quello predefinito. + I suoi file vengono rimossi dall\'app. + I suoi file vengono rimossi. È in uso, quindi subentra quello predefinito. + Schermo + Effetti + Controlli + Componenti + Controller + Impostazioni + Pagina %1$s + Steam + Desktop + Menu di Steam + Accesso rapido + Apri il menu di accesso rapido + Pagina precedente + Pagina successiva + HUD prestazioni + Modalità di ridimensionamento + Nitidezza del ridimensionamento + Effetti schermo + In tempo reale. Usa un po\' di tempo GPU mentre un effetto è attivo. + Preset + Personalizzato + Avanzate + Espanso + Compresso + Nitidezza (CAS) + Livello di nitidezza + HDR simulato + Riduzione del banding + Intensità riduzione banding + Luminosità + Contrasto + Gamma + Saturazione + FXAA (smussatura dei bordi) + Contorni cartoon + CRT + NTSC + Filtro delle texture + Giochi DirectX 9–11, dal prossimo avvio. + Filtro anisotropico + Nitidezza delle texture + Un mip bias negativo: texture più nitide, un po\' di sfarfallio. Auto si adatta a quanto la sessione viene ingrandita sullo schermo. + Auto (%1$s) + Tastiera + Tastiera PC + Android + Secondo schermo + Modalità + Mostra + App Android + Avvia un\'app + Nascondi + Mostra + Nessuna app avviabile + Comportamento della sessione + Si applica subito e alle sessioni future in questa modalità. + Prossima sessione + Assistenza + Log della sessione + Da allegare a una segnalazione di bug + Condividi log + In background + Termina + Terminare la sessione? + In esecuzione ora + Nessun gioco in esecuzione su un Proton + Profilo del gioco + I dettagli del profilo non sono ancora disponibili. + Impossibile caricare i profili dei giochi. Nuovo tentativo automatico. + Impossibile aggiornare i profili dei giochi. Vengono mostrati gli ultimi dati disponibili; nuovo tentativo automatico. + Visualizza e modifica le impostazioni salvate usate al prossimo avvio. + In uso il Profilo predefinito + Profilo personalizzato + In uso %1$s dal Profilo predefinito. + Le modifiche vengono salvate nel profilo di %1$s e si applicano al prossimo avvio del gioco. + + %1$d personalizzazione del gioco + %1$d personalizzazioni del gioco + %1$d personalizzazioni del gioco + + Nessun Proton ancora installato. + gioco in esecuzione + riapplicato all\'avvio %1$s + Originale + Importa ed elimina nella pagina Componenti. + Fatto + Richiede attenzione + In corso + Controlla l\'indirizzo IP e la porta di Debug wireless, oppure esegui di nuovo l\'accoppiamento se Android ha rimosso questo dispositivo. + Installa + Gestisci + Panoramica + Controller + Sessione + Launcher + Configurazione + ● Tutto pronto + %1$d di %2$d pronti + Controllo del sistema + Dispositivo supportato + GPU non testata + GPU non supportata + %1$s · %2$s + %1$s: %2$s + Steam richiede un driver Adreno; %1$s potrebbe mostrare una schermata nera + Runtime Linux + %1$s · %2$d%% + Non installato · circa 3 GB, si installa la prima volta che premi Gioca + Installato + %1$s · aggiornamento disponibile + %1$s · aggiornato + Limite processi figlio + Attivo · Android potrebbe chiudere Steam. Disattivarlo richiede un minuto. + Non impostato · Android potrebbe chiudere Steam. Disattivarlo richiede un minuto. + Impossibile verificare · disattivarlo richiede un minuto. + Risolvi + Dettagli + Opzioni sviluppatore + Usa Debug wireless + Controlla di nuovo + Attiva il limite + Account Steam + Accesso eseguito come %1$s · modalità offline + Accesso eseguito come %1$s + Premi Gioca e accedi a Steam + Strumenti + Controller + Le impostazioni del controller non sono disponibili. + Sessione + Log della sessione + Salvati dopo ogni sessione + Strumenti di debug + Log dell\'ultima sessione + Log delle sessioni salvati + Conserva i %1$d più recenti + Cancella log + Modalità offline + Accedi prima a Steam + Launcher + Lingua + La usa anche Steam, dal suo prossimo avvio. + Predefinita di sistema (%1$s) + Predefinita di sistema + Scala dell\'app + Testo e controlli di DroidDeck + %1$d%% (predefinita) + Tema + Aspetto del launcher + Usa come schermata Home + DroidDeck può essere l\'app Home + DroidDeck non viene mai proposta come app Home + Orientamento + Orizzontale automatico + Orizzontale + Orizzontale invertito + Schermo intero + Nasconde la barra di stato e la barra di navigazione di Android + Mostra la barra di stato e la barra di navigazione di Android + App Home predefinita + Scegli un\'app Home + Scegli + App Linux (beta) + File + Sfoglia e gestisci i file + Versioni di Proton + Installa build ARM64 di Proton + Prestazioni + Assegnazione dei core della CPU + Cartella ROM + Dove sono salvati i giochi per emulatori + Componenti + FEX, DXVK, VKD3D + Controlli + Mappatura dei tasti + Scopri + Cerca + Installate + %1$s · %2$d + Negozio + Flathub · ARM64 + ● Flatpak pronto + Installa prima il runtime Linux da Configurazione. + Configura Flatpak + Aggiunge Flatpak e lo collega a Flathub (circa 20 MB). La prima app scarica anche il suo runtime. + Configurazione… + Flatpak + Aggiornamenti + %1$s · %2$s + %1$s · %2$s · %3$d%% + Avvio… + Categorie + Impossibile raggiungere Flathub. Controlla la connessione e riprova. + Riprova + Cerca su Flathub + Firefox, giochi, emulatori… + Cerca + Tutte + Ricerca… + Cerca per nome o scegli una categoria. Vengono elencate solo app ARM64. + Nessuna app ARM64 su Flathub corrisponde alla ricerca. + Risultati + Controlla aggiornamenti + Aggiorna tutte (%1$d) + ● Tutto è aggiornato + Ancora nessuna app. Trovane qualcuna in Scopri o Cerca. + Configura Flatpak per installare app. + Aggiornamento disponibile · %1$s + Apri + Dettagli + Rimuovi + Premi di nuovo per rimuovere + Installazione… + Installa + Configura prima Flatpak + %1$s da scaricare + ● Installata + Impossibile caricare la pagina Flathub di questa app. + Caricamento da Flathub… + Nessuna build ARM64 su Flathub, quindi non si può installare qui. + Screenshot + Informazioni + ID app + Licenza + Runtime + Dimensione installata + %1$s (più il suo runtime condiviso) + Sito web + Verificata + Aggiornamenti + DroidDeck %1$s · %2$s + Controllato %1$s + Non ancora controllato + Lascia che DroidDeck si aggiorni da sé + Nella prossima schermata, attiva Consenti da questa fonte, poi torna qui. L\'aggiornamento continua da solo. + Apri Impostazioni + Non ora + Firma diversa + Impossibile aggiornare senza reinstallare + Questa copia non è firmata con la chiave di rilascio di DroidDeck, quindi gli aggiornamenti non possono installarsi sopra di essa. Disinstallala per passare all\'altra. + Aggiornamento disponibile + Impossibile installare + Pronto per il passaggio + In anticipo su Stable + Sei in anticipo su Stable + Questa build è più recente dell\'ultima Stable. Passerai a Stable con la sua prossima versione. + Test terminato + Questo test è terminato + La sua correzione è stata unita o scartata. Segui Preview per le correzioni più recenti. + Aggiornato + Hai la versione più recente (%1$s) + · %1$s + %1$s… %2$d%% + %1$s… + Aggiorna%1$s + Installa%1$s + Installa comunque Stable %1$s + Segui Preview + Interrompi la sessione in corso per aggiornare. + Meno + Altro + DroidDeck %1$s + Nuova build Preview + Build di test della PR #%1$d + %1$d MB + CANALE DI AGGIORNAMENTO + Stable + Versioni testate, per la maggior parte delle persone + %1$s · %2$s + Preview + Le correzioni più recenti, prima di Stable + Aggiornato + Build di test + Prova una correzione prima che venga rilasciata + Nessuna al momento + PR #%1$d + Non compilata per questa copia di DroidDeck + Aggiornato %1$s + il test della PR #%1$d + qualche tempo fa + proprio ora + + %1$d da provare + %1$d da provare + %1$d da provare + + + Modifiche rispetto alla tua build · %1$d build indietro + Modifiche rispetto alla tua build · %1$d build indietro + Modifiche rispetto alla tua build · %1$d build indietro + + + Modifiche recenti · %1$d build + Modifiche recenti · %1$d build + Modifiche recenti · %1$d build + + Un\'impostazione di Android da cambiare + Steam + Android arresta le app che avviano molti processi in background, e Steam ne avvia decine. Finché questo non cambia, i giochi si chiudono senza un errore. + Risolvilo per me + Attiva “Disattiva limitazioni dei processi figlio”, poi torna qui. Lascia attive le Opzioni sviluppatore, altrimenti l\'interruttore si disattiva di nuovo. + Apri Opzioni sviluppatore + Questa versione di Android non ha un interruttore per questo, quindi DroidDeck lo cambia tramite il Debug wireless. Richiede circa un minuto, senza bisogno di un computer. + Il Debug wireless richiede il Wi-Fi. Va bene qualsiasi rete; non serve l\'accesso a internet. + Il codice di accoppiamento arriva in una notifica. Consenti le notifiche per DroidDeck, oppure usa Altri modi. + Consenti notifiche + Apri Impostazioni → Opzioni sviluppatore → Debug wireless e attivalo + Tocca “Accoppia dispositivo con codice di accoppiamento” + Digita il codice nella notifica di DroidDeck + In attesa del popup di accoppiamento… + Popup trovato. Inserisci il codice nella notifica. + Stato + Controllo ogni 2 secondi. Questa pagina si chiude quando è disattivato. + Altri modi + Debug wireless, indirizzo manuale, ADB da computer + Indirizzo manuale, ADB da computer, Opzioni sviluppatore + Usa invece il Debug wireless + Alcune ROM aggiungono comunque l\'interruttore. Cerca “processi figlio” nelle Opzioni sviluppatore. + Se la notifica non funziona, inserisci tu l\'indirizzo e il codice del popup. + Inserisci l\'indirizzo manualmente + Da un computer con ADB: + Copia comando + Scegli su quale schermo aprire le Impostazioni di Android. + Schermo principale + Accoppiato. Ricerca della porta del Debug wireless… + Accoppiato. Inserisci la porta dalle Impostazioni. + Dispositivo trovato. Applicazione dell\'impostazione… + Impossibile applicare automaticamente. Controlla l\'indirizzo qui sotto e riprova. %1$s + Debug wireless + Limite dei processi figlio aggiornato + Cambia il limite dei processi figlio + Se l\'opzione non è presente, accoppia tramite il Debug wireless. + Accoppiato. Controlla l\'indirizzo IP e la porta nel Debug wireless. + Il limite dei processi figlio è stato aggiornato. + Usa il passaggio 2 se le Opzioni sviluppatore non hanno un\'impostazione per i processi figlio. Apri Opzioni sviluppatore → Debug wireless → Accoppia dispositivo con codice di accoppiamento. + Accoppiato. Usa l\'indirizzo qui sotto, oppure aggiornalo da Debug wireless → Indirizzo IP e porta. + Il limite dei processi figlio è stato aggiornato tramite il Debug wireless. + Android ha confermato che il limite è attivo. + Android ha confermato che il limite è disattivato. + 2 · Accoppia il Debug wireless + Indirizzo IP e porta del popup di accoppiamento + Codice di accoppiamento (PIN) + 6 cifre + Accoppia + 2 · Applica tramite il Debug wireless + Indirizzo IP e porta di connessione + Accoppia di nuovo + Disattiva il limite + Altri modi per cambiarlo + 1 · Opzioni sviluppatore (da provare prima) + Attiva “Limita processi figlio”. + Disattiva “Limita processi figlio”. + 3 · ADB da computer (ultima risorsa) + Solo se il Debug wireless non è disponibile. + 3 · ADB da computer (ultima risorsa) + Quando i controlli touch appaiono in una sessione + Colore + Tonalità dei tasti sullo schermo + Personalizzato + Opacità + %1$d%% + Dimensione dei tasti + 100% è la dimensione standard + Clic levetta + Tocca due volte e tieni premuta una levetta per L3 o R3 + Levette adattive + Vibrazione di gioco + Tasto Steam + Tasto QAM + Tasto tastiera + Premi Select per aprire il menu Steam + Invece di View/Select. + Animazioni + Le levette appaiono dove tocchi e si nascondono al rilascio + Layout + Posizioni personalizzate salvate + Adattato a questo schermo + Modifica + Ripristina + Mappatura dei tasti + Ogni tasto invia il proprio input + %1$d di %2$d tasti rimappati + Configura + Ripristina controller + Ripristina tutte le impostazioni del controller e il layout + Cosa invia al gioco ciascun tasto sullo schermo. Nascosto rimuove il tasto. + Tasti sullo schermo + Predefiniti + Ripristina mappatura + App Android + %1$d app + Nessuna app Android avviabile trovata. + ● Desktop installato + Si installa alla prima apertura + Disponibile per l\'installazione + Non installato. + %1$s · non installato + Apri %1$s + Giochi + Scegli una cartella ROM. + Sfoglia fino a /root/ROMs in RetroArch. + Aggiungi i giochi %1$s a ROMs/%2$s. + Installa %1$s + Interrompi la sessione per installare + Runtime richiesto + Caricamento dei dettagli di installazione… + I dettagli di installazione non sono disponibili. Riprova più tardi. + Installa prima il runtime Linux da Configurazione. + Interrompi la sessione in corso per installare le app desktop. + Installa %1$s nel desktop Linux. + %1$s · %2$d%% + Nasconderlo lo rimuove da Desktop. I suoi file restano nel runtime Linux. + Quel gioco non è più nella cartella ROM. + Desktop · %1$s + Avvia in %1$s + Anche in %1$s + Quella pagina non esiste più. Scegli una sezione a sinistra. + Apri desktop + Installa e apri desktop + Disinstalla desktop + Rimuove il desktop e le sue app incluse. Mantiene giochi, app aggiunte, impostazioni e librerie condivise. + Rimozione del desktop + Desktop rimosso + Rimozione dei vecchi file LXQt + Aggiorna DroidDeck prima di rimuovere questa versione del desktop. + Desktop Linux + KDE Plasma, Firefox e i tuoi emulatori + I giochi che installi in Steam compaiono qui dopo la sessione. + Gioca su Steam + Impostazioni di avvio + Avvia + File di gioco + Prefisso Proton + Ricerca dei salvataggi… + Nessuna cartella di salvataggio trovata per nome. L\'esportazione prende l\'intera cartella utente. + %1$d cartelle di salvataggio + %1$s · %2$d file · %3$s + %1$d KB + Gestisci salvataggi + Salvataggi di gioco + Importa salvataggi… + Esporta come zip GameHub… + Esporta come zip Winlator… + Apri cartella di salvataggio + Mai giocato + Runtime occupato · %1$d%% + Runtime occupato + Il runtime si installa al primo Gioca + Aggiornamento del runtime disponibile + ● Runtime pronto + Giocato l\'ultima volta %1$s + Memoria interna + Gioco aggiunto + Gioco personalizzato + Archiviazione dei giochi + La coppia migliore per questa GPU, mantenuta aggiornata + Scegli tu una coppia + Ricerca dei driver… + Ancora nessun driver elencato. Aggiorna per cercarli. + Nascondi i driver per altre GPU + Mostra i driver per altre GPU + Avanzate + Impostato insieme al driver video: %1$s include entrambi. + Impostato insieme al driver del runtime: %1$s include entrambi. + Usato dai giochi e dal desktop. + Runtime predefinito + Disegna i fotogrammi sullo schermo. Si applica dopo il riavvio di DroidDeck. + Auto - scelto in base alla GPU + Scarica, importa o elimina… + IN USO + I driver integrati in DroidDeck + Driver del runtime e video impostati insieme come coppia abbinata. + Consigliato per questa GPU: %1$s. + Aggiorna per cercare driver per questa GPU. + CONSIGLIATO + Non disponibile + Runtime Linux + Preparazione del runtime Linux + Sessione + Avvio di schermo e audio + Client Steam + Controllo di Steam e dei suoi strumenti di compatibilità + Apertura di Steam + In attesa del primo frame di Steam + Desktop + Preparazione del desktop + Apertura + In attesa del primo frame + Avvio di Steam + La sessione è terminata + Passaggio %1$d di %2$d + Steam è in pausa + Impugnature + Pad sinistro + Pad destro + Entrambi + Gamepad + Levetta + Trackpad + Alterna tra levetta e trackpad + Altri controlli Deck + PANNELLI + Chiudi i controlli sul secondo schermo + Apri il menu Steam + Sinistra · in alto + Sinistra · in basso + Destra · in alto + Destra · in basso + Clic + Trackpad destro + Trackpad sinistro + Trackpad + Mostra tastiera + Nascondi tastiera + Terminale Linux + La sessione Linux non è ancora pronta per un terminale. + Steam + Giochi + Desktop + Negozio + Componenti + App + Configurazione + Aggiornamenti + %1$d%% + Riprendi %1$s + Appare come /root/ROMs in ogni sessione. La memoria interna si trova in /root/Storage. + Nessuna cartella scelta + Si applica dalla prossima sessione. Le schede SD funzionano. + Scegli cartella + Cambia cartella + Installa una build ARM64 di Proton, poi sceglila per ogni gioco in Steam → Proprietà → Compatibilità. + Interrompi la sessione in corso per installare o rimuovere uno strumento di compatibilità. + Build disponibili + Installata: %1$s + In attesa da una richiesta precedente + Non installata + Aggiornamento disponibile → %1$s + Rimuovi le vecchie versioni di %1$s + Rimuove %1$s. I giochi impostati su queste versioni devono passare a un altro Proton. + I giochi impostati su questa versione devono passare a un altro Proton. + Installa ora + Togli dalla coda + Tastiera Android + Nascondi ✕ + Avvia %1$s + Scegli uno schermo. Secondario: %1$s. + Lo schermo secondario non è più disponibile. + Schermo principale + Schermo secondario + Scheda precedente + Scheda successiva + Attivo + Tutti i %1$d + Nessuno + %1$d di %2$d + Disattivato + Schermo inferiore + Si applica al prossimo avvio del gioco. Il profilo di un gioco ha la precedenza su quello condiviso. + Un altro gioco tramite ID app… + Fine + Predefinito + Disattivato + Cambia… + Cambia variabile + Ripristina predefinito + Giochi Proton + Picture in picture + Passa automaticamente in PiP + Controlli sullo schermo aperto + Da aperto, il gioco sta sopra i trackpad e le impugnature posteriori di Steam Deck. Da chiuso, il gioco è a schermo intero. + Sospeso + Il picture in picture è disponibile quando la sessione è pronta. + Consenti il picture in picture per DroidDeck nelle impostazioni di Android. + Android non è riuscito ad aprire il picture in picture. + Scorciatoia + Aggiungi alla schermata Home + Scegli gioco + Ricerca dei giochi installati… + Copia link di avvio + Link di avvio copiato + Questo launcher non può aggiungere scorciatoie. Usa Copia link di avvio. + Link di avvio del gioco non valido. + Questo gioco non è più nella tua libreria DroidDeck. + Interrompi la sessione attuale prima di avviare un gioco Steam. + Impossibile inviare il gioco a Steam. Prova ad avviarlo di nuovo. + Riprova rimozione + Rimozione + Rimozione incompleta + Esporta file del gioco + Sincronizza tutti i file dei giochi nella cartella + Interrompi sincronizzazione dei file dei giochi + Scegli una cartella per i file dei giochi del frontend + File dei giochi salvati in %1$s + File del frontend + + Attendi la fine dell\'operazione sul runtime + Attendi la fine dell\'installazione dello strumento di compatibilità + Attendi la fine dell\'installazione dell\'app desktop + Attendi la fine dell\'installazione prima di avviare un gioco + Chiudi prima la sessione Steam, così il gioco non potrà sovrascrivere i dati importati + Importazione in %1$s + + %1$d file importato (%2$s) in %3$s + %1$d file importati (%2$s) in %3$s + %1$d file importati (%2$s) in %3$s + + + %1$d file importato (%2$s) in %3$s. Backup dei vecchi salvataggi in Download/DroidDeck/Saves/backups + %1$d file importati (%2$s) in %3$s. Backup dei vecchi salvataggi in Download/DroidDeck/Saves/backups + %1$d file importati (%2$s) in %3$s. Backup dei vecchi salvataggi in Download/DroidDeck/Saves/backups + + Scegli uno zip di salvataggi per %1$s + Esportazione di %1$s + + %1$d file esportato come %2$s: %3$s + %1$d file esportati come %2$s: %3$s + %1$d file esportati come %2$s: %3$s + + Scegli dove salvare %1$s (%2$s) + Operazione non riuscita (%1$s): %2$s + Ancora nessun log di sessione: avvia prima una sessione. + Nessun log di sessione da cancellare. + + %1$d cartella di log di sessione cancellata. + %1$d cartelle di log di sessione cancellate. + %1$d cartelle di log di sessione cancellate. + + Accoppiamento per il debug wireless non riuscito + Comando di debug wireless non riuscito + Il debug wireless non è disponibile + Comando ADB copiato + Scegli la cartella delle ROM + Non è una GPU Adreno + Turnip supporta le GPU Adreno. Su %1$s, Steam potrebbe mostrare una schermata nera. Download: %2$.0f MB. + Installa comunque + Rimuovi runtime Linux + Verranno eliminati il runtime, il client Steam al suo interno e tutti i giochi installati lì. + Applicato: %1$s + Impossibile aprire %1$s sullo schermo principale + Impossibile aprire %1$s su %2$s + Impossibile aprire %1$s sullo schermo %2$d + Importazione + Sostituzione + Ripristino + Annullamento + Eliminazione + impossibile leggere il file + Importato %1$s + La sostituzione in attesa è stata annullata. + Scegli un pacchetto di componenti (-linux .wcp) + Impossibile raggiungere le build Nightly + Salvato %1$s + Download non riuscito: %1$s + Scegli un driver per il runtime Linux (zip -Linux o pacchetto Android + Linux) + Scegli un driver video (zip AdrenoTools o pacchetto Android + Linux) + Scegli uno zip di driver o un pacchetto Android + Linux + %1$s: %2$s + Rimozione di %1$s… + Rimozione di %1$s dall\'elenco… + Scegli la cartella di archiviazione dei giochi + Scegli uno ZIP di plugin Decky + Non utilizzabile: %1$s + Programma + Rimozione del runtime Linux + Impossibile aprire Impostazioni sullo schermo inferiore; aperte sullo schermo principale. + Apri Opzioni sviluppatore in Impostazioni + + Impossibile aprire l\'impostazione Android “Installa app sconosciute” + Impossibile controllare: %1$s + Questa build non ha un download per questa copia di DroidDeck + Interrompi prima la sessione in corso, poi aggiorna + Download + Installazione + In attesa della tua conferma + Aggiornamento non riuscito + Android non ha proposto di installare l\'aggiornamento + + Installazione di Decky non riuscita: %1$s + Avvio dell\'importazione del plugin… + Importazione del plugin non riuscita: %1$s + Plugin installato. Riavvia Steam per caricarlo. + + Non ancora controllato - tocca aggiorna per cercare nuovi driver + Android + Linux: imposta anche il driver video + Android + Linux: imposta anche il driver del runtime + il Turnip integrato nel runtime + Vulkan di sistema: nessuna build inclusa per questa GPU + %1$s (incluso) + Manuale + %1$s · %2$s + Al momento ne è pubblicata solo una metà + %1$.0f MB per entrambi + Nessun driver da impostare: %1$s + Ricerca dei driver più recenti… + Aggiornato (impossibile controllare: %1$s) + Al momento i driver consigliati non sono pubblicati + Download di %1$s %2$s… + Aggiornato a %1$s %2$s. + Aggiornato a %1$s %2$s. Il driver video si applica dopo il riavvio di DroidDeck. + Passato a %1$s %2$s. + Passato a %1$s %2$s. Il driver video si applica dopo il riavvio di DroidDeck. + In uso: %1$s %2$s. + In uso: %1$s %2$s. Il driver video si applica dopo il riavvio di DroidDeck. + Non è uno zip di driver: non è un driver AdrenoTools, un Turnip -Linux né un pacchetto Android + Linux + Importazione non riuscita: %1$s + Driver importato + %1$s in uso sia come driver del runtime sia come driver video. + %1$s in uso sia come driver del runtime sia come driver video. Il driver video si applica dopo il riavvio di DroidDeck. + Eliminato %1$s (entrambi i driver) + Eliminato %1$s + Ultimi: %1$s · controllato %2$s + Ultimi: %1$s · %2$s non raggiungibile · controllato %3$s + + %1$d min fa + %1$d min fa + %1$d min fa + + + %1$d h fa + %1$d h fa + %1$d h fa + + + %1$d giorno fa + %1$d giorni fa + %1$d giorni fa + + Controllo di Banners-Turnip, WinNative e DroidDeck… + controllo non riuscito + Installato %1$s - sceglilo in uno dei due elenchi per impostare entrambi i driver + Installato %1$s - sceglilo nel menu + + Attendi la fine della rimozione del runtime + Gioco + touchpad + diretto + Sostituzione non riuscita: %1$s + Salvataggio del profilo non riuscito: %1$s + Il desktop è in pausa + %1$s è in pausa + Il programma è in pausa + Avvio del desktop + Avvio di %1$s + Avvio del programma + Nessun log per questa sessione. + Impossibile creare l\'archivio dei log della sessione. + Impossibile condividere i log della sessione. + Avvio della sessione… + download del runtime Linux + download del runtime Linux · %1$d di %2$d MB + verifica del runtime Linux + estrazione del runtime Linux + download del desktop + download del desktop · %1$d di %2$d MB + verifica del desktop + estrazione del desktop + download di Proton Experimental (ARM64) + download di Proton Experimental (ARM64) · %1$d di %2$d MB + verifica di Proton Experimental (ARM64) + estrazione di Proton Experimental (ARM64) + Impossibile raggiungere il catalogo dei runtime. Controlla la connessione e premi di nuovo Gioca. + Il runtime Linux non è stato installato. Controlla il runtime in Configurazione e premi di nuovo Gioca. + Il desktop non è stato installato (%1$s). Controlla la connessione e premi di nuovo Desktop. + %1$s si è interrotto in modo imprevisto. Condividi i log in una segnalazione di bug o riprova. + Il programma si è interrotto in modo imprevisto. Condividi i log in una segnalazione di bug o riprova. + Il desktop si è interrotto in modo imprevisto. Condividi i log in una segnalazione di bug o riprova. + Stato di uscita %1$d · %2$s + nessun log + + Il log mostra %1$d errore “Function not implemented”: l\'accelerazione seccomp di proot fa fallire un helper su questo dispositivo. Prova %2$s → “%3$s” (o “%4$s”) e riavvia. + Il log mostra %1$d errori “Function not implemented”: l\'accelerazione seccomp di proot fa fallire un helper su questo dispositivo. Prova %2$s → “%3$s” (o “%4$s”) e riavvia. + Il log mostra %1$d errori “Function not implemented”: l\'accelerazione seccomp di proot fa fallire un helper su questo dispositivo. Prova %2$s → “%3$s” (o “%4$s”) e riavvia. + + + Plugin + Installa uno ZIP di plugin per la prossima sessione Steam. + Download… + Installa da ZIP + Giochi + Audio e video + Grafica + Internet + Ufficio + Sviluppo + Istruzione + Scienza + Sistema + Utilità + Popolari + Di tendenza + Novità + Aggiornate + caricamento… + Configurazione di Flatpak + %1$s: %2$s + Impossibile cercare aggiornamenti su Flathub + installato + cartella + Grafite + Superfici color grafite, con il blu della sfera del logo per tutto ciò che è selezionato, attivo o premuto. + Carta su nero + Il bianco è il colore principale, il blu dell\'icona fa da segnale per linee, anelli e punti. + Blu icona + Fondo nero, testo bianco, il blu per tutto ciò che è selezionato, attivo o premuto. + Blu elettrico + Il blu più brillante, su superfici che tendono al blu navy invece che al grigio. + Esc + Ins + Canc + Home + Fine + PgSu + PgGiù + ⌫ Backspace + Tab ⇥ + Bloc Maiusc + Invio ⏎ + ⇧ Maiusc + Ctrl + Super + Alt + Spazio + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Installa prima il runtime + La libreria di Steam non è disponibile + La libreria SD precedente non è disponibile + L\'archivio dei giochi non è disponibile + La cartella dei giochi aggiunti non è disponibile + I manifest di Steam non sono disponibili + Molti sistemi + Interna + Sistema Linux + Scheda SD + Memoria + Seleziona + Proprietà + Aggiungi ai preferiti + Rimuovi dai preferiti + Rinomina + Copia + Taglia + Azioni + Preferiti + Fissa la cartella attuale + Ancora nessun preferito - fissa una cartella dal suo menu ⋮ per tornarci in un attimo. + Rimuovi dai preferiti + 0 byte + byte + Posizione + Tipo + Dimensioni + Modificato + Cartella + File + File %1$s + Sola lettura + Impedisce ai giochi (e a Wine) di sovrascrivere o eliminare questo file. + Impossibile cambiare Sola lettura + Nascosto + Questa memoria non può salvare l\'attributo nascosto. + Contrassegna il file come nascosto in Windows (attributo nascosto di Wine). + Impossibile cambiare Nascosto su questa memoria + Fine + Eliminazione... + Eliminazione non riuscita + Spostamento... + Copia in corso... + Spostamento - %1$s + Copia - %1$s + Spostamento %1$d/%2$d - %3$s + Copia %1$d/%2$d - %3$s + %1$d completati, %2$d non riusciti + %1$d completati, %2$d saltati + + %1$d elemento spostato + %1$d elementi spostati + %1$d elementi spostati + + + %1$d elemento copiato + %1$d elementi copiati + %1$d elementi copiati + + “%1$s” esiste già + Rinomina in corso... + Impossibile rinominare + Creazione della cartella... + Impossibile creare la cartella + Nuova cartella + Nome cartella + Crea + Nuovo nome + Eliminare? + Eliminare definitivamente “%1$s”? + + Eliminare %1$d elemento? + Eliminare %1$d elementi? + Eliminare %1$d elementi? + + L\'operazione non può essere annullata. + + …e %1$d altro + …e altri %1$d + …e altri %1$d + + Eliminazione %1$d/%2$d - %3$s + + Impossibile eliminare %1$d elemento + Impossibile eliminare %1$d elementi + Impossibile eliminare %1$d elementi + + Unisci aggiunge e sostituisce i file nella cartella esistente. + Sovrascrivi sostituisce il file esistente. + Applica a tutti i conflitti + Unisci + Sovrascrivi + Mantieni entrambi + Salta + Seleziona questa cartella + Indietro + %1$s: montata ma al momento non leggibile + Visualizzazione elenco + Visualizzazione griglia + Cerca + Ordina + Nome + Data di modifica + Righe ampie + Righe compatte + Nascondi file nascosti + Mostra file nascosti + Nascondi preferiti + Mostra preferiti + Filtra questa cartella + %1$s liberi + %1$d selezionati + Nessuno + Tutti + Incolla + + %1$d elemento + %1$d elementi + %1$d elementi + + Incolla %1$s qui + Incolla %1$s qui (sposta) + File + UNITÀ + ACCESSO RAPIDO + PREFERITI + Download + Download + ROM + Log di sessione + Immagini + “%1$s” aggiunta ai preferiti + “%1$s” rimossa dai preferiti + Cartella vuota + Livello superiore + Scegli una cartella + Scegli un file + Blu Steam + Cielo + Bianco + Viola + Verde + Ambra + Rosso + Rosa + Pulsante A + Pulsante B + Pulsante X + Pulsante Y + Dorsale sinistro + Dorsale destro + Grilletto sinistro + Grilletto destro + Pulsante Visualizza + Pulsante Menu + Visualizza + Menu + Croce su + Croce giù + Croce sinistra + Croce destra + Nascosto + Trascina per spostare + Aggancia + Salva + Nessuno + Tastiera + trackpad + Terminale + Impugnature Deck + trackpad + + Rimozione del runtime Linux + + Rimozione del runtime Linux · %1$d elemento + Rimozione del runtime Linux · %1$d elementi + Rimozione del runtime Linux · %1$d elementi + + Runtime Linux rimosso + Impossibile completare la rimozione del runtime Linux. Riprova la rimozione in Configurazione. + Rimozione dei residui del runtime + Download + Verifica + Estrazione + Visibile durante l\'installazione o la rimozione del runtime Linux + Installazione del runtime Linux + Rimozione del runtime Linux + + Il runtime Linux non è installato + Il catalogo non ha un checksum per %1$s + Checksum non corrispondente - non è stato modificato nulla + Installazione di %1$s + Impossibile posizionare l\'AppImage + Estrazione non riuscita + Installazione non riuscita + + Configurazione di Flatpak non riuscita (uscita %1$d) + La configurazione di Flatpak non è stata completata + È in corso un\'altra operazione del Negozio + Configura prima Flatpak + Avvio + Flatpak è terminato con stato %1$d + Installazione di %1$s + Aggiornamento di %1$s + Rimozione di %1$s + Installazione di %1$s (%2$d/%3$d) + Aggiornamento di %1$s (%2$d/%3$d) + Rimozione di %1$s (%2$d/%3$d) + traduzioni + driver grafici + codec multimediali + runtime %1$s + runtime %1$s %2$s + + Il file è troppo piccolo per essere un AppImage + Impossibile leggere il file: %1$s + Questo non è un AppImage (non è un programma Linux) + Questo AppImage non è compilato né per ARM64 (aarch64) né per PC x86 + Questo è un programma Linux, ma non un AppImage + Si possono importare solo AppImage di tipo 2 (questo è di tipo %1$d) + Spazio libero insufficiente: l\'importazione richiede circa %1$s + Spazio libero insufficiente: l\'aggiornamento richiede circa %1$s + Importazione non riuscita + Quell\'AppImage non c\'è più + Aggiornamento non riuscito + Estrazione di %1$s + Impossibile estrarre l\'AppImage + Impossibile estrarre l\'AppImage: %1$s + + Interrompi la sessione attiva prima di installare Decky Loader + Download di Decky Loader non riuscito + La dimensione di Decky Loader non corrisponde ai metadati della release + Impossibile scaricare il checksum della release + Checksum della release mancante + Checksum della release non valido + Checksum di Decky Loader non corrispondente + Il file della release non è un eseguibile Linux a 64 bit compatibile + È stata avviata una sessione durante il download; interrompila prima di installare Decky Loader + Impossibile rendere eseguibile PluginLoader + Impossibile attivare il debug remoto CEF di Steam + Impossibile preparare PluginLoader + Impossibile conservare il PluginLoader esistente + Impossibile attivare il nuovo PluginLoader + Interrompi la sessione attiva prima di installare un plugin + Installa Decky Loader prima di importare un plugin + Impossibile leggere il file ZIP selezionato + Impossibile creare la cartella dei plugin Decky + Estrazione dello ZIP del plugin + Lo ZIP del plugin deve contenere una sola cartella di plugin al primo livello + Nello ZIP del plugin mancano plugin.json, package.json o dist/index.js + Nome del plugin mancante + È stata avviata una sessione durante l\'installazione; interrompila prima di installare un plugin + Plugin installato · riavvia Steam per caricarlo + Installazione dello ZIP del plugin non riuscita + Lo ZIP del plugin contiene troppi elementi + Lo ZIP del plugin contiene un percorso non valido + Lo ZIP del plugin contiene percorsi duplicati + Lo ZIP del plugin contiene un percorso non sicuro + Impossibile creare la cartella del plugin + Una volta estratto, lo ZIP del plugin supera il limite di 1 GiB + Impossibile rendere eseguibile il binario incluso nel plugin + Voce remote_binary non valida + Nome del binario remoto non valido + L\'URL del binario remoto deve usare HTTPS + Il binario remoto %1$s ha un hash SHA-256 non valido + Impossibile creare la cartella bin del plugin + Download del binario del plugin · %1$s + Impossibile scaricare il binario del plugin %1$s + Checksum del binario del plugin non corrispondente · %1$s + Impossibile rendere eseguibile il binario del plugin · %1$s + Impossibile sostituire il binario del plugin · %1$s + Impossibile installare il binario del plugin · %1$s + La cartella del plugin appartiene a un altro plugin + Impossibile conservare il plugin esistente + Impossibile attivare il plugin importato + Impossibile aggiornare le impostazioni dei plugin Decky + Impossibile ripristinare il plugin esistente + + Impossibile salvare l\'identità del debug wireless + Comando ADB non riuscito (%1$d) + Android non ha confermato la modifica del limite dei processi secondari: %1$s + Nessun accoppiamento del debug wireless salvato + Inserisci l\'indirizzo IP e la porta attuali del debug wireless + + Tempo scaduto in attesa del popup di accoppiamento. Riprova da DroidDeck. + Impossibile cercare il popup di accoppiamento (%1$d). Controlla che il Wi-Fi sia attivo. + Impossibile cercare il popup di accoppiamento: %1$s + Inserisci tutte e 6 le cifre del codice + Accoppiamento… + Il codice non ha funzionato. Riapri il popup di accoppiamento e inserisci il nuovo codice. + Accoppiato. Connessione… + Accoppiato, ma la porta del debug wireless non è stata trovata. Lascia attivo il debug wireless e riprova. + Applicazione dell\'impostazione… + Comando di debug wireless non riuscito + Configurazione del debug wireless + Chiede il codice di accoppiamento del debug wireless mentre Impostazioni è aperto + Accoppia il debug wireless + Attiva Debug wireless, poi tocca “Accoppia dispositivo con codice di accoppiamento”. + Attiva Debug wireless, poi tocca “Accoppia dispositivo con codice di accoppiamento”. Resta in Impostazioni: il codice va inserito in questa notifica. + Codice di accoppiamento a 6 cifre + Tocca Inserisci codice e digita le 6 cifre mostrate in Impostazioni. + Inserisci il codice di accoppiamento Wi-Fi + Riprova con il codice + Inserisci codice + Configurazione di Steam + Steam è pronto + Tocca per tornare a DroidDeck. Ora puoi disattivare il debug wireless. + Configurazione del debug wireless interrotta + + Il download ha risposto HTTP %1$d + La dimensione del download non corrispondeva all\'APK pubblicato ed è stato scartato - riprova + Il download non corrispondeva al suo checksum ed è stato scartato - riprova + L\'aggiornamento è per il pacchetto Android %1$s, non %2$s + I metadati dell\'aggiornamento non indicano la chiave di firma delle release di DroidDeck + Android non è riuscito a leggere l\'APK scaricato + L\'APK scaricato è per %1$s, non %2$s + L\'APK scaricato indica versionCode %1$d, non il %2$d pubblicato + Android non installa il versionCode %1$d sopra il %2$d installato + L\'APK scaricato non ha informazioni di firma + L\'APK scaricato non è firmato con la chiave di release di DroidDeck + Il catalogo degli aggiornamenti ha risposto HTTP %1$d + + Avvio della sessione… + %1$d:%2$02d trascorsi · ancora in corso + Download dell\'aggiornamento del client Steam · %1$d%% + Download del client Steam · %1$s + + È già in corso l\'installazione di uno strumento di compatibilità + Interrompi la sessione attiva prima di installare strumenti di compatibilità + Impossibile trovare una release ARM64 per %1$s + Per installare %1$s servono almeno 4 GB liberi più la dimensione del download + Download di %1$s %2$s + Nuovo tentativo di download · tentativo %1$d di %2$d + Download interrotto + Download non riuscito; il file parziale viene conservato per riprendere al prossimo tentativo + Il download ha superato la dimensione della release ed è stato eliminato + Download incompleto; il file parziale viene conservato per riprendere al prossimo tentativo + Verifica del download + Impossibile leggere il checksum della release; riprova + %1$s %2$s non pubblica alcun checksum; non è stato installato nulla + Checksum non corrispondente; il download è stato eliminato + È stata avviata una sessione durante il download; interrompila prima di installare strumenti di compatibilità + Installazione di %1$s + Installazione di %1$s non riuscita (uscita %2$d) + Impossibile installare %1$s + Recupero del pacchetto droiddeck-esync + + con helper Linux + Solo DLL + Originale (Proton aggiornato dopo l\'ultima sostituzione) + Modificato fuori da Componenti + non presente + Originale %1$s + Originale da %1$s + %1$s %2$s entrerà in %3$s quando si chiuderà il gioco che lo sta usando. + %1$s %2$s ora è in %3$s. Si applica al prossimo avvio di un gioco. + Quel pacchetto originale non è salvato + %1$s di %2$s tornerà all\'originale quando si chiuderà il gioco che lo sta usando. + %1$s di %2$s ripristinato dall\'originale di %3$s. + %1$s è in uso in %2$s. Prima passa quel Proton a un\'altra versione. + %1$s non è salvato + %1$s eliminato. + L\'originale della build installata viene conservato: serve per tornare indietro. + Non salvato + Originale %1$s di %2$s eliminato. + I download provengono solo dalle release Nightly + Questo elenco di pacchetti è anteriore ai checksum - aggiornalo e riprova + Checksum non corrispondente + + Lineare + Più vicino + Snapdragon GSR (qualità) + Solo nitidezza + Auto (segue il ridimensionamento) + + Nessun effetto applicato. + Chiarezza di gioco + Nitidezza con un tocco di contrasto - quello per tutti i giorni. + Vivace + Colori molto accentuati. Deband attivo, così i cieli non fanno strisce. + Cinematografico + Contrasto più profondo, colori più freddi, bordi smussati. + Competitivo + Ombre schiarite e nitidezza elevata - avvista gli avversari per primo. + Nitidezza adattiva + Solo nitidezza, colori invariati. + Filmico + Curva da pellicola, leggermente attenuata. + Arcade + Forte e incisivo. + CRT rétro + I passaggi tubo CRT e segnale analogico insieme. + Upscaling nitido + Per giocare sotto la risoluzione del pannello. + Pixel puliti + Pixel nitidi per il 2D e i titoli più vecchi. + Contorni anime + Contorni da cartone animato più nitidezza. + + Non è una GPU Adreno + Supportata + Sperimentale: i suoi driver sono build di test + Inferiore all\'hardware testato (Adreno 725 e successive): potrebbe non funzionare + Non supportata: DroidDeck richiede una GPU Adreno (Snapdragon) + + Raggiunto il limite di richieste di GitHub - riprova più tardi + GitHub ha risposto HTTP %1$d + il download ha risposto HTTP %1$d + Questo elenco di driver è anteriore ai checksum - aggiornalo e riprova + Checksum non corrispondente - il download è stato scartato + Non è un driver AdrenoTools: %1$s + manca meta.json + meta.json non è leggibile (%1$s) + questo è un driver per il runtime Linux (importalo in “Driver del runtime Linux”) + questo è un driver per giochi Wayland, che a questa app non serve + meta.json non indica alcun libraryName, quindi non c\'era nulla da passare ad AdrenoTools + meta.json indica %1$s, che non è nello zip + %1$s non è una libreria condivisa ELF AArch64 a 64 bit + %1$s è collegato a glibc: è un Turnip “-Linux”, che va in “Driver del runtime Linux” + Nessun libvulkan_freedreno*.so in questo zip. Uno zip Turnip Android (AdrenoTools) o -Wayland non è un driver per il runtime Linux. + %1$s non è una libreria condivisa ELF AArch64 a 64 bit. + %1$s non è un driver glibc: è collegato alla libc di Android. Il runtime Linux richiede uno zip “-Linux”; un Turnip normale o “-Wayland” non può essere caricato in alcun modo dal client Steam. + Non è un pacchetto di driver: manca un manifest.json che indichi un driver Android e uno Linux + Non è un pacchetto di driver: %1$s punta al di fuori di esso + Non è un pacchetto di driver: una volta estratto supera %1$d MB + + Scheda SD + Cartella + + Condividi log di sessione + + richiede Android 10 o successivo (livelli di visualizzazione con spazio colore) + questo schermo non segnala supporto HDR + questo schermo supporta %1$s, non HDR10 + + questa GPU + Limite dei processi secondari attivo + Limite dei processi secondari non impostato + Impossibile verificare il limite dei processi secondari + Limite dei processi secondari disattivato + Limite dei processi secondari non necessario + %1$s Se non è possibile, collega questo dispositivo a un computer con ADB ed esegui il comando qui sotto. Poi torna qui e ricontrolla. + Le sessioni Steam possono avviarsi. + Questa versione di Android non usa questo limite. + Attiva “Disattiva limitazioni processi secondari” in Opzioni sviluppatore e poi lascia attive le Opzioni sviluppatore. + Questa versione di Android non ha un interruttore per farlo. Usa il debug wireless per cambiarlo da questo dispositivo. + Questa build non ha un download per questa copia di DroidDeck. + Android non la installerà sopra questa build perché il suo versionCode %1$d è inferiore al %2$d installato. + Android non installa una versione precedente sopra una più recente. Resta su questa build finché il canale non si aggiorna. + Questa copia di DroidDeck ha una firma diversa (una build locale?), quindi Android non può aggiornarla direttamente. + Non c\'è abbastanza spazio libero per l\'aggiornamento. + L\'aggiornamento è stato annullato. + L\'aggiornamento non è stato installato: %1$s + L\'aggiornamento non è stato installato. + zip %1$s + zip + %1$s non è ancora stato avviato + Nessun file di salvataggio trovato per %1$s + %1$s non ha una cartella drive_c, quindi non è uno zip di salvataggi GameHub o Winlator + impossibile creare cartelle in %1$s + impossibile scrivere in %1$s + impossibile scrivere in %1$s (%2$s) + Cartella non disponibile + Un altro file usa già %1$s + Cartella delle illustrazioni non disponibile + La cartella ha un inventario di sincronizzazione non correlato + Impossibile rimuovere %1$s + Il file esce dalla cartella di esportazione + FEX (build senza nome di versione) + nessun profile.json in %1$s + %1$s non è un pacchetto di componenti Linux (tipo %2$s) + %1$s · %2$s liberi + Automatico + Il catalogo degli aggiornamenti non ha superato i controlli (%1$s) + Impostazione processi secondari di DroidDeck + Accesso eseguito · modalità offline + Accesso eseguito + INCLUSO + SCARICATO + IMPORTATO + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Schermo della sessione + Risoluzione + Adattamento giochi 16:9 + Come i giochi 16:9 si adattano a questo schermo. Dalla prossima sessione. + Scegli Adatta allo schermo o una dimensione personalizzata più stretta di 16:9. + Mantieni proporzioni + Estendi allo schermo + Ridimensionamento immagine + Filtro di ridimensionamento + Condiviso da Steam e Desktop. + Si applica subito. Condiviso da Steam e Desktop. + FSR, GSR e NIS si applicano quando l\'immagine della sessione viene ingrandita. Solo nitidezza funziona anche senza ingrandimento. + Nitidezza del filtro + Frequenza fotogrammi + %1$s Imposta il ridimensionamento su %2$s. + Adatta allo schermo (%1$d×%2$d) + Segui lo schermo (si ridimensiona quando lo pieghi o lo apri) + Steam + Mostra dettagli dispositivo + Nascondi dettagli dispositivo + Lettura del dispositivo… + Copia + Copia questi dettagli in una segnalazione di bug. + Copiato. + Forza SSBS + Mantiene attiva un\'impostazione di velocità della CPU che Wine continua a disattivare. Può rendere più veloci i giochi Windows su Snapdragon 8 Elite; poche differenze sui chip più vecchi. Si applica al prossimo avvio del gioco. + + Negozi + Steam + Personalizzati + Negozi + Apri un negozio su + Livello di velocità di download + Motore dei negozi + ● Motore pronto + Non compilato + Bilanciato + Veloce + Massimo + Negozi + Impostazioni negozi + Impostazioni + (accesso non eseguito) + Negozio + Installati + Libreria + Tutti + Download + %1$s (%2$d) + Accedi + %1$s · esci + Uscire da %1$s? + I giochi installati restano. La libreria verrà recuperata di nuovo quando accederai. + Resta connesso + Esci + Cerca nel catalogo di %1$s… + Cerca tra i giochi installati… + Cerca nella tua libreria… + Cerca + Niente di installato + Caricamento + Nessun gioco + Caricamento del negozio… + Nessun catalogo + Novità + Offerte + Giochi gratuiti + Di tendenza + La tua libreria + Gioca + Installa + Riprendi installazione + Interrotto + Installa · %1$s + Ottieni gratis + Visualizza su %1$s + Acquista · %1$s + Installato + In libreria + Gratis + Riprendi + Aggiorna + Mostra contenuti per adulti + Epic + EOS + Offline + Disattivato + Accesso Epic (EOS) + Avvia offline + Risolvi l\'accesso Epic + Salvataggi nel cloud + Carica nel cloud + Scarica dal cloud + Nessun salvataggio nel cloud + Conflitto cloud + Conflitto cloud · %1$d file + … + Mantieni cloud + Mantieni locale + Controllo del cloud… + Ultima sincronizzazione %1$s + Non ancora sincronizzato + %1$d file + installato + nella tua libreria + negozio + Obiettivi + %1$d di %2$d sbloccati + %1$d / %2$d + Sbloccati · %1$d + Bloccati · %1$d + Nascosti · %1$d + Obiettivo nascosto + I dettagli vengono mostrati una volta sbloccato. + Tutti gli obiettivi sbloccati + Caricamento… + Accedi a %1$s per vedere i tuoi obiettivi. + Impossibile raggiungere %1$s. Riprova quando sei online. + %1$s non elenca obiettivi per questo gioco. + Mostra tutti i %1$d + Obiettivi · %1$s + ✓ In Steam + Disinstalla + Disinstallazione… + Disinstallare %1$s? + Libera %1$s e lo rimuove da Steam. + Rimuove i suoi file e il suo collegamento in Steam. + Steam rileverà il gioco al prossimo avvio; avvialo da lì, oppure avvia Steam ora. + Proton + Scelta di Steam per ogni gioco + Audio + DirectAudio + Componenti + FEX, DXVK, VKD3D + Motore + %1$s · nativo + %1$s · integrato + Come funziona + Scaricato da + %1$s, con il tuo account + Installato in + Archivio dei giochi (Games/%1$s/%2$s) + Avviato da + Steam e Proton, come i giochi Steam + Steam e Proton · accesso Epic a ogni avvio, overlay EOS disattivato + Steam e Proton · variabili di Amazon Games Services impostate + Steam + aggiunto a Steam all\'installazione (interfaccia Deck + Steam Input) + Nessun download + In pausa + In coda + In coda · #%1$d + Annullato + Non riuscito + Non riuscito · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Pausa + Riprendi + Annulla + Riprova + Cancella + Cancella completati + Manifest + Download + Verifica + Installazione + Completato + Controllo + Download + Eliminare il download? + Non riuscito · %1$s conservati + Verifica + Installazione + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d file + Gestione download + Download simultanei + Installa in + Installa %1$s: + Interna + %1$s liberi + Download dei negozi + Mostra l\'avanzamento dei download dei giochi e li mantiene attivi in background + Download in corso + %1$d download + Preparazione… + Download dei negozi completati + %1$s installato + Download di %1$s non riuscito + Elimina + Mantieni + Accedi a %1$s + Accedi con il tuo provider + Ricarica + Chiudi + Riprova + Completamento dell\'accesso… + Impossibile caricare la pagina di accesso: %1$s + Questo provider non consente l\'accesso da una pagina incorporata. Accedi invece con la password del negozio. + La pagina di accesso ha risposto con un errore (HTTP %1$d). + La connessione alla pagina di accesso non è sicura. + Impossibile verificare l\'accesso. Riprova. + Accesso non riuscito. Riprova. + Chiudi + Indietro + Modifica + Aggiungi tutti i giochi in questa cartella + Niente di nuovo in questa cartella + Nome + Destinazione + Avvia in + Opzioni di avvio + Illustrazioni + Altro exe… + Rimuovi + Copertina + Sfondo + Logo + Icona + Cartella del gioco + Steam + SteamGridDB + Il tuo file + Dall\'exe + Automatico + Scegli immagine… + Scegli un\'immagine: %1$s + Ripristina automatico + + %1$d gioco aggiunto + %1$d giochi aggiunti + %1$d giochi aggiunti + + + %1$d già in Giochi + %1$d già in Giochi + %1$d già in Giochi + + B · Torna a %1$s + il gioco + Impostazioni di gioco + Driver grafici + Questo Proton + Ogni Proton + Per ogni Proton + Disponibili · %1$d + Controllato %1$s + Cerca nuovi componenti e driver + Ottieni + %1$s · DXVK, VKD3D-Proton e FEX qui sotto sono di questo Proton + %1$s · testato + %1$s · non testato + %1$s · non supportato + Mantiene aggiornata la coppia consigliata + Auto · %1$s + Driver del runtime e video separati + Usare %1$s? + Rimuovere %1$s? + Diagnostica + Pronto per giocare + Non ancora pronto per giocare + runtime %1$s + limite processi disattivato + accesso come %1$s + Quando serve il tuo intervento + Aspetto + Questo dispositivo + Illustrazioni della libreria + APP LINUX + Flathub + App per il desktop Linux, da aprire in Desktop. + Configura Flathub + Rimuovere %1$s? + Le sue impostazioni e i suoi file vengono conservati. + Mantieni + Spazio su disco + Disinstallare il desktop? + Tutti + Obiettivi GOG + Sincronizza gli obiettivi per i giochi GOG supportati. Si applica al prossimo avvio. + Obiettivi disattivati + Obiettivi attivati + Accedi di nuovo a GOG + Obiettivi caricati + Caricamento obiettivo in sospeso + Attiva gli obiettivi GOG nelle impostazioni Store + Obiettivi non disponibili; controlla i log della sessione + Obiettivo sbloccato + Incolla + Croce direzionale + Stick sinistro + Stick destro + Menu + Tastiera + Croce direzionale su + Croce direzionale giù + Croce direzionale sinistra + Croce direzionale destra + Controlli touch + Controller touch + Mouse + Attivo + Disattivo + Lenta + Normale + Rapida + Modifica layout + Menu di DroidDeck + Trascina i controlli sullo schermo + Disponi + Annulla + Salvataggio… + Icona + L− + L+ + A− + A+ + Nascondi + Colore e opacità… + Copia questo layout + Layout copiato; incollalo nel layout touch di qualsiasi gioco + Incolla layout copiato + Ripristina quello predefinito di Steam + Questo controllo non ha un\'associazione in Steam per assegnargli un\'icona + Icona per %1$s + Colore del layout touch + Fine + Impossibile salvare il layout touch + Layout touch salvato su Steam + App %1$d + Etichetta (opzionale) + Le icone di Steam non sono state trovate + + %1$d icona da Steam + %1$d icone da Steam + %1$d icone da Steam + + Colore dell\'icona + Colore del tasto + Nessuna icona + + Verifica dell\'accesso… + Recupero della libreria… + Recupero dell\'elenco giochi… + Caricamento dei dettagli dei giochi… (%1$d/%2$d) + + Sincronizzazione di %1$d gioco… + Sincronizzazione di %1$d giochi… + Sincronizzazione di %1$d giochi… + + Sincronizzazione… %1$d/%2$d + Recupero delle copertine… + Sincronizzazione non riuscita + Nessun gioco trovato nella tua libreria %1$s + Amazon Games non ha risposto per la tua libreria: aggiorna, oppure accedi di nuovo + Impossibile contattare GOG + Impossibile leggere la tua libreria GOG + Registrazione su Steam… + Cardboard VR + Modalità Cardboard + Immagini per l\'occhio sinistro e destro, per un visore VR per telefono. + Immagine + Schermo virtuale + 3D affiancato + Visore + Cardboard + Cardboard (2014) + Senza lenti + Distanza tra gli occhi + Larghezza schermo + Distanza schermo + Tracciamento della testa + Questo dispositivo non ha un sensore di rotazione. + Ricentra vista + Ricentra + SteamVR (sperimentale) + Installa + Scarica SteamVR ARM64 di Valve (571 MB). + Download ed estrazione di SteamVR… + Avvia + Avvia SteamVR usando la tua testa come visore; l\'immagine passa al 3D affiancato. + Arresta + SteamVR è in esecuzione. + Non disponibile in questa sessione. + SteamVR non ha risposto; controlla il log della sessione. + Scarica componenti + I download sono condivisi con la pagina Componenti. Scegli sopra una versione scaricata per usarla al prossimo avvio del gioco. + Scegli una versione da scaricare + Sviluppatore + Editore + Uscita + Principale + + Extra + Completista + %1$s h + %1$d min + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 19a74fc1c..8c4edf41a 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -15,8 +15,10 @@ セッション Steamまたはデスクトップのセッション中に表示 Steamが実行中です + Steamのダウンロードが進行中 セッションが一時停止中 - 準備ができたらタップして再開 再開 + 今すぐ一時停止 セッションを終了 フレーム生成 使用するフレーム生成モードを選択してください @@ -182,6 +184,8 @@ アプリID org.ppsspp.PPSSPP 有効なFlathubアプリIDではありません + Flathubにないアプリの場合は、.flatpakファイルも使えます。 + .flatpakファイルを選択 ソース デスクトップに表示 新しいアプリ @@ -251,6 +255,10 @@ 画面コントローラー Steam + QAM 自動では、コントローラーが接続されていないときに画面コントローラーを表示します。 + Steamタッチ(ベータ版) + Steamのタッチコントロールは次回のSteamセッションから有効になります + Steamタッチのレイアウト + このゲームのタッチコントロールを移動、サイズ変更、非表示にします。Steamに保存されます コントローラー Steamがコントローラーを認識する方法。次回のセッションで有効になります。 Steam Deckコントローラー @@ -268,6 +276,8 @@ 手動 ネイティブ 自動では一時停止し、表示すると再開します。ネイティブでは先にSteamをスリープに備えさせます。手動では「再開」をタップするまで待機します。「しない」では実行し続けます。 + ダウンロードを継続 + Steamがダウンロード中は、バックグラウンドでの一時停止を遅らせます。 自動ではバックグラウンドで一時停止し、表示すると再開します。手動では「再開」をタップするまで待機します。「しない」では実行し続けます。 起動 DroidDeck起動時にSteamを起動 @@ -331,6 +341,7 @@ ゲームに%1$sがあります(そのままで動くことが多いです) このゲームに導入済み ゲームのフォルダーには、必要なコンポーネントを示すものがありません。 + このゲームにインストール済み(%1$d) すべてのコンポーネント(%1$d) Windowsインストーラーが必要(%1$d) これらはプレフィックス内で実際のインストーラーを実行します(.NET、PhysX、XNA、Visual C++のフルインストーラーなど)。DroidDeckはまだ対応していませんが、次に対応予定です。 @@ -341,6 +352,9 @@ コンポーネントを読み込み中… インストールできませんでした: %1$s ダウンロード済み + %1$sをダウンロード中 + インストール中:%1$s + オフにしています… ダウンロード %1$s 次にゲームを起動したときに反映されます。 16:9ゲームを引き伸ばす @@ -397,6 +411,44 @@ Nightliesの新しいパッケージを確認 Proton ここで選択したProtonは、Steamで個別のProtonが選択されていないすべてのゲームを実行します。Steamで変更した場合もここに反映されます。Steamのアップデート時にValveのProtonは置き換えられますが、元のファイルはビルドごとに保持されます。 + 構成 + プロファイルを設定 + プロファイル + デフォルトプロファイル + 個別設定のないゲームで自動的に使用されます。 + %1$sの起動時に自動的に使用される保存済みの設定です。 + このゲームに必要な設定だけを選んでください。それ以外はデフォルトプロファイルを引き続き使用します。 + FEXプリセット + デフォルトプロファイルのプリセットを使用中。 + このゲーム用のカスタムプリセットを保存済み。 + カスタムFEX変数 + デフォルトを使用 + ゲームの互換性 + このゲームのProton、DXVK、VKD3D、同期の環境オプション。 + デフォルトプロファイルのProtonを使用中。 + Steamの互換性マッピングを通じて、このゲーム用のカスタムProtonを保存済み。 + Protonなし + DXVK + VKD3D-Proton + このゲームで別のパッケージを選択しない限り、%1$sのデフォルトプロファイルのパッケージを使用します。 + デフォルトプロファイルのコンポーネントパッケージを使用します。 + このゲームでのみ%1$sを使用します。 + ランタイムオプション + その他のゲームごとのProton、DXVK、VKD3D-Proton、同期の環境オプション。 + %1$sのプロファイルを保存しました。次回のゲーム起動時に有効になります。 + Steamなしで起動 + すぐに終了してしまうゲーム用:ProtonのSteamセットアップなしで起動します。オーバーレイなどのSteam機能はこのゲームでは無効になります。 + オン + %1$sのProtonの選択を保存しました。Steamですぐに使用できます。 + %1$sのProtonの選択を保存しました。ゲームを起動する前にSteamを再起動してください。 + %1$sは、選択したProtonをこのデバイスで使用できません。 + %1$sのプロファイルを読み込めませんでした:%2$s + ゲームプロファイルを保存できませんでした:%1$s + ゲームプロファイルをリセット + %1$sのプロファイルをリセットしますか? + このゲームのProton、FEX、コンポーネントパッケージ、環境、テクスチャフィルタリング、Windowsコンポーネントの個別設定を削除しますか?デフォルトプロファイルが使用されます。 + %1$sはデフォルトプロファイルを使用するようになりました。 + ゲームプロファイルを完全にはリセットできませんでした:%1$s Steamで個別のProtonが選択されていないすべてのゲームを、現在%1$sで実行します。 次回Steamの起動時から、個別のProtonが選択されていないすべてのゲームを%1$sで実行します。 %1$sはここでゲームを起動できないため、デフォルトにできません。 @@ -560,6 +612,18 @@ セッションを停止しますか? 現在実行中 現在Protonで実行中のゲームはありません + ゲームプロファイル + プロファイルの詳細はまだ利用できません。 + ゲームプロファイルを読み込めませんでした。自動的に再試行します。 + ゲームプロファイルを更新できませんでした。最後に取得したデータを表示しています。自動的に再試行します。 + 次回の起動時に使用される保存済みの設定を表示・編集します。 + デフォルトプロファイルを使用中 + カスタムプロファイル + デフォルトプロファイルの%1$sを使用中。 + 変更は%1$sのプロファイルに保存され、次回のゲーム起動時に有効になります。 + + %1$d件のゲーム個別設定 + Protonがインストールされていません。 ゲーム実行中 %1$sの起動時に再適用しました @@ -631,6 +695,10 @@ ホーム画面として使用 DroidDeckをホームアプリとして使用できます DroidDeckはホームアプリとして提供されません + 画面の向き + 横向き(自動回転) + 横向き + 横向き(反転) 全画面 Androidのステータスバーとナビゲーションバーを非表示 Androidのステータスバーとナビゲーションバーを表示 @@ -888,6 +956,12 @@ このページは存在しません。左側からセクションを選択してください。 デスクトップを開く インストールしてデスクトップを開く + デスクトップをアンインストール + デスクトップと同梱のアプリを削除します。ゲーム、追加したアプリ、設定、共有ライブラリは保持されます。 + デスクトップを削除中 + デスクトップを削除しました + 古いLXQtファイルを削除中 + このバージョンのデスクトップを削除する前に、DroidDeckを更新してください。 Linuxデスクトップ KDE Plasma、Firefox、エミュレーター Steamでインストールされたゲームは、セッション終了後にここに表示されます。 @@ -916,6 +990,8 @@ 最終プレイ %1$s 内部ストレージ 追加されたゲーム + カスタムゲーム + ゲームストレージ このGPUに最適なドライバー構成を選択し、最新の状態に保ちます ドライバー構成を自分で選択する ドライバーを確認中… @@ -957,6 +1033,12 @@ 左トラックパッド 右トラックパッド 両方 + ゲームパッド + スティック + トラックパッド + スティックとトラックパッドを切り替え + その他のDeckコントロール + パネル セカンドスクリーンコントロールを閉じる Steamメニューを開く 左 · 上 @@ -992,6 +1074,10 @@ %1$s インストール済み 前回のリクエストによる保留中の項目 未インストール + アップデートあり → %1$s + 古い%1$sのバージョンを削除 + %1$sを削除します。これらのバージョンに設定されているゲームは、別のProtonに切り替える必要があります。 + このバージョンに設定されているゲームは、別のProtonに切り替える必要があります。 今すぐインストール キューをキャンセル Androidキーボード @@ -1020,6 +1106,8 @@ Protonゲーム ピクチャーインピクチャー 自動でピクチャーインピクチャー + 開いた状態の画面のコントロール + 開いた状態では、ゲームがSteam Deckのトラックパッドと背面グリップの上に表示されます。折りたたんだ状態では、ゲームが全画面で表示されます。 一時停止中 セッションの準備ができたらピクチャーインピクチャーを使用できます。 Android設定でDroidDeckのピクチャーインピクチャーを許可してください。 @@ -1046,17 +1134,14 @@ フロントエンドファイル - 実行環境の処理が完了するまでお待ちください 互換性ツールのインストールが完了するまでお待ちください デスクトップアプリのインストールが完了するまでお待ちください インストールが完了してからゲームを起動してください インポートしたデータがゲームに上書き保存されないよう、先にSteamセッションを終了してください - %1$sにインポート中 - %2$sから%1$d個のファイルを%3$sにインポートしました @@ -1065,18 +1150,14 @@ %2$sから%1$d個のファイルを%3$sにインポートしました。以前のセーブデータはDownload/DroidDeck/Saves/backupsにバックアップされました %1$sのセーブデータzipを選択 - %1$sをエクスポート中 - %1$d個のファイルを%2$sとしてエクスポートしました:%3$s - %1$sの保存先を選択(%2$s) - %1$sに失敗しました:%2$s セッションログはまだありません。先にセッションを実行してください。 @@ -1090,23 +1171,18 @@ ADBコマンドをコピーしました ROMフォルダを選択 Adreno GPUではありません - TurnipはAdreno GPUをサポートしています。%1$sでは、Steamの画面が真っ暗になる可能性があります。ダウンロード:%2$.0f MB。 インストールする Linux実行環境を削除 実行環境、その中のSteamクライアント、そこにインストールされたすべてのゲームが削除されます。 - 適用しました:%1$s メイン画面で%1$sを開けませんでした - %2$sで%1$sを開けませんでした - ディスプレイ%2$dで%1$sを開けませんでした - インポート中 置き換え中 @@ -1114,20 +1190,17 @@ キャンセル中 削除中 ファイルを読み取れません - %1$sをインポートしました 待機中の置き換えをキャンセルしました。 コンポーネントパッケージ(-linux .wcp)を選択 Nightliesに接続できませんでした - %1$sを保存しました ダウンロードに失敗しました:%1$s Linuxランタイムドライバー(-Linux zipまたはAndroid + Linuxバンドル)を選択 ディスプレイドライバー(AdrenoTools zipまたはAndroid + Linuxバンドル)を選択 ドライバーzipまたはAndroid + Linuxバンドルを選択 - %1$s:%2$s %1$sを削除中… @@ -1135,7 +1208,6 @@ ゲームの保存先フォルダを選択 Deckyプラグインのzipを選択 使用できません:%1$s - プログラム Linux実行環境を削除中 @@ -1147,7 +1219,6 @@ 確認できませんでした:%1$s このビルドには、このDroidDeckコピー用のダウンロードがありません 実行中のセッションを停止してから更新してください - ダウンロード中 インストール中 @@ -1169,18 +1240,15 @@ システムのVulkan:このGPU用の同梱ビルドはありません %1$s(同梱) 手動 - %1$s · %2$s 現在、片方のみが公開されています 両方で%1$.0f MB - 設定できるドライバーがありません:%1$s 最新のドライバーを確認中… 最新です(確認できませんでした:%1$s) 推奨ドライバーは現在公開されていません - %1$s %2$sをダウンロード中… %1$s %2$sに更新しました。 @@ -1196,10 +1264,8 @@ %1$sをランタイムドライバーとディスプレイドライバーの両方として使用中。ディスプレイドライバーはDroidDeckの再起動後に有効になります。 %1$sを削除しました(両方のドライバー) %1$sを削除しました - 最新:%1$s · 確認 %2$s - 最新:%1$s · %2$sに接続できません · 確認 %3$s @@ -1213,30 +1279,25 @@ Banners-Turnip、WinNative、DroidDeckを確認中… 確認に失敗 - %1$sをインストールしました - どちらかの一覧で選択すると両方のドライバーが設定されます %1$sをインストールしました - メニューで選択してください 実行環境の削除が完了するまでお待ちください - ゲーム - トラックパッド - ダイレクト 置き換えに失敗しました:%1$s + プロファイルの保存に失敗しました:%1$s デスクトップは一時停止中です - %1$sは一時停止中です プログラムは一時停止中です デスクトップを起動中 - %1$sを起動中 プログラムを起動中 @@ -1244,7 +1305,6 @@ セッションログのアーカイブを作成できませんでした。 セッションログを共有できませんでした。 セッションを開始中… - Linux実行環境をダウンロード中 Linux実行環境をダウンロード中 · %1$d / %2$d MB @@ -1260,32 +1320,26 @@ Proton Experimental(ARM64)を展開中 実行環境のカタログに接続できませんでした。接続を確認して、もう一度Playをタップしてください。 Linux実行環境をインストールできませんでした。「設定」で実行環境を確認して、もう一度Playをタップしてください。 - デスクトップをインストールできませんでした(%1$s)。接続を確認して、もう一度「デスクトップ」をタップしてください。 - %1$sが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。 プログラムが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。 デスクトップが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。 - 終了ステータス %1$d · %2$s ログなし - ログに「Function not implemented」エラーが%1$d件あります。このデバイスでは、prootのseccomp高速化が補助プロセスの処理に失敗しています。%2$s →「%3$s」(または「%4$s」)を試してから、もう一度起動してください。 - プラグイン 次回のSteamセッション用にプラグインのzipをインストールします。 ダウンロード中… zipからインストール - ゲーム オーディオ・ビデオ @@ -1299,27 +1353,20 @@ ユーティリティ 人気 トレンド - 新着 - 最近の更新 読み込み中… Flatpakのセットアップ - %1$s:%2$s Flathubでアップデートを確認できませんでした - - インストール済み - フォルダ - グラファイト グラファイト調の画面に、選択・フォーカス・押下時にはロゴのオーブと同じ青を使用します。 @@ -1329,7 +1376,6 @@ 黒の背景に白の文字、選択・フォーカス・押下時には青を使用します。 エレクトリックネイビー より明るい青を、グレーではなくネイビー寄りの画面に使用します。 - Esc Ins @@ -1348,15 +1394,12 @@ Alt スペース AltGr - Tab - %1$s ✓ Ctrl+C Ctrl+D - 先に実行環境をインストールしてください Steamライブラリを利用できません @@ -1364,10 +1407,8 @@ ゲームストレージを利用できません 追加したゲームのフォルダを利用できません Steamのマニフェストを利用できません - 多数のシステム - 内部 Linux システム @@ -1386,7 +1427,6 @@ お気に入りはまだありません。フォルダの⋮メニューからピン留めすると、すぐにここへ戻れます。 お気に入りから削除 0バイト - バイト 場所 @@ -1395,7 +1435,6 @@ 更新日時 フォルダ ファイル - %1$sファイル 読み取り専用 @@ -1410,18 +1449,14 @@ 削除に失敗しました 移動中... コピー中... - 移動中 - %1$s コピー中 - %1$s - 移動中 %1$d/%2$d - %3$s コピー中 %1$d/%2$d - %3$s - %1$d件完了、%2$d件失敗 - %1$d件完了、%2$d件スキップ @@ -1430,7 +1465,6 @@ %1$d個の項目をコピーしました - 「%1$s」はすでに存在します 名前を変更中... @@ -1442,19 +1476,16 @@ 作成 新しい名前 削除しますか? - 「%1$s」を完全に削除しますか? %1$d個の項目を削除しますか? この操作は元に戻せません。 - …ほか%1$d件 - 削除中 %1$d/%2$d - %3$s @@ -1469,7 +1500,6 @@ スキップ このフォルダを選択 戻る - %1$sはマウントされていますが、現在読み取れません リスト表示 @@ -1485,50 +1515,40 @@ お気に入りを非表示 お気に入りを表示 このフォルダを絞り込み - 空き %1$s %1$d個選択中 - 解除 - すべて 貼り付け %1$d個の項目 - %1$sをここに貼り付け - %1$sをここに貼り付け(移動) ファイル ストレージ - クイック お気に入り ダウンロード - DL ROM セッションログ 画像 - 「%1$s」をお気に入りに追加しました - 「%1$s」をお気に入りから削除しました 空のフォルダ 1つ上の階層へ フォルダを選択 ファイルを選択 - Steamブルー スカイ @@ -1538,7 +1558,6 @@ アンバー レッド ピンク - Aボタン Bボタン @@ -1550,25 +1569,20 @@ 右トリガー ビューボタン メニューボタン - - ビュー - メニュー 十字キー上 十字キー下 十字キー左 十字キー右 - 非表示 ドラッグして移動 スナップ 保存 - なし キーボード + トラックパッド @@ -1576,7 +1590,6 @@ Deckグリップ + トラックパッド - Linux実行環境を削除中 @@ -1594,11 +1607,9 @@ Linux実行環境がインストールされていません - カタログに%1$sのチェックサムがありません チェックサムが一致しません - 何も変更されていません - %1$sをインストール中 AppImageを配置できませんでした @@ -1612,51 +1623,41 @@ 先にFlatpakをセットアップしてください 開始中 Flatpakがステータス%1$dで終了しました - %1$sをインストール中 %1$sを更新中 %1$sを削除中 - %1$sをインストール中(%2$d/%3$d) %1$sを更新中(%2$d/%3$d) %1$sを削除中(%2$d/%3$d) - 翻訳 グラフィックドライバー メディアコーデック - %1$sランタイム - %1$sランタイム %2$s ファイルが小さすぎるため、AppImageではありません - ファイルを読み取れません:%1$s AppImageではありません(Linuxプログラムではありません) このAppImageはARM64(aarch64)向けでもx86 PC向けでもありません Linuxプログラムですが、AppImageではありません インポートできるのはタイプ2のAppImageのみです(このファイルはタイプ%1$d) - 空き容量不足:インポートには約%1$sが必要です - 空き容量不足:更新には約%1$sが必要です インポートに失敗しました このAppImageは存在しません 更新に失敗しました - %1$sを展開中 AppImageを展開できませんでした - AppImageを展開できませんでした:%1$s @@ -1696,26 +1697,19 @@ remote_binaryの項目が無効です リモートバイナリ名が無効です リモートバイナリのURLにはHTTPSを使用する必要があります - リモートバイナリ%1$sのSHA-256ハッシュが無効です プラグインのbinフォルダを作成できませんでした - プラグインバイナリをダウンロード中 · %1$s - プラグインバイナリ%1$sをダウンロードできませんでした - プラグインバイナリのチェックサムが一致しません · %1$s - プラグインバイナリを実行可能にできませんでした · %1$s - プラグインバイナリを置き換えられませんでした · %1$s - プラグインバイナリをインストールできませんでした · %1$s このプラグインフォルダは別のプラグインが使用しています @@ -1726,10 +1720,8 @@ ワイヤレスデバッグの識別情報を保存できませんでした - ADBコマンドに失敗しました(%1$d) - Androidは子プロセス制限の変更を確認しませんでした:%1$s ワイヤレスデバッグのペアリング情報が保存されていません @@ -1737,10 +1729,8 @@ ペアリングのポップアップを待機中にタイムアウトしました。DroidDeckからもう一度お試しください。 - ペアリングのポップアップを検索できませんでした(%1$d)。Wi-Fiがオンになっているか確認してください。 - ペアリングのポップアップを検索できませんでした:%1$s 6桁のコードをすべて入力してください @@ -1755,13 +1745,11 @@ ワイヤレスデバッグのペアリング ワイヤレスデバッグをオンにして、「ペアリングコードでデバイスをペア設定」をタップしてください。 ワイヤレスデバッグをオンにして、「ペアリングコードでデバイスをペア設定」をタップしてください。設定画面のままにしてください:コードはこの通知に入力します。 - 6桁のペアリングコード 「コードを入力」をタップし、設定に表示された6桁の数字を入力してください。 Wi-Fiペアリングコードを入力 コードをもう一度入力 - コードを入力 Steamをセットアップ中 @@ -1773,12 +1761,10 @@ ダウンロード先がHTTP %1$dを返しました ダウンロードしたファイルのサイズが公開されているAPKと一致しないため、破棄しました。もう一度お試しください ダウンロードしたファイルがチェックサムと一致しないため、破棄しました。もう一度お試しください - このアップデートはAndroidパッケージ%1$s用で、%2$s用ではありません アップデート情報にDroidDeckのリリース署名キーが記載されていません AndroidがダウンロードしたAPKを読み取れませんでした - ダウンロードしたAPKは%1$s用で、%2$s用ではありません ダウンロードしたAPKのversionCodeは%1$dで、公開されている%2$dと一致しません @@ -1788,14 +1774,11 @@ アップデートカタログがHTTP %1$dを返しました - セッションを開始中… - %1$d:%2$02d 経過 · 処理中 Steamクライアントのアップデートをダウンロード中 · %1$d%% - Steamクライアントをダウンロード中 · %1$s @@ -1816,7 +1799,6 @@ チェックサムが一致しないため、ダウンロードを削除しました ダウンロード中にセッションが開始されました。停止してから互換性ツールをインストールしてください %1$sをインストール中 - %1$sのインストールに失敗しました(終了コード %2$d) %1$sをインストールできませんでした @@ -1827,30 +1809,24 @@ DLLのみ 元(前回の置き換え以降にProtonが更新されました) 「コンポーネント」以外で変更されました - なし - 元 %1$s %1$sの元のファイル %3$sで実行中のゲームが終了すると、%1$s %2$sがそこに入ります。 %1$s %2$sが%3$sに入りました。次回のゲーム起動時に有効になります。 その元のバンドルは保存されていません - %2$sで実行中のゲームが終了すると、その%1$sが元に戻ります。 - %2$sの%1$sを%3$sの元のファイルから復元しました。 - %1$sは%2$sで使用中です。先にそのProtonを別のものに置き換えてください。 %1$sは保存されていません %1$sを削除しました。 インストール済みビルドの元のファイルは、元に戻すために保持されます。 保存されていません - %2$sの%1$s版の元のファイルを削除しました。 ダウンロードはNightliesのリリースからのみ行われます @@ -1902,7 +1878,6 @@ ダウンロード先がHTTP %1$dを返しました このドライバー一覧はチェックサム導入前のものです。更新してからもう一度お試しください チェックサムが一致しないため、ダウンロードを破棄しました - AdrenoToolsドライバーではありません:%1$s meta.jsonがありません @@ -1910,7 +1885,6 @@ これはLinuxランタイムドライバーです(「ランタイムドライバー」でインポートしてください) これはWaylandゲーム用ドライバーで、このアプリでは使用しません meta.jsonにlibraryNameが指定されていないため、AdrenoToolsに渡せるものがありません - meta.jsonで指定された%1$sがzipに含まれていません %1$sは64ビットAArch64 ELF共有ライブラリではありません @@ -1924,22 +1898,19 @@ SDカード - フォルダ - セッションログを共有 + セッションログを共有 Android 10以降が必要です(色空間付きのディスプレイレイヤー) このディスプレイはHDRに対応していません - このディスプレイは%1$sに対応していますが、HDR10には対応していません - このGPU 子プロセス制限がオンです @@ -1947,7 +1918,6 @@ 子プロセス制限を確認できませんでした 子プロセス制限はオフです 子プロセス制限の変更は不要です - %1$s それができない場合は、このデバイスをADBが使えるPCに接続し、以下のコマンドを実行してください。その後ここに戻って再確認してください。 Steamセッションを開始できます。 @@ -1955,81 +1925,60 @@ 開発者向けオプションで「子プロセス制限を無効にする」をオンにし、その後も開発者向けオプションをオンのままにしてください。 このAndroidバージョンにはこのスイッチがありません。ワイヤレスデバッグを使って、このデバイスから変更してください。 このビルドには、このDroidDeckコピー用のダウンロードがありません。 - このビルドのversionCode %1$dはインストール済みの%2$dより小さいため、Androidは上書きインストールしません。 Androidは新しいバージョンの上に古いバージョンをインストールしません。チャンネルが追いつくまで、このビルドを使い続けてください。 このDroidDeckコピーは署名が異なる(ローカルビルド?)ため、Androidは上書き更新しません。 アップデートに必要な空き容量が不足しています。 アップデートはキャンセルされました。 - アップデートをインストールできませんでした:%1$s アップデートをインストールできませんでした。 - %1$s zip - zip - %1$sはまだ起動されていません %1$sのセーブファイルが見つかりません - %1$sにはdrive_cフォルダがないため、GameHubまたはWinlatorのセーブデータzipではありません - %1$sにフォルダを作成できません %1$sに書き込めません - %1$sに書き込めません(%2$s) フォルダを利用できません - %1$sは別のファイルがすでに使用しています 画像フォルダを利用できません フォルダに無関係な同期情報があります - %1$sを削除できませんでした ファイルがエクスポートフォルダの外を指しています - FEX(バージョン名のないビルド) - %1$sにprofile.jsonがありません - %1$sはLinuxコンポーネントパッケージではありません(タイプ %2$s) - %1$s · 空き %2$s - 自動 - アップデートカタログの検証に失敗しました(%1$s) - DroidDeckの子プロセス設定 - ログイン中 · オフラインモード ログイン中 - 同梱 ダウンロード インポート - ANDROID + LINUX - %1$s。%2$s\n\n%3$s セッションの表示 @@ -2048,6 +1997,7 @@ フレームレート %1$s スケーリングを%2$sに設定します。 画面に合わせる(%1$d×%2$d) + 画面に追従(開閉時にサイズを変更) Steam デバイスの詳細を表示 デバイスの詳細を非表示 @@ -2057,6 +2007,186 @@ コピーしました。 SSBSを強制 Wineがオフにし続けるCPUの速度設定をオンに保ちます。Snapdragon 8 EliteではWindowsゲームが速くなることがあります。古いチップではほとんど変化がありません。次のゲーム起動時に反映されます。 + + ストア + Steam + カスタム + ストア + ストアの初期表示 + ダウンロード速度レベル + ストアエンジン + ● エンジン準備完了 + 未ビルド + バランス + 高速 + 最大 + ストア + ストア設定 + 設定 + (未ログイン) + ストア + インストール済み + ライブラリ + すべて + ダウンロード + %1$s(%2$d) + ログイン + %1$s · ログアウト + %1$sからログアウトしますか? + インストール済みのゲームは残ります。再度ログインすると、ライブラリをもう一度取得します。 + ログインしたままにする + ログアウト + %1$sのカタログを検索… + インストール済みのゲームを検索… + ライブラリを検索… + 検索 + 何もインストールされていません + 読み込み中 + ゲームがありません + ストアを読み込み中… + カタログなし + 新着 + セール + 無料ゲーム + トレンド + マイライブラリ + プレイ + インストール + インストールを再開 + 中断 + インストール · %1$s + 無料で入手 + %1$sで見る + 購入 · %1$s + インストール済み + ライブラリにあり + 無料 + 再開 + 更新 + 成人向けコンテンツを表示 + Epic + EOS + オフライン + オフ + Epicログイン(EOS) + オフラインで起動 + Epicログインの問題を解決 + クラウドセーブ + クラウドにアップロード + クラウドからダウンロード + クラウドセーブなし + クラウドの競合 + クラウドの競合 · %1$d個のファイル + … + クラウドを残す + ローカルを残す + クラウドを確認中… + 最終同期 %1$s + 未同期 + %1$d個のファイル + インストール済み + ライブラリ内 + ストア + 実績 + %2$d 個中 %1$d 個を解除 + %1$d / %2$d + 解除済み · %1$d + 未解除 · %1$d + 隠し実績 · %1$d + 隠し実績 + 解除すると詳細が表示されます。 + すべての実績を解除 + 読み込み中… + 実績を表示するには %1$s にサインインしてください。 + %1$s に接続できませんでした。オンラインになってから再度確認してください。 + %1$s にはこのゲームの実績がありません。 + すべて表示(%1$d) + 実績 · %1$s + ✓ Steamに追加済み + アンインストール + アンインストール中… + %1$sをアンインストールしますか? + %1$sの空き容量を確保し、Steamから削除します。 + ファイルとSteamのショートカットを削除します。 + Steamは次回の起動時にこのゲームを認識します。Steamから起動するか、今すぐSteamを起動してください。 + Proton + ゲームごとにSteamが選択 + オーディオ + DirectAudio + コンポーネント + FEX、DXVK、VKD3D + エンジン + %1$s · ネイティブ + %1$s · 内蔵 + 実行の仕組み + 取得元 + %1$s(あなたのアカウントで取得) + インストール先 + ゲームストレージ(Games/%1$s/%2$s) + 起動方法 + SteamとProton(Steamゲームと同じ経路) + SteamとProton · 起動ごとにEpicログイン、EOSオーバーレイはオフ + SteamとProton · Amazon Games Servicesの変数を設定 + Steam + インストール時にSteamに追加(Deck UI + Steam Input) + ダウンロードはありません + 一時停止中 + 待機中 + 待機中 · #%1$d + キャンセル済み + 失敗 + 失敗 · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + 一時停止 + 再開 + キャンセル + 再試行 + 消去 + 完了分を消去 + マニフェスト + ダウンロード + 検証 + インストール + 完了 + 確認中 + ダウンロード中 + ダウンロードを削除しますか? + 失敗 · %1$sを保持 + 検証中 + インストール中 + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + ファイル %1$d/%2$d + ダウンロードマネージャー + 同時ダウンロード数 + インストール先 + %1$sをインストール: + 内部ストレージ + 空き %1$s + ストアのダウンロード + ゲームのダウンロードの進行状況を表示し、バックグラウンドでダウンロードを継続します + ダウンロード中 + %1$d件のダウンロード + 準備中… + 完了したストアのダウンロード + %1$sをインストールしました + %1$sのダウンロードに失敗しました + 削除 + 保持 + %1$sにログイン + プロバイダーでログイン + 再読み込み + 閉じる + 再試行 + ログインを完了しています… + ログインページを読み込めませんでした:%1$s + このプロバイダーは埋め込みページからのログインを拒否しています。代わりにストアのパスワードでログインしてください。 + ログインページがエラーを返しました(HTTP %1$d)。 + ログインページへの接続は安全ではありません。 + ログインを確認できませんでした。もう一度お試しください。 + ログインに失敗しました。もう一度お試しください。 閉じる 戻る 編集 @@ -2088,21 +2218,122 @@ %1$d 本はすでにゲームにあります - 実績 - %2$d 個中 %1$d 個を解除 - %1$d / %2$d - 解除済み · %1$d - 未解除 · %1$d - 隠し実績 · %1$d - 隠し実績 - 解除すると詳細が表示されます。 - すべての実績を解除 - 読み込み中… - 実績を表示するには %1$s にサインインしてください。 - %1$s に接続できませんでした。オンラインになってから再度確認してください。 - %1$s にはこのゲームの実績がありません。 - すべて表示(%1$d) - 実績 · %1$s + B · %1$sに戻る + ゲーム + プレイ設定 + グラフィックドライバー + このProton + すべてのProton + すべてのProtonに適用 + 利用可能 · %1$d + 確認日時 %1$s + 新しいコンポーネントとドライバーを確認 + 入手 + %1$s · 以下のDXVK、VKD3D-Proton、FEXはこのProtonのものです + %1$s · テスト済み + %1$s · 未テスト + %1$s · サポート外 + 推奨の組み合わせを最新に保ちます + 自動 · %1$s + ランタイムドライバーとディスプレイドライバーを個別に設定 + %1$sを使用しますか? + %1$sを削除しますか? + 診断 + プレイの準備完了 + まだプレイの準備ができていません + 実行環境 %1$s + プロセス制限オフ + %1$sとしてログイン中 + 対応が必要なとき + 外観 + このデバイス + ライブラリのアートワーク + Linuxアプリ + Flathub + Linuxデスクトップ用のアプリ。「デスクトップ」から開きます。 + Flathubをセットアップ + %1$sを削除しますか? + 設定とファイルは保持されます。 + 保持 + ディスク上のサイズ + デスクトップをアンインストールしますか? + すべて + GOGの実績 + 対応するGOGゲームの実績を同期します。次回の起動から適用されます。 + 実績オフ + 実績オン + GOGに再度サインインしてください + 実績をアップロードしました + 実績のアップロード待ち + ストア設定でGOGの実績を有効にしてください + 実績を利用できません。セッションログを確認してください + 実績を解除しました + 貼り付け + 十字キー + 左スティック + 右スティック + メニュー + キーボード + 十字キー上 + 十字キー下 + 十字キー左 + 十字キー右 + タッチ操作 + タッチコントローラー + マウス + オン + オフ + 遅い + 普通 + 速い + レイアウトを編集 + DroidDeckメニュー + 操作ボタンを画面にドラッグしてください + 配置 + 破棄 + 保存中… + アイコン + 幅− + 幅+ + 高− + 高+ + 非表示 + 色と不透明度… + このレイアウトをコピー + レイアウトをコピーしました。任意のゲームのタッチレイアウトに貼り付けられます + コピーしたレイアウトを貼り付け + Steamの初期設定に戻す + この操作にはSteamでアイコンを設定できる割り当てがありません + %1$sのアイコン + タッチレイアウトの色 + 完了 + タッチレイアウトを保存できませんでした + タッチレイアウトをSteamに保存しました + アプリ%1$d + ラベル(任意) + Steamのアイコンが見つかりませんでした + + Steamのアイコン%1$d個 + + アイコンの色 + ボタンの色 + アイコンなし + + サインインを確認中… + ライブラリを取得中… + ゲームリストを取得中… + ゲーム情報を読み込み中… (%1$d/%2$d) + + %1$d個のゲームを同期中… + + 同期中… %1$d/%2$d + カバーアートを取得中… + 同期に失敗しました + %1$sのライブラリにゲームが見つかりません + Amazon Gamesからライブラリの応答がありません。更新するか、再度サインインしてください + GOGに接続できませんでした + GOGのライブラリを読み込めませんでした + Steamに登録中… 開発元 パブリッシャー 発売日 @@ -2111,4 +2342,34 @@ 完全制覇 %1$s時間 %1$d分 + Cardboard VR + Cardboardモード + スマホ用VRビューアー向けの左右の目の映像です。 + 映像 + 仮想スクリーン + サイドバイサイド3D + ビューアー + Cardboard + Cardboard(2014) + レンズなし + 目の間隔 + スクリーンの幅 + スクリーンまでの距離 + ヘッドトラッキング + このデバイスには回転センサーがありません。 + 視点をリセット + リセット + SteamVR(試験運用) + インストール + ValveのARM64版SteamVRをダウンロードします(571 MB)。 + SteamVRをダウンロードして展開しています… + 開始 + 頭の動きをヘッドセットとしてSteamVRを実行します。映像はサイドバイサイド3Dに切り替わります。 + 停止 + SteamVRは実行中です。 + このセッションでは利用できません。 + SteamVRが応答しませんでした。セッションログを確認してください。 + コンポーネントをダウンロード + ダウンロードは「コンポーネント」ページと共有されます。上でダウンロード済みのバージョンを選ぶと、次回のゲーム起動時に使用されます。 + ダウンロードするバージョンを選択 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 687dfd59a..fa9282a09 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -15,8 +15,10 @@ 세션 Steam 또는 데스크톱 세션이 실행 중일 때 표시 Steam이 실행 중입니다 + Steam 다운로드 진행 중 세션이 일시 중지됨 - 준비되면 \"재개\"를 탭하세요 재개 + 지금 일시 중지 세션 종료 프레임 생성 사용할 프레임 생성 모드를 선택하세요 @@ -182,6 +184,8 @@ 앱 ID org.ppsspp.PPSSPP 유효한 Flathub 앱 ID가 아닙니다 + 또는 Flathub에 없는 앱의 .flatpak 파일. + .flatpak 파일 선택 소스 데스크톱에 표시 새 앱 @@ -251,6 +255,10 @@ 화면 컨트롤 Steam + QAM 자동 모드에서는 컨트롤러가 연결되지 않은 경우 화면 컨트롤을 표시합니다. + Steam 터치(베타) + Steam 터치 컨트롤은 다음 Steam 세션부터 적용됩니다 + Steam 터치 레이아웃 + 이 게임의 터치 컨트롤을 옮기거나 크기를 바꾸거나 숨깁니다. Steam에 저장됩니다 컨트롤러 Steam이 컨트롤러를 인식하는 방식입니다. 다음 세션에 적용됩니다. Steam Deck 컨트롤러 @@ -268,6 +276,8 @@ 수동 네이티브 자동: 일시 중지되고 다시 표시되면 재개됩니다. 네이티브: 먼저 Steam을 절전에 대비시킵니다. 수동: \"재개\"를 탭할 때까지 기다립니다. 안 함: 계속 실행됩니다. + 다운로드 계속 진행 + Steam이 다운로드 중일 때는 백그라운드 일시 중지를 미룹니다. 자동: 백그라운드에서 일시 중지되고 다시 표시되면 재개됩니다. 수동: \"재개\"를 탭할 때까지 기다립니다. 안 함: 계속 실행됩니다. 시작 DroidDeck을 열 때 Steam 시작 @@ -331,6 +341,7 @@ 게임에 %1$s이(가) 있습니다 (대개 그대로 작동) 이 게임에 이미 있음 게임 폴더에서 필요한 구성 요소를 가리키는 것이 없습니다. + 이 게임에 설치됨 (%1$d) 모든 구성 요소 (%1$d) Windows 설치 프로그램 필요 (%1$d) 이 항목들은 프리픽스 안에서 실제 설치 프로그램을 실행합니다(.NET, PhysX, XNA, 전체 Visual C++ 설치 프로그램 등). DroidDeck은 아직 지원하지 않으며 다음에 지원할 예정입니다. @@ -341,6 +352,9 @@ 구성 요소를 불러오는 중… 설치할 수 없습니다: %1$s 다운로드됨 + %1$s 다운로드 중 + 설치 중: %1$s + 끄는 중… %1$s 다운로드 다음에 게임을 시작할 때 적용됩니다. 16:9 게임 늘리기 @@ -397,6 +411,44 @@ Nightlies의 새 패키지 확인 Proton 여기에서 선택한 Proton은 Steam에서 Proton을 따로 선택하지 않은 모든 게임을 실행하며, Steam에서 변경한 내용도 여기에 표시됩니다. Steam 업데이트 시 Valve의 Proton이 교체되지만 각 빌드의 원본 파일은 유지됩니다. + 구성 + 프로필 구성 + 프로필 + 기본 프로필 + 재정의가 없는 게임에서 자동으로 사용됩니다. + %1$s 실행 시 자동으로 사용되는 저장된 설정입니다. + 이 게임에 필요한 설정만 선택하세요. 나머지는 계속 기본 프로필을 사용합니다. + FEX 프리셋 + 기본 프로필의 프리셋 사용 중. + 이 게임에 사용자 정의 프리셋이 저장되었습니다. + 사용자 정의 FEX 변수 + 기본값 사용 + 게임 호환성 + 이 게임의 Proton, DXVK, VKD3D 및 동기화 환경 옵션입니다. + 기본 프로필의 Proton 사용 중. + Steam 호환성 매핑을 통해 이 게임에 사용자 정의 Proton이 저장되었습니다. + Proton 없음 + DXVK + VKD3D-Proton + 이 게임에서 다른 패키지를 선택하지 않으면 %1$s의 기본 프로필 패키지를 사용합니다. + 기본 프로필의 구성 요소 패키지를 사용합니다. + 이 게임에만 %1$s을(를) 사용합니다. + 런타임 옵션 + 기타 게임별 Proton, DXVK, VKD3D-Proton 및 동기화 환경 옵션입니다. + %1$s의 프로필이 저장되었습니다. 다음 게임 실행 시 적용됩니다. + Steam 없이 시작 + 바로 닫히는 게임의 경우: Proton의 Steam 설정 없이 실행합니다. 오버레이 같은 Steam 기능은 이 게임에서 꺼집니다. + 켜짐 + %1$s의 Proton 선택이 저장되었으며 지금 Steam에서 사용할 수 있습니다. + %1$s의 Proton 선택이 저장되었습니다. 게임을 실행하기 전에 Steam을 다시 시작하세요. + %1$s은(는) 이 기기에서 선택한 Proton을 사용할 수 없습니다. + %1$s의 프로필을 불러올 수 없습니다: %2$s + 게임 프로필을 저장할 수 없습니다: %1$s + 게임 프로필 초기화 + %1$s의 프로필을 초기화하시겠습니까? + 이 게임의 Proton, FEX, 구성 요소 패키지, 환경, 텍스처 필터링 및 Windows 구성 요소 재정의를 제거하시겠습니까? 이후 기본 프로필을 사용합니다. + 이제 %1$s은(는) 기본 프로필을 사용합니다. + 게임 프로필을 완전히 초기화할 수 없습니다: %1$s 이제 Steam에서 Proton을 따로 선택하지 않은 모든 게임을 %1$s(으)로 실행합니다. 다음에 Steam을 시작할 때부터 Proton을 따로 선택하지 않은 모든 게임을 %1$s(으)로 실행합니다. %1$s(으)로는 여기에서 게임을 실행할 수 없으므로 기본값으로 설정할 수 없습니다. @@ -560,6 +612,18 @@ 세션을 중지하시겠습니까? 현재 실행 중 현재 Proton으로 실행 중인 게임이 없습니다 + 게임 프로필 + 프로필 세부 정보를 아직 사용할 수 없습니다. + 게임 프로필을 불러올 수 없습니다. 자동으로 다시 시도합니다. + 게임 프로필을 새로 고칠 수 없습니다. 마지막으로 가져온 데이터를 표시하며 자동으로 다시 시도합니다. + 다음 실행 시 사용되는 저장된 설정을 보고 편집합니다. + 기본 프로필 사용 중 + 사용자 정의 프로필 + 기본 프로필의 %1$s 사용 중. + 변경 사항은 %1$s의 프로필에 저장되며 다음 게임 실행 시 적용됩니다. + + 게임 재정의 %1$d개 + Proton이 설치되지 않았습니다. 게임 실행 중 %1$s 시작 시 다시 적용됨 @@ -631,6 +695,10 @@ 홈 화면으로 사용 DroidDeck을 홈 앱으로 사용할 수 있습니다 DroidDeck은 홈 앱으로 제공되지 않습니다 + 화면 방향 + 자동 가로 모드 + 가로 모드 + 역방향 가로 모드 전체 화면 Android 상태 표시줄 및 탐색 표시줄 숨기기 Android 상태 표시줄 및 탐색 표시줄 표시 @@ -888,6 +956,12 @@ 해당 페이지가 더 이상 존재하지 않습니다. 왼쪽에서 항목을 선택하세요. 데스크톱 열기 설치 및 데스크톱 열기 + 데스크톱 제거 + 데스크톱과 번들 앱을 제거합니다. 게임, 추가한 앱, 설정 및 공유 라이브러리는 유지됩니다. + 데스크톱 제거 중 + 데스크톱 제거됨 + 이전 LXQt 파일 제거 중 + 이 데스크톱 버전을 제거하기 전에 DroidDeck을 업데이트하세요. Linux 데스크톱 KDE Plasma, Firefox 및 에뮬레이터 Steam에서 설치한 게임은 세션 종료 후 여기에 표시됩니다. @@ -916,6 +990,8 @@ 마지막 실행 %1$s 내부 저장 공간 추가된 게임 + 사용자 정의 게임 + 게임 저장 공간 이 GPU에 가장 적합한 드라이버 조합을 선택하고 최신 상태로 유지합니다 드라이버 조합을 직접 선택 드라이버 확인 중… @@ -957,6 +1033,12 @@ 왼쪽 터치패드 오른쪽 터치패드 양쪽 + 게임패드 + 스틱 + 터치패드 + 스틱과 터치패드 간 전환 + Deck 컨트롤 더 보기 + 패널 보조 화면 컨트롤 닫기 Steam 메뉴 열기 왼쪽 · 위 @@ -992,6 +1074,10 @@ %1$s 설치됨 이전 요청으로 인한 대기 항목 설치되지 않음 + 업데이트 있음 → %1$s + 이전 %1$s 버전 제거 + %1$s을(를) 제거합니다. 이 버전으로 설정된 게임은 다른 Proton으로 전환해야 합니다. + 이 버전으로 설정된 게임은 다른 Proton으로 전환해야 합니다. 지금 설치 대기 취소 Android 키보드 @@ -1020,6 +1106,8 @@ Proton 게임 PIP (픽처 인 픽처) 자동으로 PIP 시작 + 펼친 화면의 컨트롤 + 펼치면 게임이 Steam Deck 터치패드와 뒷면 그립 위에 표시됩니다. 접으면 게임이 전체 화면으로 표시됩니다. 일시 중지됨 세션이 준비되면 PIP를 사용할 수 있습니다. Android 설정에서 DroidDeck의 PIP 사용을 허용하세요. @@ -1046,17 +1134,14 @@ 프론트엔드 파일 - 런타임 작업이 끝날 때까지 기다려 주세요 호환성 도구 설치가 끝날 때까지 기다려 주세요 데스크톱 앱 설치가 끝날 때까지 기다려 주세요 설치가 끝난 후 게임을 실행하세요 게임이 가져온 데이터를 덮어쓰지 않도록 먼저 Steam 세션을 종료하세요 - %1$s(으)로 가져오는 중 - %2$s에서 %3$s(으)로 파일 %1$d개를 가져왔습니다 @@ -1065,18 +1150,14 @@ %2$s에서 %3$s(으)로 파일 %1$d개를 가져왔습니다. 이전 저장 데이터는 Download/DroidDeck/Saves/backups에 백업되었습니다 %1$s의 저장 데이터 zip 선택 - %1$s 내보내는 중 - 파일 %1$d개를 %2$s(으)로 내보냈습니다: %3$s - %1$s을(를) 저장할 위치 선택(%2$s) - %1$s 실패: %2$s 아직 세션 로그가 없습니다. 먼저 세션을 실행하세요. @@ -1090,23 +1171,18 @@ ADB 명령이 복사되었습니다 ROM 폴더 선택 Adreno GPU가 아님 - Turnip은 Adreno GPU를 지원합니다. %1$s에서는 Steam 화면이 검게 표시될 수 있습니다. 다운로드: %2$.0f MB. 그래도 설치 Linux 런타임 제거 런타임과 그 안의 Steam 클라이언트, 그리고 런타임에 설치된 모든 게임이 삭제됩니다. - 적용됨: %1$s 메인 화면에서 %1$s을(를) 열 수 없습니다 - %2$s에서 %1$s을(를) 열 수 없습니다 - 디스플레이 %2$d에서 %1$s을(를) 열 수 없습니다 - 가져오는 중 교체하는 중 @@ -1114,20 +1190,17 @@ 취소하는 중 삭제하는 중 파일을 읽을 수 없음 - %1$s 가져옴 대기 중이던 교체가 취소되었습니다. 구성 요소 패키지 선택(-linux .wcp) Nightlies에 연결할 수 없습니다 - %1$s 저장됨 다운로드 실패: %1$s Linux 런타임 드라이버 선택(-Linux zip 또는 Android + Linux 번들) 디스플레이 드라이버 선택(AdrenoTools zip 또는 Android + Linux 번들) 드라이버 zip 또는 Android + Linux 번들 선택 - %1$s: %2$s %1$s 제거 중… @@ -1135,7 +1208,6 @@ 게임 저장 폴더 선택 Decky 플러그인 ZIP 선택 사용할 수 없음: %1$s - 프로그램 Linux 런타임 제거 중 @@ -1147,7 +1219,6 @@ 확인할 수 없음: %1$s 이 빌드에는 이 DroidDeck 복사본용 다운로드가 없습니다 실행 중인 세션을 먼저 중지한 후 업데이트하세요 - 다운로드 중 설치 중 @@ -1169,18 +1240,15 @@ 시스템 Vulkan: 이 GPU용 내장 빌드 없음 %1$s(내장) 수동 - %1$s · %2$s 현재 한쪽만 배포되어 있습니다 둘 다 %1$.0f MB - 설정할 드라이버 없음: %1$s 최신 드라이버 확인 중… 최신 버전(확인 실패: %1$s) 현재 권장 드라이버가 배포되지 않았습니다 - %1$s %2$s 다운로드 중… %1$s %2$s(으)로 업데이트했습니다. @@ -1196,10 +1264,8 @@ %1$s을(를) 런타임 및 디스플레이 드라이버로 사용 중. 디스플레이 드라이버는 DroidDeck을 다시 시작한 후 적용됩니다. %1$s 삭제됨(드라이버 2개) %1$s 삭제됨 - 최신: %1$s · %2$s 확인 - 최신: %1$s · %2$s 연결 불가 · %3$s 확인 @@ -1213,30 +1279,25 @@ Banners-Turnip, WinNative 및 DroidDeck 확인 중… 확인 실패 - %1$s 설치됨 - 두 목록 중 어디에서든 선택하면 두 드라이버가 모두 설정됩니다 %1$s 설치됨 - 메뉴에서 선택하세요 런타임 제거가 끝날 때까지 기다려 주세요 - 게임 - 터치패드 - 직접 터치 교체 실패: %1$s + 프로필 저장 실패: %1$s 데스크톱이 일시 중지됨 - %1$s 일시 중지됨 프로그램이 일시 중지됨 데스크톱 시작 중 - %1$s 시작 중 프로그램 시작 중 @@ -1244,7 +1305,6 @@ 세션 로그 압축 파일을 만들 수 없습니다. 세션 로그를 공유할 수 없습니다. 세션 시작 중… - Linux 런타임 다운로드 중 Linux 런타임 다운로드 중 · %1$d/%2$d MB @@ -1260,32 +1320,26 @@ Proton Experimental(ARM64) 압축 해제 중 런타임 카탈로그에 연결할 수 없습니다. 연결을 확인한 후 \"Steam 실행\"을 다시 누르세요. Linux 런타임이 설치되지 않았습니다. \"설정\"에서 런타임을 확인한 후 \"Steam 실행\"을 다시 누르세요. - 데스크톱이 설치되지 않았습니다(%1$s). 연결을 확인한 후 \"데스크톱\"을 다시 누르세요. - %1$s이(가) 예기치 않게 중지되었습니다. 버그 리포트와 함께 로그를 공유하거나 다시 시도하세요. 프로그램이 예기치 않게 중지되었습니다. 버그 리포트와 함께 로그를 공유하거나 다시 시도하세요. 데스크톱이 예기치 않게 중지되었습니다. 버그 리포트와 함께 로그를 공유하거나 다시 시도하세요. - 종료 상태 %1$d · %2$s 로그 없음 - 로그에 \"Function not implemented\" 오류가 %1$d개 있습니다. 이 기기에서 proot의 seccomp 가속이 보조 프로세스를 실패시키고 있습니다. %2$s → \"%3$s\"(또는 \"%4$s\")를 시도한 후 다시 시작하세요. - 플러그인 다음 Steam 세션에 사용할 플러그인 ZIP을 설치합니다. 다운로드 중… ZIP에서 설치 - 게임 오디오 및 비디오 @@ -1299,27 +1353,20 @@ 유틸리티 인기 급상승 - 신규 - 업데이트됨 불러오는 중… Flatpak 설정 - %1$s: %2$s Flathub에서 업데이트를 확인할 수 없습니다 - - 설치됨 - 폴더 - 그래파이트 그래파이트 색상의 화면에, 선택·포커스·누름 상태에는 로고의 오브 블루를 사용합니다. @@ -1329,7 +1376,6 @@ 검은색 배경에 흰색 글자, 선택·포커스·누름 상태에는 파란색을 사용합니다. 일렉트릭 네이비 회색 대신 네이비 톤의 화면에 더 밝은 파란색을 사용합니다. - Esc Ins @@ -1348,15 +1394,12 @@ Alt Space AltGr - Tab - %1$s ✓ Ctrl+C Ctrl+D - 먼저 런타임을 설치하세요 Steam 라이브러리를 사용할 수 없습니다 @@ -1364,10 +1407,8 @@ 게임 저장 공간을 사용할 수 없습니다 추가된 게임 폴더를 사용할 수 없습니다 Steam 매니페스트를 사용할 수 없습니다 - 다양한 기종 - 내부 Linux 시스템 @@ -1386,7 +1427,6 @@ 즐겨찾기가 없습니다. 폴더의 ⋮ 메뉴에서 고정하면 여기로 빠르게 이동할 수 있습니다. 즐겨찾기에서 제거 0바이트 - 바이트 위치 @@ -1395,7 +1435,6 @@ 수정한 날짜 폴더 파일 - %1$s 파일 읽기 전용 @@ -1410,18 +1449,14 @@ 삭제 실패 이동 중... 복사 중... - 이동 중 - %1$s 복사 중 - %1$s - 이동 중 %1$d/%2$d - %3$s 복사 중 %1$d/%2$d - %3$s - %1$d개 완료, %2$d개 실패 - %1$d개 완료, %2$d개 건너뜀 @@ -1430,7 +1465,6 @@ 항목 %1$d개 복사됨 - \"%1$s\"이(가) 이미 있습니다 이름 바꾸는 중... @@ -1442,19 +1476,16 @@ 만들기 새 이름 삭제하시겠습니까? - \"%1$s\"을(를) 영구적으로 삭제하시겠습니까? 항목 %1$d개를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. - …외 %1$d개 - 삭제 중 %1$d/%2$d - %3$s @@ -1469,7 +1500,6 @@ 건너뛰기 이 폴더 선택 뒤로 - %1$s이(가) 마운트되었지만 지금은 읽을 수 없습니다 목록 보기 @@ -1485,50 +1515,40 @@ 즐겨찾기 숨기기 즐겨찾기 표시 이 폴더에서 필터 - %1$s 사용 가능 %1$d개 선택됨 - 선택 해제 - 전체 붙여넣기 항목 %1$d개 - 여기에 %1$s 붙여넣기 - 여기에 %1$s 붙여넣기(이동) 파일 저장소 - 바로 가기 즐겨찾기 다운로드 - 다운로드 ROMs 세션 로그 사진 - \"%1$s\"을(를) 즐겨찾기에 추가했습니다 - \"%1$s\"을(를) 즐겨찾기에서 제거했습니다 빈 폴더 상위 폴더로 폴더 선택 파일 선택 - Steam 블루 하늘색 @@ -1538,7 +1558,6 @@ 호박색 빨간색 분홍색 - A 버튼 B 버튼 @@ -1550,25 +1569,20 @@ 오른쪽 트리거 보기 버튼 메뉴 버튼 - - 보기 - 메뉴 방향 패드 위 방향 패드 아래 방향 패드 왼쪽 방향 패드 오른쪽 - 숨김 드래그하여 이동 맞춤 저장 - 없음 키보드 + 터치패드 @@ -1576,7 +1590,6 @@ Deck 그립 + 터치패드 - Linux 런타임 제거 중 @@ -1594,11 +1607,9 @@ Linux 런타임이 설치되지 않았습니다 - 카탈로그에 %1$s의 체크섬이 없습니다 체크섬이 일치하지 않습니다. 변경된 내용은 없습니다 - %1$s 설치 중 AppImage를 배치할 수 없습니다 @@ -1612,51 +1623,41 @@ 먼저 Flatpak을 설정하세요 시작 중 Flatpak이 종료되었습니다(상태 %1$d) - %1$s 설치 중 %1$s 업데이트 중 %1$s 제거 중 - %1$s 설치 중(%2$d/%3$d) %1$s 업데이트 중(%2$d/%3$d) %1$s 제거 중(%2$d/%3$d) - 번역 그래픽 드라이버 미디어 코덱 - %1$s 런타임 - %1$s 런타임 %2$s 파일이 너무 작아 AppImage가 아닙니다 - 파일을 읽을 수 없습니다: %1$s AppImage가 아닙니다(Linux 프로그램이 아님) 이 AppImage는 ARM64(aarch64)용도, x86 PC용도 아닙니다 Linux 프로그램이지만 AppImage가 아닙니다 유형 2 AppImage만 가져올 수 있습니다(이 파일은 유형 %1$d) - 사용 가능한 공간 부족: 가져오려면 약 %1$s이(가) 필요합니다 - 사용 가능한 공간 부족: 업데이트하려면 약 %1$s이(가) 필요합니다 가져오기 실패 AppImage가 더 이상 존재하지 않습니다 업데이트 실패 - %1$s 압축 해제 중 AppImage의 압축을 풀 수 없습니다 - AppImage의 압축을 풀 수 없습니다: %1$s @@ -1696,26 +1697,19 @@ 잘못된 remote_binary 항목 잘못된 원격 바이너리 이름 원격 바이너리 URL은 HTTPS를 사용해야 합니다 - 원격 바이너리 %1$s의 SHA-256 해시가 잘못되었습니다 플러그인 bin 폴더를 만들 수 없습니다 - 플러그인 바이너리 다운로드 중 · %1$s - 플러그인 바이너리 %1$s을(를) 다운로드할 수 없습니다 - 플러그인 바이너리 체크섬 불일치 · %1$s - 플러그인 바이너리를 실행 가능으로 설정할 수 없습니다 · %1$s - 플러그인 바이너리를 교체할 수 없습니다 · %1$s - 플러그인 바이너리를 설치할 수 없습니다 · %1$s 플러그인 폴더를 다른 플러그인이 사용하고 있습니다 @@ -1726,10 +1720,8 @@ 무선 디버깅 ID를 저장할 수 없습니다 - ADB 명령 실패(%1$d) - Android에서 하위 프로세스 제한 변경을 확인하지 않았습니다: %1$s 저장된 무선 디버깅 페어링이 없습니다 @@ -1737,10 +1729,8 @@ 페어링 팝업을 기다리는 시간이 초과되었습니다. DroidDeck에서 다시 시도하세요. - 페어링 팝업을 찾을 수 없습니다(%1$d). Wi-Fi가 켜져 있는지 확인하세요. - 페어링 팝업을 찾을 수 없습니다: %1$s 코드 6자리를 모두 입력하세요 @@ -1755,13 +1745,11 @@ 무선 디버깅 페어링 무선 디버깅을 켠 다음 “페어링 코드로 기기 페어링”을 탭하세요. 무선 디버깅을 켠 다음 “페어링 코드로 기기 페어링”을 탭하세요. 코드는 이 알림에 입력하므로 설정 화면에 머물러 있으세요. - 6자리 페어링 코드 \"코드 입력\"을 탭하고 설정에 표시된 6자리 숫자를 입력하세요. Wi-Fi 페어링 코드 입력 코드를 다시 입력하세요 - 코드 입력 Steam 설정 중 @@ -1773,12 +1761,10 @@ 다운로드 응답: HTTP %1$d 다운로드 크기가 배포된 APK와 일치하지 않아 삭제했습니다. 다시 시도하세요 다운로드한 파일이 체크섬과 일치하지 않아 삭제했습니다. 다시 시도하세요 - 이 업데이트는 %2$s이(가) 아닌 Android 패키지 %1$s용입니다 업데이트 메타데이터에 DroidDeck 릴리스 서명 키가 지정되어 있지 않습니다 Android에서 다운로드한 APK를 읽을 수 없습니다 - 다운로드한 APK는 %2$s이(가) 아닌 %1$s용입니다 다운로드한 APK의 versionCode가 배포된 %2$d이(가) 아닌 %1$d입니다 @@ -1788,14 +1774,11 @@ 업데이트 카탈로그 응답: HTTP %1$d - 세션 시작 중… - %1$d:%2$02d 경과 · 아직 진행 중 Steam 클라이언트 업데이트 다운로드 중 · %1$d%% - Steam 클라이언트 다운로드 중 · %1$s @@ -1816,7 +1799,6 @@ 체크섬이 일치하지 않아 다운로드를 삭제했습니다 다운로드 중에 세션이 시작되었습니다. 호환성 도구를 설치하기 전에 세션을 중지하세요 %1$s 설치 중 - %1$s 설치 실패(종료 코드 %2$d) %1$s을(를) 설치할 수 없습니다 @@ -1827,30 +1809,24 @@ DLL만 원본(마지막 교체 이후 Proton 업데이트됨) \"구성 요소\" 외부에서 변경됨 - 없음 - 원본 %1$s %1$s의 원본 %3$s을(를) 사용하는 게임이 종료되면 %1$s %2$s(으)로 교체됩니다. 이제 %3$s에 %1$s %2$s이(가) 적용되었습니다. 다음 게임 실행 시 적용됩니다. 해당 원본 번들이 저장되어 있지 않습니다 - %2$s의 %1$s은(는) 해당 Proton을 사용하는 게임이 종료되면 원본으로 복원됩니다. - %2$s의 %1$s을(를) %3$s 원본으로 복원했습니다. - %1$s은(는) %2$s에서 사용 중입니다. 먼저 해당 Proton을 다른 항목으로 교체하세요. %1$s이(가) 저장되어 있지 않습니다 %1$s을(를) 삭제했습니다. 설치된 빌드의 원본은 되돌리기 위해 유지됩니다. 저장되지 않음 - %2$s의 %1$s 원본을 삭제했습니다. Nightlies 릴리스에서만 다운로드할 수 있습니다 @@ -1902,7 +1878,6 @@ 다운로드 응답: HTTP %1$d 이 드라이버 목록은 체크섬 도입 이전의 것입니다. 새로 고친 후 다시 시도하세요 체크섬이 일치하지 않아 다운로드를 삭제했습니다 - AdrenoTools 드라이버가 아닙니다: %1$s meta.json 없음 @@ -1910,7 +1885,6 @@ Linux 런타임 드라이버입니다(\"Linux 런타임 드라이버\"에서 가져오세요) Wayland 게임 드라이버이며, 이 앱에서는 사용하지 않습니다 meta.json에 libraryName이 지정되지 않아 AdrenoTools에 전달할 수 없습니다 - meta.json에 지정된 %1$s이(가) zip에 없습니다 %1$s은(는) 64비트 AArch64 ELF 공유 라이브러리가 아닙니다 @@ -1924,22 +1898,19 @@ SD 카드 - 폴더 - 세션 로그 공유 + 세션 로그 공유 Android 10 이상 필요(색 공간을 지원하는 디스플레이 레이어) 이 디스플레이는 HDR을 지원하지 않습니다 - 이 디스플레이는 HDR10이 아닌 %1$s 형식을 지원합니다 - 이 GPU 하위 프로세스 제한 켜짐 @@ -1947,7 +1918,6 @@ 하위 프로세스 제한을 확인할 수 없음 하위 프로세스 제한 꺼짐 하위 프로세스 제한 해당 없음 - %1$s 이 방법을 사용할 수 없으면 이 기기를 ADB가 설치된 PC에 연결하고 아래 명령을 실행하세요. 그런 다음 여기로 돌아와 다시 확인하세요. Steam 세션을 시작할 수 있습니다. @@ -1955,81 +1925,60 @@ 개발자 옵션에서 “하위 프로세스 제한 사용 중지”를 켜고, 이후에도 개발자 옵션을 켜 둔 상태로 유지하세요. 이 Android 버전에는 해당 스위치가 없습니다. 무선 디버깅을 사용하여 이 기기에서 변경하세요. 이 빌드에는 이 DroidDeck 복사본용 다운로드가 없습니다. - versionCode %1$d이(가) 설치된 %2$d보다 낮아 Android에서 이 빌드 위에 설치하지 않습니다. Android에서는 최신 버전 위에 이전 버전을 설치할 수 없습니다. 채널이 따라잡을 때까지 현재 빌드를 유지하세요. 이 DroidDeck 복사본은 다른 키로 서명되어 있어(로컬 빌드일 수 있음) Android에서 덮어쓰기 업데이트를 할 수 없습니다. 업데이트에 필요한 저장 공간이 부족합니다. 업데이트가 취소되었습니다. - 업데이트가 설치되지 않았습니다: %1$s 업데이트가 설치되지 않았습니다. - %1$s zip - zip - %1$s을(를) 아직 실행한 적이 없습니다 %1$s의 저장 파일을 찾을 수 없습니다 - %1$s에 drive_c 폴더가 없으므로 GameHub 또는 Winlator 저장 데이터 zip이 아닙니다 - %1$s에 폴더를 만들 수 없음 %1$s에 쓸 수 없음 - %1$s에 쓸 수 없음(%2$s) 폴더를 사용할 수 없습니다 - 다른 파일이 이미 %1$s 이름을 사용하고 있습니다 이미지 폴더를 사용할 수 없습니다 폴더에 관련 없는 동기화 목록이 있습니다 - %1$s을(를) 제거할 수 없습니다 파일이 내보내기 폴더 밖을 가리킵니다 - FEX(버전 이름 없는 빌드) - %1$s에 profile.json이 없음 - %1$s은(는) Linux 구성 요소 패키지가 아닙니다(유형 %2$s) - %1$s · %2$s 사용 가능 - 자동 - 업데이트 카탈로그가 검사를 통과하지 못했습니다(%1$s) - DroidDeck 하위 프로세스 설정 - 로그인됨 · 오프라인 모드 로그인됨 - 내장 다운로드됨 가져옴 - ANDROID + LINUX - %1$s. %2$s\n\n%3$s 세션 디스플레이 @@ -2048,6 +1997,7 @@ 프레임 속도 %1$s 스케일링을 %2$s(으)로 설정합니다. 화면에 맞춤(%1$d×%2$d) + 화면 따라가기(접거나 펼치면 크기 변경) Steam 기기 세부 정보 표시 기기 세부 정보 숨기기 @@ -2057,6 +2007,186 @@ 복사했습니다. SSBS 강제 Wine이 계속 끄는 CPU 속도 설정을 켜 둡니다. Snapdragon 8 Elite에서 Windows 게임이 빨라질 수 있으며, 이전 칩에서는 변화가 거의 없습니다. 다음 게임 실행 시 적용됩니다. + + 스토어 + Steam + 사용자 정의 + 스토어 + 스토어를 열 때 표시 + 다운로드 속도 단계 + 스토어 엔진 + ● 엔진 준비됨 + 빌드되지 않음 + 균형 + 빠름 + 최대 + 스토어 + 스토어 설정 + 설정 + (로그인 안 됨) + 스토어 + 설치됨 + 라이브러리 + 전체 + 다운로드 + %1$s (%2$d) + 로그인 + %1$s · 로그아웃 + %1$s에서 로그아웃하시겠습니까? + 설치된 게임은 유지됩니다. 다시 로그인하면 라이브러리를 다시 가져옵니다. + 로그인 유지 + 로그아웃 + %1$s 카탈로그 검색… + 설치된 게임 검색… + 내 라이브러리 검색… + 검색 + 설치된 항목 없음 + 불러오는 중 + 게임 없음 + 스토어 불러오는 중… + 카탈로그 없음 + 새로 나온 게임 + 할인 + 무료 게임 + 인기 급상승 + 내 라이브러리 + 플레이 + 설치 + 설치 재개 + 중단됨 + 설치 · %1$s + 무료로 받기 + %1$s에서 보기 + 구매 · %1$s + 설치됨 + 라이브러리에 있음 + 무료 + 재개 + 새로 고침 + 성인용 콘텐츠 표시 + Epic + EOS + 오프라인 + 끔 + Epic 로그인(EOS) + 오프라인으로 실행 + Epic 로그인 문제 해결 + 클라우드 저장 + 클라우드에 업로드 + 클라우드에서 다운로드 + 클라우드 저장 없음 + 클라우드 충돌 + 클라우드 충돌 · 파일 %1$d개 + … + 클라우드 유지 + 로컬 유지 + 클라우드 확인 중… + 마지막 동기화 %1$s + 아직 동기화 안 됨 + 파일 %1$d개 + 설치됨 + 내 라이브러리 + 스토어 + 도전 과제 + %2$d개 중 %1$d개 달성 + %1$d / %2$d + 달성 · %1$d + 미달성 · %1$d + 숨겨짐 · %1$d + 숨겨진 도전 과제 + 달성하면 세부 정보가 표시됩니다. + 모든 도전 과제 달성 + 불러오는 중… + 도전 과제를 보려면 %1$s에 로그인하세요. + %1$s에 연결할 수 없습니다. 온라인 상태에서 다시 확인하세요. + %1$s에 이 게임의 도전 과제가 없습니다. + 모두 보기(%1$d) + 도전 과제 · %1$s + ✓ Steam에 있음 + 제거 + 제거 중… + %1$s을(를) 제거하시겠습니까? + %1$s의 공간을 확보하고 Steam에서 제거합니다. + 게임 파일과 Steam 바로 가기를 제거합니다. + 다음에 Steam을 시작하면 게임이 표시됩니다. Steam에서 실행하거나 지금 Steam을 시작하세요. + Proton + 게임별 Steam 선택 + 오디오 + DirectAudio + 구성 요소 + FEX, DXVK, VKD3D + 엔진 + %1$s · 네이티브 + %1$s · 내장 + 실행 방식 + 가져온 곳 + %1$s, 내 계정으로 + 설치 위치 + 게임 저장 공간(Games/%1$s/%2$s) + 실행 도구 + Steam 및 Proton, Steam 게임과 같은 방식 + Steam 및 Proton · 실행할 때마다 Epic 로그인, EOS 오버레이 끔 + Steam 및 Proton · Amazon Games Services 변수 설정됨 + Steam + 설치 시 Steam에 추가됨(Deck UI + Steam Input) + 다운로드 없음 + 일시 중지됨 + 대기 중 + 대기 중 · #%1$d + 취소됨 + 실패 + 실패 · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + 일시 중지 + 재개 + 취소 + 다시 시도 + 지우기 + 완료된 항목 지우기 + 매니페스트 + 다운로드 + 검증 + 설치 + 완료 + 확인 중 + 다운로드 중 + 다운로드를 삭제하시겠습니까? + 실패 · %1$s 유지됨 + 검증 중 + 설치 중 + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + 파일 %1$d/%2$d개 + 다운로드 관리자 + 동시 다운로드 수 + 설치 위치 + %1$s 설치: + 내부 저장 공간 + %1$s 사용 가능 + 스토어 다운로드 + 게임 다운로드 진행률을 표시하고 백그라운드에서 다운로드를 계속 진행합니다 + 다운로드 중 + 다운로드 %1$d개 + 준비 중… + 완료된 스토어 다운로드 + %1$s 설치됨 + %1$s 다운로드 실패 + 삭제 + 유지 + %1$s에 로그인 + 계정 제공업체로 로그인 + 새로 고침 + 닫기 + 다시 시도 + 로그인 완료 중… + 로그인 페이지를 불러올 수 없습니다: %1$s + 이 제공업체는 앱에 포함된 페이지에서 로그인을 허용하지 않습니다. 대신 스토어 비밀번호로 로그인하세요. + 로그인 페이지에서 오류를 반환했습니다(HTTP %1$d). + 로그인 페이지 연결이 안전하지 않습니다. + 로그인을 확인할 수 없습니다. 다시 시도하세요. + 로그인에 실패했습니다. 다시 시도하세요. 닫기 뒤로 편집 @@ -2088,21 +2218,122 @@ %1$d개는 이미 게임에 있음 - 도전 과제 - %2$d개 중 %1$d개 달성 - %1$d / %2$d - 달성 · %1$d - 미달성 · %1$d - 숨겨짐 · %1$d - 숨겨진 도전 과제 - 달성하면 세부 정보가 표시됩니다. - 모든 도전 과제 달성 - 불러오는 중… - 도전 과제를 보려면 %1$s에 로그인하세요. - %1$s에 연결할 수 없습니다. 온라인 상태에서 다시 확인하세요. - %1$s에 이 게임의 도전 과제가 없습니다. - 모두 보기(%1$d) - 도전 과제 · %1$s + B · %1$s(으)로 돌아가기 + 게임 + 플레이 설정 + 그래픽 드라이버 + 이 Proton + 모든 Proton + 모든 Proton에 적용 + 사용 가능 · %1$d + 확인 시각 %1$s + 새 구성 요소 및 드라이버 확인 + 받기 + %1$s · 아래의 DXVK, VKD3D-Proton 및 FEX는 이 Proton용입니다 + %1$s · 테스트됨 + %1$s · 테스트되지 않음 + %1$s · 지원되지 않음 + 권장 조합을 최신 상태로 유지합니다 + 자동 · %1$s + 런타임 및 디스플레이 드라이버 개별 설정 + %1$s을(를) 사용하시겠습니까? + %1$s을(를) 제거하시겠습니까? + 진단 + 플레이 준비 완료 + 아직 플레이할 준비가 되지 않음 + 런타임 %1$s + 프로세스 제한 꺼짐 + %1$s(으)로 로그인됨 + 확인이 필요한 항목 + 외관 + 이 기기 + 라이브러리 아트워크 + LINUX 앱 + Flathub + Linux 데스크톱용 앱으로, 데스크톱에서 엽니다. + Flathub 설정 + %1$s을(를) 제거하시겠습니까? + 설정과 파일은 유지됩니다. + 유지 + 디스크 사용량 + 데스크톱을 제거하시겠습니까? + 전체 + GOG 도전 과제 + 지원되는 GOG 게임의 도전 과제를 동기화합니다. 다음 실행부터 적용됩니다. + 도전 과제 꺼짐 + 도전 과제 켜짐 + GOG에 다시 로그인하세요 + 도전 과제 업로드됨 + 도전 과제 업로드 대기 중 + 스토어 설정에서 GOG 도전 과제를 활성화하세요 + 도전 과제를 사용할 수 없습니다. 세션 로그를 확인하세요 + 도전 과제 잠금 해제됨 + 붙여넣기 + 방향패드 + 왼쪽 스틱 + 오른쪽 스틱 + 메뉴 + 키보드 + 방향패드 위 + 방향패드 아래 + 방향패드 왼쪽 + 방향패드 오른쪽 + 터치 컨트롤 + 터치 컨트롤러 + 마우스 + 켜짐 + 꺼짐 + 느림 + 보통 + 빠름 + 레이아웃 편집 + DroidDeck 메뉴 + 화면으로 컨트롤을 끌어다 놓으세요 + 배치 + 취소 + 저장 중… + 아이콘 + 너− + 너+ + 높− + 높+ + 숨기기 + 색상 및 불투명도… + 이 레이아웃 복사 + 레이아웃이 복사되었습니다. 어떤 게임의 터치 레이아웃에도 붙여넣을 수 있습니다 + 복사한 레이아웃 붙여넣기 + Steam 기본값으로 재설정 + 이 컨트롤에는 아이콘을 지정할 Steam 바인딩이 없습니다 + %1$s 아이콘 + 터치 레이아웃 색상 + 완료 + 터치 레이아웃을 저장할 수 없습니다 + 터치 레이아웃이 Steam에 저장되었습니다 + 앱 %1$d + 라벨(선택 사항) + Steam 아이콘을 찾을 수 없습니다 + + Steam 아이콘 %1$d개 + + 아이콘 색상 + 버튼 색상 + 아이콘 없음 + + 로그인 확인 중… + 라이브러리를 가져오는 중… + 게임 목록을 가져오는 중… + 게임 정보를 불러오는 중… (%1$d/%2$d) + + 게임 %1$d개 동기화 중… + + 동기화 중… %1$d/%2$d + 커버 아트를 가져오는 중… + 동기화 실패 + %1$s 라이브러리에서 게임을 찾을 수 없습니다 + Amazon Games가 라이브러리 요청에 응답하지 않았습니다. 새로고침하거나 다시 로그인하세요 + GOG에 연결할 수 없습니다 + GOG 라이브러리를 읽을 수 없습니다 + Steam에 등록 중… 개발사 배급사 출시일 @@ -2111,4 +2342,34 @@ 완전 정복 %1$s시간 %1$d분 + Cardboard VR + Cardboard 모드 + 휴대폰 VR 뷰어용 왼쪽 및 오른쪽 눈 화면입니다. + 화면 + 가상 스크린 + 사이드 바이 사이드 3D + 뷰어 + Cardboard + Cardboard (2014) + 렌즈 없음 + 눈 사이 거리 + 스크린 너비 + 스크린 거리 + 머리 추적 + 이 기기에는 회전 센서가 없습니다. + 시점 재설정 + 재설정 + SteamVR (실험적) + 설치 + Valve의 ARM64용 SteamVR을 다운로드합니다(571MB). + SteamVR 다운로드 및 압축 해제 중… + 시작 + 머리를 헤드셋으로 삼아 SteamVR을 실행하며, 화면이 사이드 바이 사이드 3D로 전환됩니다. + 중지 + SteamVR이 실행 중입니다. + 이 세션에서는 사용할 수 없습니다. + SteamVR이 응답하지 않았습니다. 세션 로그를 확인하세요. + 구성 요소 다운로드 + 다운로드는 구성 요소 페이지와 공유됩니다. 위에서 다운로드한 버전을 선택하면 다음 게임 실행 시 사용됩니다. + 다운로드할 버전 선택 diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml new file mode 100644 index 000000000..d197aae8a --- /dev/null +++ b/app/src/main/res/values-ms/strings.xml @@ -0,0 +1,2210 @@ + + + DroidDeck + + Larian pertama memuat turun Steam. Ini mungkin mengambil beberapa minit. + Secara lalai, meninggalkan apl ini menjedakan Steam; ia menyambung semula dari tempat ia berhenti apabila anda kembali. + Pengawal anda akan muncul sebagai pad Xbox 360. Jika tidak, gunakan kawalan pada skrin. + + + Larian pertama menyediakan desktop. Ini mungkin mengambil beberapa minit. + Secara lalai, meninggalkan apl ini menjedakan sesi; ia menyambung semula dari tempat ia berhenti apabila anda kembali. + Sentuhan berfungsi sebagai pad sentuh: seret untuk menggerakkan penunjuk, ketik untuk klik. + Pengawal anda akan muncul sebagai pad Xbox 360. Jika tidak, gunakan kawalan pada skrin. + + Sesi + Dipaparkan semasa sesi Steam atau desktop sedang dibuka + Steam sedang berjalan + Muat turun Steam aktif + Sesi dijeda - ketik Sambung apabila sedia + Sambung + Gantung sekarang + Hentikan sesi + Penjanaan bingkai + Pilih mod penjanaan bingkai + Mati + Win-FG %1$d× + LSFG %1$d× + LSFG Adaptif %1$d + Memerlukan Lossless Scaling pada Steam + Pasang Lossless Scaling dalam Steam atau import Lossless.dll + Import Lossless.dll… + Menggunakan salinan daripada Steam + Menggunakan salinan yang diimport + Pilih Lossless.dll + Lossless.dll diimport + Lossless.dll ini sudah digunakan + Lossless Scaling tidak dimiliki pada akaun Steam ini + Fail ini bukan Lossless.dll milik Lossless Scaling + Tidak dapat menyediakan LSFG daripada Lossless.dll ini + LSFG memerlukan Lossless Scaling daripada Steam atau Lossless.dll yang diimport + LSFG tidak dapat menyediakan shadernya daripada Lossless.dll + %1$s: tidak disokong + %1$s: enjin gagal + %1$s: memerlukan Lossless Scaling + %1$s %2$s · bermula + Menambah pustaka Steam dan mengimbasnya untuk permainan Windows. Permainan yang dimiliki menggunakan entri Steam masing-masing; selebihnya menjadi pintasan bukan Steam. + Persekitaran permainan + Pembolehubah persekitaran untuk permainan Proton. + Edit + Semua permainan Proton + Profil + ID apl Steam + Buka + Permainan %1$s + Tambah pembolehubah + Nama pembolehubah + Nilai + Simpan + Alih keluar + Warisi semula + Kosongkan penggantian persekitaran + Nyahtetapkan untuk profil ini + Tiada pembolehubah ditetapkan. + Diwarisi + Pembolehubah tersuai + Tidak dapat memuatkan atau menyimpan. Semak storan kosong dan cuba lagi. + Gunakan nama yang sah dan nilai sehingga 8192 aksara. + Runtime Proton standard tidak membaca pembolehubah ini. + Memuatkan… + Penggantian tahap ciri D3D12. 12_2 melaporkan keupayaan DirectX 12 Ultimate; ciri yang tidak disokong boleh gagal. Biarkan tidak ditetapkan untuk pengesanan automatik. + Penggantian model shader D3D12. 6_9 memerlukan VKD3D yang cukup terkini dan ciri GPU yang sepadan. Biarkan tidak ditetapkan untuk pengesanan automatik. + Kekalkan caching shader Mesa dihidupkan untuk mengurangkan penyusunan berulang. false menghidupkan caching. + Saiz maksimum cache shader Mesa, seperti 1G. Cache yang lebih besar menggunakan lebih banyak storan. + Pilihan VKD3D untuk D3D12. nodxr melumpuhkan ray tracing; gunakan hanya untuk permainan yang memerlukannya. + Tetapkan kepada 1 untuk log diagnostik Proton. Pengelogan boleh mengurangkan prestasi dan menggunakan storan. + Tetapkan kepada 1 untuk menggunakan WineD3D berasaskan OpenGL dan bukan DXVK, untuk pengujian keserasian D3D9–11. + Tetapkan kepada 1 untuk mengekalkan permainan ini di luar ntsync (droiddeck-ntsync atau pemacu kernel); ia menggunakan droiddeck-esync jika pek wujud, jika tidak droiddeck-fsync. + Tetapkan kepada 1 untuk mematikan droiddeck-esync bagi permainan ini; ia menggunakan droiddeck-fsync sebagai ganti, atau droiddeck-ntsync jika fsync juga dimatikan. + Tetapkan kepada 1 untuk mengekalkan permainan ini di luar droiddeck-fsync; ia menggunakan droiddeck-esync jika pek wujud, jika tidak droiddeck-ntsync. + Tetapkan kepada 1 untuk menjalankan permainan ini pada droiddeck-fsync, atau 0 untuk meletakkan droiddeck-esync dahulu jika pek sepadan dengan Protonnya. + Tetapkan kepada 0 untuk melumpuhkan pembantu navigasi pad permainan Proton bagi permainan ini. + Overlay DXVK untuk D3D9–11. fps memaparkan kadar bingkai; nyahtetapkan untuk menyembunyikannya. + Tetapan DXVK diasingkan dengan tanda koma bertitik. Binaan terkini menggunakan dxvk.maxFrameRate untuk pengehadan bingkai; lebih baik gunakan pengehad bingkai sesi untuk permainan biasa. + Had kadar bingkai D3D12. 0 melumpuhkan had. + Threading OpenGL Mesa. true boleh membantu permainan OpenGL yang terhad oleh CPU; uji bagi setiap permainan. + Segerak Wine + Cara permainan menyelaraskan bebenangnya. Berkuat kuasa pada sesi seterusnya. + esync + ntsync + fsync + wineserver + Praset FEX + Berkuat kuasa pada pelancaran permainan seterusnya. + Permainan + Lalai FEX + Menggunakan lalai FEX. + Kestabilan + Tetapan paling selamat dan paling perlahan. + Keserasian + TSO penuh dengan multiblock. + Pertengahan + TSO seimbang dengan ketepatan x87 dikurangkan. + Prestasi + Semua TSO dilumpuhkan; multiblock. + Prestasi + TSO + Tetapan Prestasi dengan TSO utama dihidupkan. + Ekstrem + Melumpuhkan pemeriksaan kod mengubah-diri-sendiri. Permainan JIT dan DRM mungkin rosak. + Ekstrem + TSO + Tetapan Ekstrem dengan TSO utama dihidupkan. + Ekstrem-gn + Prestasi dengan petunjuk TSC dan metadata. Cuba sebelum Ekstrem. + Denuvo + Prestasi dengan pemeriksaan kod penuh dan bit hypervisor disembunyikan. + Nilai tersuai + Pilih nilai + Emulator & apl + Tambah + Apl atau permainan anda sendiri + Skrip + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Bermula… + %1$s ditambah + %1$s dialih keluar + Desktop + Apl itu sudah tiada. Pilih semula daripada Desktop. + Buka %1$s + Alih keluar + Tekan sekali lagi untuk alih keluar + Hentikan sesi untuk alih keluar + Menjalankan %1$s daripada folder asalnya, dipautkan ke dalam sesi di tempat ia berada. Mengalih keluar apl ini tidak mengubah folder tersebut. + Menjalankan %1$s daripada salinan foldernya dalam DroidDeck. Mengalih keluar apl ini memadam salinan tersebut, bukan yang asal. + Pasang runtime Linux dahulu + Fail itu sudah tiada + Ruang kosong tidak mencukupi: menyalin folder memerlukan %1$s + Menyalin %1$s + Menambah apl gagal + Mencari keluaran %1$s + Tidak dapat membaca keluaran %1$s + Tiada keluaran %1$s mempunyai AppImage ARM64 atau x86_64 + Memuat turun %1$s + Muat turun gagal + Mengemas kini apl gagal + Ikon itu tidak dapat dibaca + %1$s disimpan + Edit + Semak kemas kini + Menyemak kemas kini… + Kemas kini kepada %1$s + Terkini · %1$s + %1$s dikemas kini + %1$s · %2$s + Tambah apl + Skrip + AppImage + GitHub + Flatpak + Fail .sh. Ia berjalan daripada folder sendiri, skrin penuh di bawah gamescope. + AppImage ARM64 (aarch64), atau yang x86_64 yang berjalan melalui FEX. Ia dinyahpek ke dalam runtime Linux sekali sahaja. + Memasang AppImage ARM64 daripada keluaran terbaharu repositori, atau versi x86_64-nya melalui FEX. + Gunakan FEX + Auto + Hidup + Mati + FEX menjalankan program Linux x86 pada peranti ARM64 ini. Auto menggunakannya apabila program dibina untuk x86. + %1$s melalui FEX + Program ini dibina untuk PC %1$s dan berjalan melalui FEX, yang belum disediakan lagi. Buka Steam sekali semasa anda log masuk: DroidDeck akan memuat turun FEX dan Steam Linux Runtime yang diperlukan, kemudian ini akan dibuka. + Program ini + %1$s dibina untuk PC x86 dan berjalan melalui FEX, yang belum dipasang lagi. Mulakan Steam sekali semasa anda log masuk dan biarkan ia memuat turun FEX dan Steam Linux Runtime, kemudian mulakan ini semula. + ID apl Flathub atau halaman Flathubnya. Flatpak akan disediakan dahulu jika belum lagi. + Fail + Tiada fail dipilih + Pilih fail + Tukar + Pilih skrip + Pilih AppImage + Pilih ikon + Repositori + owner/repo atau pautan github.com + Bukan repositori GitHub + ID apl + org.ppsspp.PPSSPP + Bukan ID apl Flathub + Atau fail .flatpak, untuk apl yang tiada di Flathub. + Pilih fail .flatpak + Sumber + Pada Desktop anda + Apl baharu + Nama + Daripada apl + Ikon + Ikon lalai + Pilih ikon + Kosongkan + Daripada repositori + Mencari ikon… + Lalai + Edit %1$s + Simpan + Menyemak folder… + Foldernya dipautkan ke dalam sesi: %1$s + Foldernya menyimpan program Linux, yang tidak dapat dijalankan oleh storan kongsi, jadi ia disalin ke dalam DroidDeck (%1$s). + Tambah + Batal + Pasang runtime Linux daripada Persediaan dahulu. + Tunggu %1$s selesai. + Serasi + Binaan yang ditanda “%1$s” dalam menu Compatibility Steam mempunyai pembaikan prefix DroidDeck dan boleh memulakan permainan di sini. Lalai adalah Proton Experimental ARM64 (%1$s), atau Proton 11.0 ARM64 (%1$s) sebaik sahaja binaan terbaharu itu dipasang. Entri Proton asal Valve tanpa tanda ini tidak boleh memulakan permainan pada peranti ini. + Dipasang %1$s · %2$s + Batal + Auto + Tidak pernah + Sentiasa + Nyahpasang + Semak + Kemas kini + Menyemak… + Memproses… + Lupakan + Tambah… + Guna + Berkuat kuasa pada sesi seterusnya. + Sesi Steam + Sesi Desktop + Paparan + Resolusi + Sehingga 720p + Sehingga 900p + Sehingga 1080p + Asli + %1$s (lalai) + Tersuai · %1$d×%2$d + Tersuai… + 720p boleh menjadikan menu lebih responsif. + Nisbah skrin + Ditetapkan oleh resolusi tersuai. + Auto menggunakan sekurang-kurangnya 16:9. + Had bingkai + Mengehadkan keseluruhan sesi. Paparan beralih kepada kadar penyegaran yang boleh dibahagi sama rata dengan had ini. + HDR + Output HDR10 + Tidak tersedia: %1$s. + Mulakan semula apl untuk menggunakannya. + Pemacu + Pemacu GPU + Dikongsi oleh Steam dan Desktop. Ditetapkan pada halaman Komponen. + Sentuhan & kawalan + Sentuhan + Pad sentuh + Terus + Auto: pad sentuh pada Desktop, terus dalam Steam. + Kawalan pada skrin + Steam + QAM + Auto memaparkan kawalan apabila tiada pengawal disambungkan. + Sentuhan Steam (beta) + Kawalan sentuh Steam bermula pada sesi Steam seterusnya + Susun atur sentuh Steam + Gerakkan, ubah saiz atau sembunyikan kawalan sentuh permainan ini; disimpan ke Steam + Pengawal + Cara Steam mengesan pengawal anda. Berkuat kuasa pada sesi seterusnya. + Pengawal Steam Deck + Steam Controller + DualShock 4 + DualSense + Pengawal Xbox 360 + Deck: butang Quick Access dan giro. Steam Controller: sama, dengan grip sense pada butang belakang. DualShock 4 dan DualSense: giro, dan trackpad sebagai pad sentuhnya. Xbox 360: buka Quick Access dengan Guide+A. + Kembali + Menu → QAM + QAM → Menu + Sesi + Gelagat latar belakang + Apabila apl disembunyikan atau skrin dimatikan. + Manual + Asli + Auto menjeda dan menyambung semula apabila dipaparkan. Asli menyediakan Steam untuk tidur dahulu. Manual menunggu Sambung. Jangan sekali-kali terus berjalan. + Teruskan muat turun + Lambatkan penjedaan latar belakang semasa Steam aktif memuat turun. + Auto menjeda di latar belakang dan menyambung semula apabila dipaparkan. Manual menunggu Sambung. Jangan sekali-kali terus berjalan. + Permulaan + Mulakan Steam bersama DroidDeck + Membuka sesi Steam apabila DroidDeck dilancarkan. + Decky + Menyemak keluaran… + Sedia untuk memasang %1$s. + Tiada binaan serasi ditemui. Cuba lagi kemudian. + Dipasang · %1$s + Kemas kini tersedia · %1$s + Terkini · %1$s + Loader + Pasang yang terbaharu + Bermula bersama Steam. Semasa dihidupkan, apl lain pada peranti ini mungkin boleh mengawal Steam. + Mati. Port debug jauh Steam kekal tertutup. + Nyahpasang Decky Loader? + Mengalih keluar loader. Plugin dan tetapan anda dikekalkan. + Klien + Overlay prestasi + Matikan jika permainan ranap atau skrin hitam dalam mod Deck. Berkuat kuasa pada sesi seterusnya. + Baik pulih klien Steam + Memuat turun semula klien Steam pada permulaan Steam seterusnya. Gunakan jika Steam tersekat pada skrin pemuatannya. Permainan, log masuk dan tetapan dikekalkan. + Klien Steam akan dimuat turun semula pada permulaan Steam seterusnya. + Baik pulih + Karya seni + Kunci API SteamGridDB + Kunci anda + Edit… + Melengkapkan karya seni yang hilang daripada Steam, kemudian SteamGridDB. cover.jpg, hero.jpg, logo.png atau icon.png anda sendiri dalam folder permainan diutamakan. + Alih keluar %1$s + Alih keluar? + Tambah permainan + Tambah permainan: + Pilih exe: + Lokasi ini tidak tersedia dalam sesi + Semak exe ini + Komponen Windows + LB / RB bertukar antara bahagian. + Tiada + DLL Windows yang tidak disertakan dalam Proton, daripada senarai komponen. Hidupkan yang diperlukan oleh permainan ini: ia dimasukkan ke dalam folder Windows permainan itu sendiri, bukan failnya, dan kekal pada Proton mana sekalipun ia berjalan. + Tidak dapat mencapai senarai komponen. Komponen yang sudah dimuat turun masih boleh ditukar. + Disyorkan untuk permainan ini + Menyemak keperluan permainan ini… + Tersedia semasa dalam talian + + Menghidupkan %1$d komponen… + + Dihidupkan untuk permainan ini + Steam memasang %1$s dengan %2$s (apl %3$s) + Daripada senarai GOG untuk permainan ini + Daripada prasyarat Epic untuk permainan ini + Daripada langkah pemasangan Amazon untuk permainan ini + Steam memasang %1$s dengan apl %2$s, disepadankan mengikut nama + Turut disenaraikan + Disenaraikan oleh %1$s; bukan komponen di sini + Steam + GOG + Epic Games + Amazon Games + Permainan ini menyertakan %1$s + Permainan ini mempunyai %1$s (selalunya berfungsi seadanya) + Sudah ada dalam permainan ini + Tiada apa-apa dalam folder permainan yang merujuk kepada komponen. + Dipasang untuk permainan ini (%1$d) + Semua komponen (%1$d) + Memerlukan pemasang Windows (%1$d) + Ini adalah program pemasangan sendiri tanpa pakej dalaman untuk dibuka, dan belum direkodkan apa yang dipasangnya. Kebanyakannya runtime lama yang sudah digantikan oleh Mono dalam Proton; rekod ditambah apabila ia disediakan. + Tunjukkan + Sembunyikan + Menjalankan program pemasangan sendiri (belum direkodkan) + Belum disokong di sini + Memuatkan komponen… + Tidak dapat memasang: %1$s + Dimuat turun + Memuat turun %1$s + Memasang: %1$s + Mematikan… + Muat turun %1$s + Berkuat kuasa pada permulaan permainan seterusnya. + Regangkan permainan 16:9 + Pada skrin yang lebih segi empat daripada 16:9, meregangkan permainan untuk memenuhi skrin dan bukan menunjukkan jalur. Berkuat kuasa pada sesi seterusnya. + Audio + DirectAudio untuk permainan + Memintas PulseAudio untuk kependaman yang lebih rendah. + Mikrofon + Untuk sembang suara. + Rangkaian + Penemuan Wi-Fi + Tunjukkan nama Wi-Fi dan rangkaian berdekatan dalam Steam. + Perkhidmatan lokasi dimatikan. + Kebenaran lokasi tidak diberikan. + Buka tetapan + Teruskan + Aktifkan penemuan Wi-Fi? + Android memerlukan kebenaran Lokasi untuk membaca nama Wi-Fi dan menemui rangkaian berdekatan. DroidDeck menggunakan akses ini untuk menunjukkan rangkaian tersebut dalam Steam. Kami tidak menjejaki lokasi anda. Akses internet berfungsi tanpa kebenaran ini. + Storan permainan + Automatik - kad SD jika ada, serta folder Games anda + Automatik - folder Games anda, serta kad SD jika ada (tiada sekarang) + Dalaman sahaja + Folder: %1$s + Pustaka kedua + Permainan yang menstrim aset daripada SD atau storan kongsi mungkin tersekat-sekat. + Diagnostik storan + Rekod keputusan peruntukan dan masa storan yang disampel dalam Kongsi log. Boleh melambatkan pemasangan semasa dihidupkan. Berkuat kuasa pada sesi seterusnya. + Pilih folder… + Resolusi tersuai + Menggantikan had dan nisbah. Saiz yang tidak sepadan dengan skrin akan mendapat jalur. + Lebar + Tinggi + Antara 320×240 dan 3840×2160. + + %1$d permainan + + OK + Padam + Pulihkan + Muat turun + Padam %1$s + %1$.1f MB + Pemacu runtime + Digunakan oleh Steam, permainan dan desktop. Berkuat kuasa pada sesi seterusnya. + Import zip Turnip… + Pemacu paparan + Melukis bingkai ke skrin dalam kedua-dua mod. Mulakan semula apl untuk menggunakannya. + Import zip AdrenoTools… + tidak pernah + Nightlies disemak %1$s + Komponen + Perihal Komponen + Semak pemacu baharu + Semak Nightlies untuk pakej baharu + Proton + Proton yang dipilih di sini menjalankan setiap permainan yang belum memilih Protonnya sendiri dalam Steam, dan perubahan yang dibuat dalam Steam turut dipaparkan di sini. Steam menggantikan Proton Valve apabila ia dikemas kini; versi asal dikekalkan bagi setiap binaan. + Konfigurasi + Konfigurasikan profil + Profil + Profil lalai + Digunakan secara automatik oleh permainan tanpa penggantian. + Tetapan tersimpan yang digunakan secara automatik apabila %1$s dilancarkan. + Pilih hanya tetapan yang diperlukan permainan ini. Selebihnya terus menggunakan profil Lalai. + Praset FEX + Menggunakan praset profil Lalai. + Praset tersuai disimpan untuk permainan ini. + Pembolehubah FEX tersuai + Guna Lalai + Keserasian permainan + Pilihan persekitaran Proton, DXVK, VKD3D dan penyegerakan untuk permainan ini. + Menggunakan Proton profil Lalai. + Proton tersuai disimpan untuk permainan ini melalui pemetaan keserasian Steam. + Tiada Proton + DXVK + VKD3D-Proton + Menggunakan pakej komponen profil Lalai daripada %1$s melainkan permainan ini memilih pakej lain. + Menggunakan pakej komponen profil Lalai. + Menggunakan %1$s hanya untuk permainan ini. + Pilihan runtime + Pilihan persekitaran Proton, DXVK, VKD3D-Proton dan penyegerakan lain bagi setiap permainan. + Profil %1$s disimpan untuk pelancaran permainan seterusnya. + Mulakan tanpa Steam + Untuk permainan yang terus tertutup: memulakannya tanpa persediaan Steam oleh Proton. Ciri Steam seperti overlay dimatikan untuknya. + Hidup + Pilihan Proton %1$s disimpan dan tersedia dalam Steam sekarang. + Pilihan Proton %1$s disimpan. Mulakan semula Steam sebelum melancarkan permainan. + %1$s tidak boleh menggunakan Proton yang dipilih pada peranti ini. + Tidak dapat memuatkan profil %1$s: %2$s + Profil permainan tidak dapat disimpan: %1$s + Tetapkan semula profil permainan + Tetapkan semula profil %1$s? + Alih keluar penggantian Proton, FEX, pakej komponen, persekitaran, penapisan tekstur dan komponen Windows permainan ini? Ia akan menggunakan profil Lalai. + %1$s kini menggunakan profil Lalai. + Profil permainan tidak dapat ditetapkan semula sepenuhnya: %1$s + %1$s kini menjalankan setiap permainan yang belum memilih Protonnya sendiri dalam Steam. + %1$s akan menjalankan setiap permainan yang belum memilih Protonnya sendiri, mulai permulaan Steam seterusnya. + %1$s tidak boleh memulakan permainan di sini, jadi ia tidak boleh menjadi lalai. + Proton lalai tidak dapat disimpan: %1$s + %1$s · %2$d ditukar + %1$s · semua asal + %1$s · permainan sedang berjalan + %1$s · pek droiddeck-esync + %1$s · belum ada pek droiddeck-esync + %1$s · esync terbina dalam, tiada pek diperlukan + Pemacu GPU + %1$s · digunakan semula pada pelancaran %2$s + Seterusnya: %1$s · selepas permainan ditutup + %1$s digunakan: %2$s + Menunggu sehingga permainan ditutup + Berkuat kuasa pada permulaan permainan seterusnya + Import .zip + Membaca Proton… + Tiada Proton dipasang lagi. Mulakan Steam sekali untuk memuat turun Proton ARM64-nya, atau tambah GE-Proton, CachyOS atau DroidDeck-Proton daripada Persediaan. + Asal · %1$s + %1$s · binaan ini + %1$s · binaan terdahulu + Dipasang · %1$d + Belum ada apa-apa disimpan untuk %1$s. Fail asal Proton ini disimpan sebagai yang asalnya. + Permainan sedang berjalan pada Proton ini, jadi ini menunggu sehingga ia ditutup. + Berkuat kuasa pada permulaan permainan seterusnya. + Tukar %1$s? + Masukkan %1$s ke dalam %2$s? Fail asalnya kekal disimpan. %3$s + Pulihkan %1$s? + Masukkan semula %1$s asal ke dalam %2$s? %3$s + Mengalih keluar pakej daripada apl. Anda boleh memuat turunnya semula. + Padam yang asal ini? + %1$s asal ini daripada binaan terdahulu %2$s. Yang asal bagi binaan semasa sentiasa dikekalkan. + Pada Nightlies · %1$d + Segarkan semula untuk menyenaraikan pakej pada Nightlies. + Tiada apa-apa yang baharu sejak semakan terakhir. + Import .wcp + Pulihkan yang asal bagi Proton ini + Masukkan semula %1$s asal milik %2$s? + Tukar + FEX, DXVK dan VKD3D-Proton untuk setiap Proton yang menjalankan permainan dalam Steam.\n\nPilih baris yang dipasang untuk menukarnya. Ia berkuat kuasa pada permulaan permainan seterusnya, atau selepas permainan yang sedang berjalan ditutup.\n\nPilihan anda disemak semula sebelum setiap pelancaran dan dikembalikan jika ada yang mengubahnya. Fail asal setiap Proton dikekalkan, jadi kemas kini Steam tidak akan menghilangkannya. + Asal + Digunakan + Baharu + Disimpan + Prestasi + Teras CPU, pembaikan sesi dan nama hos. Berkuat kuasa pada sesi seterusnya. + Teras klien Steam + Gantikan pilihan teras Steam + Memasak Steam, pembantu UI-nya dan gamescope kepada teras di bawah. + Teras klien + Teras untuk Steam. + Hidupkan penggantian untuk memilih. + Teras permainan + Teras permainan + Teras untuk permainan. Dengan semua dipilih, Android menjadualkan secara bebas. + Antara muka klien + GL berbenang + Boleh menjadikan menu Steam lebih responsif. + Zink: deskriptor malas + Disyorkan untuk pemacu tanpa penampan deskriptor. + Langkau semakan ralat GL + Mematikan pengesahan GL bagi setiap panggilan. + gamescope: baris gilir GPU masa nyata + Mengutamakan kerja GPU compositor. Boleh melicinkan rentak bingkai dengan sedikit kos kepada permainan. + GPU + Kekalkan GPU pada kelajuan tertinggi + Adreno sahaja. Kurang tersekat-sekat, lebih penggunaan bateri dan haba. + Pembetulan sesi + Turnip: rendering sysmem + Lebih pantas untuk Steam dan kebanyakan permainan. Diperlukan pada Adreno 710/720/722. + Langkau pembantu xalia Steam + droiddeck-ntsync (eksperimen) + Pengganti ruang pengguna untuk pemacu ntsync Linux, untuk pengujian. Semasa ia hidup, Proton menggunakannya sebagai ganti droiddeck-esync dan droiddeck-fsync. Berfungsi dengan Proton Experimental ARM64, GE-Proton dan proton-cachyos. Berkuat kuasa pada sesi seterusnya. + droiddeck-esync + droiddeck-fsync dahulu + Menjalankan fsync Proton sendiri pada fail Proton standard, dengan futex_waitv dibekalkan dalam ruang pengguna, jadi ia tidak memerlukan pek dan terus berfungsi sebaik sahaja Steam mengemas kini Proton. Semasa ia hidup, permainan menggunakannya mendahului droiddeck-esync, kecuali beberapa Proton yang mematikan fsync. Berkuat kuasa pada sesi seterusnya. + Thread Windows permainan menunggu satu sama lain melalui eventfd dan memori kongsi daripada rangkaian pergi balik melalui wineserver. Menggunakan pek droiddeck-esync yang dibuat untuk binaan Proton itu dengan tepat; pek diambil secara automatik. Sehingga satu wujud untuk binaan Proton baharu, droiddeck-fsync menggantikannya. Berkuat kuasa pada sesi seterusnya. + Melumpuhkan pembantu navigasi pad Proton, yang memakan CPU dalam setiap permainan. + Jalankan proot tanpa seccomp + Mungkin membetulkan ralat syscall yang hilang pada sesetengah kernel. Boleh mengurangkan prestasi. + Laluan pantas proot + Memerlukan penapis seccomp proot. + Carian fail lebih pantas dan kurang tersekat-sekat. Matikan jika sesuatu program tidak dapat mencari failnya. + Identiti sesi + Nama hos + Nama yang dilihat Steam untuk peranti ini. Kosongkan untuk memulihkan %1$s. + Huruf, digit dan tanda sempang dalaman sahaja, sehingga 63 aksara. + Android ditetapkan untuk menamatkan sesi ini + Dipasang + Tersedia untuk dimuat turun + Tiada yang baharu sejak semakan terakhir. + Pulihkan pemacu terbina dalam + Terbina dalam apl: ini mengalih keluar fail nyahpaknya dan menyembunyikannya. Pulihkan pemacu terbina dalam akan mengembalikannya. + Separuh daripada gabungan Android + Linux: kedua-dua separuh dialih keluar. Pemacu yang digunakan beralih kepada lalai. + Failnya dialih keluar daripada apl. + Failnya dialih keluar. Ia sedang digunakan, jadi lalai mengambil alih. + Paparan + Kesan + Kawalan + Komponen + Pengawal + Tetapan + Halaman %1$s + Steam + Desktop + Menu Steam + Akses pantas + Buka Menu Akses Pantas + Halaman sebelumnya + Halaman seterusnya + HUD prestasi + Mod penskalaan + Ketajaman penskalaan + Kesan skrin + Langsung. Memakan sedikit masa GPU semasa mana-mana kesan dihidupkan. + Prapasang + Tersuai + Lanjutan + Dikembangkan + Dikuncupkan + Ketajaman (CAS) + Tahap ketajaman + HDR palsu + Pengurangan jalur + Kekuatan pengurangan jalur + Kecerahan + Kontras + Gamma + Ketepuan + FXAA (pelicinan tepi) + Garis luar kartun + CRT + NTSC + Penapisan tekstur + Permainan DirectX 9–11, bermula daripada pelancaran seterusnya. + Penapisan anisotropik + Ketajaman tekstur + Pincang mip negatif: tekstur lebih tajam, sedikit kilauan. Auto sepadan dengan sejauh mana sesi dibesarkan ke skrin. + Auto (%1$s) + Papan kekunci + Papan kekunci PC + Android + Skrin kedua + Mod + Memaparkan + Apl Android + Lancarkan apl + Sembunyi + Papar + Tiada apl boleh dilancarkan + Kelakuan sesi + Berkuat kuasa sekarang dan untuk sesi akan datang dalam mod ini. + Sesi seterusnya + Sokongan + Log sesi + Lampirkan pada laporan pepijat + Kongsi log + Latar belakang + Hentikan + Hentikan sesi? + Sedang berjalan + Tiada permainan berjalan pada mana-mana Proton + Profil permainan + Butiran profil belum tersedia. + Tidak dapat memuatkan profil permainan. Mencuba semula secara automatik. + Tidak dapat menyegarkan profil permainan. Memaparkan data terakhir yang tersedia; mencuba semula secara automatik. + Lihat dan sunting tetapan tersimpan yang digunakan pada pelancaran seterusnya. + Menggunakan profil Lalai + Profil tersuai + Menggunakan %1$s daripada profil Lalai. + Perubahan disimpan ke profil %1$s dan berkuat kuasa pada kali seterusnya permainan dilancarkan. + + %1$d tindanan permainan + + Belum ada Proton dipasang. + permainan sedang berjalan + digunakan semula pada pelancaran %1$s + Asal + Import dan padam pada halaman Komponen. + Selesai + Perlu perhatian + Sedang berjalan + Semak alamat IP dan port Debugging Wayarles, atau gandingkan semula jika Android mengalih keluar peranti ini. + Pasang + Urus + Gambaran keseluruhan + Pengawal + Sesi + Pelancar + Persediaan + ● Semua sedia + %1$d daripada %2$d sedia + Semakan sistem + Peranti disokong + GPU belum diuji + GPU tidak disokong + %1$s · %2$s + %1$s: %2$s + Steam memerlukan pemacu Adreno; %1$s mungkin memaparkan skrin hitam + Masa jalan Linux + %1$s · %2$d%% + Belum dipasang · kira-kira 3 GB, dipasang pada Main pertama + Dipasang + %1$s · kemas kini tersedia + %1$s · terkini + Had proses anak + Hidup · Android mungkin menutup Steam. Mematikannya mengambil masa seminit. + Tidak ditetapkan · Android mungkin menutup Steam. Mematikannya mengambil masa seminit. + Tidak dapat disemak · mematikannya mengambil masa seminit. + Betulkan + Butiran + Pilihan pembangun + Gunakan Debugging Wayarles + Semak semula + Hidupkan had + Akaun Steam + Dilog masuk sebagai %1$s · mod luar talian + Dilog masuk sebagai %1$s + Tekan Main dan log masuk ke Steam + Alatan + Pengawal + Tetapan pengawal tidak tersedia. + Sesi + Log sesi + Disimpan selepas setiap sesi + Alatan penyahpepijatan + Log sesi terkini + Log sesi tersimpan + Menyimpan %1$d yang terbaharu + Kosongkan log + Mod luar talian + Log masuk ke Steam dahulu + Pelancar + Bahasa + Steam juga menggunakannya, dari kali seterusnya ia dimulakan. + Lalai sistem (%1$s) + Lalai sistem + Skala apl + Teks dan kawalan DroidDeck + %1$d%% (Lalai) + Tema + Rupa pelancar + Gunakan sebagai skrin Utama + DroidDeck boleh menjadi apl Utama + DroidDeck tidak sekali-kali ditawarkan sebagai apl Utama + Orientasi + Lanskap automatik + Lanskap + Lanskap songsang + Skrin penuh + Menyembunyikan bar status dan navigasi Android + Memaparkan bar status dan navigasi Android + Apl Utama lalai + Pilih apl Utama + Pilih + Apl Linux (beta) + Fail + Semak imbas dan urus fail + Versi Proton + Pasang binaan Proton ARM64 + Prestasi + Penetapan teras CPU + Folder ROM + Tempat permainan emulator disimpan + Komponen + FEX, DXVK, VKD3D + Kawalan + Pemetaan butang + Terokai + Cari + Dipasang + %1$s · %2$d + Kedai + Flathub · ARM64 + ● Flatpak sedia + Pasang masa jalan Linux daripada Persediaan dahulu. + Sediakan Flatpak + Menambah Flatpak dan menyambungkannya ke Flathub (kira-kira 20 MB). Apl pertama juga memuat turun masa jalannya. + Menyediakan… + Flatpak + Kemas kini + %1$s · %2$s + %1$s · %2$s · %3$d%% + Memulakan… + Kategori + Tidak dapat menghubungi Flathub. Semak sambungan anda dan cuba lagi. + Cuba lagi + Cari di Flathub + Firefox, permainan, emulator… + Cari + Semua + Mencari… + Cari mengikut nama atau pilih kategori. Hanya apl ARM64 disenaraikan. + Tiada apl ARM64 di Flathub sepadan dengan itu. + Hasil + Semak kemas kini + Kemas kini semua (%1$d) + ● Semuanya terkini + Tiada apl lagi. Cari di Terokai atau Cari. + Sediakan Flatpak untuk memasang apl. + Kemas kini tersedia · %1$s + Buka + Butiran + Alih keluar + Tekan sekali lagi untuk alih keluar + Memasang… + Pasang + Sediakan Flatpak dahulu + Muat turun %1$s + ● Dipasang + Tidak dapat memuatkan halaman Flathub apl ini. + Memuatkan daripada Flathub… + Tiada binaan ARM64 di Flathub, jadi ia tidak boleh dipasang di sini. + Tangkapan skrin + Perihal + ID Apl + Lesen + Masa jalan + Saiz dipasang + %1$s (tambah masa jalan kongsinya) + Laman web + Disahkan + Kemas kini + DroidDeck %1$s · %2$s + Disemak %1$s + Belum disemak + Benarkan DroidDeck mengemas kini sendiri + Pada skrin seterusnya, hidupkan Benarkan daripada sumber ini, kemudian kembali semula. Kemas kini akan diteruskan secara automatik. + Buka tetapan + Bukan sekarang + Ditandatangani berbeza + Tidak boleh dikemas kini di tempat + Salinan ini tidak ditandatangani dengan kunci keluaran DroidDeck, jadi kemas kini tidak boleh dipasang menggantikannya. Nyahpasang untuk bertukar. + Kemas kini tersedia + Tidak boleh dipasang + Sedia untuk bertukar + Mendahului Stable + Anda mendahului Stable + Binaan ini lebih baharu daripada Stable terkini. Anda akan beralih ke Stable pada keluaran seterusnya. + Ujian tamat + Ujian ini telah tamat + Pembetulannya telah digabungkan atau dibuang. Ikuti Preview untuk pembetulan terbaharu. + Terkini + Anda mempunyai %1$s yang terbaharu + · %1$s + %1$s… %2$d%% + %1$s… + Kemas kini%1$s + Pasang%1$s + Pasang Stable %1$s juga + Ikuti Preview + Hentikan sesi yang berjalan untuk mengemas kini. + Kurang + Lagi + DroidDeck %1$s + Binaan Preview baharu + Binaan ujian PR #%1$d + %1$d MB + SALURAN KEMAS KINI + Stable + Keluaran yang telah diuji, untuk kebanyakan orang + %1$s · %2$s + Preview + Pembetulan terbaharu, sebelum Stable + Terkini + Binaan ujian + Cuba pembetulan sebelum dikeluarkan + Tiada pada masa ini + PR #%1$d + Tidak dibina untuk salinan DroidDeck ini + Dikemas kini %1$s + ujian PR #%1$d + beberapa ketika dahulu + sebentar tadi + + %1$d untuk dicuba + + + Perubahan sejak binaan anda · %1$d binaan ketinggalan + + + Perubahan terkini · %1$d binaan + + Satu tetapan Android untuk ditukar + Steam + Android menghentikan apl yang memulakan banyak proses latar belakang, dan Steam memulakan berpuluh-puluh. Sehingga ini berubah, permainan tertutup tanpa ralat. + Betulkan untuk saya + Hidupkan “Lumpuhkan sekatan proses anak”, kemudian kembali semula. Biarkan Pilihan pembangun hidup, atau suis itu akan mati semula. + Buka Pilihan pembangun + Versi Android ini tidak mempunyai suis untuknya, jadi DroidDeck menukarnya melalui Debugging Wayarles. Mengambil masa kira-kira seminit, tanpa komputer diperlukan. + Debugging Wayarles memerlukan Wi-Fi. Mana-mana rangkaian berfungsi; ia tidak memerlukan internet. + Kod gandingan dihantar dalam pemberitahuan. Benarkan pemberitahuan untuk DroidDeck, atau gunakan Cara lain. + Benarkan pemberitahuan + Buka Tetapan → Pilihan pembangun → Debugging Wayarles dan hidupkannya + Ketik “Gandingkan peranti dengan kod gandingan” + Taip kod itu ke dalam pemberitahuan DroidDeck + Menunggu tetingkap timbul gandingan… + Tetingkap timbul dijumpai. Masukkan kodnya dalam pemberitahuan. + Status + Menyemak setiap 2 saat. Halaman ini ditutup apabila ia mati. + Cara lain + Debugging Wayarles, alamat manual, ADB komputer + Alamat manual, ADB komputer, Pilihan pembangun + Gunakan Debugging Wayarles sebaliknya + Sesetengah ROM turut menambah suis itu. Cari “proses anak” dalam Pilihan pembangun. + Jika pemberitahuan tidak berfungsi, masukkan alamat dan kod tetingkap timbul itu sendiri. + Masukkan alamat secara manual + Daripada komputer dengan ADB: + Salin arahan + Pilih paparan mana untuk membuka Tetapan Android. + Skrin utama + Digandingkan. Mencari port Debugging Wayarles… + Digandingkan. Masukkan port daripada Tetapan. + Peranti dijumpai. Menggunakan tetapan… + Tidak dapat digunakan secara automatik. Semak alamat di bawah dan cuba lagi. %1$s + Debugging Wayarles + Had proses anak dikemas kini + Tukar had proses anak + Jika pilihan itu tiada, gandingkan melalui Debugging Wayarles. + Digandingkan. Semak alamat IP dan port dalam Debugging Wayarles. + Had proses anak telah dikemas kini. + Gunakan langkah 2 jika Pilihan pembangun tidak mempunyai tetapan proses anak. Buka Pilihan pembangun → Debugging Wayarles → Gandingkan peranti dengan kod gandingan. + Digandingkan. Gunakan alamat di bawah, atau kemas kini daripada Debugging Wayarles → Alamat IP & Port. + Had proses anak telah dikemas kini melalui Debugging Wayarles. + Android mengesahkan had itu hidup. + Android mengesahkan had itu mati. + 2 · Gandingkan Debugging Wayarles + Alamat IP & port tetingkap timbul gandingan + Kod gandingan (PIN) + 6 digit + Gandingkan + 2 · Gunakan melalui Debugging Wayarles + Alamat IP & port sambungan + Gandingkan semula + Matikan had + Cara lain untuk menukarnya + 1 · Pilihan pembangun (cuba dahulu) + Tetapkan “Sekat proses anak” hidup. + Tetapkan “Sekat proses anak” mati. + 3 · ADB komputer (pilihan akhir) + Hanya jika Debugging Wayarles tidak tersedia. + 3 · ADB komputer (pilihan akhir) + Bila kawalan sentuh muncul dalam sesi + Warna + Rona butang pada skrin + Tersuai + Kelegapan + %1$d%% + Saiz butang + 100% ialah saiz standard + Klik stik + Ketik dua kali dan tahan stik untuk L3 atau R3 + Stik adaptif + Getaran permainan + Butang Steam + Butang QAM + Butang papan kekunci + Tekan Select untuk membuka menu Steam + Sebagai ganti View/Select. + Animasi + Stik muncul di tempat anda sentuh dan tersembunyi apabila dilepaskan + Susun atur + Kedudukan tersuai disimpan + Disesuaikan dengan skrin ini + Sunting + Tetapkan semula + Pemetaan butang + Setiap butang menghantar inputnya sendiri + %1$d daripada %2$d butang dipetakan semula + Konfigurasikan + Tetapkan semula pengawal + Memulihkan semua tetapan pengawal dan susun atur + Apa yang setiap butang pada skrin hantar kepada permainan. Tersembunyi mengalih keluar butang itu. + Butang pada skrin + Lalai + Tetapkan semula pemetaan + Apl Android + %1$d apl + Tiada apl Android boleh dilancarkan dijumpai. + ● Desktop dipasang + Dipasang pada pembukaan pertama + Tersedia untuk dipasang + Belum dipasang. + %1$s · belum dipasang + Buka %1$s + Permainan + Pilih folder ROM. + Semak imbas ke /root/ROMs dalam RetroArch. + Tambah permainan %1$s ke ROMs/%2$s. + Pasang %1$s + Hentikan sesi untuk memasang + Masa jalan diperlukan + Memuatkan butiran pemasangan… + Butiran pemasangan tidak tersedia. Cuba lagi kemudian. + Pasang masa jalan Linux daripada Persediaan dahulu. + Hentikan sesi yang berjalan untuk memasang apl desktop. + Memasang %1$s ke dalam desktop Linux. + %1$s · %2$d%% + Menyembunyikannya mengalih keluarkannya daripada Desktop. Failnya kekal dalam masa jalan Linux. + Permainan itu tidak lagi berada dalam folder ROM. + Desktop · %1$s + Lancarkan dalam %1$s + Juga dalam %1$s + Halaman itu telah hilang. Pilih bahagian di sebelah kiri. + Buka desktop + Pasang & buka desktop + Nyahpasang desktop + Alih keluar desktop dan apl terbundlenya. Kekalkan permainan, apl ditambah, tetapan dan pustaka kongsi. + Mengalih keluar desktop + Desktop dialih keluar + Mengalih keluar fail LXQt lama + Kemas kini DroidDeck sebelum mengalih keluar versi desktop ini. + Desktop Linux + KDE Plasma, Firefox dan emulator anda + Permainan yang anda pasang dalam Steam muncul di sini selepas sesi. + Main Steam + Tetapan pelancaran + Lancarkan + Fail permainan + Prefix Proton + Mencari simpanan… + Tiada folder simpanan dijumpai mengikut nama. Eksport mengambil seluruh folder pengguna. + %1$d folder simpanan + %1$s · %2$d fail · %3$s + %1$d KB + Urus simpanan + Simpanan permainan + Import simpanan… + Eksport sebagai zip GameHub… + Eksport sebagai zip Winlator… + Buka folder simpanan + Belum pernah dimainkan + Masa jalan sibuk · %1$d%% + Masa jalan sibuk + Masa jalan dipasang pada Main pertama + Kemas kini masa jalan tersedia + ● Masa jalan sedia + Terakhir dimainkan %1$s + Storan dalaman + Permainan ditambah + Permainan tersuai + Storan permainan + Pasangan terbaik untuk GPU ini, dikemas kini sentiasa + Pilih pasangan sendiri + Menyemak pemacu… + Belum ada pemacu disenaraikan. Segarkan untuk mencarinya. + Sembunyikan pemacu untuk GPU lain + Papar pemacu untuk GPU lain + Lanjutan + Ditetapkan bersama pemacu paparan: %1$s membawa kedua-duanya. + Ditetapkan bersama pemacu masa jalan: %1$s membawa kedua-duanya. + Digunakan oleh permainan dan desktop. + Lalai masa jalan + Melukis bingkai ke skrin. Berkuat kuasa selepas DroidDeck dimulakan semula. + Auto - dipilih mengikut GPU + Muat turun, import atau padam… + SEDANG DIGUNAKAN + Pemacu terbina dalam DroidDeck + Pemacu masa jalan dan paparan ditetapkan bersama sebagai pasangan sepadan. + Disyorkan untuk GPU ini: %1$s. + Segarkan untuk mencari pemacu bagi GPU ini. + DISYORKAN + Tidak tersedia + Masa jalan Linux + Menyediakan masa jalan Linux + Sesi + Memulakan paparan dan audio + Klien Steam + Menyemak Steam dan alatan keserasiannya + Membuka Steam + Menunggu bingkai pertama Steam + Desktop + Menyediakan desktop + Membuka + Menunggu bingkai pertama + Memulakan Steam + Sesi telah tamat + Langkah %1$d daripada %2$d + Steam dijeda + Pemegang + Pad kiri + Pad kanan + Kedua-dua + Gamepad + Stik + Trackpad + Tukar antara stik dan trackpad + Lagi kawalan Deck + PANEL + Tutup kawalan skrin kedua + Buka menu Steam + Kiri · atas + Kiri · bawah + Kanan · atas + Kanan · bawah + Klik + Trackpad kanan + Trackpad kiri + Trackpad + Papar papan kekunci + Sembunyikan papan kekunci + Terminal Linux + Sesi Linux belum sedia untuk terminal. + Steam + Permainan + Desktop + Kedai + Komponen + Apl + Persediaan + Kemas kini + %1$d%% + Sambung %1$s + Muncul sebagai /root/ROMs dalam setiap sesi. Storan dalaman berada di /root/Storage. + Tiada folder dipilih + Berkuat kuasa pada sesi seterusnya. Kad SD berfungsi. + Pilih folder + Tukar folder + Pasang binaan Proton ARM64, kemudian pilihnya mengikut permainan dalam Steam → Properties → Compatibility. + Hentikan sesi yang berjalan untuk memasang atau mengalih keluar alat keserasian. + Binaan tersedia + %1$s dipasang + Tertangguh daripada permintaan terdahulu + Belum dipasang + Kemas kini tersedia → %1$s + Alih keluar versi %1$s lama + Mengalih keluar %1$s. Permainan yang ditetapkan ke versi ini perlu ditukar ke Proton lain. + Permainan yang ditetapkan ke versi ini perlu ditukar ke Proton lain. + Pasang sekarang + Batalkan yang tertangguh + Papan kekunci Android + Sembunyi ✕ + Lancarkan %1$s + Pilih skrin. Kedua: %1$s. + Paparan kedua tidak lagi tersedia. + Skrin utama + Skrin kedua + Tab sebelumnya + Tab seterusnya + Hidup + Semua %1$d + Tiada + %1$d daripada %2$d + Mati + Skrin bawah + Berkuat kuasa pada pelancaran permainan seterusnya. Profil permainan mengatasi profil kongsi. + Permainan lain mengikut ID apl… + Selesai + Lalai + Mati + Tukar… + Tukar pemboleh ubah + Kembali ke lalai + Permainan Proton + Gambar dalam gambar + Masuk ke PiP secara automatik + Kawalan pada skrin terbuka + Terbuka, permainan terletak di atas trackpad dan pemegang belakang Steam Deck. Dilipat, permainan menjadi skrin penuh. + Digantung + Gambar dalam gambar tersedia sebaik sahaja sesi sedia. + Benarkan gambar dalam gambar untuk DroidDeck dalam tetapan Android. + Android tidak dapat membuka gambar dalam gambar. + Pintasan + Tambah ke skrin utama + Pilih permainan + Mengimbas permainan dipasang… + Salin pautan pelancaran + Pautan pelancaran disalin + Pelancar ini tidak dapat menambah pintasan. Gunakan Salin pautan pelancaran sebaliknya. + Pautan pelancaran permainan tidak sah. + Permainan ini tidak lagi berada dalam pustaka DroidDeck anda. + Hentikan sesi semasa sebelum melancarkan permainan Steam. + Tidak dapat menghantar permainan ke Steam. Cuba lancarkan semula. + Cuba semula pengalihan keluar + Mengalih keluar + Pengalihan keluar tidak selesai + Eksport fail permainan + Segerakkan semua fail permainan ke folder + Hentikan penyegerakan fail permainan + Pilih folder untuk fail permainan frontend + Fail permainan disimpan ke %1$s + Fail frontend + + Tunggu operasi masa jalan selesai + Tunggu pemasangan alat keserasian selesai + Tunggu pemasangan apl desktop selesai + Tunggu pemasangan selesai sebelum melancarkan permainan + Tutup sesi Steam dahulu, supaya permainan tidak dapat menyimpan di atas import + Mengimport ke %1$s + + Mengimport %1$d fail daripada %2$s ke %3$s + + + Mengimport %1$d fail daripada %2$s ke %3$s. Simpanan lama disandarkan ke Download/DroidDeck/Saves/backups + + Pilih zip simpanan untuk %1$s + Mengeksport %1$s + + Mengeksport %1$d fail sebagai %2$s: %3$s + + Pilih tempat menyimpan %1$s (%2$s) + %1$s gagal: %2$s + Belum ada log sesi: jalankan sesi dahulu. + Tiada log sesi untuk dikosongkan. + + %1$d folder log sesi dikosongkan. + + Gandingan Debugging Wayarles gagal + Arahan Debugging Wayarles gagal + Debugging Wayarles tidak tersedia + Arahan ADB disalin + Pilih folder ROM + Bukan GPU Adreno + Turnip menyokong GPU Adreno. Pada %1$s, Steam mungkin memaparkan skrin hitam. Muat turun: %2$.0f MB. + Pasang juga + Alih keluar masa jalan Linux + Ini memadam masa jalan, klien Steam di dalamnya, dan setiap permainan yang dipasang di sana. + Digunakan: %1$s + Tidak dapat membuka %1$s pada skrin utama + Tidak dapat membuka %1$s pada %2$s + Tidak dapat membuka %1$s pada paparan %2$d + Mengimport + Menukar + Memulihkan + Membatalkan + Memadam + tidak dapat membaca fail + %1$s diimport + Penukaran yang menunggu telah dibatalkan. + Pilih pakej komponen (-linux .wcp) + Nightlies tidak dapat dihubungi + %1$s disimpan + Muat turun gagal: %1$s + Pilih pemacu masa jalan Linux (zip -Linux atau gabungan Android + Linux) + Pilih pemacu paparan (zip AdrenoTools atau gabungan Android + Linux) + Pilih zip pemacu atau gabungan Android + Linux + %1$s: %2$s + Mengalih keluar %1$s… + Melupakan %1$s… + Pilih folder storan permainan + Pilih ZIP pemalam Decky + Tidak dapat digunakan: %1$s + Program + Mengalih keluar masa jalan Linux + Tidak dapat membuka Tetapan pada skrin bawah; dibuka pada skrin utama. + Buka Pilihan pembangun dalam Tetapan + + Tidak dapat membuka tetapan “Pasang apl tidak diketahui” Android + Tidak dapat menyemak: %1$s + Binaan ini tidak mempunyai muat turun untuk salinan DroidDeck ini + Hentikan sesi yang berjalan dahulu, kemudian kemas kini + Memuat turun + Memasang + Menunggu pengesahan anda + Kemas kini gagal + Android tidak menawarkan untuk memasang kemas kini + + Pemasangan Decky gagal: %1$s + Memulakan import pemalam… + Import pemalam gagal: %1$s + Pemalam dipasang. Mulakan semula Steam untuk memuatkannya. + + Belum disemak - ketik segar semula untuk mencari pemacu baharu + Android + Linux: menetapkan pemacu paparan sekali + Android + Linux: menetapkan pemacu masa jalan sekali + Turnip yang terbina dalam masa jalan + Vulkan sistem: tiada binaan terbundle untuk GPU ini + %1$s (terbundle) + Manual + %1$s · %2$s + Hanya satu separuh diterbitkan sekarang + %1$.0f MB untuk kedua-duanya + Tiada pemacu untuk ditetapkan: %1$s + Menyemak pemacu terkini… + Terkini (tidak dapat disemak: %1$s) + Pemacu yang disyorkan tidak diterbitkan sekarang + Memuat turun %1$s %2$s… + Dikemas kini ke %1$s %2$s. + Dikemas kini ke %1$s %2$s. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula. + Ditukar ke %1$s %2$s. + Ditukar ke %1$s %2$s. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula. + Menggunakan %1$s %2$s. + Menggunakan %1$s %2$s. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula. + Bukan zip pemacu: bukan pemacu AdrenoTools, Turnip -Linux, atau gabungan Android + Linux + Import gagal: %1$s + Pemacu diimport + Menggunakan %1$s sebagai pemacu masa jalan dan paparan. + Menggunakan %1$s sebagai pemacu masa jalan dan paparan. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula. + %1$s dipadam (kedua-dua pemacu) + %1$s dipadam + Terkini: %1$s · disemak %2$s + Terkini: %1$s · %2$s tidak dapat dihubungi · disemak %3$s + + %1$d minit lalu + + + %1$d jam lalu + + + %1$d hari lalu + + Menyemak Banners-Turnip, WinNative dan DroidDeck… + semakan gagal + %1$s dipasang - pilih dalam mana-mana senarai untuk menetapkan kedua-dua pemacu + %1$s dipasang - pilih dalam menu + + Tunggu pengalihan keluar masa jalan selesai + Permainan + touchpad + langsung + Penukaran gagal: %1$s + Penyimpanan profil gagal: %1$s + Desktop dijeda + %1$s dijeda + Program dijeda + Memulakan desktop + Memulakan %1$s + Memulakan program + Tiada log untuk sesi ini. + Tidak dapat membuat arkib log sesi. + Tidak dapat berkongsi log sesi. + Memulakan sesi… + memuat turun masa jalan Linux + memuat turun masa jalan Linux · %1$d daripada %2$d MB + menyemak masa jalan Linux + menyahpak masa jalan Linux + memuat turun desktop + memuat turun desktop · %1$d daripada %2$d MB + menyemak desktop + menyahpak desktop + memuat turun Proton Experimental (ARM64) + memuat turun Proton Experimental (ARM64) · %1$d daripada %2$d MB + menyemak Proton Experimental (ARM64) + menyahpak Proton Experimental (ARM64) + Tidak dapat menghubungi katalog masa jalan. Semak sambungan dan tekan Main semula. + Masa jalan Linux tidak berjaya dipasang. Semak masa jalan dalam Persediaan dan tekan Main semula. + Desktop tidak berjaya dipasang (%1$s). Semak sambungan dan tekan Desktop semula. + %1$s terhenti secara tidak dijangka. Kongsi log dengan laporan pepijat, atau cuba lagi. + Program terhenti secara tidak dijangka. Kongsi log dengan laporan pepijat, atau cuba lagi. + Desktop terhenti secara tidak dijangka. Kongsi log dengan laporan pepijat, atau cuba lagi. + Status keluar %1$d · %2$s + tiada log + + Log menunjukkan %1$d ralat “Function not implemented”: pecutan seccomp proot gagal pada pembantu di peranti ini. Cuba %2$s → “%3$s” (atau “%4$s”) dan mulakan semula. + + + Pemalam + Pasang ZIP pemalam untuk sesi Steam seterusnya. + Memuat turun… + Pasang daripada ZIP + Permainan + Audio & Video + Grafik + Internet + Pejabat + Pembangun + Pendidikan + Sains + Sistem + Utiliti + Popular + Tular + Baharu + Dikemas kini + memuatkan… + Persediaan Flatpak + %1$s: %2$s + Tidak dapat menyemak kemas kini di Flathub + dipasang + folder + Graphite + Permukaan graphite, dengan bebola biru logo untuk semua yang dipilih, difokus atau ditekan. + Kertas pada hitam + Putih sebagai utama, biru ikon sebagai isyarat untuk garis, gelang dan titik. + Biru ikon + Latar hitam, teks putih, biru untuk semua yang dipilih, difokus atau ditekan. + Navy elektrik + Biru lebih cerah, pada permukaan yang lebih cenderung navy daripada kelabu. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Pasang masa jalan dahulu + Pustaka Steam tidak tersedia + Pustaka SD terdahulu tidak tersedia + Storan permainan tidak tersedia + Folder permainan yang ditambah tidak tersedia + Manifest Steam tidak tersedia + Banyak sistem + Dalaman + Sistem Linux + Kad SD + Storan + Pilih + Ciri + Tambah ke Kegemaran + Alih keluar daripada Kegemaran + Namakan semula + Salin + Potong + Tindakan + Kegemaran + Sematkan folder semasa + Belum ada kegemaran - sematkan folder dengan menu ⋮-nya untuk melompat ke sini dengan pantas. + Alih keluar daripada kegemaran + 0 bait + bait + Lokasi + Jenis + Saiz + Diubah suai + Folder + Fail + Fail %1$s + Baca sahaja + Menghalang permainan (dan Wine) daripada menulis ganti atau memadam fail ini. + Tidak dapat menukar Baca sahaja + Tersembunyi + Storan ini tidak dapat menyimpan bendera tersembunyi. + Menandakan fail tersembunyi dalam Windows (atribut tersembunyi Wine). + Tidak dapat menukar Tersembunyi pada storan ini + Selesai + Memadam... + Padam gagal + Mengalihkan... + Menyalin... + Mengalihkan - %1$s + Menyalin - %1$s + Mengalihkan %1$d/%2$d - %3$s + Menyalin %1$d/%2$d - %3$s + %1$d selesai, %2$d gagal + %1$d selesai, %2$d dilangkau + + %1$d item dialihkan + + + %1$d item disalin + + “%1$s” sudah wujud + Menamakan semula... + Namakan semula gagal + Mencipta folder... + Tidak dapat mencipta folder + Folder Baharu + Nama folder + Cipta + Nama baharu + Padam? + Padam “%1$s” secara kekal? + + Padam %1$d item? + + Ini tidak boleh dibuat asal. + + …dan %1$d lagi + + Memadam %1$d/%2$d - %3$s + + %1$d tidak dapat dipadam + + Gabung menambah dan menggantikan fail di dalam folder sedia ada. + Tulis ganti menggantikan fail sedia ada. + Gunakan untuk semua konflik + Gabung + Tulis ganti + Simpan kedua-duanya + Langkau + Pilih folder ini + Kembali + %1$s dilekapkan tetapi tidak dapat dibaca sekarang + Paparan senarai + Paparan grid + Cari + Susun + Nama + Tarikh diubah suai + Baris selesa + Baris padat + Sembunyikan fail tersembunyi + Papar fail tersembunyi + Sembunyikan kegemaran + Papar kegemaran + Tapis folder ini + %1$s bebas + %1$d dipilih + Tiada + Semua + Tampal + + %1$d item + + Tampal %1$s di sini + Tampal %1$s (alih) di sini + Fail + STORAN + PANTAS + KEGEMARAN + Muat turun + MuatTrn + ROM + Log sesi + Gambar + “%1$s” ditambah ke Kegemaran + “%1$s” dialih keluar daripada Kegemaran + Direktori kosong + Naik satu tahap + Pilih folder + Pilih fail + Biru Steam + Langit + Putih + Violet + Hijau + Ambar + Merah + Merah jambu + Butang A + Butang B + Butang X + Butang Y + Bumper kiri + Bumper kanan + Pencetus kiri + Pencetus kanan + Butang View + Butang Menu + View + Menu + D-pad atas + D-pad bawah + D-pad kiri + D-pad kanan + Tersembunyi + Seret untuk alih + Lekat + Simpan + Tiada + Papan kekunci + trackpad + Terminal + Pemegang Deck + trackpad + + Mengalih keluar masa jalan Linux + + Mengalih keluar masa jalan Linux · %1$d entri + + Masa jalan Linux dialih keluar + Tidak dapat selesai mengalih keluar masa jalan Linux. Cuba semula pengalihan keluar dalam Persediaan. + Mengalih keluar baki masa jalan + Memuat turun + Mengesahkan + Mengekstrak + Dipaparkan semasa masa jalan Linux dipasang atau dialih keluar + Memasang masa jalan Linux + Mengalih keluar masa jalan Linux + + Masa jalan Linux belum dipasang + Katalog tidak mempunyai checksum untuk %1$s + Checksum tidak sepadan - tiada apa yang diubah + Memasang %1$s + Tidak dapat meletakkan AppImage + Pengekstrakan gagal + Pemasangan gagal + + Persediaan Flatpak gagal (keluar %1$d) + Persediaan Flatpak tidak selesai + Tugas kedai lain sedang berjalan + Sediakan Flatpak dahulu + Memulakan + Flatpak keluar dengan status %1$d + Memasang %1$s + Mengemas kini %1$s + Mengalih keluar %1$s + Memasang %1$s (%2$d/%3$d) + Mengemas kini %1$s (%2$d/%3$d) + Mengalih keluar %1$s (%2$d/%3$d) + terjemahan + pemacu grafik + kodek media + masa jalan %1$s + masa jalan %1$s %2$s + + Fail terlalu kecil untuk menjadi AppImage + Fail tidak dapat dibaca: %1$s + Ini bukan AppImage (ia bukan program Linux) + AppImage ini dibina bukan untuk ARM64 (aarch64) mahupun PC x86 + Ini adalah program Linux tetapi bukan AppImage + Hanya AppImage jenis 2 boleh diimport (ini jenis %1$d) + Ruang bebas tidak mencukupi: import memerlukan kira-kira %1$s + Ruang bebas tidak mencukupi: kemas kini memerlukan kira-kira %1$s + Import gagal + AppImage itu sudah hilang + Kemas kini gagal + Mengekstrak %1$s + AppImage tidak dapat diekstrak + AppImage tidak dapat diekstrak: %1$s + + Hentikan sesi aktif sebelum memasang Decky Loader + Muat turun Decky Loader gagal + Saiz Decky Loader tidak sepadan dengan metadata keluaran + Tidak dapat memuat turun checksum keluaran + Checksum keluaran tiada + Checksum keluaran tidak sah + Checksum Decky Loader tidak sepadan + Aset keluaran bukan boleh laku Linux 64-bit yang serasi + Sesi dimulakan semasa muat turun; hentikan dahulu sebelum memasang Decky Loader + Tidak dapat menandakan PluginLoader sebagai boleh laku + Tidak dapat membenarkan penyahpepijatan jauh CEF Steam + Tidak dapat menyediakan PluginLoader + Tidak dapat mengekalkan PluginLoader sedia ada + Tidak dapat mengaktifkan PluginLoader baharu + Hentikan sesi aktif sebelum memasang pemalam + Pasang Decky Loader sebelum mengimport pemalam + Tidak dapat membaca fail ZIP yang dipilih + Tidak dapat mencipta direktori pemalam Decky + Mengekstrak ZIP pemalam + ZIP pemalam mesti mengandungi satu folder pemalam peringkat atas + ZIP pemalam tiada plugin.json, package.json atau dist/index.js + Nama pemalam tiada + Sesi dimulakan semasa pemasangan; hentikan dahulu sebelum memasang pemalam + Pemalam dipasang · mulakan semula Steam untuk memuatkannya + Pemasangan ZIP pemalam gagal + ZIP pemalam mengandungi terlalu banyak entri + ZIP pemalam mengandungi laluan yang tidak sah + ZIP pemalam mengandungi laluan pendua + ZIP pemalam mengandungi laluan yang tidak selamat + Tidak dapat mencipta folder pemalam + ZIP pemalam mengembang melebihi had 1 GiB + Tidak dapat menandakan binari pemalam terbundle sebagai boleh laku + Entri remote_binary tidak sah + Nama binari jauh tidak sah + URL binari jauh mesti menggunakan HTTPS + Binari jauh %1$s mempunyai cincang SHA-256 yang tidak sah + Tidak dapat mencipta direktori bin pemalam + Memuat turun binari pemalam · %1$s + Tidak dapat memuat turun binari pemalam %1$s + Checksum binari pemalam tidak sepadan · %1$s + Tidak dapat menandakan binari pemalam sebagai boleh laku · %1$s + Tidak dapat menggantikan binari pemalam · %1$s + Tidak dapat memasang binari pemalam · %1$s + Folder pemalam dimiliki oleh pemalam lain + Tidak dapat mengekalkan pemalam sedia ada + Tidak dapat mengaktifkan pemalam yang diimport + Tidak dapat mengemas kini tetapan pemalam Decky + Tidak dapat memulihkan pemalam sedia ada + + Tidak dapat menyimpan identiti Debugging Wayarles + Arahan ADB gagal (%1$d) + Android tidak mengesahkan had proses anak telah ditukar: %1$s + Tiada gandingan Debugging Wayarles disimpan + Masukkan alamat IP & Port Debugging Wayarles semasa + + Masa tamat menunggu tetingkap timbul gandingan. Cuba lagi daripada DroidDeck. + Tidak dapat mencari tetingkap timbul gandingan (%1$d). Semak Wi-Fi dihidupkan. + Tidak dapat mencari tetingkap timbul gandingan: %1$s + Masukkan kesemua 6 digit kod + Menggandingkan… + Kod itu tidak berfungsi. Buka tetingkap timbul gandingan semula dan masukkan kod baharunya. + Digandingkan. Menyambung… + Digandingkan, tetapi port Debugging Wayarles tidak dijumpai. Biarkan Debugging Wayarles hidup dan cuba lagi. + Menggunakan tetapan… + Arahan Debugging Wayarles gagal + Persediaan Debugging Wayarles + Meminta kod gandingan Debugging Wayarles semasa Tetapan dibuka + Gandingkan Debugging Wayarles + Hidupkan Debugging Wayarles, kemudian ketik “Gandingkan peranti dengan kod gandingan”. + Hidupkan Debugging Wayarles, kemudian ketik “Gandingkan peranti dengan kod gandingan”. Kekal dalam Tetapan: kod akan masuk dalam pemberitahuan ini. + Kod gandingan 6 digit + Ketik Masukkan kod dan taip 6 digit yang dipaparkan dalam Tetapan. + Masukkan kod gandingan Wi-Fi + Cuba kod itu sekali lagi + Masukkan kod + Menyediakan Steam + Steam sedia + Ketik untuk kembali ke DroidDeck. Anda boleh matikan Debugging Wayarles sekarang. + Persediaan Debugging Wayarles terhenti + + Muat turun menjawab HTTP %1$d + Saiz muat turun tidak sepadan dengan APK yang diterbitkan dan telah dibuang - cuba lagi + Muat turun tidak sepadan dengan checksumnya dan telah dibuang - cuba lagi + Kemas kini ini untuk pakej Android %1$s, bukan %2$s + Metadata kemas kini tidak menamakan kunci tandatangan keluaran DroidDeck + Android tidak dapat membaca APK yang dimuat turun + APK yang dimuat turun untuk %1$s, bukan %2$s + APK yang dimuat turun menyatakan versionCode %1$d, bukan %2$d yang diterbitkan + Android tidak akan memasang versionCode %1$d menggantikan %2$d yang dipasang + APK yang dimuat turun tidak mempunyai maklumat tandatangan + APK yang dimuat turun tidak ditandatangani dengan kunci keluaran DroidDeck + Katalog kemas kini menjawab HTTP %1$d + + Memulakan sesi… + %1$d:%2$02d berlalu · masih berjalan + Memuat turun kemas kini klien Steam · %1$d%% + Memuat turun klien Steam · %1$s + + Pemasangan alat keserasian sudah berjalan + Hentikan sesi aktif sebelum memasang alat keserasian + Tidak dapat mencari keluaran ARM64 untuk %1$s + Sekurang-kurangnya 4 GB bebas tambah saiz muat turun diperlukan untuk memasang %1$s + Memuat turun %1$s %2$s + Mencuba semula muat turun · percubaan %1$d daripada %2$d + Muat turun terganggu + Muat turun gagal; fail separa disimpan supaya cubaan semula boleh disambung + Muat turun melebihi saiz keluaran dan telah dipadam + Muat turun tidak selesai; fail separa disimpan untuk cubaan semula yang boleh disambung + Mengesahkan muat turun + Tidak dapat membaca checksum keluaran; cuba lagi + %1$s %2$s tidak menerbitkan checksum; tiada apa yang dipasang + Checksum tidak sepadan; muat turun telah dipadam + Sesi dimulakan semasa muat turun; hentikan dahulu sebelum memasang alat keserasian + Memasang %1$s + Pemasangan %1$s gagal (keluar %2$d) + Tidak dapat memasang %1$s + Mengambil pek droiddeck-esync + + dengan pembantu Linux + DLL sahaja + Asal (Proton dikemas kini sejak penukaran terakhir) + Diubah di luar Komponen + tidak ada + Asal %1$s + Asal daripada %1$s + %1$s %2$s akan masuk ke %3$s apabila permainan yang berjalan padanya ditutup. + %1$s %2$s kini dalam %3$s. Ia berkuat kuasa pada kali seterusnya permainan dimulakan. + Pakej asal itu tidak disimpan + %1$s bagi %2$s akan kembali kepada asalnya apabila permainan yang berjalan padanya ditutup. + %1$s bagi %2$s dipulihkan daripada asal %3$s-nya. + %1$s sedang digunakan dalam %2$s. Tukar Proton itu kepada yang lain dahulu. + %1$s tidak disimpan + %1$s telah dipadam. + Asal binaan yang dipasang dikekalkan: itulah jalan kembali. + Tidak disimpan + Asal %1$s bagi %2$s telah dipadam. + Muat turun hanya datang daripada keluaran Nightlies + Senarai pakej ini mendahului checksum - segarkan dan cuba lagi + Checksum tidak sepadan + + Linear + Terdekat + Snapdragon GSR (kualiti) + Ketajaman sahaja + Auto (sepadan penskalaan) + + Tiada yang digunakan. + Kejelasan Permainan + Ketajaman dengan sedikit kontras - yang harian. + Jelas + Warna didorong kuat. Pengurangan jalur dihidupkan supaya langit tidak bergaris. + Sinematik + Kontras lebih dalam, warna lebih sejuk, tepi dilicinkan. + Kompetitif + Bayang diangkat dan ketajaman berat - kesan orang dahulu. + Ketajaman Adaptif + Ketajaman sahaja, warna tidak disentuh. + Filmik + Lengkuk seperti filem, sedikit lembut. + Arked + Kuat dan bertenaga. + CRT Retro + Tiub CRT dan isyarat analog bergabung sekali. + Peningkatan Skala Tajam + Untuk berjalan di bawah resolusi panel. + Piksel Bersih + Piksel tajam untuk 2D dan tajuk lama. + Tepi Anime + Garis luar kartun tambah ketajaman. + + Bukan GPU Adreno + Disokong + Eksperimen: pemacunya ialah binaan ujian + Di bawah perkakasan yang diuji (Adreno 725 dan lebih baharu): mungkin tidak dapat berjalan + Tidak disokong: DroidDeck memerlukan GPU Adreno (Snapdragon) + + Had kadar GitHub dicapai - cuba lagi kemudian + GitHub menjawab HTTP %1$d + muat turun menjawab HTTP %1$d + Senarai pemacu ini mendahului checksum - segarkan dan cuba lagi + Checksum tidak sepadan - muat turun telah dibuang + Bukan pemacu AdrenoTools: %1$s + tiada meta.json + meta.json tidak dapat dibaca (%1$s) + ini ialah pemacu masa jalan Linux (import di bawah “Pemacu masa jalan Linux”) + ini ialah pemacu permainan Wayland, yang tidak berguna untuk apl ini + meta.json tidak menamakan libraryName, jadi tiada apa dapat diberikan kepada AdrenoTools + meta.json menamakan %1$s, yang tidak ada dalam zip + %1$s bukan pustaka kongsi ELF AArch64 64-bit + %1$s menghubungkan glibc - Turnip “-Linux”, yang tergolong di bawah “Pemacu masa jalan Linux” + Tiada libvulkan_freedreno*.so dalam zip ini. Zip Turnip Android (AdrenoTools) atau -Wayland bukan pemacu masa jalan Linux. + %1$s bukan pustaka kongsi ELF AArch64 64-bit. + %1$s bukan pemacu glibc - ia menghubungkan libc Android. Masa jalan Linux memerlukan zip “-Linux”; Turnip biasa atau “-Wayland” langsung tidak dapat dimuatkan oleh klien Steam. + Bukan gabungan pemacu: tiada manifest.json yang menamakan pemacu android dan linux + Bukan gabungan pemacu: %1$s menunjuk ke luar darinya + Bukan gabungan pemacu: ia mengembang lebih daripada %1$d MB + + Kad SD + Folder + + Kongsi log sesi + + memerlukan Android 10 atau lebih baharu (lapisan paparan dengan ruang warna) + paparan ini melaporkan tiada sokongan HDR + paparan ini menyokong %1$s, bukan HDR10 + + GPU ini + Had proses anak dihidupkan + Had proses anak tidak ditetapkan + Had proses anak tidak dapat disemak + Had proses anak dimatikan + Had proses anak tidak diperlukan + %1$s Jika itu tidak boleh, sambungkan peranti ini ke komputer dengan ADB dan jalankan arahan di bawah. Kembali ke sini dan semak semula. + Sesi Steam boleh dimulakan. + Versi Android ini tidak menggunakan had ini. + Hidupkan “Lumpuhkan sekatan proses anak” dalam Pilihan pembangun, dan biarkan Pilihan pembangun hidup selepas itu. + Versi Android ini tidak mempunyai suis untuknya. Gunakan Debugging Wayarles untuk menukarnya daripada peranti ini. + Binaan ini tidak mempunyai muat turun untuk salinan DroidDeck ini. + Android tidak akan memasangnya menggantikan binaan ini kerana versionCode %1$d lebih rendah daripada %2$d yang dipasang. + Android tidak akan memasang versi lama menggantikan versi baharu. Kekal pada binaan ini sehingga saluran menyusul. + Salinan DroidDeck ini ditandatangani secara berbeza (binaan tempatan?), jadi Android tidak akan mengemas kininya di tempat. + Tidak cukup ruang bebas untuk kemas kini. + Kemas kini telah dibatalkan. + Kemas kini tidak terpasang: %1$s + Kemas kini tidak terpasang. + zip %1$s + zip + %1$s belum pernah dilancarkan + Tiada fail simpanan dijumpai untuk %1$s + %1$s tidak mempunyai folder drive_c, jadi ia bukan zip simpanan GameHub atau Winlator + tidak dapat mencipta folder dalam %1$s + tidak dapat menulis dalam %1$s + tidak dapat menulis dalam %1$s (%2$s) + Folder tidak tersedia + Fail lain sudah menggunakan %1$s + Folder seni tidak tersedia + Folder mempunyai inventori penyegerakan yang tidak berkaitan + Tidak dapat mengalih keluar %1$s + Fail keluar daripada folder eksport + FEX (binaan tanpa nama versi) + tiada profile.json dalam %1$s + %1$s bukan pakej komponen Linux (jenis %2$s) + %1$s · %2$s bebas + Automatik + Katalog kemas kini gagal semakannya (%1$s) + Tetapan proses anak DroidDeck + Dilog masuk · mod luar talian + Dilog masuk + TERBUNDLE + DIMUAT TURUN + DIIMPORT + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Paparan sesi + Resolusi + Suaian permainan 16:9 + Cara permainan 16:9 disuaikan ke paparan ini. Sesi seterusnya. + Pilih Sepadan skrin atau saiz tersuai lebih nipis daripada 16:9. + Kekalkan nisbah + Regang ke paparan + Penskalaan imej + Penapis penskalaan + Dikongsi oleh Steam dan Desktop. + Berkuat kuasa secara langsung. Dikongsi oleh Steam dan Desktop. + FSR, GSR dan NIS berkuat kuasa semasa membesarkan imej sesi. Ketajaman sahaja juga berfungsi tanpa pembesaran. + Ketajaman penapis + Kadar bingkai + %1$s Menetapkan penskalaan ke %2$s. + Sepadan skrin (%1$d×%2$d) + Ikut skrin (mengubah saiz apabila dilipat atau dibuka) + Steam + Papar butiran peranti + Sembunyikan butiran peranti + Membaca peranti… + Salin + Salin butiran ini ke dalam laporan pepijat. + Disalin. + Paksa SSBS + Mengekalkan tetapan kelajuan CPU yang sentiasa dimatikan oleh Wine. Boleh melajukan permainan Windows pada Snapdragon 8 Elite; sedikit perubahan dilihat pada cip lama. Berkuat kuasa pada pelancaran permainan seterusnya. + + Kedai + Steam + Tersuai + Kedai + Buka kedai pada + Tahap kelajuan muat turun + Enjin kedai + ● Enjin sedia + Tidak dibina + Seimbang + Pantas + Maksimum + Kedai + Tetapan kedai + Tetapan + (belum log masuk) + Kedai + Dipasang + Pustaka + Semua + Muat turun + %1$s (%2$d) + Log masuk + %1$s · log keluar + Log keluar daripada %1$s? + Permainan yang dipasang kekal. Pustaka diambil semula apabila anda log masuk kembali. + Kekal log masuk + Log keluar + Cari katalog %1$s… + Cari permainan dipasang… + Cari pustaka anda… + Cari + Tiada apa dipasang + Memuatkan + Tiada permainan + Memuatkan kedai… + Tiada katalog + Apa yang baharu + Tawaran + Permainan percuma + Tular + Pustaka anda + Main + Pasang + Sambung pemasangan + Terganggu + Pasang · %1$s + Dapatkan percuma + Lihat pada %1$s + Beli · %1$s + Dipasang + Dalam pustaka + Percuma + Sambung + Segar semula + Papar kandungan matang + Epic + EOS + Luar talian + Mati + Log masuk Epic (EOS) + Lancarkan secara luar talian + Selesaikan log masuk Epic + Simpanan awan + Muat naik ke awan + Muat turun daripada awan + Tiada simpanan awan + Konflik awan + Konflik awan · %1$d fail + … + Kekalkan awan + Kekalkan tempatan + Menyemak awan… + Segerak terakhir %1$s + Belum disegerakkan + %1$d fail + dipasang + dalam pustaka anda + kedai + Pencapaian + %1$d daripada %2$d dibuka kunci + %1$d / %2$d + Dibuka kunci · %1$d + Berkunci · %1$d + Tersembunyi · %1$d + Pencapaian tersembunyi + Butiran dipaparkan sebaik ia dibuka kunci. + Semua pencapaian dibuka kunci + Memuatkan… + Log masuk ke %1$s untuk melihat pencapaian anda. + Tidak dapat menghubungi %1$s. Semak semula sebaik anda dalam talian. + %1$s tidak menyenaraikan pencapaian untuk permainan ini. + Papar semua %1$d + Pencapaian · %1$s + ✓ Dalam Steam + Nyahpasang + Menyahpasang… + Nyahpasang %1$s? + Membebaskan %1$s dan mengalih keluarkannya daripada Steam. + Mengalih keluar failnya dan pintasan Steamnya. + Steam akan mengambilnya pada kali seterusnya ia dimulakan; lancarkan dari sana, atau mulakan Steam sekarang. + Proton + Pilihan Steam mengikut permainan + Audio + DirectAudio + Komponen + FEX, DXVK, VKD3D + Enjin + %1$s · asli + %1$s · terbina dalam + Cara ia berjalan + Diambil daripada + %1$s, dengan akaun anda + Dipasang ke + Storan permainan (Games/%1$s/%2$s) + Dilancarkan oleh + Steam dan Proton, laluan yang sama seperti permainan Steam + Steam dan Proton · Log masuk Epic setiap pelancaran, overlay EOS mati + Steam dan Proton · Pemboleh ubah Amazon Games Services ditetapkan + Steam + ditambah ke Steam semasa pemasangan (UI Deck + Steam Input) + Tiada muat turun + Dijeda + Dalam baris gilir + Dalam baris gilir · #%1$d + Dibatalkan + Gagal + Gagal · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Jeda + Sambung + Batal + Cuba lagi + Kosongkan + Kosongkan yang selesai + Manifest + Muat turun + Sahkan + Pasang + Selesai + Menyemak + Memuat turun + Padam muat turun? + Gagal · %1$s dikekalkan + Mengesahkan + Memasang + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d fail + Pengurus muat turun + Muat turun pada satu masa + Pasang ke + Pasang %1$s: + Dalaman + %1$s bebas + Muat turun kedai + Memaparkan kemajuan muat turun permainan dan mengekalkan muat turun berjalan di latar belakang + Memuat turun + %1$d muat turun + Menyediakan… + Muat turun kedai selesai + %1$s dipasang + Muat turun %1$s gagal + Padam + Kekalkan + Log masuk ke %1$s + Log masuk dengan penyedia anda + Muat semula + Tutup + Cuba lagi + Menyelesaikan log masuk… + Halaman log masuk tidak dapat dimuatkan: %1$s + Penyedia ini menolak log masuk daripada halaman terbenam. Log masuk dengan kata laluan kedai anda sebaliknya. + Halaman log masuk menjawab dengan ralat (HTTP %1$d). + Sambungan ke halaman log masuk tidak selamat. + Log masuk tidak dapat disahkan. Cuba lagi. + Log masuk gagal. Cuba lagi. + Tutup + Kembali + Sunting + Tambah semua permainan dalam folder ini + Tiada yang baharu dalam folder ini + Nama + Sasaran + Mula dalam + Pilihan pelancaran + Seni + Exe lain… + Alih keluar + Kulit + Latar belakang + Logo + Ikon + Folder permainan + Steam + SteamGridDB + Fail anda + Daripada exe + Automatik + Pilih imej… + Pilih imej: %1$s + Tetapkan semula ke automatik + + %1$d permainan ditambah + + + %1$d sudah berada dalam Permainan + + B · Kembali ke %1$s + permainan itu + Tetapan Main + Pemacu grafik + Proton ini + Setiap Proton + Untuk setiap Proton + Tersedia · %1$d + Disemak %1$s + Semak komponen dan pemacu baharu + Dapatkan + %1$s · DXVK, VKD3D-Proton dan FEX di bawah adalah milik Proton ini + %1$s · diuji + %1$s · belum diuji + %1$s · tidak disokong + Mengekalkan pasangan disyorkan terkini + Auto · %1$s + Pemacu masa jalan dan paparan secara berasingan + Gunakan %1$s? + Alih keluar %1$s? + Diagnostik + Sedia untuk bermain + Belum sedia untuk bermain + masa jalan %1$s + had proses dimatikan + dilog masuk sebagai %1$s + Apabila sesuatu memerlukan anda + Rupa + Peranti ini + Seni pustaka + APL LINUX + Flathub + Apl desktop untuk desktop Linux, dibuka daripada Desktop. + Sediakan Flathub + Alih keluar %1$s? + Tetapan dan failnya dikekalkan. + Kekalkan + Saiz pada cakera + Nyahpasang desktop? + Semua + Pencapaian GOG + Menyegerakkan pencapaian untuk permainan GOG yang disokong. Terpakai pada pelancaran seterusnya. + Pencapaian dimatikan + Pencapaian dihidupkan + Log masuk semula ke GOG + Pencapaian dimuat naik + Muat naik pencapaian belum selesai + Hidupkan pencapaian GOG dalam tetapan Stores + Pencapaian tidak tersedia; semak log sesi + Pencapaian dibuka kunci + Tampal + Pad arah + Kayu bedik kiri + Kayu bedik kanan + Menu + Papan kekunci + Pad arah atas + Pad arah bawah + Pad arah kiri + Pad arah kanan + Kawalan sentuh + Pengawal sentuh + Tetikus + Hidup + Mati + Perlahan + Biasa + Pantas + Edit susun atur + Menu DroidDeck + Seret kawalan ke skrin + Susun + Buang + Menyimpan… + Ikon + L− + L+ + T− + T+ + Sembunyikan + Warna dan kelegapan… + Salin susun atur ini + Susun atur disalin; tampalkannya pada susun atur sentuh mana-mana permainan + Tampal susun atur yang disalin + Tetapkan semula kepada lalai Steam + Kawalan ini tidak mempunyai pengikatan dalam Steam untuk diberikan ikon + Ikon untuk %1$s + Warna susun atur sentuh + Selesai + Susun atur sentuh tidak dapat disimpan + Susun atur sentuh disimpan ke Steam + Apl %1$d + Label (pilihan) + Ikon Steam tidak dijumpai + + %1$d ikon daripada Steam + + Warna ikon + Warna butang + Tiada ikon + + Menyemak log masuk… + Mengambil pustaka anda… + Mengambil senarai permainan… + Memuatkan butiran permainan… (%1$d/%2$d) + + Menyegerakkan %1$d permainan… + + Menyegerakkan… %1$d/%2$d + Mengambil kulit permainan… + Penyegerakan gagal + Tiada permainan dijumpai dalam pustaka %1$s anda + Amazon Games tidak memberi respons untuk pustaka anda: muat semula, atau log masuk semula + Tidak dapat menyambung ke GOG + Pustaka GOG anda tidak dapat dibaca + Mendaftar dengan Steam… + Cardboard VR + Mod Cardboard + Paparan mata kiri dan kanan untuk kaca mata VR telefon. + Gambar + Skrin maya + 3D sebelah-menyebelah + Kaca mata + Cardboard + Cardboard (2014) + Tiada kanta + Jarak mata + Lebar skrin + Jarak skrin + Penjejakan kepala + Peranti ini tiada penderia putaran. + Pusatkan semula paparan + Pusatkan semula + SteamVR (eksperimen) + Pasang + Memuat turun SteamVR ARM64 daripada Valve (571 MB). + Memuat turun dan menyahmampat SteamVR… + Mula + Menjalankan SteamVR dengan kepala anda sebagai set kepala; gambar bertukar kepada 3D sebelah-menyebelah. + Henti + SteamVR sedang berjalan. + Tidak tersedia dalam sesi ini. + SteamVR tidak memberi respons; semak log sesi. + Muat turun komponen + Muat turun dikongsi dengan halaman Komponen. Pilih versi yang telah dimuat turun di atas untuk menggunakannya pada pelancaran permainan seterusnya. + Pilih versi untuk dimuat turun + Pembangun + Penerbit + Dikeluarkan + Utama + + Tambahan + Lengkap + %1$s j + %1$d min + diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml new file mode 100644 index 000000000..4e03c7770 --- /dev/null +++ b/app/src/main/res/values-nb/strings.xml @@ -0,0 +1,2235 @@ + + + DroidDeck + + Første gang lastes Steam ned. Dette kan ta noen minutter. + Som standard settes Steam på pause når du lukker appen, og fortsetter der du slapp når du kommer tilbake. + Kontrolleren din vises som en Xbox 360-kontroller. Hvis ikke, bruk skjermkontrollene. + + + Første gang settes skrivebordet opp. Dette kan ta noen minutter. + Som standard settes sesjonen på pause når du lukker appen, og fortsetter der du slapp når du kommer tilbake. + Berøring fungerer som styreflate: dra for å flytte pekeren, trykk for å klikke. + Kontrolleren din vises som en Xbox 360-kontroller. Hvis ikke, bruk skjermkontrollene. + + Sesjon + Vises mens en Steam- eller skrivebordssesjon er åpen + Steam kjører + Steam-nedlasting aktiv + Sesjonen er pauset – trykk på Gjenoppta når du er klar + Gjenoppta + Sett på pause nå + Avslutt sesjon + Bildegenerering + Velg modus for bildegenerering + Av + Win-FG %1$d× + LSFG %1$d× + LSFG Adaptiv %1$d + Krever Lossless Scaling på Steam + Installer Lossless Scaling via Steam, eller importer Lossless.dll + Importer Lossless.dll… + Bruker kopien fra Steam + Bruker en importert kopi + Velg Lossless.dll + Lossless.dll importert + Denne Lossless.dll-filen er allerede i bruk + Lossless Scaling eies ikke av denne Steam-kontoen + Denne filen er ikke Lossless Scalings Lossless.dll + Kunne ikke forberede LSFG fra denne Lossless.dll-filen + LSFG krever Lossless Scaling fra Steam eller en importert Lossless.dll + LSFG kunne ikke forberede shaderne sine fra Lossless.dll + %1$s: ikke støttet + %1$s: motoren feilet + %1$s: krever Lossless Scaling + %1$s %2$s · starter + Legger til et Steam-bibliotek og søker gjennom det etter Windows-spill. Eide spill bruker sine Steam-oppføringer, resten blir snarveier til spill som ikke er fra Steam. + Spillmiljø + Miljøvariabler for Proton-spill. + Rediger + Alle Proton-spill + Profil + Steam-app-ID + Åpne + Spill %1$s + Legg til variabel + Variabelnavn + Verdi + Lagre + Fjern + Arv på nytt + Fjern alle miljøoverstyringer + Fjern for denne profilen + Ingen variabler satt. + Arvet + Egendefinert variabel + Kunne ikke laste eller lagre. Kontroller ledig lagringsplass og prøv igjen. + Bruk et gyldig navn og en verdi på opptil 8192 tegn. + Standard Proton-kjøretid leser ikke denne variabelen. + Laster… + Overstyring av D3D12-funksjonsnivå. 12_2 rapporterer DirectX 12 Ultimate-funksjoner; ustøttede funksjoner kan feile. La stå uten verdi for automatisk oppdagelse. + Overstyring av D3D12-shadermodell. 6_9 krever en tilstrekkelig ny VKD3D og tilsvarende GPU-funksjoner. La stå uten verdi for automatisk oppdagelse. + Behold Mesa shader-mellomlagring aktivert for å redusere gjentatt kompilering. false aktiverer mellomlagring. + Maksimal størrelse på Mesa shader-mellomlageret, for eksempel 1G. Større mellomlagre bruker mer lagringsplass. + VKD3D-innstillinger for D3D12. nodxr slår av ray tracing; bruk bare for spill som krever det. + Sett til 1 for en Proton-diagnostikklogg. Logging kan redusere ytelsen og bruke lagringsplass. + Sett til 1 for å bruke OpenGL-baserte WineD3D i stedet for DXVK til kompatibilitetstesting av D3D9–11. + Sett til 1 for å holde dette spillet unna ntsync (droiddeck-ntsync eller en kjernedriver); det bruker droiddeck-esync der en pakke finnes, ellers droiddeck-fsync. + Sett til 1 for å slå av droiddeck-esync for dette spillet; det bruker droiddeck-fsync i stedet, eller droiddeck-ntsync der fsync også er av. + Sett til 1 for å holde dette spillet unna droiddeck-fsync; det bruker droiddeck-esync der en pakke finnes, ellers droiddeck-ntsync. + Sett til 1 for å kjøre dette spillet på droiddeck-fsync, eller 0 for å sette droiddeck-esync først der en pakke passer Protonen. + Sett til 0 for å slå av Protons hjelper for kontrollernavigasjon for dette spillet. + DXVK-overlegg for D3D9–11. fps viser bildefrekvens; uten verdi skjules det. + DXVK-innstillinger skilt med semikolon. Nyere versjoner bruker dxvk.maxFrameRate for bildefrekvensbegrensning; bruk heller sesjonens bildefrekvensbegrensning for vanlig spilling. + Bildefrekvensgrense for D3D12. 0 slår av grensen. + Mesa OpenGL-tråding. true kan hjelpe CPU-begrensede OpenGL-spill; test per spill. + Wine-synkronisering + Hvordan spill holder trådene sine i takt. Gjelder fra neste sesjon. + esync + ntsync + fsync + wineserver + FEX-forhåndsinnstilling + Gjelder fra neste spilløkt. + Spill + FEX-standard + Bruker FEX-standardinnstillinger. + Stabilitet + Tryggeste, saktest innstillinger. + Kompatibilitet + Full TSO med multiblokk. + Middels + Balansert TSO med redusert x87-presisjon. + Ytelse + All TSO av; multiblokk. + Ytelse + TSO + Ytelsesinnstillinger med hoved-TSO aktivert. + Ekstrem + Slår av kontroller for selvmodifiserende kode. JIT-spill og DRM kan feile. + Ekstrem + TSO + Ekstreminnstillinger med hoved-TSO aktivert. + Ekstrem-gn + Ytelse med TSC og metadatahint. Prøv før Ekstrem. + Denuvo + Ytelse med fulle kodekontroller og hypervisor-biten skjult. + Egendefinert verdi + Velg verdi + Emulatorer & apper + Legg til + Din egen app eller spill + Skript + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Starter… + %1$s lagt til + %1$s fjernet + Skrivebord + Appen finnes ikke lenger. Velg den på nytt fra Skrivebord. + Åpne %1$s + Fjern + Trykk igjen for å fjerne + Avslutt sesjonen for å fjerne + Kjører %1$s fra mappen sin, lenket inn i sesjonen der den ligger. Fjerning av appen lar mappen være i fred. + Kjører %1$s fra en kopi av mappen sin inne i DroidDeck. Fjerning av appen sletter kopien, ikke originalen. + Installer Linux-kjøretiden først + Filen finnes ikke lenger + Ikke nok ledig plass: å kopiere mappen krever %1$s + Kopierer %1$s + Kunne ikke legge til appen + Søker etter en utgivelse av %1$s + Kunne ikke lese utgivelsene til %1$s + Ingen utgivelse av %1$s har en ARM64- eller x86_64-AppImage + Laster ned %1$s + Nedlastingen mislyktes + Oppdatering av appen mislyktes + Dette ikonet kunne ikke leses + %1$s lagret + Rediger + Se etter oppdateringer + Ser etter oppdateringer… + Oppdater til %1$s + Oppdatert · %1$s + %1$s oppdatert + %1$s · %2$s + Legg til en app + Skript + AppImage + GitHub + Flatpak + En .sh-fil. Den kjører fra sin egen mappe, i fullskjerm under gamescope. + En ARM64 (aarch64) AppImage, eller en x86_64-AppImage som kjører via FEX. Den pakkes ut i Linux-kjøretiden én gang. + Installerer ARM64-AppImage-filen fra den nyeste utgivelsen i repositoriet, eller x86_64-filen via FEX. + Bruk FEX + Auto + På + Av + FEX kjører x86-Linux-programmer på denne ARM64-enheten. Auto bruker det når programmet er bygget for x86. + %1$s via FEX + Dette programmet er bygget for %1$s-PC-er og kjører via FEX, som ikke er satt opp ennå. Åpne Steam én gang mens du er innlogget: DroidDeck lar den laste ned FEX og Steam Linux Runtime den trenger, og så åpnes dette. + Dette programmet + %1$s er bygget for x86-PC-er og kjører via FEX, som ikke er installert ennå. Start Steam én gang mens du er innlogget, og la den laste ned FEX og Steam Linux Runtime, og start dette på nytt. + En Flathub-app-ID eller dens Flathub-side. Flatpak settes opp først hvis det ikke allerede er gjort. + Fil + Ingen fil valgt + Velg fil + Endre + Velg et skript + Velg en AppImage + Velg et ikon + Repositorium + eier/repo eller en github.com-lenke + Ikke et GitHub-repositorium + App-ID + org.ppsspp.PPSSPP + Ikke en gyldig Flathub-app-ID + Eller en .flatpak-fil, for en app som ikke er på Flathub. + Velg en .flatpak-fil + Kilde + På skrivebordet ditt + Ny app + Navn + Fra appen + Ikon + Standardikon + Velg ikon + Fjern + Fra repositoriet + Søker etter ikoner… + Standard + Rediger %1$s + Lagre + Kontrollerer mappen… + Mappen er lenket inn i sesjonen: %1$s + Mappen inneholder Linux-programmer, som delt lagring ikke kan kjøre, så den kopieres inn i DroidDeck (%1$s). + Legg til + Avbryt + Installer Linux-kjøretiden fra Oppsett først. + Vent til %1$s er ferdig. + Kompatibel + Builder merket «%1$s» i Steams Kompatibilitet-meny har DroidDecks prefiksfiksing og starter spill her. Standarden er Proton Experimental ARM64 (%1$s), eller Proton 11.0 ARM64 (%1$s) når den nyere builden er installert. Valves egne Proton-oppføringer uten merket kan ikke starte et spill på denne enheten. + Installert %1$s · %2$s + Avbryt + Auto + Aldri + Alltid + Avinstaller + Se etter + Oppdater + Ser etter… + Arbeider… + Glem + Legg til… + Bruk + Gjelder fra neste sesjon. + Steam-sesjon + Skrivebordssesjon + Skjerm + Oppløsning + Opptil 720p + Opptil 900p + Opptil 1080p + Nativ + %1$s (standard) + Egendefinert · %1$d×%2$d + Egendefinert… + 720p kan gjøre menyer mer responsive. + Skjermforhold + Angis av den egendefinerte oppløsningen. + Auto bruker minst 16:9. + Bildefrekvensgrense + Begrenser hele sesjonen. Skjermen bytter til en oppdateringsfrekvens som grensen går opp i. + HDR + HDR10-utgang + Ikke tilgjengelig: %1$s. + Start appen på nytt for å bruke dette. + Drivere + GPU-drivere + Delt av Steam og Skrivebord. Angis på Komponenter-siden. + Berøring og kontroller + Berøring + Styreflate + Direkte + Auto: styreflate på Skrivebord, direkte i Steam. + Skjermkontroller + Steam + QAM + Auto viser kontrollene når ingen kontroller er tilkoblet. + Steam-berøring (beta) + Steams berøringskontroller starter med neste Steam-sesjon + Steam-berøringslayout + Flytt, endre størrelse på eller skjul dette spillets berøringskontroller; lagres til Steam + Kontroller + Hvordan Steam ser kontrolleren din. Gjelder fra neste sesjon. + Steam Deck-kontroller + Steam Controller + DualShock 4 + DualSense + Xbox 360-kontroller + Deck: Hurtigmeny-knapp og gyro. Steam Controller: det samme, med gripesensor på bakknappene. DualShock 4 og DualSense: gyro, og styreflatene som styreflate. Xbox 360: åpne Hurtigmenyen med Guide+A. + Tilbake + Meny → QAM + QAM → Meny + Sesjon + Bakgrunnsoppførsel + Når appen er skjult eller skjermen slås av. + Manuell + Nativ + Auto setter på pause og gjenopptar når den vises. Nativ forbereder Steam på hvilemodus først. Manuell venter på Gjenoppta. Aldri lar den fortsette å kjøre. + La nedlastinger fortsette + Utsetter bakgrunnspause mens Steam laster aktivt ned. + Auto setter på pause i bakgrunnen og gjenopptar når den vises. Manuell venter på Gjenoppta. Aldri lar den fortsette å kjøre. + Oppstart + Start Steam med DroidDeck + Åpner Steam-sesjonen når DroidDeck starter. + Decky + Ser etter utgivelser… + Klar til å installere %1$s. + Ingen kompatibel build funnet. Prøv igjen senere. + Installert · %1$s + Oppdatering tilgjengelig · %1$s + Oppdatert · %1$s + Loader + Installer nyeste + Starter med Steam. Mens på, kan andre apper på denne enheten kanskje styre Steam. + Av. Steams fjernsøkeport for feilsøking forblir stengt. + Avinstaller Decky Loader? + Fjerner loaderen. Programtilleggene og innstillingene dine beholdes. + Klient + Ytelsesoverlegg + Slå av hvis spill krasjer eller blir svarte i Deck-modus. Gjelder fra neste sesjon. + Reparer Steam-klienten + Laster ned Steam-klienten igjen neste gang Steam starter. Bruk dette hvis Steam blir hengende på lasteskjermen. Spill, innlogging og innstillinger beholdes. + Steam-klienten lastes ned igjen neste gang Steam starter. + Reparer + Grafikk + SteamGridDB API-nøkkel + Nøkkelen din + Rediger… + Fyller inn manglende grafikk fra Steam, deretter SteamGridDB. Din egen cover.jpg, hero.jpg, logo.png eller icon.png i spillmappen har forrang. + Fjern %1$s + Fjerne? + Legg til spill + Legg til et spill: + Velg exe-filen: + Denne plasseringen er ikke tilgjengelig i sesjonen + Kontroller denne exe-filen + Windows-komponenter + LB/RB hopper mellom delene. + Ingen + Windows-DLL-er Proton ikke inkluderer, fra komponentlisten. Slå på de dette spillet trenger: de legges inn i spillets egen Windows-mappe, aldri i filene det kom med, og blir med uansett hvilken Proton det kjører på. + Kunne ikke nå komponentlisten. Komponenter som allerede er lastet ned, kan fremdeles slås av og på. + Anbefalt for dette spillet + Ser på hva dette spillet trenger… + Tilgjengelig når du er tilkoblet + + Slår på %1$d komponent… + Slår på %1$d komponenter… + + På for dette spillet + Steam installerer %1$s med %2$s (app %3$s) + Fra GOGs liste for dette spillet + Fra Epics forutsetninger for dette spillet + Fra Amazons installasjonssteg for dette spillet + Steam installerer %1$s med app %2$s, matchet etter navn + Også oppført + Oppført av %1$s; ikke en komponent her + Steam + GOG + Epic Games + Amazon Games + Spillet leveres med %1$s + Spillet har %1$s (fungerer ofte som det er) + Allerede i dette spillet + Ingenting i spillets mappe peker på en komponent. + Installert for dette spillet (%1$d) + Alle komponenter (%1$d) + Krever et Windows-installasjonsprogram (%1$d) + Disse er egne installasjonsprogrammer uten en pakke inni å ta fra hverandre, og ingen oversikt over hva de installerer ennå. De fleste er gamle kjøretider som Mono inne i Proton allerede dekker; oversikter legges til etter hvert som de blir laget. + Vis + Skjul + Kjører sitt eget installasjonsprogram (ingen oversikt ennå) + Ikke støttet her ennå + Laster komponentene… + Kunne ikke installere: %1$s + Lastet ned + Laster ned %1$s + Installerer: %1$s + Slår av… + %1$s nedlasting + Trer i kraft neste gang spillet starter. + Strekk 16:9-spill + På skjermer som er mer kvadratiske enn 16:9, strekkes spill til å fylle skjermen i stedet for å vise sorte striper. Gjelder fra neste sesjon. + Lyd + DirectAudio for spill + Går forbi PulseAudio for lavere ventetid. + Mikrofon + For talechat. + Nettverk + Wi-Fi-oppdagelse + Vis Wi-Fi-navn og nettverk i nærheten i Steam. + Posisjonstjenester er av. + Posisjonstillatelse ikke gitt. + Åpne innstillinger + Fortsett + Slå på Wi-Fi-oppdagelse? + Android krever posisjonstillatelse for å lese Wi-Fi-navn og oppdage nettverk i nærheten. DroidDeck bruker denne tillatelsen til å vise disse nettverkene i Steam. Vi følger ikke med på posisjonen din. Internett-tilgang fungerer uten denne tillatelsen. + Spillagring + Automatisk – SD-kortet når det finnes, pluss Spill-mappene dine + Automatisk – Spill-mappene dine, pluss SD-kortet når det finnes (ingen nå) + Bare intern + Mappe: %1$s + Andre bibliotek + Spill som strømmer ressurser fra SD-kort eller delt lagring kan hakke. + Lagringsdiagnostikk + Lagrer tildelingsresultater og tidsmålinger for lagring i Del logger. Kan gjøre installasjoner tregere mens dette er på. Gjelder fra neste sesjon. + Velg en mappe… + Egendefinert oppløsning + Erstatter grensen og forholdet. En størrelse som ikke passer skjermen, gir sorte striper. + Bredde + Høyde + Mellom 320×240 og 3840×2160. + + %1$d spill + %1$d spill + + OK + Slett + Gjenopprett + Last ned + Slett %1$s + %1$.1f MB + Kjøretidsdriver + Brukes av Steam, spill og skrivebordet. Gjelder fra neste sesjon. + Importer Turnip-zip… + Skjermdriver + Tegner bilder til skjermen i begge moduser. Start appen på nytt for å bruke dette. + Importer en AdrenoTools-zip… + aldri + Nightlies sjekket %1$s + Komponenter + Om Komponenter + Se etter nye drivere + Se etter nye pakker på Nightlies + Proton + Protonen som velges her, kjører hvert spill som ikke har valgt sin egen i Steam, og en endring gjort i Steam vises også her. Steam erstatter Valves Proton når den oppdaterer; originalene beholdes per build. + Konfigurasjon + Konfigurer profil + Profil + Standardprofil + Brukes automatisk av spill uten overstyringer. + Lagrede innstillinger brukes automatisk når %1$s starter. + Velg bare innstillingene dette spillet trenger. Alt annet fortsetter å bruke standardprofilen. + FEX-forhåndsinnstilling + Bruker forhåndsinnstillingen fra standardprofilen. + Egendefinert forhåndsinnstilling lagret for dette spillet. + Egendefinerte FEX-variabler + Bruk standard + Spillkompatibilitet + Proton-, DXVK-, VKD3D- og synkroniseringsalternativer for dette spillet. + Bruker Protonen fra standardprofilen. + Egendefinert Proton lagret for dette spillet via Steams kompatibilitetstilordning. + Ingen Proton + DXVK + VKD3D-Proton + Bruker komponentpakken fra standardprofilen fra %1$s, med mindre dette spillet velger en annen pakke. + Bruker standardprofilens komponentpakke. + Bruker bare %1$s for dette spillet. + Kjøretidsalternativer + Andre spillspesifikke Proton-, DXVK-, VKD3D-Proton- og synkroniseringsalternativer. + Profilen til %1$s er lagret for neste spilløkt. + Start uten Steam + For et spill som lukkes med én gang: starter det uten Protons Steam-oppsett. Steam-funksjoner som overlay er slått av for det. + På + Proton-valget til %1$s er lagret og tilgjengelig i Steam nå. + Proton-valget til %1$s er lagret. Start Steam på nytt før du starter spillet. + %1$s kan ikke bruke den valgte Protonen på denne enheten. + Kunne ikke laste profilen til %1$s: %2$s + Spillprofilen kunne ikke lagres: %1$s + Nullstill spillprofil + Nullstille profilen til %1$s? + Fjerne dette spillets overstyringer for Proton, FEX, komponentpakke, miljø, teksturfiltrering og Windows-komponenter? Det vil bruke standardprofilen. + %1$s bruker nå standardprofilen. + Spillprofilen kunne ikke nullstilles helt: %1$s + %1$s kjører nå hvert spill som ikke har valgt sin egen Proton i Steam. + %1$s vil kjøre hvert spill som ikke har valgt sin egen Proton, fra neste gang Steam starter. + %1$s kan ikke starte spill her, så den kan ikke være standarden. + Standard-Protonen kunne ikke lagres: %1$s + %1$s · %2$d byttet ut + %1$s · alt originalt + %1$s · spillet kjører + %1$s · droiddeck-esync-pakke + %1$s · ingen droiddeck-esync-pakke ennå + %1$s · esync innebygd, ingen pakke nødvendig + GPU-drivere + %1$s · brukt på nytt ved start %2$s + Neste: %1$s · etter at spillet avsluttes + %1$s i bruk: %2$s + Venter til spillet avsluttes + Gjelder neste gang et spill starter + Importer .zip + Leser Protoner… + Ingen Proton installert ennå. Start Steam én gang for å laste ned dens ARM64-Proton, eller legg til GE-Proton, CachyOS eller DroidDeck-Proton fra Oppsett. + Original · %1$s + %1$s · denne builden + %1$s · tidligere build + Installert · %1$d + Ingenting lagret for %1$s ennå. Denne Protonens egne filer lagres som dens original. + Et spill kjører på denne Protonen, så dette venter til det avsluttes. + Gjelder neste gang et spill starter. + Bytte ut %1$s? + Sette %1$s inn i %2$s? De originale filene forblir lagret. %3$s + Gjenopprette %1$s? + Sette %1$s-originalen tilbake i %2$s? %3$s + Fjerner pakken fra appen. Du kan laste den ned igjen. + Slette denne originalen? + Denne %1$s-originalen er fra en tidligere build av %2$s. Den gjeldende buildens original beholdes alltid. + På Nightlies · %1$d + Oppdater for å liste pakker på Nightlies. + Ingenting nytt siden siste sjekk. + Importer .wcp + Gjenopprett denne Protonens original + Sette %1$ss original %2$s tilbake? + Bytt ut + FEX, DXVK og VKD3D-Proton for hver Proton som Steam kjører spill med.\n\nVelg en installert rad for å bytte den inn. Det gjelder neste gang et spill starter, eller etter at det kjørende spillet avsluttes.\n\nValgene dine kontrolleres på nytt før hver start og settes tilbake hvis noe har endret dem. Hver Protons originale filer beholdes, så Steam-oppdateringer mister dem aldri. + Original + I bruk + Ny + Lagret + Ytelse + CPU-kjerner, sesjonsfikser og vertsnavnet. Gjelder fra neste sesjon. + Steam-klientens kjerner + Overstyr Steams kjernevalg + Fester Steam, dets grensesnitthjelper og gamescope til kjernene under. + Klientkjerner + Kjerner for Steam. + Slå på overstyringen for å velge. + Spillkjerner + Spillkjerner + Kjerner for spill. Med alle valgt, planlegger Android fritt. + Klientgrensesnitt + Trådet GL + Kan gjøre Steam-menyer mer responsive. + Zink: lazy descriptors + Anbefales for drivere uten descriptor buffers. + Hopp over GL-feilkontroller + Slår av GL-validering per kall. + gamescope: realtime GPU-kø + Prioriterer kompositørens GPU-arbeid. Kan jevne ut bildetakten, på bekostning av spillene. + GPU + Hold GPU-en på toppfrekvens + Bare Adreno. Færre hikk, mer batteribruk og varme. + Sesjonsfikser + Turnip: sysmem-rendering + Raskere for Steam og de fleste spill. Påkrevd på Adreno 710/720/722. + Hopp over Steams xalia-hjelper + droiddeck-ntsync (eksperimentell) + En erstatning i brukerrom for Linux-driveren ntsync, for testing. Mens dette er på, bruker Proton den i stedet for droiddeck-esync og droiddeck-fsync. Fungerer med Proton Experimental ARM64, GE-Proton og proton-cachyos. Gjelder fra neste sesjon. + droiddeck-esync + droiddeck-fsync først + Kjører Protons egen fsync på de uendrede Proton-filene, med futex_waitv levert i brukerrom, så den trenger ingen pakke og fortsetter å fungere i det Steam oppdaterer Proton. Mens dette er på, bruker spill den foran droiddeck-esync, med unntak av de få Proton holder utenfor fsync. Gjelder fra neste sesjon. + Spillenes Windows-tråder venter på hverandre gjennom eventfd og delt minne i stedet for rundturer via wineserver. Bruker en droiddeck-esync-pakke laget for nettopp den Proton-builden; pakker hentes automatisk. Til det finnes en for en ny Proton-build, steppes droiddeck-fsync inn i stedet. Gjelder fra neste sesjon. + Slår av Protons hjelper for kontrollernavigasjon, som koster CPU i hvert spill. + Kjør proot uten seccomp + Kan fikse feil om manglende systemkall på noen kjerner. Kan redusere ytelsen. + proot-hurtigvei + Krever prootens seccomp-filter. + Raskere filoppslag og mindre hakking. Slå av hvis et program ikke finner filene sine. + Sesjonsidentitet + Vertsnavn + Navnet Steam ser for denne enheten. Tomt gjenoppretter %1$s. + Bare bokstaver, tall og bindestreker inni navnet, opptil 63 tegn. + Android er satt til å avslutte denne sesjonen + Installert + Tilgjengelig for nedlasting + Ingenting nytt siden siste sjekk. + Gjenopprett innebygde drivere + Innebygd i appen: dette fjerner de utpakkede filene og skjuler den. Gjenopprett innebygde drivere bringer den tilbake. + Halvparten av en Android + Linux-pakke: begge halvdelene fjernes. En driver i bruk faller tilbake til standarden. + Filene fjernes fra appen. + Filene fjernes. Den er i bruk, så standarden tar over. + Skjerm + Effekter + Kontroller + Komponenter + Kontroller + Innstillinger + %1$s-side + Steam + Skrivebord + Steam-meny + Hurtigmeny + Åpne Hurtigmenyen + Forrige side + Neste side + Ytelses-HUD + Skaleringsmodus + Skaleringsskarphet + Skjermeffekter + Live. Koster litt GPU-tid mens en effekt er på. + Forhåndsinnstillinger + Egendefinert + Avansert + Utvidet + Sammenslått + Skarphet (CAS) + Skarphetsnivå + Falsk HDR + Fargebåndfjerning + Styrke på fargebåndfjerning + Lysstyrke + Kontrast + Gamma + Metning + FXAA (kantutjevning) + Tegnefilm-konturer + CRT + NTSC + Teksturfiltrering + DirectX 9–11-spill, fra neste gang de starter. + Anisotropisk filtrering + Teksturskarphet + En negativ mip-skjevhet: skarpere teksturer, litt glitring. Auto følger hvor mye sesjonen forstørres til skjermen. + Auto (%1$s) + Tastatur + PC-tastatur + Android + Andre skjerm + Modus + Viser + Android-apper + Start en app + Skjul + Vis + Ingen startbare apper + Sesjonsoppførsel + Gjelder nå og for fremtidige sesjoner i denne modusen. + Neste sesjon + Support + Sesjonslogger + Legg ved en feilrapport + Del logger + Bakgrunn + Avslutt + Avslutte sesjonen? + Kjører nå + Ingen spill kjører på en Proton + Spillprofil + Profildetaljer er ikke tilgjengelige ennå. + Kunne ikke laste spillprofiler. Prøver automatisk igjen. + Kunne ikke oppdatere spillprofiler. Viser de siste tilgjengelige dataene; prøver automatisk igjen. + Se og rediger de lagrede innstillingene som brukes neste gang. + Bruker standardprofilen + Egendefinert profil + Bruker %1$s fra standardprofilen. + Endringer lagres i profilen til %1$s og gjelder neste gang spillet starter. + + %1$d spilloverstyring + %1$d spilloverstyringer + + Ingen Proton er installert ennå. + spill kjører + brukt på nytt ved start %1$s + Original + Importer og slett på Komponenter-siden. + Ferdig + Krever oppmerksomhet + Arbeider + Kontroller IP-adressen og porten for trådløs feilsøking, eller par på nytt hvis Android har fjernet denne enheten. + Installer + Behandle + Oversikt + Kontroller + Sesjon + Launcher + Oppsett + ● Alt klart + %1$d av %2$d klare + Systemkontroll + Enheten er støttet + Utestet GPU + GPU ikke støttet + %1$s · %2$s + %1$s: %2$s + Steam krever en Adreno-driver; %1$s kan vise en svart skjerm + Linux-kjøretid + %1$s · %2$d%% + Ikke installert · omtrent 3 GB, installeres ved første Spill + Installert + %1$s · oppdatering tilgjengelig + %1$s · oppdatert + Grense for underprosesser + På · Android kan avslutte Steam. Det tar et minutt å slå den av. + Ikke angitt · Android kan avslutte Steam. Det tar et minutt å slå den av. + Kunne ikke kontrolleres · det tar et minutt å slå den av. + Fiks det + Detaljer + Utvikleralternativer + Bruk trådløs feilsøking + Kontroller igjen + Slå på grensen + Steam-konto + Innlogget som %1$s · frakoblet modus + Innlogget som %1$s + Trykk på Spill og logg inn på Steam + Verktøy + Kontroller + Kontrollerinnstillinger er ikke tilgjengelige. + Sesjon + Sesjonslogger + Lagret etter hver sesjon + Feilsøkingsverktøy + Nyeste sesjonslogger + Lagrede sesjonslogger + Beholder de %1$d nyeste + Tøm logger + Frakoblet modus + Logg inn på Steam først + Launcher + Språk + Steam bruker det også, fra neste gang den starter. + Systemstandard (%1$s) + Systemstandard + Appskalering + Tekst og kontroller i DroidDeck + %1$d%% (Standard) + Tema + Launcherens utseende + Bruk som Hjem-skjerm + DroidDeck kan være Hjem-appen + DroidDeck tilbys aldri som en Hjem-app + Retning + Automatisk liggende + Liggende + Omvendt liggende + Fullskjerm + Skjuler Androids status- og navigasjonslinjer + Viser Androids status- og navigasjonslinjer + Standard Hjem-app + Velg en Hjem-app + Velg + Linux-apper (beta) + Filer + Bla gjennom og behandle filer + Proton-versjoner + Installer ARM64 Proton-builder + Ytelse + Tildeling av CPU-kjerner + ROM-mappe + Hvor emulatorspill lagres + Komponenter + FEX, DXVK, VKD3D + Kontroller + Knappetilordning + Utforsk + Søk + Installert + %1$s · %2$d + Butikk + Flathub · ARM64 + ● Flatpak klar + Installer Linux-kjøretiden fra Oppsett først. + Sett opp Flatpak + Legger til Flatpak og kobler det til Flathub (omtrent 20 MB). Den første appen laster også ned kjøretiden sin. + Setter opp… + Flatpak + Oppdateringer + %1$s · %2$s + %1$s · %2$s · %3$d%% + Starter… + Kategorier + Kunne ikke nå Flathub. Kontroller tilkoblingen og prøv igjen. + Prøv igjen + Søk på Flathub + Firefox, spill, emulatorer… + Søk + Alle + Søker… + Søk etter navn eller velg en kategori. Bare ARM64-apper er listet. + Ingen ARM64-apper på Flathub matcher det. + Resultater + Se etter oppdateringer + Oppdater alle (%1$d) + ● Alt er oppdatert + Ingen apper ennå. Finn noen under Utforsk eller Søk. + Sett opp Flatpak for å installere apper. + Oppdatering tilgjengelig · %1$s + Åpne + Detaljer + Fjern + Trykk igjen for å fjerne + Installerer… + Installer + Sett opp Flatpak først + %1$s nedlasting + ● Installert + Kunne ikke laste denne appens Flathub-side. + Laster fra Flathub… + Ingen ARM64-build på Flathub, så den kan ikke installeres her. + Skjermbilder + Om + App-ID + Lisens + Kjøretid + Installert størrelse + %1$s (pluss den delte kjøretiden) + Nettside + Verifisert + Oppdateringer + DroidDeck %1$s · %2$s + Sjekket %1$s + Ikke sjekket ennå + La DroidDeck oppdatere seg selv + På neste skjerm, slå på Tillat fra denne kilden, og kom så tilbake. Oppdateringen fortsetter av seg selv. + Åpne innstillinger + Ikke nå + Signert annerledes + Kan ikke oppdatere direkte + Denne kopien er ikke signert med DroidDecks utgivelsesnøkkel, så oppdateringer kan ikke installeres over den. Avinstaller den for å bytte. + Oppdatering tilgjengelig + Kan ikke installere + Klar til å bytte + Foran Stable + Du er foran Stable + Denne builden er nyere enn den nyeste Stable. Du flyttes til Stable med dens neste utgivelse. + Testen er avsluttet + Denne testen er avsluttet + Fiksen ble slått sammen eller forkastet. Følg Preview for de nyeste fiksene. + Oppdatert + Du har den nyeste %1$s + · %1$s + %1$s… %2$d%% + %1$s… + Oppdater%1$s + Installer%1$s + Installer Stable %1$s likevel + Følg Preview + Avslutt den kjørende sesjonen for å oppdatere. + Mindre + Mer + DroidDeck %1$s + Ny Preview-build + PR #%1$d-testbuild + %1$d MB + OPPDATERINGSKANAL + Stable + Testede utgivelser, for de fleste + %1$s · %2$s + Preview + Nyeste fikser, før Stable + Oppdatert + Testbuilder + Prøv en fiks før den blir utgitt + Ingen akkurat nå + PR #%1$d + Ikke bygget for denne kopien av DroidDeck + Oppdatert %1$s + PR #%1$d-testen + en stund siden + akkurat nå + + %1$d å prøve + %1$d å prøve + + + Endringer siden din build · %1$d build bak + Endringer siden din build · %1$d builder bak + + + Nylige endringer · %1$d build + Nylige endringer · %1$d builder + + En Android-innstilling å endre + Steam + Android stopper apper som starter mange bakgrunnsprosesser, og Steam starter dusinvis. Til dette endres, avsluttes spill uten en feilmelding. + Fiks det for meg + Slå på «Disable child process restrictions», og kom så tilbake. La Utvikleralternativer stå på, ellers slår bryteren seg av igjen. + Åpne Utvikleralternativer + Denne Android-versjonen har ingen bryter for det, så DroidDeck endrer det via trådløs feilsøking. Tar omtrent ett minutt, ingen datamaskin nødvendig. + Trådløs feilsøking krever Wi-Fi. Hvilket nettverk som helst fungerer; det krever ikke internett. + Koblingskoden kommer i en varsling. Tillat varslinger for DroidDeck, eller bruk Andre måter. + Tillat varslinger + Åpne Innstillinger → Utvikleralternativer → Trådløs feilsøking og slå det på + Trykk på «Par enhet med koblingskode» + Skriv koden inn i DroidDeck-varslingen + Venter på koblingsvarselet… + Varsel funnet. Skriv inn koden i varslingen. + Status + Kontrollerer hvert 2. sekund. Denne siden lukkes når det er av. + Andre måter + Trådløs feilsøking, manuell adresse, ADB fra datamaskin + Manuell adresse, ADB fra datamaskin, Utvikleralternativer + Bruk trådløs feilsøking i stedet + Noen ROM-er legger til bryteren likevel. Se etter «child process» i Utvikleralternativer. + Hvis varslingen ikke fungerer, skriv inn varselets adresse og kode selv. + Skriv inn adresse manuelt + Fra en datamaskin med ADB: + Kopier kommando + Velg hvilken skjerm Android-innstillinger skal åpnes på. + Hovedskjerm + Koblet sammen. Finner porten for trådløs feilsøking… + Koblet sammen. Skriv inn porten fra Innstillinger. + Enhet funnet. Bruker innstillingen… + Kunne ikke bruke den automatisk. Kontroller adressen under og prøv igjen. %1$s + Trådløs feilsøking + Grensen for underprosesser er oppdatert + Endre grensen for underprosesser + Hvis alternativet er borte, par over trådløs feilsøking. + Koblet sammen. Kontroller IP-adressen og porten i Trådløs feilsøking. + Grensen for underprosesser ble oppdatert. + Bruk steg 2 hvis Utvikleralternativer ikke har en innstilling for underprosesser. Åpne Utvikleralternativer → Trådløs feilsøking → Par enhet med koblingskode. + Koblet sammen. Bruk adressen under, eller oppdater den fra Trådløs feilsøking → IP-adresse og port. + Grensen for underprosesser ble oppdatert via trådløs feilsøking. + Android bekreftet at grensen er på. + Android bekreftet at grensen er av. + 2 · Koble sammen trådløs feilsøking + IP-adresse og port for koblingsvarsel + Koblingskode (PIN) + 6 tall + Koble sammen + 2 · Bruk via trådløs feilsøking + IP-adresse og port for tilkobling + Koble sammen igjen + Slå av grensen + Andre måter å endre det + 1 · Utvikleralternativer (prøv først) + Slå på «Restrict child processes». + Slå av «Restrict child processes». + 3 · ADB fra datamaskin (siste utvei) + Bare hvis trådløs feilsøking ikke er tilgjengelig. + 3 · ADB fra datamaskin (siste utvei) + Når skjermkontrollene vises i en sesjon + Farge + Fargetone på skjermknappene + Egendefinert + Opasitet + %1$d%% + Knappestørrelse + 100% er standardstørrelsen + Stikke-klikk + Dobbelttrykk og hold en stikke for L3 eller R3 + Adaptive stikker + Spillvibrasjon + Steam-knapp + QAM-knapp + Tastatur-knapp + Trykk på Select for å åpne Steam-menyen + I stedet for View/Select. + Animasjoner + Stikker vises der du trykker og skjules når du slipper + Oppsett + Egendefinerte posisjoner lagret + Tilpasset denne skjermen + Rediger + Nullstill + Knappetilordning + Hver knapp sender sin egen inndata + %1$d av %2$d knapper omtilordnet + Konfigurer + Nullstill kontroller + Gjenoppretter alle kontrollerinnstillinger og oppsettet + Hva hver skjermknapp sender til spillet. Skjult fjerner knappen. + Skjermknapper + Standardverdier + Nullstill tilordning + Android-apper + %1$d apper + Fant ingen startbare Android-apper. + ● Skrivebord installert + Installeres ved første åpning + Tilgjengelig for installasjon + Ikke installert. + %1$s · ikke installert + Åpne %1$s + Spill + Velg en ROM-mappe. + Bla til /root/ROMs i RetroArch. + Legg %1$s-spill til i ROMs/%2$s. + Installer %1$s + Avslutt sesjonen for å installere + Kjøretid nødvendig + Laster installasjonsdetaljer… + Installasjonsdetaljer er ikke tilgjengelige. Prøv igjen senere. + Installer Linux-kjøretiden fra Oppsett først. + Avslutt den kjørende sesjonen for å installere skrivebordsapper. + Installerer %1$s i Linux-skrivebordet. + %1$s · %2$d%% + Å skjule den fjerner den fra Skrivebord. Filene ligger fortsatt i Linux-kjøretiden. + Det spillet er ikke lenger i ROM-mappen. + Skrivebord · %1$s + Start i %1$s + Også i %1$s + Den siden finnes ikke lenger. Velg en del til venstre. + Åpne skrivebord + Installer og åpne skrivebord + Avinstaller skrivebord + Fjern skrivebordet og appene det kom med. Behold spill, tillagte apper, innstillinger og delte biblioteker. + Fjerner skrivebord + Skrivebord fjernet + Fjerner gamle LXQt-filer + Oppdater DroidDeck før du fjerner denne skrivebordsversjonen. + Linux-skrivebord + KDE Plasma, Firefox og emulatorene dine + Spill du installerer i Steam, vises her etter sesjonen. + Spill Steam + Startinnstillinger + Start + Spillfiler + Proton-prefiks + Ser etter lagringer… + Fant ingen lagringsmappe på navn. Eksport tar hele brukermappen. + %1$d lagringsmapper + %1$s · %2$d filer · %3$s + %1$d KB + Behandle lagringer + Spilllagringer + Importer lagringer… + Eksporter som GameHub-zip… + Eksporter som Winlator-zip… + Åpne lagringsmappe + Aldri spilt + Kjøretid opptatt · %1$d%% + Kjøretid opptatt + Kjøretiden installeres ved første Spill + Kjøretidsoppdatering tilgjengelig + ● Kjøretid klar + Sist spilt %1$s + Intern lagring + Lagt til spill + Egendefinert spill + Spillagring + Det beste paret for denne GPU-en, holdt oppdatert + Velg et par selv + Ser etter drivere… + Ingen drivere listet ennå. Oppdater for å se etter dem. + Skjul drivere for andre GPU-er + Vis drivere for andre GPU-er + Avansert + Angis sammen med skjermdriveren: %1$s bærer begge. + Angis sammen med kjøretidsdriveren: %1$s bærer begge. + Brukes av spill og skrivebordet. + Kjøretidsstandard + Tegner bilder til skjermen. Gjelder etter at DroidDeck starter på nytt. + Auto – valgt etter GPU + Last ned, importer eller slett… + I BRUK + Driverne innebygd i DroidDeck + Kjøretids- og skjermdrivere angitt sammen som et matchet par. + Anbefalt for denne GPU-en: %1$s. + Oppdater for å se etter drivere for denne GPU-en. + ANBEFALT + Ikke tilgjengelig + Linux-kjøretid + Gjør Linux-kjøretiden klar + Sesjon + Starter skjerm og lyd + Steam-klient + Kontrollerer Steam og kompatibilitetsverktøyene + Åpner Steam + Venter på Steams første bilde + Skrivebord + Gjør skrivebordet klart + Åpner + Venter på det første bildet + Starter Steam + Sesjonen ble avsluttet + Steg %1$d av %2$d + Steam er pauset + Grep + Venstre plate + Høyre plate + Begge + Kontroller + Stikke + Styreflate + Bytt mellom stikke og styreflate + Flere Deck-kontroller + PANELER + Lukk kontroller for andre skjerm + Åpne Steam-menyen + Venstre · øvre + Venstre · nedre + Høyre · øvre + Høyre · nedre + Klikk + Høyre styreflate + Venstre styreflate + Styreflate + Vis tastatur + Skjul tastatur + Linux-terminal + Linux-sesjonen er ikke klar for en terminal ennå. + Steam + Spill + Skrivebord + Butikk + Komponenter + Apper + Oppsett + Oppdateringer + %1$d%% + Gjenoppta %1$s + Vises som /root/ROMs i hver sesjon. Intern lagring er på /root/Storage. + Ingen mappe valgt + Gjelder fra neste sesjon. SD-kort fungerer. + Velg mappe + Endre mappe + Installer en ARM64 Proton-build, og velg den så per spill i Steam → Egenskaper → Kompatibilitet. + Avslutt den kjørende sesjonen for å installere eller fjerne et kompatibilitetsverktøy. + Tilgjengelige builder + Installert %1$s + Venter fra en tidligere forespørsel + Ikke installert + Oppdatering tilgjengelig → %1$s + Fjern gamle %1$s-versjoner + Fjerner %1$s. Spill satt til disse versjonene må byttes til en annen Proton. + Spill satt til denne versjonen må byttes til en annen Proton. + Installer nå + Avbryt i kø + Android-tastatur + Skjul ✕ + Start %1$s + Velg en skjerm. Sekundær: %1$s. + Den sekundære skjermen er ikke tilgjengelig lenger. + Hovedskjerm + Sekundær skjerm + Forrige fane + Neste fane + På + Alle %1$d + Ingen + %1$d av %2$d + Av + Nedre skjerm + Gjelder fra neste spilløkt. En spillprofil overstyrer den delte. + Et annet spill etter app-ID… + Ferdig + Standard + Av + Endre… + Endre variabel + Tilbake til standard + Proton-spill + Bilde-i-bilde + Gå automatisk inn i PiP + Kontroller på den utbrettede skjermen + Utbrettet sitter spillet over Steam Deck-styreflatene og bakgrepene. Sammenslått er spillet i fullskjerm. + Satt på pause + Bilde-i-bilde er tilgjengelig når sesjonen er klar. + Tillat bilde-i-bilde for DroidDeck i Android-innstillingene. + Android kunne ikke åpne bilde-i-bilde. + Snarvei + Legg til på hjem-skjermen + Velg spill + Skanner installerte spill… + Kopier oppstartslenke + Oppstartslenke kopiert + Denne launcheren kan ikke legge til snarveier. Bruk Kopier oppstartslenke i stedet. + Ugyldig oppstartslenke for spill. + Dette spillet er ikke lenger i DroidDeck-biblioteket ditt. + Avslutt den nåværende sesjonen før du starter et Steam-spill. + Kunne ikke sende spillet til Steam. Prøv å starte det igjen. + Prøv fjerning igjen + Fjerner + Fjerning ikke fullført + Eksporter spillfil + Synkroniser alle spillfiler til mappe + Stopp synkronisering av spillfiler + Velg en mappe for frontend-spillfiler + Spillfiler lagret i %1$s + Frontend-filer + + Vent til kjøretidsoperasjonen er ferdig + Vent til installasjonen av kompatibilitetsverktøyet er ferdig + Vent til installasjonen av skrivebordsappen er ferdig + Vent til installasjonen er ferdig før du starter et spill + Lukk Steam-sesjonen først, så spillet ikke kan lagre over importen + Importerer til %1$s + + Importerte %1$d fil fra %2$s til %3$s + Importerte %1$d filer fra %2$s til %3$s + + + Importerte %1$d fil fra %2$s til %3$s. Gamle lagringer sikkerhetskopiert til Download/DroidDeck/Saves/backups + Importerte %1$d filer fra %2$s til %3$s. Gamle lagringer sikkerhetskopiert til Download/DroidDeck/Saves/backups + + Velg en lagrings-zip for %1$s + Eksporterer %1$s + + Eksporterte %1$d fil som en %2$s: %3$s + Eksporterte %1$d filer som en %2$s: %3$s + + Velg hvor du vil lagre %1$s (%2$s) + %1$s mislyktes: %2$s + Ingen sesjonslogger ennå: kjør en sesjon først. + Ingen sesjonslogger å tømme. + + Tømte %1$d sesjonsloggmappe. + Tømte %1$d sesjonsloggmapper. + + Kobling for trådløs feilsøking mislyktes + Kommando for trådløs feilsøking mislyktes + Trådløs feilsøking er ikke tilgjengelig + ADB-kommando kopiert + Velg ROM-mappen + Ikke en Adreno-GPU + Turnip støtter Adreno-GPU-er. På %1$s kan Steam vise en svart skjerm. Nedlasting: %2$.0f MB. + Installer likevel + Fjern Linux-kjøretid + Dette sletter kjøretiden, Steam-klienten inni den, og hvert spill installert der. + Brukt: %1$s + Kunne ikke åpne %1$s på hovedskjermen + Kunne ikke åpne %1$s på %2$s + Kunne ikke åpne %1$s på skjerm %2$d + Importerer + Bytter ut + Gjenoppretter + Avbryter + Sletter + kan ikke lese filen + Importerte %1$s + Den ventende byttingen ble avbrutt. + Velg en komponentpakke (-linux .wcp) + Kunne ikke nå Nightlies + Lagret %1$s + Nedlasting mislyktes: %1$s + Velg en Linux-kjøretidsdriver (-Linux-zip eller Android + Linux-pakke) + Velg en skjermdriver (AdrenoTools-zip eller Android + Linux-pakke) + Velg en driver-zip eller en Android + Linux-pakke + %1$s: %2$s + Fjerner %1$s… + Glemmer %1$s… + Velg spillagringsmappen + Velg en Decky-programtillegg-ZIP + Ikke brukbar: %1$s + Program + Fjerner Linux-kjøretid + Kunne ikke åpne Innstillinger på den nedre skjermen; åpnet den på hovedskjermen. + Åpne Utvikleralternativer i Innstillinger + + Kunne ikke åpne Androids innstilling «Installer ukjente apper» + Kunne ikke sjekke: %1$s + Denne builden har ingen nedlasting for denne kopien av DroidDeck + Avslutt den kjørende sesjonen først, og oppdater så + Laster ned + Installerer + Venter på at du bekrefter + Oppdateringen mislyktes + Android tilbød ikke å installere oppdateringen + + Decky-installasjon mislyktes: %1$s + Starter import av programtillegg… + Import av programtillegg mislyktes: %1$s + Programtillegg installert. Start Steam på nytt for å laste det. + + Ikke sjekket ennå – trykk oppdater for å se etter nye drivere + Android + Linux: setter skjermdriveren med den + Android + Linux: setter kjøretidsdriveren med den + Turnipen innebygd i kjøretiden + system-Vulkan: ingen bunnet build for denne GPU-en + %1$s (medfølgende) + Manuell + %1$s · %2$s + Bare den ene halvdelen er publisert akkurat nå + %1$.0f MB for begge + Ingen drivere å angi: %1$s + Ser etter de nyeste driverne… + Oppdatert (kunne ikke sjekke: %1$s) + De anbefalte driverne er ikke publisert akkurat nå + Laster ned %1$s %2$s… + Oppdatert til %1$s %2$s. + Oppdatert til %1$s %2$s. Skjermdriveren gjelder etter at DroidDeck starter på nytt. + Byttet til %1$s %2$s. + Byttet til %1$s %2$s. Skjermdriveren gjelder etter at DroidDeck starter på nytt. + Bruker %1$s %2$s. + Bruker %1$s %2$s. Skjermdriveren gjelder etter at DroidDeck starter på nytt. + Ikke en driver-zip: verken en AdrenoTools-driver, en -Linux Turnip eller en Android + Linux-pakke + Import mislyktes: %1$s + Importert driver + Bruker %1$s som både kjøretids- og skjermdriver. + Bruker %1$s som både kjøretids- og skjermdriver. Skjermdriveren gjelder etter at DroidDeck starter på nytt. + Slettet %1$s (begge driverne) + Slettet %1$s + Nyeste: %1$s · sjekket %2$s + Nyeste: %1$s · %2$s utilgjengelig · sjekket %3$s + + %1$d min siden + %1$d min siden + + + %1$d t siden + %1$d t siden + + + %1$d dag siden + %1$d dager siden + + Sjekker Banners-Turnip, WinNative og DroidDeck… + sjekk mislyktes + Installert %1$s – velg den i begge lister for å angi begge driverne + Installert %1$s – velg den i menyen + + Vent til fjerning av kjøretiden er ferdig + Spill + styreflate + direkte + Bytting mislyktes: %1$s + Lagring av profil mislyktes: %1$s + Skrivebordet er pauset + %1$s er pauset + Programmet er pauset + Starter skrivebordet + Starter %1$s + Starter programmet + Ingen logger for denne sesjonen. + Kunne ikke opprette sesjonsloggarkivet. + Kunne ikke dele sesjonsloggene. + Starter sesjonen… + laster ned Linux-kjøretiden + laster ned Linux-kjøretiden · %1$d av %2$d MB + kontrollerer Linux-kjøretiden + pakker ut Linux-kjøretiden + laster ned skrivebordet + laster ned skrivebordet · %1$d av %2$d MB + kontrollerer skrivebordet + pakker ut skrivebordet + laster ned Proton Experimental (ARM64) + laster ned Proton Experimental (ARM64) · %1$d av %2$d MB + kontrollerer Proton Experimental (ARM64) + pakker ut Proton Experimental (ARM64) + Kunne ikke nå kjøretidskatalogen. Kontroller tilkoblingen og trykk Spill igjen. + Linux-kjøretiden ble ikke installert. Kontroller kjøretiden i Oppsett og trykk Spill igjen. + Skrivebordet ble ikke installert (%1$s). Kontroller tilkoblingen og trykk Skrivebord igjen. + %1$s stoppet uventet. Del loggene med en feilrapport, eller prøv igjen. + Programmet stoppet uventet. Del loggene med en feilrapport, eller prøv igjen. + Skrivebordet stoppet uventet. Del loggene med en feilrapport, eller prøv igjen. + Avslutningsstatus %1$d · %2$s + ingen logg + + Loggen viser %1$d «Function not implemented»-feil: prootens seccomp-akselerasjon feiler en hjelper på denne enheten. Prøv %2$s → «%3$s» (eller «%4$s») og start på nytt. + Loggen viser %1$d «Function not implemented»-feil: prootens seccomp-akselerasjon feiler en hjelper på denne enheten. Prøv %2$s → «%3$s» (eller «%4$s») og start på nytt. + + + Programtillegg + Installer en programtillegg-ZIP for neste Steam-sesjon. + Laster ned… + Installer fra ZIP + Spill + Lyd og video + Grafikk + Internett + Kontor + Utvikler + Utdanning + Vitenskap + System + Verktøy + Populært + Trender + Nytt + Oppdatert + laster… + Flatpak-oppsett + %1$s: %2$s + Kunne ikke sjekke Flathub for oppdateringer + installert + mappe + Grafitt + Grafittflater, med logoens egen kuleblå farge for alt som er valgt, fokusert eller trykket. + Papir på svart + Hvitt er primærfargen, ikonets blå er signalet for linjer, ringer og prikker. + Ikonblå + Svart bakgrunn, hvit skrift, det blå for alt som er valgt, fokusert eller trykket. + Elektrisk marineblå + Det blå lysere, på flater som lener mer mot marineblått enn grått. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Installer kjøretiden først + Steam-biblioteket er ikke tilgjengelig + Det forrige SD-biblioteket er ikke tilgjengelig + Spillagringen er ikke tilgjengelig + Mappen for tillagte spill er ikke tilgjengelig + Steam-manifestene er ikke tilgjengelige + Mange systemer + Intern + Linux-system + SD-kort + Lagring + Velg + Egenskaper + Legg til i favoritter + Fjern fra favoritter + Gi nytt navn + Kopier + Klipp ut + Handlinger + Favoritter + Fest denne mappen + Ingen favoritter ennå – fest en mappe med ⋮-menyen for å komme raskt tilbake hit. + Fjern fra favoritter + 0 byte + byte + Plassering + Type + Størrelse + Endret + Mappe + Fil + %1$s-fil + Skrivebeskyttet + Stopper spill (og Wine) fra å overskrive eller slette denne filen. + Kunne ikke endre Skrivebeskyttet + Skjult + Denne lagringen kan ikke lagre den skjulte-flagget. + Markerer filen som skjult i Windows (Wines skjult-attributt). + Kunne ikke endre Skjult på denne lagringen + Ferdig + Sletter… + Sletting mislyktes + Flytter… + Kopierer… + Flytter – %1$s + Kopierer – %1$s + Flytter %1$d/%2$d – %3$s + Kopierer %1$d/%2$d – %3$s + %1$d ferdig, %2$d mislyktes + %1$d ferdig, %2$d hoppet over + + %1$d element flyttet + %1$d elementer flyttet + + + %1$d element kopiert + %1$d elementer kopiert + + «%1$s» finnes allerede + Gir nytt navn… + Kunne ikke gi nytt navn + Oppretter mappe… + Kunne ikke opprette mappe + Ny mappe + Mappenavn + Opprett + Nytt navn + Slette? + Slette «%1$s» permanent? + + Slette %1$d element? + Slette %1$d elementer? + + Dette kan ikke angres. + + …og %1$d til + …og %1$d til + + Sletter %1$d/%2$d – %3$s + + %1$d kunne ikke slettes + %1$d kunne ikke slettes + + Slå sammen legger til og erstatter filer inne i den eksisterende mappen. + Overskriv erstatter den eksisterende filen. + Bruk på alle konflikter + Slå sammen + Overskriv + Behold begge + Hopp over + Velg denne mappen + Tilbake + %1$s er montert, men ikke lesbar akkurat nå + Listevisning + Rutenettvisning + Søk + Sorter + Navn + Dato endret + Luftige rader + Kompakte rader + Skjul skjulte filer + Vis skjulte filer + Skjul favoritter + Vis favoritter + Filtrer denne mappen + %1$s ledig + %1$d valgt + Ingen + Alle + Lim inn + + %1$d element + %1$d elementer + + Lim inn %1$s her + Lim inn %1$s (flytt) her + Filer + LAGRING + RASK + FAVORITTER + Nedlastinger + Nedlast + ROM-er + Sesjonslogger + Bilder + La til «%1$s» i favoritter + Fjernet «%1$s» fra favoritter + Tom mappe + Opp ett nivå + Velg en mappe + Velg en fil + Steam-blå + Himmel + Hvit + Fiolett + Grønn + Amber + Rød + Rosa + A-knapp + B-knapp + X-knapp + Y-knapp + Venstre skulderknapp + Høyre skulderknapp + Venstre trigger + Høyre trigger + View-knapp + Menu-knapp + View + Menu + D-pad opp + D-pad ned + D-pad venstre + D-pad høyre + Skjult + Dra for å flytte + Fest + Lagre + Ingen + Tastatur + styreflate + Terminal + Deck-grep + styreflater + + Fjerner Linux-kjøretid + + Fjerner Linux-kjøretid · %1$d oppføring + Fjerner Linux-kjøretid · %1$d oppføringer + + Linux-kjøretiden fjernet + Kunne ikke fullføre fjerningen av Linux-kjøretiden. Prøv fjerning igjen i Oppsett. + Fjerner kjøretidsrester + Laster ned + Verifiserer + Pakker ut + Vises mens Linux-kjøretiden installeres eller fjernes + Installerer Linux-kjøretiden + Fjerner Linux-kjøretiden + + Linux-kjøretiden er ikke installert + Katalogen har ingen sjekksum for %1$s + Sjekksum stemmer ikke – ingenting ble endret + Installerer %1$s + Kunne ikke plassere AppImage-filen + Utpakking mislyktes + Installasjon mislyktes + + Flatpak-oppsett mislyktes (avslutningskode %1$d) + Flatpak-oppsettet ble ikke fullført + En annen butikkoppgave kjører + Sett opp Flatpak først + Starter + Flatpak avsluttet med status %1$d + Installerer %1$s + Oppdaterer %1$s + Fjerner %1$s + Installerer %1$s (%2$d/%3$d) + Oppdaterer %1$s (%2$d/%3$d) + Fjerner %1$s (%2$d/%3$d) + oversettelser + grafikkdrivere + mediekodeker + %1$s-kjøretid + %1$s-kjøretid %2$s + + Filen er for liten til å være en AppImage + Filen kan ikke leses: %1$s + Dette er ikke en AppImage (det er ikke et Linux-program) + Denne AppImage-filen er bygget for verken ARM64 (aarch64) eller x86-PC-er + Dette er et Linux-program, men ikke en AppImage + Bare type 2-AppImages kan importeres (denne er type %1$d) + Ikke nok ledig plass: import krever omtrent %1$s + Ikke nok ledig plass: oppdatering krever omtrent %1$s + Import mislyktes + Den AppImage-filen finnes ikke lenger + Oppdatering mislyktes + Pakker ut %1$s + AppImage-filen kunne ikke pakkes ut + AppImage-filen kunne ikke pakkes ut: %1$s + + Avslutt den aktive sesjonen før du installerer Decky Loader + Nedlasting av Decky Loader mislyktes + Decky Loader-størrelsen stemte ikke med utgivelsens metadata + Kunne ikke laste ned utgivelsens sjekksum + Utgivelsens sjekksum mangler + Utgivelsens sjekksum er ugyldig + Sjekksummen til Decky Loader stemmer ikke + Utgivelsesfilen er ikke en kompatibel 64-bit Linux-kjørbar fil + En sesjon startet under nedlastingen; avslutt den før du installerer Decky Loader + Kunne ikke merke PluginLoader som kjørbar + Kunne ikke slå på Steams CEF-fjernsøking for feilsøking + Kunne ikke klargjøre PluginLoader + Kunne ikke bevare den eksisterende PluginLoader + Kunne ikke aktivere den nye PluginLoader + Avslutt den aktive sesjonen før du installerer et programtillegg + Installer Decky Loader før du importerer et programtillegg + Kunne ikke lese den valgte ZIP-filen + Kunne ikke opprette mappen for Decky-programtillegg + Pakker ut programtillegg-ZIP + Programtillegg-ZIP-en må inneholde én mappe på toppnivå + Programtillegg-ZIP-en mangler plugin.json, package.json eller dist/index.js + Programtilleggets navn mangler + En sesjon startet under installasjonen; avslutt den før du installerer et programtillegg + Programtillegg installert · start Steam på nytt for å laste det + Installasjon av programtillegg-ZIP mislyktes + Programtillegg-ZIP-en inneholder for mange oppføringer + Programtillegg-ZIP-en inneholder en ugyldig sti + Programtillegg-ZIP-en inneholder like stier + Programtillegg-ZIP-en inneholder en utrygg sti + Kunne ikke opprette programtillegg-mappen + Programtillegg-ZIP-en pakker ut til mer enn 1 GiB-grensen + Kunne ikke merke den medfølgende programtilleggsfilen som kjørbar + Ugyldig remote_binary-oppføring + Ugyldig navn på ekstern kjørbar fil + URL-en til den eksterne kjørbare filen må bruke HTTPS + Den eksterne kjørbare filen %1$s har en ugyldig SHA-256-hash + Kunne ikke opprette bin-mappen for programtillegget + Laster ned programtilleggets kjørbare fil · %1$s + Kunne ikke laste ned programtilleggets kjørbare fil %1$s + Sjekksummen til den kjørbare filen stemmer ikke · %1$s + Kunne ikke merke den kjørbare filen som kjørbar · %1$s + Kunne ikke erstatte den kjørbare filen · %1$s + Kunne ikke installere den kjørbare filen · %1$s + Programtillegg-mappen tilhører et annet programtillegg + Kunne ikke bevare det eksisterende programtillegget + Kunne ikke aktivere det importerte programtillegget + Kunne ikke oppdatere innstillingene for Decky-programtillegget + Kunne ikke gjenopprette det eksisterende programtillegget + + Kunne ikke lagre identiteten for trådløs feilsøking + ADB-kommandoen mislyktes (%1$d) + Android bekreftet ikke at grensen for underprosesser ble endret: %1$s + Ingen kobling for trådløs feilsøking er lagret + Skriv inn dagens IP-adresse og port for trådløs feilsøking + + Tidsavbrudd ved venting på koblingsvarselet. Prøv igjen fra DroidDeck. + Kunne ikke se etter koblingsvarselet (%1$d). Kontroller at Wi-Fi er på. + Kunne ikke se etter koblingsvarselet: %1$s + Skriv inn alle 6 tallene i koden + Kobler sammen… + Den koden fungerte ikke. Åpne koblingsvarselet igjen og skriv inn den nye koden. + Koblet sammen. Kobler til… + Koblet sammen, men porten for trådløs feilsøking ble ikke funnet. La trådløs feilsøking stå på og prøv igjen. + Bruker innstillingen… + Kommandoen for trådløs feilsøking mislyktes + Oppsett av trådløs feilsøking + Ber om koblingskoden for trådløs feilsøking mens Innstillinger er åpen + Koble sammen trådløs feilsøking + Slå på trådløs feilsøking, og trykk så på «Par enhet med koblingskode». + Slå på trådløs feilsøking, og trykk så på «Par enhet med koblingskode». Hold deg i Innstillinger: koden kommer i denne varslingen. + 6-tallers koblingskode + Trykk på Skriv inn kode og skriv de 6 tallene som vises i Innstillinger. + Skriv inn Wi-Fi-koblingskoden + Prøv koden igjen + Skriv inn kode + Setter opp Steam + Steam er klar + Trykk for å gå tilbake til DroidDeck. Du kan slå av trådløs feilsøking nå. + Oppsettet for trådløs feilsøking stoppet + + Nedlastingen svarte HTTP %1$d + Nedlastingens størrelse stemte ikke med den publiserte APK-en og ble forkastet – prøv igjen + Nedlastingen stemte ikke med sjekksummen og ble forkastet – prøv igjen + Oppdateringen er for Android-pakken %1$s, ikke %2$s + Oppdateringens metadata nevner ikke DroidDecks utgivelsessigneringsnøkkel + Android kunne ikke lese den nedlastede APK-en + Den nedlastede APK-en er for %1$s, ikke %2$s + Den nedlastede APK-en sier versionCode %1$d, ikke den publiserte %2$d + Android vil ikke installere versionCode %1$d over den installerte %2$d + Den nedlastede APK-en har ingen signeringsinformasjon + Den nedlastede APK-en er ikke signert med DroidDecks utgivelsesnøkkel + Oppdateringskatalogen svarte HTTP %1$d + + Starter sesjonen… + %1$d:%2$02d gått · fortsatt i gang + Laster ned Steam-klientoppdateringen · %1$d%% + Laster ned Steam-klienten · %1$s + + En installasjon av et kompatibilitetsverktøy kjører allerede + Avslutt den aktive sesjonen før du installerer kompatibilitetsverktøy + Fant ingen ARM64-utgivelse for %1$s + Minst 4 GB ledig pluss nedlastingens størrelse er nødvendig for å installere %1$s + Laster ned %1$s %2$s + Prøver nedlastingen igjen · forsøk %1$d av %2$d + Nedlastingen ble avbrutt + Nedlastingen mislyktes; den delvise filen beholdes slik at et nytt forsøk kan fortsette + Nedlastingen overskred utgivelsens størrelse og ble slettet + Nedlastingen var ikke fullført; den delvise filen beholdes for et gjenopptakende nytt forsøk + Verifiserer nedlastingen + Kunne ikke lese utgivelsens sjekksum; prøv igjen + %1$s %2$s publiserer ingen sjekksum; ingenting ble installert + Sjekksummen stemmer ikke; nedlastingen ble slettet + En sesjon startet under nedlastingen; avslutt den før du installerer kompatibilitetsverktøy + Installerer %1$s + Installasjonen av %1$s mislyktes (avslutningskode %2$d) + Kunne ikke installere %1$s + Henter droiddeck-esync-pakke + + med Linux-hjelpere + Bare DLL-er + Original (Proton oppdatert siden siste bytte) + Endret utenfor Komponenter + ikke til stede + Original %1$s + Original fra %1$s + %1$s %2$s settes inn i %3$s når spillet som kjører på den avsluttes. + %1$s %2$s er nå i %3$s. Det gjelder neste gang et spill starter. + Den originale pakken er ikke lagret + %1$s av %2$s går tilbake til sin original når spillet som kjører på den avsluttes. + %1$s av %2$s gjenopprettet fra sin %3$s-original. + %1$s er i bruk i %2$s. Bytt ut den Protonen med noe annet først. + %1$s er ikke lagret + Slettet %1$s. + Originalen til den installerte builden beholdes: det er veien tilbake. + Ikke lagret + Slettet %1$s-originalen av %2$s. + Nedlastinger kommer bare fra Nightlies-utgivelsene + Denne pakkelisten er eldre enn sjekksummene – oppdater den og prøv igjen + Sjekksummen stemmer ikke + + Lineær + Nærmeste + Snapdragon GSR (kvalitet) + Bare skarphet + Auto (følger skalering) + + Ingenting brukt. + Spillklarhet + Skarphet med et snev kontrast – den for hver dag. + Levende + Farger skrudd opp. Fargebåndfjerning på, så himmelen ikke striper. + Filmatisk + Dypere kontrast, kjøligere farger, kanter utjevnet. + Konkurranse + Skygger hevet og kraftig skarphet – se folk først. + Adaptiv skarphet + Bare skarphet, farger urørt. + Filmaktig + Filmlignende kurve, litt dempet. + Arkade + Høylytt og kraftfull. + Retro CRT + CRT-rørets og det analoge signalet går sammen. + Oppskalert skarp + For å kjøre under skjermens oppløsning. + Piksel-ren + Skarpe piksler for 2D og gamle titler. + Anime-kant + Tegnefilm-konturer pluss skarphet. + + Ikke en Adreno-GPU + Støttet + Eksperimentell: driverne er testbuilder + Under testet maskinvare (Adreno 725 og nyere): den kan ikke kjøre + Ikke støttet: DroidDeck krever en Adreno (Snapdragon) GPU + + GitHubs hastighetsgrense ble nådd – prøv igjen senere + GitHub svarte HTTP %1$d + nedlastingen svarte HTTP %1$d + Denne driverlisten er eldre enn sjekksummene – oppdater den og prøv igjen + Sjekksummen stemmer ikke – nedlastingen ble forkastet + Ikke en AdrenoTools-driver: %1$s + ingen meta.json + meta.json kan ikke leses (%1$s) + dette er en Linux-kjøretidsdriver (importer den under «Linux-kjøretidsdriver») + dette er en Wayland-spilldriver, som denne appen ikke har bruk for + meta.json nevner ingen libraryName, så ingenting kunne gis til AdrenoTools + meta.json nevner %1$s, som ikke finnes i zip-filen + %1$s er ikke et 64-bit AArch64 ELF-delt bibliotek + %1$s lenker glibc – en «-Linux»-Turnip, som hører under «Linux-kjøretidsdriver» + Ingen libvulkan_freedreno*.so i denne zip-filen. En Android- (AdrenoTools-) eller -Wayland-Turnip-zip er ikke en Linux-kjøretidsdriver. + %1$s er ikke et 64-bit AArch64 ELF-delt bibliotek. + %1$s er ikke en glibc-driver – den lenker Androids libc. Linux-kjøretiden krever en «-Linux»-zip; en vanlig eller «-Wayland»-Turnip kan ikke lastes av Steam-klienten i det hele tatt. + Ikke en driverpakke: ingen manifest.json som nevner en android- og en linux-driver + Ikke en driverpakke: %1$s peker utenfor den + Ikke en driverpakke: den pakkes ut til mer enn %1$d MB + + SD-kort + Mappe + + Del sesjonslogger + + krever Android 10 eller nyere (skjermlag med et fargerom) + denne skjermen rapporterer ingen HDR-støtte + denne skjermen støtter %1$s, ikke HDR10 + + denne GPU-en + Grensen for underprosesser er på + Grensen for underprosesser er ikke angitt + Grensen for underprosesser kunne ikke kontrolleres + Grensen for underprosesser er av + Grensen for underprosesser ikke nødvendig + %1$s Hvis det ikke er mulig, koble denne enheten til en datamaskin med ADB og kjør kommandoen under. Kom tilbake hit og kontroller igjen. + Steam-sesjoner kan starte. + Denne Android-versjonen bruker ikke denne grensen. + Slå på «Disable child process restrictions» i Utvikleralternativer, og la Utvikleralternativer stå på etterpå. + Denne Android-versjonen har ingen bryter for det. Bruk trådløs feilsøking for å endre det fra denne enheten. + Denne builden har ingen nedlasting for denne kopien av DroidDeck. + Android vil ikke installere den over denne builden fordi dens versionCode %1$d er under den installerte %2$d. + Android vil ikke installere en eldre versjon over en nyere. Bli på denne builden til kanalen tar den igjen. + Denne kopien av DroidDeck er signert annerledes (en lokal build?), så Android vil ikke oppdatere den direkte. + Det er ikke nok ledig plass for oppdateringen. + Oppdateringen ble avbrutt. + Oppdateringen ble ikke installert: %1$s + Oppdateringen ble ikke installert. + %1$s-zip + zip + %1$s har ikke blitt startet ennå + Fant ingen lagringsfiler for %1$s + %1$s har ingen drive_c-mappe, så det er ikke en GameHub- eller Winlator-lagrings-zip + kan ikke opprette mapper i %1$s + kan ikke skrive i %1$s + kan ikke skrive i %1$s (%2$s) + Mappen er ikke tilgjengelig + En annen fil bruker allerede %1$s + Grafikkmappen er ikke tilgjengelig + Mappen har en urelatert synkroniseringsoversikt + Kunne ikke fjerne %1$s + Filen ligger utenfor eksportmappen + FEX (build uten versjonsnavn) + ingen profile.json i %1$s + %1$s er ikke en Linux-komponentpakke (type %2$s) + %1$s · %2$s ledig + Automatisk + Oppdateringskatalogen mislyktes i sine kontroller (%1$s) + DroidDeck-innstilling for underprosesser + Innlogget · frakoblet modus + Innlogget + MEDFØLGER + NEDLASTET + IMPORTERT + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Sesjonsskjerm + Oppløsning + 16:9-spilltilpasning + Hvordan 16:9-spill tilpasses denne skjermen. Fra neste sesjon. + Velg Tilpass skjerm eller en egendefinert størrelse smalere enn 16:9. + Behold proporsjoner + Strekk til skjermen + Bildeskalering + Skaleringsfilter + Delt av Steam og Skrivebord. + Gjelder live. Delt av Steam og Skrivebord. + FSR, GSR og NIS gjelder ved forstørring av sesjonsbildet. Bare skarphet fungerer også uten forstørring. + Filterskarphet + Bildefrekvens + %1$s Setter skalering til %2$s. + Tilpass skjerm (%1$d×%2$d) + Følg skjermen (endrer størrelse ved sammenslåing eller utbretting) + Steam + Vis enhetsdetaljer + Skjul enhetsdetaljer + Leser enheten… + Kopier + Kopier disse detaljene inn i en feilrapport. + Kopiert. + Tvungen SSBS + Holder en CPU-hastighetsinnstilling som Wine stadig slår av. Kan gjøre Windows-spill raskere på Snapdragon 8 Elite; lite endring sett på eldre brikker. Gjelder fra neste spilløkt. + + Butikker + Steam + Egendefinert + Butikker + Åpne en butikk på + Nedlastingshastighetsnivå + Butikkmotor + ● Motor klar + Ikke bygget + Balansert + Rask + Maks + Butikker + Butikkinnstillinger + Innstillinger + (ikke innlogget) + Butikk + Installert + Bibliotek + Alle + Nedlastinger + %1$s (%2$d) + Logg inn + %1$s · logg ut + Logge ut av %1$s? + Installerte spill blir liggende. Biblioteket hentes på nytt når du logger inn igjen. + Fortsett innlogget + Logg ut + Søk i %1$s-katalogen… + Søk i installerte spill… + Søk i biblioteket ditt… + Søk + Ingenting installert + Laster + Ingen spill + Laster butikken… + Ingen katalog + Nytt + Tilbud + Gratisspill + Trender + Biblioteket ditt + Spill + Installer + Gjenoppta installasjon + Avbrutt + Installer · %1$s + Hent gratis + Vis på %1$s + Kjøp · %1$s + Installert + I biblioteket + Gratis + Gjenoppta + Oppdater + Vis innhold for voksne + Epic + EOS + Frakoblet + Av + Epic-innlogging (EOS) + Start frakoblet + Løs Epic-innlogging + Skylagringer + Last opp til skyen + Last ned fra skyen + Ingen skylagringer + Skykonflikt + Skykonflikt · %1$d filer + … + Behold skyversjon + Behold lokal versjon + Kontrollerer skyen… + Siste synkronisering %1$s + Ikke synkronisert ennå + %1$d filer + installert + i biblioteket ditt + butikk + Prestasjoner + %1$d av %2$d låst opp + %1$d / %2$d + Låst opp · %1$d + Låst · %1$d + Skjult · %1$d + Skjult prestasjon + Detaljer vises når den er låst opp. + Alle prestasjoner låst opp + Laster… + Logg inn på %1$s for å se prestasjonene dine. + Kunne ikke nå %1$s. Sjekk igjen når du er på nett. + %1$s har ingen prestasjoner for dette spillet. + Vis alle %1$d + Prestasjoner · %1$s + ✓ I Steam + Avinstaller + Avinstallerer… + Avinstallere %1$s? + Frigjør %1$s og fjerner det fra Steam. + Fjerner filene og Steam-snarveien. + Steam plukker opp spillet neste gang det starter; start det derfra, eller start Steam nå. + Proton + Steams valg per spill + Lyd + DirectAudio + Komponenter + FEX, DXVK, VKD3D + Motor + %1$s · nativ + %1$s · innebygd + Hvordan det kjører + Hentet fra + %1$s, med kontoen din + Installert til + Spillagring (Games/%1$s/%2$s) + Startet av + Steam og Proton, samme sti som Steam-spill + Steam og Proton · Epic-innlogging per start, EOS-overlegg av + Steam og Proton · Amazon Games Services-variabler satt + Steam + lagt til i Steam ved installasjon (Deck-grensesnitt + Steam Input) + Ingen nedlastinger + Pauset + I kø + I kø · #%1$d + Avbrutt + Mislyktes + Mislyktes · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Pause + Gjenoppta + Avbryt + Prøv igjen + Fjern + Fjern fullførte + Manifest + Nedlasting + Verifisering + Installasjon + Ferdig + Kontrollerer + Laster ned + Slette nedlastingen? + Mislyktes · %1$s beholdt + Verifiserer + Installerer + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d filer + Nedlastingsbehandling + Samtidige nedlastinger + Installer til + Installer %1$s: + Intern + %1$s ledig + Butikknedlastinger + Viser nedlastingsfremgang for spill og holder nedlastinger i gang i bakgrunnen + Laster ned + %1$d nedlastinger + Forbereder… + Fullførte butikknedlastinger + %1$s installert + Nedlasting av %1$s mislyktes + Slett + Behold + Logg inn på %1$s + Logg inn med leverandøren din + Last på nytt + Lukk + Prøv igjen + Fullfører innlogging… + Innloggingssiden kunne ikke lastes: %1$s + Denne leverandøren tillater ikke innlogging fra en innebygd side. Logg inn med butikkpassordet ditt i stedet. + Innloggingssiden svarte med en feil (HTTP %1$d). + Tilkoblingen til innloggingssiden er ikke sikker. + Innloggingen kunne ikke verifiseres. Prøv igjen. + Innlogging mislyktes. Prøv igjen. + Lukk + Tilbake + Rediger + Legg til alle spill i denne mappen + Ingenting nytt i denne mappen + Navn + Mål + Start i + Startalternativer + Grafikk + Annen exe… + Fjern + Omslag + Bakgrunn + Logo + Ikon + Spillmappe + Steam + SteamGridDB + Din fil + Fra exe-filen + Automatisk + Velg bilde… + Velg et bilde: %1$s + Tilbakestill til automatisk + + %1$d spill lagt til + %1$d spill lagt til + + + %1$d allerede i Spill + %1$d allerede i Spill + + B · Tilbake til %1$s + spillet + Spillinnstillinger + Grafikkdrivere + Denne Protonen + Hver Proton + For hver Proton + Tilgjengelig · %1$d + Sjekket %1$s + Se etter nye komponenter og drivere + Hent + %1$s · DXVK, VKD3D-Proton og FEX under hører til denne Protonen + %1$s · testet + %1$s · ikke testet + %1$s · ikke støttet + Holder det anbefalte paret oppdatert + Auto · %1$s + Kjøretids- og skjermdrivere separat + Bruke %1$s? + Fjerne %1$s? + Diagnostikk + Klar til å spille + Ikke klar til å spille ennå + kjøretid %1$s + prosessgrense av + innlogget som %1$s + Når noe trenger deg + Utseende + Denne enheten + Biblioteksgrafikk + LINUX-APPER + Flathub + Skrivebordsapper for Linux-skrivebordet, åpnet fra Skrivebord. + Sett opp Flathub + Fjerne %1$s? + Innstillingene og filene beholdes. + Behold + Størrelse på disk + Avinstallere skrivebordet? + Alle + GOG-prestasjoner + Synkroniserer prestasjoner for støttede GOG-spill. Gjelder fra neste oppstart. + Prestasjoner av + Prestasjoner på + Logg inn på GOG igjen + Prestasjoner lastet opp + Opplasting av prestasjon venter + Slå på GOG-prestasjoner i Store-innstillinger + Prestasjoner ikke tilgjengelig; sjekk sesjonsloggene + Prestasjon låst opp + Lim inn + Styrekors + Venstre stikke + Høyre stikke + Meny + Tastatur + Styrekors opp + Styrekors ned + Styrekors venstre + Styrekors høyre + Berøringskontroller + Berøringskontroll + Mus + På + Av + Sakte + Normal + Rask + Rediger oppsett + DroidDeck-meny + Dra kontroller ut på skjermen + Ordne + Forkast + Lagrer… + Ikon + B− + B+ + H− + H+ + Skjul + Farge og opasitet… + Kopier dette oppsettet + Oppsett kopiert; lim det inn i berøringsoppsettet til et hvilket som helst spill + Lim inn kopiert oppsett + Tilbakestill til Steams standard + Denne kontrollen har ingen binding i Steam å gi et ikon + Ikon for %1$s + Farge på berøringsoppsett + Fullført + Kunne ikke lagre berøringsoppsettet + Berøringsoppsett lagret til Steam + App %1$d + Etikett (valgfritt) + Steams ikoner ble ikke funnet + + %1$d ikon fra Steam + %1$d ikoner fra Steam + + Ikonfarge + Knappefarge + Ikke noe ikon + + Kontrollerer innlogging… + Henter biblioteket ditt… + Henter spilliste… + Laster spilldetaljer… (%1$d/%2$d) + + Synkroniserer %1$d spill… + Synkroniserer %1$d spill… + + Synkroniserer… %1$d/%2$d + Henter omslagsbilder… + Synkronisering mislyktes + Fant ingen spill i %1$s-biblioteket ditt + Amazon Games svarte ikke for biblioteket ditt: oppdater, eller logg inn på nytt + Kunne ikke nå GOG + Kunne ikke lese GOG-biblioteket ditt + Registrerer hos Steam… + Cardboard VR + Cardboard-modus + Bilder for venstre og høyre øye til en VR-brille for mobil. + Bilde + Virtuell skjerm + Side-ved-side-3D + Briller + Cardboard + Cardboard (2014) + Ingen linse + Øyeavstand + Skjermbredde + Skjermavstand + Hodesporing + Denne enheten har ingen rotasjonssensor. + Sentrer visningen + Sentrer + SteamVR (eksperimentell) + Installer + Laster ned Valves SteamVR for ARM64 (571 MB). + Laster ned og pakker ut SteamVR … + Start + Kjører SteamVR med hodet ditt som headset; bildet bytter til side-ved-side-3D. + Stopp + SteamVR kjører. + Ikke tilgjengelig i denne økten. + SteamVR svarte ikke; sjekk sesjonsloggen. + Last ned komponenter + Nedlastinger deles med Komponenter-siden. Velg en nedlastet versjon ovenfor for å bruke den fra neste spilløkt. + Velg en versjon å laste ned + Utvikler + Utgiver + Utgitt + Hovedhistorie + + Ekstra + Komplettering + %1$s t + %1$d min + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000..e34010cf8 --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,2235 @@ + + + DroidDeck + + Bij de eerste start wordt Steam gedownload. Dit kan een paar minuten duren. + Standaard wordt Steam gepauzeerd als je de app verlaat; als je terugkomt, gaat het verder waar je was. + Je controller wordt gezien als Xbox 360-controller. Gebruik anders de schermbediening. + + + Bij de eerste start wordt de desktop ingericht. Dit kan een paar minuten duren. + Standaard wordt de sessie gepauzeerd als je de app verlaat; als je terugkomt, gaat die verder waar je was. + Aanraken werkt als touchpad: sleep om de aanwijzer te verplaatsen, tik om te klikken. + Je controller wordt gezien als Xbox 360-controller. Gebruik anders de schermbediening. + + Sessie + Wordt getoond zolang er een Steam- of desktopsessie open is + Steam is actief + Steam-download actief + Sessie gepauzeerd - tik op Hervatten als je klaar bent + Hervatten + Nu onderbreken + Sessie stoppen + Framegeneratie + Kies de modus voor framegeneratie + Uit + Win-FG %1$d× + LSFG %1$d× + LSFG adaptief %1$d + Vereist Lossless Scaling op Steam + Installeer Lossless Scaling in Steam of importeer Lossless.dll + Lossless.dll importeren… + De kopie uit Steam wordt gebruikt + Een geïmporteerde kopie wordt gebruikt + Kies Lossless.dll + Lossless.dll geïmporteerd + Deze Lossless.dll wordt al gebruikt + Lossless Scaling staat niet op dit Steam-account + Dit bestand is niet de Lossless.dll van Lossless Scaling + Kan LSFG niet voorbereiden met deze Lossless.dll + LSFG vereist Lossless Scaling uit Steam of een geïmporteerde Lossless.dll + LSFG kan zijn shaders niet voorbereiden uit Lossless.dll + %1$s: niet ondersteund + %1$s: engine mislukt + %1$s: vereist Lossless Scaling + %1$s %2$s · starten + Voegt een Steam-bibliotheek toe en scant die op Windows-games. Games die je bezit gebruiken hun Steam-items; de rest wordt een snelkoppeling voor een niet-Steam-game. + Game-omgeving + Omgevingsvariabelen voor Proton-games. + Bewerken + Alle Proton-games + Profiel + Steam-app-ID + Openen + Game %1$s + Variabele toevoegen + Naam van variabele + Waarde + Opslaan + Verwijderen + Weer overnemen + Omgevingsoverschrijvingen wissen + Niet instellen voor dit profiel + Geen variabelen ingesteld. + Overgenomen + Aangepaste variabele + Kan niet laden of opslaan. Controleer de vrije opslagruimte en probeer het opnieuw. + Gebruik een geldige naam en een waarde van maximaal 8192 tekens. + De standaard Proton-runtime leest deze variabele niet. + Laden… + Overschrijving van het D3D12-featureniveau. 12_2 meldt DirectX 12 Ultimate-mogelijkheden; niet-ondersteunde functies kunnen mislukken. Niet instellen voor automatische detectie. + Overschrijving van het D3D12-shadermodel. 6_9 vereist een voldoende recente VKD3D en bijpassende GPU-functies. Niet instellen voor automatische detectie. + Houd de Mesa-shadercache ingeschakeld om herhaald compileren te beperken. false schakelt caching in. + Maximale grootte van de Mesa-shadercache, zoals 1G. Grotere caches gebruiken meer opslag. + VKD3D-opties voor D3D12. nodxr schakelt raytracing uit; gebruik dit alleen voor een game die dat nodig heeft. + Stel in op 1 voor een diagnostisch Proton-log. Loggen kan de prestaties verlagen en kost opslag. + Stel in op 1 om voor compatibiliteitstests met D3D9–11 het op OpenGL gebaseerde WineD3D te gebruiken in plaats van DXVK. + Stel in op 1 om deze game niet op ntsync (droiddeck-ntsync of een kerneldriver) te draaien; dan wordt droiddeck-esync gebruikt als er een pakket is, anders droiddeck-fsync. + Stel in op 1 om droiddeck-esync uit te zetten voor deze game; dan wordt droiddeck-fsync gebruikt, of droiddeck-ntsync als fsync ook uit staat. + Stel in op 1 om deze game niet op droiddeck-fsync te draaien; dan wordt droiddeck-esync gebruikt als er een pakket is, anders droiddeck-ntsync. + Stel in op 1 om deze game op droiddeck-fsync te draaien, of op 0 om droiddeck-esync voorrang te geven als er een pakket bij de Proton-versie past. + Stel in op 0 om de gamepadnavigatiehulp van Proton voor deze game uit te schakelen. + DXVK-overlay voor D3D9–11. fps toont de framerate; niet ingesteld verbergt de overlay. + DXVK-instellingen, gescheiden door puntkomma’s. Recente builds gebruiken dxvk.maxFrameRate om de framerate te begrenzen; gebruik voor normaal spelen liever de framelimiet van de sessie. + Framerate-limiet voor D3D12. 0 schakelt de limiet uit. + OpenGL-threading van Mesa. true kan helpen bij CPU-beperkte OpenGL-games; test per game. + Wine-synchronisatie + Hoe games hun threads gelijk laten lopen. Geldt vanaf de volgende sessie. + esync + ntsync + fsync + wineserver + FEX-voorinstelling + Geldt bij de volgende start van een game. + Games + FEX-standaard + Gebruikt de standaardinstellingen van FEX. + Stabiliteit + Veiligste, traagste instellingen. + Compatibiliteit + Volledige TSO met multiblock. + Gemiddeld + Gebalanceerde TSO met lagere x87-precisie. + Prestaties + Alle TSO uitgeschakeld; multiblock. + Prestaties + TSO + Prestatie-instellingen met de hoofd-TSO ingeschakeld. + Extreem + Schakelt controles op zelfwijzigende code uit. JIT-games en DRM kunnen stukgaan. + Extreem + TSO + Extreme instellingen met de hoofd-TSO ingeschakeld. + Extreem-gn + Prestaties met TSC- en metadatahints. Probeer dit vóór Extreem. + Denuvo + Prestaties met volledige codecontroles en verborgen hypervisorbit. + Aangepaste waarde + Kies een waarde + Emulators & apps + Toevoegen + Je eigen app of game + Script + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Starten… + %1$s toegevoegd + %1$s verwijderd + Desktop + Die app bestaat niet meer. Kies hem opnieuw via Desktop. + %1$s openen + Verwijderen + Druk nogmaals om te verwijderen + Stop de sessie om te verwijderen + Voert %1$s uit vanuit de eigen map, die in de sessie wordt gekoppeld waar hij staat. Als je de app verwijdert, blijft die map ongemoeid. + Voert %1$s uit vanuit een kopie van de map in DroidDeck. Als je de app verwijdert, wordt de kopie gewist, niet het origineel. + Installeer eerst de Linux-runtime + Het bestand bestaat niet meer + Niet genoeg vrije ruimte: voor het kopiëren van de map is %1$s nodig + %1$s kopiëren + App toevoegen mislukt + Zoeken naar een release van %1$s + Kan de releases van %1$s niet lezen + Geen enkele release van %1$s heeft een ARM64- of x86_64-AppImage + %1$s downloaden + Downloaden mislukt + App updaten mislukt + Dat pictogram kan niet worden gelezen + %1$s opgeslagen + Bewerken + Controleren op updates + Controleren op updates… + Updaten naar %1$s + Up-to-date · %1$s + %1$s geüpdatet + %1$s · %2$s + App toevoegen + Script + AppImage + GitHub + Flatpak + Een .sh-bestand. Het draait vanuit de eigen map, op volledig scherm onder gamescope. + Een ARM64-AppImage (aarch64), of een x86_64-versie die via FEX draait. Die wordt eenmalig uitgepakt in de Linux-runtime. + Installeert de ARM64-AppImage uit de nieuwste release van de repository, of de x86_64-versie via FEX. + FEX gebruiken + Automatisch + Aan + Uit + FEX draait x86-Linux-programma’s op dit ARM64-apparaat. Bij Automatisch wordt FEX gebruikt als het programma voor x86 is gemaakt. + %1$s via FEX + Dit programma is gemaakt voor %1$s-pc’s en draait via FEX, dat nog niet is ingesteld. Open Steam één keer terwijl je bent ingelogd: DroidDeck laat het dan FEX en de benodigde Steam Linux Runtime downloaden. Daarna opent dit. + Dit programma + %1$s is gemaakt voor x86-pc’s en draait via FEX, dat nog niet is geïnstalleerd. Start Steam één keer terwijl je bent ingelogd en laat het FEX en de Steam Linux Runtime downloaden. Start dit daarna opnieuw. + Een Flathub-app-ID of de Flathub-pagina ervan. Flatpak wordt eerst ingesteld als dat nog niet is gebeurd. + Bestand + Geen bestand gekozen + Bestand kiezen + Wijzigen + Kies een script + Kies een AppImage + Kies een pictogram + Repository + eigenaar/repo of een github.com-link + Geen GitHub-repository + App-ID + org.ppsspp.PPSSPP + Geen Flathub-app-ID + Of een .flatpak-bestand, voor een app die niet op Flathub staat. + Kies een .flatpak-bestand + Bron + Op je desktop + Nieuwe app + Naam + Uit de app + Pictogram + Standaardpictogram + Pictogram kiezen + Wissen + Uit de repository + Zoeken naar pictogrammen… + Standaard + %1$s bewerken + Opslaan + Map controleren… + De map wordt in de sessie gekoppeld: %1$s + De map bevat Linux-programma’s, die vanaf gedeelde opslag niet kunnen draaien, dus wordt hij naar DroidDeck gekopieerd (%1$s). + Toevoegen + Annuleren + Installeer eerst de Linux-runtime via Installatie. + Wacht tot %1$s klaar is. + Compatibel + Builds die in het menu Compatibiliteit van Steam zijn gemarkeerd met “%1$s” hebben de prefixfixes van DroidDeck en starten hier games. Standaard is dat Proton Experimental ARM64 (%1$s), of Proton 11.0 ARM64 (%1$s) zodra die nieuwere build is geïnstalleerd. Valves eigen Proton-items zonder die markering kunnen op dit apparaat geen game starten. + %1$s geïnstalleerd · %2$s + Annuleren + Automatisch + Nooit + Altijd + Verwijderen + Controleren + Updaten + Controleren… + Bezig… + Vergeten + Toevoegen… + Gebruiken + Geldt vanaf de volgende sessie. + Steam-sessie + Desktopsessie + Scherm + Resolutie + Tot 720p + Tot 900p + Tot 1080p + Oorspronkelijk + %1$s (standaard) + Aangepast · %1$d×%2$d + Aangepast… + Met 720p kunnen menu’s sneller reageren. + Beeldverhouding + Ingesteld door de aangepaste resolutie. + Automatisch gebruikt minimaal 16:9. + Framelimiet + Begrenst de hele sessie. Het scherm schakelt naar een verversingssnelheid die een veelvoud van de limiet is. + HDR + HDR10-uitvoer + Niet beschikbaar: %1$s. + Start de app opnieuw om dit toe te passen. + Drivers + GPU-drivers + Gedeeld door Steam en Desktop. In te stellen op de pagina Componenten. + Aanraken & bediening + Aanraken + Touchpad + Direct + Automatisch: touchpad op Desktop, direct in Steam. + Schermbediening + Steam + QAM + Automatisch toont de bediening als er geen controller is verbonden. + Steam-aanraakbediening (bèta) + De aanraakbediening van Steam start bij de volgende Steam-sessie + Steam-aanraakindeling + Verplaats, schaal of verberg de aanraakbediening van deze game; opgeslagen in Steam + Controller + Hoe Steam je controller ziet. Geldt vanaf de volgende sessie. + Steam Deck-controller + Steam Controller + DualShock 4 + DualSense + Xbox 360-controller + Deck: knop Snelle toegang en gyro. Steam Controller: hetzelfde, met gripsensor op de achterknoppen. DualShock 4 en DualSense: gyro, en de trackpads als hun touchpad. Xbox 360: open Snelle toegang met Guide+A. + Terug + Menu → QAM + QAM → Menu + Sessie + Gedrag op de achtergrond + Als de app verborgen is of het scherm uitgaat. + Handmatig + Systeemeigen + Automatisch pauzeert en hervat zodra de app weer zichtbaar is. Systeemeigen maakt Steam eerst klaar voor de slaapstand. Handmatig wacht op Hervatten. Nooit laat alles doorlopen. + Downloads laten doorlopen + Stel onderbreken op de achtergrond uit zolang Steam actief downloadt. + Automatisch pauzeert op de achtergrond en hervat zodra de app weer zichtbaar is. Handmatig wacht op Hervatten. Nooit laat alles doorlopen. + Opstarten + Steam starten met DroidDeck + Opent de Steam-sessie als DroidDeck start. + Decky + Zoeken naar releases… + Klaar om %1$s te installeren. + Geen compatibele build gevonden. Probeer het later opnieuw. + Geïnstalleerd · %1$s + Update beschikbaar · %1$s + Up-to-date · %1$s + Loader + Nieuwste installeren + Start met Steam. Zolang dit aan staat, kunnen andere apps op dit apparaat Steam mogelijk bedienen. + Uit. De poort voor foutopsporing op afstand van Steam blijft dicht. + Decky Loader verwijderen? + Verwijdert de loader. Je plug-ins en instellingen blijven behouden. + Client + Prestatie-overlay + Zet dit uit als games crashen of zwart worden in de Deck-modus. Geldt vanaf de volgende sessie. + Steam-client repareren + Downloadt de Steam-client opnieuw bij de volgende start van Steam. Gebruik dit als Steam op het laadscherm blijft hangen. Games, login en instellingen blijven behouden. + De Steam-client wordt opnieuw gedownload bij de volgende start van Steam. + Repareren + Afbeeldingen + SteamGridDB-API-sleutel + Je eigen sleutel + Bewerken… + Vult ontbrekende afbeeldingen aan uit Steam en daarna uit SteamGridDB. Je eigen cover.jpg, hero.jpg, logo.png of icon.png in de gamemap heeft voorrang. + %1$s verwijderen + Verwijderen? + Game toevoegen + Game toevoegen: + Kies de exe: + Deze locatie is niet beschikbaar in de sessie + Deze exe controleren + Windows-componenten + Met LB / RB spring je tussen de secties. + Geen + Windows-DLL’s die Proton niet meelevert, uit de componentenlijst. Zet aan wat deze game nodig heeft: ze komen in de eigen Windows-map van de game, nooit tussen de gamebestanden, en blijven staan op welke Proton de game ook draait. + Kan de componentenlijst niet bereiken. Al gedownloade componenten kun je nog steeds aan- en uitzetten. + Aanbevolen voor deze game + Controleren wat deze game nodig heeft… + Beschikbaar als je online bent + + %1$d component aanzetten… + %1$d componenten aanzetten… + + Aan voor deze game + Steam installeert %1$s met %2$s (app %3$s) + Uit de lijst van GOG voor deze game + Uit de vereisten van Epic voor deze game + Uit de installatiestappen van Amazon voor deze game + Steam installeert %1$s met app %2$s, gevonden op naam + Ook vermeld + Vermeld door %1$s; hier geen component + Steam + GOG + Epic Games + Amazon Games + De game levert %1$s mee + De game heeft %1$s (werkt vaak zoals het is) + Al aanwezig in deze game + Niets in de gamemap wijst op een component. + Geïnstalleerd voor deze game (%1$d) + Alle componenten (%1$d) + Vereisen een Windows-installatieprogramma (%1$d) + Dit zijn eigen installatieprogramma’s zonder pakket dat uit elkaar gehaald kan worden, en er is nog geen opname van wat ze installeren. De meeste zijn oude runtimes waarvoor de Mono in Proton al invalt; opnames worden toegevoegd zodra ze gemaakt zijn. + Tonen + Verbergen + Voert een eigen installatieprogramma uit (nog geen opname) + Hier nog niet ondersteund + Componenten laden… + Kan niet installeren: %1$s + Gedownload + %1$s downloaden + Installeren: %1$s + Uitzetten… + %1$s te downloaden + Wordt actief bij de volgende start van de game. + 16:9-games uitrekken + Rekt games op schermen die vierkanter zijn dan 16:9 uit tot het hele scherm, in plaats van balken te tonen. Geldt vanaf de volgende sessie. + Audio + DirectAudio voor games + Omzeilt PulseAudio voor minder vertraging. + Microfoon + Voor spraakchat. + Netwerk + Wi-Fi-detectie + Toon Wi-Fi-namen en netwerken in de buurt in Steam. + Locatieservices staan uit. + Locatietoestemming niet verleend. + Instellingen openen + Doorgaan + Wi-Fi-detectie aanzetten? + Android vereist locatietoestemming om Wi-Fi-namen te lezen en netwerken in de buurt te vinden. DroidDeck gebruikt deze toegang om die netwerken in Steam te tonen. We volgen je locatie niet. Internettoegang werkt ook zonder deze toestemming. + Gameopslag + Automatisch - de SD-kaart als die er is, plus je Games-mappen + Automatisch - je Games-mappen, plus de SD-kaart als die er is (nu geen) + Alleen intern + Map: %1$s + Tweede bibliotheek + Games die assets streamen vanaf SD of gedeelde opslag kunnen haperen. + Opslagdiagnose + Legt toewijzingsresultaten en steekproeven van opslagtijden vast in Logs delen. Kan installaties vertragen zolang dit aan staat. Geldt vanaf de volgende sessie. + Kies een map… + Aangepaste resolutie + Vervangt de limiet en de beeldverhouding. Een formaat dat niet bij het scherm past, krijgt balken. + Breedte + Hoogte + Tussen 320×240 en 3840×2160. + + %1$d game + %1$d games + + OK + Verwijderen + Herstellen + Downloaden + %1$s verwijderen + %1$.1f MB + Runtimedriver + Gebruikt door Steam, games en de desktop. Geldt vanaf de volgende sessie. + Turnip-zip importeren… + Weergavedriver + Tekent frames op het scherm in beide modi. Start de app opnieuw om dit toe te passen. + AdrenoTools-zip importeren… + nooit + Nightlies gecontroleerd: %1$s + Componenten + Over Componenten + Zoeken naar nieuwe drivers + Nightlies controleren op nieuwe pakketten + Proton + De hier gekozen Proton draait elke game die in Steam geen eigen Proton heeft gekozen, en een wijziging in Steam zie je hier ook. Steam vervangt Valves Proton bij updates; originelen worden per build bewaard. + Configuratie + Profiel configureren + Profiel + Standaardprofiel + Automatisch gebruikt door games zonder overschrijvingen. + Opgeslagen instellingen die automatisch worden gebruikt als %1$s start. + Kies alleen de instellingen die deze game nodig heeft. Al het andere blijft het Standaardprofiel gebruiken. + FEX-voorinstelling + Gebruikt de voorinstelling van het Standaardprofiel. + Aangepaste voorinstelling opgeslagen voor deze game. + Aangepaste FEX-variabelen + Standaard gebruiken + Gamecompatibiliteit + Omgevingsopties voor Proton, DXVK, VKD3D en synchronisatie voor deze game. + Gebruikt de Proton van het Standaardprofiel. + Aangepaste Proton opgeslagen voor deze game via de compatibiliteitstoewijzing van Steam. + Geen Proton + DXVK + VKD3D-Proton + Gebruikt het pakket van het Standaardprofiel uit %1$s, tenzij deze game een ander pakket kiest. + Gebruikt het componentpakket van het Standaardprofiel. + Gebruikt %1$s alleen voor deze game. + Runtime-opties + Overige omgevingsopties per game voor Proton, DXVK, VKD3D-Proton en synchronisatie. + Het profiel van %1$s is opgeslagen voor de volgende start van de game. + Starten zonder Steam + Voor een game die meteen sluit: start deze zonder Protons Steam-setup. Steam-functies zoals de overlay staan uit voor deze game. + Aan + De Proton-keuze van %1$s is opgeslagen en nu beschikbaar in Steam. + De Proton-keuze van %1$s is opgeslagen. Start Steam opnieuw voordat je de game start. + %1$s kan de gekozen Proton op dit apparaat niet gebruiken. + Kan het profiel van %1$s niet laden: %2$s + Het gameprofiel kan niet worden opgeslagen: %1$s + Gameprofiel resetten + Profiel van %1$s resetten? + De overschrijvingen voor Proton, FEX, componentpakket, omgeving, textuurfiltering en Windows-componenten van deze game verwijderen? De game gebruikt dan het Standaardprofiel. + %1$s gebruikt nu het Standaardprofiel. + Het gameprofiel kan niet volledig worden gereset: %1$s + %1$s draait nu elke game die in Steam geen eigen Proton heeft gekozen. + %1$s draait elke game die geen eigen Proton heeft gekozen, vanaf de volgende start van Steam. + %1$s kan hier geen games starten en kan dus niet de standaard zijn. + De standaard-Proton kan niet worden opgeslagen: %1$s + %1$s · %2$d gewisseld + %1$s · alles origineel + %1$s · game actief + %1$s · droiddeck-esync-pakket + %1$s · nog geen droiddeck-esync-pakket + %1$s · esync ingebouwd, geen pakket nodig + GPU-drivers + %1$s · opnieuw toegepast bij start %2$s + Volgende: %1$s · nadat de game sluit + %1$s in gebruik: %2$s + Wacht tot de game sluit + Geldt bij de volgende start van een game + .zip importeren + Protons lezen… + Nog geen Proton geïnstalleerd. Start Steam één keer om de ARM64-Proton te downloaden, of voeg GE-Proton, CachyOS of DroidDeck-Proton toe via Installatie. + Origineel · %1$s + %1$s · deze build + %1$s · eerdere build + Geïnstalleerd · %1$d + Nog niets bewaard voor %1$s. De eigen bestanden van deze Proton worden als origineel opgeslagen. + Er draait een game op deze Proton, dus dit wacht tot die sluit. + Geldt bij de volgende start van een game. + %1$s wisselen? + %1$s in %2$s zetten? De originele bestanden blijven bewaard. %3$s + %1$s herstellen? + Het origineel van %1$s terugzetten in %2$s? %3$s + Verwijdert het pakket uit de app. Je kunt het opnieuw downloaden. + Dit origineel verwijderen? + Dit origineel van %1$s komt uit een eerdere build van %2$s. Het origineel van de huidige build wordt altijd bewaard. + In de Nightlies · %1$d + Vernieuw om de pakketten in de Nightlies te tonen. + Niets nieuws sinds de laatste controle. + .wcp importeren + Origineel van deze Proton herstellen + Het originele %2$s van %1$s terugzetten? + Wisselen + FEX, DXVK en VKD3D-Proton voor elke Proton waarmee Steam games draait.\n\nKies een geïnstalleerde rij om die in te wisselen. Dit geldt bij de volgende start van een game, of nadat de draaiende game sluit.\n\nJe keuzes worden voor elke start opnieuw gecontroleerd en teruggezet als iets ze heeft gewijzigd. De originele bestanden van elke Proton worden bewaard, zodat Steam-updates ze nooit kwijtraken. + Origineel + In gebruik + Nieuw + Bewaard + Prestaties + CPU-cores, sessiefixes en de hostnaam. Geldt vanaf de volgende sessie. + Cores voor de Steam-client + Corekeuze van Steam overschrijven + Bindt Steam, de UI-helper en gamescope aan de cores hieronder. + Clientcores + Cores voor Steam. + Zet de overschrijving aan om te kiezen. + Gamecores + Gamecores + Cores voor games. Met alles geselecteerd verdeelt Android vrij. + Clientinterface + GL-threading + Kan Steam-menu’s sneller laten reageren. + Zink: luie descriptors + Aanbevolen voor drivers zonder descriptorbuffers. + GL-foutcontroles overslaan + Schakelt GL-validatie per aanroep uit. + gamescope: realtime GPU-wachtrij + Geeft het GPU-werk van de compositor voorrang. Kan de framepacing gladder maken, ten koste van wat gameprestaties. + GPU + GPU op hoogste kloksnelheid houden + Alleen Adreno. Minder haperingen, meer batterijverbruik en warmte. + Sessiefixes + Turnip: sysmem-rendering + Sneller voor Steam en de meeste games. Vereist op Adreno 710/720/722. + Sla de xalia-helper van Steam over + droiddeck-ntsync (experimenteel) + Een userspace-vervanger voor de ntsync-driver van Linux, voor tests. Terwijl dit aan staat, gebruikt Proton dit in plaats van droiddeck-esync en droiddeck-fsync. Werkt met Proton Experimental ARM64, GE-Proton en proton-cachyos. Geldt vanaf de volgende sessie. + droiddeck-esync + droiddeck-fsync eerst + Draait de eigen fsync van Proton op de standaard Proton-bestanden, met futex_waitv in userspace geleverd, zodat er geen pakket nodig is en dit blijft werken zodra Steam Proton bijwerkt. Terwijl dit aan staat, gebruiken games dit vóór droiddeck-esync, behalve de paar die Proton niet op fsync laat draaien. Geldt vanaf de volgende sessie. + De Windows-threads van games wachten via eventfd en gedeeld geheugen op elkaar, in plaats van heen-en-weer via wineserver. Gebruikt een droiddeck-esync-pakket dat voor die exacte Proton-build is gemaakt; pakketten worden automatisch gehaald. Totdat er een pakket is voor een nieuwe Proton-build, springt droiddeck-fsync in. Geldt vanaf de volgende sessie. + Schakelt de gamepadnavigatiehulp van Proton uit, die in elke game CPU kost. + Draai proot zonder seccomp + Kan foutmeldingen over ontbrekende systeemaanroepen op sommige kernels verhelpen. Kan de prestaties verlagen. + proot-snelpad + Vereist het seccomp-filter van proot. + Snellere bestandstoegang en minder stotteren. Zet dit uit als een programma zijn bestanden niet kan vinden. + Sessie-identiteit + Hostnaam + De naam die Steam voor dit apparaat ziet. Leeg herstelt %1$s. + Alleen letters, cijfers en koppeltekens binnen de naam, tot 63 tekens. + Android is ingesteld om deze sessie te sluiten + Geïnstalleerd + Beschikbaar om te downloaden + Niets nieuws sinds de laatste controle. + Ingebouwde drivers herstellen + Ingebouwd in de app: dit verwijdert de uitgepakte bestanden en verbergt de driver. Met Ingebouwde drivers herstellen komt hij terug. + Een helft van een Android + Linux-bundel: beide helften worden verwijderd. Een driver die in gebruik is, valt terug op de standaard. + De bestanden worden uit de app verwijderd. + De bestanden worden verwijderd. Hij is in gebruik, dus de standaard neemt het over. + Scherm + Effecten + Bediening + Componenten + Controller + Instellingen + %1$s-pagina + Steam + Desktop + Steam-menu + Snelmenu + Snelmenu openen + Vorige pagina + Volgende pagina + Prestatie-HUD + Schaalmodus + Schaalscherpte + Schermeffecten + Live. Kost wat GPU-tijd zolang er een effect aan staat. + Voorinstellingen + Aangepast + Geavanceerd + Uitgeklapt + Ingeklapt + Verscherping (CAS) + Verscherpingsniveau + Nep-HDR + Debanding + Debanding-sterkte + Helderheid + Contrast + Gamma + Verzadiging + FXAA (vloeiende randen) + Toon-contouren + CRT + NTSC + Textuurfiltering + DirectX 9–11-games, vanaf hun volgende start. + Anisotrope filtering + Textuurscherpte + Een negatieve mip-bias: scherpere texturen, met wat shimmer. Automatisch volgt hoe ver de sessie wordt uitvergroot naar het scherm. + Automatisch (%1$s) + Toetsenbord + Pc-toetsenbord + Android + Tweede scherm + Modus + Toont + Android-apps + Een app starten + Verbergen + Tonen + Geen apps om te starten + Sessiegedrag + Geldt nu en voor toekomstige sessies in deze modus. + Volgende sessie + Ondersteuning + Sessielogs + Voeg bij een bugmelding + Logs delen + Achtergrond + Stoppen + Sessie stoppen? + Nu actief + Er draait geen game op een Proton + Gameprofiel + Profielgegevens zijn nog niet beschikbaar. + Kan gameprofielen niet laden. Wordt automatisch opnieuw geprobeerd. + Kan gameprofielen niet vernieuwen. Toont de laatst beschikbare gegevens; wordt automatisch opnieuw geprobeerd. + Bekijk en bewerk de opgeslagen instellingen voor de volgende start. + Gebruikt het standaardprofiel + Aangepast profiel + Gebruikt %1$s van het standaardprofiel. + Wijzigingen worden opgeslagen in het profiel van %1$s en gelden vanaf de volgende keer dat de game start. + + %1$d game-override + %1$d game-overrides + + Er is nog geen Proton geïnstalleerd. + game actief + opnieuw toegepast bij start %1$s + Origineel + Importeren en verwijderen op de pagina Componenten. + Klaar + Aandacht nodig + Bezig + Controleer het IP-adres en de poort van Draadloos debuggen, of koppel opnieuw als Android dit apparaat heeft verwijderd. + Installeren + Beheren + Overzicht + Controller + Sessie + Launcher + Installatie + ● Alles klaar + %1$d van %2$d gereed + Systeemcontrole + Apparaat ondersteund + Ongeteste GPU + GPU niet ondersteund + %1$s · %2$s + %1$s: %2$s + Steam heeft een Adreno-driver nodig; %1$s kan een zwart scherm tonen + Linux-runtime + %1$s · %2$d%% + Niet geïnstalleerd · ongeveer 3 GB, geïnstalleerd bij de eerste keer Spelen + Geïnstalleerd + %1$s · update beschikbaar + %1$s · up-to-date + Limiet voor subprocessen + Aan · Android kan Steam sluiten. Uitzetten duurt een minuutje. + Niet ingesteld · Android kan Steam sluiten. Uitzetten duurt een minuutje. + Kan niet worden gecontroleerd · uitzetten duurt een minuutje. + Oplossen + Details + Opties voor ontwikkelaars + Draadloos debuggen gebruiken + Opnieuw controleren + Limiet inschakelen + Steam-account + Aangemeld als %1$s · offlinemodus + Aangemeld als %1$s + Druk op Spelen en meld je aan bij Steam + Hulpmiddelen + Controller + Controllerinstellingen zijn niet beschikbaar. + Sessie + Sessielogs + Opgeslagen na elke sessie + Hulpmiddelen voor debuggen + Nieuwste sessielogs + Opgeslagen sessielogs + Bewaart de nieuwste %1$d + Logs wissen + Offlinemodus + Meld je eerst aan bij Steam + Launcher + Taal + Steam gebruikt deze ook, vanaf de volgende keer dat het start. + Systeemstandaard (%1$s) + Systeemstandaard + Appschaal + Tekst en bediening van DroidDeck + %1$d%% (Standaard) + Thema + Uiterlijk van de launcher + Als startscherm gebruiken + DroidDeck kan de startschermapp zijn + DroidDeck wordt nooit aangeboden als startschermapp + Oriëntatie + Automatisch liggend + Liggend + Liggend omgekeerd + Volledig scherm + Verbergt de statusbalk en navigatiebalk van Android + Toont de statusbalk en navigatiebalk van Android + Standaard startschermapp + Kies een startschermapp + Kiezen + Linux-apps (bèta) + Bestanden + Bestanden doorbladeren en beheren + Proton-versies + ARM64 Proton-builds installeren + Prestaties + Toewijzing van CPU-kernen + ROM-map + Waar emulatorgames worden opgeslagen + Componenten + FEX, DXVK, VKD3D + Bediening + Knoppenindeling + Ontdekken + Zoeken + Geïnstalleerd + %1$s · %2$d + Store + Flathub · ARM64 + ● Flatpak gereed + Installeer eerst de Linux-runtime via Installatie. + Flatpak instellen + Voegt Flatpak toe en verbindt het met Flathub (ongeveer 20 MB). Bij de eerste app wordt ook de bijbehorende runtime gedownload. + Instellen… + Flatpak + Updates + %1$s · %2$s + %1$s · %2$s · %3$d%% + Starten… + Categorieën + Kan Flathub niet bereiken. Controleer je verbinding en probeer het opnieuw. + Opnieuw proberen + Flathub doorzoeken + Firefox, games, emulators… + Zoeken + Alle + Zoeken… + Zoek op naam of kies een categorie. Alleen ARM64-apps worden weergegeven. + Geen ARM64-apps op Flathub komen daarmee overeen. + Resultaten + Controleren op updates + Alles updaten (%1$d) + ● Alles is up-to-date + Nog geen apps. Zoek ze onder Ontdekken of Zoeken. + Stel Flatpak in om apps te installeren. + Update beschikbaar · %1$s + Openen + Details + Verwijderen + Druk nogmaals om te verwijderen + Installeren… + Installeren + Stel eerst Flatpak in + %1$s download + ● Geïnstalleerd + Kan de Flathub-pagina van deze app niet laden. + Laden vanuit Flathub… + Geen ARM64-build op Flathub, dus kan dit hier niet worden geïnstalleerd. + Screenshots + Over + App-ID + Licentie + Runtime + Geïnstalleerde grootte + %1$s (plus de gedeelde runtime) + Website + Geverifieerd + Updates + DroidDeck %1$s · %2$s + Gecontroleerd %1$s + Nog niet gecontroleerd + Laat DroidDeck zichzelf updaten + Op het volgende scherm zet je Van deze bron toestaan aan, en kom dan terug. De update gaat daarna automatisch verder. + Instellingen openen + Niet nu + Anders ondertekend + Kan niet rechtstreeks worden bijgewerkt + Deze kopie is niet ondertekend met de releasesleutel van DroidDeck, dus updates kunnen er niet overheen worden geïnstalleerd. Verwijder de app om te wisselen. + Update beschikbaar + Installeren niet mogelijk + Klaar om te wisselen + Voor op Stabiel + Je loopt voor op Stabiel + Deze build is nieuwer dan de nieuwste Stabiele build. Je gaat naar Stabiel zodra de volgende release daarvan verschijnt. + Test afgelopen + Deze test is afgelopen + De bijbehorende fix is samengevoegd of geschrapt. Volg Preview voor de nieuwste fixes. + Up-to-date + Je hebt de nieuwste %1$s + · %1$s + %1$s… %2$d%% + %1$s… + Updaten%1$s + Installeren%1$s + Stabiel %1$s toch installeren + Preview volgen + Stop de actieve sessie om te updaten. + Minder + Meer + DroidDeck %1$s + Nieuwe Preview-build + PR #%1$d-testbuild + %1$d MB + UPDATEKANAAL + Stabiel + Geteste releases, voor de meeste mensen + %1$s · %2$s + Preview + Nieuwste fixes, vóór Stabiel + Up-to-date + Testbuilds + Probeer een fix voordat die wordt uitgebracht + Momenteel geen + PR #%1$d + Niet gebouwd voor deze kopie van DroidDeck + Geüpdatet %1$s + de PR #%1$d-test + een tijdje geleden + zojuist + + %1$d om te proberen + %1$d om te proberen + + + Wijzigingen sinds jouw build · %1$d build achter + Wijzigingen sinds jouw build · %1$d builds achter + + + Recente wijzigingen · %1$d build + Recente wijzigingen · %1$d builds + + Eén Android-instelling om te wijzigen + Steam + Android stopt apps die veel achtergrondprocessen starten, en Steam start er tientallen. Totdat dit verandert, sluiten games zonder foutmelding. + Voor mij oplossen + Zet “Beperkingen voor subprocessen uitschakelen” aan, en kom dan terug. Laat Opties voor ontwikkelaars aan staan, anders schakelt de schakelaar weer uit. + Opties voor ontwikkelaars openen + Deze Android-versie heeft hiervoor geen schakelaar, dus wijzigt DroidDeck dit via Draadloos debuggen. Duurt ongeveer een minuut, geen computer nodig. + Draadloos debuggen heeft Wi-Fi nodig. Elk netwerk werkt; internet is niet vereist. + De koppelingscode komt in een melding. Sta meldingen toe voor DroidDeck, of gebruik Andere manieren. + Meldingen toestaan + Open Instellingen → Opties voor ontwikkelaars → Draadloos debuggen en zet het aan + Tik op “Apparaat koppelen met koppelingscode” + Voer de code in bij de melding van DroidDeck + Wachten op de koppelingspop-up… + Pop-up gevonden. Voer de code ervan in bij de melding. + Status + Controleert elke 2 seconden. Deze pagina sluit zodra het uit staat. + Andere manieren + Draadloos debuggen, handmatig adres, computer-ADB + Handmatig adres, computer-ADB, Opties voor ontwikkelaars + Gebruik Draadloos debuggen in plaats daarvan + Sommige ROMs voegen de schakelaar toch toe. Zoek naar “subproces” in Opties voor ontwikkelaars. + Als de melding niet werkt, voer je het adres en de code van de pop-up zelf in. + Adres handmatig invoeren + Vanaf een computer met ADB: + Opdracht kopiëren + Kies op welk scherm Android-instellingen wordt geopend. + Hoofdscherm + Gekoppeld. Poort van Draadloos debuggen zoeken… + Gekoppeld. Voer de poort uit Instellingen in. + Apparaat gevonden. Instelling toepassen… + Kan niet automatisch worden toegepast. Controleer het adres hieronder en probeer het opnieuw. %1$s + Draadloos debuggen + Limiet voor subprocessen bijgewerkt + De limiet voor subprocessen wijzigen + Als de optie ontbreekt, koppel dan via Draadloos debuggen. + Gekoppeld. Controleer het IP-adres en de poort in Draadloos debuggen. + De limiet voor subprocessen is bijgewerkt. + Gebruik stap 2 als Opties voor ontwikkelaars geen instelling voor subprocessen heeft. Open Opties voor ontwikkelaars → Draadloos debuggen → Apparaat koppelen met koppelingscode. + Gekoppeld. Gebruik het adres hieronder, of werk het bij via Draadloos debuggen → IP-adres & Poort. + De limiet voor subprocessen is bijgewerkt via Draadloos debuggen. + Android heeft bevestigd dat de limiet aan staat. + Android heeft bevestigd dat de limiet uit staat. + 2 · Draadloos debuggen koppelen + IP-adres & poort van de koppelingspop-up + Koppelingscode (pincode) + 6 cijfers + Koppelen + 2 · Toepassen via Draadloos debuggen + IP-adres & poort van de verbinding + Opnieuw koppelen + Limiet uitschakelen + Andere manieren om dit te wijzigen + 1 · Opties voor ontwikkelaars (probeer dit eerst) + Zet “Subprocessen beperken” aan. + Zet “Subprocessen beperken” uit. + 3 · Computer-ADB (laatste redmiddel) + Alleen als Draadloos debuggen niet beschikbaar is. + 3 · Computer-ADB (laatste redmiddel) + Wanneer de aanraakbediening in een sessie verschijnt + Kleur + Tint van de knoppen op het scherm + Aangepast + Transparantie + %1$d%% + Knopgrootte + 100% is de standaardgrootte + Stickklik + Dubbeltik en houd een stick ingedrukt voor L3 of R3 + Adaptieve sticks + Trillen in games + Steam-knop + QAM-knop + Toetsenbordknop + Druk op Select om het Steam-menu te openen + In plaats van View/Select. + Animaties + Sticks verschijnen waar je aanraakt en verdwijnen als je loslaat + Indeling + Aangepaste posities opgeslagen + Aangepast aan dit scherm + Bewerken + Resetten + Knoppenindeling + Elke knop stuurt zijn eigen invoer + %1$d van %2$d knoppen opnieuw toegewezen + Configureren + Controller resetten + Herstelt alle controllerinstellingen en de indeling + Wat elke schermknop naar de game stuurt. Verborgen verwijdert de knop. + Schermknoppen + Standaardinstellingen + Indeling resetten + Android-apps + %1$d apps + Geen startbare Android-apps gevonden. + ● Desktop geïnstalleerd + Wordt geïnstalleerd bij eerste keer openen + Beschikbaar om te installeren + Niet geïnstalleerd. + %1$s · niet geïnstalleerd + %1$s openen + Games + Kies een ROM-map. + Blader naar /root/ROMs in RetroArch. + Voeg %1$s-games toe aan ROMs/%2$s. + %1$s installeren + Stop de sessie om te installeren + Runtime vereist + Installatiegegevens laden… + Installatiegegevens zijn niet beschikbaar. Probeer het later opnieuw. + Installeer eerst de Linux-runtime via Installatie. + Stop de actieve sessie om desktopapps te installeren. + Installeert %1$s in de Linux-desktop. + %1$s · %2$d%% + Verbergen haalt het van Desktop. De bestanden blijven in de Linux-runtime. + Die game staat niet meer in de ROM-map. + Desktop · %1$s + Starten in %1$s + Ook in %1$s + Die pagina bestaat niet meer. Kies een onderdeel aan de linkerkant. + Desktop openen + Installeren & desktop openen + Desktop verwijderen + Verwijdert de desktop en de meegeleverde apps. Games, toegevoegde apps, instellingen en gedeelde bibliotheken blijven behouden. + Desktop wordt verwijderd + Desktop verwijderd + Oude LXQt-bestanden verwijderen + Werk DroidDeck bij voordat je deze desktopversie verwijdert. + Linux-desktop + KDE Plasma, Firefox en je emulators + Games die je in Steam installeert, verschijnen hier na de sessie. + Steam spelen + Opstartinstellingen + Starten + Gamebestanden + Proton-prefix + Opslagbestanden zoeken… + Geen opslagmap gevonden op naam. Exporteren neemt de hele gebruikersmap. + %1$d opslagmappen + %1$s · %2$d bestanden · %3$s + %1$d KB + Opslagbestanden beheren + Gameopslag + Opslagbestanden importeren… + Exporteren als GameHub-zip… + Exporteren als Winlator-zip… + Opslagmap openen + Nooit gespeeld + Runtime bezig · %1$d%% + Runtime bezig + Runtime wordt geïnstalleerd bij de eerste keer Spelen + Runtime-update beschikbaar + ● Runtime gereed + Laatst gespeeld %1$s + Interne opslag + Toegevoegde game + Aangepaste game + Opslag voor games + Het beste paar voor deze GPU, blijft up-to-date + Kies zelf een paar + Controleren op drivers… + Nog geen drivers vermeld. Vernieuw om ernaar te zoeken. + Drivers voor andere GPU’s verbergen + Drivers voor andere GPU’s tonen + Geavanceerd + Wordt samen met de beeldschermdriver ingesteld: %1$s bevat beide. + Wordt samen met de runtimedriver ingesteld: %1$s bevat beide. + Gebruikt door games en de desktop. + Runtimestandaard + Tekent frames naar het scherm. Geldt nadat DroidDeck opnieuw is gestart. + Automatisch - gekozen op basis van de GPU + Downloaden, importeren of verwijderen… + IN GEBRUIK + De drivers die ingebouwd zijn in DroidDeck + Runtime- en beeldschermdrivers samen ingesteld als bijpassend paar. + Aanbevolen voor deze GPU: %1$s. + Vernieuw om te zoeken naar drivers voor deze GPU. + AANBEVOLEN + Niet beschikbaar + Linux-runtime + Linux-runtime wordt voorbereid + Sessie + Beeld en audio starten + Steam-client + Steam en de compatibiliteitstools controleren + Steam wordt geopend + Wachten op het eerste frame van Steam + Desktop + Desktop wordt voorbereid + Openen + Wachten op het eerste frame + Steam wordt gestart + De sessie is beëindigd + Stap %1$d van %2$d + Steam is gepauzeerd + Grepen + Linkerpad + Rechterpad + Beide + Gamepad + Stick + Trackpad + Wisselen tussen stick en trackpad + Meer Deck-bediening + PANELEN + Bediening op tweede scherm sluiten + Steam-menu openen + Links · boven + Links · onder + Rechts · boven + Rechts · onder + Klik + Rechtertrackpad + Linkertrackpad + Trackpad + Toetsenbord tonen + Toetsenbord verbergen + Linux-terminal + De Linux-sessie is nog niet klaar voor een terminal. + Steam + Games + Desktop + Winkel + Componenten + Apps + Installatie + Updates + %1$d%% + %1$s hervatten + Verschijnt in elke sessie als /root/ROMs. Interne opslag staat op /root/Storage. + Geen map gekozen + Geldt vanaf de volgende sessie. SD-kaarten werken ook. + Map kiezen + Map wijzigen + Installeer een ARM64-build van Proton en kies die per game in Steam → Eigenschappen → Compatibiliteit. + Stop de actieve sessie om een compatibiliteitstool te installeren of te verwijderen. + Beschikbare builds + %1$s geïnstalleerd + In afwachting van een eerder verzoek + Niet geïnstalleerd + Update beschikbaar → %1$s + Oude versies van %1$s verwijderen + Verwijdert %1$s. Games die op deze versies zijn ingesteld, moeten naar een andere Proton worden overgezet. + Games die op deze versie zijn ingesteld, moeten naar een andere Proton worden overgezet. + Nu installeren + Wachtrij annuleren + Android-toetsenbord + Verbergen ✕ + %1$s starten + Kies een scherm. Tweede scherm: %1$s. + Het tweede scherm is niet meer beschikbaar. + Hoofdscherm + Tweede scherm + Vorig tabblad + Volgend tabblad + Aan + Alle %1$d + Geen + %1$d van %2$d + Uit + Onderste scherm + Geldt bij de volgende gamestart. Het profiel van een game gaat voor het gedeelde profiel. + Andere game via app-ID… + Klaar + Standaard + Uit + Wijzigen… + Variabele wijzigen + Terug naar standaard + Proton-games + Scherm-in-scherm + Automatisch scherm-in-scherm starten + Bediening op het uitgevouwen scherm + Uitgevouwen staat de game boven de Steam Deck-trackpads en achtergrepen. Dichtgevouwen is de game schermvullend. + Onderbroken + Scherm-in-scherm is beschikbaar zodra de sessie klaar is. + Sta scherm-in-scherm toe voor DroidDeck in de Android-instellingen. + Android kon scherm-in-scherm niet openen. + Snelkoppeling + Toevoegen aan startscherm + Game kiezen + Geïnstalleerde games zoeken… + Startlink kopiëren + Startlink gekopieerd + Deze launcher kan geen snelkoppelingen toevoegen. Gebruik in plaats daarvan “Startlink kopiëren”. + Ongeldige startlink voor game. + Deze game staat niet meer in je DroidDeck-bibliotheek. + Stop de huidige sessie voordat je een Steam-game start. + Kan de game niet naar Steam sturen. Probeer hem opnieuw te starten. + Verwijderen opnieuw proberen + Verwijderen + Verwijderen onvolledig + Gamebestand exporteren + Alle gamebestanden naar map synchroniseren + Synchroniseren van gamebestanden stoppen + Kies een map voor frontend-gamebestanden + Gamebestanden opgeslagen in %1$s + Frontend-bestanden + + Wacht tot de runtime-bewerking klaar is + Wacht tot de installatie van de compatibiliteitstool klaar is + Wacht tot de installatie van de desktop-app klaar is + Wacht tot de installatie klaar is voordat je een game start + Sluit eerst de Steam-sessie, zodat de game niet over de import heen kan opslaan + Importeren in %1$s + + %1$d bestand uit de %2$s geïmporteerd in %3$s + %1$d bestanden uit de %2$s geïmporteerd in %3$s + + + %1$d bestand uit de %2$s geïmporteerd in %3$s. Oude savegames als back-up bewaard in Download/DroidDeck/Saves/backups + %1$d bestanden uit de %2$s geïmporteerd in %3$s. Oude savegames als back-up bewaard in Download/DroidDeck/Saves/backups + + Kies een savegame-zip voor %1$s + %1$s exporteren + + %1$d bestand geëxporteerd als %2$s: %3$s + %1$d bestanden geëxporteerd als %2$s: %3$s + + Kies waar je %1$s wilt opslaan (%2$s) + %1$s mislukt: %2$s + Nog geen sessielogs: start eerst een sessie. + Geen sessielogs om te wissen. + + %1$d map met sessielogs gewist. + %1$d mappen met sessielogs gewist. + + Koppelen voor draadloze foutopsporing mislukt + Opdracht voor draadloze foutopsporing mislukt + Draadloze foutopsporing is niet beschikbaar + ADB-opdracht gekopieerd + Kies de ROM-map + Geen Adreno-GPU + Turnip ondersteunt Adreno-GPU\'s. Op %1$s kan Steam een zwart scherm tonen. Download: %2$.0f MB. + Toch installeren + Linux-runtime verwijderen + Hiermee verwijder je de runtime, de Steam-client erin en alle games die daar zijn geïnstalleerd. + Toegepast: %1$s + Kan %1$s niet openen op het hoofdscherm + Kan %1$s niet openen op %2$s + Kan %1$s niet openen op scherm %2$d + Importeren + Wisselen + Herstellen + Annuleren + Verwijderen + kan het bestand niet lezen + %1$s geïmporteerd + De wachtende wissel is geannuleerd. + Kies een componentpakket (-linux .wcp) + De Nightlies zijn niet bereikbaar + %1$s opgeslagen + Downloaden mislukt: %1$s + Kies een Linux-runtimedriver (-Linux-zip of Android + Linux-bundel) + Kies een weergavedriver (AdrenoTools-zip of Android + Linux-bundel) + Kies een driver-zip of een Android + Linux-bundel + %1$s: %2$s + %1$s verwijderen… + %1$s vergeten… + Kies de map voor gameopslag + Kies een Decky-plug-in (ZIP) + Niet bruikbaar: %1$s + Programma + Linux-runtime verwijderen + Kan Instellingen niet openen op het onderste scherm; geopend op het hoofdscherm. + Open Ontwikkelaarsopties in Instellingen + + Kan de Android-instelling “Onbekende apps installeren” niet openen + Kan niet controleren: %1$s + Deze build heeft geen download voor deze versie van DroidDeck + Stop eerst de actieve sessie en update daarna + Downloaden + Installeren + Wachten op je bevestiging + De update is mislukt + Android heeft niet aangeboden om de update te installeren + + Installatie van Decky mislukt: %1$s + Plug-in importeren… + Importeren van plug-in mislukt: %1$s + Plug-in geïnstalleerd. Start Steam opnieuw om hem te laden. + + Nog niet gecontroleerd - tik op vernieuwen om nieuwe drivers te zoeken + Android + Linux: stelt ook de weergavedriver in + Android + Linux: stelt ook de runtimedriver in + de Turnip die in de runtime is ingebouwd + systeem-Vulkan: geen meegeleverde build voor deze GPU + %1$s (meegeleverd) + Handmatig + %1$s · %2$s + Er is momenteel maar één helft gepubliceerd + %1$.0f MB voor beide + Geen drivers om in te stellen: %1$s + Nieuwste drivers zoeken… + Up-to-date (kan niet controleren: %1$s) + De aanbevolen drivers zijn momenteel niet gepubliceerd + %1$s %2$s downloaden… + Geüpdatet naar %1$s %2$s. + Geüpdatet naar %1$s %2$s. De weergavedriver werkt na een herstart van DroidDeck. + Overgeschakeld naar %1$s %2$s. + Overgeschakeld naar %1$s %2$s. De weergavedriver werkt na een herstart van DroidDeck. + %1$s %2$s wordt gebruikt. + %1$s %2$s wordt gebruikt. De weergavedriver werkt na een herstart van DroidDeck. + Geen driver-zip: geen AdrenoTools-driver, geen -Linux-Turnip en geen Android + Linux-bundel + Importeren mislukt: %1$s + Geïmporteerde driver + %1$s wordt gebruikt als runtime- én weergavedriver. + %1$s wordt gebruikt als runtime- én weergavedriver. De weergavedriver werkt na een herstart van DroidDeck. + %1$s verwijderd (beide drivers) + %1$s verwijderd + Nieuwste: %1$s · gecontroleerd %2$s + Nieuwste: %1$s · %2$s onbereikbaar · gecontroleerd %3$s + + %1$d min geleden + %1$d min geleden + + + %1$d u geleden + %1$d u geleden + + + %1$d dag geleden + %1$d dagen geleden + + Banners-Turnip, WinNative en DroidDeck controleren… + controle mislukt + %1$s geïnstalleerd - kies hem in een van beide lijsten om beide drivers in te stellen + %1$s geïnstalleerd - kies hem in het menu + + Wacht tot de runtime is verwijderd + Game + touchpad + direct + Wisselen mislukt: %1$s + Profiel opslaan mislukt: %1$s + De desktop is gepauzeerd + %1$s is gepauzeerd + Het programma is gepauzeerd + Desktop wordt gestart + %1$s wordt gestart + Programma wordt gestart + Geen logs voor deze sessie. + Kan het archief met sessielogs niet maken. + Kan de sessielogs niet delen. + Sessie wordt gestart… + Linux-runtime downloaden + Linux-runtime downloaden · %1$d van %2$d MB + Linux-runtime controleren + Linux-runtime uitpakken + desktop downloaden + desktop downloaden · %1$d van %2$d MB + desktop controleren + desktop uitpakken + Proton Experimental (ARM64) downloaden + Proton Experimental (ARM64) downloaden · %1$d van %2$d MB + Proton Experimental (ARM64) controleren + Proton Experimental (ARM64) uitpakken + Kan de runtimecatalogus niet bereiken. Controleer de verbinding en druk opnieuw op Spelen. + De Linux-runtime is niet geïnstalleerd. Controleer de runtime bij Installatie en druk opnieuw op Spelen. + De desktop is niet geïnstalleerd (%1$s). Controleer de verbinding en druk opnieuw op Desktop. + %1$s is onverwacht gestopt. Deel de logs in een bugrapport of probeer het opnieuw. + Het programma is onverwacht gestopt. Deel de logs in een bugrapport of probeer het opnieuw. + De desktop is onverwacht gestopt. Deel de logs in een bugrapport of probeer het opnieuw. + Afsluitstatus %1$d · %2$s + geen log + + Het log toont %1$d fout “Function not implemented”: de seccomp-versnelling van proot laat op dit apparaat een hulpprogramma falen. Probeer %2$s → “%3$s” (of “%4$s”) en start opnieuw. + Het log toont %1$d fouten “Function not implemented”: de seccomp-versnelling van proot laat op dit apparaat een hulpprogramma falen. Probeer %2$s → “%3$s” (of “%4$s”) en start opnieuw. + + + Plug-ins + Installeer een plug-in-ZIP voor de volgende Steam-sessie. + Downloaden… + Installeren uit ZIP + Games + Audio & video + Grafisch + Internet + Kantoor + Ontwikkeling + Onderwijs + Wetenschap + Systeem + Hulpprogramma\'s + Populair + In opkomst + Nieuw + Bijgewerkt + laden… + Flatpak instellen + %1$s: %2$s + Kan Flathub niet controleren op updates + geïnstalleerd + map + Grafiet + Grafietkleurige vlakken, met het eigen bolblauw van het logo voor alles wat geselecteerd, gefocust of ingedrukt is. + Papier op zwart + Wit is de hoofdkleur, het blauw van het pictogram is het signaal voor lijnen, ringen en stippen. + Pictogramblauw + Zwarte achtergrond, witte tekst, het blauw voor alles wat geselecteerd, gefocust of ingedrukt is. + Elektrisch marineblauw + Het blauw feller, op vlakken die naar marineblauw neigen in plaats van grijs. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Spatie + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Installeer eerst de runtime + Steam-bibliotheek is niet beschikbaar + Vorige SD-bibliotheek is niet beschikbaar + Gameopslag is niet beschikbaar + Map met toegevoegde games is niet beschikbaar + Steam-manifesten zijn niet beschikbaar + Veel systemen + Intern + Linux-systeem + SD-kaart + Opslag + Selecteren + Eigenschappen + Toevoegen aan favorieten + Verwijderen uit favorieten + Naam wijzigen + Kopiëren + Knippen + Acties + Favorieten + Huidige map vastzetten + Nog geen favorieten - zet een map vast via het ⋮-menu om hier snel naar terug te gaan. + Verwijderen uit favorieten + 0 bytes + bytes + Locatie + Type + Grootte + Gewijzigd + Map + Bestand + %1$s-bestand + Alleen-lezen + Voorkomt dat games (en Wine) dit bestand overschrijven of verwijderen. + Kan Alleen-lezen niet wijzigen + Verborgen + Deze opslag kan de verborgen-markering niet opslaan. + Markeert het bestand als verborgen in Windows (het verborgen-kenmerk van Wine). + Kan Verborgen niet wijzigen op deze opslag + Klaar + Verwijderen... + Verwijderen mislukt + Verplaatsen... + Kopiëren... + Verplaatsen - %1$s + Kopiëren - %1$s + Verplaatsen %1$d/%2$d - %3$s + Kopiëren %1$d/%2$d - %3$s + %1$d klaar, %2$d mislukt + %1$d klaar, %2$d overgeslagen + + %1$d item verplaatst + %1$d items verplaatst + + + %1$d item gekopieerd + %1$d items gekopieerd + + “%1$s” bestaat al + Naam wijzigen... + Naam wijzigen mislukt + Map maken... + Kan map niet maken + Nieuwe map + Mapnaam + Maken + Nieuwe naam + Verwijderen? + “%1$s” definitief verwijderen? + + %1$d item verwijderen? + %1$d items verwijderen? + + Dit kan niet ongedaan worden gemaakt. + + …en nog %1$d + …en nog %1$d + + Verwijderen %1$d/%2$d - %3$s + + %1$d kon niet worden verwijderd + %1$d konden niet worden verwijderd + + Samenvoegen voegt bestanden toe aan de bestaande map en vervangt ze daar. + Overschrijven vervangt het bestaande bestand. + Toepassen op alle conflicten + Samenvoegen + Overschrijven + Beide behouden + Overslaan + Deze map selecteren + Terug + %1$s is gekoppeld, maar nu niet leesbaar + Lijstweergave + Rasterweergave + Zoeken + Sorteren + Naam + Datum gewijzigd + Ruime rijen + Compacte rijen + Verborgen bestanden verbergen + Verborgen bestanden tonen + Favorieten verbergen + Favorieten tonen + Deze map filteren + %1$s vrij + %1$d geselecteerd + Geen + Alle + Plakken + + %1$d item + %1$d items + + %1$s hier plakken + %1$s hier plakken (verplaatsen) + Bestanden + OPSLAG + SNEL + FAVORIETEN + Downloads + Downlds + ROMs + Sessielogs + Afbeeldingen + “%1$s” toegevoegd aan favorieten + “%1$s” verwijderd uit favorieten + Lege map + Eén niveau omhoog + Kies een map + Kies een bestand + Steam-blauw + Hemelsblauw + Wit + Violet + Groen + Amber + Rood + Roze + A-knop + B-knop + X-knop + Y-knop + Linkerbumper + Rechterbumper + Linkertrigger + Rechtertrigger + Weergaveknop + Menuknop + Weergave + Menu + D-pad omhoog + D-pad omlaag + D-pad links + D-pad rechts + Verborgen + Sleep om te verplaatsen + Uitlijnen + Opslaan + Geen + Toetsenbord + trackpad + Terminal + Deck-grepen + trackpads + + Linux-runtime verwijderen + + Linux-runtime verwijderen · %1$d item + Linux-runtime verwijderen · %1$d items + + Linux-runtime verwijderd + Kan het verwijderen van de Linux-runtime niet voltooien. Probeer het opnieuw bij Installatie. + Restanten van de runtime verwijderen + Downloaden + Verifiëren + Uitpakken + Verschijnt tijdens het installeren of verwijderen van de Linux-runtime + Linux-runtime installeren + Linux-runtime verwijderen + + De Linux-runtime is niet geïnstalleerd + De catalogus heeft geen checksum voor %1$s + Checksum komt niet overeen - er is niets gewijzigd + %1$s installeren + Kan de AppImage niet plaatsen + Uitpakken mislukt + Installatie mislukt + + Flatpak instellen mislukt (exitcode %1$d) + Flatpak instellen is niet voltooid + Er loopt al een andere winkeltaak + Stel eerst Flatpak in + Starten + Flatpak is gestopt met status %1$d + %1$s installeren + %1$s updaten + %1$s verwijderen + %1$s installeren (%2$d/%3$d) + %1$s updaten (%2$d/%3$d) + %1$s verwijderen (%2$d/%3$d) + vertalingen + grafische drivers + mediacodecs + %1$s-runtime + %1$s-runtime %2$s + + Het bestand is te klein voor een AppImage + Het bestand kan niet worden gelezen: %1$s + Dit is geen AppImage (het is geen Linux-programma) + Deze AppImage is niet gebouwd voor ARM64 (aarch64) of x86-pc\'s + Dit is een Linux-programma, maar geen AppImage + Alleen AppImages van type 2 kunnen worden geïmporteerd (deze is type %1$d) + Niet genoeg vrije ruimte: importeren vraagt ongeveer %1$s + Niet genoeg vrije ruimte: updaten vraagt ongeveer %1$s + Importeren mislukt + Die AppImage bestaat niet meer + Update mislukt + %1$s uitpakken + De AppImage kan niet worden uitgepakt + De AppImage kan niet worden uitgepakt: %1$s + + Stop de actieve sessie voordat je Decky Loader installeert + Downloaden van Decky Loader mislukt + De grootte van Decky Loader kwam niet overeen met de metadata van de release + Kan de checksum van de release niet downloaden + Checksum van de release ontbreekt + Checksum van de release is ongeldig + Checksum van Decky Loader komt niet overeen + Release-bestand is geen compatibel 64-bit Linux-programma + Er is een sessie gestart tijdens het downloaden; stop die voordat je Decky Loader installeert + Kan PluginLoader niet als uitvoerbaar markeren + Kan foutopsporing op afstand voor Steam CEF niet inschakelen + Kan PluginLoader niet klaarzetten + Kan de bestaande PluginLoader niet behouden + Kan de nieuwe PluginLoader niet activeren + Stop de actieve sessie voordat je een plug-in installeert + Installeer Decky Loader voordat je een plug-in importeert + Kan het geselecteerde zip-bestand niet lezen + Kan de map voor Decky-plug-ins niet maken + Zip-bestand van de plug-in uitpakken + Het zip-bestand van de plug-in moet precies één hoofdmap met de plug-in bevatten + In het zip-bestand van de plug-in ontbreekt plugin.json, package.json of dist/index.js + Naam van de plug-in ontbreekt + Er is een sessie gestart tijdens de installatie; stop die voordat je een plug-in installeert + Plug-in geïnstalleerd · start Steam opnieuw om hem te laden + Installeren van het zip-bestand van de plug-in is mislukt + Het zip-bestand van de plug-in bevat te veel items + Het zip-bestand van de plug-in bevat een ongeldig pad + Het zip-bestand van de plug-in bevat dubbele paden + Het zip-bestand van de plug-in bevat een onveilig pad + Kan de map van de plug-in niet maken + Het zip-bestand van de plug-in is na uitpakken groter dan de limiet van 1 GiB + Kan het meegeleverde binaire bestand van de plug-in niet als uitvoerbaar markeren + Ongeldig remote_binary-item + Ongeldige naam voor extern binair bestand + De URL van het externe binaire bestand moet HTTPS gebruiken + Extern binair bestand %1$s heeft een ongeldige SHA-256-hash + Kan de bin-map van de plug-in niet maken + Binair bestand van de plug-in downloaden · %1$s + Kan het binaire bestand van de plug-in %1$s niet downloaden + Checksum van het binaire bestand van de plug-in komt niet overeen · %1$s + Kan het binaire bestand van de plug-in niet als uitvoerbaar markeren · %1$s + Kan het binaire bestand van de plug-in niet vervangen · %1$s + Kan het binaire bestand van de plug-in niet installeren · %1$s + Deze map hoort al bij een andere plug-in + Kan de bestaande plug-in niet behouden + Kan de geïmporteerde plug-in niet activeren + Kan de instellingen van de Decky-plug-in niet bijwerken + Kan de bestaande plug-in niet herstellen + + Kan de identiteit voor draadloze foutopsporing niet opslaan + ADB-opdracht mislukt (%1$d) + Android heeft niet bevestigd dat de limiet voor onderliggende processen is gewijzigd: %1$s + Er is geen koppeling voor draadloze foutopsporing opgeslagen + Voer het huidige IP-adres en de poort voor draadloze foutopsporing in + + Wachten op de pop-up met de koppelcode duurde te lang. Probeer het opnieuw vanuit DroidDeck. + Kan niet naar de pop-up met de koppelcode zoeken (%1$d). Controleer of wifi aan staat. + Kan niet naar de pop-up met de koppelcode zoeken: %1$s + Voer alle 6 cijfers van de code in + Koppelen… + Die code werkte niet. Open de pop-up met de koppelcode opnieuw en voer de nieuwe code in. + Gekoppeld. Verbinden… + Gekoppeld, maar de poort voor draadloze foutopsporing is niet gevonden. Laat draadloze foutopsporing aan staan en probeer het opnieuw. + Instelling toepassen… + Opdracht voor draadloze foutopsporing mislukt + Draadloze foutopsporing instellen + Vraagt om de koppelcode voor draadloze foutopsporing terwijl Instellingen open is + Draadloze foutopsporing koppelen + Schakel draadloze foutopsporing in en tik vervolgens op “Apparaat koppelen met koppelcode”. + Schakel draadloze foutopsporing in en tik vervolgens op “Apparaat koppelen met koppelcode”. Blijf in Instellingen: de code komt in deze melding. + Koppelcode van 6 cijfers + Tik op Code invoeren en typ de 6 cijfers die in Instellingen worden getoond. + Voer de Wi-Fi-koppelcode in + Probeer de code opnieuw + Code invoeren + Steam instellen + Steam is klaar + Tik om terug te gaan naar DroidDeck. Je kunt draadloze foutopsporing nu uitschakelen. + Instellen van draadloze foutopsporing gestopt + + De download antwoordde met HTTP %1$d + De downloadgrootte kwam niet overeen met de gepubliceerde APK en is verwijderd - probeer het opnieuw + De download kwam niet overeen met de checksum en is verwijderd - probeer het opnieuw + De update is voor Android-package %1$s, niet voor %2$s + De metadata van de update vermeldt niet de ondertekeningssleutel van DroidDecks release + Android kon de gedownloade APK niet lezen + De gedownloade APK is voor %1$s, niet voor %2$s + De gedownloade APK geeft versionCode %1$d aan, niet de gepubliceerde %2$d + Android installeert versionCode %1$d niet over de geïnstalleerde %2$d heen + De gedownloade APK heeft geen ondertekeningsinformatie + De gedownloade APK is niet ondertekend met de release-sleutel van DroidDeck + De updatecatalogus antwoordde met HTTP %1$d + + Sessie wordt gestart… + %1$d:%2$02d verstreken · nog bezig + Update van de Steam-client downloaden · %1$d%% + Steam-client downloaden · %1$s + + Er wordt al een compatibiliteitstool geïnstalleerd + Stop de actieve sessie voordat je compatibiliteitstools installeert + Kan geen ARM64-release vinden voor %1$s + Om %1$s te installeren is minstens 4 GB vrije ruimte nodig, plus de downloadgrootte + %1$s %2$s downloaden + Download opnieuw proberen · poging %1$d van %2$d + Download onderbroken + Download mislukt; het deelbestand is bewaard zodat een nieuwe poging kan worden hervat + Download was groter dan de release en is verwijderd + Download was niet volledig; het deelbestand is bewaard zodat de poging kan worden hervat + Download verifiëren + Kan de checksum van de release niet lezen; probeer het opnieuw + %1$s %2$s publiceert geen checksum; er is niets geïnstalleerd + Checksum komt niet overeen; de download is verwijderd + Er is een sessie gestart tijdens het downloaden; stop die voordat je compatibiliteitstools installeert + %1$s installeren + Installeren van %1$s is mislukt (afsluitcode %2$d) + Kan %1$s niet installeren + Pakket droiddeck-esync ophalen + + met Linux-hulpprogramma’s + Alleen DLL’s + Origineel (Proton bijgewerkt sinds de laatste wissel) + Gewijzigd buiten Componenten + niet aanwezig + Origineel %1$s + Origineel van %1$s + %1$s %2$s wordt in %3$s geplaatst zodra de game die erop draait, wordt afgesloten. + %1$s %2$s staat nu in %3$s. Dit wordt toegepast bij de volgende gamestart. + Die originele bundel is niet opgeslagen + %1$s van %2$s gaat terug naar het origineel zodra de game die erop draait, wordt afgesloten. + %1$s van %2$s is hersteld vanuit het %3$s-origineel. + %1$s is in gebruik in %2$s. Wissel die Proton eerst naar iets anders. + %1$s is niet opgeslagen + %1$s verwijderd. + Het origineel van de geïnstalleerde build wordt bewaard: dat is de weg terug. + Niet opgeslagen + Het %1$s-origineel van %2$s is verwijderd. + Downloads komen alleen van de Nightlies-releases + Deze pakketlijst is ouder dan de checksums - vernieuw de lijst en probeer het opnieuw + Checksum komt niet overeen + + Lineair + Dichtstbijzijnde + Snapdragon GSR (kwaliteit) + Alleen verscherpen + Automatisch (volgt de schaling) + + Niets toegepast. + Game-helderheid + Verscherpen met een vleugje contrast - de dagelijkse optie. + Levendig + Kleur flink opgevoerd. Deband staat aan zodat luchten niet strepen. + Cinematisch + Dieper contrast, koelere kleur, randen afgevlakt. + Competitief + Schaduwen opgelicht en zware verscherping - zie anderen als eerste. + Adaptief verscherpen + Alleen verscherpen, kleur ongewijzigd. + Filmisch + Filmachtige curve, licht gedempt. + Arcade + Luid en krachtig. + Retro CRT + De CRT-buis en het analoge signaal samen toegepast. + Scherp opschalen + Voor gebruik onder de schermresolutie. + Scherpe pixels + Scherpe pixels voor 2D- en oudere games. + Anime-lijnen + Contourlijnen plus verscherping. + + Geen Adreno-GPU + Ondersteund + Experimenteel: de drivers zijn testbuilds + Onder de geteste hardware (Adreno 725 en nieuwer): werkt mogelijk niet + Niet ondersteund: DroidDeck heeft een Adreno-GPU (Snapdragon) nodig + + De ratelimiet van GitHub is bereikt - probeer het later opnieuw + GitHub antwoordde met HTTP %1$d + download antwoordde met HTTP %1$d + Deze driverlijst is ouder dan de checksums - vernieuw de lijst en probeer het opnieuw + Checksum komt niet overeen - de download is verwijderd + Geen AdrenoTools-driver: %1$s + geen meta.json + meta.json is onleesbaar (%1$s) + dit is een Linux-runtimedriver (importeer die onder “Linux-runtimedriver”) + dit is een Wayland-gamedriver, waar deze app niets aan heeft + meta.json noemt geen libraryName, dus er kon niets aan AdrenoTools worden doorgegeven + meta.json noemt %1$s, dat niet in de zip staat + %1$s is geen 64-bit AArch64 ELF shared library + %1$s koppelt aan glibc - een “-Linux”-Turnip, die onder “Linux-runtimedriver” hoort + Geen libvulkan_freedreno*.so in deze zip. Een Android-zip (AdrenoTools) of -Wayland-Turnip-zip is geen Linux-runtimedriver. + %1$s is geen 64-bit AArch64 ELF shared library. + %1$s is geen glibc-driver - hij koppelt aan de libc van Android. De Linux-runtime heeft een “-Linux”-zip nodig; een gewone of “-Wayland”-Turnip kan door de Steam-client helemaal niet worden geladen. + Geen driverbundel: geen manifest.json dat een android- en een linux-driver noemt + Geen driverbundel: %1$s wijst buiten de bundel + Geen driverbundel: na uitpakken is dit groter dan %1$d MB + + SD-kaart + Map + + Sessielogs delen + + vereist Android 10 of nieuwer (schermlagen met een kleurruimte) + dit scherm geeft geen ondersteuning voor HDR aan + dit scherm ondersteunt %1$s, niet HDR10 + + deze GPU + Limiet voor onderliggende processen staat aan + Limiet voor onderliggende processen is niet ingesteld + Limiet voor onderliggende processen kon niet worden gecontroleerd + Limiet voor onderliggende processen staat uit + Limiet voor onderliggende processen niet nodig + %1$s Als dat niet mogelijk is, sluit dit apparaat aan op een computer met ADB en voer de onderstaande opdracht uit. Kom hierna terug en controleer opnieuw. + Steam-sessies kunnen starten. + Deze Android-versie gebruikt deze limiet niet. + Schakel “Beperkingen voor onderliggende processen uitschakelen” in bij Ontwikkelaarsopties en laat Ontwikkelaarsopties daarna aan staan. + Deze Android-versie heeft hier geen schakelaar voor. Gebruik draadloze foutopsporing om dit vanaf dit apparaat te wijzigen. + Deze build heeft geen download voor dit exemplaar van DroidDeck. + Android installeert dit niet over deze build heen, omdat de versionCode %1$d lager is dan de geïnstalleerde %2$d. + Android installeert geen oudere versie over een nieuwere heen. Blijf op deze build totdat het kanaal is bijgewerkt. + Dit exemplaar van DroidDeck is anders ondertekend (een lokale build?), waardoor Android het niet ter plekke kan bijwerken. + Er is niet genoeg vrije ruimte voor de update. + De update is geannuleerd. + De update is niet geïnstalleerd: %1$s + De update is niet geïnstalleerd. + %1$s-zip + zip + %1$s is nog niet gestart + Geen savegames gevonden voor %1$s + %1$s heeft geen drive_c-map, dus het is geen savegame-zip van GameHub of Winlator + kan geen mappen maken in %1$s + kan niet schrijven in %1$s + kan niet schrijven in %1$s (%2$s) + Map is niet beschikbaar + Een ander bestand gebruikt al %1$s + Artwork-map is niet beschikbaar + Map heeft een synchronisatie-inventaris die hier niet bij hoort + Kan %1$s niet verwijderen + Bestand verlaat de exportmap + FEX (build zonder versienaam) + geen profile.json in %1$s + %1$s is geen Linux-componentpakket (type %2$s) + %1$s · %2$s vrij + Automatisch + De updatecatalogus voldeed niet aan de controles (%1$s) + DroidDeck-instelling voor onderliggende processen + Aangemeld · offline modus + Aangemeld + MEEGELEVERD + GEDOWNLOAD + GEÏMPORTEERD + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Sessiescherm + Resolutie + 16:9-beeldvulling + Hoe 16:9-games dit scherm vullen. Volgende sessie. + Kies Schermformaat of een aangepast formaat dat smaller is dan 16:9. + Beeldverhouding behouden + Uitrekken naar scherm + Beeldschaling + Schaalfilter + Gedeeld door Steam en Desktop. + Wordt direct toegepast. Gedeeld door Steam en Desktop. + FSR, GSR en NIS worden toegepast bij het vergroten van het sessiebeeld. Alleen verscherpen werkt ook zonder vergroting. + Filterscherpte + Framerate + %1$s Zet schaling op %2$s. + Schermformaat (%1$d×%2$d) + Scherm volgen (past formaat aan bij vouwen of uitvouwen) + Steam + Apparaatgegevens tonen + Apparaatgegevens verbergen + Apparaat lezen… + Kopiëren + Kopieer deze gegevens in een bugrapport. + Gekopieerd. + SSBS forceren + Houdt een CPU-snelheidsinstelling aan die Wine steeds uitschakelt. Kan Windows-games sneller maken op Snapdragon 8 Elite; op oudere chips is weinig verschil te zien. Geldt bij de volgende gamestart. + + Winkels + Steam + Aangepast + Winkels + Open een winkel op + Niveau voor downloadsnelheid + Winkels-engine + ● Engine gereed + Niet gebouwd + Gebalanceerd + Snel + Max + Winkels + Instellingen voor winkels + Instellingen + (niet aangemeld) + Winkel + Geïnstalleerd + Bibliotheek + Alle + Downloads + %1$s (%2$d) + Aanmelden + %1$s · afmelden + Afmelden bij %1$s? + Geïnstalleerde games blijven staan. De bibliotheek wordt opnieuw opgehaald wanneer je je weer aanmeldt. + Aangemeld blijven + Afmelden + Zoeken in de catalogus van %1$s… + Geïnstalleerde games zoeken… + Zoeken in je bibliotheek… + Zoeken + Niets geïnstalleerd + Laden + Geen games + Winkel laden… + Geen catalogus + Nieuw + Aanbiedingen + Gratis games + Populair + Jouw bibliotheek + Spelen + Installeren + Installatie hervatten + Onderbroken + Installeren · %1$s + Gratis verkrijgen + Bekijken op %1$s + Kopen · %1$s + Geïnstalleerd + In bibliotheek + Gratis + Hervatten + Vernieuwen + Inhoud voor volwassenen tonen + Epic + EOS + Offline + Uit + Epic-aanmelding (EOS) + Offline starten + Epic-aanmelding oplossen + Cloudsaves + Uploaden naar cloud + Downloaden vanaf cloud + Geen cloudsaves + Cloudconflict + Cloudconflict · %1$d bestanden + … + Cloud behouden + Lokaal behouden + Cloud controleren… + Laatste synchronisatie %1$s + Nog niet gesynchroniseerd + %1$d bestanden + geïnstalleerd + in je bibliotheek + winkel + Prestaties + %1$d van %2$d ontgrendeld + %1$d / %2$d + Ontgrendeld · %1$d + Vergrendeld · %1$d + Verborgen · %1$d + Verborgen prestatie + Details worden getoond zodra deze is ontgrendeld. + Alle prestaties ontgrendeld + Laden… + Log in bij %1$s om je prestaties te bekijken. + Kan %1$s niet bereiken. Controleer opnieuw zodra je online bent. + %1$s vermeldt geen prestaties voor deze game. + Alle %1$d weergeven + Prestaties · %1$s + ✓ In Steam + Verwijderen + Verwijderen… + %1$s verwijderen? + Maakt %1$s vrij en verwijdert het uit Steam. + Verwijdert de bestanden en de Steam-snelkoppeling. + Steam herkent de game zodra het de volgende keer start; start de game vanuit Steam, of start Steam nu. + Proton + Steams keuze per game + Audio + DirectAudio + Componenten + FEX, DXVK, VKD3D + Engine + %1$s · native + %1$s · ingebouwd + Hoe het draait + Opgehaald van + %1$s, met jouw account + Geïnstalleerd op + Gameopslag (Games/%1$s/%2$s) + Gestart door + Steam en Proton, hetzelfde pad als Steam-games + Steam en Proton · Epic-aanmelding per start, EOS-overlay uit + Steam en Proton · Amazon Games Services-variabelen ingesteld + Steam + toegevoegd aan Steam bij installatie (Deck-UI + Steam Input) + Geen downloads + Gepauzeerd + In wachtrij + In wachtrij · #%1$d + Geannuleerd + Mislukt + Mislukt · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Pauzeren + Hervatten + Annuleren + Opnieuw proberen + Wissen + Voltooide wissen + Manifest + Download + Verifiëren + Installeren + Klaar + Controleren + Downloaden + Download verwijderen? + Mislukt · %1$s bewaard + Verifiëren + Installeren + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d bestanden + Downloadbeheer + Downloads tegelijk + Installeren op + %1$s installeren: + Intern + %1$s vrij + Winkeldownloads + Toont de voortgang van gamedownloads en houdt downloads op de achtergrond actief + Downloaden + %1$d downloads + Voorbereiden… + Voltooide winkeldownloads + %1$s geïnstalleerd + Download van %1$s mislukt + Verwijderen + Bewaren + Aanmelden bij %1$s + Aanmelden bij je provider + Opnieuw laden + Sluiten + Probeer het opnieuw + Aanmelden afronden… + De aanmeldpagina kon niet worden geladen: %1$s + Deze provider staat geen aanmelding via een ingesloten pagina toe. Meld je in plaats daarvan aan met je winkelwachtwoord. + De aanmeldpagina gaf een foutmelding (HTTP %1$d). + De verbinding met de aanmeldpagina is niet beveiligd. + De aanmelding kon niet worden geverifieerd. Probeer het opnieuw. + Aanmelden mislukt. Probeer het opnieuw. + Sluiten + Terug + Bewerken + Alle games in deze map toevoegen + Niets nieuws in deze map + Naam + Doel + Beginnen in + Opstartopties + Artwork + Andere exe… + Verwijderen + Cover + Achtergrond + Logo + Pictogram + Gamemap + Steam + SteamGridDB + Jouw bestand + Vanuit de exe + Automatisch + Afbeelding kiezen… + Kies een afbeelding: %1$s + Terugzetten naar automatisch + + %1$d game toegevoegd + %1$d games toegevoegd + + + %1$d al in Games + %1$d al in Games + + B · Terug naar %1$s + de game + Spelinstellingen + Grafische drivers + Deze Proton + Elke Proton + Voor elke Proton + Beschikbaar · %1$d + Gecontroleerd %1$s + Controleren op nieuwe componenten en drivers + Ophalen + %1$s · DXVK, VKD3D-Proton en FEX hieronder zijn van deze Proton + %1$s · getest + %1$s · ongetest + %1$s · niet ondersteund + Houdt het aanbevolen paar actueel + Automatisch · %1$s + Runtime- en weergavedriver afzonderlijk + %1$s gebruiken? + %1$s verwijderen? + Diagnostiek + Klaar om te spelen + Nog niet klaar om te spelen + runtime %1$s + proceslimiet uit + aangemeld als %1$s + Als iets je aandacht nodig heeft + Weergave + Dit apparaat + Bibliotheekartwork + LINUX-APPS + Flathub + Bureaubladapps voor het Linux-bureaublad, geopend vanuit Desktop. + Flathub instellen + %1$s verwijderen? + De instellingen en bestanden worden bewaard. + Bewaren + Grootte op schijf + De desktop verwijderen? + Alle + GOG-prestaties + Synchroniseert prestaties voor ondersteunde GOG-games. Geldt vanaf de volgende start. + Prestaties uit + Prestaties aan + Log opnieuw in bij GOG + Prestaties geüpload + Uploaden van prestatie in behandeling + Zet GOG-prestaties aan in de Stores-instellingen + Prestaties niet beschikbaar; controleer sessielogs + Prestatie ontgrendeld + Plakken + Richtingstoetsen + Linkerstick + Rechterstick + Menu + Toetsenbord + Richtingstoetsen omhoog + Richtingstoetsen omlaag + Richtingstoetsen links + Richtingstoetsen rechts + Aanraakbediening + Aanraakcontroller + Muis + Aan + Uit + Langzaam + Normaal + Snel + Indeling bewerken + DroidDeck-menu + Sleep bedieningselementen naar het scherm + Schikken + Negeren + Opslaan… + Pictogram + B− + B+ + H− + H+ + Verbergen + Kleur en transparantie… + Deze indeling kopiëren + Indeling gekopieerd; plak deze in de aanraakindeling van elke game + Gekopieerde indeling plakken + Terugzetten naar Steams standaard + Deze bediening heeft geen toewijzing in Steam om er een pictogram aan te geven + Pictogram voor %1$s + Kleur van aanraakindeling + Klaar + De aanraakindeling kon niet worden opgeslagen + Aanraakindeling opgeslagen in Steam + App %1$d + Label (optioneel) + Steams pictogrammen zijn niet gevonden + + %1$d pictogram van Steam + %1$d pictogrammen van Steam + + Pictogramkleur + Knopkleur + Geen pictogram + + Aanmelding controleren… + Je bibliotheek ophalen… + Gamelijst ophalen… + Gamedetails laden… (%1$d/%2$d) + + %1$d game synchroniseren… + %1$d games synchroniseren… + + Synchroniseren… %1$d/%2$d + Coverafbeeldingen ophalen… + Synchronisatie mislukt + Geen games gevonden in je %1$s-bibliotheek + Amazon Games reageerde niet voor je bibliotheek: vernieuw, of meld je opnieuw aan + Kan GOG niet bereiken + Je GOG-bibliotheek kon niet worden gelezen + Registreren bij Steam… + Cardboard VR + Cardboard-modus + Beelden voor linker- en rechteroog voor een VR-bril voor je telefoon. + Beeld + Virtueel scherm + Side-by-side 3D + Bril + Cardboard + Cardboard (2014) + Geen lens + Oogafstand + Schermbreedte + Schermafstand + Hoofdtracking + Dit apparaat heeft geen rotatiesensor. + Weergave centreren + Centreren + SteamVR (experimenteel) + Installeren + Downloadt Valves SteamVR voor ARM64 (571 MB). + SteamVR downloaden en uitpakken… + Starten + Draait SteamVR met je hoofd als headset; het beeld schakelt naar side-by-side 3D. + Stoppen + SteamVR draait. + Niet beschikbaar in deze sessie. + SteamVR reageerde niet; controleer het sessielog. + Componenten downloaden + Downloads worden gedeeld met de pagina Componenten. Kies hierboven een gedownloade versie om die bij de volgende start van een game te gebruiken. + Kies een versie om te downloaden + Ontwikkelaar + Uitgever + Uitgebracht + Hoofdverhaal + + Extra\'s + Completionist + %1$s u + %1$d min + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 000000000..930e5a1f7 --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,2285 @@ + + + DroidDeck + + Przy pierwszym uruchomieniu pobierany jest Steam. Może to potrwać kilka minut. + Domyślnie wyjście z aplikacji wstrzymuje Steam; po powrocie wznowi on działanie tam, gdzie przerwał. + Twój kontroler jest widoczny jako pad Xbox 360. Bez niego użyj sterowania ekranowego. + + + Przy pierwszym uruchomieniu konfigurowany jest pulpit. Może to potrwać kilka minut. + Domyślnie wyjście z aplikacji wstrzymuje sesję; po powrocie wznowi się ona tam, gdzie została przerwana. + Ekran działa jak touchpad: przeciągnij, aby przesunąć kursor, dotknij, aby kliknąć. + Twój kontroler jest widoczny jako pad Xbox 360. Bez niego użyj sterowania ekranowego. + + Sesja + Widoczne, gdy otwarta jest sesja Steam lub pulpitu + Steam działa + Trwa pobieranie w Steam + Sesja wstrzymana – dotknij „Wznów”, aby kontynuować + Wznów + Wstrzymaj teraz + Zatrzymaj sesję + Generowanie klatek + Wybierz tryb generowania klatek + Wył. + Win-FG %1$d× + LSFG %1$d× + LSFG adaptacyjne %1$d + Wymaga Lossless Scaling ze Steam + Zainstaluj Lossless Scaling w Steam lub zaimportuj Lossless.dll + Importuj Lossless.dll… + Używana jest kopia ze Steam + Używana jest zaimportowana kopia + Wybierz Lossless.dll + Zaimportowano Lossless.dll + Ten plik Lossless.dll jest już używany + To konto Steam nie ma Lossless Scaling + Ten plik to nie Lossless.dll z Lossless Scaling + Nie udało się przygotować LSFG z tego pliku Lossless.dll + LSFG wymaga Lossless Scaling ze Steam lub zaimportowanego pliku Lossless.dll + Nie udało się przygotować shaderów LSFG z pliku Lossless.dll + %1$s: nieobsługiwane + %1$s: błąd silnika + %1$s: wymaga Lossless Scaling + %1$s %2$s · uruchamianie + Dodaje bibliotekę Steam i wyszukuje w niej gry na Windows. Posiadane gry korzystają ze swoich wpisów w Steam, a pozostałe stają się skrótami do gier spoza Steam. + Środowisko gry + Zmienne środowiskowe dla gier uruchamianych przez Proton. + Edytuj + Wszystkie gry Proton + Profil + ID aplikacji Steam + Otwórz + Gra %1$s + Dodaj zmienną + Nazwa zmiennej + Wartość + Zapisz + Usuń + Przywróć dziedziczenie + Wyczyść nadpisania środowiska + Nie ustawiaj w tym profilu + Brak ustawionych zmiennych. + Dziedziczona + Własna zmienna + Nie udało się wczytać ani zapisać. Sprawdź wolne miejsce i spróbuj ponownie. + Podaj prawidłową nazwę i wartość do 8192 znaków. + Standardowe środowisko uruchomieniowe Proton nie odczytuje tej zmiennej. + Wczytywanie… + Wymuszenie poziomu funkcji D3D12. 12_2 zgłasza możliwości DirectX 12 Ultimate; nieobsługiwane funkcje mogą zawieść. Pozostaw nieustawione, aby wykrywać automatycznie. + Wymuszenie modelu shaderów D3D12. 6_9 wymaga odpowiednio nowego VKD3D i pasujących funkcji GPU. Pozostaw nieustawione, aby wykrywać automatycznie. + Pozostaw włączoną pamięć podręczną shaderów Mesa, aby ograniczyć ponowną kompilację. false włącza pamięć podręczną. + Maksymalny rozmiar pamięci podręcznej shaderów Mesa, np. 1G. Większa pamięć podręczna zajmuje więcej miejsca. + Opcje VKD3D dla D3D12. nodxr wyłącza ray tracing; używaj tylko w grze, która tego wymaga. + Ustaw 1, aby tworzyć dziennik diagnostyczny Proton. Zapis dziennika może obniżyć wydajność i zajmuje miejsce. + Ustaw 1, aby do testów zgodności D3D9–11 używać WineD3D opartego na OpenGL zamiast DXVK. + Ustaw 1, aby ta gra nie korzystała z ntsync (droiddeck-ntsync ani sterownika jądra); użyje droiddeck-esync, jeśli istnieje pakiet, a w przeciwnym razie droiddeck-fsync. + Ustaw 1, aby wyłączyć droiddeck-esync w tej grze; zamiast niego użyje droiddeck-fsync lub droiddeck-ntsync, gdy fsync też jest wyłączone. + Ustaw 1, aby ta gra nie korzystała z droiddeck-fsync; użyje droiddeck-esync, jeśli istnieje pakiet, a w przeciwnym razie droiddeck-ntsync. + Ustaw 1, aby uruchamiać tę grę na droiddeck-fsync, lub 0, aby preferować droiddeck-esync, gdy pakiet pasuje do jej wersji Proton. + Ustaw 0, aby wyłączyć w tej grze pomocnika Proton do nawigacji kontrolerem. + Nakładka DXVK dla D3D9–11. fps pokazuje liczbę klatek; brak wartości ją ukrywa. + Ustawienia DXVK rozdzielone średnikami. Nowsze wersje ograniczają klatki przez dxvk.maxFrameRate; do zwykłej gry lepiej użyć limitu klatek sesji. + Limit liczby klatek D3D12. 0 wyłącza limit. + Wielowątkowość OpenGL w Mesa. true może pomóc grom OpenGL ograniczonym przez CPU; testuj każdą grę osobno. + Synchronizacja Wine + Jak gry synchronizują swoje wątki. Działa od następnej sesji. + esync + ntsync + fsync + wineserver + Preset FEX + Działa od następnego uruchomienia gry. + Gry + Domyślne FEX + Używa domyślnych ustawień FEX. + Stabilność + Najbezpieczniejsze, najwolniejsze ustawienia. + Zgodność + Pełne TSO z multiblock. + Pośredni + Zrównoważone TSO z obniżoną precyzją x87. + Wydajność + Całe TSO wyłączone; multiblock. + Wydajność + TSO + Ustawienia „Wydajność” z włączonym głównym TSO. + Ekstremalny + Wyłącza kontrole kodu samomodyfikującego się. Gry z JIT i DRM mogą przestać działać. + Ekstremalny + TSO + Ustawienia „Ekstremalny” z włączonym głównym TSO. + Ekstremalny-gn + Wydajność z TSC i wskazówkami metadanych. Wypróbuj przed „Ekstremalnym”. + Denuvo + Wydajność z pełnymi kontrolami kodu i ukrytym bitem hiperwizora. + Własna wartość + Wybierz wartość + Emulatory i aplikacje + Dodaj + Twoja własna aplikacja lub gra + Skrypt + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Uruchamianie… + Dodano: %1$s + Usunięto: %1$s + Pulpit + Tej aplikacji już nie ma. Wybierz ją ponownie z Pulpitu. + Otwórz %1$s + Usuń + Naciśnij ponownie, aby usunąć + Zatrzymaj sesję, aby usunąć + Uruchamia %1$s z jego folderu, podłączonego do sesji tam, gdzie się znajduje. Usunięcie aplikacji nie zmienia tego folderu. + Uruchamia %1$s z kopii jego folderu w DroidDeck. Usunięcie aplikacji kasuje kopię, a nie oryginał. + Najpierw zainstaluj środowisko Linux + Pliku już nie ma + Za mało wolnego miejsca: skopiowanie folderu wymaga %1$s + Kopiowanie %1$s + Nie udało się dodać aplikacji + Szukanie wydania %1$s + Nie udało się odczytać wydań %1$s + Żadne wydanie %1$s nie zawiera AppImage ARM64 ani x86_64 + Pobieranie %1$s + Pobieranie nie powiodło się + Nie udało się zaktualizować aplikacji + Nie udało się odczytać tej ikony + Zapisano: %1$s + Edytuj + Sprawdź aktualizacje + Sprawdzanie aktualizacji… + Aktualizuj do %1$s + Aktualna · %1$s + Zaktualizowano: %1$s + %1$s · %2$s + Dodaj aplikację + Skrypt + AppImage + GitHub + Flatpak + Plik .sh. Uruchamia się z własnego folderu, na pełnym ekranie w gamescope. + AppImage ARM64 (aarch64) lub x86_64 uruchamiany przez FEX. Jest jednorazowo rozpakowywany do środowiska Linux. + Instaluje AppImage ARM64 z najnowszego wydania repozytorium lub wersję x86_64 przez FEX. + Używaj FEX + Auto + Wł. + Wył. + FEX uruchamia programy Linux x86 na tym urządzeniu ARM64. Auto używa go, gdy program jest zbudowany dla x86. + %1$s przez FEX + Ten program jest zbudowany dla komputerów %1$s i działa przez FEX, który nie jest jeszcze skonfigurowany. Otwórz raz Steam po zalogowaniu: DroidDeck pobierze wtedy FEX i potrzebne Steam Linux Runtime, a potem program się otworzy. + Ten program + %1$s jest zbudowany dla komputerów x86 i działa przez FEX, który nie jest jeszcze zainstalowany. Uruchom raz Steam po zalogowaniu i pozwól mu pobrać FEX oraz Steam Linux Runtime, a potem uruchom program ponownie. + ID aplikacji z Flathub lub jej strona na Flathub. Jeśli Flatpak nie jest jeszcze skonfigurowany, zostanie skonfigurowany najpierw. + Plik + Nie wybrano pliku + Wybierz plik + Zmień + Wybierz skrypt + Wybierz AppImage + Wybierz ikonę + Repozytorium + właściciel/repo lub link github.com + To nie jest repozytorium GitHub + ID aplikacji + org.ppsspp.PPSSPP + To nie jest ID aplikacji z Flathub + Lub plik .flatpak dla aplikacji spoza Flathub. + Wybierz plik .flatpak + Źródło + Na pulpicie + Nowa aplikacja + Nazwa + Z aplikacji + Ikona + Domyślna ikona + Wybierz ikonę + Wyczyść + Z repozytorium + Szukanie ikon… + Domyślna + Edytuj %1$s + Zapisz + Sprawdzanie folderu… + Jego folder jest podłączony do sesji: %1$s + Jego folder zawiera programy Linux, których nie można uruchomić z pamięci współdzielonej, więc zostanie skopiowany do DroidDeck (%1$s). + Dodaj + Anuluj + Najpierw zainstaluj środowisko Linux w sekcji Konfiguracja. + Poczekaj, aż %1$s się zakończy. + Zgodny + Wersje oznaczone „%1$s” w menu Zgodność w Steam mają poprawki prefiksu od DroidDeck i uruchamiają tu gry. Domyślnie jest to Proton Experimental ARM64 (%1$s) lub Proton 11.0 ARM64 (%1$s), gdy ta nowsza wersja jest zainstalowana. Własne wpisy Proton od Valve bez tego oznaczenia nie uruchomią gry na tym urządzeniu. + Zainstalowano %1$s · %2$s + Anuluj + Auto + Nigdy + Zawsze + Odinstaluj + Sprawdź + Aktualizuj + Sprawdzanie… + Przetwarzanie… + Zapomnij + Dodaj… + Użyj + Działa od następnej sesji. + Sesja Steam + Sesja pulpitu + Wyświetlacz + Rozdzielczość + Do 720p + Do 900p + Do 1080p + Natywna + %1$s (domyślna) + Własna · %1$d×%2$d + Własna… + 720p może poprawić responsywność menu. + Proporcje ekranu + Ustalane przez własną rozdzielczość. + Auto używa co najmniej 16:9. + Limit klatek + Ogranicza całą sesję. Wyświetlacz przełącza się na częstotliwość odświeżania będącą wielokrotnością limitu. + HDR + Wyjście HDR10 + Niedostępne: %1$s. + Uruchom ponownie aplikację, aby zastosować. + Sterowniki + Sterowniki GPU + Wspólne dla Steam i pulpitu. Ustawiane na stronie Komponenty. + Dotyk i sterowanie + Dotyk + Touchpad + Bezpośredni + Auto: touchpad na pulpicie, bezpośredni w Steam. + Sterowanie ekranowe + Steam + QAM + Auto pokazuje sterowanie, gdy nie jest podłączony żaden kontroler. + Dotyk Steam (beta) + Sterowanie dotykowe Steam zacznie działać od następnej sesji Steam + Układ dotykowy Steam + Przesuwaj, zmieniaj rozmiar lub ukrywaj sterowanie dotykowe tej gry; zapisywane w Steam + Kontroler + Jak Steam widzi Twój kontroler. Działa od następnej sesji. + Kontroler Steam Deck + Steam Controller + DualShock 4 + DualSense + Kontroler Xbox 360 + Deck: przycisk szybkiego dostępu i żyroskop. Steam Controller: to samo oraz wykrywanie chwytu na tylnych przyciskach. DualShock 4 i DualSense: żyroskop, a gładziki działają jako ich touchpad. Xbox 360: szybki dostęp otwiera Guide+A. + Wstecz + Menu → QAM + QAM → Menu + Sesja + Działanie w tle + Gdy aplikacja jest ukryta lub ekran się wyłącza. + Ręcznie + Natywnie + „Auto” wstrzymuje i wznawia po powrocie. „Natywnie” najpierw przygotowuje Steam do uśpienia. „Ręcznie” czeka na „Wznów”. „Nigdy” nie przerywa działania. + Nie przerywaj pobierania + Opóźnia wstrzymanie w tle, gdy Steam aktywnie pobiera. + „Auto” wstrzymuje w tle i wznawia po powrocie. „Ręcznie” czeka na „Wznów”. „Nigdy” nie przerywa działania. + Uruchamianie + Uruchamiaj Steam z DroidDeck + Otwiera sesję Steam przy uruchomieniu DroidDeck. + Decky + Sprawdzanie wydań… + Gotowe do instalacji: %1$s. + Nie znaleziono zgodnej wersji. Spróbuj ponownie później. + Zainstalowano · %1$s + Dostępna aktualizacja · %1$s + Aktualna · %1$s + Program ładujący + Zainstaluj najnowszą + Uruchamia się ze Steam. Gdy jest włączony, inne aplikacje na tym urządzeniu mogą sterować Steam. + Wyłączony. Port zdalnego debugowania Steam pozostaje zamknięty. + Odinstalować Decky Loader? + Usuwa program ładujący. Twoje wtyczki i ustawienia zostaną zachowane. + Klient + Nakładka wydajności + Wyłącz, jeśli gry ulegają awarii lub mają czarny ekran w trybie Deck. Działa od następnej sesji. + Napraw klienta Steam + Pobiera klienta Steam ponownie przy następnym uruchomieniu Steam. Użyj, jeśli Steam utknie na ekranie wczytywania. Gry, logowanie i ustawienia zostaną zachowane. + Klient Steam zostanie pobrany ponownie przy następnym uruchomieniu Steam. + Napraw + Grafiki + Klucz API SteamGridDB + Twój klucz + Edytuj… + Uzupełnia brakujące grafiki ze Steam, a potem z SteamGridDB. Pierwszeństwo mają Twoje pliki cover.jpg, hero.jpg, logo.png lub icon.png w folderze gry. + Usuń %1$s + Usunąć? + Dodaj grę + Dodaj grę: + Wybierz plik exe: + Ta lokalizacja nie jest dostępna w sesji + Sprawdź ten plik exe + Komponenty Windows + LB / RB przełączają między sekcjami. + Brak + Biblioteki DLL Windows, których Proton nie zawiera, z listy komponentów. Włącz te, których potrzebuje ta gra: trafiają do własnego folderu Windows gry, nigdy do jej plików, i zostają niezależnie od wersji Proton, na której działa. + Nie udało się pobrać listy komponentów. Już pobrane komponenty nadal można przełączać. + Zalecane dla tej gry + Sprawdzanie, czego potrzebuje ta gra… + Dostępne po połączeniu z internetem + + Włączanie %1$d komponentu… + Włączanie %1$d komponentów… + Włączanie %1$d komponentów… + Włączanie %1$d komponentów… + + Włączone dla tej gry + Steam instaluje %1$s razem z %2$s (aplikacja %3$s) + Z listy GOG dla tej gry + Z wymagań wstępnych Epic dla tej gry + Z kroków instalacji Amazon dla tej gry + Steam instaluje %1$s z aplikacją %2$s, dopasowano po nazwie + Również na liście + Wymienione przez %1$s; tutaj to nie jest komponent + Steam + GOG + Epic Games + Amazon Games + Gra dostarcza %1$s + Gra ma %1$s (często działa bez zmian) + Już jest w tej grze + Nic w folderze gry nie wskazuje na żaden komponent. + Zainstalowane dla tej gry (%1$d) + Wszystkie komponenty (%1$d) + Wymagają instalatora Windows (%1$d) + To osobne programy instalacyjne bez pakietu w środku, który można rozebrać, i jeszcze bez nagrania tego, co instalują. Większość to stare środowiska uruchomieniowe, które Mono w Proton już zastępuje; nagrania są dodawane, gdy powstają. + Pokaż + Ukryj + Uruchamia własny instalator (jeszcze brak nagrania) + Tutaj jeszcze nieobsługiwane + Wczytywanie komponentów… + Nie udało się zainstalować: %1$s + Pobrano + Pobieranie %1$s + Instalowanie: %1$s + Wyłączanie… + Do pobrania: %1$s + Zacznie działać przy następnym uruchomieniu gry. + Rozciągaj gry 16:9 + Na ekranach bardziej kwadratowych niż 16:9 rozciąga gry na cały ekran zamiast pokazywać pasy. Działa od następnej sesji. + Dźwięk + DirectAudio dla gier + Omija PulseAudio, aby zmniejszyć opóźnienie. + Mikrofon + Do czatu głosowego. + Sieć + Wykrywanie Wi-Fi + Pokazuj w Steam nazwy Wi-Fi i sieci w pobliżu. + Usługi lokalizacyjne są wyłączone. + Nie przyznano uprawnienia do lokalizacji. + Otwórz ustawienia + Dalej + Włączyć wykrywanie Wi-Fi? + Android wymaga uprawnienia do lokalizacji, aby odczytywać nazwy Wi-Fi i wykrywać sieci w pobliżu. DroidDeck używa tego dostępu, aby pokazać te sieci w Steam. Nie śledzimy Twojej lokalizacji. Dostęp do internetu działa bez tego uprawnienia. + Pamięć na gry + Automatycznie – karta SD, gdy jest, oraz Twoje foldery Games + Automatycznie – Twoje foldery Games oraz karta SD, gdy jest (teraz brak) + Tylko pamięć wewnętrzna + Folder: %1$s + Druga biblioteka + Gry, które strumieniują zasoby z karty SD lub pamięci współdzielonej, mogą się zacinać. + Diagnostyka pamięci + Zapisuje wyniki alokacji i próbkowane czasy dostępu do pamięci w „Udostępnij logi”. Po włączeniu może spowalniać instalacje. Działa od następnej sesji. + Wybierz folder… + Własna rozdzielczość + Zastępuje limit i proporcje. Przy rozmiarze niepasującym do ekranu pojawią się pasy. + Szerokość + Wysokość + Od 320×240 do 3840×2160. + + %1$d gra + %1$d gry + %1$d gier + %1$d gry + + OK + Usuń + Przywróć + Pobierz + Usuń %1$s + %1$.1f MB + Sterownik środowiska + Używany przez Steam, gry i pulpit. Działa od następnej sesji. + Importuj plik zip Turnip… + Sterownik wyświetlania + Rysuje klatki na ekranie w obu trybach. Uruchom ponownie aplikację, aby zastosować. + Importuj plik zip AdrenoTools… + nigdy + Wersje nightly sprawdzono: %1$s + Komponenty + Informacje o Komponentach + Sprawdź nowe sterowniki + Szukaj nowych pakietów w wersjach nightly + Proton + Wybrany tutaj Proton uruchamia każdą grę, która nie ma własnego wyboru w Steam, a zmiana dokonana w Steam też się tu pojawia. Steam zastępuje Proton od Valve przy aktualizacji; oryginały są zachowywane dla każdej wersji. + Konfiguracja + Skonfiguruj profil + Profil + Profil domyślny + Używany automatycznie przez gry bez własnych ustawień. + Zapisane ustawienia używane automatycznie przy uruchomieniu %1$s. + Wybierz tylko ustawienia, których potrzebuje ta gra. Cała reszta nadal korzysta z profilu domyślnego. + Preset FEX + Używany jest preset z profilu domyślnego. + Zapisano własny preset dla tej gry. + Własne zmienne FEX + Użyj domyślnego + Zgodność gry + Opcje środowiska Proton, DXVK, VKD3D i synchronizacji dla tej gry. + Używany jest Proton z profilu domyślnego. + Zapisano własny Proton dla tej gry przez mapowanie zgodności Steam. + Brak Proton + DXVK + VKD3D-Proton + Używa pakietu z profilu domyślnego z %1$s, chyba że ta gra wybierze inny pakiet. + Używa pakietu komponentów z profilu domyślnego. + Używa %1$s tylko w tej grze. + Opcje środowiska uruchomieniowego + Inne opcje środowiska Proton, DXVK, VKD3D-Proton i synchronizacji dla poszczególnych gier. + Profil gry %1$s zapisano na następne uruchomienie gry. + Uruchom bez Steam + Dla gry, która zamyka się od razu: uruchamia ją bez konfiguracji Steam w Protonie. Funkcje Steam, takie jak nakładka, są dla niej wyłączone. + Wł. + Wybór Proton dla %1$s zapisano i jest już dostępny w Steam. + Wybór Proton dla %1$s zapisano. Uruchom ponownie Steam przed uruchomieniem gry. + %1$s nie może używać wybranego Proton na tym urządzeniu. + Nie udało się wczytać profilu gry %1$s: %2$s + Nie udało się zapisać profilu gry: %1$s + Resetuj profil gry + Zresetować profil gry %1$s? + Usunąć ustawienia Proton, FEX, pakietu komponentów, środowiska, filtrowania tekstur i komponentów Windows dla tej gry? Będzie ona używać profilu domyślnego. + %1$s używa teraz profilu domyślnego. + Nie udało się całkowicie zresetować profilu gry: %1$s + %1$s uruchamia teraz każdą grę, która nie ma własnego Proton wybranego w Steam. + %1$s będzie uruchamiać każdą grę bez własnego Proton od następnego uruchomienia Steam. + %1$s nie może tu uruchamiać gier, więc nie może być domyślny. + Nie udało się zapisać domyślnego Proton: %1$s + %1$s · podmienione: %2$d + %1$s · wszystko oryginalne + %1$s · gra działa + %1$s · pakiet droiddeck-esync + %1$s · brak pakietu droiddeck-esync + %1$s · wbudowany esync, pakiet zbędny + Sterowniki GPU + %1$s · ponownie zastosowano przy uruchomieniu %2$s + Następnie: %1$s · po zamknięciu gry + %1$s w użyciu: %2$s + Czeka na zamknięcie gry + Działa od następnego uruchomienia gry + Importuj .zip + Odczytywanie wersji Proton… + Nie zainstalowano jeszcze żadnego Proton. Uruchom raz Steam, aby pobrać jego Proton ARM64, lub dodaj GE-Proton, CachyOS albo DroidDeck-Proton w sekcji Konfiguracja. + Oryginał · %1$s + %1$s · ta wersja + %1$s · wcześniejsza wersja + Zainstalowane · %1$d + Nic jeszcze nie zapisano dla %1$s. Własne pliki tego Proton są zapisywane jako jego oryginał. + Na tym Proton działa gra, więc zmiana poczeka do jej zamknięcia. + Zacznie działać przy następnym uruchomieniu gry. + Podmienić %1$s? + Umieścić %1$s w %2$s? Jego oryginalne pliki pozostaną zapisane. %3$s + Przywrócić %1$s? + Przywrócić oryginał %1$s w %2$s? %3$s + Usuwa pakiet z aplikacji. Możesz pobrać go ponownie. + Usunąć ten oryginał? + Ten oryginał %1$s pochodzi z wcześniejszej wersji %2$s. Oryginał bieżącej wersji jest zawsze zachowywany. + W wersjach nightly · %1$d + Odśwież, aby wyświetlić pakiety w wersjach nightly. + Nic nowego od ostatniego sprawdzenia. + Importuj .wcp + Przywróć oryginał tego Proton + Przywrócić oryginalny %2$s w %1$s? + Podmień + FEX, DXVK i VKD3D-Proton dla każdej wersji Proton, z którą Steam uruchamia gry.\n\nWybierz zainstalowany wiersz, aby go podmienić. Zmiana działa od następnego uruchomienia gry lub po zamknięciu działającej gry.\n\nTwoje wybory są sprawdzane przed każdym uruchomieniem i przywracane, jeśli coś je zmieniło. Oryginalne pliki każdej wersji Proton są zachowywane, więc aktualizacje Steam nigdy ich nie usuwają. + Oryginał + W użyciu + Nowy + Zapisany + Wydajność + Rdzenie CPU, poprawki sesji i nazwa hosta. Działa od następnej sesji. + Rdzenie klienta Steam + Zastąp wybór rdzeni przez Steam + Przypina Steam, jego pomocnika interfejsu i gamescope do rdzeni poniżej. + Rdzenie klienta + Rdzenie dla Steam. + Włącz zastępowanie, aby wybrać. + Rdzenie gier + Rdzenie gier + Rdzenie dla gier. Gdy wszystkie są zaznaczone, Android swobodnie przydziela zadania. + Interfejs klienta + Wielowątkowy GL + Może poprawić responsywność menu Steam. + Zink: leniwe deskryptory + Zalecane dla sterowników bez buforów deskryptorów. + Pomijaj sprawdzanie błędów GL + Wyłącza walidację GL przy każdym wywołaniu. + gamescope: kolejka GPU czasu rzeczywistego + Nadaje priorytet pracy GPU kompozytora. Może wygładzić tempo klatek kosztem części wydajności gier. + GPU + Utrzymuj GPU na najwyższym taktowaniu + Tylko Adreno. Mniej przycięć, większe zużycie baterii i więcej ciepła. + Poprawki sesji + Turnip: renderowanie sysmem + Szybsze dla Steam i większości gier. Wymagane na Adreno 710/720/722. + Pomijaj pomocnika xalia w Steam + droiddeck-ntsync (eksperymentalne) + Zastępnik sterownika Linux ntsync w przestrzeni użytkownika, do testów. Gdy jest włączony, Proton używa go zamiast droiddeck-esync i droiddeck-fsync. Zgodny z Proton Experimental ARM64, GE-Proton i proton-cachyos. Działa od następnej sesji. + droiddeck-esync + Najpierw droiddeck-fsync + Uruchamia własny fsync Proton na standardowych plikach Proton, z futex_waitv dostarczanym w przestrzeni użytkownika, więc nie wymaga pakietu i działa od razu po aktualizacji Proton przez Steam. Gdy jest włączony, gry używają go przed droiddeck-esync, z wyjątkiem nielicznych, którym Proton wyłącza fsync. Działa od następnej sesji. + Wątki Windows w grach czekają na siebie przez eventfd i pamięć współdzieloną zamiast przez wymiany z wineserver. Używa pakietu droiddeck-esync przygotowanego dla dokładnie tej wersji Proton; pakiety są pobierane automatycznie. Dopóki nie ma pakietu dla nowej wersji Proton, zastępuje go droiddeck-fsync. Działa od następnej sesji. + Wyłącza pomocnika Proton do nawigacji kontrolerem, który zużywa CPU w każdej grze. + Uruchamiaj proot bez seccomp + Może naprawić błędy brakujących wywołań systemowych na niektórych jądrach. Może obniżyć wydajność. + Szybka ścieżka proot + Wymaga filtra seccomp proot. + Szybsze wyszukiwanie plików i mniej przycięć. Wyłącz, jeśli program nie może znaleźć swoich plików. + Tożsamość sesji + Nazwa hosta + Nazwa tego urządzenia widoczna dla Steam. Puste pole przywraca %1$s. + Tylko litery, cyfry i łączniki w środku, do 63 znaków. + Android jest ustawiony tak, że zamknie tę sesję + Zainstalowane + Do pobrania + Nic nowego od ostatniego sprawdzenia. + Przywróć wbudowane sterowniki + Wbudowany w aplikację: usuwa to jego rozpakowane pliki i go ukrywa. „Przywróć wbudowane sterowniki” przywraca go z powrotem. + Połowa pakietu Android + Linux: obie połowy zostaną usunięte. Zamiast używanego sterownika zostanie użyty domyślny. + Jego pliki zostaną usunięte z aplikacji. + Jego pliki zostaną usunięte. Jest w użyciu, więc zastąpi go domyślny. + Wyświetlacz + Efekty + Sterowanie + Komponenty + Kontroler + Ustawienia + Strona: %1$s + Steam + Pulpit + Menu Steam + Szybki dostęp + Otwórz menu szybkiego dostępu + Poprzednia strona + Następna strona + HUD wydajności + Tryb skalowania + Ostrość skalowania + Efekty ekranowe + Na żywo. Zużywa trochę czasu GPU, gdy włączony jest dowolny efekt. + Presety + Własny + Zaawansowane + Rozwinięte + Zwinięte + Wyostrzanie (CAS) + Poziom wyostrzania + Sztuczne HDR + Usuwanie bandingu + Siła usuwania bandingu + Jasność + Kontrast + Gamma + Nasycenie + FXAA (wygładzanie krawędzi) + Kontury kreskówkowe + CRT + NTSC + Filtrowanie tekstur + Gry DirectX 9–11, od następnego uruchomienia. + Filtrowanie anizotropowe + Ostrość tekstur + Ujemny bias mipmap: ostrzejsze tekstury, trochę migotania. Auto dopasowuje się do tego, jak bardzo sesja jest powiększona na ekranie. + Auto (%1$s) + Klawiatura + Klawiatura PC + Android + Drugi ekran + Tryb + Pokazuje + Aplikacje Android + Uruchom aplikację + Ukryj + Pokaż + Brak aplikacji do uruchomienia + Działanie sesji + Działa od teraz i w przyszłych sesjach w tym trybie. + Następna sesja + Pomoc + Logi sesji + Dołącz do zgłoszenia błędu + Udostępnij logi + Przenieś w tło + Zatrzymaj + Zatrzymać sesję? + Teraz działa + Żadna gra nie działa teraz przez Proton + Profil gry + Szczegóły profilu nie są jeszcze dostępne. + Nie udało się wczytać profili gier. Automatyczne ponawianie. + Nie udało się odświeżyć profili gier. Wyświetlane są ostatnie dostępne dane; automatyczne ponawianie. + Wyświetl i edytuj zapisane ustawienia używane przy następnym uruchomieniu. + Używany jest profil domyślny + Własny profil + Używany jest %1$s z profilu domyślnego. + Zmiany są zapisywane w profilu gry %1$s i działają od następnego uruchomienia gry. + + %1$d nadpisanie gry + %1$d nadpisania gry + %1$d nadpisań gry + %1$d nadpisania gry + + Nie zainstalowano jeszcze żadnego Proton. + gra działa + ponownie zastosowano przy uruchomieniu %1$s + Oryginał + Importowanie i usuwanie na stronie Komponenty. + Gotowe + Wymaga uwagi + W toku + Sprawdź adres IP i port w sekcji Debugowanie bezprzewodowe lub sparuj ponownie, jeśli Android usunął to urządzenie. + Zainstaluj + Zarządzaj + Przegląd + Kontroler + Sesja + Aplikacja + Konfiguracja + ● Wszystko gotowe + Gotowe: %1$d z %2$d + Sprawdzenie systemu + Urządzenie obsługiwane + Nieprzetestowany GPU + GPU nieobsługiwany + %1$s · %2$s + %1$s: %2$s + Steam wymaga sterownika Adreno; %1$s może wyświetlać czarny ekran + Środowisko Linux + %1$s · %2$d%% + Nie zainstalowano · ok. 3 GB, instalowane przy pierwszym „Graj” + Zainstalowano + %1$s · dostępna aktualizacja + %1$s · aktualne + Limit procesów podrzędnych + Wł. · Android może zamykać Steam. Wyłączenie zajmuje minutę. + Nieustawiony · Android może zamykać Steam. Wyłączenie zajmuje minutę. + Nie udało się sprawdzić · wyłączenie zajmuje minutę. + Napraw + Szczegóły + Opcje programisty + Użyj debugowania bezprzewodowego + Sprawdź ponownie + Włącz limit + Konto Steam + Zalogowano jako %1$s · tryb offline + Zalogowano jako %1$s + Naciśnij „Graj” i zaloguj się do Steam + Narzędzia + Kontroler + Ustawienia kontrolera są niedostępne. + Sesja + Logi sesji + Zapisywane po każdej sesji + Narzędzia debugowania + Najnowsze logi sesji + Zapisane logi sesji + Zachowuje najnowsze: %1$d + Wyczyść logi + Tryb offline + Najpierw zaloguj się do Steam + Aplikacja + Język + Steam też go używa, od następnego uruchomienia. + Domyślny systemu (%1$s) + Domyślny systemu + Skala aplikacji + Tekst i elementy sterujące DroidDeck + %1$d%% (domyślnie) + Motyw + Wygląd aplikacji + Użyj jako ekranu głównego + DroidDeck może być aplikacją ekranu głównego + DroidDeck nigdy nie jest proponowany jako aplikacja ekranu głównego + Orientacja + Automatyczna pozioma + Pozioma + Odwrócona pozioma + Pełny ekran + Ukrywa systemowe paski stanu i nawigacji + Pokazuje systemowe paski stanu i nawigacji + Domyślna aplikacja ekranu głównego + Wybierz aplikację ekranu głównego + Wybierz + Aplikacje Linux (beta) + Pliki + Przeglądaj pliki i zarządzaj nimi + Wersje Proton + Instaluj wersje Proton ARM64 + Wydajność + Przydział rdzeni CPU + Folder ROM-ów + Miejsce przechowywania gier do emulatorów + Komponenty + FEX, DXVK, VKD3D + Sterowanie + Mapowanie przycisków + Odkrywaj + Szukaj + Zainstalowane + %1$s · %2$d + Sklep + Flathub · ARM64 + ● Flatpak gotowy + Najpierw zainstaluj środowisko Linux w sekcji Konfiguracja. + Skonfiguruj Flatpak + Dodaje Flatpak i łączy go z Flathub (ok. 20 MB). Pierwsza aplikacja pobiera też swoje środowisko uruchomieniowe. + Konfigurowanie… + Flatpak + Aktualizacje + %1$s · %2$s + %1$s · %2$s · %3$d%% + Uruchamianie… + Kategorie + Nie udało się połączyć z Flathub. Sprawdź połączenie i spróbuj ponownie. + Spróbuj ponownie + Szukaj w Flathub + Firefox, gry, emulatory… + Szukaj + Wszystkie + Wyszukiwanie… + Szukaj po nazwie lub wybierz kategorię. Wyświetlane są tylko aplikacje ARM64. + Brak pasujących aplikacji ARM64 w Flathub. + Wyniki + Sprawdź aktualizacje + Aktualizuj wszystkie (%1$d) + ● Wszystko jest aktualne + Brak aplikacji. Znajdź je w sekcji Odkrywaj lub Szukaj. + Skonfiguruj Flatpak, aby instalować aplikacje. + Dostępna aktualizacja · %1$s + Otwórz + Szczegóły + Usuń + Naciśnij ponownie, aby usunąć + Instalowanie… + Zainstaluj + Najpierw skonfiguruj Flatpak + Do pobrania: %1$s + ● Zainstalowano + Nie udało się wczytać strony tej aplikacji w Flathub. + Wczytywanie z Flathub… + Brak wersji ARM64 w Flathub, więc nie można jej tu zainstalować. + Zrzuty ekranu + Informacje + ID aplikacji + Licencja + Środowisko uruchomieniowe + Rozmiar po instalacji + %1$s (plus współdzielone środowisko uruchomieniowe) + Strona internetowa + Zweryfikowano + Aktualizacje + DroidDeck %1$s · %2$s + Sprawdzono %1$s + Jeszcze nie sprawdzono + Pozwól DroidDeck się aktualizować + Na następnym ekranie włącz „Zezwól z tego źródła” i wróć tutaj. Aktualizacja będzie kontynuowana sama. + Otwórz ustawienia + Nie teraz + Inny podpis + Nie można zaktualizować w miejscu + Ta kopia nie jest podpisana kluczem wydań DroidDeck, więc aktualizacje nie mogą jej zastąpić. Odinstaluj ją, aby przełączyć. + Dostępna aktualizacja + Nie można zainstalować + Gotowe do przełączenia + Przed Stable + Jesteś przed wersją Stable + Ta kompilacja jest nowsza niż najnowsza Stable. Przejdziesz na Stable przy jej następnym wydaniu. + Test zakończony + Ten test się zakończył + Jego poprawka została scalona albo odrzucona. Śledź Preview, aby mieć najnowsze poprawki. + Aktualne + Masz najnowszą wersję %1$s + · %1$s + %1$s… %2$d%% + %1$s… + Aktualizuj%1$s + Instaluj%1$s + Zainstaluj Stable %1$s mimo to + Śledź Preview + Zatrzymaj działającą sesję, aby zaktualizować. + Mniej + Więcej + DroidDeck %1$s + Nowa kompilacja Preview + Kompilacja testowa PR #%1$d + %1$d MB + KANAŁ AKTUALIZACJI + Stable + Przetestowane wydania, dla większości osób + %1$s · %2$s + Preview + Najnowsze poprawki, zanim trafią do Stable + Aktualne + Kompilacje testowe + Wypróbuj poprawkę przed jej wydaniem + Obecnie żadnych + PR #%1$d + Nie zbudowano dla tej kopii DroidDeck + Zaktualizowano %1$s + test PR #%1$d + jakiś czas temu + przed chwilą + + %1$d do wypróbowania + %1$d do wypróbowania + %1$d do wypróbowania + %1$d do wypróbowania + + + Zmiany od Twojej kompilacji · %1$d kompilacja wstecz + Zmiany od Twojej kompilacji · %1$d kompilacje wstecz + Zmiany od Twojej kompilacji · %1$d kompilacji wstecz + Zmiany od Twojej kompilacji · %1$d kompilacji wstecz + + + Niedawne zmiany · %1$d kompilacja + Niedawne zmiany · %1$d kompilacje + Niedawne zmiany · %1$d kompilacji + Niedawne zmiany · %1$d kompilacji + + Jedno ustawienie Androida do zmiany + Steam + Android zatrzymuje aplikacje, które uruchamiają wiele procesów w tle, a Steam uruchamia ich kilkadziesiąt. Dopóki to się nie zmieni, gry zamykają się bez błędu. + Napraw to za mnie + Włącz „Wyłącz ograniczenia procesów podrzędnych” i wróć tutaj. Zostaw włączone Opcje programisty, bo inaczej przełącznik znowu się wyłączy. + Otwórz Opcje programisty + Ta wersja Androida nie ma dla tego przełącznika, więc DroidDeck zmienia go przez debugowanie bezprzewodowe. Zajmuje to około minuty, bez potrzeby komputera. + Debugowanie bezprzewodowe wymaga Wi-Fi. Działa każda sieć; nie musi mieć internetu. + Kod parowania pojawia się w powiadomieniu. Zezwól DroidDeck na powiadomienia albo użyj sekcji „Inne sposoby”. + Zezwól na powiadomienia + Otwórz Ustawienia → Opcje programisty → Debugowanie bezprzewodowe i włącz je + Dotknij „Sparuj urządzenie za pomocą kodu parowania” + Wpisz kod w powiadomieniu DroidDeck + Czekanie na okienko parowania… + Znaleziono okienko. Wpisz jego kod w powiadomieniu. + Stan + Sprawdzanie co 2 sekundy. Ta strona zamknie się, gdy przełącznik będzie wyłączony. + Inne sposoby + Debugowanie bezprzewodowe, adres ręczny, komputer z ADB + Adres ręczny, komputer z ADB, Opcje programisty + Użyj za to debugowania bezprzewodowego + Niektóre ROM-y dodają ten przełącznik mimo to. Poszukaj „child process” w Opcjach programisty. + Jeśli powiadomienie nie działa, wpisz adres i kod z okienka samodzielnie. + Wpisz adres ręcznie + Z komputera z ADB: + Kopiuj polecenie + Wybierz, na którym ekranie otworzyć Ustawienia Androida. + Ekran główny + Sparowano. Wyszukiwanie portu debugowania bezprzewodowego… + Sparowano. Wpisz port z Ustawień. + Znaleziono urządzenie. Stosowanie ustawienia… + Nie udało się zastosować automatycznie. Sprawdź adres poniżej i spróbuj ponownie. %1$s + Debugowanie bezprzewodowe + Limit procesów podrzędnych zmieniony + Zmień limit procesów podrzędnych + Jeśli opcja jest niedostępna, sparuj przez debugowanie bezprzewodowe. + Sparowano. Sprawdź adres IP i port w Debugowaniu bezprzewodowym. + Limit procesów podrzędnych został zmieniony. + Użyj kroku 2, jeśli Opcje programisty nie mają ustawienia procesów podrzędnych. Otwórz Opcje programisty → Debugowanie bezprzewodowe → Sparuj urządzenie za pomocą kodu parowania. + Sparowano. Użyj adresu poniżej lub zaktualizuj go z Debugowanie bezprzewodowe → Adres IP i port. + Limit procesów podrzędnych został zmieniony przez debugowanie bezprzewodowe. + Android potwierdził, że limit jest włączony. + Android potwierdził, że limit jest wyłączony. + 2 · Sparuj debugowanie bezprzewodowe + Adres IP i port okienka parowania + Kod parowania (PIN) + 6 cyfr + Sparuj + 2 · Zastosuj przez debugowanie bezprzewodowe + Adres IP i port połączenia + Sparuj ponownie + Wyłącz limit + Inne sposoby na zmianę tego + 1 · Opcje programisty (spróbuj najpierw) + Ustaw „Ogranicz procesy podrzędne” na włączone. + Ustaw „Ogranicz procesy podrzędne” na wyłączone. + 3 · Komputer z ADB (ostatnia opcja) + Tylko jeśli debugowanie bezprzewodowe jest niedostępne. + 3 · Komputer z ADB (ostatnia opcja) + Gdy w sesji pojawiają się sterowanie dotykowe + Kolor + Odcień przycisków na ekranie + Własny + Przezroczystość + %1$d%% + Rozmiar przycisków + 100% to rozmiar standardowy + Kliknięcie gałki + Dotknij dwukrotnie i przytrzymaj gałkę, aby uzyskać L3 lub R3 + Adaptacyjne gałki + Wibracje gry + Przycisk Steam + Przycisk QAM + Przycisk klawiatury + Naciśnij Select, aby otworzyć menu Steam + Zamiast View/Select. + Animacje + Gałki pojawiają się tam, gdzie dotykasz, i chowają się po puszczeniu + Układ + Zapisano własne pozycje + Dopasowany do tego ekranu + Edytuj + Resetuj + Mapowanie przycisków + Każdy przycisk wysyła własny sygnał + Zmapowano %1$d z %2$d przycisków + Skonfiguruj + Resetuj kontroler + Przywraca wszystkie ustawienia kontrolera i układ + Co wysyła do gry każdy przycisk na ekranie. Ukryty usuwa przycisk. + Przyciski na ekranie + Domyślne + Resetuj mapowanie + Aplikacje Android + %1$d aplikacji + Nie znaleziono żadnych aplikacji Android do uruchomienia. + ● Pulpit zainstalowany + Instaluje się przy pierwszym otwarciu + Dostępne do instalacji + Nie zainstalowano. + %1$s · nie zainstalowano + Otwórz %1$s + Gry + Wybierz folder ROM-ów. + Przejdź do /root/ROMs w RetroArch. + Dodaj gry %1$s do ROMs/%2$s. + Zainstaluj %1$s + Zatrzymaj sesję, aby zainstalować + Wymagane środowisko uruchomieniowe + Wczytywanie szczegółów instalacji… + Szczegóły instalacji są niedostępne. Spróbuj ponownie później. + Najpierw zainstaluj środowisko Linux w sekcji Konfiguracja. + Zatrzymaj działającą sesję, aby zainstalować aplikacje pulpitu. + Instaluje %1$s w pulpicie Linux. + %1$s · %2$d%% + Ukrycie usuwa to z Pulpitu. Jego pliki zostają w środowisku Linux. + Tej gry nie ma już w folderze ROM-ów. + Pulpit · %1$s + Uruchom w %1$s + Także w %1$s + Ta strona nie istnieje. Wybierz sekcję po lewej. + Otwórz pulpit + Zainstaluj i otwórz pulpit + Odinstaluj pulpit + Usuwa pulpit i jego dołączone aplikacje. Zachowuje gry, dodane aplikacje, ustawienia i współdzielone biblioteki. + Usuwanie pulpitu + Pulpit usunięty + Usuwanie starych plików LXQt + Zaktualizuj DroidDeck przed usunięciem tej wersji pulpitu. + Pulpit Linux + KDE Plasma, Firefox i Twoje emulatory + Gry, które instalujesz w Steam, pojawią się tutaj po sesji. + Zagraj w Steam + Ustawienia uruchamiania + Uruchom + Pliki gry + Prefiks Proton + Wyszukiwanie zapisów… + Nie znaleziono folderu zapisów po nazwie. Eksport obejmuje cały folder użytkownika. + %1$d folderów zapisów + %1$s · %2$d plików · %3$s + %1$d KB + Zarządzaj zapisami + Zapisy gry + Importuj zapisy… + Eksportuj jako zip GameHub… + Eksportuj jako zip Winlator… + Otwórz folder zapisów + Nigdy nie grano + Środowisko zajęte · %1$d%% + Środowisko zajęte + Środowisko instaluje się przy pierwszym uruchomieniu + Dostępna aktualizacja środowiska + ● Środowisko gotowe + Ostatnio grano %1$s + Pamięć wewnętrzna + Dodana gra + Własna gra + Miejsce na gry + Najlepsza para dla tego GPU, aktualizowana automatycznie + Wybierz parę samodzielnie + Sprawdzanie sterowników… + Brak sterowników na liście. Odśwież, aby ich poszukać. + Ukryj sterowniki innych GPU + Pokaż sterowniki innych GPU + Zaawansowane + Ustawiany wraz ze sterownikiem wyświetlania: %1$s obejmuje oba. + Ustawiany wraz ze sterownikiem środowiska: %1$s obejmuje oba. + Używany przez gry i pulpit. + Domyślny dla środowiska + Rysuje obraz na ekranie. Działa po ponownym uruchomieniu DroidDeck. + Auto – wybrany według GPU + Pobierz, zaimportuj lub usuń… + W UŻYCIU + Sterowniki wbudowane w DroidDeck + Sterowniki środowiska i wyświetlania ustawiane razem jako dobrana para. + Polecany dla tego GPU: %1$s. + Odśwież, aby poszukać sterowników dla tego GPU. + POLECANY + Niedostępny + Środowisko Linux + Przygotowywanie środowiska Linux + Sesja + Uruchamianie obrazu i dźwięku + Klient Steam + Sprawdzanie Steam i jego narzędzi zgodności + Otwieranie Steam + Czekanie na pierwszą klatkę Steam + Pulpit + Przygotowywanie pulpitu + Otwieranie + Oczekiwanie na pierwszą klatkę + Uruchamianie Steam + Sesja się zakończyła + Krok %1$d z %2$d + Steam jest wstrzymany + Uchwyty + Lewy pad + Prawy pad + Oba + Gamepad + Gałka + Trackpad + Przełącz między gałką a trackpadem + Więcej sterowania Deck + PANELE + Zamknij sterowanie na drugim ekranie + Otwórz menu Steam + Lewy · górny + Lewy · dolny + Prawy · górny + Prawy · dolny + Kliknięcie + Prawy trackpad + Lewy trackpad + Trackpad + Pokaż klawiaturę + Ukryj klawiaturę + Terminal Linux + Sesja Linux nie jest jeszcze gotowa na terminal. + Steam + Gry + Pulpit + Sklep + Komponenty + Aplikacje + Konfiguracja + Aktualizacje + %1$d%% + Wznów %1$s + W każdej sesji widoczny jako /root/ROMs. Pamięć wewnętrzna jest w /root/Storage. + Nie wybrano folderu + Zadziała od następnej sesji. Karty SD są obsługiwane. + Wybierz folder + Zmień folder + Zainstaluj wersję Proton dla ARM64, a potem wybierz ją dla gry w Steam → Właściwości → Zgodność. + Zatrzymaj działającą sesję, aby zainstalować lub usunąć narzędzie zgodności. + Dostępne wersje + Zainstalowano %1$s + Oczekuje z wcześniejszego żądania + Nie zainstalowano + Dostępna aktualizacja → %1$s + Usuń stare wersje %1$s + Usuwa %1$s. Gry ustawione na te wersje trzeba przełączyć na inny Proton. + Gry ustawione na tę wersję trzeba przełączyć na inny Proton. + Zainstaluj teraz + Anuluj oczekujące + Klawiatura Android + Ukryj ✕ + Uruchom %1$s + Wybierz ekran. Dodatkowy: %1$s. + Dodatkowy wyświetlacz nie jest już dostępny. + Ekran podstawowy + Ekran dodatkowy + Poprzednia karta + Następna karta + Wł. + Wszystkie %1$d + Brak + %1$d z %2$d + Wył. + Dolny ekran + Zadziała przy następnym uruchomieniu gry. Profil gry zastępuje profil wspólny. + Inna gra według ID aplikacji… + Gotowe + Domyślne + Wył. + Zmień… + Zmień zmienną + Przywróć domyślne + Gry Proton + Obraz w obrazie + Automatycznie włączaj obraz w obrazie + Sterowanie na rozłożonym ekranie + Po rozłożeniu gra jest nad trackpadami i tylnymi uchwytami Steam Deck. Po złożeniu gra zajmuje cały ekran. + Wstrzymano + Obraz w obrazie będzie dostępny, gdy sesja będzie gotowa. + Zezwól DroidDeck na obraz w obrazie w ustawieniach systemu Android. + Android nie mógł otworzyć obrazu w obrazie. + Skrót + Dodaj do ekranu głównego + Wybierz grę + Skanowanie zainstalowanych gier… + Kopiuj link uruchamiania + Skopiowano link uruchamiania + Ten launcher nie może dodawać skrótów. Użyj opcji „Kopiuj link uruchamiania”. + Nieprawidłowy link uruchamiania gry. + Tej gry nie ma już w bibliotece DroidDeck. + Zatrzymaj bieżącą sesję przed uruchomieniem gry Steam. + Nie udało się wysłać gry do Steam. Spróbuj uruchomić ją ponownie. + Ponów usuwanie + Usuwanie + Usuwanie nieukończone + Eksportuj plik gry + Synchronizuj wszystkie pliki gier z folderem + Zatrzymaj synchronizację plików gier + Wybierz folder na pliki gier dla frontendów + Pliki gier zapisano w %1$s + Pliki frontendów + + Poczekaj na zakończenie operacji na środowisku + Poczekaj na zakończenie instalacji narzędzia zgodności + Poczekaj na zakończenie instalacji aplikacji desktopowej + Poczekaj na zakończenie instalacji, zanim uruchomisz grę + Najpierw zamknij sesję Steam, aby gra nie nadpisała importowanych zapisów + Importowanie do %1$s + + Zaimportowano %1$d plik z %2$s do %3$s + Zaimportowano %1$d pliki z %2$s do %3$s + Zaimportowano %1$d plików z %2$s do %3$s + Zaimportowano %1$d pliku z %2$s do %3$s + + + Zaimportowano %1$d plik z %2$s do %3$s. Kopię starych zapisów umieszczono w Download/DroidDeck/Saves/backups + Zaimportowano %1$d pliki z %2$s do %3$s. Kopię starych zapisów umieszczono w Download/DroidDeck/Saves/backups + Zaimportowano %1$d plików z %2$s do %3$s. Kopię starych zapisów umieszczono w Download/DroidDeck/Saves/backups + Zaimportowano %1$d pliku z %2$s do %3$s. Kopię starych zapisów umieszczono w Download/DroidDeck/Saves/backups + + Wybierz plik zip z zapisami dla %1$s + Eksportowanie %1$s + + Wyeksportowano %1$d plik jako %2$s: %3$s + Wyeksportowano %1$d pliki jako %2$s: %3$s + Wyeksportowano %1$d plików jako %2$s: %3$s + Wyeksportowano %1$d pliku jako %2$s: %3$s + + Wybierz, gdzie zapisać %1$s (%2$s) + %1$s nie powiodło się: %2$s + Brak logów sesji: najpierw uruchom sesję. + Brak logów sesji do wyczyszczenia. + + Wyczyszczono %1$d folder logów sesji. + Wyczyszczono %1$d foldery logów sesji. + Wyczyszczono %1$d folderów logów sesji. + Wyczyszczono %1$d folderu logów sesji. + + Parowanie przez debugowanie bezprzewodowe nie powiodło się + Polecenie debugowania bezprzewodowego nie powiodło się + Debugowanie bezprzewodowe jest niedostępne + Skopiowano polecenie ADB + Wybierz folder ROM-ów + To nie jest GPU Adreno + Turnip obsługuje GPU Adreno. Na tym urządzeniu (%1$s) Steam może wyświetlać czarny ekran. Pobieranie: %2$.0f MB. + Zainstaluj mimo to + Usuń środowisko Linux + Spowoduje to usunięcie środowiska, klienta Steam w nim i wszystkich zainstalowanych tam gier. + Zastosowano: %1$s + Nie udało się otworzyć %1$s na ekranie podstawowym + Nie udało się otworzyć %1$s na wyświetlaczu %2$s + Nie udało się otworzyć %1$s na wyświetlaczu %2$d + Importowanie + Zamienianie + Przywracanie + Anulowanie + Usuwanie + nie można odczytać pliku + Zaimportowano %1$s + Anulowano oczekującą zamianę. + Wybierz pakiet komponentu (-linux .wcp) + Nie udało się połączyć z Nightlies + Zapisano %1$s + Pobieranie nie powiodło się: %1$s + Wybierz sterownik środowiska Linux (zip -Linux lub pakiet Android + Linux) + Wybierz sterownik wyświetlania (zip AdrenoTools lub pakiet Android + Linux) + Wybierz zip ze sterownikiem lub pakiet Android + Linux + %1$s: %2$s + Usuwanie %1$s… + Zapominanie %1$s… + Wybierz folder na gry + Wybierz plik ZIP wtyczki Decky + Nie nadaje się: %1$s + Program + Usuwanie środowiska Linux + Nie udało się otworzyć Ustawień na dolnym ekranie; otwarto je na ekranie podstawowym. + Otwórz Opcje programisty w Ustawieniach + + Nie udało się otworzyć ustawienia systemu Android „Instalowanie nieznanych aplikacji” + Nie udało się sprawdzić: %1$s + Ta wersja nie ma pliku do pobrania dla tej kopii DroidDeck + Najpierw zatrzymaj działającą sesję, a potem zaktualizuj + Pobieranie + Instalowanie + Oczekiwanie na Twoje potwierdzenie + Aktualizacja nie powiodła się + Android nie zaproponował instalacji aktualizacji + + Instalacja Decky nie powiodła się: %1$s + Rozpoczynanie importu wtyczki… + Import wtyczki nie powiódł się: %1$s + Zainstalowano wtyczkę. Uruchom ponownie Steam, aby ją wczytać. + + Jeszcze nie sprawdzono – dotknij odświeżania, aby poszukać nowych sterowników + Android + Linux: ustawia też sterownik wyświetlania + Android + Linux: ustawia też sterownik środowiska + Turnip wbudowany w środowisko + systemowy Vulkan: brak dołączonej wersji dla tego GPU + %1$s (dołączony) + Ręcznie + %1$s · %2$s + Obecnie opublikowano tylko jedną połowę + %1$.0f MB łącznie + Brak sterowników do ustawienia: %1$s + Szukanie najnowszych sterowników… + Aktualne (nie udało się sprawdzić: %1$s) + Zalecane sterowniki nie są obecnie opublikowane + Pobieranie %1$s %2$s… + Zaktualizowano do %1$s %2$s. + Zaktualizowano do %1$s %2$s. Sterownik wyświetlania zadziała po ponownym uruchomieniu DroidDeck. + Przełączono na %1$s %2$s. + Przełączono na %1$s %2$s. Sterownik wyświetlania zadziała po ponownym uruchomieniu DroidDeck. + Używany: %1$s %2$s. + Używany: %1$s %2$s. Sterownik wyświetlania zadziała po ponownym uruchomieniu DroidDeck. + To nie jest zip ze sterownikiem: ani sterownik AdrenoTools, ani Turnip -Linux, ani pakiet Android + Linux + Import nie powiódł się: %1$s + Zaimportowany sterownik + %1$s służy teraz jako sterownik środowiska i wyświetlania. + %1$s służy teraz jako sterownik środowiska i wyświetlania. Sterownik wyświetlania zadziała po ponownym uruchomieniu DroidDeck. + Usunięto %1$s (oba sterowniki) + Usunięto %1$s + Najnowsze: %1$s · sprawdzono %2$s + Najnowsze: %1$s · %2$s niedostępne · sprawdzono %3$s + + %1$d min temu + %1$d min temu + %1$d min temu + %1$d min temu + + + %1$d godz. temu + %1$d godz. temu + %1$d godz. temu + %1$d godz. temu + + + %1$d dzień temu + %1$d dni temu + %1$d dni temu + %1$d dnia temu + + Sprawdzanie Banners-Turnip, WinNative i DroidDeck… + błąd sprawdzania + Zainstalowano %1$s – wybierz go na dowolnej liście, aby ustawić oba sterowniki + Zainstalowano %1$s – wybierz go w menu + + Poczekaj na zakończenie usuwania środowiska + Gra + touchpad + bezpośredni + Zamiana nie powiodła się: %1$s + Zapis profilu nie powiódł się: %1$s + Pulpit jest wstrzymany + %1$s – wstrzymano + Program jest wstrzymany + Uruchamianie pulpitu + Uruchamianie %1$s + Uruchamianie programu + Brak logów dla tej sesji. + Nie udało się utworzyć archiwum logów sesji. + Nie udało się udostępnić logów sesji. + Uruchamianie sesji… + pobieranie środowiska Linux + pobieranie środowiska Linux · %1$d z %2$d MB + sprawdzanie środowiska Linux + rozpakowywanie środowiska Linux + pobieranie pulpitu + pobieranie pulpitu · %1$d z %2$d MB + sprawdzanie pulpitu + rozpakowywanie pulpitu + pobieranie Proton Experimental (ARM64) + pobieranie Proton Experimental (ARM64) · %1$d z %2$d MB + sprawdzanie Proton Experimental (ARM64) + rozpakowywanie Proton Experimental (ARM64) + Nie udało się połączyć z katalogiem środowisk. Sprawdź połączenie i ponownie naciśnij Graj. + Środowisko Linux nie zostało zainstalowane. Sprawdź środowisko w sekcji Konfiguracja i ponownie naciśnij Graj. + Pulpit nie został zainstalowany (%1$s). Sprawdź połączenie i ponownie naciśnij Pulpit. + %1$s – nieoczekiwane zatrzymanie. Udostępnij logi w zgłoszeniu błędu lub spróbuj ponownie. + Program nieoczekiwanie się zatrzymał. Udostępnij logi w zgłoszeniu błędu lub spróbuj ponownie. + Pulpit nieoczekiwanie się zatrzymał. Udostępnij logi w zgłoszeniu błędu lub spróbuj ponownie. + Kod wyjścia %1$d · %2$s + brak logu + + Log zawiera %1$d błąd „Function not implemented”: przyspieszenie seccomp w proot zawodzi przy procesie pomocniczym na tym urządzeniu. Wybierz %2$s → „%3$s” (lub „%4$s”) i uruchom ponownie. + Log zawiera %1$d błędy „Function not implemented”: przyspieszenie seccomp w proot zawodzi przy procesie pomocniczym na tym urządzeniu. Wybierz %2$s → „%3$s” (lub „%4$s”) i uruchom ponownie. + Log zawiera %1$d błędów „Function not implemented”: przyspieszenie seccomp w proot zawodzi przy procesie pomocniczym na tym urządzeniu. Wybierz %2$s → „%3$s” (lub „%4$s”) i uruchom ponownie. + Log zawiera %1$d błędu „Function not implemented”: przyspieszenie seccomp w proot zawodzi przy procesie pomocniczym na tym urządzeniu. Wybierz %2$s → „%3$s” (lub „%4$s”) i uruchom ponownie. + + + Wtyczki + Zainstaluj wtyczkę z pliku ZIP na następną sesję Steam. + Pobieranie… + Zainstaluj z pliku ZIP + Gry + Audio i wideo + Grafika + Internet + Biuro + Programowanie + Edukacja + Nauka + System + Narzędzia + Popularne + Na czasie + Nowe + Zaktualizowane + wczytywanie… + Konfiguracja Flatpak + %1$s: %2$s + Nie udało się sprawdzić aktualizacji w Flathub + zainstalowana + folder + Grafit + Grafitowe powierzchnie i niebieski kuli z logo dla wszystkiego, co zaznaczone, aktywne lub naciśnięte. + Papier na czerni + Biel jest kolorem głównym, a niebieski z ikony wyróżnia linie, pierścienie i kropki. + Niebieski z ikony + Czarne tło, biały tekst i niebieski dla wszystkiego, co zaznaczone, aktywne lub naciśnięte. + Elektryczny granat + Jaśniejszy niebieski na powierzchniach wpadających w granat zamiast w szarość. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Spacja + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Najpierw zainstaluj środowisko + Biblioteka Steam jest niedostępna + Poprzednia biblioteka na karcie SD jest niedostępna + Miejsce na gry jest niedostępne + Folder dodanych gier jest niedostępny + Manifesty Steam są niedostępne + Wiele systemów + Wewnętrzna + System Linux + Karta SD + Pamięć + Zaznacz + Właściwości + Dodaj do ulubionych + Usuń z ulubionych + Zmień nazwę + Kopiuj + Wytnij + Działania + Ulubione + Przypnij bieżący folder + Brak ulubionych – przypnij folder w jego menu ⋮, aby szybko tu wracać. + Usuń z ulubionych + 0 bajtów + B + Lokalizacja + Typ + Rozmiar + Zmodyfikowano + Folder + Plik + Plik %1$s + Tylko do odczytu + Chroni ten plik przed nadpisaniem lub usunięciem przez gry (i Wine). + Nie udało się zmienić atrybutu Tylko do odczytu + Ukryty + Ta pamięć nie obsługuje atrybutu ukrycia. + Oznacza plik jako ukryty w Windows (atrybut ukrycia Wine). + Nie udało się zmienić atrybutu Ukryty w tej pamięci + Gotowe + Usuwanie... + Usuwanie nie powiodło się + Przenoszenie... + Kopiowanie... + Przenoszenie - %1$s + Kopiowanie - %1$s + Przenoszenie %1$d/%2$d - %3$s + Kopiowanie %1$d/%2$d - %3$s + Gotowe: %1$d, błędy: %2$d + Gotowe: %1$d, pominięte: %2$d + + Przeniesiono %1$d element + Przeniesiono %1$d elementy + Przeniesiono %1$d elementów + Przeniesiono %1$d elementu + + + Skopiowano %1$d element + Skopiowano %1$d elementy + Skopiowano %1$d elementów + Skopiowano %1$d elementu + + „%1$s” już istnieje + Zmiana nazwy... + Zmiana nazwy nie powiodła się + Tworzenie folderu... + Nie udało się utworzyć folderu + Nowy folder + Nazwa folderu + Utwórz + Nowa nazwa + Usunąć? + Trwale usunąć „%1$s”? + + Usunąć %1$d element? + Usunąć %1$d elementy? + Usunąć %1$d elementów? + Usunąć %1$d elementu? + + Tej operacji nie można cofnąć. + + …i jeszcze %1$d + …i jeszcze %1$d + …i jeszcze %1$d + …i jeszcze %1$d + + Usuwanie %1$d/%2$d - %3$s + + Nie udało się usunąć %1$d elementu + Nie udało się usunąć %1$d elementów + Nie udało się usunąć %1$d elementów + Nie udało się usunąć %1$d elementu + + „Scal” dodaje i zastępuje pliki w istniejącym folderze. + „Zastąp” podmienia istniejący plik. + Zastosuj do wszystkich konfliktów + Scal + Zastąp + Zachowaj oba + Pomiń + Wybierz ten folder + Wstecz + %1$s: zamontowano, ale obecnie nie można odczytać + Widok listy + Widok siatki + Szukaj + Sortuj + Nazwa + Data modyfikacji + Przestronne wiersze + Kompaktowe wiersze + Ukryj ukryte pliki + Pokaż ukryte pliki + Ukryj ulubione + Pokaż ulubione + Filtruj ten folder + Wolne: %1$s + Zaznaczono: %1$d + Nic + Wszystko + Wklej + + %1$d element + %1$d elementy + %1$d elementów + %1$d elementu + + Wklej tutaj: %1$s + Wklej tutaj (przenieś): %1$s + Pliki + PAMIĘĆ + SZYBKI DOSTĘP + ULUBIONE + Pobrane + Pobrane + ROM-y + Logi sesji + Obrazy + Dodano „%1$s” do ulubionych + Usunięto „%1$s” z ulubionych + Pusty katalog + Poziom wyżej + Wybierz folder + Wybierz plik + Niebieski Steam + Błękitny + Biały + Fioletowy + Zielony + Bursztynowy + Czerwony + Różowy + Przycisk A + Przycisk B + Przycisk X + Przycisk Y + Lewy bumper + Prawy bumper + Lewy spust + Prawy spust + Przycisk Widok + Przycisk Menu + Widok + Menu + Krzyżak w górę + Krzyżak w dół + Krzyżak w lewo + Krzyżak w prawo + Ukryty + Przeciągnij, aby przenieść + Przyciąganie + Zapisz + Brak + Klawiatura + trackpad + Terminal + Uchwyty Deck + trackpady + + Usuwanie środowiska Linux + + Usuwanie środowiska Linux · %1$d wpis + Usuwanie środowiska Linux · %1$d wpisy + Usuwanie środowiska Linux · %1$d wpisów + Usuwanie środowiska Linux · %1$d wpisu + + Usunięto środowisko Linux + Nie udało się dokończyć usuwania środowiska Linux. Ponów usuwanie w sekcji Konfiguracja. + Usuwanie pozostałości środowiska + Pobieranie + Weryfikowanie + Wypakowywanie + Wyświetlane podczas instalowania lub usuwania środowiska Linux + Instalowanie środowiska Linux + Usuwanie środowiska Linux + + Środowisko Linux nie jest zainstalowane + Katalog nie zawiera sumy kontrolnej dla %1$s + Niezgodna suma kontrolna – niczego nie zmieniono + Instalowanie %1$s + Nie udało się umieścić pliku AppImage + Wypakowywanie nie powiodło się + Instalacja nie powiodła się + + Konfiguracja Flatpak nie powiodła się (kod %1$d) + Konfiguracja Flatpak nie została ukończona + Trwa inne zadanie sklepu + Najpierw skonfiguruj Flatpak + Rozpoczynanie + Flatpak zakończył działanie z kodem %1$d + Instalowanie %1$s + Aktualizowanie %1$s + Usuwanie %1$s + Instalowanie %1$s (%2$d/%3$d) + Aktualizowanie %1$s (%2$d/%3$d) + Usuwanie %1$s (%2$d/%3$d) + tłumaczeń + sterowników graficznych + kodeków multimedialnych + środowiska %1$s + środowiska %1$s %2$s + + Plik jest za mały jak na AppImage + Nie można odczytać pliku: %1$s + To nie jest AppImage (to nie jest program dla systemu Linux) + Ten AppImage nie jest zbudowany ani dla ARM64 (aarch64), ani dla komputerów x86 + To program dla systemu Linux, ale nie AppImage + Można importować tylko AppImage typu 2 (ten jest typu %1$d) + Za mało wolnego miejsca: import wymaga około %1$s + Za mało wolnego miejsca: aktualizacja wymaga około %1$s + Import nie powiódł się + Tego AppImage już nie ma + Aktualizacja nie powiodła się + Wypakowywanie %1$s + Nie udało się wypakować AppImage + Nie udało się wypakować AppImage: %1$s + + Zatrzymaj aktywną sesję przed instalacją Decky Loader + Pobieranie Decky Loader nie powiodło się + Rozmiar Decky Loader nie zgadza się z metadanymi wydania + Nie udało się pobrać sumy kontrolnej wydania + Brak sumy kontrolnej wydania + Suma kontrolna wydania jest nieprawidłowa + Niezgodna suma kontrolna Decky Loader + Plik wydania nie jest zgodnym 64-bitowym programem dla systemu Linux + Podczas pobierania uruchomiono sesję; zatrzymaj ją przed instalacją Decky Loader + Nie udało się oznaczyć PluginLoader jako wykonywalnego + Nie udało się włączyć zdalnego debugowania CEF w Steam + Nie udało się przygotować PluginLoader + Nie udało się zachować istniejącego PluginLoader + Nie udało się aktywować nowego PluginLoader + Zatrzymaj aktywną sesję przed instalacją wtyczki + Zainstaluj Decky Loader przed importem wtyczki + Nie udało się odczytać wybranego pliku ZIP + Nie udało się utworzyć katalogu wtyczek Decky + Wypakowywanie pliku ZIP wtyczki + ZIP wtyczki musi zawierać jeden główny folder wtyczki + W pliku ZIP wtyczki brakuje plugin.json, package.json lub dist/index.js + Brak nazwy wtyczki + Podczas instalacji uruchomiono sesję; zatrzymaj ją przed instalacją wtyczki + Zainstalowano wtyczkę · uruchom ponownie Steam, aby ją wczytać + Instalacja wtyczki z pliku ZIP nie powiodła się + ZIP wtyczki zawiera zbyt wiele wpisów + ZIP wtyczki zawiera nieprawidłową ścieżkę + ZIP wtyczki zawiera powielone ścieżki + ZIP wtyczki zawiera niebezpieczną ścieżkę + Nie udało się utworzyć folderu wtyczki + Po rozpakowaniu ZIP wtyczki przekracza limit 1 GiB + Nie udało się oznaczyć dołączonego pliku binarnego wtyczki jako wykonywalnego + Nieprawidłowy wpis remote_binary + Nieprawidłowa nazwa zdalnego pliku binarnego + URL zdalnego pliku binarnego musi używać HTTPS + Zdalny plik binarny %1$s ma nieprawidłowy skrót SHA-256 + Nie udało się utworzyć katalogu bin wtyczki + Pobieranie pliku binarnego wtyczki · %1$s + Nie udało się pobrać pliku binarnego wtyczki %1$s + Niezgodna suma kontrolna pliku binarnego wtyczki · %1$s + Nie udało się oznaczyć pliku binarnego wtyczki jako wykonywalnego · %1$s + Nie udało się zastąpić pliku binarnego wtyczki · %1$s + Nie udało się zainstalować pliku binarnego wtyczki · %1$s + Folder wtyczki należy do innej wtyczki + Nie udało się zachować istniejącej wtyczki + Nie udało się aktywować zaimportowanej wtyczki + Nie udało się zaktualizować ustawień wtyczek Decky + Nie udało się przywrócić istniejącej wtyczki + + Nie udało się zapisać tożsamości debugowania bezprzewodowego + Polecenie ADB nie powiodło się (%1$d) + Android nie potwierdził zmiany limitu procesów podrzędnych: %1$s + Brak zapisanego parowania debugowania bezprzewodowego + Wpisz bieżący adres IP i port z ekranu Debugowanie bezprzewodowe + + Upłynął limit czasu oczekiwania na okienko parowania. Spróbuj ponownie z poziomu DroidDeck. + Nie udało się wyszukać okienka parowania (%1$d). Sprawdź, czy Wi-Fi jest włączone. + Nie udało się wyszukać okienka parowania: %1$s + Wpisz wszystkie 6 cyfr kodu + Parowanie… + Ten kod nie zadziałał. Otwórz ponownie okienko parowania i wpisz nowy kod. + Sparowano. Łączenie… + Sparowano, ale nie znaleziono portu debugowania bezprzewodowego. Nie wyłączaj debugowania bezprzewodowego i spróbuj ponownie. + Stosowanie ustawienia… + Polecenie debugowania bezprzewodowego nie powiodło się + Konfiguracja debugowania bezprzewodowego + Prosi o kod parowania debugowania bezprzewodowego, gdy Ustawienia są otwarte + Sparuj debugowanie bezprzewodowe + Włącz Debugowanie bezprzewodowe, a potem dotknij „Sparuj urządzenie za pomocą kodu parowania”. + Włącz Debugowanie bezprzewodowe, a potem dotknij „Sparuj urządzenie za pomocą kodu parowania”. Pozostań w Ustawieniach: kod wpiszesz w tym powiadomieniu. + 6-cyfrowy kod parowania + Dotknij „Wpisz kod” i podaj 6 cyfr widocznych w Ustawieniach. + Wpisz kod parowania Wi-Fi + Spróbuj wpisać kod ponownie + Wpisz kod + Konfigurowanie Steam + Steam jest gotowy + Dotknij, aby wrócić do DroidDeck. Możesz już wyłączyć debugowanie bezprzewodowe. + Konfiguracja debugowania bezprzewodowego została przerwana + + Serwer pobierania odpowiedział HTTP %1$d + Rozmiar pobranego pliku nie zgadzał się z opublikowanym APK, więc go odrzucono – spróbuj ponownie + Pobrany plik nie zgadzał się z sumą kontrolną, więc go odrzucono – spróbuj ponownie + Aktualizacja jest dla pakietu Android %1$s, a nie %2$s + Metadane aktualizacji nie wskazują klucza podpisywania wydań DroidDeck + Android nie mógł odczytać pobranego APK + Pobrany APK jest dla %1$s, a nie %2$s + Pobrany APK ma versionCode %1$d, a nie opublikowany %2$d + Android nie zainstaluje versionCode %1$d na zainstalowanym %2$d + Pobrany APK nie zawiera informacji o podpisie + Pobrany APK nie jest podpisany kluczem wydań DroidDeck + Katalog aktualizacji odpowiedział HTTP %1$d + + Uruchamianie sesji… + Minęło %1$d:%2$02d · nadal trwa + Pobieranie aktualizacji klienta Steam · %1$d%% + Pobieranie klienta Steam · %1$s + + Instalacja narzędzia zgodności już trwa + Zatrzymaj aktywną sesję przed instalacją narzędzi zgodności + Nie znaleziono wydania ARM64 dla %1$s + Do instalacji %1$s potrzeba co najmniej 4 GB wolnego miejsca plus rozmiar pobierania + Pobieranie %1$s %2$s + Ponawianie pobierania · próba %1$d z %2$d + Pobieranie przerwane + Pobieranie nie powiodło się; częściowy plik zachowano, aby można było je wznowić + Pobierany plik przekroczył rozmiar wydania i został usunięty + Pobieranie było niepełne; częściowy plik zachowano, aby można było je wznowić + Weryfikowanie pobranego pliku + Nie udało się odczytać sumy kontrolnej wydania; spróbuj ponownie + %1$s %2$s nie publikuje sumy kontrolnej; niczego nie zainstalowano + Niezgodna suma kontrolna; pobrany plik usunięto + Podczas pobierania uruchomiono sesję; zatrzymaj ją przed instalacją narzędzi zgodności + Instalowanie %1$s + Instalacja %1$s nie powiodła się (kod %2$d) + Nie udało się zainstalować %1$s + Pobieranie pakietu droiddeck-esync + + z modułami pomocniczymi Linux + tylko biblioteki DLL + Oryginał (Proton zaktualizowany od ostatniej zamiany) + Zmieniono poza sekcją Komponenty + brak + Oryginał %1$s + Oryginał z %1$s + %1$s %2$s trafi do %3$s, gdy zostanie zamknięta działająca na nim gra. + %1$s %2$s jest teraz w %3$s. Zadziała przy następnym uruchomieniu gry. + Ten oryginalny pakiet nie jest przechowywany + %1$s w %2$s wróci do oryginału, gdy zostanie zamknięta działająca na nim gra. + %1$s w %2$s przywrócono z oryginału %3$s. + Pakiet %1$s jest używany w %2$s. Najpierw przełącz ten Proton na coś innego. + Pakiet %1$s nie jest przechowywany + Usunięto %1$s. + Oryginał zainstalowanej wersji zostaje zachowany: to droga powrotu. + Nie przechowywano + Usunięto oryginał %2$s z %1$s. + Pobieranie odbywa się tylko z wydań Nightlies + Ta lista pakietów jest starsza niż sumy kontrolne – odśwież ją i spróbuj ponownie + Niezgodna suma kontrolna + + Liniowy + Najbliższy sąsiad + Snapdragon GSR (jakość) + Tylko wyostrzanie + Auto (zgodnie ze skalowaniem) + + Bez efektów. + Wyrazistość + Wyostrzenie z odrobiną kontrastu – na co dzień. + Żywe kolory + Mocno podbite kolory. Debanding włączony, by niebo nie miało pasów. + Kinowy + Głębszy kontrast, chłodniejsze kolory, wygładzone krawędzie. + Rywalizacja + Rozjaśnione cienie i mocne wyostrzenie – zauważ przeciwnika pierwszy. + Adaptacyjne wyostrzanie + Tylko wyostrzanie, kolory bez zmian. + Filmowy + Krzywa jak z taśmy filmowej, lekko stonowana. + Salon gier + Głośno i z pazurem. + Retro CRT + Kineskop CRT i analogowy sygnał razem. + Ostre skalowanie + Do grania poniżej rozdzielczości ekranu. + Czyste piksele + Ostre piksele dla gier 2D i starych tytułów. + Krawędzie anime + Kreskówkowe kontury i wyostrzenie. + + To nie jest GPU Adreno + Obsługiwany + Eksperymentalny: jego sterowniki to wersje testowe + Poniżej testowanego sprzętu (Adreno 725 i nowsze): może nie działać + Nieobsługiwany: DroidDeck wymaga GPU Adreno (Snapdragon) + + Osiągnięto limit zapytań GitHub – spróbuj ponownie później + GitHub odpowiedział HTTP %1$d + serwer pobierania odpowiedział HTTP %1$d + Ta lista sterowników jest starsza niż sumy kontrolne – odśwież ją i spróbuj ponownie + Niezgodna suma kontrolna – pobrany plik odrzucono + To nie jest sterownik AdrenoTools: %1$s + brak meta.json + nie można odczytać meta.json (%1$s) + to sterownik środowiska Linux (zaimportuj go w sekcji „Sterownik środowiska Linux”) + to sterownik gier Wayland, który nie jest przydatny w tej aplikacji + meta.json nie podaje libraryName, więc nie można niczego przekazać do AdrenoTools + meta.json wskazuje %1$s, którego nie ma w pliku zip + %1$s nie jest 64-bitową biblioteką współdzieloną ELF AArch64 + %1$s linkuje glibc – to Turnip „-Linux”, który należy do sekcji „Sterownik środowiska Linux” + Brak libvulkan_freedreno*.so w tym pliku zip. Zip z Turnip dla Android (AdrenoTools) lub -Wayland nie jest sterownikiem środowiska Linux. + %1$s nie jest 64-bitową biblioteką współdzieloną ELF AArch64. + %1$s nie jest sterownikiem glibc – linkuje libc systemu Android. Środowisko Linux wymaga pliku zip „-Linux”; zwykłego Turnip ani wersji „-Wayland” klient Steam w ogóle nie wczyta. + To nie jest pakiet sterowników: brak manifest.json wskazującego sterownik Android i Linux + To nie jest pakiet sterowników: %1$s wskazuje poza niego + To nie jest pakiet sterowników: po rozpakowaniu przekracza %1$d MB + + Karta SD + Folder + + Udostępnij logi sesji + + wymaga systemu Android 10 lub nowszego (warstwy wyświetlania z przestrzenią barw) + ten wyświetlacz nie zgłasza obsługi HDR + ten wyświetlacz obsługuje %1$s, ale nie HDR10 + + ten GPU + Limit procesów podrzędnych jest włączony + Limit procesów podrzędnych nie jest ustawiony + Nie udało się sprawdzić limitu procesów podrzędnych + Limit procesów podrzędnych jest wyłączony + Limit procesów podrzędnych: nie dotyczy + %1$s Jeśli to niemożliwe, podłącz to urządzenie do komputera z ADB i uruchom poniższe polecenie. Wróć tutaj i sprawdź ponownie. + Sesje Steam mogą się uruchamiać. + Ta wersja systemu Android nie używa tego limitu. + Włącz „Wyłącz ograniczenia procesów podrzędnych” w Opcjach programisty i nie wyłączaj potem Opcji programisty. + Ta wersja systemu Android nie ma dla niego przełącznika. Użyj debugowania bezprzewodowego, aby zmienić go z tego urządzenia. + Ta wersja nie ma pliku do pobrania dla tej kopii DroidDeck. + Android nie zainstaluje jej na tej wersji, bo jej versionCode %1$d jest niższy niż zainstalowany %2$d. + Android nie zainstaluje starszej wersji na nowszej. Zostań przy tej wersji, aż kanał ją dogoni. + Ta kopia DroidDeck ma inny podpis (lokalna kompilacja?), więc Android nie zaktualizuje jej w miejscu. + Za mało wolnego miejsca na aktualizację. + Aktualizacja została anulowana. + Aktualizacja nie została zainstalowana: %1$s + Aktualizacja nie została zainstalowana. + archiwum zip %1$s + archiwum zip + Gra %1$s nie była jeszcze uruchamiana + Nie znaleziono plików zapisu dla %1$s + Plik %1$s nie zawiera folderu drive_c, więc nie jest archiwum zapisów GameHub ani Winlator + nie można tworzyć folderów w %1$s + nie można zapisywać w %1$s + nie można zapisywać w %1$s (%2$s) + Folder jest niedostępny + Inny plik już używa nazwy %1$s + Folder grafik jest niedostępny + Folder zawiera niepowiązany spis synchronizacji + Nie udało się usunąć %1$s + Plik wychodzi poza folder eksportu + FEX (wersja bez nazwy) + brak profile.json w %1$s + %1$s nie jest pakietem komponentu Linux (typ %2$s) + %1$s · wolne: %2$s + Automatycznie + Katalog aktualizacji nie przeszedł weryfikacji (%1$s) + Ustawienie procesów podrzędnych DroidDeck + Zalogowano · tryb offline + Zalogowano + DOŁĄCZONY + POBRANY + ZAIMPORTOWANY + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Ekran sesji + Rozdzielczość + Dopasowanie gier 16:9 + Jak gry 16:9 mieszczą się na tym ekranie. Od następnej sesji. + Wybierz „Dopasuj do ekranu” lub własny rozmiar węższy niż 16:9. + Zachowaj proporcje + Rozciągnij do ekranu + Skalowanie obrazu + Filtr skalowania + Wspólny dla Steam i Pulpitu. + Działa od razu. Wspólny dla Steam i Pulpitu. + FSR, GSR i NIS działają przy powiększaniu obrazu sesji. „Tylko wyostrzanie” działa też bez powiększania. + Ostrość filtra + Liczba klatek + %1$s Ustawia skalowanie na %2$s. + Dopasuj do ekranu (%1$d×%2$d) + Dopasuj do ekranu (zmienia rozmiar po złożeniu lub rozłożeniu) + Steam + Pokaż szczegóły urządzenia + Skryj szczegóły urządzenia + Odczytywanie urządzenia… + Kopiuj + Skopiuj te szczegóły do zgłoszenia błędu. + Skopiowano. + Wymuś SSBS + Utrzymuje ustawienie szybkości CPU, które Wine wciąż wyłącza. Może przyspieszyć gry Windows na Snapdragon 8 Elite; na starszych chipsetach niewielka różnica. Działa od następnego uruchomienia gry. + + Sklepy + Steam + Własna + Sklepy + Otwórz sklep w + Poziom szybkości pobierania + Mechanizm sklepów + ● Mechanizm gotowy + Nie zbudowano + Zbalansowany + Szybki + Maksymalny + Sklepy + Ustawienia sklepów + Ustawienia + (niezalogowano) + Sklep + Zainstalowane + Biblioteka + Wszystkie + Pobierania + %1$s (%2$d) + Zaloguj się + %1$s · wyloguj się + Wylogować się z %1$s? + Zainstalowane gry zostają. Biblioteka zostanie pobrana ponownie po zalogowaniu. + Zostań zalogowany + Wyloguj się + Szukaj w katalogu %1$s… + Szukaj zainstalowanych gier… + Szukaj w Twojej bibliotece… + Szukaj + Nic nie zainstalowano + Wczytywanie + Brak gier + Wczytywanie sklepu… + Brak katalogu + Nowości + Promocje + Darmowe gry + Popularne + Twoja biblioteka + Graj + Zainstaluj + Wznów instalację + Przerwano + Instalacja · %1$s + Odbierz bezpłatnie + Zobacz w %1$s + Kup · %1$s + Zainstalowano + W bibliotece + Bezpłatna + Wznów + Odśwież + Pokaż treści dla dorosłych + Epic + EOS + Offline + Wyłączone + Logowanie Epic (EOS) + Uruchom offline + Rozwiąż logowanie Epic + Zapisy w chmurze + Wyślij do chmury + Pobierz z chmury + Brak zapisów w chmurze + Konflikt w chmurze + Konflikt w chmurze · %1$d plików + … + Zachowaj z chmury + Zachowaj lokalne + Sprawdzanie chmury… + Ostatnia synchronizacja %1$s + Jeszcze nie zsynchronizowano + %1$d plików + zainstalowana + w Twojej bibliotece + sklep + Osiągnięcia + %1$d z %2$d odblokowanych + %1$d / %2$d + Odblokowane · %1$d + Zablokowane · %1$d + Ukryte · %1$d + Ukryte osiągnięcie + Szczegóły są widoczne po odblokowaniu. + Wszystkie osiągnięcia odblokowane + Wczytywanie… + Zaloguj się do %1$s, aby zobaczyć swoje osiągnięcia. + Nie udało się połączyć z %1$s. Sprawdź ponownie, gdy będziesz online. + %1$s nie wyświetla osiągnięć dla tej gry. + Pokaż wszystkie (%1$d) + Osiągnięcia · %1$s + ✓ W Steam + Odinstaluj + Odinstalowywanie… + Odinstalować %1$s? + Zwolni %1$s i usunie ją ze Steam. + Usuwa jej pliki i skrót w Steam. + Steam wykryje grę przy następnym uruchomieniu; uruchom ją stamtąd albo uruchom teraz Steam. + Proton + Wybór Steam dla każdej gry + Dźwięk + DirectAudio + Komponenty + FEX, DXVK, VKD3D + Mechanizm + %1$s · natywny + %1$s · wbudowany + Jak to działa + Pobrano z + %1$s, z Twoim kontem + Zainstalowano w + Miejsce na gry (Games/%1$s/%2$s) + Uruchamiane przez + Steam i Proton, tą samą ścieżką co gry Steam + Steam i Proton · logowanie Epic przy każdym uruchomieniu, nakładka EOS wyłączona + Steam i Proton · ustawione zmienne Amazon Games Services + Steam + dodawana do Steam przy instalacji (interfejs Deck + Steam Input) + Brak pobierań + Wstrzymano + W kolejce + W kolejce · #%1$d + Anulowano + Niepowodzenie + Niepowodzenie · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Wstrzymaj + Wznów + Anuluj + Spróbuj ponownie + Wyczyść + Wyczyść zakończone + Manifest + Pobieranie + Weryfikacja + Instalacja + Gotowe + Sprawdzanie + Pobieranie + Usunąć pobieranie? + Niepowodzenie · %1$s zachowano + Weryfikowanie + Instalowanie + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d plików + Menedżer pobierania + Pobierania naraz + Instaluj w + Zainstaluj %1$s: + Wewnętrzna + %1$s wolnego miejsca + Pobierania ze sklepów + Pokazuje postęp pobierania gier i utrzymuje pobierania w tle + Pobieranie + %1$d pobierań + Przygotowywanie… + Zakończone pobierania ze sklepów + Zainstalowano %1$s + Pobieranie %1$s nie powiodło się + Usuń + Zachowaj + Zaloguj się do %1$s + Zaloguj się przez swojego dostawcę + Odśwież + Zamknij + Spróbuj ponownie + Kończenie logowania… + Nie udało się wczytać strony logowania: %1$s + Ten dostawca odmawia logowania z osadzonej strony. Zaloguj się za pomocą hasła do sklepu. + Strona logowania odpowiedziała błędem (HTTP %1$d). + Połączenie ze stroną logowania nie jest bezpieczne. + Nie udało się zweryfikować logowania. Spróbuj ponownie. + Logowanie nie powiodło się. Spróbuj ponownie. + Zamknij + Wstecz + Edytuj + Dodaj wszystkie gry z tego folderu + Brak nowych gier w tym folderze + Nazwa + Element docelowy + Uruchom w + Opcje uruchamiania + Grafika + Inny plik exe… + Usuń + Okładka + Tło + Logo + Ikona + Folder gry + Steam + SteamGridDB + Twój plik + Z pliku exe + Automatycznie + Wybierz obraz… + Wybierz obraz: %1$s + Przywróć automatyczne + + Dodano %1$d grę + Dodano %1$d gry + Dodano %1$d gier + Dodano %1$d gry + + + %1$d już w Grach + %1$d już w Grach + %1$d już w Grach + %1$d już w Grach + + B · Wstecz do %1$s + gry + Ustawień uruchamiania + Sterowniki graficzne + Ten Proton + Każdy Proton + Dla każdego Protona + Dostępne · %1$d + Sprawdzono %1$s + Sprawdź nowe komponenty i sterowniki + Pobierz + %1$s · DXVK, VKD3D-Proton i FEX poniżej należą do tego Protona + %1$s · przetestowany + %1$s · nieprzetestowany + %1$s · nieobsługiwany + Utrzymuje polecaną parę aktualną + Auto · %1$s + Sterowniki środowiska i wyświetlania osobno + Użyć %1$s? + Usunąć %1$s? + Diagnostyka + Gotowe do gry + Jeszcze nie gotowe do gry + środowisko %1$s + limit procesów wyłączony + zalogowano jako %1$s + Gdy coś wymaga Twojej uwagi + Wygląd + To urządzenie + Grafika biblioteki + APLIKACJE LINUX + Flathub + Aplikacje pulpitowe dla pulpitu Linux, otwierane z Pulpitu. + Skonfiguruj Flathub + Usunąć %1$s? + Jej ustawienia i pliki zostają zachowane. + Zachowaj + Rozmiar na dysku + Odinstalować pulpit? + Wszystkie + Osiągnięcia GOG + Synchronizuje osiągnięcia dla obsługiwanych gier GOG. Zacznie działać od następnego uruchomienia. + Osiągnięcia wyłączone + Osiągnięcia włączone + Zaloguj się ponownie do GOG + Osiągnięcia wysłane + Wysyłanie osiągnięcia w toku + Włącz osiągnięcia GOG w ustawieniach Store + Osiągnięcia niedostępne; sprawdź dzienniki sesji + Osiągnięcie odblokowane + Wklej + Krzyżak + Lewy drążek + Prawy drążek + Menu + Klawiatura + Krzyżak góra + Krzyżak dół + Krzyżak lewo + Krzyżak prawo + Sterowanie dotykowe + Kontroler dotykowy + Mysz + Wł. + Wył. + Wolna + Normalna + Szybka + Edytuj układ + Menu DroidDeck + Przeciągnij elementy sterujące na ekran + Rozmieść + Odrzuć + Zapisywanie… + Ikona + Sz− + Sz+ + W− + W+ + Skryj + Kolor i przezroczystość… + Skopiuj ten układ + Układ skopiowany; możesz go wkleić w układzie dotykowym każdej gry + Wklej skopiowany układ + Przywróć domyślny układ Steam + Ten element sterujący nie ma przypisania w Steam, aby nadać mu ikonę + Ikona dla %1$s + Kolor układu dotykowego + Gotowe + Nie udało się zapisać układu dotykowego + Układ dotykowy zapisany w Steam + Aplikacja %1$d + Etykieta (opcjonalnie) + Nie znaleziono ikon Steam + + %1$d ikona ze Steam + %1$d ikony ze Steam + %1$d ikon ze Steam + %1$d ikony ze Steam + + Kolor ikony + Kolor przycisku + Brak ikony + + Sprawdzanie logowania… + Pobieranie biblioteki… + Pobieranie listy gier… + Wczytywanie informacji o grach… (%1$d/%2$d) + + Synchronizowanie %1$d gry… + Synchronizowanie %1$d gier… + Synchronizowanie %1$d gier… + Synchronizowanie %1$d gry… + + Synchronizowanie… %1$d/%2$d + Pobieranie okładek… + Synchronizacja nie powiodła się + Nie znaleziono gier w Twojej bibliotece %1$s + Amazon Games nie odpowiedział w sprawie Twojej biblioteki: odśwież lub zaloguj się ponownie + Nie udało się połączyć z GOG + Nie udało się wczytać Twojej biblioteki GOG + Rejestrowanie w Steam… + Cardboard VR + Tryb Cardboard + Obraz dla lewego i prawego oka do gogli VR na telefon. + Obraz + Wirtualny ekran + 3D obok siebie + Gogle + Cardboard + Cardboard (2014) + Bez soczewek + Rozstaw oczu + Szerokość ekranu + Odległość ekranu + Śledzenie głowy + To urządzenie nie ma czujnika obrotu. + Wyśrodkuj widok + Wyśrodkuj + SteamVR (eksperymentalne) + Zainstaluj + Pobiera SteamVR dla ARM64 od Valve (571 MB). + Pobieranie i rozpakowywanie SteamVR… + Uruchom + Uruchamia SteamVR, a twoja głowa działa jak gogle; obraz przełącza się na 3D obok siebie. + Zatrzymaj + SteamVR działa. + Niedostępne w tej sesji. + SteamVR nie odpowiada; sprawdź dziennik sesji. + Pobierz komponenty + Pobrane pliki są wspólne ze stroną Komponenty. Wybierz powyżej pobraną wersję, aby użyć jej przy następnym uruchomieniu gry. + Wybierz wersję do pobrania + Producent + Wydawca + Premiera + Fabuła + + Dodatki + 100% + %1$s godz. + %1$d min + diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 000000000..2bb0f9764 --- /dev/null +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,2260 @@ + + + DroidDeck + + A primeira execução transfere o Steam. Isto pode demorar alguns minutos. + Por predefinição, sair da app coloca o Steam em pausa; ao voltar, continua onde ficou. + O seu comando aparece como um comando Xbox 360. Se não tiver comando, utilize os controlos no ecrã. + + + A primeira execução configura o ambiente de trabalho. Isto pode demorar alguns minutos. + Por predefinição, sair da app coloca a sessão em pausa; ao voltar, continua onde ficou. + O toque funciona como um painel tátil: arraste para mover o ponteiro, toque para clicar. + O seu comando aparece como um comando Xbox 360. Se não tiver comando, utilize os controlos no ecrã. + + Sessão + Apresentada enquanto houver uma sessão do Steam ou do ambiente de trabalho aberta + O Steam está em execução + Transferência do Steam ativa + Sessão em pausa - toque em Retomar quando estiver pronto + Retomar + Suspender agora + Terminar sessão + Geração de fotogramas + Selecione o modo de geração de fotogramas + Desligado + Win-FG %1$d× + LSFG %1$d× + LSFG Adaptativo %1$d + Requer o Lossless Scaling no Steam + Instale o Lossless Scaling no Steam ou importe o Lossless.dll + Importar Lossless.dll… + A utilizar a cópia do Steam + A utilizar uma cópia importada + Escolher Lossless.dll + Lossless.dll importado + Este Lossless.dll já está a ser utilizado + Esta conta Steam não possui o Lossless Scaling + Este ficheiro não é o Lossless.dll do Lossless Scaling + Não foi possível preparar o LSFG a partir deste Lossless.dll + O LSFG requer o Lossless Scaling do Steam ou um Lossless.dll importado + O LSFG não conseguiu preparar os shaders a partir do Lossless.dll + %1$s: não suportado + %1$s: falha do motor + %1$s: requer Lossless Scaling + %1$s %2$s · a iniciar + Adiciona uma biblioteca Steam e procura jogos Windows nela. Os jogos que possui utilizam as respetivas entradas do Steam; os restantes tornam-se atalhos de jogos não Steam. + Ambiente do jogo + Variáveis de ambiente para jogos Proton. + Editar + Todos os jogos Proton + Perfil + ID da app Steam + Abrir + Jogo %1$s + Adicionar variável + Nome da variável + Valor + Guardar + Remover + Voltar a herdar + Limpar substituições de ambiente + Não definir neste perfil + Nenhuma variável definida. + Herdada + Variável personalizada + Não foi possível carregar nem guardar. Verifique o armazenamento livre e tente novamente. + Utilize um nome válido e um valor com até 8192 carateres. + O ambiente de execução padrão do Proton não lê esta variável. + A carregar… + Substituição do nível de funcionalidades D3D12. 12_2 indica as capacidades do DirectX 12 Ultimate; funcionalidades não suportadas podem falhar. Deixe por definir para deteção automática. + Substituição do modelo de shader D3D12. 6_9 requer um VKD3D suficientemente recente e funcionalidades de GPU compatíveis. Deixe por definir para deteção automática. + Mantenha a cache de shaders do Mesa ativa para reduzir compilações repetidas. false ativa a cache. + Tamanho máximo da cache de shaders do Mesa, por exemplo 1G. Caches maiores ocupam mais armazenamento. + Opções do VKD3D para D3D12. nodxr desativa o ray tracing; utilize apenas num jogo que o exija. + Defina como 1 para obter um registo de diagnóstico do Proton. O registo pode reduzir o desempenho e ocupar armazenamento. + Defina como 1 para utilizar o WineD3D baseado em OpenGL em vez do DXVK, para testar a compatibilidade com D3D9–11. + Defina como 1 para manter este jogo fora do ntsync (droiddeck-ntsync ou um controlador do kernel); utiliza o droiddeck-esync onde existir um pacote, caso contrário o droiddeck-fsync. + Defina como 1 para desativar o droiddeck-esync neste jogo; utiliza o droiddeck-fsync em vez dele, ou o droiddeck-ntsync se o fsync também estiver desativado. + Defina como 1 para manter este jogo fora do droiddeck-fsync; utiliza o droiddeck-esync onde existir um pacote, caso contrário o droiddeck-ntsync. + Defina como 1 para executar este jogo com o droiddeck-fsync, ou 0 para dar prioridade ao droiddeck-esync onde houver um pacote compatível com o respetivo Proton. + Defina como 0 para desativar o assistente de navegação por comando do Proton neste jogo. + Sobreposição do DXVK para D3D9–11. fps mostra a taxa de fotogramas; sem valor, fica oculta. + Definições do DXVK separadas por ponto e vírgula. As versões recentes utilizam dxvk.maxFrameRate para limitar os fotogramas; para jogar normalmente, prefira o limitador de fotogramas da sessão. + Limite da taxa de fotogramas em D3D12. 0 desativa o limite. + Threading OpenGL do Mesa. true pode ajudar jogos OpenGL limitados pela CPU; teste jogo a jogo. + Sincronização do Wine + Como os jogos mantêm os seus threads sincronizados. Aplica-se na próxima sessão. + esync + ntsync + fsync + wineserver + Predefinição do FEX + Aplica-se quando iniciar o próximo jogo. + Jogos + Padrão do FEX + Utiliza os valores padrão do FEX. + Estabilidade + Definições mais seguras e mais lentas. + Compatibilidade + TSO completo com multibloco. + Intermédio + TSO equilibrado com precisão x87 reduzida. + Desempenho + Todo o TSO desativado; multibloco. + Desempenho + TSO + Definições de Desempenho com o TSO principal ativado. + Extremo + Desativa as verificações de código automodificável. Jogos com JIT e DRM podem deixar de funcionar. + Extremo + TSO + Definições de Extremo com o TSO principal ativado. + Extremo-gn + Desempenho com sugestões de TSC e metadados. Experimente antes do Extremo. + Denuvo + Desempenho com verificações de código completas e o bit de hipervisor oculto. + Valor personalizado + Selecionar valor + Emuladores e apps + Adicionar + A sua própria app ou jogo + Script + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + A iniciar… + %1$s adicionado + %1$s removido + Ambiente de trabalho + Essa app já não existe. Escolha-a novamente no Ambiente de trabalho. + Abrir %1$s + Remover + Prima novamente para remover + Termine a sessão para remover + Executa %1$s a partir da respetiva pasta, ligada à sessão no local onde se encontra. Remover a app não altera essa pasta. + Executa %1$s a partir de uma cópia da respetiva pasta dentro do DroidDeck. Remover a app elimina a cópia, não o original. + Instale primeiro o ambiente de execução Linux + O ficheiro já não existe + Espaço livre insuficiente: copiar a pasta requer %1$s + A copiar %1$s + Falha ao adicionar a app + A procurar uma versão de %1$s + Não foi possível ler as versões de %1$s + Nenhuma versão de %1$s tem uma AppImage ARM64 ou x86_64 + A transferir %1$s + A transferência falhou + Falha ao atualizar a app + Não foi possível ler esse ícone + %1$s guardado + Editar + Procurar atualizações + A procurar atualizações… + Atualizar para %1$s + Atualizado · %1$s + %1$s atualizado + %1$s · %2$s + Adicionar uma app + Script + AppImage + GitHub + Flatpak + Um ficheiro .sh. É executado a partir da própria pasta, em ecrã inteiro no gamescope. + Uma AppImage ARM64 (aarch64), ou uma x86_64 executada através do FEX. É extraída para o ambiente de execução Linux uma única vez. + Instala a AppImage ARM64 da versão mais recente do repositório, ou a x86_64 através do FEX. + Utilizar FEX + Automático + Ligado + Desligado + O FEX executa programas Linux x86 neste dispositivo ARM64. Em Automático, é utilizado quando o programa foi compilado para x86. + %1$s através do FEX + Este programa foi compilado para PCs %1$s e é executado através do FEX, que ainda não está configurado. Abra o Steam uma vez com a sessão iniciada: o DroidDeck transfere o FEX e o Steam Linux Runtime necessários e, depois, este programa abre. + Este programa + %1$s foi compilado para PCs x86 e é executado através do FEX, que ainda não está instalado. Abra o Steam uma vez com a sessão iniciada e deixe-o transferir o FEX e o Steam Linux Runtime; depois, volte a iniciá-lo. + Um ID de app do Flathub ou a respetiva página no Flathub. O Flatpak é configurado primeiro, se ainda não estiver. + Ficheiro + Nenhum ficheiro escolhido + Escolher ficheiro + Alterar + Escolher um script + Escolher uma AppImage + Escolher um ícone + Repositório + proprietário/repo ou uma ligação do github.com + Não é um repositório GitHub + ID da app + org.ppsspp.PPSSPP + Não é um ID de app do Flathub + Ou um ficheiro .flatpak, para uma app que não esteja no Flathub. + Escolher um ficheiro .flatpak + Origem + No seu Ambiente de trabalho + Nova app + Nome + Da app + Ícone + Ícone padrão + Escolher ícone + Limpar + Do repositório + A procurar ícones… + Padrão + Editar %1$s + Guardar + A verificar a pasta… + A pasta está ligada à sessão: %1$s + A pasta contém programas Linux, que o armazenamento partilhado não consegue executar, por isso é copiada para o DroidDeck (%1$s). + Adicionar + Cancelar + Instale primeiro o ambiente de execução Linux na Configuração. + Aguarde que %1$s termine. + Compatível + As versões marcadas com “%1$s” no menu Compatibilidade do Steam incluem as correções de prefixo do DroidDeck e iniciam jogos aqui. O padrão é o Proton Experimental ARM64 (%1$s), ou o Proton 11.0 ARM64 (%1$s) quando essa versão mais recente estiver instalada. As entradas do Proton da própria Valve sem a marca não conseguem iniciar jogos neste dispositivo. + Instalado %1$s · %2$s + Cancelar + Automático + Nunca + Sempre + Desinstalar + Verificar + Atualizar + A verificar… + A processar… + Esquecer + Adicionar… + Utilizar + Aplica-se na próxima sessão. + Sessão do Steam + Sessão do ambiente de trabalho + Ecrã + Resolução + Até 720p + Até 900p + Até 1080p + Nativa + %1$s (padrão) + Personalizada · %1$d×%2$d + Personalizada… + Com 720p, os menus podem responder mais depressa. + Proporção do ecrã + Definida pela resolução personalizada. + Automático utiliza pelo menos 16:9. + Limite de fotogramas + Limita toda a sessão. O ecrã muda para uma taxa de atualização que seja múltipla do limite. + HDR + Saída HDR10 + Indisponível: %1$s. + Reinicie a app para aplicar. + Controladores + Controladores da GPU + Partilhados pelo Steam e pelo Ambiente de trabalho. Defina-os na página Componentes. + Toque e controlos + Toque + Painel tátil + Direto + Automático: painel tátil no Ambiente de trabalho, direto no Steam. + Controlos no ecrã + Steam + QAM + Em Automático, os controlos aparecem quando não há nenhum comando ligado. + Controlos táteis do Steam (beta) + Os controlos táteis do Steam começam na próxima sessão do Steam + Esquema tátil do Steam + Mova, redimensione ou oculte os controlos táteis deste jogo; é guardado no Steam + Comando + Como o Steam vê o seu comando. Aplica-se na próxima sessão. + Comando Steam Deck + Steam Controller + DualShock 4 + DualSense + Comando Xbox 360 + Deck: botão de Acesso rápido e giroscópio. Steam Controller: o mesmo, com sensor de pega nos botões traseiros. DualShock 4 e DualSense: giroscópio, e os trackpads como o painel tátil do comando. Xbox 360: abra o Acesso rápido com Guide+A. + Voltar + Menu → QAM + QAM → Menu + Sessão + Comportamento em segundo plano + Quando a app fica oculta ou o ecrã se desliga. + Manual + Nativo + Automático: pausa e retoma ao voltar a ser apresentada. Nativo: prepara primeiro o Steam para a suspensão. Manual: aguarda por Retomar. Nunca: continua em execução. + Manter transferências ativas + Adia a suspensão em segundo plano enquanto o Steam está a transferir. + Automático: pausa em segundo plano e retoma ao voltar a ser apresentada. Manual: aguarda por Retomar. Nunca: continua em execução. + Arranque + Iniciar o Steam com o DroidDeck + Abre a sessão do Steam quando o DroidDeck é iniciado. + Decky + A procurar versões… + Pronto para instalar %1$s. + Nenhuma versão compatível encontrada. Tente novamente mais tarde. + Instalado · %1$s + Atualização disponível · %1$s + Atualizado · %1$s + Carregador + Instalar a versão mais recente + Inicia com o Steam. Enquanto estiver ligado, outras apps neste dispositivo poderão controlar o Steam. + Desligado. A porta de depuração remota do Steam mantém-se fechada. + Desinstalar o Decky Loader? + Remove o carregador. Os seus plugins e definições são mantidos. + Cliente + Sobreposição de desempenho + Desative se os jogos falharem ou ficarem a preto no Modo Deck. Aplica-se na próxima sessão. + Reparar cliente Steam + Volta a transferir o cliente Steam na próxima vez que o Steam iniciar. Utilize se o Steam ficar preso no ecrã de carregamento. Os jogos, a sessão iniciada e as definições são mantidos. + O cliente Steam será transferido novamente na próxima vez que o Steam iniciar. + Reparar + Imagens + Chave da API do SteamGridDB + A sua chave + Editar… + Preenche as imagens em falta a partir do Steam e, depois, do SteamGridDB. Os seus próprios cover.jpg, hero.jpg, logo.png ou icon.png na pasta do jogo têm prioridade. + Remover %1$s + Remover? + Adicionar jogo + Adicionar um jogo: + Escolha o exe: + Esta localização não está disponível na sessão + Verificar este exe + Componentes do Windows + LB / RB alternam entre as secções. + Nenhum + DLLs do Windows que o Proton não inclui, da lista de componentes. Ative as de que este jogo precisa: vão para a pasta Windows própria do jogo, nunca para os ficheiros dele, e mantêm-se seja qual for o Proton em que é executado. + Não foi possível aceder à lista de componentes. Os componentes já transferidos podem continuar a ser ativados ou desativados. + Recomendado para este jogo + A verificar de que precisa este jogo… + Disponível quando houver ligação + + A ativar %1$d componente… + A ativar %1$d componentes… + A ativar %1$d componentes… + + Ativos para este jogo + O Steam instala %1$s com %2$s (app %3$s) + Da lista do GOG para este jogo + Dos pré-requisitos da Epic para este jogo + Dos passos de instalação da Amazon para este jogo + O Steam instala %1$s com a app %2$s, identificada pelo nome + Também listados + Listado por %1$s; não é um componente aqui + Steam + GOG + Epic Games + Amazon Games + O jogo inclui %1$s + O jogo tem %1$s (muitas vezes funciona assim) + Já está neste jogo + Nada na pasta do jogo aponta para um componente. + Instalados para este jogo (%1$d) + Todos os componentes (%1$d) + Requerem um instalador do Windows (%1$d) + São programas de instalação próprios, sem nenhum pacote no interior para desmontar e ainda sem gravação do que instalam. A maioria são ambientes de execução antigos que o Mono incluído no Proton já substitui; as gravações são adicionadas à medida que são feitas. + Mostrar + Ocultar + Executa o seu próprio programa de instalação (ainda sem gravação) + Ainda não suportado aqui + A carregar os componentes… + Não foi possível instalar: %1$s + Transferido + A transferir %1$s + A instalar: %1$s + A desativar… + Transferência de %1$s + Tem efeito na próxima vez que o jogo for iniciado. + Esticar jogos 16:9 + Em ecrãs mais quadrados do que 16:9, estica os jogos para preencher o ecrã em vez de mostrar barras. Aplica-se na próxima sessão. + Áudio + DirectAudio para jogos + Ignora o PulseAudio para reduzir a latência. + Microfone + Para chat de voz. + Rede + Deteção de Wi-Fi + Mostrar nomes de Wi-Fi e redes próximas no Steam. + Os serviços de localização estão desativados. + Autorização de localização não concedida. + Abrir definições + Continuar + Ativar a deteção de Wi-Fi? + O Android requer a autorização de Localização para ler nomes de Wi-Fi e detetar redes próximas. O DroidDeck utiliza este acesso para mostrar essas redes no Steam. Não monitorizamos a sua localização. O acesso à Internet funciona sem esta autorização. + Armazenamento de jogos + Automático - o cartão SD quando presente, mais as suas pastas Games + Automático - as suas pastas Games, mais o cartão SD quando presente (nenhum agora) + Apenas interno + Pasta: %1$s + Segunda biblioteca + Os jogos que carregam recursos em streaming a partir do SD ou do armazenamento partilhado podem apresentar soluços. + Diagnóstico de armazenamento + Regista os resultados de alocação e amostras de tempos de armazenamento em Partilhar registos. Pode tornar as instalações mais lentas enquanto estiver ativado. Aplica-se na próxima sessão. + Escolher uma pasta… + Resolução personalizada + Substitui o limite e a proporção. Um tamanho que não corresponda ao ecrã fica com barras. + Largura + Altura + Entre 320×240 e 3840×2160. + + %1$d jogo + %1$d jogos + %1$d jogos + + OK + Eliminar + Restaurar + Transferir + Eliminar %1$s + %1$.1f MB + Controlador de execução + Utilizado pelo Steam, pelos jogos e pelo ambiente de trabalho. Aplica-se na próxima sessão. + Importar zip do Turnip… + Controlador de ecrã + Desenha os fotogramas no ecrã em ambos os modos. Reinicie a app para aplicar. + Importar um zip do AdrenoTools… + nunca + Última verificação das Nightlies: %1$s + Componentes + Acerca dos Componentes + Procurar novos controladores + Procurar novos pacotes nas Nightlies + Proton + O Proton escolhido aqui executa todos os jogos que não escolheram o seu próprio no Steam, e uma alteração feita no Steam também aparece aqui. O Steam substitui o Proton da Valve quando o atualiza; os originais são mantidos por versão. + Configuração + Configurar perfil + Perfil + Perfil padrão + Utilizado automaticamente pelos jogos sem substituições. + Definições guardadas, utilizadas automaticamente quando %1$s é iniciado. + Escolha apenas as definições de que este jogo precisa. Tudo o resto continua a utilizar o Perfil padrão. + Predefinição do FEX + A utilizar a predefinição do Perfil padrão. + Predefinição personalizada guardada para este jogo. + Variáveis do FEX personalizadas + Utilizar padrão + Compatibilidade do jogo + Opções de ambiente do Proton, DXVK, VKD3D e sincronização para este jogo. + A utilizar o Proton do Perfil padrão. + Proton personalizado guardado para este jogo através do mapeamento de compatibilidade do Steam. + Sem Proton + DXVK + VKD3D-Proton + Utiliza o pacote do Perfil padrão de %1$s, a menos que este jogo selecione outro pacote. + Utiliza o pacote de componentes do Perfil padrão. + Utiliza %1$s apenas neste jogo. + Opções de execução + Outras opções de ambiente por jogo do Proton, DXVK, VKD3D-Proton e sincronização. + O perfil de %1$s foi guardado para o próximo arranque do jogo. + Iniciar sem o Steam + Para um jogo que fecha de imediato: inicia-o sem a configuração do Steam no Proton. Funcionalidades do Steam, como a sobreposição, ficam desativadas para ele. + Ativado + A escolha de Proton de %1$s foi guardada e já está disponível no Steam. + A escolha de Proton de %1$s foi guardada. Reinicie o Steam antes de iniciar o jogo. + %1$s não pode utilizar o Proton selecionado neste dispositivo. + Não foi possível carregar o perfil de %1$s: %2$s + Não foi possível guardar o perfil do jogo: %1$s + Repor perfil do jogo + Repor o perfil de %1$s? + Remover as substituições de Proton, FEX, pacote de componentes, ambiente, filtragem de texturas e componentes do Windows deste jogo? Passará a utilizar o Perfil padrão. + %1$s utiliza agora o Perfil padrão. + Não foi possível repor totalmente o perfil do jogo: %1$s + %1$s executa agora todos os jogos que não escolheram o seu próprio Proton no Steam. + A partir do próximo arranque do Steam, %1$s vai executar todos os jogos que não escolheram o seu próprio Proton. + %1$s não consegue iniciar jogos aqui, por isso não pode ser o padrão. + Não foi possível guardar o Proton padrão: %1$s + %1$s · %2$d trocados + %1$s · todos originais + %1$s · jogo em execução + %1$s · pacote droiddeck-esync + %1$s · ainda sem pacote droiddeck-esync + %1$s · esync integrado, não precisa de pacote + Controladores da GPU + %1$s · reaplicado no arranque %2$s + Seguinte: %1$s · depois de o jogo fechar + %1$s em utilização: %2$s + Aguarda até o jogo fechar + Aplica-se na próxima vez que um jogo iniciar + Importar .zip + A ler os Protons… + Ainda não há nenhum Proton instalado. Inicie o Steam uma vez para transferir o respetivo Proton ARM64, ou adicione o GE-Proton, o CachyOS ou o DroidDeck-Proton a partir da Configuração. + Original · %1$s + %1$s · esta versão + %1$s · versão anterior + Instalados · %1$d + Ainda nada guardado para %1$s. Os ficheiros deste Proton são guardados como o original. + Há um jogo em execução com este Proton, por isso isto aguarda até que feche. + Aplica-se na próxima vez que um jogo iniciar. + Trocar %1$s? + Colocar %1$s em %2$s? Os ficheiros originais ficam guardados. %3$s + Restaurar %1$s? + Repor o original de %1$s em %2$s? %3$s + Remove o pacote da app. Pode transferi-lo novamente. + Eliminar este original? + Este original de %1$s é de uma versão anterior de %2$s. O original da versão atual é sempre mantido. + Nas Nightlies · %1$d + Atualize para listar os pacotes nas Nightlies. + Nada de novo desde a última verificação. + Importar .wcp + Restaurar o original deste Proton + Repor o %2$s original de %1$s? + Trocar + FEX, DXVK e VKD3D-Proton para cada Proton com que o Steam executa jogos.\n\nSelecione uma linha instalada para a trocar. Aplica-se na próxima vez que um jogo iniciar, ou depois de o jogo em execução fechar.\n\nAs suas escolhas são verificadas novamente antes de cada arranque e repostas se algo as tiver alterado. Os ficheiros originais de cada Proton são mantidos, por isso as atualizações do Steam nunca os perdem. + Original + Em utilização + Novo + Guardado + Desempenho + Núcleos da CPU, correções da sessão e nome do anfitrião. Aplica-se na próxima sessão. + Núcleos do cliente Steam + Substituir a escolha de núcleos do Steam + Fixa o Steam, o respetivo auxiliar de interface e o gamescope nos núcleos abaixo. + Núcleos do cliente + Núcleos para o Steam. + Ative a substituição para escolher. + Núcleos dos jogos + Núcleos dos jogos + Núcleos para os jogos. Com todos selecionados, o Android distribui livremente. + Interface do cliente + GL multithread + Pode fazer os menus do Steam responder mais depressa. + Zink: descritores diferidos + Recomendado para controladores sem buffers de descritores. + Ignorar verificações de erros GL + Desativa a validação GL em cada chamada. + gamescope: fila de GPU em tempo real + Dá prioridade ao trabalho de GPU do compositor. Pode suavizar o ritmo de fotogramas, com algum custo para os jogos. + GPU + Manter a GPU na frequência máxima + Apenas Adreno. Menos soluços, mais consumo de bateria e mais calor. + Correções da sessão + Turnip: renderização em sysmem + Mais rápido para o Steam e a maioria dos jogos. Obrigatório no Adreno 710/720/722. + Ignorar o assistente xalia do Steam + droiddeck-ntsync (experimental) + Um substituto em espaço de utilizador para o controlador ntsync do Linux, para testes. Enquanto estiver ativado, o Proton utiliza-o em vez do droiddeck-esync e do droiddeck-fsync. Funciona com o Proton Experimental ARM64, o GE-Proton e o proton-cachyos. Aplica-se na próxima sessão. + droiddeck-esync + droiddeck-fsync primeiro + Executa o fsync do próprio Proton sobre os ficheiros originais do Proton, com o futex_waitv fornecido em espaço de utilizador, por isso não precisa de pacote e continua a funcionar assim que o Steam atualiza o Proton. Enquanto estiver ativado, os jogos utilizam-no antes do droiddeck-esync, exceto os poucos que o Proton mantém fora do fsync. Aplica-se na próxima sessão. + Os threads Windows dos jogos esperam uns pelos outros através de eventfd e memória partilhada, em vez de idas e voltas pelo wineserver. Utiliza um pacote droiddeck-esync feito para essa versão exata do Proton; os pacotes são obtidos automaticamente. Até existir um para uma nova versão do Proton, o droiddeck-fsync assume o seu lugar. Aplica-se na próxima sessão. + Desativa o assistente de navegação por comando do Proton, que gasta CPU em todos os jogos. + Executar o proot sem seccomp + Pode corrigir erros de chamadas de sistema em falta em alguns kernels. Pode reduzir o desempenho. + Caminho rápido do proot + Requer o filtro seccomp do proot. + Pesquisas de ficheiros mais rápidas e menos soluços. Desative se um programa não encontrar os seus ficheiros. + Identidade da sessão + Nome do anfitrião + O nome com que o Steam vê este dispositivo. Em branco, repõe %1$s. + Apenas letras, algarismos e hífenes interiores, até 63 carateres. + O Android está configurado para terminar esta sessão + Instalado + Disponível para transferir + Nada de novo desde a última verificação. + Restaurar controladores integrados + Integrado na app: isto remove os ficheiros extraídos e oculta-o. Restaurar controladores integrados volta a mostrá-lo. + Metade de um pacote Android + Linux: as duas metades são removidas. Um controlador em utilização volta ao padrão. + Os ficheiros são removidos da app. + Os ficheiros são removidos. Como está em utilização, o padrão passa a ser utilizado. + Ecrã + Efeitos + Controlos + Componentes + Comando + Definições + Página %1$s + Steam + Ambiente de trabalho + Menu do Steam + Acesso rápido + Abrir o Menu de acesso rápido + Página anterior + Página seguinte + HUD de desempenho + Modo de escala + Nitidez da escala + Efeitos de ecrã + Em tempo real. Gasta algum tempo de GPU enquanto houver um efeito ativo. + Predefinições + Personalizado + Avançado + Expandido + Recolhido + Nitidez (CAS) + Nível de nitidez + HDR simulado + Redução de bandas + Intensidade da redução de bandas + Brilho + Contraste + Gama + Saturação + FXAA (suavização de arestas) + Contornos de desenho animado + CRT + NTSC + Filtragem de texturas + Jogos DirectX 9–11, a partir do próximo arranque. + Filtragem anisotrópica + Nitidez das texturas + Um desvio de mip negativo: texturas mais nítidas, algum cintilar. Automático acompanha o quanto a sessão é ampliada para o ecrã. + Automático (%1$s) + Teclado + Teclado de PC + Android + Segundo ecrã + Modo + Mostra + Apps Android + Iniciar uma app + Ocultar + Mostrar + Nenhuma app para iniciar + Comportamento da sessão + Aplica-se agora e às próximas sessões neste modo. + Próxima sessão + Suporte + Registos da sessão + Para anexar a um relatório de erros + Partilhar registos + Segundo plano + Terminar + Terminar sessão? + Em execução + Nenhum jogo em execução num Proton + Perfil do jogo + Os detalhes do perfil ainda não estão disponíveis. + Não foi possível carregar os perfis dos jogos. A tentar novamente de forma automática. + Não foi possível atualizar os perfis dos jogos. A mostrar os últimos dados disponíveis; a tentar novamente de forma automática. + Ver e editar as definições guardadas que são utilizadas no próximo arranque. + A utilizar o Perfil padrão + Perfil personalizado + A utilizar %1$s do Perfil padrão. + As alterações são guardadas no perfil de %1$s e aplicam-se na próxima vez que o jogo for iniciado. + + %1$d substituição do jogo + %1$d substituições do jogo + %1$d substituições do jogo + + Ainda não há nenhum Proton instalado. + jogo em execução + reaplicado no arranque %1$s + Original + Importe e elimine na página Componentes. + Concluído + Requer atenção + A processar + Verifique o endereço IP e a porta da Depuração sem fios, ou sincronize novamente se o Android tiver removido este dispositivo. + Instalar + Gerir + Vista geral + Comando + Sessão + Iniciador + Configuração + ● Tudo pronto + %1$d de %2$d prontos + Verificação do sistema + Dispositivo suportado + GPU não testada + GPU não suportada + %1$s · %2$s + %1$s: %2$s + O Steam requer um controlador Adreno; %1$s pode mostrar um ecrã preto + Ambiente de execução Linux + %1$s · %2$d%% + Não instalado · cerca de 3 GB, instalado ao tocar em Jogar pela primeira vez + Instalado + %1$s · atualização disponível + %1$s · atualizado + Limite de processos secundários + Ativado · o Android pode fechar o Steam. Desativá-lo demora um minuto. + Não definido · o Android pode fechar o Steam. Desativá-lo demora um minuto. + Não foi possível verificar · desativá-lo demora um minuto. + Corrigir + Detalhes + Opções de programador + Utilizar a Depuração sem fios + Verificar novamente + Ativar limite + Conta Steam + Sessão iniciada como %1$s · modo offline + Sessão iniciada como %1$s + Prima Jogar e inicie sessão no Steam + Ferramentas + Comando + As definições do comando não estão disponíveis. + Sessão + Registos da sessão + Guardados após cada sessão + Ferramentas de depuração + Registos da última sessão + Registos de sessões guardados + Mantém os %1$d mais recentes + Limpar registos + Modo offline + Inicie sessão no Steam primeiro + Iniciador + Idioma + O Steam também o utiliza, a partir do próximo arranque. + Predefinição do sistema (%1$s) + Predefinição do sistema + Escala da app + Texto e controlos do DroidDeck + %1$d%% (padrão) + Tema + Aspeto do iniciador + Utilizar como página inicial + O DroidDeck pode ser a app de página inicial + O DroidDeck nunca é proposto como app de página inicial + Orientação + Horizontal automática + Horizontal + Horizontal invertida + Ecrã inteiro + Oculta as barras de estado e de navegação do Android + Mostra as barras de estado e de navegação do Android + App de página inicial predefinida + Escolher uma app de página inicial + Escolher + Apps Linux (beta) + Ficheiros + Explorar e gerir ficheiros + Versões do Proton + Instalar versões ARM64 do Proton + Desempenho + Atribuição de núcleos da CPU + Pasta de ROMs + Onde são guardados os jogos de emuladores + Componentes + FEX, DXVK, VKD3D + Controlos + Mapeamento de botões + Descobrir + Pesquisar + Instaladas + %1$s · %2$d + Loja + Flathub · ARM64 + ● Flatpak pronto + Instale primeiro o ambiente de execução Linux na Configuração. + Configurar o Flatpak + Adiciona o Flatpak e liga-o ao Flathub (cerca de 20 MB). A primeira app também transfere o respetivo ambiente de execução. + A configurar… + Flatpak + Atualizações + %1$s · %2$s + %1$s · %2$s · %3$d%% + A iniciar… + Categorias + Não foi possível aceder ao Flathub. Verifique a sua ligação e tente novamente. + Tentar novamente + Pesquisar no Flathub + Firefox, jogos, emuladores… + Pesquisar + Todas + A pesquisar… + Pesquise por nome ou escolha uma categoria. Só são listadas apps ARM64. + Nenhuma app ARM64 no Flathub corresponde a essa pesquisa. + Resultados + Procurar atualizações + Atualizar todas (%1$d) + ● Tudo atualizado + Ainda não há apps. Encontre algumas em Descobrir ou Pesquisar. + Configure o Flatpak para instalar apps. + Atualização disponível · %1$s + Abrir + Detalhes + Remover + Prima novamente para remover + A instalar… + Instalar + Configure primeiro o Flatpak + Transferência de %1$s + ● Instalada + Não foi possível carregar a página desta app no Flathub. + A carregar do Flathub… + Não há versão ARM64 no Flathub, por isso não pode ser instalada aqui. + Capturas de ecrã + Acerca de + ID da app + Licença + Ambiente de execução + Tamanho instalado + %1$s (mais o ambiente de execução partilhado) + Site + Verificado + Atualizações + DroidDeck %1$s · %2$s + Verificado %1$s + Ainda não verificado + Permitir que o DroidDeck se atualize + No ecrã seguinte, ative Permitir desta fonte e depois volte. A atualização continua sozinha. + Abrir definições + Agora não + Assinatura diferente + Não é possível atualizar diretamente + Esta cópia não está assinada com a chave de lançamento do DroidDeck, por isso as atualizações não podem ser instaladas por cima dela. Desinstale-a para mudar. + Atualização disponível + Não é possível instalar + Pronto para mudar + À frente do Estável + Está à frente do Estável + Esta versão é mais recente do que a última Estável. Passará para o Estável quando sair a próxima versão. + Teste terminado + Este teste terminou + A correção foi integrada ou abandonada. Siga a Antevisão para obter as correções mais recentes. + Atualizado + Já tem a versão mais recente: %1$s + · %1$s + %1$s… %2$d%% + %1$s… + Atualizar%1$s + Instalar%1$s + Instalar a versão Estável %1$s mesmo assim + Seguir a Antevisão + Termine a sessão em execução para atualizar. + Menos + Mais + DroidDeck %1$s + Nova versão de Antevisão + Versão de teste do PR #%1$d + %1$d MB + CANAL DE ATUALIZAÇÃO + Estável + Versões testadas, para a maioria das pessoas + %1$s · %2$s + Antevisão + As correções mais recentes, antes do Estável + Atualizado + Versões de teste + Experimente uma correção antes de ser lançada + Nenhuma de momento + PR #%1$d + Não compilada para esta cópia do DroidDeck + Atualizado %1$s + o teste do PR #%1$d + há algum tempo + agora mesmo + + %1$d para experimentar + %1$d para experimentar + %1$d para experimentar + + + Alterações desde a sua versão · %1$d versão em atraso + Alterações desde a sua versão · %1$d versões em atraso + Alterações desde a sua versão · %1$d versões em atraso + + + Alterações recentes · %1$d versão + Alterações recentes · %1$d versões + Alterações recentes · %1$d versões + + Uma definição do Android a alterar + Steam + O Android termina as apps que iniciam muitos processos em segundo plano, e o Steam inicia dezenas. Enquanto isto não for alterado, os jogos fecham sem qualquer erro. + Corrigir por mim + Ative “Desativar restrições de processos secundários” e depois volte. Mantenha as Opções de programador ativadas, ou o interruptor volta a desligar-se. + Abrir Opções de programador + Esta versão do Android não tem um interruptor para isto, por isso o DroidDeck altera-o através da Depuração sem fios. Demora cerca de um minuto e não é preciso computador. + A Depuração sem fios requer Wi-Fi. Qualquer rede serve; não precisa de Internet. + O código de sincronização é introduzido numa notificação. Permita as notificações do DroidDeck ou utilize Outras formas. + Permitir notificações + Abra Definições → Opções de programador → Depuração sem fios e ative-a + Toque em “Sincronizar dispositivo com código de sincronização” + Escreva o código na notificação do DroidDeck + A aguardar a janela de sincronização… + Janela encontrada. Introduza o código dela na notificação. + Estado + A verificar a cada 2 segundos. Esta página fecha-se quando estiver desativado. + Outras formas + Depuração sem fios, endereço manual, ADB num computador + Endereço manual, ADB num computador, Opções de programador + Utilizar antes a Depuração sem fios + Algumas ROMs adicionam o interruptor mesmo assim. Procure “processos secundários” nas Opções de programador. + Se a notificação não funcionar, introduza manualmente o endereço e o código da janela. + Introduzir endereço manualmente + A partir de um computador com ADB: + Copiar comando + Escolha em que ecrã abrir as Definições do Android. + Ecrã principal + Sincronizado. A procurar a porta da Depuração sem fios… + Sincronizado. Introduza a porta indicada nas Definições. + Dispositivo encontrado. A aplicar a definição… + Não foi possível aplicar automaticamente. Verifique o endereço abaixo e tente novamente. %1$s + Depuração sem fios + Limite de processos secundários atualizado + Alterar o limite de processos secundários + Se a opção não existir, sincronize através da Depuração sem fios. + Sincronizado. Verifique o endereço IP e a porta na Depuração sem fios. + O limite de processos secundários foi atualizado. + Utilize o passo 2 se as Opções de programador não tiverem a definição de processos secundários. Abra Opções de programador → Depuração sem fios → Sincronizar dispositivo com código de sincronização. + Sincronizado. Utilize o endereço abaixo ou atualize-o em Depuração sem fios → Endereço IP e porta. + O limite de processos secundários foi atualizado através da Depuração sem fios. + O Android confirmou que o limite está ativado. + O Android confirmou que o limite está desativado. + 2 · Sincronizar a Depuração sem fios + Endereço IP e porta da janela de sincronização + Código de sincronização (PIN) + 6 dígitos + Sincronizar + 2 · Aplicar através da Depuração sem fios + Endereço IP e porta de ligação + Sincronizar novamente + Desativar limite + Outras formas de o alterar + 1 · Opções de programador (tente primeiro) + Ative “Restringir processos secundários”. + Desative “Restringir processos secundários”. + 3 · ADB num computador (último recurso) + Apenas se a Depuração sem fios não estiver disponível. + 3 · ADB num computador (último recurso) + Quando os controlos táteis aparecem numa sessão + Cor + Tonalidade dos botões no ecrã + Personalizada + Opacidade + %1$d%% + Tamanho dos botões + O tamanho padrão é 100% + Clique do analógico + Toque duas vezes num analógico e mantenha para L3 ou R3 + Analógicos adaptáveis + Vibração do jogo + Botão Steam + Botão QAM + Botão do teclado + Premir Select para abrir o menu do Steam + Em vez de View/Select. + Animações + Os analógicos aparecem onde tocar e ocultam-se ao soltar + Esquema + Posições personalizadas guardadas + Ajustado a este ecrã + Editar + Repor + Mapeamento de botões + Cada botão envia a sua própria entrada + %1$d de %2$d botões remapeados + Configurar + Repor comando + Restaura todas as definições do comando e o esquema + O que cada botão no ecrã envia para o jogo. Oculto remove o botão. + Botões no ecrã + Valores padrão + Repor mapeamento + Apps Android + %1$d apps + Não foram encontradas apps Android para iniciar. + ● Ambiente de trabalho instalado + É instalado ao abrir pela primeira vez + Disponível para instalar + Não instalado. + %1$s · não instalado + Abrir %1$s + Jogos + Escolha uma pasta de ROMs. + Navegue até /root/ROMs no RetroArch. + Adicione jogos de %1$s a ROMs/%2$s. + Instalar %1$s + Termine a sessão para instalar + Ambiente de execução necessário + A carregar os detalhes da instalação… + Os detalhes da instalação não estão disponíveis. Tente novamente mais tarde. + Instale primeiro o ambiente de execução Linux na Configuração. + Termine a sessão em execução para instalar apps do ambiente de trabalho. + Instala %1$s no ambiente de trabalho Linux. + %1$s · %2$d%% + Ocultar remove-o do Ambiente de trabalho. Os ficheiros permanecem no ambiente de execução Linux. + Esse jogo já não está na pasta de ROMs. + Ambiente de trabalho · %1$s + Iniciar em %1$s + Também em %1$s + Essa página já não existe. Escolha uma secção à esquerda. + Abrir ambiente de trabalho + Instalar e abrir ambiente de trabalho + Desinstalar ambiente de trabalho + Remove o ambiente de trabalho e as apps incluídas. Mantém os jogos, as apps adicionadas, as definições e as bibliotecas partilhadas. + A remover o ambiente de trabalho + Ambiente de trabalho removido + A remover ficheiros antigos do LXQt + Atualize o DroidDeck antes de remover esta versão do ambiente de trabalho. + Ambiente de trabalho Linux + KDE Plasma, Firefox e os seus emuladores + Os jogos que instalar no Steam aparecem aqui após a sessão. + Jogar no Steam + Definições de arranque + Iniciar + Ficheiros do jogo + Prefixo do Proton + A procurar gravações… + Nenhuma pasta de gravações encontrada pelo nome. Exportar inclui toda a pasta do utilizador. + %1$d pastas de gravações + %1$s · %2$d ficheiros · %3$s + %1$d KB + Gerir gravações + Gravações do jogo + Importar gravações… + Exportar como zip do GameHub… + Exportar como zip do Winlator… + Abrir pasta de gravações + Nunca jogado + Ambiente de execução ocupado · %1$d%% + Ambiente de execução ocupado + O ambiente de execução é instalado ao tocar em Jogar pela primeira vez + Atualização do ambiente de execução disponível + ● Ambiente de execução pronto + Jogado pela última vez %1$s + Armazenamento interno + Jogo adicionado + Jogo personalizado + Armazenamento de jogos + O melhor par para esta GPU, sempre atualizado + Escolha um par manualmente + A procurar controladores… + Ainda não há controladores listados. Atualize para os procurar. + Ocultar controladores para outras GPUs + Mostrar controladores para outras GPUs + Avançado + Definido em conjunto com o controlador de ecrã: %1$s inclui ambos. + Definido em conjunto com o controlador de execução: %1$s inclui ambos. + Utilizado pelos jogos e pelo ambiente de trabalho. + Padrão do ambiente de execução + Desenha os fotogramas no ecrã. Aplica-se depois de o DroidDeck reiniciar. + Automático - escolhido conforme a GPU + Transferir, importar ou eliminar… + EM UTILIZAÇÃO + Os controladores integrados no DroidDeck + Controladores de execução e de ecrã definidos em conjunto como um par correspondente. + Recomendado para esta GPU: %1$s. + Atualize para procurar controladores para esta GPU. + RECOMENDADO + Indisponível + Ambiente de execução Linux + A preparar o ambiente de execução Linux + Sessão + A iniciar o ecrã e o áudio + Cliente Steam + A verificar o Steam e as respetivas ferramentas de compatibilidade + A abrir o Steam + A aguardar o primeiro fotograma do Steam + Ambiente de trabalho + A preparar o ambiente de trabalho + A abrir + A aguardar o primeiro fotograma + A iniciar o Steam + A sessão terminou + Passo %1$d de %2$d + O Steam está em pausa + Pegas + Painel esquerdo + Painel direito + Ambos + Comando + Joystick + Trackpad + Alternar entre joystick e trackpad + Mais controlos do Deck + PAINÉIS + Fechar controlos do segundo ecrã + Abrir menu Steam + Esquerda · superior + Esquerda · inferior + Direita · superior + Direita · inferior + Clique + Trackpad direito + Trackpad esquerdo + Trackpad + Mostrar teclado + Ocultar teclado + Terminal Linux + A sessão Linux ainda não está pronta para um terminal. + Steam + Jogos + Desktop + Loja + Componentes + Apps + Configuração + Atualizações + %1$d%% + Retomar %1$s + Aparece como /root/ROMs em cada sessão. O armazenamento interno está em /root/Storage. + Nenhuma pasta escolhida + Aplica-se na próxima sessão. Funciona com cartões SD. + Escolher pasta + Alterar pasta + Instale uma versão ARM64 do Proton e escolha-a para cada jogo em Steam → Propriedades → Compatibilidade. + Pare a sessão em curso para instalar ou remover uma ferramenta de compatibilidade. + Versões disponíveis + Instalado: %1$s + Pendente de um pedido anterior + Não instalado + Atualização disponível → %1$s + Remover versões antigas do %1$s + Remove %1$s. Os jogos definidos para estas versões têm de mudar para outro Proton. + Os jogos definidos para esta versão têm de mudar para outro Proton. + Instalar agora + Cancelar pedido pendente + Teclado Android + Ocultar ✕ + Iniciar %1$s + Escolha um ecrã. Secundário: %1$s. + O ecrã secundário já não está disponível. + Ecrã principal + Ecrã secundário + Separador anterior + Separador seguinte + Ativado + Todos (%1$d) + Nenhum + %1$d de %2$d + Desativado + Ecrã inferior + Aplica-se no próximo arranque do jogo. O perfil de um jogo substitui o partilhado. + Outro jogo por ID da app… + Concluído + Predefinido + Desativado + Alterar… + Alterar variável + Repor predefinição + Jogos Proton + Ecrã no ecrã + Entrar automaticamente em ecrã no ecrã + Controlos no ecrã desdobrado + Desdobrado, o jogo fica acima dos trackpads e das pegas traseiras do Steam Deck. Dobrado, o jogo ocupa o ecrã inteiro. + Suspenso + O ecrã no ecrã fica disponível quando a sessão estiver pronta. + Permita o ecrã no ecrã para o DroidDeck nas definições do Android. + O Android não conseguiu abrir o ecrã no ecrã. + Atalho + Adicionar ao ecrã principal + Escolher jogo + A procurar jogos instalados… + Copiar link de arranque + Link de arranque copiado + Este iniciador não permite adicionar atalhos. Use antes «Copiar link de arranque». + Link de arranque do jogo inválido. + Este jogo já não está na sua biblioteca do DroidDeck. + Pare a sessão atual antes de iniciar um jogo do Steam. + Não foi possível enviar o jogo para o Steam. Tente iniciá-lo novamente. + Tentar remover novamente + A remover + Remoção incompleta + Exportar ficheiro do jogo + Sincronizar todos os ficheiros de jogos com uma pasta + Parar a sincronização dos ficheiros de jogos + Escolha uma pasta para os ficheiros de jogos do frontend + Ficheiros de jogos guardados em %1$s + Ficheiros para frontends + + Aguarde que a operação do runtime termine + Aguarde que a instalação da ferramenta de compatibilidade termine + Aguarde que a instalação da app de ambiente de trabalho termine + Aguarde que a instalação termine antes de iniciar um jogo + Feche primeiro a sessão do Steam, para que o jogo não grave por cima da importação + A importar para %1$s + + %1$d ficheiro importado do %2$s para %3$s + %1$d ficheiros importados do %2$s para %3$s + %1$d ficheiros importados do %2$s para %3$s + + + %1$d ficheiro importado do %2$s para %3$s. Cópia de segurança dos dados guardados antigos em Download/DroidDeck/Saves/backups + %1$d ficheiros importados do %2$s para %3$s. Cópia de segurança dos dados guardados antigos em Download/DroidDeck/Saves/backups + %1$d ficheiros importados do %2$s para %3$s. Cópia de segurança dos dados guardados antigos em Download/DroidDeck/Saves/backups + + Escolha um zip de dados guardados para %1$s + A exportar %1$s + + %1$d ficheiro exportado como %2$s: %3$s + %1$d ficheiros exportados como %2$s: %3$s + %1$d ficheiros exportados como %2$s: %3$s + + Escolha onde guardar %1$s (%2$s) + %1$s: falhou (%2$s) + Ainda não há registos de sessão: execute primeiro uma sessão. + Não há registos de sessão para limpar. + + Foi limpa %1$d pasta de registos de sessão. + Foram limpas %1$d pastas de registos de sessão. + Foram limpas %1$d pastas de registos de sessão. + + Falha na sincronização da depuração sem fios + Falha no comando de depuração sem fios + A depuração sem fios não está disponível + Comando ADB copiado + Escolha a pasta de ROMs + Não é uma GPU Adreno + O Turnip suporta GPUs Adreno. Com %1$s, o Steam pode mostrar um ecrã preto. Transferência: %2$.0f MB. + Instalar mesmo assim + Remover runtime Linux + Isto elimina o runtime, o cliente Steam que nele existe e todos os jogos lá instalados. + Aplicado: %1$s + Não foi possível abrir %1$s no ecrã principal + Não foi possível abrir %1$s em %2$s + Não foi possível abrir %1$s no ecrã %2$d + A importar + A trocar + A restaurar + A cancelar + A eliminar + não é possível ler o ficheiro + %1$s importado + A troca pendente foi cancelada. + Escolha um pacote de componente (-linux .wcp) + Não foi possível aceder às Nightlies + %1$s guardado + Falha na transferência: %1$s + Escolha um controlador do runtime Linux (zip -Linux ou pacote Android + Linux) + Escolha um controlador de ecrã (zip AdrenoTools ou pacote Android + Linux) + Escolha um zip de controlador ou um pacote Android + Linux + %1$s: %2$s + A remover %1$s… + A esquecer %1$s… + Escolha a pasta de armazenamento de jogos + Escolha um ZIP de plugin Decky + Não utilizável: %1$s + Programa + A remover o runtime Linux + Não foi possível abrir as Definições no ecrã inferior; foram abertas no ecrã principal. + Abra as Opções de programador nas Definições + + Não foi possível abrir a definição «Instalar apps desconhecidas» do Android + Não foi possível verificar: %1$s + Esta versão não tem transferência para esta cópia do DroidDeck + Pare primeiro a sessão em curso e depois atualize + A transferir + A instalar + A aguardar a sua confirmação + A atualização falhou + O Android não ofereceu a instalação da atualização + + Falha na instalação do Decky: %1$s + A iniciar a importação do plugin… + Falha na importação do plugin: %1$s + Plugin instalado. Reinicie o Steam para o carregar. + + Ainda não verificado - toque em atualizar para procurar novos controladores + Android + Linux: define também o controlador de ecrã + Android + Linux: define também o controlador do runtime + o Turnip incluído no runtime + Vulkan do sistema: sem versão incluída para esta GPU + %1$s (incluído) + Manual + %1$s · %2$s + De momento, só uma das metades está publicada + %1$.0f MB para ambos + Nenhum controlador para definir: %1$s + A procurar os controladores mais recentes… + Atualizado (não foi possível verificar: %1$s) + Os controladores recomendados não estão publicados de momento + A transferir %1$s %2$s… + Atualizado para %1$s %2$s. + Atualizado para %1$s %2$s. O controlador de ecrã aplica-se depois de reiniciar o DroidDeck. + Alterado para %1$s %2$s. + Alterado para %1$s %2$s. O controlador de ecrã aplica-se depois de reiniciar o DroidDeck. + A usar %1$s %2$s. + A usar %1$s %2$s. O controlador de ecrã aplica-se depois de reiniciar o DroidDeck. + Não é um zip de controlador: não é um controlador AdrenoTools, um Turnip -Linux nem um pacote Android + Linux + Falha na importação: %1$s + Controlador importado + A usar %1$s como controlador do runtime e de ecrã. + A usar %1$s como controlador do runtime e de ecrã. O controlador de ecrã aplica-se depois de reiniciar o DroidDeck. + %1$s eliminado (ambos os controladores) + %1$s eliminado + Mais recentes: %1$s · verificado %2$s + Mais recentes: %1$s · sem acesso a %2$s · verificado %3$s + + há %1$d min + há %1$d min + há %1$d min + + + há %1$d h + há %1$d h + há %1$d h + + + há %1$d dia + há %1$d dias + há %1$d dias + + A verificar Banners-Turnip, WinNative e DroidDeck… + falha na verificação + %1$s instalado - escolha-o em qualquer das listas para definir ambos os controladores + %1$s instalado - escolha-o no menu + + Aguarde que a remoção do runtime termine + Jogo + painel tátil + direto + Falha na troca: %1$s + Falha ao guardar o perfil: %1$s + O ambiente de trabalho está em pausa + %1$s está em pausa + O programa está em pausa + A iniciar o ambiente de trabalho + A iniciar %1$s + A iniciar o programa + Não há registos para esta sessão. + Não foi possível criar o arquivo dos registos da sessão. + Não foi possível partilhar os registos da sessão. + A iniciar a sessão… + a transferir o runtime Linux + a transferir o runtime Linux · %1$d de %2$d MB + a verificar o runtime Linux + a descompactar o runtime Linux + a transferir o ambiente de trabalho + a transferir o ambiente de trabalho · %1$d de %2$d MB + a verificar o ambiente de trabalho + a descompactar o ambiente de trabalho + a transferir o Proton Experimental (ARM64) + a transferir o Proton Experimental (ARM64) · %1$d de %2$d MB + a verificar o Proton Experimental (ARM64) + a descompactar o Proton Experimental (ARM64) + Não foi possível aceder ao catálogo de runtimes. Verifique a ligação e prima Jogar novamente. + O runtime Linux não foi instalado. Verifique o runtime em Configuração e prima Jogar novamente. + O ambiente de trabalho não foi instalado (%1$s). Verifique a ligação e prima Ambiente de trabalho novamente. + %1$s parou inesperadamente. Partilhe os registos num relatório de erro ou tente novamente. + O programa parou inesperadamente. Partilhe os registos num relatório de erro ou tente novamente. + O ambiente de trabalho parou inesperadamente. Partilhe os registos num relatório de erro ou tente novamente. + Código de saída %1$d · %2$s + sem registo + + O registo mostra %1$d erro «Function not implemented»: a aceleração seccomp do proot está a fazer falhar um processo auxiliar neste dispositivo. Experimente %2$s → «%3$s» (ou «%4$s») e inicie novamente. + O registo mostra %1$d erros «Function not implemented»: a aceleração seccomp do proot está a fazer falhar um processo auxiliar neste dispositivo. Experimente %2$s → «%3$s» (ou «%4$s») e inicie novamente. + O registo mostra %1$d erros «Function not implemented»: a aceleração seccomp do proot está a fazer falhar um processo auxiliar neste dispositivo. Experimente %2$s → «%3$s» (ou «%4$s») e inicie novamente. + + + Plugins + Instale um ZIP de plugin para a próxima sessão do Steam. + A transferir… + Instalar a partir de ZIP + Jogos + Áudio e vídeo + Gráficos + Internet + Escritório + Programação + Educação + Ciência + Sistema + Utilitários + Populares + Tendências + Novidades + Atualizadas + a carregar… + Configuração do Flatpak + %1$s: %2$s + Não foi possível procurar atualizações no Flathub + instalado + pasta + Grafite + Superfícies em grafite, com o azul da esfera do logótipo para tudo o que está selecionado, focado ou premido. + Papel sobre preto + O branco é a cor principal; o azul do ícone é o sinal para linhas, anéis e pontos. + Azul do ícone + Fundo preto, texto branco e o azul para tudo o que está selecionado, focado ou premido. + Azul-marinho elétrico + O azul mais vivo, em superfícies que tendem para o azul-marinho em vez do cinzento. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Retrocesso + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Espaço + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Instale primeiro o runtime + A biblioteca do Steam não está disponível + A biblioteca anterior do cartão SD não está disponível + O armazenamento de jogos não está disponível + A pasta de jogos adicionados não está disponível + Os manifestos do Steam não estão disponíveis + Vários sistemas + Interno + Sistema Linux + Cartão SD + Armazenamento + Selecionar + Propriedades + Adicionar aos favoritos + Remover dos favoritos + Mudar o nome + Copiar + Cortar + Ações + Favoritos + Fixar pasta atual + Ainda não há favoritos - fixe uma pasta através do respetivo menu ⋮ para voltar aqui rapidamente. + Remover dos favoritos + 0 bytes + bytes + Localização + Tipo + Tamanho + Modificado + Pasta + Ficheiro + Ficheiro %1$s + Só de leitura + Impede os jogos (e o Wine) de substituir ou eliminar este ficheiro. + Não foi possível alterar «Só de leitura» + Oculto + Este armazenamento não suporta o atributo de oculto. + Marca o ficheiro como oculto no Windows (atributo oculto do Wine). + Não foi possível alterar «Oculto» neste armazenamento + Concluído + A eliminar... + Falha ao eliminar + A mover... + A copiar... + A mover - %1$s + A copiar - %1$s + A mover %1$d/%2$d - %3$s + A copiar %1$d/%2$d - %3$s + Concluídos: %1$d, com falha: %2$d + Concluídos: %1$d, ignorados: %2$d + + %1$d item movido + %1$d itens movidos + %1$d itens movidos + + + %1$d item copiado + %1$d itens copiados + %1$d itens copiados + + «%1$s» já existe + A mudar o nome... + Falha ao mudar o nome + A criar pasta... + Não foi possível criar a pasta + Nova pasta + Nome da pasta + Criar + Novo nome + Eliminar? + Eliminar «%1$s» permanentemente? + + Eliminar %1$d item? + Eliminar %1$d itens? + Eliminar %1$d itens? + + Esta ação não pode ser anulada. + + …e mais %1$d + …e mais %1$d + …e mais %1$d + + A eliminar %1$d/%2$d - %3$s + + Não foi possível eliminar %1$d item + Não foi possível eliminar %1$d itens + Não foi possível eliminar %1$d itens + + «Combinar» adiciona e substitui ficheiros dentro da pasta existente. + «Substituir» troca o ficheiro existente pelo novo. + Aplicar a todos os conflitos + Combinar + Substituir + Manter ambos + Ignorar + Selecionar esta pasta + Voltar + %1$s está montado, mas não é possível lê-lo de momento + Vista de lista + Vista de grelha + Pesquisar + Ordenar + Nome + Data de modificação + Linhas espaçosas + Linhas compactas + Ocultar ficheiros ocultos + Mostrar ficheiros ocultos + Ocultar favoritos + Mostrar favoritos + Filtrar esta pasta + %1$s livres + Selecionados: %1$d + Nenhum + Todos + Colar + + %1$d item + %1$d itens + %1$d itens + + Colar %1$s aqui + Colar %1$s aqui (mover) + Ficheiros + ARMAZENAMENTO + ACESSO RÁPIDO + FAVORITOS + Transferências + Transf. + ROMs + Registos de sessão + Imagens + «%1$s» adicionado aos favoritos + «%1$s» removido dos favoritos + Pasta vazia + Subir um nível + Escolha uma pasta + Escolha um ficheiro + Azul Steam + Céu + Branco + Violeta + Verde + Âmbar + Vermelho + Rosa + Botão A + Botão B + Botão X + Botão Y + Botão de ombro esquerdo + Botão de ombro direito + Gatilho esquerdo + Gatilho direito + Botão Ver + Botão Menu + Ver + Menu + Direcional para cima + Direcional para baixo + Direcional para a esquerda + Direcional para a direita + Oculto + Arraste para mover + Encaixar + Guardar + Nenhum + Teclado + trackpad + Terminal + Pegas + trackpads do Deck + + A remover o runtime Linux + + A remover o runtime Linux · %1$d entrada + A remover o runtime Linux · %1$d entradas + A remover o runtime Linux · %1$d entradas + + Runtime Linux removido + Não foi possível concluir a remoção do runtime Linux. Use «Tentar remover novamente» em Configuração. + A remover restos do runtime + A transferir + A verificar + A extrair + Aparece enquanto o runtime Linux é instalado ou removido + A instalar o runtime Linux + A remover o runtime Linux + + O runtime Linux não está instalado + O catálogo não tem soma de verificação para %1$s + A soma de verificação não corresponde - nada foi alterado + A instalar %1$s + Não foi possível colocar a AppImage + Falha na extração + Falha na instalação + + Falha na configuração do Flatpak (saída %1$d) + A configuração do Flatpak não terminou + Está em curso outra tarefa da loja + Configure primeiro o Flatpak + A iniciar + O Flatpak terminou com o código %1$d + A instalar %1$s + A atualizar %1$s + A remover %1$s + A instalar %1$s (%2$d/%3$d) + A atualizar %1$s (%2$d/%3$d) + A remover %1$s (%2$d/%3$d) + traduções + controladores gráficos + codecs multimédia + runtime %1$s + runtime %1$s %2$s + + O ficheiro é demasiado pequeno para ser uma AppImage + Não é possível ler o ficheiro: %1$s + Isto não é uma AppImage (não é um programa Linux) + Esta AppImage não foi compilada para ARM64 (aarch64) nem para PCs x86 + Isto é um programa Linux, mas não é uma AppImage + Só é possível importar AppImages do tipo 2 (esta é do tipo %1$d) + Espaço livre insuficiente: a importação precisa de cerca de %1$s + Espaço livre insuficiente: a atualização precisa de cerca de %1$s + Falha na importação + Essa AppImage já não existe + Falha na atualização + A extrair %1$s + Não foi possível extrair a AppImage + Não foi possível extrair a AppImage: %1$s + + Pare a sessão ativa antes de instalar o Decky Loader + Falha na transferência do Decky Loader + O tamanho do Decky Loader não corresponde aos metadados da versão + Não foi possível transferir a soma de verificação da versão + Falta a soma de verificação da versão + A soma de verificação da versão é inválida + A soma de verificação do Decky Loader não corresponde + O ficheiro da versão não é um executável Linux de 64 bits compatível + Foi iniciada uma sessão durante a transferência; pare-a antes de instalar o Decky Loader + Não foi possível tornar o PluginLoader executável + Não foi possível ativar a depuração remota do CEF do Steam + Não foi possível preparar o PluginLoader + Não foi possível preservar o PluginLoader existente + Não foi possível ativar o novo PluginLoader + Pare a sessão ativa antes de instalar um plugin + Instale o Decky Loader antes de importar um plugin + Não foi possível ler o ficheiro ZIP selecionado + Não foi possível criar a pasta de plugins do Decky + A extrair o ZIP do plugin + O ZIP do plugin tem de conter uma única pasta de plugin no nível superior + Faltam plugin.json, package.json ou dist/index.js no ZIP do plugin + Falta o nome do plugin + Foi iniciada uma sessão durante a instalação; pare-a antes de instalar um plugin + Plugin instalado · reinicie o Steam para o carregar + Falha na instalação do ZIP do plugin + O ZIP do plugin contém demasiadas entradas + O ZIP do plugin contém um caminho inválido + O ZIP do plugin contém caminhos duplicados + O ZIP do plugin contém um caminho inseguro + Não foi possível criar a pasta do plugin + O ZIP do plugin excede o limite de 1 GiB depois de extraído + Não foi possível tornar executável o binário incluído no plugin + Entrada remote_binary inválida + Nome de binário remoto inválido + O URL do binário remoto tem de usar HTTPS + O binário remoto %1$s tem um hash SHA-256 inválido + Não foi possível criar a pasta bin do plugin + A transferir o binário do plugin · %1$s + Não foi possível transferir o binário do plugin %1$s + A soma de verificação do binário do plugin não corresponde · %1$s + Não foi possível tornar executável o binário do plugin · %1$s + Não foi possível substituir o binário do plugin · %1$s + Não foi possível instalar o binário do plugin · %1$s + A pasta do plugin pertence a outro plugin + Não foi possível preservar o plugin existente + Não foi possível ativar o plugin importado + Não foi possível atualizar as definições de plugins do Decky + Não foi possível restaurar o plugin existente + + Não foi possível guardar a identidade da depuração sem fios + O comando ADB falhou (%1$d) + O Android não confirmou a alteração do limite de processos secundários: %1$s + Não há nenhuma sincronização de depuração sem fios guardada + Introduza o endereço IP e a porta atuais da depuração sem fios + + Tempo esgotado à espera da janela de sincronização. Tente novamente a partir do DroidDeck. + Não foi possível procurar a janela de sincronização (%1$d). Verifique se o Wi-Fi está ativado. + Não foi possível procurar a janela de sincronização: %1$s + Introduza os 6 dígitos do código + A sincronizar… + Esse código não funcionou. Abra novamente a janela de sincronização e introduza o novo código. + Sincronizado. A ligar… + Sincronizado, mas a porta da depuração sem fios não foi encontrada. Mantenha a depuração sem fios ativada e tente novamente. + A aplicar a definição… + Falha no comando de depuração sem fios + Configuração da depuração sem fios + Pede o código de sincronização da depuração sem fios enquanto as Definições estão abertas + Sincronizar a depuração sem fios + Ative a depuração sem fios e toque em «Sincronizar dispositivo com código de sincronização». + Ative a depuração sem fios e toque em «Sincronizar dispositivo com código de sincronização». Fique nas Definições: o código é introduzido nesta notificação. + Código de sincronização de 6 dígitos + Toque em Introduzir código e escreva os 6 dígitos mostrados nas Definições. + Introduza o código de sincronização Wi-Fi + Introduza o código novamente + Introduzir código + A configurar o Steam + O Steam está pronto + Toque para voltar ao DroidDeck. Já pode desativar a depuração sem fios. + A configuração da depuração sem fios parou + + A transferência respondeu HTTP %1$d + O tamanho da transferência não correspondia ao APK publicado e foi descartada - tente novamente + A transferência não correspondia à soma de verificação e foi descartada - tente novamente + A atualização é para o pacote Android %1$s, não para %2$s + Os metadados da atualização não indicam a chave de assinatura de lançamento do DroidDeck + O Android não conseguiu ler o APK transferido + O APK transferido é para %1$s, não para %2$s + O APK transferido indica o versionCode %1$d, não o %2$d publicado + O Android não instala o versionCode %1$d por cima do %2$d instalado + O APK transferido não tem informações de assinatura + O APK transferido não está assinado com a chave de lançamento do DroidDeck + O catálogo de atualizações respondeu HTTP %1$d + + A iniciar a sessão… + %1$d:%2$02d decorridos · ainda a trabalhar + A transferir a atualização do cliente Steam · %1$d%% + A transferir o cliente Steam · %1$s + + Já está em curso a instalação de uma ferramenta de compatibilidade + Pare a sessão ativa antes de instalar ferramentas de compatibilidade + Não foi possível encontrar uma versão ARM64 de %1$s + São necessários pelo menos 4 GB livres, mais o tamanho da transferência, para instalar %1$s + A transferir %1$s %2$s + A repetir a transferência · tentativa %1$d de %2$d + Transferência interrompida + A transferência falhou; o ficheiro parcial é mantido para que uma nova tentativa a possa retomar + A transferência excedeu o tamanho da versão e foi eliminada + A transferência ficou incompleta; o ficheiro parcial é mantido para ser retomado numa nova tentativa + A verificar a transferência + Não foi possível ler a soma de verificação da versão; tente novamente + %1$s %2$s não publica soma de verificação; nada foi instalado + A soma de verificação não corresponde; a transferência foi eliminada + Foi iniciada uma sessão durante a transferência; pare-a antes de instalar ferramentas de compatibilidade + A instalar %1$s + Falha na instalação de %1$s (saída %2$d) + Não foi possível instalar %1$s + A obter o pacote droiddeck-esync + + com auxiliares Linux + Só DLLs + Original (Proton atualizado desde a última troca) + Alterado fora de Componentes + não presente + Original %1$s + Original de %1$s + %1$s %2$s será colocado em %3$s quando o jogo que o está a usar fechar. + %1$s %2$s está agora em %3$s. Aplica-se da próxima vez que um jogo iniciar. + Esse pacote original não está guardado + %1$s de %2$s volta ao original quando o jogo que o está a usar fechar. + %1$s de %2$s restaurado a partir do original de %3$s. + %1$s está em uso em %2$s. Troque-o primeiro por outro nesse Proton. + %1$s não está guardado + %1$s eliminado. + O original da versão instalada é mantido: é o caminho de volta. + Não guardado + Original de %1$s de %2$s eliminado. + As transferências vêm apenas das versões Nightlies + Esta lista de pacotes é anterior às somas de verificação - atualize-a e tente novamente + A soma de verificação não corresponde + + Linear + Mais próximo + Snapdragon GSR (qualidade) + Só nitidez + Automático (acompanha a escala) + + Nada aplicado. + Clareza de jogo + Nitidez com um toque de contraste - a do dia a dia. + Vívido + Cor bem carregada. Debanding ativo para o céu não ficar às riscas. + Cinematográfico + Contraste mais profundo, cor mais fria, contornos suavizados. + Competitivo + Sombras realçadas e nitidez forte - aviste os adversários primeiro. + Nitidez adaptativa + Só nitidez, cor intacta. + Fílmico + Curva tipo película, ligeiramente esbatida. + Arcade + Forte e com impacto. + CRT retro + As passagens de tubo CRT e de sinal analógico, em conjunto. + Ampliação nítida + Para jogar abaixo da resolução do ecrã. + Píxeis limpos + Píxeis nítidos para 2D e jogos antigos. + Contornos anime + Contornos de desenho animado mais nitidez. + + Não é uma GPU Adreno + Suportada + Experimental: os controladores são versões de teste + Abaixo do hardware testado (Adreno 725 e mais recentes): pode não funcionar + Não suportada: o DroidDeck precisa de uma GPU Adreno (Snapdragon) + + Foi atingido o limite de pedidos do GitHub - tente novamente mais tarde + O GitHub respondeu HTTP %1$d + a transferência respondeu HTTP %1$d + Esta lista de controladores é anterior às somas de verificação - atualize-a e tente novamente + A soma de verificação não corresponde - a transferência foi descartada + Não é um controlador AdrenoTools: %1$s + sem meta.json + não é possível ler meta.json (%1$s) + este é um controlador do runtime Linux (importe-o em «Controlador do runtime Linux») + este é um controlador de jogos Wayland, que esta app não utiliza + meta.json não indica libraryName, por isso nada pôde ser passado ao AdrenoTools + meta.json indica %1$s, que não está no zip + %1$s não é uma biblioteca partilhada ELF AArch64 de 64 bits + %1$s liga à glibc - é um Turnip «-Linux», que pertence a «Controlador do runtime Linux» + Não há libvulkan_freedreno*.so neste zip. Um zip Turnip Android (AdrenoTools) ou -Wayland não é um controlador do runtime Linux. + %1$s não é uma biblioteca partilhada ELF AArch64 de 64 bits. + %1$s não é um controlador glibc - liga à libc do Android. O runtime Linux precisa de um zip «-Linux»; um Turnip simples ou «-Wayland» não pode ser carregado pelo cliente Steam. + Não é um pacote de controladores: não há manifest.json que indique um controlador Android e um Linux + Não é um pacote de controladores: %1$s aponta para fora dele + Não é um pacote de controladores: descompactado ocupa mais de %1$d MB + + Cartão SD + Pasta + + Partilhar registos da sessão + + requer Android 10 ou mais recente (camadas de ecrã com espaço de cor) + este ecrã não indica suporte HDR + este ecrã suporta %1$s, não HDR10 + + esta GPU + O limite de processos secundários está ativado + O limite de processos secundários não está definido + Não foi possível verificar o limite de processos secundários + O limite de processos secundários está desativado + Limite de processos secundários não necessário + %1$s Se não for possível, ligue este dispositivo a um computador com ADB e execute o comando abaixo. Volte aqui e verifique novamente. + As sessões Steam podem ser iniciadas. + Esta versão do Android não usa este limite. + Ative «Desativar restrições de processos secundários» nas Opções de programador e mantenha as Opções de programador ativadas depois. + Esta versão do Android não tem um interruptor para isto. Use a depuração sem fios para o alterar a partir deste dispositivo. + Esta versão não tem transferência para esta cópia do DroidDeck. + O Android não a instala por cima desta versão porque o versionCode %1$d é inferior ao %2$d instalado. + O Android não instala uma versão mais antiga por cima de uma mais recente. Mantenha esta versão até o canal a alcançar. + Esta cópia do DroidDeck tem uma assinatura diferente (compilação local?), por isso o Android não a atualiza no lugar. + Não há espaço livre suficiente para a atualização. + A atualização foi cancelada. + A atualização não foi instalada: %1$s + A atualização não foi instalada. + zip do %1$s + zip + %1$s ainda não foi iniciado + Nenhum ficheiro de dados guardados encontrado para %1$s + %1$s não tem pasta drive_c, por isso não é um zip de dados guardados do GameHub nem do Winlator + não é possível criar pastas em %1$s + não é possível escrever em %1$s + não é possível escrever em %1$s (%2$s) + A pasta não está disponível + Outro ficheiro já usa %1$s + A pasta de imagens não está disponível + A pasta tem um inventário de sincronização não relacionado + Não foi possível remover %1$s + O ficheiro sai da pasta de exportação + FEX (compilação sem nome de versão) + sem profile.json em %1$s + %1$s não é um pacote de componente Linux (tipo %2$s) + %1$s · %2$s livres + Automático + O catálogo de atualizações falhou as verificações (%1$s) + Definição de processos secundários do DroidDeck + Sessão iniciada · modo offline + Sessão iniciada + INCLUÍDO + TRANSFERIDO + IMPORTADO + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Ecrã da sessão + Resolução + Ajuste de jogos 16:9 + Como os jogos 16:9 se ajustam a este ecrã. Próxima sessão. + Escolha «Igual ao ecrã» ou um tamanho personalizado mais estreito do que 16:9. + Manter proporções + Esticar até ao ecrã + Escala da imagem + Filtro de escala + Partilhado pelo Steam e pelo Ambiente de trabalho. + Aplica-se em tempo real. Partilhado pelo Steam e pelo Ambiente de trabalho. + FSR, GSR e NIS aplicam-se ao ampliar a imagem da sessão. «Só nitidez» também funciona sem ampliação. + Nitidez do filtro + Taxa de fotogramas + %1$s Define a escala como %2$s. + Igual ao ecrã (%1$d×%2$d) + Seguir o ecrã (redimensiona ao dobrar ou desdobrar) + Steam + Mostrar detalhes do dispositivo + Ocultar detalhes do dispositivo + A ler o dispositivo… + Copiar + Copie estes detalhes para um relatório de erro. + Copiado. + Forçar SSBS + Mantém ativa uma definição de velocidade da CPU que o Wine está sempre a desativar. Pode tornar os jogos Windows mais rápidos no Snapdragon 8 Elite; pouca diferença nos chips mais antigos. Aplica-se no próximo arranque do jogo. + + Lojas + Steam + Personalizados + Lojas + Abrir uma loja em + Nível de velocidade de transferência + Motor das lojas + ● Motor pronto + Não compilado + Equilibrado + Rápido + Máximo + Lojas + Definições das lojas + Definições + (sessão não iniciada) + Loja + Instalados + Biblioteca + Todos + Transferências + %1$s (%2$d) + Iniciar sessão + %1$s · terminar sessão + Terminar sessão em %1$s? + Os jogos instalados mantêm-se. A biblioteca volta a ser obtida quando iniciar sessão novamente. + Manter sessão iniciada + Terminar sessão + Pesquisar no catálogo %1$s… + Pesquisar jogos instalados… + Pesquisar na sua biblioteca… + Pesquisar + Nada instalado + A carregar + Nenhum jogo + A carregar a loja… + Sem catálogo + Novidades + Promoções + Jogos gratuitos + Tendências + A sua biblioteca + Jogar + Instalar + Retomar instalação + Interrompida + Instalar · %1$s + Obter grátis + Ver em %1$s + Comprar · %1$s + Instalado + Na biblioteca + Grátis + Retomar + Atualizar + Mostrar conteúdo para adultos + Epic + EOS + Offline + Desativado + Início de sessão Epic (EOS) + Iniciar offline + Resolver início de sessão Epic + Dados guardados na nuvem + Carregar para a nuvem + Transferir da nuvem + Sem dados guardados na nuvem + Conflito na nuvem + Conflito na nuvem · %1$d ficheiros + … + Manter nuvem + Manter local + A verificar a nuvem… + Última sincronização: %1$s + Ainda não sincronizado + %1$d ficheiros + instalado + na sua biblioteca + loja + Conquistas + %1$d de %2$d desbloqueadas + %1$d / %2$d + Desbloqueadas · %1$d + Bloqueadas · %1$d + Ocultas · %1$d + Conquista oculta + Os detalhes são mostrados quando for desbloqueada. + Todas as conquistas desbloqueadas + A carregar… + Inicia sessão no %1$s para ver as tuas conquistas. + Não foi possível aceder ao %1$s. Verifica novamente quando estiveres online. + O %1$s não lista conquistas para este jogo. + Mostrar todas as %1$d + Conquistas · %1$s + ✓ No Steam + Desinstalar + A desinstalar… + Desinstalar %1$s? + Liberta %1$s e remove-o do Steam. + Remove os ficheiros e o atalho do Steam. + O Steam deteta o jogo no próximo arranque; inicie-o a partir daí ou inicie o Steam agora. + Proton + Escolha do Steam por jogo + Áudio + DirectAudio + Componentes + FEX, DXVK, VKD3D + Motor + %1$s · nativo + %1$s · integrado + Como funciona + Obtido de + %1$s, com a sua conta + Instalado em + Armazenamento de jogos (Games/%1$s/%2$s) + Iniciado por + Steam e Proton, o mesmo caminho dos jogos Steam + Steam e Proton · início de sessão Epic em cada arranque, sobreposição EOS desativada + Steam e Proton · variáveis do Amazon Games Services definidas + Steam + adicionado ao Steam na instalação (interface Deck + Steam Input) + Sem transferências + Em pausa + Em fila + Em fila · #%1$d + Cancelada + Falhou + Falhou · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Pausar + Retomar + Cancelar + Tentar novamente + Limpar + Limpar concluídas + Manifesto + Transferência + Verificação + Instalação + Concluído + A verificar + A transferir + Eliminar transferência? + Falhou · %1$s mantidos + A verificar + A instalar + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d ficheiros + Gestor de transferências + Transferências em simultâneo + Instalar em + Instalar %1$s: + Interno + %1$s livres + Transferências da loja + Mostra o progresso das transferências de jogos e mantém as transferências ativas em segundo plano + A transferir + %1$d transferências + A preparar… + Transferências da loja concluídas + %1$s instalado + Falha na transferência de %1$s + Eliminar + Manter + Iniciar sessão em %1$s + Iniciar sessão com o seu fornecedor + Recarregar + Fechar + Tentar novamente + A concluir o início de sessão… + Não foi possível carregar a página de início de sessão: %1$s + Este fornecedor recusa o início de sessão a partir de uma página incorporada. Inicie sessão com a palavra-passe da loja. + A página de início de sessão respondeu com um erro (HTTP %1$d). + A ligação à página de início de sessão não é segura. + Não foi possível verificar o início de sessão. Tente novamente. + Falha no início de sessão. Tente novamente. + Fechar + Voltar + Editar + Adicionar todos os jogos desta pasta + Nada de novo nesta pasta + Nome + Destino + Iniciar em + Opções de arranque + Imagens + Outro exe… + Remover + Capa + Fundo + Logótipo + Ícone + Pasta do jogo + Steam + SteamGridDB + O seu ficheiro + Do exe + Automático + Escolher imagem… + Escolha uma imagem: %1$s + Repor automático + + %1$d jogo adicionado + %1$d jogos adicionados + %1$d jogos adicionados + + + %1$d já está em Jogos + %1$d já estão em Jogos + %1$d já estão em Jogos + + B · Voltar para %1$s + o jogo + as definições de jogo + Controladores gráficos + Este Proton + Todos os Proton + Para todos os Proton + Disponíveis · %1$d + Verificado %1$s + Procurar novos componentes e controladores + Obter + %1$s · DXVK, VKD3D-Proton e FEX abaixo são deste Proton + %1$s · testado + %1$s · não testado + %1$s · não suportado + Mantém o par recomendado atualizado + Automático · %1$s + Controladores do runtime e de ecrã em separado + Usar %1$s? + Remover %1$s? + Diagnóstico + Pronto para jogar + Ainda não está pronto para jogar + runtime %1$s + limite de processos desativado + sessão iniciada como %1$s + Quando algo precisa de si + Aspeto + Este dispositivo + Imagens da biblioteca + APPS LINUX + Flathub + Apps para o ambiente de trabalho Linux, abertas a partir de Ambiente de trabalho. + Configurar o Flathub + Remover %1$s? + As definições e os ficheiros da app são mantidos. + Manter + Tamanho no disco + Desinstalar o ambiente de trabalho? + Todos + Conquistas da GOG + Sincroniza conquistas dos jogos compatíveis da GOG. Aplica-se no próximo arranque. + Conquistas desativadas + Conquistas ativadas + Inicia sessão na GOG novamente + Conquistas enviadas + Envio da conquista pendente + Ativa as conquistas da GOG nas definições de Lojas + Conquistas indisponíveis; verifica os registos da sessão + Conquista desbloqueada + Colar + Direcional + Stick esquerdo + Stick direito + Menu + Teclado + Direcional para cima + Direcional para baixo + Direcional para a esquerda + Direcional para a direita + Controlos táteis + Controlador tátil + Rato + Ativado + Desativado + Lenta + Normal + Rápida + Editar esquema + Menu do DroidDeck + Arrasta os controlos para o ecrã + Organizar + Rejeitar + A guardar… + Ícone + L− + L+ + A− + A+ + Ocultar + Cor e opacidade… + Copiar este esquema + Esquema copiado; cola-o no esquema tátil de qualquer jogo + Colar esquema copiado + Restaurar o padrão da Steam + Este controlo não tem uma ação atribuída na Steam para receber um ícone + Ícone para %1$s + Cor do esquema tátil + Concluído + Não foi possível guardar o esquema tátil + Esquema tátil guardado na Steam + Aplicação %1$d + Etiqueta (opcional) + Os ícones da Steam não foram encontrados + + %1$d ícone da Steam + %1$d ícones da Steam + %1$d ícones da Steam + + Cor do ícone + Cor do botão + Sem ícone + + A verificar o início de sessão… + A obter a tua biblioteca… + A obter a lista de jogos… + A carregar os detalhes dos jogos… (%1$d/%2$d) + + A sincronizar %1$d jogo… + A sincronizar %1$d jogos… + A sincronizar %1$d jogos… + + A sincronizar… %1$d/%2$d + A obter as capas… + Falha na sincronização + Não foram encontrados jogos na tua biblioteca da %1$s + A Amazon Games não respondeu sobre a tua biblioteca: atualiza, ou inicia sessão novamente + Não foi possível contactar a GOG + Não foi possível ler a tua biblioteca da GOG + A registar na Steam… + Cardboard VR + Modo Cardboard + Imagens para o olho esquerdo e o direito, para uns óculos de RV para telemóvel. + Imagem + Ecrã virtual + 3D lado a lado + Óculos + Cardboard + Cardboard (2014) + Sem lente + Distância entre os olhos + Largura do ecrã + Distância do ecrã + Seguimento da cabeça + Este dispositivo não tem sensor de rotação. + Recentrar vista + Recentrar + SteamVR (experimental) + Instalar + Transfere o SteamVR ARM64 da Valve (571 MB). + A transferir e a descompactar o SteamVR… + Iniciar + Executa o SteamVR com a tua cabeça como headset; a imagem muda para 3D lado a lado. + Parar + O SteamVR está em execução. + Indisponível nesta sessão. + O SteamVR não respondeu; verifica o registo da sessão. + Transferir componentes + As transferências são partilhadas com a página Componentes. Escolhe acima uma versão transferida para a usar quando iniciares o próximo jogo. + Escolhe uma versão para transferir + Programadora + Editora + Lançamento + Principal + + Extras + Completista + %1$s h + %1$d min + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml new file mode 100644 index 000000000..ae5b83b25 --- /dev/null +++ b/app/src/main/res/values-pt/strings.xml @@ -0,0 +1,2260 @@ + + + DroidDeck + + Na primeira execução, a Steam é baixada. Isso pode levar alguns minutos. + Por padrão, sair do app pausa a Steam; ela continua de onde parou quando você voltar. + Seu controle aparece como um controle de Xbox 360. Sem controle, use os controles na tela. + + + Na primeira execução, a área de trabalho é preparada. Isso pode levar alguns minutos. + Por padrão, sair do app pausa a sessão; ela continua de onde parou quando você voltar. + O toque funciona como um touchpad: arraste para mover o ponteiro, toque para clicar. + Seu controle aparece como um controle de Xbox 360. Sem controle, use os controles na tela. + + Sessão + Aparece enquanto uma sessão da Steam ou da área de trabalho está aberta + A Steam está em execução + Download da Steam em andamento + Sessão pausada - toque em Retomar quando quiser + Retomar + Suspender agora + Encerrar sessão + Geração de quadros + Selecione o modo de geração de quadros + Desativada + Win-FG %1$d× + LSFG %1$d× + LSFG Adaptativo %1$d + Requer o Lossless Scaling na Steam + Instale o Lossless Scaling na Steam ou importe o Lossless.dll + Importar Lossless.dll… + Usando a cópia da Steam + Usando uma cópia importada + Escolha o Lossless.dll + Lossless.dll importado + Este Lossless.dll já está em uso + Esta conta Steam não tem o Lossless Scaling + Este arquivo não é o Lossless.dll do Lossless Scaling + Não foi possível preparar o LSFG com este Lossless.dll + O LSFG requer o Lossless Scaling da Steam ou um Lossless.dll importado + O LSFG não conseguiu preparar os shaders a partir do Lossless.dll + %1$s: sem suporte + %1$s: falha no mecanismo + %1$s: requer Lossless Scaling + %1$s %2$s · iniciando + Adiciona uma biblioteca Steam e procura jogos de Windows nela. Os jogos da sua conta usam as entradas da Steam; os demais viram atalhos não Steam. + Ambiente do jogo + Variáveis de ambiente para jogos com Proton. + Editar + Todos os jogos com Proton + Perfil + ID do app na Steam + Abrir + Jogo %1$s + Adicionar variável + Nome da variável + Valor + Salvar + Remover + Herdar novamente + Limpar substituições de ambiente + Não definir neste perfil + Nenhuma variável definida. + Herdada + Variável personalizada + Não foi possível carregar ou salvar. Verifique o espaço livre e tente novamente. + Use um nome válido e um valor de até 8192 caracteres. + O runtime padrão do Proton não lê esta variável. + Carregando… + Substitui o nível de recursos do D3D12. 12_2 informa os recursos do DirectX 12 Ultimate; recursos sem suporte podem falhar. Deixe sem definir para detecção automática. + Substitui o shader model do D3D12. 6_9 requer um VKD3D recente o bastante e recursos de GPU compatíveis. Deixe sem definir para detecção automática. + Mantenha o cache de shaders do Mesa ativado para reduzir compilações repetidas. false ativa o cache. + Tamanho máximo do cache de shaders do Mesa, como 1G. Caches maiores ocupam mais armazenamento. + Opções do VKD3D para D3D12. nodxr desativa o ray tracing; use só em um jogo que precise disso. + Defina como 1 para gerar um log de diagnóstico do Proton. O log pode reduzir o desempenho e ocupar armazenamento. + Defina como 1 para usar o WineD3D (baseado em OpenGL) em vez do DXVK ao testar a compatibilidade com D3D9–11. + Defina como 1 para manter este jogo fora do ntsync (droiddeck-ntsync ou um driver do kernel); ele usa o droiddeck-esync onde houver um pacote, senão o droiddeck-fsync. + Defina como 1 para desativar o droiddeck-esync neste jogo; ele passa a usar o droiddeck-fsync, ou o droiddeck-ntsync onde o fsync também estiver desativado. + Defina como 1 para manter este jogo fora do droiddeck-fsync; ele usa o droiddeck-esync onde houver um pacote, senão o droiddeck-ntsync. + Defina como 1 para rodar este jogo no droiddeck-fsync, ou como 0 para priorizar o droiddeck-esync onde houver um pacote compatível com o Proton dele. + Defina como 0 para desativar o assistente de navegação por controle do Proton neste jogo. + Overlay do DXVK para D3D9–11. fps mostra a taxa de quadros; sem definir, fica oculto. + Configurações do DXVK separadas por ponto e vírgula. Versões recentes usam dxvk.maxFrameRate para limitar os quadros; para jogar normalmente, prefira o limitador de quadros da sessão. + Limite de taxa de quadros do D3D12. 0 desativa o limite. + Threading do OpenGL no Mesa. true pode ajudar jogos OpenGL limitados pela CPU; teste jogo a jogo. + Sincronização do Wine + Como os jogos mantêm suas threads em sincronia. Vale a partir da próxima sessão. + esync + ntsync + fsync + wineserver + Predefinição do FEX + Vale a partir da próxima vez que um jogo for aberto. + Jogos + Padrões do FEX + Usa os padrões do FEX. + Estabilidade + Configurações mais seguras e mais lentas. + Compatibilidade + TSO completo com multiblock. + Intermediário + TSO equilibrado com precisão x87 reduzida. + Desempenho + Todo o TSO desativado; multiblock. + Desempenho + TSO + Configurações de Desempenho com o TSO principal ativado. + Extremo + Desativa as verificações de código automodificável. Jogos com JIT e DRM podem quebrar. + Extremo + TSO + Configurações do Extremo com o TSO principal ativado. + Extremo-gn + Desempenho com dicas de TSC e de metadados. Tente antes do Extremo. + Denuvo + Desempenho com verificações de código completas e o bit de hypervisor oculto. + Valor personalizado + Selecionar valor + Emuladores e apps + Adicionar + Seu próprio app ou jogo + Script + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Iniciando… + %1$s adicionado + %1$s removido + Área de trabalho + Esse app não existe mais. Escolha-o de novo na Área de trabalho. + Abrir %1$s + Remover + Pressione de novo para remover + Encerre a sessão para remover + Executa %1$s a partir da própria pasta, vinculada à sessão no local onde está. Remover o app não mexe nessa pasta. + Executa %1$s a partir de uma cópia da pasta dele dentro do DroidDeck. Remover o app apaga a cópia, não o original. + Instale primeiro o runtime do Linux + O arquivo não existe mais + Espaço livre insuficiente: copiar a pasta requer %1$s + Copiando %1$s + Falha ao adicionar o app + Procurando uma versão de %1$s + Não foi possível ler as versões de %1$s + Nenhuma versão de %1$s tem um AppImage ARM64 ou x86_64 + Baixando %1$s + Falha no download + Falha ao atualizar o app + Não foi possível ler esse ícone + %1$s salvo + Editar + Verificar atualizações + Verificando atualizações… + Atualizar para %1$s + Atualizado · %1$s + %1$s atualizado + %1$s · %2$s + Adicionar um app + Script + AppImage + GitHub + Flatpak + Um arquivo .sh. Ele roda a partir da própria pasta, em tela cheia no gamescope. + Um AppImage ARM64 (aarch64) ou um x86_64 que roda pelo FEX. Ele é descompactado no runtime do Linux uma única vez. + Instala o AppImage ARM64 da versão mais recente do repositório, ou o x86_64 dela pelo FEX. + Usar FEX + Automático + Ativado + Desativado + O FEX roda programas Linux x86 neste dispositivo ARM64. No Automático, ele é usado quando o programa é feito para x86. + %1$s pelo FEX + Este programa é feito para PCs %1$s e roda pelo FEX, que ainda não está configurado. Abra a Steam uma vez com a sessão iniciada: o DroidDeck faz com que ela baixe o FEX e o Steam Linux Runtime necessários, e então isto abre. + Este programa + %1$s é feito para PCs x86 e roda pelo FEX, que ainda não está instalado. Abra a Steam uma vez com a sessão iniciada e deixe-a baixar o FEX e o Steam Linux Runtime; depois, abra isto de novo. + Um ID de app do Flathub ou a página dele no Flathub. O Flatpak é configurado antes, se ainda não estiver. + Arquivo + Nenhum arquivo escolhido + Escolher arquivo + Alterar + Escolha um script + Escolha um AppImage + Escolha um ícone + Repositório + dono/repo ou um link do github.com + Não é um repositório do GitHub + ID do app + org.ppsspp.PPSSPP + Não é um ID de app do Flathub + Ou um arquivo .flatpak, para um app que não está no Flathub. + Escolha um arquivo .flatpak + Origem + Na sua Área de trabalho + Novo app + Nome + Do app + Ícone + Ícone padrão + Escolher ícone + Limpar + Do repositório + Procurando ícones… + Padrão + Editar %1$s + Salvar + Verificando a pasta… + A pasta dele é vinculada à sessão: %1$s + A pasta dele contém programas Linux, que o armazenamento compartilhado não consegue executar, então ela é copiada para o DroidDeck (%1$s). + Adicionar + Cancelar + Instale primeiro o runtime do Linux em Configuração. + Aguarde %1$s terminar. + Compatível + As versões marcadas como “%1$s” no menu Compatibilidade da Steam têm as correções de prefixo do DroidDeck e iniciam jogos aqui. O padrão é o Proton Experimental ARM64 (%1$s), ou o Proton 11.0 ARM64 (%1$s) quando essa versão mais nova estiver instalada. As entradas do Proton da própria Valve sem a marca não conseguem iniciar jogos neste dispositivo. + %1$s instalado · %2$s + Cancelar + Automático + Nunca + Sempre + Desinstalar + Verificar + Atualizar + Verificando… + Processando… + Esquecer + Adicionar… + Usar + Vale a partir da próxima sessão. + Sessão da Steam + Sessão da área de trabalho + Tela + Resolução + Até 720p + Até 900p + Até 1080p + Nativa + %1$s (padrão) + Personalizada · %1$d×%2$d + Personalizada… + Em 720p, os menus podem responder mais rápido. + Proporção da tela + Definida pela resolução personalizada. + O Automático usa no mínimo 16:9. + Limite de quadros + Limita a sessão inteira. A tela muda para uma taxa de atualização que seja múltipla do limite. + HDR + Saída HDR10 + Indisponível: %1$s. + Reinicie o app para aplicar. + Drivers + Drivers de GPU + Compartilhados pela Steam e pela Área de trabalho. Defina na página Componentes. + Toque e controles + Toque + Touchpad + Direto + Automático: touchpad na Área de trabalho, direto na Steam. + Controles na tela + Steam + QAM + No Automático, os controles na tela aparecem quando não há um controle conectado. + Toque da Steam (beta) + Os controles de toque da Steam começam na próxima sessão da Steam + Layout de toque da Steam + Mova, redimensione ou oculte os controles de toque deste jogo; salvo na Steam + Controle + Como a Steam vê seu controle. Vale a partir da próxima sessão. + Controle do Steam Deck + Steam Controller + DualShock 4 + DualSense + Controle de Xbox 360 + Deck: botão de Acesso rápido e giroscópio. Steam Controller: o mesmo, com detecção de pegada nos botões traseiros. DualShock 4 e DualSense: giroscópio, e os trackpads como o touchpad deles. Xbox 360: abra o Acesso rápido com Guide+A. + Voltar + Menu → QAM + QAM → Menu + Sessão + Comportamento em segundo plano + Quando o app fica oculto ou a tela se apaga. + Manual + Nativo + Automático pausa e retoma quando o app reaparece. Nativo prepara a Steam para a suspensão antes. Manual aguarda Retomar. Nunca continua em execução. + Manter downloads ativos + Adia a suspensão em segundo plano enquanto a Steam estiver baixando. + Automático pausa em segundo plano e retoma quando o app reaparece. Manual aguarda Retomar. Nunca continua em execução. + Inicialização + Iniciar a Steam com o DroidDeck + Abre a sessão da Steam quando o DroidDeck é iniciado. + Decky + Procurando versões… + Pronto para instalar %1$s. + Nenhuma versão compatível encontrada. Tente novamente mais tarde. + Instalado · %1$s + Atualização disponível · %1$s + Atualizado · %1$s + Loader + Instalar a mais recente + Inicia com a Steam. Enquanto estiver ativado, outros apps neste dispositivo podem conseguir controlar a Steam. + Desativado. A porta de depuração remota da Steam fica fechada. + Desinstalar o Decky Loader? + Remove o loader. Seus plugins e configurações são mantidos. + Cliente + Overlay de desempenho + Desative se os jogos travarem ou ficarem com a tela preta no modo Deck. Vale a partir da próxima sessão. + Reparar o cliente Steam + Baixa o cliente Steam de novo na próxima vez que a Steam iniciar. Use se a Steam ficar presa na tela de carregamento. Jogos, login e configurações são mantidos. + O cliente Steam será baixado de novo na próxima vez que a Steam iniciar. + Reparar + Artes + Chave de API do SteamGridDB + Sua chave + Editar… + Preenche as artes que faltam com a Steam e, depois, com o SteamGridDB. Seus próprios cover.jpg, hero.jpg, logo.png ou icon.png na pasta do jogo têm prioridade. + Remover %1$s + Remover? + Adicionar jogo + Adicionar um jogo: + Escolha o exe: + Este local não está disponível na sessão + Verificar este exe + Componentes do Windows + LB / RB alternam entre as seções. + Nenhum + DLLs do Windows que o Proton não inclui, da lista de componentes. Ative as de que este jogo precisa: elas vão para a pasta do Windows do próprio jogo, nunca para os arquivos dele, e continuam lá em qualquer Proton que ele usar. + Não foi possível acessar a lista de componentes. Os componentes já baixados ainda podem ser ativados ou desativados. + Recomendados para este jogo + Verificando do que este jogo precisa… + Disponível quando on-line + + Ativando %1$d componente… + Ativando %1$d componentes… + Ativando %1$d componentes… + + Ativados para este jogo + A Steam instala %1$s com %2$s (app %3$s) + Da lista do GOG para este jogo + Dos pré-requisitos da Epic para este jogo + Das etapas de instalação da Amazon para este jogo + A Steam instala %1$s com o app %2$s, identificado pelo nome + Também listados + Listado por %1$s; não é um componente aqui + Steam + GOG + Epic Games + Amazon Games + O jogo vem com %1$s + O jogo tem %1$s (costuma funcionar assim mesmo) + Já está neste jogo + Nada na pasta do jogo indica um componente. + Instalados para este jogo (%1$d) + Todos os componentes (%1$d) + Requerem um instalador do Windows (%1$d) + Estes são programas de instalação próprios, sem um pacote dentro para desmontar, e ainda sem uma gravação do que instalam. A maioria são runtimes antigos que o Mono do Proton já substitui; as gravações são adicionadas à medida que são feitas. + Mostrar + Ocultar + Executa o próprio programa de instalação (ainda sem gravação) + Ainda sem suporte aqui + Carregando os componentes… + Não foi possível instalar: %1$s + Baixado + Baixando %1$s + Instalando: %1$s + Desativando… + Download de %1$s + Entra em vigor na próxima vez que o jogo iniciar. + Esticar jogos 16:9 + Em telas mais quadradas que 16:9, estica os jogos para preencher a tela em vez de mostrar barras. Vale a partir da próxima sessão. + Áudio + DirectAudio para jogos + Ignora o PulseAudio para reduzir a latência. + Microfone + Para chat de voz. + Rede + Descoberta de Wi-Fi + Mostra os nomes de Wi-Fi e as redes próximas na Steam. + Os serviços de localização estão desativados. + Permissão de localização não concedida. + Abrir configurações + Continuar + Ativar a descoberta de Wi-Fi? + O Android exige a permissão de localização para ler nomes de Wi-Fi e descobrir redes próximas. O DroidDeck usa esse acesso para mostrar essas redes na Steam. Não rastreamos sua localização. O acesso à internet funciona sem essa permissão. + Armazenamento de jogos + Automático - o cartão SD, se houver, mais suas pastas Games + Automático - suas pastas Games, mais o cartão SD, se houver (nenhum agora) + Somente interno + Pasta: %1$s + Segunda biblioteca + Jogos que carregam recursos do SD ou do armazenamento compartilhado durante o jogo podem ter engasgos. + Diagnóstico de armazenamento + Registra os resultados de alocação e amostras de tempos de armazenamento em Compartilhar logs. Pode deixar as instalações mais lentas enquanto estiver ativado. Vale a partir da próxima sessão. + Escolher uma pasta… + Resolução personalizada + Substitui o limite e a proporção. Um tamanho que não corresponda à tela fica com barras. + Largura + Altura + Entre 320×240 e 3840×2160. + + %1$d jogo + %1$d jogos + %1$d jogos + + OK + Excluir + Restaurar + Baixar + Excluir %1$s + %1$.1f MB + Driver do runtime + Usado pela Steam, pelos jogos e pela área de trabalho. Vale a partir da próxima sessão. + Importar zip do Turnip… + Driver de exibição + Desenha os quadros na tela nos dois modos. Reinicie o app para aplicar. + Importar um zip do AdrenoTools… + nunca + Nightlies verificadas: %1$s + Componentes + Sobre Componentes + Procurar novos drivers + Procurar novos pacotes nas Nightlies + Proton + O Proton escolhido aqui roda todos os jogos que não escolheram o próprio na Steam, e uma mudança feita na Steam também aparece aqui. A Steam substitui o Proton da Valve quando atualiza; os originais são mantidos para cada versão. + Configuração + Configurar perfil + Perfil + Perfil padrão + Usado automaticamente por jogos sem substituições. + Configurações salvas, usadas automaticamente quando %1$s é iniciado. + Escolha só as configurações de que este jogo precisa. Todo o resto continua usando o Perfil padrão. + Predefinição do FEX + Usando a predefinição do Perfil padrão. + Predefinição personalizada salva para este jogo. + Variáveis personalizadas do FEX + Usar padrão + Compatibilidade do jogo + Opções de ambiente do Proton, DXVK, VKD3D e de sincronização para este jogo. + Usando o Proton do Perfil padrão. + Proton personalizado salvo para este jogo pelo mapeamento de compatibilidade da Steam. + Sem Proton + DXVK + VKD3D-Proton + Usa o pacote do Perfil padrão de %1$s, a menos que este jogo selecione outro pacote. + Usa o pacote de componentes do Perfil padrão. + Usa %1$s só neste jogo. + Opções de runtime + Outras opções de ambiente por jogo do Proton, DXVK, VKD3D-Proton e de sincronização. + O perfil de %1$s foi salvo para a próxima vez que o jogo for iniciado. + Iniciar sem o Steam + Para um jogo que fecha na hora: inicia ele sem a configuração do Steam no Proton. Recursos do Steam, como a sobreposição, ficam desativados para ele. + Ativado + A escolha de Proton de %1$s foi salva e já está disponível na Steam. + A escolha de Proton de %1$s foi salva. Reinicie a Steam antes de iniciar o jogo. + %1$s não pode usar o Proton selecionado neste dispositivo. + Não foi possível carregar o perfil de %1$s: %2$s + Não foi possível salvar o perfil do jogo: %1$s + Redefinir perfil do jogo + Redefinir o perfil de %1$s? + Remover as substituições de Proton, FEX, pacote de componentes, ambiente, filtragem de texturas e componentes do Windows deste jogo? Ele passará a usar o Perfil padrão. + %1$s agora usa o Perfil padrão. + Não foi possível redefinir completamente o perfil do jogo: %1$s + %1$s agora roda todos os jogos que não escolheram o próprio Proton na Steam. + %1$s vai rodar todos os jogos que não escolheram o próprio Proton a partir da próxima vez que a Steam iniciar. + %1$s não consegue iniciar jogos aqui, então não pode ser o padrão. + Não foi possível salvar o Proton padrão: %1$s + %1$s · %2$d trocados + %1$s · todos originais + %1$s · jogo em execução + %1$s · pacote droiddeck-esync + %1$s · ainda sem pacote droiddeck-esync + %1$s · esync integrado, sem necessidade de pacote + Drivers de GPU + %1$s · reaplicado ao iniciar %2$s + Próximo: %1$s · depois que o jogo fechar + %1$s em uso: %2$s + Aguarda o jogo fechar + Vale a partir da próxima vez que um jogo iniciar + Importar .zip + Lendo as versões do Proton… + Nenhum Proton instalado ainda. Inicie a Steam uma vez para baixar o Proton ARM64 dela, ou adicione o GE-Proton, CachyOS ou DroidDeck-Proton em Configuração. + Original · %1$s + %1$s · esta versão + %1$s · versão anterior + Instalados · %1$d + Nada armazenado para %1$s ainda. Os arquivos deste Proton são salvos como o original dele. + Há um jogo rodando neste Proton, então isto aguarda até ele fechar. + Vale a partir da próxima vez que um jogo iniciar. + Trocar %1$s? + Colocar %1$s em %2$s? Os arquivos originais continuam salvos. %3$s + Restaurar %1$s? + Devolver o %1$s original para %2$s? %3$s + Remove o pacote do app. Você pode baixá-lo de novo. + Excluir este original? + Este %1$s original é de uma versão anterior de %2$s. O original da versão atual é sempre mantido. + Nas Nightlies · %1$d + Atualize para listar os pacotes nas Nightlies. + Nada novo desde a última verificação. + Importar .wcp + Restaurar o original deste Proton + Devolver o %2$s original de %1$s? + Trocar + FEX, DXVK e VKD3D-Proton para cada Proton com que a Steam roda jogos.\n\nSelecione uma linha instalada para colocá-la em uso. A troca vale a partir da próxima vez que um jogo iniciar, ou depois que o jogo em execução fechar.\n\nSuas escolhas são verificadas de novo antes de cada inicialização e restauradas se algo as tiver alterado. Os arquivos originais de cada Proton são mantidos, então as atualizações da Steam nunca os perdem. + Original + Em uso + Novo + Armazenado + Desempenho + Núcleos da CPU, correções da sessão e o nome do host. Vale a partir da próxima sessão. + Núcleos do cliente Steam + Substituir a escolha de núcleos da Steam + Fixa a Steam, o auxiliar de interface dela e o gamescope nos núcleos abaixo. + Núcleos do cliente + Núcleos para a Steam. + Ative a substituição para escolher. + Núcleos dos jogos + Núcleos dos jogos + Núcleos para os jogos. Com todos selecionados, o Android distribui livremente. + Interface do cliente + GL em threads + Pode deixar os menus da Steam mais responsivos. + Zink: descritores sob demanda + Recomendado para drivers sem buffers de descritores. + Pular verificações de erro do GL + Desativa a validação do GL a cada chamada. + gamescope: fila de GPU em tempo real + Prioriza o trabalho de GPU do compositor. Pode suavizar o ritmo dos quadros, com algum custo para os jogos. + GPU + Manter a GPU no clock máximo + Só em Adreno. Menos engasgos, mais gasto de bateria e mais calor. + Correções da sessão + Turnip: renderização sysmem + Mais rápido para a Steam e a maioria dos jogos. Obrigatório nas Adreno 710/720/722. + Pular o assistente xalia da Steam + droiddeck-ntsync (experimental) + Um substituto em espaço de usuário para o driver ntsync do Linux, para testes. Enquanto estiver ativado, o Proton o usa no lugar do droiddeck-esync e do droiddeck-fsync. Funciona com o Proton Experimental ARM64, o GE-Proton e o proton-cachyos. Vale a partir da próxima sessão. + droiddeck-esync + droiddeck-fsync primeiro + Roda o fsync do próprio Proton nos arquivos originais do Proton, com o futex_waitv fornecido em espaço de usuário, então não precisa de pacote e continua funcionando assim que a Steam atualiza o Proton. Enquanto estiver ativado, os jogos o usam antes do droiddeck-esync, exceto os poucos que o Proton mantém fora do fsync. Vale a partir da próxima sessão. + As threads do Windows dos jogos esperam umas pelas outras via eventfd e memória compartilhada, em vez de idas e voltas pelo wineserver. Usa um pacote droiddeck-esync feito para aquela versão exata do Proton; os pacotes são obtidos automaticamente. Até existir um para uma nova versão do Proton, o droiddeck-fsync faz o papel dele. Vale a partir da próxima sessão. + Desativa o assistente de navegação por controle do Proton, que consome CPU em todos os jogos. + Rodar o proot sem seccomp + Pode corrigir erros de syscall ausente em alguns kernels. Pode reduzir o desempenho. + Caminho rápido do proot + Requer o filtro seccomp do proot. + Buscas de arquivos mais rápidas e menos engasgos. Desative se um programa não encontrar os arquivos dele. + Identidade da sessão + Nome do host + O nome que a Steam vê para este dispositivo. Em branco, volta a ser %1$s. + Só letras, dígitos e hifens internos, até 63 caracteres. + O Android está configurado para encerrar esta sessão + Instalados + Disponíveis para download + Nada novo desde a última verificação. + Restaurar drivers integrados + Integrado ao app: isto remove os arquivos descompactados dele e o oculta. Restaurar drivers integrados o traz de volta. + Metade de um pacote Android + Linux: as duas metades são removidas. Um driver em uso volta para o padrão. + Os arquivos dele são removidos do app. + Os arquivos dele são removidos. Ele está em uso, então o padrão assume. + Tela + Efeitos + Controles + Componentes + Controle + Configurações + Página %1$s + Steam + Área de trabalho + Menu Steam + Acesso rápido + Abrir o menu de Acesso rápido + Página anterior + Próxima página + HUD de desempenho + Modo de escala + Nitidez da escala + Efeitos de tela + Em tempo real. Custa um pouco de tempo de GPU enquanto algum efeito estiver ativado. + Predefinições + Personalizado + Avançado + Expandido + Recolhido + Nitidez (CAS) + Nível de nitidez + HDR falso + Redução de banding + Intensidade da redução de banding + Brilho + Contraste + Gama + Saturação + FXAA (suavização de bordas) + Contornos cartoon + CRT + NTSC + Filtragem de texturas + Jogos DirectX 9–11, a partir da próxima vez que forem iniciados. + Filtragem anisotrópica + Nitidez das texturas + Um mip bias negativo: texturas mais nítidas, com um pouco de cintilação. O Automático acompanha o quanto a sessão é ampliada para a tela. + Automático (%1$s) + Teclado + Teclado de PC + Android + Segunda tela + Modo + Mostra + Apps Android + Abrir um app + Ocultar + Mostrar + Nenhum app que possa ser aberto + Comportamento da sessão + Vale agora e nas próximas sessões neste modo. + Próxima sessão + Suporte + Logs da sessão + Anexe a um relatório de bug + Compartilhar logs + Segundo plano + Encerrar + Encerrar a sessão? + Em execução agora + Nenhum jogo está rodando em um Proton + Perfil do jogo + Os detalhes do perfil ainda não estão disponíveis. + Não foi possível carregar os perfis de jogos. Tentando de novo automaticamente. + Não foi possível atualizar os perfis de jogos. Mostrando os últimos dados disponíveis; tentando de novo automaticamente. + Veja e edite as configurações salvas usadas na próxima inicialização. + Usando o Perfil padrão + Perfil personalizado + Usando %1$s do Perfil padrão. + As alterações são salvas no perfil de %1$s e valem a partir da próxima vez que o jogo for iniciado. + + %1$d substituição do jogo + %1$d substituições do jogo + %1$d substituições do jogo + + Nenhum Proton instalado ainda. + jogo em execução + reaplicado ao iniciar %1$s + Original + Importe e exclua na página Componentes. + Concluído + Requer atenção + Processando + Verifique o endereço IP e a porta da Depuração por Wi-Fi, ou pareie de novo se o Android removeu este dispositivo. + Instalar + Gerenciar + Visão geral + Controle + Sessão + Inicializador + Configuração + ● Tudo pronto + %1$d de %2$d prontos + Verificação do sistema + Dispositivo compatível + GPU não testada + GPU sem suporte + %1$s · %2$s + %1$s: %2$s + A Steam requer um driver Adreno; %1$s pode mostrar uma tela preta + Runtime do Linux + %1$s · %2$d%% + Não instalado · cerca de 3 GB, instalado no primeiro Jogar + Instalado + %1$s · atualização disponível + %1$s · atualizado + Limite de processos filhos + Ativado · o Android pode fechar a Steam. Desativá-lo leva um minuto. + Não definido · o Android pode fechar a Steam. Desativá-lo leva um minuto. + Não foi possível verificar · desativá-lo leva um minuto. + Corrigir + Detalhes + Opções do desenvolvedor + Usar a Depuração por Wi-Fi + Verificar de novo + Ativar o limite + Conta Steam + Sessão iniciada como %1$s · modo offline + Sessão iniciada como %1$s + Pressione Jogar e inicie sessão na Steam + Ferramentas + Controle + As configurações de controle não estão disponíveis. + Sessão + Logs da sessão + Salvos após cada sessão + Ferramentas de depuração + Logs da última sessão + Logs de sessão salvos + Mantém os %1$d mais recentes + Limpar logs + Modo offline + Inicie sessão na Steam primeiro + Inicializador + Idioma + A Steam também o usa, a partir da próxima vez que iniciar. + Padrão do sistema (%1$s) + Padrão do sistema + Escala do app + Texto e controles do DroidDeck + %1$d%% (padrão) + Tema + Aparência do inicializador + Usar como tela inicial + O DroidDeck pode ser o app de início + O DroidDeck nunca é oferecido como app de início + Orientação + Paisagem automática + Paisagem + Paisagem invertida + Tela cheia + Oculta as barras de status e de navegação do Android + Mostra as barras de status e de navegação do Android + App de início padrão + Escolher um app de início + Escolher + Apps Linux (beta) + Arquivos + Navegue e gerencie arquivos + Versões do Proton + Instale versões ARM64 do Proton + Desempenho + Atribuição de núcleos da CPU + Pasta de ROMs + Onde os jogos de emuladores ficam armazenados + Componentes + FEX, DXVK, VKD3D + Controles + Mapeamento de botões + Descobrir + Buscar + Instalados + %1$s · %2$d + Loja + Flathub · ARM64 + ● Flatpak pronto + Instale primeiro o runtime do Linux em Configuração. + Configurar o Flatpak + Adiciona o Flatpak e o conecta ao Flathub (cerca de 20 MB). O primeiro app também baixa o runtime dele. + Configurando… + Flatpak + Atualizações + %1$s · %2$s + %1$s · %2$s · %3$d%% + Iniciando… + Categorias + Não foi possível acessar o Flathub. Verifique sua conexão e tente novamente. + Tentar novamente + Buscar no Flathub + Firefox, jogos, emuladores… + Buscar + Todos + Buscando… + Busque pelo nome ou escolha uma categoria. Só apps ARM64 são listados. + Nenhum app ARM64 no Flathub corresponde a isso. + Resultados + Verificar atualizações + Atualizar tudo (%1$d) + ● Tudo está atualizado + Nenhum app ainda. Encontre alguns em Descobrir ou Buscar. + Configure o Flatpak para instalar apps. + Atualização disponível · %1$s + Abrir + Detalhes + Remover + Pressione de novo para remover + Instalando… + Instalar + Configure o Flatpak primeiro + Download de %1$s + ● Instalado + Não foi possível carregar a página deste app no Flathub. + Carregando do Flathub… + Não há versão ARM64 no Flathub, então não é possível instalar aqui. + Capturas de tela + Sobre + ID do app + Licença + Runtime + Tamanho instalado + %1$s (mais o runtime compartilhado) + Site + Verificado + Atualizações + DroidDeck %1$s · %2$s + Verificado %1$s + Ainda não verificado + Permita que o DroidDeck se atualize + Na próxima tela, ative Permitir desta fonte e volte. A atualização continua sozinha. + Abrir configurações + Agora não + Assinatura diferente + Não é possível atualizar por cima + Esta cópia não está assinada com a chave de lançamento do DroidDeck, então as atualizações não podem ser instaladas por cima dela. Desinstale-a para trocar. + Atualização disponível + Não é possível instalar + Pronto para trocar + À frente da Estável + Você está à frente da Estável + Esta versão é mais nova que a Estável mais recente. Você passará para a Estável no próximo lançamento dela. + Teste encerrado + Este teste foi encerrado + A correção dele foi incorporada ou descartada. Siga a Prévia para ter as correções mais recentes. + Atualizado + Você tem a versão mais recente: %1$s + · %1$s + %1$s… %2$d%% + %1$s… + Atualizar%1$s + Instalar%1$s + Instalar a Estável %1$s mesmo assim + Seguir a Prévia + Encerre a sessão em execução para atualizar. + Menos + Mais + DroidDeck %1$s + Nova versão Prévia + Versão de teste do PR #%1$d + %1$d MB + CANAL DE ATUALIZAÇÃO + Estável + Versões testadas, para a maioria das pessoas + %1$s · %2$s + Prévia + As correções mais recentes, antes da Estável + Atualizado + Versões de teste + Experimente uma correção antes do lançamento + Nenhuma no momento + PR #%1$d + Não compilada para esta cópia do DroidDeck + Atualizado %1$s + teste do PR #%1$d + há algum tempo + agora mesmo + + %1$d para testar + %1$d para testar + %1$d para testar + + + Mudanças desde a sua versão · %1$d versão atrás + Mudanças desde a sua versão · %1$d versões atrás + Mudanças desde a sua versão · %1$d versões atrás + + + Mudanças recentes · %1$d versão + Mudanças recentes · %1$d versões + Mudanças recentes · %1$d versões + + Uma configuração do Android para mudar + Steam + O Android interrompe apps que iniciam muitos processos em segundo plano, e a Steam inicia dezenas. Até isso mudar, os jogos fecham sem mostrar erro. + Corrigir para mim + Ative “Desativar restrições de processos filhos” e volte. Deixe as Opções do desenvolvedor ativadas, ou a chave volta a ser desativada. + Abrir as Opções do desenvolvedor + Esta versão do Android não tem uma chave para isso, então o DroidDeck faz a mudança pela Depuração por Wi-Fi. Leva cerca de um minuto, sem precisar de computador. + A Depuração por Wi-Fi requer Wi-Fi. Qualquer rede serve; não precisa de internet. + O código de pareamento é digitado em uma notificação. Permita notificações do DroidDeck ou use Outras formas. + Permitir notificações + Abra Configurações → Opções do desenvolvedor → Depuração por Wi-Fi e ative + Toque em “Parear o dispositivo com um código de pareamento” + Digite o código na notificação do DroidDeck + Aguardando o pop-up de pareamento… + Pop-up encontrado. Digite o código dele na notificação. + Status + Verificando a cada 2 segundos. Esta página fecha quando o limite for desativado. + Outras formas + Depuração por Wi-Fi, endereço manual, ADB pelo computador + Endereço manual, ADB pelo computador, Opções do desenvolvedor + Usar a Depuração por Wi-Fi + Algumas ROMs adicionam a chave mesmo assim. Procure “processos filhos” nas Opções do desenvolvedor. + Se a notificação não funcionar, digite você mesmo o endereço e o código do pop-up. + Digitar endereço manualmente + De um computador com ADB: + Copiar comando + Escolha em qual tela abrir as Configurações do Android. + Tela principal + Pareado. Procurando a porta da Depuração por Wi-Fi… + Pareado. Digite a porta mostrada nas Configurações. + Dispositivo encontrado. Aplicando a configuração… + Não foi possível aplicar automaticamente. Verifique o endereço abaixo e tente de novo. %1$s + Depuração por Wi-Fi + Limite de processos filhos atualizado + Alterar o limite de processos filhos + Se a opção não existir, pareie pela Depuração por Wi-Fi. + Pareado. Verifique o endereço IP e a porta na Depuração por Wi-Fi. + O limite de processos filhos foi atualizado. + Use a etapa 2 se as Opções do desenvolvedor não tiverem a configuração de processos filhos. Abra Opções do desenvolvedor → Depuração por Wi-Fi → Parear o dispositivo com um código de pareamento. + Pareado. Use o endereço abaixo ou atualize-o em Depuração por Wi-Fi → Endereço IP e porta. + O limite de processos filhos foi atualizado pela Depuração por Wi-Fi. + O Android confirmou que o limite está ativado. + O Android confirmou que o limite está desativado. + 2 · Parear pela Depuração por Wi-Fi + Endereço IP e porta do pop-up de pareamento + Código de pareamento (PIN) + 6 dígitos + Parear + 2 · Aplicar pela Depuração por Wi-Fi + Endereço IP e porta de conexão + Parear de novo + Desativar o limite + Outras formas de alterar + 1 · Opções do desenvolvedor (tente primeiro) + Ative “Restringir processos filhos”. + Desative “Restringir processos filhos”. + 3 · ADB pelo computador (último recurso) + Só se a Depuração por Wi-Fi não estiver disponível. + 3 · ADB pelo computador (último recurso) + Quando os controles de toque aparecem em uma sessão + Cor + Tom dos botões na tela + Personalizada + Opacidade + %1$d%% + Tamanho dos botões + 100% é o tamanho padrão + Clique do analógico + Toque duas vezes e segure um analógico para L3 ou R3 + Analógicos adaptáveis + Vibração do jogo + Botão Steam + Botão QAM + Botão do teclado + Pressione Select para abrir o menu Steam + Em vez de View/Select. + Animações + Os analógicos aparecem onde você toca e somem ao soltar + Layout + Posições personalizadas salvas + Ajustado a esta tela + Editar + Redefinir + Mapeamento de botões + Cada botão envia o próprio comando + %1$d de %2$d botões remapeados + Configurar + Redefinir controle + Restaura todas as configurações do controle e o layout + O que cada botão na tela envia ao jogo. Oculto remove o botão. + Botões na tela + Padrões + Redefinir mapeamento + Apps Android + %1$d apps + Nenhum app Android que possa ser aberto foi encontrado. + ● Área de trabalho instalada + Instalada ao abrir pela primeira vez + Disponível para instalar + Não instalado. + %1$s · não instalado + Abrir %1$s + Jogos + Escolha uma pasta de ROMs. + Navegue até /root/ROMs no RetroArch. + Adicione jogos de %1$s em ROMs/%2$s. + Instalar %1$s + Encerre a sessão para instalar + Runtime necessário + Carregando detalhes da instalação… + Os detalhes da instalação não estão disponíveis. Tente novamente mais tarde. + Instale primeiro o runtime do Linux em Configuração. + Encerre a sessão em execução para instalar apps da área de trabalho. + Instala %1$s na área de trabalho Linux. + %1$s · %2$d%% + Ocultar tira o item da Área de trabalho. Os arquivos dele continuam no runtime do Linux. + Esse jogo não está mais na pasta de ROMs. + Área de trabalho · %1$s + Abrir em %1$s + Também em %1$s + Essa página não existe mais. Escolha uma seção à esquerda. + Abrir área de trabalho + Instalar e abrir área de trabalho + Desinstalar área de trabalho + Remove a área de trabalho e os apps incluídos nela. Mantém jogos, apps adicionados, configurações e bibliotecas compartilhadas. + Removendo a área de trabalho + Área de trabalho removida + Removendo arquivos antigos do LXQt + Atualize o DroidDeck antes de remover esta versão da área de trabalho. + Área de trabalho Linux + KDE Plasma, Firefox e seus emuladores + Os jogos que você instala na Steam aparecem aqui após a sessão. + Jogar na Steam + Configurações de inicialização + Iniciar + Arquivos do jogo + Prefixo do Proton + Procurando jogos salvos… + Nenhuma pasta de jogos salvos encontrada pelo nome. A exportação inclui toda a pasta do usuário. + %1$d pastas de jogos salvos + %1$s · %2$d arquivos · %3$s + %1$d KB + Gerenciar jogos salvos + Jogos salvos + Importar jogos salvos… + Exportar como zip do GameHub… + Exportar como zip do Winlator… + Abrir pasta de jogos salvos + Nunca jogado + Runtime ocupado · %1$d%% + Runtime ocupado + O runtime é instalado no primeiro Jogar + Atualização do runtime disponível + ● Runtime pronto + Jogado por último: %1$s + Armazenamento interno + Jogo adicionado + Jogo personalizado + Armazenamento de jogos + O melhor par para esta GPU, sempre atualizado + Escolha um par você mesmo + Procurando drivers… + Nenhum driver listado ainda. Atualize para procurá-los. + Ocultar drivers de outras GPUs + Mostrar drivers de outras GPUs + Avançado + Definido junto com o driver de exibição: %1$s contém os dois. + Definido junto com o driver do runtime: %1$s contém os dois. + Usado pelos jogos e pela área de trabalho. + Padrão do runtime + Desenha os quadros na tela. Vale depois que o DroidDeck reiniciar. + Automático - escolhido pela GPU + Baixar, importar ou excluir… + EM USO + Os drivers integrados ao DroidDeck + Drivers do runtime e de exibição definidos juntos como um par compatível. + Recomendado para esta GPU: %1$s. + Atualize para procurar drivers para esta GPU. + RECOMENDADO + Indisponível + Runtime do Linux + Preparando o runtime do Linux + Sessão + Iniciando a tela e o áudio + Cliente Steam + Verificando a Steam e as ferramentas de compatibilidade dela + Abrindo a Steam + Aguardando o primeiro quadro da Steam + Área de trabalho + Preparando a área de trabalho + Abrindo + Aguardando o primeiro quadro + Iniciando o Steam + A sessão terminou + Etapa %1$d de %2$d + O Steam está pausado + Botões traseiros + Lado esquerdo + Lado direito + Ambos + Controle + Analógico + Trackpad + Alternar entre analógico e trackpad + Mais controles do Deck + PAINÉIS + Fechar controles da segunda tela + Abrir o menu Steam + Esquerdo · superior + Esquerdo · inferior + Direito · superior + Direito · inferior + Clique + Trackpad direito + Trackpad esquerdo + Trackpad + Mostrar teclado + Ocultar teclado + Terminal Linux + A sessão Linux ainda não está pronta para um terminal. + Steam + Jogos + Desktop + Loja + Componentes + Apps + Configuração + Atualizações + %1$d%% + Retomar %1$s + Aparece como /root/ROMs em cada sessão. O armazenamento interno fica em /root/Storage. + Nenhuma pasta escolhida + Vale a partir da próxima sessão. Cartões SD funcionam. + Escolher pasta + Trocar pasta + Instale uma versão ARM64 do Proton e escolha-a para cada jogo em Steam → Propriedades → Compatibilidade. + Pare a sessão em execução para instalar ou remover uma ferramenta de compatibilidade. + Versões disponíveis + Instalado: %1$s + Pendente de uma solicitação anterior + Não instalado + Atualização disponível → %1$s + Remover versões antigas do %1$s + Remove %1$s. Jogos configurados com essas versões precisam passar para outro Proton. + Jogos configurados com esta versão precisam passar para outro Proton. + Instalar agora + Cancelar pendente + Teclado do Android + Ocultar ✕ + Iniciar %1$s + Escolha uma tela. Secundária: %1$s. + A tela secundária não está mais disponível. + Tela principal + Tela secundária + Aba anterior + Próxima aba + Ativado + Todos (%1$d) + Nenhum + %1$d de %2$d + Desativado + Tela inferior + Vale a partir da próxima vez que o jogo for iniciado. O perfil de um jogo tem prioridade sobre o compartilhado. + Outro jogo pelo ID do app… + Concluído + Padrão + Desativado + Alterar… + Alterar variável + Voltar ao padrão + Jogos no Proton + Picture-in-picture + Entrar em PiP automaticamente + Controles com a tela desdobrada + Desdobrado, o jogo fica acima dos trackpads e dos botões traseiros do Steam Deck. Dobrado, o jogo fica em tela cheia. + Suspenso + O picture-in-picture fica disponível quando a sessão estiver pronta. + Permita o picture-in-picture para o DroidDeck nas configurações do Android. + O Android não conseguiu abrir o picture-in-picture. + Atalho + Adicionar à tela inicial + Escolher jogo + Verificando os jogos instalados… + Copiar link de inicialização + Link de inicialização copiado + Este launcher não pode adicionar atalhos. Use Copiar link de inicialização. + Link de inicialização de jogo inválido. + Este jogo não está mais na sua biblioteca do DroidDeck. + Pare a sessão atual antes de iniciar um jogo do Steam. + Não foi possível enviar o jogo ao Steam. Tente iniciá-lo novamente. + Tentar remover de novo + Removendo + Remoção incompleta + Exportar arquivo do jogo + Sincronizar todos os arquivos de jogo com a pasta + Parar de sincronizar arquivos de jogo + Escolha uma pasta para os arquivos de jogo do frontend + Arquivos de jogo salvos em %1$s + Arquivos do frontend + + Aguarde a operação do runtime terminar + Aguarde a instalação da ferramenta de compatibilidade terminar + Aguarde a instalação do app de área de trabalho terminar + Aguarde a instalação terminar antes de iniciar um jogo + Feche a sessão do Steam primeiro, para que o jogo não salve por cima da importação + Importando para %1$s + + %1$d arquivo importado de %2$s para %3$s + %1$d arquivos importados de %2$s para %3$s + %1$d arquivos importados de %2$s para %3$s + + + %1$d arquivo importado de %2$s para %3$s. Backup dos saves antigos em Download/DroidDeck/Saves/backups + %1$d arquivos importados de %2$s para %3$s. Backup dos saves antigos em Download/DroidDeck/Saves/backups + %1$d arquivos importados de %2$s para %3$s. Backup dos saves antigos em Download/DroidDeck/Saves/backups + + Escolha um zip de saves para %1$s + Exportando %1$s + + %1$d arquivo exportado como %2$s: %3$s + %1$d arquivos exportados como %2$s: %3$s + %1$d arquivos exportados como %2$s: %3$s + + Escolha onde salvar %1$s (%2$s) + Falha (%1$s): %2$s + Ainda não há logs de sessão: inicie uma sessão primeiro. + Não há logs de sessão para limpar. + + %1$d pasta de logs de sessão limpa. + %1$d pastas de logs de sessão limpas. + %1$d pastas de logs de sessão limpas. + + Falha no pareamento da depuração por Wi-Fi + Falha no comando de depuração por Wi-Fi + A depuração por Wi-Fi não está disponível + Comando ADB copiado + Escolha a pasta de ROMs + Não é uma GPU Adreno + O Turnip é compatível com GPUs Adreno. Com %1$s, o Steam pode mostrar uma tela preta. Download: %2$.0f MB. + Instalar mesmo assim + Remover runtime Linux + Isso exclui o runtime, o cliente Steam dentro dele e todos os jogos instalados nele. + Aplicado: %1$s + Não foi possível abrir %1$s na tela principal + Não foi possível abrir %1$s em %2$s + Não foi possível abrir %1$s na tela %2$d + Importando + Trocando + Restaurando + Cancelando + Excluindo + não foi possível ler o arquivo + %1$s importado + A troca pendente foi cancelada. + Escolha um pacote de componente (-linux .wcp) + Não foi possível acessar as Nightlies + %1$s armazenado + Falha no download: %1$s + Escolha um driver do runtime Linux (zip -Linux ou pacote Android + Linux) + Escolha um driver de vídeo (zip do AdrenoTools ou pacote Android + Linux) + Escolha um zip de driver ou um pacote Android + Linux + %1$s: %2$s + Removendo %1$s… + Esquecendo %1$s… + Escolha a pasta de armazenamento dos jogos + Escolha um ZIP de plugin do Decky + Não utilizável: %1$s + Programa + Removendo o runtime Linux + Não foi possível abrir as Configurações na tela inferior; elas foram abertas na tela principal. + Abra as Opções do desenvolvedor nas Configurações + + Não foi possível abrir a configuração “Instalar apps desconhecidos” do Android + Não foi possível verificar: %1$s + Esta versão não tem download para esta cópia do DroidDeck + Pare a sessão em execução primeiro e depois atualize + Baixando + Instalando + Aguardando sua confirmação + A atualização falhou + O Android não ofereceu a instalação da atualização + + Falha ao instalar o Decky: %1$s + Iniciando a importação do plugin… + Falha ao importar o plugin: %1$s + Plugin instalado. Reinicie o Steam para carregá-lo. + + Ainda não verificado - toque em atualizar para procurar novos drivers + Android + Linux: define também o driver de vídeo + Android + Linux: define também o driver do runtime + o Turnip integrado ao runtime + Vulkan do sistema: não há versão incluída para esta GPU + %1$s (incluído) + Manual + %1$s · %2$s + No momento, só uma das metades está publicada + %1$.0f MB para os dois + Nenhum driver para definir: %1$s + Verificando os drivers mais recentes… + Atualizado (não foi possível verificar: %1$s) + Os drivers recomendados não estão publicados no momento + Baixando %1$s %2$s… + Atualizado para %1$s %2$s. + Atualizado para %1$s %2$s. O driver de vídeo é aplicado depois que o DroidDeck reiniciar. + Alterado para %1$s %2$s. + Alterado para %1$s %2$s. O driver de vídeo é aplicado depois que o DroidDeck reiniciar. + Usando %1$s %2$s. + Usando %1$s %2$s. O driver de vídeo é aplicado depois que o DroidDeck reiniciar. + Não é um zip de driver: não é um driver AdrenoTools, um Turnip -Linux nem um pacote Android + Linux + Falha na importação: %1$s + Driver importado + Usando %1$s como driver do runtime e de vídeo. + Usando %1$s como driver do runtime e de vídeo. O driver de vídeo é aplicado depois que o DroidDeck reiniciar. + %1$s excluído (os dois drivers) + %1$s excluído + Mais recente: %1$s · verificado %2$s + Mais recente: %1$s · %2$s inacessível · verificado %3$s + + há %1$d min + há %1$d min + há %1$d min + + + há %1$d h + há %1$d h + há %1$d h + + + há %1$d dia + há %1$d dias + há %1$d dias + + Verificando Banners-Turnip, WinNative e DroidDeck… + falha na verificação + %1$s instalado - escolha-o em qualquer uma das listas para definir os dois drivers + %1$s instalado - escolha-o no menu + + Aguarde a remoção do runtime terminar + Jogo + touchpad + direto + Falha na troca: %1$s + Falha ao salvar o perfil: %1$s + A área de trabalho está pausada + %1$s está pausado + O programa está pausado + Iniciando a área de trabalho + Iniciando %1$s + Iniciando o programa + Não há logs desta sessão. + Não foi possível criar o arquivo compactado dos logs da sessão. + Não foi possível compartilhar os logs da sessão. + Iniciando a sessão… + baixando o runtime Linux + baixando o runtime Linux · %1$d de %2$d MB + verificando o runtime Linux + descompactando o runtime Linux + baixando a área de trabalho + baixando a área de trabalho · %1$d de %2$d MB + verificando a área de trabalho + descompactando a área de trabalho + baixando o Proton Experimental (ARM64) + baixando o Proton Experimental (ARM64) · %1$d de %2$d MB + verificando o Proton Experimental (ARM64) + descompactando o Proton Experimental (ARM64) + Não foi possível acessar o catálogo de runtimes. Verifique a conexão e toque em Jogar novamente. + O runtime Linux não foi instalado. Verifique o runtime em Configuração e toque em Jogar novamente. + A área de trabalho não foi instalada (%1$s). Verifique a conexão e toque em Área de trabalho novamente. + %1$s parou inesperadamente. Compartilhe os logs em um relatório de bug ou tente de novo. + O programa parou inesperadamente. Compartilhe os logs em um relatório de bug ou tente de novo. + A área de trabalho parou inesperadamente. Compartilhe os logs em um relatório de bug ou tente de novo. + Código de saída %1$d · %2$s + sem log + + O log mostra %1$d erro “Function not implemented”: a aceleração seccomp do proot está falhando em um auxiliar neste dispositivo. Tente %2$s → “%3$s” (ou “%4$s”) e inicie de novo. + O log mostra %1$d erros “Function not implemented”: a aceleração seccomp do proot está falhando em um auxiliar neste dispositivo. Tente %2$s → “%3$s” (ou “%4$s”) e inicie de novo. + O log mostra %1$d erros “Function not implemented”: a aceleração seccomp do proot está falhando em um auxiliar neste dispositivo. Tente %2$s → “%3$s” (ou “%4$s”) e inicie de novo. + + + Plugins + Instale um ZIP de plugin para a próxima sessão do Steam. + Baixando… + Instalar de um ZIP + Jogos + Áudio e vídeo + Gráficos + Internet + Escritório + Desenvolvimento + Educação + Ciência + Sistema + Utilitários + Populares + Em alta + Novidades + Atualizados + carregando… + Configuração do Flatpak + %1$s: %2$s + Não foi possível verificar atualizações no Flathub + instalado + pasta + Grafite + Superfícies grafite, com o azul do orbe do logo em tudo que estiver selecionado, em foco ou pressionado. + Papel sobre preto + O branco é a cor principal; o azul do ícone sinaliza linhas, anéis e pontos. + Azul do ícone + Fundo preto, texto branco e o azul em tudo que estiver selecionado, em foco ou pressionado. + Marinho elétrico + O azul mais vivo, sobre superfícies puxadas para o marinho em vez do cinza. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Espaço + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Instale o runtime primeiro + A biblioteca do Steam não está disponível + A biblioteca anterior no cartão SD não está disponível + O armazenamento de jogos não está disponível + A pasta de jogos adicionados não está disponível + Os manifestos do Steam não estão disponíveis + Vários sistemas + Interno + Sistema Linux + Cartão SD + Armazenamento + Selecionar + Propriedades + Adicionar aos favoritos + Remover dos favoritos + Renomear + Copiar + Recortar + Ações + Favoritos + Fixar pasta atual + Nenhum favorito ainda - fixe uma pasta pelo menu ⋮ dela para voltar aqui rapidamente. + Remover dos favoritos + 0 bytes + bytes + Local + Tipo + Tamanho + Modificado em + Pasta + Arquivo + Arquivo %1$s + Somente leitura + Impede que jogos (e o Wine) substituam ou excluam este arquivo. + Não foi possível alterar Somente leitura + Oculto + Este armazenamento não consegue guardar o atributo de oculto. + Marca o arquivo como oculto no Windows (atributo de oculto do Wine). + Não foi possível alterar Oculto neste armazenamento + Concluído + Excluindo... + Falha ao excluir + Movendo... + Copiando... + Movendo - %1$s + Copiando - %1$s + Movendo %1$d/%2$d - %3$s + Copiando %1$d/%2$d - %3$s + %1$d concluídos, %2$d com falha + %1$d concluídos, %2$d ignorados + + %1$d item movido + %1$d itens movidos + %1$d itens movidos + + + %1$d item copiado + %1$d itens copiados + %1$d itens copiados + + “%1$s” já existe + Renomeando... + Falha ao renomear + Criando pasta... + Não foi possível criar a pasta + Nova pasta + Nome da pasta + Criar + Novo nome + Excluir? + Excluir “%1$s” permanentemente? + + Excluir %1$d item? + Excluir %1$d itens? + Excluir %1$d itens? + + Não é possível desfazer essa ação. + + …e mais %1$d + …e mais %1$d + …e mais %1$d + + Excluindo %1$d/%2$d - %3$s + + %1$d não pôde ser excluído + %1$d não puderam ser excluídos + %1$d não puderam ser excluídos + + Mesclar adiciona e substitui arquivos dentro da pasta existente. + Substituir troca o arquivo existente. + Aplicar a todos os conflitos + Mesclar + Substituir + Manter ambos + Pular + Selecionar esta pasta + Voltar + %1$s está montado, mas não pode ser lido agora + Visualização em lista + Visualização em grade + Pesquisar + Ordenar + Nome + Data de modificação + Linhas confortáveis + Linhas compactas + Ocultar arquivos ocultos + Mostrar arquivos ocultos + Ocultar favoritos + Mostrar favoritos + Filtrar esta pasta + %1$s livres + Selecionados: %1$d + Nenhum + Todos + Colar + + %1$d item + %1$d itens + %1$d itens + + Colar %1$s aqui + Colar %1$s aqui (mover) + Arquivos + ARMAZENAMENTO + ACESSO RÁPIDO + FAVORITOS + Downloads + Downl. + ROMs + Logs de sessão + Imagens + “%1$s” adicionado aos favoritos + “%1$s” removido dos favoritos + Pasta vazia + Subir um nível + Escolha uma pasta + Escolha um arquivo + Azul Steam + Céu + Branco + Violeta + Verde + Âmbar + Vermelho + Rosa + Botão A + Botão B + Botão X + Botão Y + Botão de ombro esquerdo + Botão de ombro direito + Gatilho esquerdo + Gatilho direito + Botão Exibir + Botão Menu + Exibir + Menu + Direcional para cima + Direcional para baixo + Direcional para a esquerda + Direcional para a direita + Oculto + Arraste para mover + Encaixar + Salvar + Nenhum + Teclado + trackpad + Terminal + Botões traseiros + trackpads do Deck + + Removendo o runtime Linux + + Removendo o runtime Linux · %1$d entrada + Removendo o runtime Linux · %1$d entradas + Removendo o runtime Linux · %1$d entradas + + Runtime Linux removido + Não foi possível concluir a remoção do runtime Linux. Tente remover de novo em Configuração. + Removendo restos do runtime + Baixando + Verificando + Extraindo + Aparece enquanto o runtime Linux é instalado ou removido + Instalando o runtime Linux + Removendo o runtime Linux + + O runtime Linux não está instalado + O catálogo não tem soma de verificação para %1$s + A soma de verificação não confere - nada foi alterado + Instalando %1$s + Não foi possível colocar o AppImage no lugar + Falha na extração + Falha na instalação + + Falha na configuração do Flatpak (código de saída %1$d) + A configuração do Flatpak não terminou + Outra tarefa da loja está em andamento + Configure o Flatpak primeiro + Iniciando + O Flatpak terminou com o código %1$d + Instalando %1$s + Atualizando %1$s + Removendo %1$s + Instalando %1$s (%2$d/%3$d) + Atualizando %1$s (%2$d/%3$d) + Removendo %1$s (%2$d/%3$d) + traduções + drivers gráficos + codecs de mídia + runtime %1$s + runtime %1$s %2$s + + O arquivo é pequeno demais para ser um AppImage + Não é possível ler o arquivo: %1$s + Isto não é um AppImage (não é um programa Linux) + Este AppImage não foi feito para ARM64 (aarch64) nem para PCs x86 + Isto é um programa Linux, mas não um AppImage + Só AppImages do tipo 2 podem ser importados (este é do tipo %1$d) + Espaço livre insuficiente: a importação precisa de cerca de %1$s + Espaço livre insuficiente: a atualização precisa de cerca de %1$s + Falha na importação + Esse AppImage não existe mais + Falha na atualização + Extraindo %1$s + Não foi possível extrair o AppImage + Não foi possível extrair o AppImage: %1$s + + Pare a sessão ativa antes de instalar o Decky Loader + Falha no download do Decky Loader + O tamanho do Decky Loader não corresponde aos metadados da versão + Não foi possível baixar a soma de verificação da versão + A soma de verificação da versão está ausente + A soma de verificação da versão é inválida + A soma de verificação do Decky Loader não confere + O arquivo da versão não é um executável Linux de 64 bits compatível + Uma sessão foi iniciada durante o download; pare-a antes de instalar o Decky Loader + Não foi possível tornar o PluginLoader executável + Não foi possível ativar a depuração remota do CEF do Steam + Não foi possível preparar o PluginLoader + Não foi possível preservar o PluginLoader existente + Não foi possível ativar o novo PluginLoader + Pare a sessão ativa antes de instalar um plugin + Instale o Decky Loader antes de importar um plugin + Não foi possível ler o arquivo ZIP selecionado + Não foi possível criar a pasta de plugins do Decky + Extraindo o ZIP do plugin + O ZIP do plugin deve conter uma única pasta de plugin no nível superior + Faltam plugin.json, package.json ou dist/index.js no ZIP do plugin + O nome do plugin está ausente + Uma sessão foi iniciada durante a instalação; pare-a antes de instalar um plugin + Plugin instalado · reinicie o Steam para carregá-lo + Falha na instalação do ZIP do plugin + O ZIP do plugin contém entradas demais + O ZIP do plugin contém um caminho inválido + O ZIP do plugin contém caminhos duplicados + O ZIP do plugin contém um caminho inseguro + Não foi possível criar a pasta do plugin + O ZIP do plugin ultrapassa o limite de 1 GiB quando extraído + Não foi possível tornar executável o binário incluído no plugin + Entrada remote_binary inválida + Nome de binário remoto inválido + A URL do binário remoto precisa usar HTTPS + O binário remoto %1$s tem um hash SHA-256 inválido + Não foi possível criar a pasta bin do plugin + Baixando binário do plugin · %1$s + Não foi possível baixar o binário do plugin %1$s + A soma de verificação do binário do plugin não confere · %1$s + Não foi possível tornar executável o binário do plugin · %1$s + Não foi possível substituir o binário do plugin · %1$s + Não foi possível instalar o binário do plugin · %1$s + A pasta do plugin pertence a outro plugin + Não foi possível preservar o plugin existente + Não foi possível ativar o plugin importado + Não foi possível atualizar as configurações de plugins do Decky + Não foi possível restaurar o plugin existente + + Não foi possível salvar a identidade da depuração por Wi-Fi + Falha no comando ADB (%1$d) + O Android não confirmou a alteração do limite de processos filhos: %1$s + Nenhum pareamento de depuração por Wi-Fi salvo + Digite o endereço IP e a porta atuais da depuração por Wi-Fi + + Tempo esgotado esperando o pop-up de pareamento. Tente novamente pelo DroidDeck. + Não foi possível procurar o pop-up de pareamento (%1$d). Verifique se o Wi-Fi está ativado. + Não foi possível procurar o pop-up de pareamento: %1$s + Digite os 6 dígitos do código + Pareando… + Esse código não funcionou. Abra o pop-up de pareamento de novo e digite o novo código. + Pareado. Conectando… + Pareado, mas a porta da depuração por Wi-Fi não foi encontrada. Mantenha a depuração por Wi-Fi ativada e tente de novo. + Aplicando a configuração… + Falha no comando de depuração por Wi-Fi + Configuração da depuração por Wi-Fi + Pede o código de pareamento da depuração por Wi-Fi enquanto as Configurações estão abertas + Parear depuração por Wi-Fi + Ative a depuração por Wi-Fi e toque em “Parear o dispositivo com um código de pareamento”. + Ative a depuração por Wi-Fi e toque em “Parear o dispositivo com um código de pareamento”. Fique nas Configurações: o código é digitado nesta notificação. + Código de pareamento de 6 dígitos + Toque em Digitar código e insira os 6 dígitos mostrados nas Configurações. + Digite o código de pareamento por Wi-Fi + Tente o código de novo + Digitar código + Configurando o Steam + O Steam está pronto + Toque para voltar ao DroidDeck. Você já pode desativar a depuração por Wi-Fi. + A configuração da depuração por Wi-Fi parou + + O download respondeu HTTP %1$d + O tamanho do download não corresponde ao APK publicado, e ele foi descartado - tente de novo + O download não corresponde à soma de verificação e foi descartado - tente de novo + A atualização é para o pacote Android %1$s, não %2$s + Os metadados da atualização não indicam a chave de assinatura de versões do DroidDeck + O Android não conseguiu ler o APK baixado + O APK baixado é para %1$s, não %2$s + O APK baixado informa versionCode %1$d, não o %2$d publicado + O Android não instala o versionCode %1$d por cima do %2$d instalado + O APK baixado não tem informações de assinatura + O APK baixado não está assinado com a chave de versões do DroidDeck + O catálogo de atualizações respondeu HTTP %1$d + + Iniciando a sessão… + %1$d:%2$02d decorridos · ainda em andamento + Baixando a atualização do cliente Steam · %1$d%% + Baixando o cliente Steam · %1$s + + Já há uma instalação de ferramenta de compatibilidade em andamento + Pare a sessão ativa antes de instalar ferramentas de compatibilidade + Não foi possível encontrar uma versão ARM64 de %1$s + É preciso ter pelo menos 4 GB livres, além do tamanho do download, para instalar %1$s + Baixando %1$s %2$s + Tentando baixar de novo · tentativa %1$d de %2$d + Download interrompido + Falha no download; o arquivo parcial foi mantido para que uma nova tentativa possa continuar + O download ultrapassou o tamanho da versão e foi excluído + O download ficou incompleto; o arquivo parcial foi mantido para continuar em uma nova tentativa + Verificando o download + Não foi possível ler a soma de verificação da versão; tente de novo + %1$s %2$s não publica soma de verificação; nada foi instalado + A soma de verificação não confere; o download foi excluído + Uma sessão foi iniciada durante o download; pare-a antes de instalar ferramentas de compatibilidade + Instalando %1$s + Falha na instalação do %1$s (código de saída %2$d) + Não foi possível instalar o %1$s + Buscando o pacote droiddeck-esync + + com auxiliares Linux + só DLLs + Original (Proton atualizado desde a última troca) + Alterado fora de Componentes + ausente + Original (%1$s) + Original de %1$s + %1$s %2$s entra no %3$s quando o jogo que está rodando nele for fechado. + %1$s %2$s agora está no %3$s. Vale a partir do próximo jogo iniciado. + Esse pacote original não está armazenado + %1$s do %2$s volta ao original quando o jogo que está rodando nele for fechado. + %1$s do %2$s restaurado a partir do original do %3$s. + %1$s está em uso no %2$s. Primeiro, troque esse Proton para outra versão. + %1$s não está armazenado + %1$s excluído. + O original da versão instalada é mantido: é o caminho de volta. + Não armazenado + Original do %1$s de %2$s excluído. + Os downloads vêm só das versões Nightlies + Esta lista de pacotes é anterior às somas de verificação - atualize-a e tente de novo + A soma de verificação não confere + + Linear + Vizinho mais próximo + Snapdragon GSR (qualidade) + Só nitidez + Automático (acompanha a escala) + + Nada aplicado. + Clareza + Nitidez com um toque de contraste - o do dia a dia. + Vívido + Cores bem fortes. Debanding ativado para o céu não ficar em faixas. + Cinematográfico + Contraste mais profundo, cores mais frias, bordas suavizadas. + Competitivo + Sombras clareadas e nitidez forte - aviste os outros primeiro. + Nitidez adaptativa + Só nitidez, cores intactas. + Fílmico + Curva de filme, cores levemente apagadas. + Arcade + Intenso e vibrante. + CRT retrô + Os passes de tubo CRT e de sinal analógico juntos. + Upscale nítido + Para rodar abaixo da resolução da tela. + Pixels limpos + Pixels nítidos para jogos 2D e antigos. + Contorno anime + Contornos de desenho animado e nitidez. + + Não é uma GPU Adreno + Compatível + Experimental: os drivers são versões de teste + Abaixo do hardware testado (Adreno 725 ou mais recente): pode não funcionar + Incompatível: o DroidDeck precisa de uma GPU Adreno (Snapdragon) + + O limite de requisições do GitHub foi atingido - tente mais tarde + O GitHub respondeu HTTP %1$d + o download respondeu HTTP %1$d + Esta lista de drivers é anterior às somas de verificação - atualize-a e tente de novo + A soma de verificação não confere - o download foi descartado + Não é um driver AdrenoTools: %1$s + sem meta.json + não é possível ler o meta.json (%1$s) + este é um driver do runtime Linux (importe-o em “Driver do runtime Linux”) + este é um driver de jogos Wayland, que não tem utilidade neste app + o meta.json não indica libraryName, então nada pôde ser entregue ao AdrenoTools + o meta.json indica %1$s, que não está no zip + %1$s não é uma biblioteca compartilhada ELF AArch64 de 64 bits + %1$s usa glibc - é um Turnip “-Linux”, que vai em “Driver do runtime Linux” + Não há libvulkan_freedreno*.so neste zip. Um zip de Turnip Android (AdrenoTools) ou -Wayland não é um driver do runtime Linux. + %1$s não é uma biblioteca compartilhada ELF AArch64 de 64 bits. + %1$s não é um driver glibc - ele usa a libc do Android. O runtime Linux precisa de um zip “-Linux”; um Turnip comum ou “-Wayland” não pode ser carregado pelo cliente Steam de jeito nenhum. + Não é um pacote de drivers: não há manifest.json indicando um driver android e um linux + Não é um pacote de drivers: %1$s aponta para fora dele + Não é um pacote de drivers: extraído, ele ocupa mais de %1$d MB + + Cartão SD + Pasta + + Compartilhar logs da sessão + + requer Android 10 ou mais recente (camadas de exibição com espaço de cores) + esta tela não informa suporte a HDR + esta tela é compatível com %1$s, não com HDR10 + + esta GPU + O limite de processos filhos está ativado + O limite de processos filhos não está definido + Não foi possível verificar o limite de processos filhos + O limite de processos filhos está desativado + O limite de processos filhos não é necessário + %1$s Se não for possível, conecte este dispositivo a um computador com ADB e execute o comando abaixo. Depois volte aqui e verifique de novo. + As sessões do Steam podem ser iniciadas. + Esta versão do Android não usa esse limite. + Ative “Desativar restrições de processos filhos” nas Opções do desenvolvedor e mantenha as Opções do desenvolvedor ativadas depois. + Esta versão do Android não tem uma chave para isso. Use a depuração por Wi-Fi para alterar pelo próprio dispositivo. + Esta versão não tem download para esta cópia do DroidDeck. + O Android não vai instalá-la por cima desta versão porque o versionCode %1$d é menor que o %2$d instalado. + O Android não instala uma versão mais antiga por cima de uma mais nova. Fique nesta versão até o canal alcançá-la. + Esta cópia do DroidDeck tem outra assinatura (uma build local?), então o Android não consegue atualizá-la diretamente. + Não há espaço livre suficiente para a atualização. + A atualização foi cancelada. + A atualização não foi instalada: %1$s + A atualização não foi instalada. + zip do %1$s + zip + %1$s ainda não foi iniciado + Nenhum arquivo de save encontrado para %1$s + %1$s não tem a pasta drive_c, então não é um zip de saves do GameHub ou do Winlator + não é possível criar pastas em %1$s + não é possível gravar em %1$s + não é possível gravar em %1$s (%2$s) + A pasta não está disponível + Outro arquivo já usa %1$s + A pasta de artes não está disponível + A pasta tem um inventário de sincronização não relacionado + Não foi possível remover %1$s + O arquivo sai da pasta de exportação + FEX (build sem nome de versão) + sem profile.json em %1$s + %1$s não é um pacote de componente Linux (tipo %2$s) + %1$s · %2$s livres + Automático + O catálogo de atualizações não passou nas verificações (%1$s) + Configuração de processos filhos do DroidDeck + Conectado · modo offline + Conectado + INCLUÍDO + BAIXADO + IMPORTADO + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Tela da sessão + Resolução + Ajuste de jogos 16:9 + Como os jogos 16:9 se ajustam a esta tela. Na próxima sessão. + Escolha Igual à tela ou um tamanho personalizado mais estreito que 16:9. + Manter proporções + Esticar para a tela + Escala de imagem + Filtro de escala + Compartilhado entre Steam e Área de trabalho. + Aplicado na hora. Compartilhado entre Steam e Área de trabalho. + FSR, GSR e NIS são aplicados ao ampliar a imagem da sessão. Só nitidez também funciona sem ampliação. + Nitidez do filtro + Taxa de quadros + %1$s Define a escala como %2$s. + Igual à tela (%1$d×%2$d) + Seguir a tela (redimensiona ao dobrar ou desdobrar) + Steam + Mostrar detalhes do dispositivo + Ocultar detalhes do dispositivo + Lendo o dispositivo… + Copiar + Copie estes detalhes para um relatório de bug. + Copiado. + Forçar SSBS + Mantém ativada uma configuração de velocidade da CPU que o Wine fica desativando. Pode deixar jogos do Windows mais rápidos no Snapdragon 8 Elite; pouca mudança em chips mais antigos. Vale a partir da próxima vez que um jogo for iniciado. + + Lojas + Steam + Personalizado + Lojas + Abrir uma loja em + Nível de velocidade de download + Mecanismo das lojas + ● Mecanismo pronto + Não compilado + Equilibrado + Rápido + Máximo + Lojas + Configurações das lojas + Configurações + (sem login) + Loja + Instalados + Biblioteca + Todos + Downloads + %1$s (%2$d) + Fazer login + %1$s · sair + Sair de %1$s? + Os jogos instalados continuam. A biblioteca é buscada de novo quando você fizer login outra vez. + Continuar conectado + Sair + Pesquisar no catálogo de %1$s… + Pesquisar jogos instalados… + Pesquisar na sua biblioteca… + Pesquisar + Nada instalado + Carregando + Nenhum jogo + Carregando a loja… + Sem catálogo + Novidades + Ofertas + Jogos grátis + Em alta + Sua biblioteca + Jogar + Instalar + Retomar instalação + Interrompido + Instalar · %1$s + Obter grátis + Ver em %1$s + Comprar · %1$s + Instalado + Na biblioteca + Grátis + Retomar + Atualizar + Mostrar conteúdo adulto + Epic + EOS + Offline + Desativado + Login da Epic (EOS) + Iniciar offline + Resolver login da Epic + Saves na nuvem + Enviar para a nuvem + Baixar da nuvem + Sem saves na nuvem + Conflito na nuvem + Conflito na nuvem · %1$d arquivos + … + Manter da nuvem + Manter local + Verificando a nuvem… + Última sincronização: %1$s + Ainda não sincronizado + %1$d arquivos + instalado + na sua biblioteca + loja + Conquistas + %1$d de %2$d desbloqueadas + %1$d / %2$d + Desbloqueadas · %1$d + Bloqueadas · %1$d + Ocultas · %1$d + Conquista oculta + Os detalhes aparecem quando ela é desbloqueada. + Todas as conquistas desbloqueadas + Carregando… + Entre no %1$s para ver suas conquistas. + Não foi possível acessar o %1$s. Verifique novamente quando estiver online. + O %1$s não lista conquistas para este jogo. + Mostrar todas as %1$d + Conquistas · %1$s + ✓ No Steam + Desinstalar + Desinstalando… + Desinstalar %1$s? + Libera %1$s e remove o jogo do Steam. + Remove os arquivos e o atalho no Steam. + O Steam detecta o jogo na próxima vez que for iniciado; abra o jogo por lá ou inicie o Steam agora. + Proton + Escolha do Steam para cada jogo + Áudio + DirectAudio + Componentes + FEX, DXVK, VKD3D + Mecanismo + %1$s · nativo + %1$s · integrado + Como ele roda + Obtido de + %1$s, com sua conta + Instalado em + Armazenamento de jogos (Games/%1$s/%2$s) + Iniciado por + Steam e Proton, o mesmo caminho dos jogos do Steam + Steam e Proton · login da Epic a cada início, overlay do EOS desativado + Steam e Proton · variáveis do Amazon Games Services definidas + Steam + adicionado ao Steam na instalação (interface do Deck + Steam Input) + Nenhum download + Pausado + Na fila + Na fila · #%1$d + Cancelado + Falhou + Falhou · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Pausar + Retomar + Cancelar + Tentar de novo + Limpar + Limpar concluídos + Manifesto + Download + Verificação + Instalação + Concluído + Verificando + Baixando + Excluir download? + Falhou · %1$s mantido + Verificando + Instalando + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d arquivos + Gerenciador de downloads + Downloads simultâneos + Instalar em + Instalar %1$s: + Interno + %1$s livres + Downloads da loja + Mostra o progresso dos downloads de jogos e mantém os downloads rodando em segundo plano + Baixando + %1$d downloads + Preparando… + Downloads da loja concluídos + %1$s instalado + Falha no download de %1$s + Excluir + Manter + Fazer login em %1$s + Fazer login com seu provedor + Recarregar + Fechar + Tentar de novo + Concluindo o login… + Não foi possível carregar a página de login: %1$s + Este provedor não permite login a partir de uma página incorporada. Faça login com a senha da loja. + A página de login respondeu com um erro (HTTP %1$d). + A conexão com a página de login não é segura. + Não foi possível verificar o login. Tente de novo. + Falha no login. Tente de novo. + Fechar + Voltar + Editar + Adicionar todos os jogos desta pasta + Nada de novo nesta pasta + Nome + Destino + Iniciar em + Opções de inicialização + Artes + Outro exe… + Remover + Capa + Fundo + Logo + Ícone + Pasta do jogo + Steam + SteamGridDB + Seu arquivo + Do exe + Automático + Escolher imagem… + Escolha uma imagem: %1$s + Voltar ao automático + + %1$d jogo adicionado + %1$d jogos adicionados + %1$d jogos adicionados + + + %1$d já está em Jogos + %1$d já estão em Jogos + %1$d já estão em Jogos + + B · Voltar para %1$s + o jogo + Configurações de jogo + Drivers gráficos + Este Proton + Todos os Protons + Para todos os Protons + Disponíveis · %1$d + Verificado %1$s + Procurar novos componentes e drivers + Obter + %1$s · DXVK, VKD3D-Proton e FEX abaixo são deste Proton + %1$s · testado + %1$s · não testado + %1$s · incompatível + Mantém o par recomendado atualizado + Automático · %1$s + Drivers do runtime e de vídeo separados + Usar %1$s? + Remover %1$s? + Diagnóstico + Pronto para jogar + Ainda não está pronto para jogar + runtime %1$s + limite de processos desativado + conectado como %1$s + Quando algo precisar de você + Aparência + Este dispositivo + Artes da biblioteca + APPS LINUX + Flathub + Apps para a área de trabalho Linux, abertos pela Área de trabalho. + Configurar o Flathub + Remover %1$s? + As configurações e os arquivos dele são mantidos. + Manter + Tamanho em disco + Desinstalar a área de trabalho? + Todos + Conquistas da GOG + Sincroniza conquistas dos jogos compatíveis da GOG. Vale a partir da próxima abertura. + Conquistas desativadas + Conquistas ativadas + Entre na GOG novamente + Conquistas enviadas + Envio de conquista pendente + Ative as conquistas da GOG nas configurações de Lojas + Conquistas indisponíveis; veja os registros da sessão + Conquista desbloqueada + Colar + Direcional + Analógico esquerdo + Analógico direito + Menu + Teclado + Direcional para cima + Direcional para baixo + Direcional para a esquerda + Direcional para a direita + Controles por toque + Controle por toque + Mouse + Ativado + Desativado + Lenta + Normal + Rápida + Editar layout + Menu do DroidDeck + Arraste os controles para a tela + Organizar + Descartar + Salvando… + Ícone + L− + L+ + A− + A+ + Ocultar + Cor e opacidade… + Copiar este layout + Layout copiado; cole no layout de toque de qualquer jogo + Colar layout copiado + Restaurar o padrão da Steam + Este controle não tem uma ação vinculada na Steam para receber um ícone + Ícone para %1$s + Cor do layout de toque + Concluído + Não foi possível salvar o layout de toque + Layout de toque salvo na Steam + App %1$d + Rótulo (opcional) + Os ícones da Steam não foram encontrados + + %1$d ícone da Steam + %1$d ícones da Steam + %1$d ícones da Steam + + Cor do ícone + Cor do botão + Sem ícone + + Verificando login… + Buscando sua biblioteca… + Buscando lista de jogos… + Carregando detalhes dos jogos… (%1$d/%2$d) + + Sincronizando %1$d jogo… + Sincronizando %1$d jogos… + Sincronizando %1$d jogos… + + Sincronizando… %1$d/%2$d + Buscando capas dos jogos… + Falha na sincronização + Nenhum jogo encontrado na sua biblioteca da %1$s + A Amazon Games não respondeu sobre sua biblioteca: atualize, ou faça login novamente + Não foi possível conectar à GOG + Não foi possível ler sua biblioteca da GOG + Registrando na Steam… + Cardboard VR + Modo Cardboard + Imagens para o olho esquerdo e o direito, para um óculos de RV de celular. + Imagem + Tela virtual + 3D lado a lado + Óculos + Cardboard + Cardboard (2014) + Sem lente + Distância entre os olhos + Largura da tela + Distância da tela + Rastreamento da cabeça + Este dispositivo não tem sensor de rotação. + Recentralizar visão + Recentralizar + SteamVR (experimental) + Instalar + Baixa o SteamVR ARM64 da Valve (571 MB). + Baixando e descompactando o SteamVR… + Iniciar + Executa o SteamVR usando sua cabeça como headset; a imagem muda para 3D lado a lado. + Parar + O SteamVR está em execução. + Indisponível nesta sessão. + O SteamVR não respondeu; veja o registro da sessão. + Baixar componentes + Os downloads são compartilhados com a página Componentes. Escolha acima uma versão baixada para usá-la na próxima vez que um jogo for aberto. + Escolha uma versão para baixar + Desenvolvedora + Publicadora + Lançamento + Principal + + Extras + Completista + %1$s h + %1$d min + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml new file mode 100644 index 000000000..8c01376ce --- /dev/null +++ b/app/src/main/res/values-ro/strings.xml @@ -0,0 +1,2260 @@ + + + DroidDeck + + Prima rulare descarcă Steam. Poate dura câteva minute. + În mod implicit, la părăsirea aplicației, Steam este pus pe pauză; continuă de unde ai rămas când te întorci. + Controllerul tău apare ca un gamepad Xbox 360. Altfel, folosește comenzile de pe ecran. + + + Prima rulare configurează desktopul. Poate dura câteva minute. + În mod implicit, la părăsirea aplicației, sesiunea este pusă pe pauză; continuă de unde ai rămas când te întorci. + Atingerea funcționează ca un touchpad: trage pentru a mișca cursorul, apasă scurt pentru a face clic. + Controllerul tău apare ca un gamepad Xbox 360. Altfel, folosește comenzile de pe ecran. + + Sesiune + Apare cât timp o sesiune Steam sau desktop este activă + Steam este în execuție + Descărcare Steam activă + Sesiune pusă pe pauză - apasă Reluare când ești pregătit + Reluare + Suspendă acum + Oprește sesiunea + Generare de cadre + Selectează modul de generare a cadrelor + Dezactivat + Win-FG %1$d× + LSFG %1$d× + LSFG Adaptiv %1$d + Necesită Lossless Scaling pe Steam + Instalează Lossless Scaling în Steam sau importă Lossless.dll + Importă Lossless.dll… + Se folosește copia din Steam + Se folosește o copie importată + Alege Lossless.dll + Lossless.dll importat + Acest Lossless.dll este deja în uz + Lossless Scaling nu este deținut pe acest cont Steam + Acest fișier nu este Lossless.dll al Lossless Scaling + Nu s-a putut pregăti LSFG din acest Lossless.dll + LSFG are nevoie de Lossless Scaling din Steam sau de un Lossless.dll importat + LSFG nu a putut pregăti shaderele din Lossless.dll + %1$s: neacceptat + %1$s: motor eșuat + %1$s: necesită Lossless Scaling + %1$s %2$s · se pornește + Adaugă o bibliotecă Steam și o scanează pentru jocuri Windows. Jocurile deținute folosesc intrările lor Steam; restul devin comenzi rapide non-Steam. + Mediu de joc + Variabile de mediu pentru jocurile Proton. + Editează + Toate jocurile Proton + Profil + ID-ul aplicației Steam + Deschide + Jocul %1$s + Adaugă variabilă + Nume variabilă + Valoare + Salvează + Elimină + Moștenește din nou + Șterge toate suprascrierile de mediu + Dezactivează pentru acest profil + Nicio variabilă definită. + Moștenită + Variabilă personalizată + Nu s-a putut încărca sau salva. Verifică spațiul de stocare liber și încearcă din nou. + Folosește un nume valid și o valoare de cel mult 8192 de caractere. + Mediul de execuție Proton standard nu citește această variabilă. + Se încarcă… + Suprascrierea nivelului de funcții D3D12. 12_2 raportează capabilitățile DirectX 12 Ultimate; funcțiile neacceptate pot eșua. Lasă nesetat pentru detectare automată. + Suprascrierea modelului de shadere D3D12. 6_9 necesită un VKD3D suficient de recent și funcții GPU corespunzătoare. Lasă nesetat pentru detectare automată. + Păstrează memorarea în cache a shaderelor Mesa activată pentru a reduce compilarea repetată. false activează memorarea în cache. + Dimensiunea maximă a cache-ului de shadere Mesa, de exemplu 1G. Cache-urile mai mari folosesc mai mult spațiu de stocare. + Opțiuni VKD3D pentru D3D12. nodxr dezactivează ray tracing-ul; folosește doar pentru un joc care are nevoie de el. + Setează la 1 pentru un jurnal de diagnosticare Proton. Înregistrarea poate reduce performanța și folosește spațiu de stocare. + Setează la 1 pentru a folosi WineD3D bazat pe OpenGL în locul DXVK, pentru testarea compatibilității D3D9–11. + Setează la 1 pentru a păstra acest joc în afara ntsync (droiddeck-ntsync sau un driver de kernel); folosește droiddeck-esync unde există un pachet, altfel droiddeck-fsync. + Setează la 1 pentru a dezactiva droiddeck-esync pentru acest joc; folosește în schimb droiddeck-fsync, sau droiddeck-ntsync unde fsync este dezactivat și el. + Setează la 1 pentru a păstra acest joc în afara droiddeck-fsync; folosește droiddeck-esync unde există un pachet, altfel droiddeck-ntsync. + Setează la 1 pentru a rula acest joc pe droiddeck-fsync, sau la 0 pentru a pune droiddeck-esync primul unde un pachet se potrivește cu Proton-ul său. + Setează la 0 pentru a dezactiva asistentul de navigare cu gamepad al Proton pentru acest joc. + Suprapunere DXVK pentru D3D9–11. fps afișează rata de cadre; nesetat o ascunde. + Setări DXVK separate prin punct și virgulă. Versiunile recente folosesc dxvk.maxFrameRate pentru limitarea cadrelor; preferă limitatorul de cadre al sesiunii pentru joc obișnuit. + Limita ratei de cadre D3D12. 0 dezactivează limita. + Procesarea pe fire a Mesa OpenGL. true poate ajuta jocurile OpenGL limitate de CPU; testează pentru fiecare joc. + Sincronizare Wine + Modul în care jocurile își sincronizează firele. Se aplică la sesiunea următoare. + esync + ntsync + fsync + wineserver + Preset FEX + Se aplică la următoarea lansare a jocului. + Jocuri + Valori implicite FEX + Folosește valorile implicite FEX. + Stabilitate + Setările cele mai sigure, dar cele mai lente. + Compatibilitate + TSO complet cu multiblock. + Intermediar + TSO echilibrat cu precizie x87 redusă. + Performanță + TSO complet dezactivat; multiblock. + Performanță + TSO + Setările de performanță cu TSO principal activat. + Extrem + Dezactivează verificările de cod auto-modificabil. Jocurile JIT și DRM-ul pot să nu mai funcționeze. + Extrem + TSO + Setările extreme cu TSO principal activat. + Extreme-gn + Performanță cu indicii TSC și metadate. Încearcă înainte de Extrem. + Denuvo + Performanță cu verificări complete de cod și bitul de hipervizor ascuns. + Valoare personalizată + Selectează valoarea + Emulatoare & aplicații + Adaugă + Propria ta aplicație sau joc + Script + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Se pornește… + %1$s adăugat + %1$s eliminat + Desktop + Aplicația nu mai există. Alege-o din nou din Desktop. + Deschide %1$s + Elimină + Apasă din nou pentru a elimina + Oprește sesiunea pentru a elimina + Rulează %1$s din folderul său, conectat în sesiune acolo unde se află. Eliminarea aplicației nu afectează acel folder. + Rulează %1$s dintr-o copie a folderului său din DroidDeck. Eliminarea aplicației șterge copia, nu originalul. + Instalează mai întâi mediul de execuție Linux + Fișierul nu mai există + Spațiu liber insuficient: copierea folderului necesită %1$s + Se copiază %1$s + Adăugarea aplicației a eșuat + Se caută o versiune a %1$s + Nu s-au putut citi versiunile %1$s + Niciuna dintre versiunile %1$s nu are un AppImage ARM64 sau x86_64 + Se descarcă %1$s + Descărcarea a eșuat + Actualizarea aplicației a eșuat + Acea iconiță nu a putut fi citită + %1$s salvat + Editează + Verifică actualizările + Se verifică actualizările… + Actualizează la %1$s + Actualizat · %1$s + %1$s actualizat + %1$s · %2$s + Adaugă o aplicație + Script + AppImage + GitHub + Flatpak + Un fișier .sh. Rulează din propriul folder, pe tot ecranul, sub gamescope. + Un AppImage ARM64 (aarch64), sau unul x86_64 care rulează prin FEX. Este despachetat o singură dată în mediul de execuție Linux. + Instalează AppImage-ul ARM64 din cea mai recentă versiune a depozitului, sau pe cel x86_64 prin FEX. + Folosește FEX + Automat + Activat + Dezactivat + FEX rulează programe Linux x86 pe acest dispozitiv ARM64. Automat îl folosește atunci când programul este compilat pentru x86. + %1$s prin FEX + Acest program este compilat pentru PC-uri %1$s și rulează prin FEX, care nu este încă configurat. Deschide Steam o dată, fiind autentificat: DroidDeck îl pune să descarce FEX și Steam Linux Runtime de care are nevoie, apoi acesta se deschide. + Acest program + %1$s este compilat pentru PC-uri x86 și rulează prin FEX, care nu este încă instalat. Pornește Steam o dată, fiind autentificat, și lasă-l să descarce FEX și Steam Linux Runtime, apoi pornește din nou acesta. + Un ID de aplicație Flathub sau pagina sa Flathub. Flatpak este configurat mai întâi, dacă nu este deja. + Fișier + Niciun fișier ales + Alege fișierul + Schimbă + Alege un script + Alege un AppImage + Alege o iconiță + Depozit + proprietar/depozit sau un link github.com + Nu este un depozit GitHub + ID aplicație + org.ppsspp.PPSSPP + Nu este un ID de aplicație Flathub + Sau un fișier .flatpak, pentru o aplicație care nu este pe Flathub. + Alege un fișier .flatpak + Sursă + Pe Desktopul tău + Aplicație nouă + Nume + Din aplicație + Iconiță + Iconiță implicită + Alege iconița + Șterge + Din depozit + Se caută iconițe… + Implicit + Editează %1$s + Salvează + Se verifică folderul… + Folderul său este conectat în sesiune: %1$s + Folderul său conține programe Linux, pe care stocarea partajată nu le poate rula, așa că este copiat în DroidDeck (%1$s). + Adaugă + Anulează + Instalează mai întâi mediul de execuție Linux din Configurare. + Așteaptă până se termină %1$s. + Compatibil + Versiunile marcate „%1$s” din meniul Compatibilitate al Steam au remedierile de prefix ale DroidDeck și pornesc jocuri aici. Cea implicită este Proton Experimental ARM64 (%1$s), sau Proton 11.0 ARM64 (%1$s) după ce acea versiune mai nouă este instalată. Intrările Proton proprii ale Valve, fără marcaj, nu pot porni un joc pe acest dispozitiv. + Instalat %1$s · %2$s + Anulează + Automat + Niciodată + Întotdeauna + Dezinstalează + Verifică + Actualizează + Se verifică… + Se procesează… + Uită + Adaugă… + Folosește + Se aplică la sesiunea următoare. + Sesiune Steam + Sesiune Desktop + Afișaj + Rezoluție + Până la 720p + Până la 900p + Până la 1080p + Nativă + %1$s (implicit) + Personalizat · %1$d×%2$d + Personalizat… + 720p poate face meniurile mai receptive. + Raport de ecran + Stabilit de rezoluția personalizată. + Automat folosește cel puțin 16:9. + Limită de cadre + Limitează întreaga sesiune. Afișajul trece la o rată de reîmprospătare pe care limita o împarte exact. + HDR + Ieșire HDR10 + Indisponibil: %1$s. + Repornește aplicația pentru a aplica. + Drivere + Drivere GPU + Partajate de Steam și Desktop. Se stabilesc pe pagina Componente. + Atingere & comenzi + Atingere + Touchpad + Direct + Automat: touchpad pe Desktop, direct în Steam. + Comenzi pe ecran + Steam + QAM + Automat afișează comenzile când niciun controller nu este conectat. + Atingere Steam (beta) + Comenzile tactile Steam pornesc odată cu următoarea sesiune Steam + Aranjament tactil Steam + Mută, redimensionează sau ascunde comenzile tactile ale acestui joc; salvate în Steam + Controller + Cum vede Steam controllerul tău. Se aplică la sesiunea următoare. + Controller Steam Deck + Steam Controller + DualShock 4 + DualSense + Controller Xbox 360 + Deck: butonul Acces rapid și giroscopul. Steam Controller: la fel, cu senzori de strângere pe butoanele din spate. DualShock 4 și DualSense: giroscop, și trackpadurile ca touchpad propriu. Xbox 360: deschide Acces rapid cu Guide+A. + Înapoi + Meniu → QAM + QAM → Meniu + Sesiune + Comportament în fundal + Când aplicația este ascunsă sau ecranul se stinge. + Manual + Nativ + Automat pune pe pauză și reia atunci când este afișată. Nativ pregătește Steam pentru repaus mai întâi. Manual așteaptă Reluare. Niciodată continuă să ruleze. + Continuă descărcările + Întârzie suspendarea în fundal cât timp Steam descarcă activ. + Automat pune pe pauză în fundal și reia atunci când este afișată. Manual așteaptă Reluare. Niciodată continuă să ruleze. + Pornire + Pornește Steam odată cu DroidDeck + Deschide sesiunea Steam la lansarea DroidDeck. + Decky + Se verifică versiunile… + Pregătit pentru instalarea %1$s. + Nu s-a găsit o versiune compatibilă. Încearcă din nou mai târziu. + Instalat · %1$s + Actualizare disponibilă · %1$s + Actualizat · %1$s + Loader + Instalează cea mai recentă versiune + Pornește odată cu Steam. Cât timp este activ, alte aplicații de pe acest dispozitiv pot fi capabile să controleze Steam. + Dezactivat. Portul de depanare la distanță al Steam rămâne închis. + Dezinstalezi Decky Loader? + Elimină loader-ul. Pluginurile și setările tale sunt păstrate. + Client + Suprapunere de performanță + Dezactivează dacă jocurile se blochează sau ecranul devine negru în modul Deck. Se aplică la sesiunea următoare. + Repară clientul Steam + Descarcă din nou clientul Steam la următoarea pornire a Steam. Folosește-l dacă Steam rămâne blocat pe ecranul de încărcare. Jocurile, autentificarea și setările sunt păstrate. + Clientul Steam va fi descărcat din nou la următoarea pornire a Steam. + Repară + Grafică + Cheie API SteamGridDB + Cheia ta + Editează… + Completează grafica lipsă din Steam, apoi din SteamGridDB. Propriile tale cover.jpg, hero.jpg, logo.png sau icon.png din folderul jocului au prioritate. + Elimină %1$s + Elimini? + Adaugă joc + Adaugă un joc: + Alege fișierul exe: + Această locație nu este disponibilă în sesiune + Verifică acest exe + Componente Windows + LB / RB trec între secțiuni. + Niciuna + DLL-uri Windows pe care Proton nu le include, din lista de componente. Activează-le pe cele de care are nevoie acest joc: se adaugă în folderul Windows propriu al jocului, niciodată în fișierele sale, și rămân indiferent pe ce Proton rulează. + Lista de componente nu a putut fi accesată. Componentele deja descărcate pot fi în continuare comutate. + Recomandate pentru acest joc + Se verifică de ce are nevoie acest joc… + Disponibil când ești online + + Se activează %1$d componentă… + Se activează %1$d componente… + Se activează %1$d de componente… + + Activ pentru acest joc + Steam instalează %1$s cu %2$s (aplicația %3$s) + Din lista GOG pentru acest joc + Din cerințele preliminare Epic pentru acest joc + Din pașii de instalare Amazon pentru acest joc + Steam instalează %1$s cu aplicația %2$s, potrivită după nume + De asemenea, listate + Listată de %1$s; nu este o componentă aici + Steam + GOG + Epic Games + Amazon Games + Jocul include %1$s + Jocul are %1$s (funcționează de obicei ca atare) + Deja prezentă în acest joc + Nimic din folderul jocului nu indică o componentă. + Instalate pentru acest joc (%1$d) + Toate componentele (%1$d) + Necesită un program de instalare Windows (%1$d) + Acestea sunt propriile lor programe de instalare, fără pachet în interior de despachetat, și fără o înregistrare a ceea ce instalează încă. Majoritatea sunt medii de execuție vechi pe care Mono din Proton le înlocuiește deja; înregistrările sunt adăugate pe măsură ce sunt create. + Arată + Ascunde + Rulează propriul program de instalare (fără înregistrare încă) + Neacceptat încă aici + Se încarcă componentele… + Nu s-a putut instala: %1$s + Descărcat + Se descarcă %1$s + Se instalează: %1$s + Se dezactivează… + Descărcare de %1$s + Are efect la următoarea pornire a jocului. + Întinde jocurile 16:9 + Pe ecrane mai pătrate decât 16:9, întinde jocurile pentru a umple ecranul, în loc să arate benzi. Se aplică la sesiunea următoare. + Sunet + DirectAudio pentru jocuri + Evită PulseAudio pentru o latență mai mică. + Microfon + Pentru chat vocal. + Rețea + Descoperire Wi-Fi + Arată numele rețelelor Wi-Fi și rețelele din apropiere în Steam. + Serviciile de localizare sunt dezactivate. + Permisiunea de localizare nu a fost acordată. + Deschide setările + Continuă + Activezi descoperirea Wi-Fi? + Android necesită permisiunea de localizare pentru a citi numele rețelelor Wi-Fi și a descoperi rețelele din apropiere. DroidDeck folosește acest acces pentru a arăta acele rețele în Steam. Nu îți urmărim locația. Accesul la internet funcționează și fără această permisiune. + Stocarea jocurilor + Automat - cardul SD când este prezent, plus folderele tale Games + Automat - folderele tale Games, plus cardul SD când este prezent (niciunul acum) + Doar internă + Folder: %1$s + A doua bibliotecă + Jocurile care transmit fișiere de pe SD sau din stocarea partajată pot avea sacadări. + Diagnosticare stocare + Înregistrează rezultatele alocărilor și timpii de stocare eșantionați în Partajare jurnale. Poate încetini instalările cât este activă. Se aplică la sesiunea următoare. + Alege un folder… + Rezoluție personalizată + Înlocuiește limita și raportul. O dimensiune care nu se potrivește cu ecranul primește benzi. + Lățime + Înălțime + Între 320×240 și 3840×2160. + + %1$d joc + %1$d jocuri + %1$d de jocuri + + OK + Șterge + Restaurează + Descarcă + Șterge %1$s + %1$.1f MB + Driver pentru mediul de execuție + Folosit de Steam, jocuri și desktop. Se aplică la sesiunea următoare. + Importă arhiva Turnip… + Driver de afișaj + Desenează cadrele pe ecran în ambele moduri. Repornește aplicația pentru a aplica. + Importă o arhivă AdrenoTools… + niciodată + Versiunile nightly verificate %1$s + Componente + Despre Componente + Verifică drivere noi + Verifică versiunile nightly pentru pachete noi + Proton + Proton-ul ales aici rulează orice joc care nu și-a ales propriul în Steam, și o modificare făcută în Steam apare și aici. Steam înlocuiește Proton-ul Valve la actualizare; originalele sunt păstrate pentru fiecare versiune. + Configurare + Configurează profilul + Profil + Profil implicit + Folosit automat de jocurile fără suprascrieri. + Setări salvate, folosite automat la lansarea %1$s. + Alege doar setările de care are nevoie acest joc. Tot restul continuă să folosească profilul implicit. + Preset FEX + Se folosește presetul profilului implicit. + Preset personalizat salvat pentru acest joc. + Variabile FEX personalizate + Folosește implicit + Compatibilitatea jocului + Opțiuni de mediu Proton, DXVK, VKD3D și sincronizare pentru acest joc. + Se folosește Proton-ul profilului implicit. + Proton personalizat salvat pentru acest joc prin maparea de compatibilitate Steam. + Fără Proton + DXVK + VKD3D-Proton + Folosește pachetul profilului implicit din %1$s, dacă acest joc nu selectează alt pachet. + Folosește pachetul de componente al profilului implicit. + Folosește %1$s doar pentru acest joc. + Opțiuni pentru mediul de execuție + Alte opțiuni de mediu Proton, DXVK, VKD3D-Proton și sincronizare, per joc. + Profilul %1$s este salvat pentru următoarea lansare a jocului. + Pornește fără Steam + Pentru un joc care se închide imediat: îl pornește fără configurarea Steam din Proton. Funcțiile Steam, precum overlay-ul, sunt dezactivate pentru el. + Activat + Alegerea Proton pentru %1$s este salvată și disponibilă în Steam acum. + Alegerea Proton pentru %1$s este salvată. Repornește Steam înainte de a lansa jocul. + %1$s nu poate folosi Proton-ul selectat pe acest dispozitiv. + Profilul %1$s nu a putut fi încărcat: %2$s + Profilul jocului nu a putut fi salvat: %1$s + Resetează profilul jocului + Resetezi profilul %1$s? + Elimini suprascrierile Proton, FEX, pachet de componente, mediu, filtrare de texturi și componente Windows ale acestui joc? Va folosi profilul implicit. + %1$s folosește acum profilul implicit. + Profilul jocului nu a putut fi resetat complet: %1$s + %1$s rulează acum orice joc care nu și-a ales propriul Proton în Steam. + %1$s va rula orice joc care nu și-a ales propriul Proton, de la următoarea pornire a Steam. + %1$s nu poate porni jocuri aici, așa că nu poate fi implicit. + Proton-ul implicit nu a putut fi salvat: %1$s + %1$s · %2$d schimbate + %1$s · toate originale + %1$s · joc în execuție + %1$s · pachet droiddeck-esync + %1$s · niciun pachet droiddeck-esync încă + %1$s · esync integrat, nu necesită pachet + Drivere GPU + %1$s · reaplicat la pornirea %2$s + Următorul: %1$s · după ce jocul se închide + %1$s în uz: %2$s + Așteaptă până se închide jocul + Se aplică la următoarea pornire a unui joc + Importă .zip + Se citesc versiunile Proton… + Niciun Proton instalat încă. Pornește Steam o dată pentru a descărca Proton-ul său ARM64, sau adaugă GE-Proton, CachyOS sau DroidDeck-Proton din Configurare. + Original · %1$s + %1$s · această versiune + %1$s · versiune anterioară + Instalate · %1$d + Nimic salvat pentru %1$s încă. Fișierele proprii ale acestui Proton sunt salvate ca original al lui. + Un joc rulează pe acest Proton, așa că aceasta așteaptă până se închide. + Se aplică la următoarea pornire a unui joc. + Schimbi %1$s? + Pui %1$s în %2$s? Fișierele sale originale rămân salvate. %3$s + Restaurezi %1$s? + Pui originalul %1$s înapoi în %2$s? %3$s + Elimină pachetul din aplicație. Îl poți descărca din nou. + Ștergi acest original? + Acest original %1$s este dintr-o versiune anterioară a %2$s. Originalul versiunii curente este păstrat mereu. + Pe versiunile nightly · %1$d + Reîmprospătează pentru a lista pachetele de pe versiunile nightly. + Nimic nou de la ultima verificare. + Importă .wcp + Restaurează originalul acestui Proton + Pui originalul %1$s al %2$s înapoi? + Schimbă + FEX, DXVK și VKD3D-Proton pentru fiecare Proton cu care Steam rulează jocuri.\n\nSelectează un rând instalat pentru a-l pune în uz. Se aplică la următoarea pornire a unui joc, sau după ce jocul în execuție se închide.\n\nAlegerile tale sunt reverificate înainte de fiecare lansare și puse înapoi dacă ceva le-a schimbat. Fișierele originale ale fiecărui Proton sunt păstrate, așa că actualizările Steam nu le pierd niciodată. + Original + În uz + Nou + Salvat + Performanță + Nuclee CPU, remedieri de sesiune și numele hostului. Se aplică la sesiunea următoare. + Nuclee pentru clientul Steam + Suprascrie alegerea de nuclee a Steam + Fixează Steam, asistentul său de interfață și gamescope pe nucleele de mai jos. + Nuclee pentru client + Nuclee pentru Steam. + Activează suprascrierea pentru a alege. + Nuclee pentru joc + Nuclee pentru joc + Nuclee pentru jocuri. Cu toate selectate, Android le programează liber. + Interfața clientului + GL pe fire + Poate face meniurile Steam mai receptive. + Zink: descriptori leneși + Recomandat pentru drivere fără buffere de descriptori. + Omite verificările de erori GL + Dezactivează validarea GL pentru fiecare apel. + gamescope: coadă GPU în timp real + Prioritizează lucrul GPU al compositorului. Poate netezi ritmul cadrelor, cu un cost pentru jocuri. + GPU + Fixează GPU-ul la frecvența maximă + Doar Adreno. Mai puține întreruperi, consum mai mare de baterie și căldură. + Remedieri de sesiune + Turnip: randare sysmem + Mai rapidă pentru Steam și majoritatea jocurilor. Necesară pe Adreno 710/720/722. + Omite asistentul xalia al Steam + droiddeck-ntsync (experimental) + Un substitut la nivel de utilizator pentru driverul Linux ntsync, pentru testare. Cât timp este activ, Proton îl folosește în locul droiddeck-esync și droiddeck-fsync. Funcționează cu Proton Experimental ARM64, GE-Proton și proton-cachyos. Se aplică la sesiunea următoare. + droiddeck-esync + droiddeck-fsync primul + Rulează propriul fsync al Proton pe fișierele Proton standard, cu futex_waitv furnizat la nivel de utilizator, deci nu are nevoie de pachet și continuă să funcționeze imediat ce Steam actualizează Proton. Cât timp este activ, jocurile îl folosesc înaintea droiddeck-esync, cu excepția celor puține pe care Proton le ține în afara fsync. Se aplică la sesiunea următoare. + Firele Windows ale jocurilor se așteaptă unele pe altele prin eventfd și memorie partajată, în loc de drumuri dus-întors prin wineserver. Folosește un pachet droiddeck-esync creat pentru exact acea versiune de Proton; pachetele sunt obținute automat. Până când există unul pentru o versiune nouă de Proton, droiddeck-fsync îl înlocuiește. Se aplică la sesiunea următoare. + Dezactivează asistentul de navigare cu gamepad al Proton, care costă CPU în fiecare joc. + Rulează proot fără seccomp + Poate remedia erori de apel de sistem lipsă pe unele kernel-uri. Poate reduce performanța. + Cale rapidă proot + Necesită filtrul seccomp al proot. + Căutări de fișiere mai rapide și mai puține sacadări. Dezactivează dacă un program nu își găsește fișierele. + Identitatea sesiunii + Nume de host + Numele pe care Steam îl vede pentru acest dispozitiv. Gol restaurează %1$s. + Doar litere, cifre și liniuțe interioare, până la 63 de caractere. + Android este setat să închidă această sesiune + Instalat + Disponibil pentru descărcare + Nimic nou de la ultima verificare. + Restaurează driverele integrate + Integrat în aplicație: aceasta elimină fișierele sale despachetate și îl ascunde. Restaurează driverele integrate îl readuce. + Jumătate dintr-un pachet Android + Linux: ambele jumătăți sunt eliminate. Un driver în uz revine la cel implicit. + Fișierele sale sunt eliminate din aplicație. + Fișierele sale sunt eliminate. Este în uz, așa că cel implicit îi ia locul. + Afișaj + Efecte + Comenzi + Componente + Controller + Setări + Pagina %1$s + Steam + Desktop + Meniu Steam + Acces rapid + Deschide meniul de acces rapid + Pagina anterioară + Pagina următoare + HUD de performanță + Mod de scalare + Claritatea scalării + Efecte de ecran + Live. Costă puțin timp GPU cât timp un efect este activ. + Presetări + Personalizat + Avansat + Expandat + Restrâns + Claritate (CAS) + Nivel de claritate + HDR fals + Elimină benzile + Intensitatea eliminării benzilor + Luminozitate + Contrast + Gamma + Saturație + FXAA (netezirea marginilor) + Contururi toon + CRT + NTSC + Filtrare de texturi + Jocuri DirectX 9–11, de la următoarea lor lansare. + Filtrare anizotropă + Claritatea texturilor + O deviere mip negativă: texturi mai clare, cu puțin tremur. Automat se potrivește cu cât de mult este mărită sesiunea pentru ecran. + Automat (%1$s) + Tastatură + Tastatură de PC + Android + Al doilea ecran + Mod + Arată + Aplicații Android + Lansează o aplicație + Ascunde + Arată + Nicio aplicație care poate fi lansată + Comportamentul sesiunii + Se aplică acum și sesiunilor viitoare din acest mod. + Sesiunea următoare + Asistență + Jurnalele sesiunii + Atașează la un raport de eroare + Partajează jurnalele + Fundal + Oprește + Oprești sesiunea? + În execuție acum + Niciun joc nu rulează pe un Proton + Profilul jocului + Detaliile profilului nu sunt încă disponibile. + Profilurile jocurilor nu au putut fi încărcate. Se reîncearcă automat. + Profilurile jocurilor nu au putut fi reîmprospătate. Se arată ultimele date disponibile; se reîncearcă automat. + Vezi și editează setările salvate folosite la următoarea lansare. + Se folosește profilul implicit + Profil personalizat + Se folosește %1$s din profilul implicit. + Modificările sunt salvate în profilul %1$s și se aplică la următoarea lansare a jocului. + + %1$d suprascriere de joc + %1$d suprascrieri de joc + %1$d de suprascrieri de joc + + Niciun Proton nu este instalat încă. + joc în execuție + reaplicat la pornirea %1$s + Original + Importă și șterge pe pagina Componente. + Finalizat + Necesită atenție + Se procesează + Verifică adresa IP și portul Depanării wireless, sau asociază din nou dacă Android a eliminat acest dispozitiv. + Instalează + Gestionează + Prezentare generală + Controller + Sesiune + Launcher + Configurare + ● Totul este pregătit + %1$d din %2$d pregătite + Verificarea sistemului + Dispozitiv acceptat + GPU netestat + GPU neacceptat + %1$s · %2$s + %1$s: %2$s + Steam are nevoie de un driver Adreno; %1$s poate arăta un ecran negru + Mediul de execuție Linux + %1$s · %2$d%% + Neinstalat · circa 3 GB, instalat la prima apăsare pe Play + Instalat + %1$s · actualizare disponibilă + %1$s · actualizat + Limita proceselor-copil + Activă · Android poate închide Steam. Dezactivarea durează un minut. + Nesetată · Android poate închide Steam. Dezactivarea durează un minut. + Nu a putut fi verificată · dezactivarea durează un minut. + Remediază + Detalii + Opțiuni pentru dezvoltatori + Folosește Depanarea wireless + Verifică din nou + Activează limita + Cont Steam + Autentificat ca %1$s · mod offline + Autentificat ca %1$s + Apasă Play și autentifică-te în Steam + Instrumente + Controller + Setările controllerului nu sunt disponibile. + Sesiune + Jurnalele sesiunii + Salvate după fiecare sesiune + Instrumente de depanare + Cele mai recente jurnale de sesiune + Jurnale de sesiune salvate + Păstrează cele mai recente %1$d + Șterge jurnalele + Mod offline + Autentifică-te mai întâi în Steam + Launcher + Limbă + Steam o folosește și el, de la următoarea pornire. + Cea implicită a sistemului (%1$s) + Cea implicită a sistemului + Scala aplicației + Textul și comenzile DroidDeck + %1$d%% (Implicit) + Temă + Aspectul launcherului + Folosește ca ecran Acasă + DroidDeck poate fi aplicația Acasă + DroidDeck nu este niciodată oferit ca aplicație Acasă + Orientare + Peisaj automat + Peisaj + Peisaj inversat + Ecran complet + Ascunde barele de stare și de navigare Android + Arată barele de stare și de navigare Android + Aplicația Acasă implicită + Alege o aplicație Acasă + Alege + Aplicații Linux (beta) + Fișiere + Răsfoiește și gestionează fișiere + Versiuni Proton + Instalează versiuni Proton ARM64 + Performanță + Alocarea nucleelor CPU + Folderul ROMs + Unde sunt stocate jocurile emulatorului + Componente + FEX, DXVK, VKD3D + Comenzi + Maparea butoanelor + Descoperă + Căutare + Instalate + %1$s · %2$d + Magazin + Flathub · ARM64 + ● Flatpak pregătit + Instalează mai întâi mediul de execuție Linux din Configurare. + Configurează Flatpak + Adaugă Flatpak și îl conectează la Flathub (circa 20 MB). Prima aplicație descarcă și mediul său de execuție. + Se configurează… + Flatpak + Actualizări + %1$s · %2$s + %1$s · %2$s · %3$d%% + Se pornește… + Categorii + Flathub nu a putut fi accesat. Verifică conexiunea și încearcă din nou. + Încearcă din nou + Caută pe Flathub + Firefox, jocuri, emulatoare… + Caută + Toate + Se caută… + Caută după nume sau alege o categorie. Sunt listate doar aplicații ARM64. + Nicio aplicație ARM64 de pe Flathub nu se potrivește. + Rezultate + Verifică actualizările + Actualizează tot (%1$d) + ● Totul este actualizat + Nicio aplicație încă. Găsește câteva la Descoperă sau Căutare. + Configurează Flatpak pentru a instala aplicații. + Actualizare disponibilă · %1$s + Deschide + Detalii + Elimină + Apasă din nou pentru a elimina + Se instalează… + Instalează + Configurează mai întâi Flatpak + Descărcare de %1$s + ● Instalat + Pagina Flathub a acestei aplicații nu a putut fi încărcată. + Se încarcă de pe Flathub… + Nicio versiune ARM64 pe Flathub, deci nu poate fi instalată aici. + Capturi de ecran + Despre + ID aplicație + Licență + Mediu de execuție + Dimensiune instalată + %1$s (plus mediul de execuție partajat) + Site web + Verificat + Actualizări + DroidDeck %1$s · %2$s + Verificat %1$s + Neverificat încă + Permite DroidDeck să se actualizeze singur + Pe ecranul următor, activează Permite din această sursă, apoi revino. Actualizarea continuă de la sine. + Deschide setările + Nu acum + Semnătură diferită + Nu se poate actualiza pe loc + Această copie nu este semnată cu cheia de lansare a DroidDeck, așa că actualizările nu se pot instala peste ea. Dezinstaleaz-o pentru a comuta. + Actualizare disponibilă + Nu se poate instala + Pregătit pentru comutare + Înaintea versiunii Stabile + Ești înaintea versiunii Stabile + Această versiune este mai nouă decât cea mai recentă Stabilă. Vei trece la Stabilă odată cu următoarea sa lansare. + Testul s-a terminat + Acest test s-a terminat + Remedierea sa a fost integrată sau abandonată. Urmărește Preview pentru cele mai noi remedieri. + Actualizat + Ai cea mai recentă versiune %1$s + · %1$s + %1$s… %2$d%% + %1$s… + Actualizează%1$s + Instalează%1$s + Instalează Stabilă %1$s oricum + Urmărește Preview + Oprește sesiunea în curs pentru a actualiza. + Mai puțin + Mai mult + DroidDeck %1$s + Versiune nouă Preview + Versiune de test PR #%1$d + %1$d MB + CANAL DE ACTUALIZARE + Stabilă + Versiuni testate, pentru majoritatea oamenilor + %1$s · %2$s + Preview + Cele mai noi remedieri, înainte de Stabilă + Actualizat + Versiuni de test + Încearcă o remediere înainte de lansare + Niciuna acum + PR #%1$d + Nu este compilată pentru această copie de DroidDeck + Actualizat %1$s + testul PR #%1$d + cu un timp în urmă + chiar acum + + %1$d de încercat + %1$d de încercat + %1$d de încercat + + + Modificări de la versiunea ta · %1$d compilare în urmă + Modificări de la versiunea ta · %1$d compilări în urmă + Modificări de la versiunea ta · %1$d de compilări în urmă + + + Modificări recente · %1$d compilare + Modificări recente · %1$d compilări + Modificări recente · %1$d de compilări + + O setare Android de schimbat + Steam + Android oprește aplicațiile care pornesc multe procese în fundal, iar Steam pornește zeci. Până se schimbă asta, jocurile se închid fără o eroare. + Repară pentru mine + Activează „Dezactivează restricțiile pentru procesele-copil”, apoi revino. Lasă Opțiunile pentru dezvoltatori activate, altfel comutatorul se dezactivează din nou. + Deschide Opțiunile pentru dezvoltatori + Această versiune de Android nu are un comutator pentru asta, așa că DroidDeck o schimbă prin Depanarea wireless. Durează circa un minut, fără a avea nevoie de un computer. + Depanarea wireless are nevoie de Wi-Fi. Orice rețea funcționează; nu are nevoie de internet. + Codul de asociere apare într-o notificare. Permite notificările pentru DroidDeck, sau folosește Alte moduri. + Permite notificările + Deschide Setări → Opțiuni pentru dezvoltatori → Depanare wireless și activează-o + Apasă „Asociază dispozitivul cu un cod de asociere” + Introdu codul în notificarea DroidDeck + Se așteaptă fereastra de asociere… + Fereastra a fost găsită. Introdu codul său în notificare. + Stare + Se verifică la fiecare 2 secunde. Această pagină se închide când este dezactivată. + Alte moduri + Depanare wireless, adresă manuală, ADB pe computer + Adresă manuală, ADB pe computer, Opțiuni pentru dezvoltatori + Folosește Depanarea wireless în schimb + Unele ROM-uri adaugă comutatorul oricum. Caută „procese-copil” în Opțiuni pentru dezvoltatori. + Dacă notificarea nu funcționează, introdu tu adresa și codul din fereastră. + Introdu adresa manual + De pe un computer cu ADB: + Copiază comanda + Alege pe care afișaj se deschid Setările Android. + Ecranul principal + Asociat. Se caută portul de Depanare wireless… + Asociat. Introdu portul din Setări. + Dispozitiv găsit. Se aplică setarea… + Nu s-a putut aplica automat. Verifică adresa de mai jos și încearcă din nou. %1$s + Depanare wireless + Limita proceselor-copil a fost actualizată + Schimbă limita proceselor-copil + Dacă opțiunea lipsește, asociază-te prin Depanare wireless. + Asociat. Verifică adresa IP și portul în Depanare wireless. + Limita proceselor-copil a fost actualizată. + Folosește pasul 2 dacă Opțiunile pentru dezvoltatori nu au o setare pentru procese-copil. Deschide Opțiuni pentru dezvoltatori → Depanare wireless → Asociază dispozitivul cu un cod de asociere. + Asociat. Folosește adresa de mai jos, sau actualizeaz-o din Depanare wireless → Adresă IP și port. + Limita proceselor-copil a fost actualizată prin Depanarea wireless. + Android a confirmat că limita este activă. + Android a confirmat că limita este dezactivată. + 2 · Asociază Depanarea wireless + Adresa IP și portul ferestrei de asociere + Cod de asociere (PIN) + 6 cifre + Asociază + 2 · Aplică prin Depanarea wireless + Adresa IP și portul de conexiune + Asociază din nou + Dezactivează limita + Alte moduri de a o schimba + 1 · Opțiuni pentru dezvoltatori (încearcă mai întâi) + Activează „Restricționează procesele-copil”. + Dezactivează „Restricționează procesele-copil”. + 3 · ADB pe computer (ultima soluție) + Doar dacă Depanarea wireless nu este disponibilă. + 3 · ADB pe computer (ultima soluție) + Când apar comenzile tactile într-o sesiune + Culoare + Tenta butoanelor de pe ecran + Personalizat + Opacitate + %1$d%% + Dimensiunea butoanelor + 100% este dimensiunea standard + Apăsarea manșei + Apasă de două ori și ține apăsată o manșă pentru L3 sau R3 + Manșe adaptive + Vibrații de joc + Butonul Steam + Butonul QAM + Butonul tastaturii + Apasă Select pentru a deschide meniul Steam + În loc de View/Select. + Animații + Manșele apar unde atingi ecranul și se ascund la eliberare + Aranjament + Poziții personalizate salvate + Adaptat pentru acest ecran + Editează + Resetează + Maparea butoanelor + Fiecare buton trimite propria intrare + %1$d din %2$d butoane remapate + Configurează + Resetează controllerul + Restaurează toate setările controllerului și aranjamentul + Ce trimite jocului fiecare buton de pe ecran. Ascuns elimină butonul. + Butoane pe ecran + Valori implicite + Resetează maparea + Aplicații Android + %1$d aplicații + Nu s-a găsit nicio aplicație Android care poate fi lansată. + ● Desktop instalat + Se instalează la prima deschidere + Disponibil pentru instalare + Neinstalat. + %1$s · neinstalat + Deschide %1$s + Jocuri + Alege un folder ROMs. + Navighează la /root/ROMs în RetroArch. + Adaugă jocuri %1$s în ROMs/%2$s. + Instalează %1$s + Oprește sesiunea pentru a instala + Mediu de execuție necesar + Se încarcă detaliile instalării… + Detaliile instalării nu sunt disponibile. Încearcă din nou mai târziu. + Instalează mai întâi mediul de execuție Linux din Configurare. + Oprește sesiunea în curs pentru a instala aplicații desktop. + Instalează %1$s în desktopul Linux. + %1$s · %2$d%% + Ascunderea o elimină din Desktop. Fișierele sale rămân în mediul de execuție Linux. + Acel joc nu mai este în folderul ROMs. + Desktop · %1$s + Lansează în %1$s + De asemenea în %1$s + Acea pagină nu mai există. Alege o secțiune din stânga. + Deschide desktopul + Instalează și deschide desktopul + Dezinstalează desktopul + Elimină desktopul și aplicațiile sale incluse. Păstrează jocurile, aplicațiile adăugate, setările și bibliotecile partajate. + Se elimină desktopul + Desktop eliminat + Se elimină fișierele LXQt vechi + Actualizează DroidDeck înainte de a elimina această versiune de desktop. + Desktop Linux + KDE Plasma, Firefox și emulatoarele tale + Jocurile pe care le instalezi în Steam apar aici după sesiune. + Joacă Steam + Setări de lansare + Lansează + Fișierele jocului + Prefix Proton + Se caută salvările… + Niciun folder de salvare găsit după nume. Exportul include tot folderul de utilizator. + %1$d foldere de salvare + %1$s · %2$d fișiere · %3$s + %1$d KB + Gestionează salvările + Salvările jocului + Importă salvări… + Exportă ca arhivă GameHub… + Exportă ca arhivă Winlator… + Deschide folderul de salvare + Niciodată jucat + Mediu de execuție ocupat · %1$d%% + Mediu de execuție ocupat + Mediul de execuție se instalează la prima apăsare pe Play + Actualizare disponibilă pentru mediul de execuție + ● Mediu de execuție pregătit + Jucat ultima dată %1$s + Stocare internă + Joc adăugat + Joc personalizat + Stocarea jocurilor + Cea mai bună pereche pentru acest GPU, păstrată actualizată + Alege o pereche tu însuți + Se verifică driverele… + Niciun driver listat încă. Reîmprospătează pentru a le căuta. + Ascunde driverele pentru alte GPU-uri + Arată driverele pentru alte GPU-uri + Avansat + Setat împreună cu driverul de afișaj: %1$s include ambele. + Setat împreună cu driverul mediului de execuție: %1$s include ambele. + Folosit de jocuri și de desktop. + Implicit pentru mediul de execuție + Desenează cadrele pe ecran. Se aplică după repornirea DroidDeck. + Automat - ales în funcție de GPU + Descarcă, importă sau șterge… + ÎN UZ + Driverele integrate în DroidDeck + Driverele mediului de execuție și de afișaj setate împreună ca pereche asortată. + Recomandat pentru acest GPU: %1$s. + Reîmprospătează pentru a căuta drivere pentru acest GPU. + RECOMANDAT + Indisponibil + Mediul de execuție Linux + Se pregătește mediul de execuție Linux + Sesiune + Se pornesc afișajul și sunetul + Clientul Steam + Se verifică Steam și instrumentele sale de compatibilitate + Se deschide Steam + Se așteaptă primul cadru al Steam + Desktop + Se pregătește desktopul + Se deschide + Se așteaptă primul cadru + Se pornește Steam + Sesiunea s-a terminat + Pasul %1$d din %2$d + Steam este pus pe pauză + Mânere + Padul stâng + Padul drept + Ambele + Gamepad + Manșă + Trackpad + Comută între manșă și trackpad + Mai multe comenzi Deck + PANOURI + Închide comenzile celui de-al doilea ecran + Deschide meniul Steam + Stânga · sus + Stânga · jos + Dreapta · sus + Dreapta · jos + Clic + Trackpad-ul drept + Trackpad-ul stâng + Trackpad + Arată tastatura + Ascunde tastatura + Terminal Linux + Sesiunea Linux nu este încă pregătită pentru un terminal. + Steam + Jocuri + Desktop + Magazin + Componente + Aplicații + Configurare + Actualizări + %1$d%% + Reluare %1$s + Apare ca /root/ROMs în fiecare sesiune. Stocarea internă este la /root/Storage. + Niciun folder ales + Se aplică la sesiunea următoare. Cardurile SD funcționează. + Alege folderul + Schimbă folderul + Instalează o versiune Proton ARM64, apoi alege-o per joc în Steam → Proprietăți → Compatibilitate. + Oprește sesiunea în curs pentru a instala sau elimina un instrument de compatibilitate. + Versiuni disponibile + Instalat %1$s + În așteptare de la o cerere anterioară + Neinstalat + Actualizare disponibilă → %1$s + Elimină versiunile vechi de %1$s + Elimină %1$s. Jocurile setate pe aceste versiuni trebuie comutate pe alt Proton. + Jocurile setate pe această versiune trebuie comutate pe alt Proton. + Instalează acum + Anulează cererea în așteptare + Tastatura Android + Ascunde ✕ + Lansează %1$s + Alege un ecran. Secundar: %1$s. + Ecranul secundar nu mai este disponibil. + Ecranul principal + Ecranul secundar + Fila anterioară + Fila următoare + Activat + Toate %1$d + Niciuna + %1$d din %2$d + Dezactivat + Ecranul de jos + Se aplică la următoarea lansare a jocului. Profilul unui joc are prioritate față de cel comun. + Alt joc după ID-ul aplicației… + Terminat + Implicit + Dezactivat + Schimbă… + Schimbă variabila + Înapoi la implicit + Jocuri Proton + Imagine în imagine + Intră automat în PiP + Comenzi pe ecranul desfăcut + Desfăcut, jocul se află peste trackpadurile și mânerele din spate ale Steam Deck. Împăturit, jocul este pe tot ecranul. + Suspendat + Imaginea în imagine este disponibilă odată ce sesiunea este pregătită. + Permite imaginea în imagine pentru DroidDeck în setările Android. + Android nu a putut deschide imaginea în imagine. + Comandă rapidă + Adaugă pe ecranul principal + Alege jocul + Se scanează jocurile instalate… + Copiază linkul de lansare + Link de lansare copiat + Acest launcher nu poate adăuga comenzi rapide. Folosește Copiază linkul de lansare în schimb. + Link de lansare a jocului invalid. + Acest joc nu mai este în biblioteca ta DroidDeck. + Oprește sesiunea curentă înainte de a lansa un joc Steam. + Jocul nu a putut fi trimis către Steam. Încearcă să îl lansezi din nou. + Reîncearcă eliminarea + Se elimină + Eliminare incompletă + Exportă fișierul jocului + Sincronizează toate fișierele jocurilor în folder + Oprește sincronizarea fișierelor jocurilor + Alege un folder pentru fișierele frontend ale jocurilor + Fișierele jocului salvate în %1$s + Fișiere frontend + + Așteaptă să se termine operația asupra mediului de execuție + Așteaptă să se termine instalarea instrumentului de compatibilitate + Așteaptă să se termine instalarea aplicației desktop + Așteaptă să se termine instalarea înainte de a lansa un joc + Închide mai întâi sesiunea Steam, ca jocul să nu poată salva peste import + Se importă în %1$s + + S-a importat %1$d fișier din %2$s în %3$s + S-au importat %1$d fișiere din %2$s în %3$s + S-au importat %1$d de fișiere din %2$s în %3$s + + + S-a importat %1$d fișier din %2$s în %3$s. Salvările vechi au fost copiate de rezervă în Download/DroidDeck/Saves/backups + S-au importat %1$d fișiere din %2$s în %3$s. Salvările vechi au fost copiate de rezervă în Download/DroidDeck/Saves/backups + S-au importat %1$d de fișiere din %2$s în %3$s. Salvările vechi au fost copiate de rezervă în Download/DroidDeck/Saves/backups + + Alege o arhivă de salvare pentru %1$s + Se exportă %1$s + + S-a exportat %1$d fișier ca %2$s: %3$s + S-au exportat %1$d fișiere ca %2$s: %3$s + S-au exportat %1$d de fișiere ca %2$s: %3$s + + Alege unde se salvează %1$s (%2$s) + %1$s a eșuat: %2$s + Niciun jurnal de sesiune încă: rulează mai întâi o sesiune. + Niciun jurnal de sesiune de șters. + + S-a șters %1$d folder de jurnal de sesiune. + S-au șters %1$d foldere de jurnal de sesiune. + S-au șters %1$d de foldere de jurnal de sesiune. + + Asocierea Depanării wireless a eșuat + Comanda Depanării wireless a eșuat + Depanarea wireless nu este disponibilă + Comanda ADB copiată + Alege folderul ROMs + Nu este un GPU Adreno + Turnip acceptă GPU-uri Adreno. Pe %1$s, Steam poate arăta un ecran negru. Descărcare: %2$.0f MB. + Instalează oricum + Elimină mediul de execuție Linux + Aceasta șterge mediul de execuție, clientul Steam din interiorul lui și fiecare joc instalat acolo. + Aplicat: %1$s + %1$s nu a putut fi deschis pe ecranul principal + %1$s nu a putut fi deschis pe %2$s + %1$s nu a putut fi deschis pe afișajul %2$d + Se importă + Se comută + Se restaurează + Se anulează + Se șterge + fișierul nu poate fi citit + Importat %1$s + Comutarea în așteptare a fost anulată. + Alege un pachet de componente (-linux .wcp) + Versiunile nightly nu au putut fi accesate + Salvat %1$s + Descărcarea a eșuat: %1$s + Alege un driver pentru mediul de execuție Linux (arhivă -Linux sau pachet Android + Linux) + Alege un driver de afișaj (arhivă AdrenoTools sau pachet Android + Linux) + Alege o arhivă de driver sau un pachet Android + Linux + %1$s: %2$s + Se elimină %1$s… + Se elimină din memorie %1$s… + Alege folderul de stocare a jocurilor + Alege o arhivă ZIP cu un plugin Decky + Nu poate fi folosit: %1$s + Program + Se elimină mediul de execuție Linux + Setările nu au putut fi deschise pe ecranul de jos; au fost deschise pe ecranul principal. + Deschide Opțiunile pentru dezvoltatori din Setări + + Setarea Android „Instalează aplicații necunoscute” nu a putut fi deschisă + Nu s-a putut verifica: %1$s + Această versiune nu are o descărcare pentru această copie de DroidDeck + Oprește mai întâi sesiunea în curs, apoi actualizează + Se descarcă + Se instalează + Se așteaptă confirmarea ta + Actualizarea a eșuat + Android nu a oferit instalarea actualizării + + Instalarea Decky a eșuat: %1$s + Se pornește importul pluginului… + Importul pluginului a eșuat: %1$s + Plugin instalat. Repornește Steam pentru a-l încărca. + + Neverificat încă - apasă reîmprospătare pentru a căuta drivere noi + Android + Linux: setează driverul de afișaj odată cu el + Android + Linux: setează driverul mediului de execuție odată cu el + Turnip-ul integrat în mediul de execuție + Vulkan de sistem: nicio versiune inclusă pentru acest GPU + %1$s (inclus) + Manual + %1$s · %2$s + Doar o jumătate este publicată acum + %1$.0f MB pentru ambele + Niciun driver de setat: %1$s + Se verifică cele mai noi drivere… + Actualizat (nu s-a putut verifica: %1$s) + Driverele recomandate nu sunt publicate acum + Se descarcă %1$s %2$s… + Actualizat la %1$s %2$s. + Actualizat la %1$s %2$s. Driverul de afișaj se aplică după repornirea DroidDeck. + Comutat la %1$s %2$s. + Comutat la %1$s %2$s. Driverul de afișaj se aplică după repornirea DroidDeck. + Se folosește %1$s %2$s. + Se folosește %1$s %2$s. Driverul de afișaj se aplică după repornirea DroidDeck. + Nu este o arhivă de driver: nici driver AdrenoTools, nici Turnip -Linux, nici pachet Android + Linux + Importul a eșuat: %1$s + Driver importat + Se folosește %1$s ca driver pentru mediul de execuție și pentru afișaj, deopotrivă. + Se folosește %1$s ca driver pentru mediul de execuție și pentru afișaj, deopotrivă. Driverul de afișaj se aplică după repornirea DroidDeck. + Șters %1$s (ambele drivere) + Șters %1$s + Cele mai recente: %1$s · verificat %2$s + Cele mai recente: %1$s · %2$s inaccesibil · verificat %3$s + + acum %1$d min + acum %1$d min + acum %1$d min + + + acum %1$d h + acum %1$d h + acum %1$d h + + + acum %1$d zi + acum %1$d zile + acum %1$d de zile + + Se verifică Banners-Turnip, WinNative și DroidDeck… + verificare eșuată + Instalat %1$s - alege-l în orice listă pentru a seta ambele drivere + Instalat %1$s - alege-l în meniu + + Așteaptă să se termine eliminarea mediului de execuție + Joc + touchpad + direct + Comutarea a eșuat: %1$s + Salvarea profilului a eșuat: %1$s + Desktopul este pus pe pauză + %1$s este pus pe pauză + Programul este pus pe pauză + Se pornește desktopul + Se pornește %1$s + Se pornește programul + Niciun jurnal pentru această sesiune. + Arhiva jurnalelor sesiunii nu a putut fi creată. + Jurnalele sesiunii nu au putut fi partajate. + Se pornește sesiunea… + se descarcă mediul de execuție Linux + se descarcă mediul de execuție Linux · %1$d din %2$d MB + se verifică mediul de execuție Linux + se despachetează mediul de execuție Linux + se descarcă desktopul + se descarcă desktopul · %1$d din %2$d MB + se verifică desktopul + se despachetează desktopul + se descarcă Proton Experimental (ARM64) + se descarcă Proton Experimental (ARM64) · %1$d din %2$d MB + se verifică Proton Experimental (ARM64) + se despachetează Proton Experimental (ARM64) + Catalogul mediului de execuție nu a putut fi accesat. Verifică conexiunea și apasă din nou Play. + Mediul de execuție Linux nu s-a instalat. Verifică mediul de execuție în Configurare și apasă din nou Play. + Desktopul nu s-a instalat (%1$s). Verifică conexiunea și apasă din nou Desktop. + %1$s s-a oprit neașteptat. Partajează jurnalele cu un raport de eroare, sau încearcă din nou. + Programul s-a oprit neașteptat. Partajează jurnalele cu un raport de eroare, sau încearcă din nou. + Desktopul s-a oprit neașteptat. Partajează jurnalele cu un raport de eroare, sau încearcă din nou. + Stare de ieșire %1$d · %2$s + niciun jurnal + + Jurnalul arată %1$d eroare „Function not implemented”: accelerarea seccomp a proot nu reușește să ruleze un auxiliar pe acest dispozitiv. Încearcă %2$s → „%3$s” (sau „%4$s”) și pornește din nou. + Jurnalul arată %1$d erori „Function not implemented”: accelerarea seccomp a proot nu reușește să ruleze un auxiliar pe acest dispozitiv. Încearcă %2$s → „%3$s” (sau „%4$s”) și pornește din nou. + Jurnalul arată %1$d de erori „Function not implemented”: accelerarea seccomp a proot nu reușește să ruleze un auxiliar pe acest dispozitiv. Încearcă %2$s → „%3$s” (sau „%4$s”) și pornește din nou. + + + Pluginuri + Instalează o arhivă ZIP cu un plugin pentru sesiunea Steam următoare. + Se descarcă… + Instalează din ZIP + Jocuri + Audio & video + Grafică + Internet + Birou + Dezvoltare + Educație + Știință + Sistem + Utilitare + Populare + În tendințe + Noi + Actualizate + se încarcă… + Configurare Flatpak + %1$s: %2$s + Nu s-au putut verifica actualizările pe Flathub + instalat + folder + Grafit + Suprafețe grafit, cu albastrul propriu al orbului din logo pentru tot ce este selectat, focalizat sau apăsat. + Hârtie pe negru + Albul este principal, albastrul iconiței este semnalul pentru linii, cercuri și puncte. + Albastru iconiță + Fundal negru, text alb, albastrul pentru tot ce este selectat, focalizat sau apăsat. + Bleumarin electric + Albastrul mai strălucitor, pe suprafețe care tind spre bleumarin în loc de gri. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Instalează mai întâi mediul de execuție + Biblioteca Steam nu este disponibilă + Biblioteca SD anterioară nu este disponibilă + Stocarea jocurilor nu este disponibilă + Folderul de jocuri adăugate nu este disponibil + Manifestele Steam nu sunt disponibile + Multe sisteme + Internă + Sistem Linux + Card SD + Stocare + Selectează + Proprietăți + Adaugă la Favorite + Elimină din Favorite + Redenumește + Copiază + Taie + Acțiuni + Favorite + Fixează folderul curent + Niciun favorit încă - fixează un folder din meniul său ⋮ pentru a reveni rapid aici. + Elimină din favorite + 0 bytes + bytes + Locație + Tip + Dimensiune + Modificat + Folder + Fișier + Fișier %1$s + Doar pentru citire + Oprește jocurile (și Wine) de la a suprascrie sau șterge acest fișier. + Nu s-a putut schimba Doar pentru citire + Ascuns + Această stocare nu poate salva indicatorul ascuns. + Marchează fișierul ca ascuns în Windows (atributul hidden al Wine). + Nu s-a putut schimba Ascuns pe această stocare + Terminat + Se șterge... + Ștergerea a eșuat + Se mută... + Se copiază... + Se mută - %1$s + Se copiază - %1$s + Se mută %1$d/%2$d - %3$s + Se copiază %1$d/%2$d - %3$s + %1$d finalizate, %2$d eșuate + %1$d finalizate, %2$d omise + + %1$d element mutat + %1$d elemente mutate + %1$d de elemente mutate + + + %1$d element copiat + %1$d elemente copiate + %1$d de elemente copiate + + „%1$s” deja există + Se redenumește... + Redenumirea a eșuat + Se creează folderul... + Folderul nu a putut fi creat + Folder nou + Nume folder + Creează + Nume nou + Ștergi? + Ștergi definitiv „%1$s”? + + Ștergi %1$d element? + Ștergi %1$d elemente? + Ștergi %1$d de elemente? + + Această acțiune nu poate fi anulată. + + …și încă %1$d + …și încă %1$d + …și încă %1$d + + Se șterge %1$d/%2$d - %3$s + + %1$d nu a putut fi șters + %1$d nu au putut fi șterse + %1$d nu au putut fi șterse + + Îmbinare adaugă și înlocuiește fișiere în folderul existent. + Suprascrie înlocuiește fișierul existent. + Aplică la toate conflictele + Îmbinare + Suprascrie + Păstrează ambele + Omite + Selectează acest folder + Înapoi + %1$s este montat, dar nu poate fi citit acum + Vizualizare listă + Vizualizare grilă + Căutare + Sortează + Nume + Data modificării + Rânduri confortabile + Rânduri compacte + Ascunde fișierele ascunse + Arată fișierele ascunse + Ascunde favoritele + Arată favoritele + Filtrează acest folder + %1$s liber + %1$d selectate + Niciunul + Toate + Lipește + + %1$d element + %1$d elemente + %1$d de elemente + + Lipește %1$s aici + Lipește %1$s (mutare) aici + Fișiere + STOCARE + RAPID + FAVORITE + Descărcări + Descărc + ROMs + Jurnale de sesiune + Imagini + „%1$s” a fost adăugat la Favorite + „%1$s” a fost eliminat din Favorite + Director gol + Un nivel mai sus + Alege un folder + Alege un fișier + Albastru Steam + Cer + Alb + Violet + Verde + Amber + Roșu + Roz + Butonul A + Butonul B + Butonul X + Butonul Y + Bumperul stâng + Bumperul drept + Trigger-ul stâng + Trigger-ul drept + Butonul View + Butonul Menu + View + Menu + D-pad sus + D-pad jos + D-pad stânga + D-pad dreapta + Ascuns + Trage pentru a mișca + Aliniere + Salvează + Niciunul + Tastatură + trackpad + Terminal + Mânere + trackpaduri Deck + + Se elimină mediul de execuție Linux + + Se elimină mediul de execuție Linux · %1$d intrare + Se elimină mediul de execuție Linux · %1$d intrări + Se elimină mediul de execuție Linux · %1$d de intrări + + Mediul de execuție Linux eliminat + Eliminarea mediului de execuție Linux nu a putut fi terminată. Reîncearcă eliminarea din Configurare. + Se elimină resturile mediului de execuție + Se descarcă + Se verifică + Se extrage + Apare cât timp mediul de execuție Linux este instalat sau eliminat + Se instalează mediul de execuție Linux + Se elimină mediul de execuție Linux + + Mediul de execuție Linux nu este instalat + Catalogul nu are o sumă de control pentru %1$s + Sumă de control incorectă - nimic nu a fost modificat + Se instalează %1$s + AppImage-ul nu a putut fi plasat + Extragerea a eșuat + Instalarea a eșuat + + Configurarea Flatpak a eșuat (cod de ieșire %1$d) + Configurarea Flatpak nu s-a terminat + O altă sarcină a magazinului este în curs + Configurează mai întâi Flatpak + Se pornește + Flatpak s-a închis cu starea %1$d + Se instalează %1$s + Se actualizează %1$s + Se elimină %1$s + Se instalează %1$s (%2$d/%3$d) + Se actualizează %1$s (%2$d/%3$d) + Se elimină %1$s (%2$d/%3$d) + traduceri + drivere grafice + codecuri media + mediul de execuție %1$s + mediul de execuție %1$s %2$s + + Fișierul este prea mic pentru a fi un AppImage + Fișierul nu poate fi citit: %1$s + Acesta nu este un AppImage (nu este un program Linux) + Acest AppImage nu este compilat nici pentru ARM64 (aarch64), nici pentru PC-uri x86 + Acesta este un program Linux, dar nu este un AppImage + Doar AppImage-urile de tip 2 pot fi importate (acesta este de tip %1$d) + Spațiu liber insuficient: importul necesită circa %1$s + Spațiu liber insuficient: actualizarea necesită circa %1$s + Importul a eșuat + Acel AppImage nu mai există + Actualizarea a eșuat + Se extrage %1$s + AppImage-ul nu a putut fi extras + AppImage-ul nu a putut fi extras: %1$s + + Oprește sesiunea activă înainte de a instala Decky Loader + Descărcarea Decky Loader a eșuat + Dimensiunea Decky Loader nu s-a potrivit cu metadatele versiunii + Suma de control a versiunii nu a putut fi descărcată + Suma de control a versiunii lipsește + Suma de control a versiunii este invalidă + Sumă de control incorectă pentru Decky Loader + Fișierul versiunii nu este un executabil Linux pe 64 de biți compatibil + O sesiune a pornit în timpul descărcării; oprește-o înainte de a instala Decky Loader + PluginLoader nu a putut fi marcat executabil + Depanarea la distanță Steam CEF nu a putut fi activată + PluginLoader nu a putut fi pregătit + PluginLoader-ul existent nu a putut fi păstrat + Noul PluginLoader nu a putut fi activat + Oprește sesiunea activă înainte de a instala un plugin + Instalează Decky Loader înainte de a importa un plugin + Arhiva ZIP selectată nu a putut fi citită + Directorul de pluginuri Decky nu a putut fi creat + Se extrage arhiva ZIP a pluginului + Arhiva ZIP a pluginului trebuie să conțină un singur folder de plugin la nivel superior + Arhivei ZIP a pluginului îi lipsesc plugin.json, package.json sau dist/index.js + Numele pluginului lipsește + O sesiune a pornit în timpul instalării; oprește-o înainte de a instala un plugin + Plugin instalat · repornește Steam pentru a-l încărca + Instalarea arhivei ZIP a pluginului a eșuat + Arhiva ZIP a pluginului conține prea multe intrări + Arhiva ZIP a pluginului conține o cale invalidă + Arhiva ZIP a pluginului conține căi duplicate + Arhiva ZIP a pluginului conține o cale nesigură + Folderul pluginului nu a putut fi creat + Arhiva ZIP a pluginului se extinde peste limita de 1 GiB + Binarul pluginului inclus nu a putut fi marcat executabil + Intrare remote_binary invalidă + Nume de binar la distanță invalid + URL-ul binarului la distanță trebuie să folosească HTTPS + Binarul la distanță %1$s are un hash SHA-256 invalid + Directorul bin al pluginului nu a putut fi creat + Se descarcă binarul pluginului · %1$s + Binarul pluginului %1$s nu a putut fi descărcat + Sumă de control incorectă pentru binarul pluginului · %1$s + Binarul pluginului nu a putut fi marcat executabil · %1$s + Binarul pluginului nu a putut fi înlocuit · %1$s + Binarul pluginului nu a putut fi instalat · %1$s + Folderul pluginului aparține altui plugin + Pluginul existent nu a putut fi păstrat + Pluginul importat nu a putut fi activat + Setările pluginului Decky nu au putut fi actualizate + Pluginul existent nu a putut fi restaurat + + Identitatea Depanării wireless nu a putut fi salvată + Comanda ADB a eșuat (%1$d) + Android nu a confirmat modificarea limitei proceselor-copil: %1$s + Nicio asociere de Depanare wireless nu este salvată + Introdu adresa IP și portul curente ale Depanării wireless + + Timpul a expirat în așteptarea ferestrei de asociere. Încearcă din nou din DroidDeck. + Fereastra de asociere nu a putut fi căutată (%1$d). Verifică dacă Wi-Fi-ul este activat. + Fereastra de asociere nu a putut fi căutată: %1$s + Introdu toate cele 6 cifre ale codului + Se asociază… + Acel cod nu a funcționat. Deschide din nou fereastra de asociere și introdu noul său cod. + Asociat. Se conectează… + Asociat, dar portul de Depanare wireless nu a fost găsit. Lasă Depanarea wireless activată și încearcă din nou. + Se aplică setarea… + Comanda Depanării wireless a eșuat + Configurarea Depanării wireless + Cere codul de asociere al Depanării wireless cât timp Setările sunt deschise + Asociază Depanarea wireless + Activează Depanarea wireless, apoi apasă „Asociază dispozitivul cu un cod de asociere”. + Activează Depanarea wireless, apoi apasă „Asociază dispozitivul cu un cod de asociere”. Rămâi în Setări: codul apare în această notificare. + Cod de asociere din 6 cifre + Apasă Introdu codul și scrie cele 6 cifre arătate în Setări. + Introdu codul de asociere Wi-Fi + Încearcă codul din nou + Introdu codul + Se configurează Steam + Steam este pregătit + Apasă pentru a reveni la DroidDeck. Poți dezactiva acum Depanarea wireless. + Configurarea Depanării wireless s-a oprit + + Descărcarea a răspuns cu HTTP %1$d + Dimensiunea descărcării nu s-a potrivit cu APK-ul publicat și a fost abandonată - încearcă din nou + Descărcarea nu s-a potrivit cu suma sa de control și a fost abandonată - încearcă din nou + Actualizarea este pentru pachetul Android %1$s, nu %2$s + Metadatele actualizării nu numesc cheia de semnare de lansare a DroidDeck + Android nu a putut citi APK-ul descărcat + APK-ul descărcat este pentru %1$s, nu %2$s + APK-ul descărcat spune versionCode %1$d, nu cel publicat %2$d + Android nu va instala versionCode %1$d peste cel instalat %2$d + APK-ul descărcat nu are informații de semnare + APK-ul descărcat nu este semnat cu cheia de lansare a DroidDeck + Catalogul de actualizări a răspuns cu HTTP %1$d + + Se pornește sesiunea… + %1$d:%2$02d scurse · încă se lucrează + Se descarcă actualizarea clientului Steam · %1$d%% + Se descarcă clientul Steam · %1$s + + O instalare a unui instrument de compatibilitate este deja în curs + Oprește sesiunea activă înainte de a instala instrumente de compatibilitate + Nu s-a găsit o versiune ARM64 pentru %1$s + Este necesar cel puțin 4 GB liber, plus dimensiunea descărcării, pentru a instala %1$s + Se descarcă %1$s %2$s + Se reîncearcă descărcarea · tentativa %1$d din %2$d + Descărcare întreruptă + Descărcarea a eșuat; fișierul parțial este păstrat astfel încât o reîncercare să poată continua + Descărcarea a depășit dimensiunea versiunii și a fost ștearsă + Descărcarea a fost incompletă; fișierul parțial este păstrat pentru o reîncercare care poate continua + Se verifică descărcarea + Suma de control a versiunii nu a putut fi citită; încearcă din nou + %1$s %2$s nu publică o sumă de control; nimic nu a fost instalat + Sumă de control incorectă; descărcarea a fost ștearsă + O sesiune a pornit în timpul descărcării; oprește-o înainte de a instala instrumente de compatibilitate + Se instalează %1$s + Instalarea %1$s a eșuat (cod de ieșire %2$d) + %1$s nu a putut fi instalat + Se obține pachetul droiddeck-esync + + cu auxiliare Linux + doar DLL-uri + Original (Proton actualizat de la ultima comutare) + Modificat în afara Componentelor + absent + Original %1$s + Original din %1$s + %1$s %2$s intră în %3$s când jocul care rulează pe el se închide. + %1$s %2$s este acum în %3$s. Se aplică la următoarea pornire a unui joc. + Acel pachet original nu este salvat + %1$s al %2$s revine la original când jocul care rulează pe el se închide. + %1$s al %2$s restaurat din originalul său %3$s. + %1$s este în uz în %2$s. Comută mai întâi acel Proton la altceva. + %1$s nu este salvat + Șters %1$s. + Originalul versiunii instalate este păstrat: este calea de revenire. + Nesalvat + S-a șters originalul %1$s al %2$s. + Descărcările vin doar din versiunile Nightlies + Această listă de pachete e anterioară sumelor de control - reîmprospătează-o și încearcă din nou + Sumă de control incorectă + + Liniar + Cel mai apropiat + Snapdragon GSR (calitate) + Doar claritate + Automat (se potrivește cu scalarea) + + Nimic aplicat. + Claritate de joc + Claritate cu puțin contrast - cea de zi cu zi. + Viu + Culoare intensificată puternic. Eliminarea benzilor activată, ca cerurile să nu aibă dungi. + Cinematic + Contrast mai profund, culoare mai rece, margini netezite. + Competitiv + Umbre ridicate și claritate puternică - observă persoanele mai întâi. + Claritate adaptivă + Doar claritate, culoarea neschimbată. + Filmic + Curbă asemănătoare filmului, puțin estompată. + Arcade + Zgomotos și intens. + CRT retro + Tubul CRT și semnalul analogic trec împreună. + Scalare clară + Pentru a rula sub rezoluția panoului. + Pixeli curați + Pixeli clari pentru 2D și titluri vechi. + Margini anime + Contururi toon plus claritate. + + Nu este un GPU Adreno + Acceptat + Experimental: driverele sale sunt versiuni de test + Sub hardware-ul testat (Adreno 725 și mai nou): poate să nu ruleze + Neacceptat: DroidDeck are nevoie de un GPU Adreno (Snapdragon) + + S-a atins limita de solicitări a GitHub - încearcă din nou mai târziu + GitHub a răspuns cu HTTP %1$d + descărcarea a răspuns cu HTTP %1$d + Această listă de drivere e anterioară sumelor de control - reîmprospătează-o și încearcă din nou + Sumă de control incorectă - descărcarea a fost abandonată + Nu este un driver AdrenoTools: %1$s + fără meta.json + meta.json nu poate fi citit (%1$s) + acesta este un driver pentru mediul de execuție Linux (importă-l la „Driver pentru mediul de execuție Linux”) + acesta este un driver de joc Wayland, pe care această aplicație nu îl poate folosi + meta.json nu numește niciun libraryName, așa că nimic nu a putut fi oferit către AdrenoTools + meta.json numește %1$s, care nu este în arhivă + %1$s nu este o bibliotecă partajată ELF AArch64 pe 64 de biți + %1$s leagă glibc - un Turnip „-Linux”, care aparține la „Driver pentru mediul de execuție Linux” + Niciun libvulkan_freedreno*.so în această arhivă. O arhivă Turnip Android (AdrenoTools) sau „-Wayland” nu este un driver pentru mediul de execuție Linux. + %1$s nu este o bibliotecă partajată ELF AArch64 pe 64 de biți. + %1$s nu este un driver glibc - leagă libc-ul Android. Mediul de execuție Linux are nevoie de o arhivă „-Linux”; un Turnip simplu sau „-Wayland” nu poate fi încărcat deloc de clientul Steam. + Nu este un pachet de drivere: niciun manifest.json nu numește un driver android și unul linux + Nu este un pachet de drivere: %1$s indică în afara lui + Nu este un pachet de drivere: se despachetează la mai mult de %1$d MB + + Card SD + Folder + + Partajează jurnalele sesiunii + + necesită Android 10 sau mai nou (straturi de afișaj cu un spațiu de culoare) + acest afișaj nu raportează suport HDR + acest afișaj acceptă %1$s, nu HDR10 + + acest GPU + Limita proceselor-copil este activă + Limita proceselor-copil nu este setată + Limita proceselor-copil nu a putut fi verificată + Limita proceselor-copil este dezactivată + Limita proceselor-copil nu este necesară + %1$s Dacă nu este posibil, conectează acest dispozitiv la un computer cu ADB și rulează comanda de mai jos. Revino aici și verifică din nou. + Sesiunile Steam pot porni. + Această versiune de Android nu folosește această limită. + Activează „Dezactivează restricțiile pentru procesele-copil” în Opțiunile pentru dezvoltatori, și lasă Opțiunile pentru dezvoltatori activate după aceea. + Această versiune de Android nu are un comutator pentru asta. Folosește Depanarea wireless pentru a o schimba de pe acest dispozitiv. + Această versiune nu are o descărcare pentru această copie de DroidDeck. + Android nu o va instala peste această versiune pentru că versionCode %1$d este sub cel instalat %2$d. + Android nu va instala o versiune mai veche peste una mai nouă. Rămâi pe această versiune până când canalul ajunge din urmă. + Această copie de DroidDeck este semnată diferit (o versiune locală?), așa că Android nu o va actualiza pe loc. + Nu este suficient spațiu liber pentru actualizare. + Actualizarea a fost anulată. + Actualizarea nu s-a instalat: %1$s + Actualizarea nu s-a instalat. + Arhivă %1$s + arhivă + %1$s nu a fost lansat încă + Niciun fișier de salvare găsit pentru %1$s + %1$s nu are un folder drive_c, așa că nu este o arhivă de salvare GameHub sau Winlator + nu se pot crea foldere în %1$s + nu se poate scrie în %1$s + nu se poate scrie în %1$s (%2$s) + Folderul nu este disponibil + Un fișier diferit folosește deja %1$s + Folderul de grafică nu este disponibil + Folderul are un inventar de sincronizare neînrudit + %1$s nu a putut fi eliminat + Fișierul lasă folderul de export + FEX (versiune fără nume de versiune) + niciun profile.json în %1$s + %1$s nu este un pachet de componente Linux (tip %2$s) + %1$s · %2$s liber + Automat + Catalogul de actualizări nu a trecut verificările (%1$s) + Setarea proceselor-copil DroidDeck + Autentificat · mod offline + Autentificat + INCLUS + DESCĂRCAT + IMPORTAT + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Afișajul sesiunii + Rezoluție + Potrivire joc 16:9 + Cum se potrivesc jocurile 16:9 cu acest afișaj. Sesiunea următoare. + Alege Potrivire ecran sau o dimensiune personalizată mai îngustă decât 16:9. + Păstrează proporțiile + Întinde pe afișaj + Scalarea imaginii + Filtru de scalare + Partajat de Steam și Desktop. + Se aplică live. Partajat de Steam și Desktop. + FSR, GSR și NIS se aplică la mărirea imaginii sesiunii. Doar claritate funcționează și fără mărire. + Claritatea filtrului + Rata de cadre + %1$s Setează scalarea la %2$s. + Potrivire ecran (%1$d×%2$d) + Urmărește ecranul (se redimensionează la împăturire sau desfacere) + Steam + Arată detaliile dispozitivului + Ascunde detaliile dispozitivului + Se citește dispozitivul… + Copiază + Copiază aceste detalii într-un raport de eroare. + Copiat. + Forțează SSBS + Păstrează o setare de viteză a CPU-ului pe care Wine o dezactivează mereu. Poate face jocurile Windows mai rapide pe Snapdragon 8 Elite; schimbare mică observată pe cipuri mai vechi. Se aplică la următoarea lansare a jocului. + + Magazine + Steam + Personalizat + Magazine + Deschide un magazin pe + Nivelul vitezei de descărcare + Motorul magazinelor + ● Motor pregătit + Necompilat + Echilibrat + Rapid + Maxim + Magazine + Setările magazinelor + Setări + (neautentificat) + Magazin + Instalate + Bibliotecă + Toate + Descărcări + %1$s (%2$d) + Autentificare + %1$s · deconectare + Te deconectezi de la %1$s? + Jocurile instalate rămân. Biblioteca este reîncărcată când te autentifici din nou. + Rămâi autentificat + Deconectare + Caută în catalogul %1$s… + Caută jocurile instalate… + Caută în biblioteca ta… + Căutare + Nimic instalat + Se încarcă + Niciun joc + Se încarcă magazinul… + Niciun catalog + Noutăți + Oferte + Jocuri gratuite + În tendințe + Biblioteca ta + Joacă + Instalează + Reia instalarea + Întreruptă + Instalare · %1$s + Obține gratuit + Vezi pe %1$s + Cumpără · %1$s + Instalat + În bibliotecă + Gratuit + Reluare + Reîmprospătează + Arată conținutul pentru adulți + Epic + EOS + Offline + Dezactivat + Autentificare Epic (EOS) + Lansează offline + Rezolvă autentificarea Epic + Salvări în cloud + Încarcă în cloud + Descarcă din cloud + Nicio salvare în cloud + Conflict în cloud + Conflict în cloud · %1$d fișiere + … + Păstrează cloud + Păstrează local + Se verifică cloud-ul… + Ultima sincronizare %1$s + Nesincronizat încă + %1$d fișiere + instalat + în biblioteca ta + magazin + Realizări + %1$d din %2$d deblocate + %1$d / %2$d + Deblocate · %1$d + Blocate · %1$d + Ascunse · %1$d + Realizare ascunsă + Detaliile apar după deblocare. + Toate realizările deblocate + Se încarcă… + Conectează-te la %1$s pentru a-ți vedea realizările. + Nu s-a putut contacta %1$s. Verifică din nou când ești online. + %1$s nu listează realizări pentru acest joc. + Arată toate cele %1$d + Realizări · %1$s + ✓ În Steam + Dezinstalează + Se dezinstalează… + Dezinstalezi %1$s? + Eliberează %1$s și îl elimină din Steam. + Elimină fișierele sale și comanda rapidă din Steam. + Steam preia jocul la următoarea pornire; lansează-l de acolo, sau pornește Steam acum. + Proton + Alegerea Steam per joc + Sunet + DirectAudio + Componente + FEX, DXVK, VKD3D + Motor + %1$s · nativ + %1$s · integrat + Cum rulează + Obținut din + %1$s, cu contul tău + Instalat în + Stocarea jocurilor (Games/%1$s/%2$s) + Lansat de + Steam și Proton, aceeași cale ca jocurile Steam + Steam și Proton · Autentificare Epic la fiecare lansare, suprapunerea EOS dezactivată + Steam și Proton · Variabile Amazon Games Services setate + Steam + adăugat la Steam la instalare (interfață Deck + Steam Input) + Nicio descărcare + Pe pauză + În așteptare + În așteptare · #%1$d + Anulată + Eșuată + Eșuată · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Pauză + Reluare + Anulează + Reîncearcă + Șterge + Șterge cele finalizate + Manifest + Descărcare + Verificare + Instalare + Terminat + Se verifică + Se descarcă + Ștergi descărcarea? + Eșuată · %1$s păstrat + Se verifică + Se instalează + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d fișiere + Administrator de descărcări + Descărcări simultane + Instalează în + Instalezi %1$s: + Internă + %1$s liber + Descărcări din magazin + Arată progresul descărcării jocului și păstrează descărcările în execuție în fundal + Se descarcă + %1$d descărcări + Se pregătește… + Descărcări din magazin finalizate + %1$s instalat + Descărcarea %1$s a eșuat + Șterge + Păstrează + Autentifică-te la %1$s + Autentifică-te cu furnizorul tău + Reîncarcă + Închide + Încearcă din nou + Se finalizează autentificarea… + Pagina de autentificare nu a putut fi încărcată: %1$s + Acest furnizor refuză autentificarea dintr-o pagină încorporată. Autentifică-te cu parola magazinului în schimb. + Pagina de autentificare a răspuns cu o eroare (HTTP %1$d). + Conexiunea la pagina de autentificare nu este sigură. + Autentificarea nu a putut fi verificată. Încearcă din nou. + Autentificarea a eșuat. Încearcă din nou. + Închide + Înapoi + Editează + Adaugă toate jocurile din acest folder + Nimic nou în acest folder + Nume + Țintă + Pornește în + Opțiuni de lansare + Grafică + Alt exe… + Elimină + Copertă + Fundal + Logo + Iconiță + Folderul jocului + Steam + SteamGridDB + Fișierul tău + Din exe + Automat + Alege imaginea… + Alege o imagine: %1$s + Resetează la automat + + %1$d joc adăugat + %1$d jocuri adăugate + %1$d de jocuri adăugate + + + %1$d deja în Jocuri + %1$d deja în Jocuri + %1$d deja în Jocuri + + B · Înapoi la %1$s + joc + Setările de lansare + Drivere grafice + Acest Proton + Fiecare Proton + Pentru fiecare Proton + Disponibile · %1$d + Verificat %1$s + Verifică componente și drivere noi + Obține + %1$s · DXVK, VKD3D-Proton și FEX de mai jos sunt ale acestui Proton + %1$s · testat + %1$s · netestat + %1$s · neacceptat + Păstrează perechea recomandată actualizată + Automat · %1$s + Drivere pentru mediul de execuție și pentru afișaj, separat + Folosești %1$s? + Elimini %1$s? + Diagnosticare + Pregătit de joc + Încă nepregătit de joc + mediu de execuție %1$s + limita proceselor dezactivată + autentificat ca %1$s + Când ceva are nevoie de tine + Aspect + Acest dispozitiv + Grafica bibliotecii + APLICAȚII LINUX + Flathub + Aplicații desktop pentru desktopul Linux, deschise din Desktop. + Configurează Flathub + Elimini %1$s? + Setările și fișierele sale sunt păstrate. + Păstrează + Dimensiune pe disc + Dezinstalezi desktopul? + Toate + Realizări GOG + Sincronizează realizările pentru jocurile GOG compatibile. Se aplică la următoarea lansare. + Realizări dezactivate + Realizări activate + Conectează-te din nou la GOG + Realizări încărcate + Încărcarea realizării este în așteptare + Activează realizările GOG din setările Stores + Realizări indisponibile; verifică jurnalele sesiunii + Realizare deblocată + Inserează + Cruce directională + Stick stânga + Stick dreapta + Meniu + Tastatură + Cruce directională sus + Cruce directională jos + Cruce directională stânga + Cruce directională dreapta + Comenzi tactile + Controler tactil + Mouse + Activat + Dezactivat + Lentă + Normală + Rapidă + Editează aranjamentul + Meniul DroidDeck + Trage elementele de control pe ecran + Aranjează + Renunță + Se salvează… + Pictogramă + L− + L+ + Î− + Î+ + Ascunde + Culoare și opacitate… + Copiază acest aranjament + Aranjament copiat; lipește-l în aranjamentul tactil al oricărui joc + Lipește aranjamentul copiat + Revino la cel implicit din Steam + Acest element de control nu are o asociere în Steam pentru a i se atribui o pictogramă + Pictogramă pentru %1$s + Culoarea aranjamentului tactil + Terminat + Aranjamentul tactil nu a putut fi salvat + Aranjamentul tactil a fost salvat în Steam + Aplicația %1$d + Etichetă (opțional) + Pictogramele Steam nu au fost găsite + + %1$d pictogramă de la Steam + %1$d pictograme de la Steam + %1$d de pictograme de la Steam + + Culoarea pictogramei + Culoarea butonului + Fără pictogramă + + Se verifică autentificarea… + Se obține biblioteca ta… + Se obține lista de jocuri… + Se încarcă detaliile jocurilor… (%1$d/%2$d) + + Se sincronizează %1$d joc… + Se sincronizează %1$d jocuri… + Se sincronizează %1$d de jocuri… + + Se sincronizează… %1$d/%2$d + Se obțin copertele… + Sincronizarea a eșuat + Nu s-au găsit jocuri în biblioteca ta %1$s + Amazon Games nu a răspuns pentru biblioteca ta: reîmprospătează sau autentifică-te din nou + Nu s-a putut contacta GOG + Biblioteca ta GOG nu a putut fi citită + Se înregistrează la Steam… + Cardboard VR + Mod Cardboard + Imagini pentru ochiul stâng și cel drept, pentru un vizor VR pentru telefon. + Imagine + Ecran virtual + 3D alăturat + Vizor + Cardboard + Cardboard (2014) + Fără lentilă + Distanța dintre ochi + Lățimea ecranului + Distanța ecranului + Urmărirea capului + Acest dispozitiv nu are senzor de rotație. + Recentrează vederea + Recentrează + SteamVR (experimental) + Instalează + Descarcă SteamVR ARM64 de la Valve (571 MB). + Se descarcă și se dezarhivează SteamVR… + Pornește + Rulează SteamVR cu capul tău drept cască; imaginea trece la 3D alăturat. + Oprește + SteamVR rulează. + Indisponibil în această sesiune. + SteamVR nu a răspuns; verifică jurnalul sesiunii. + Descarcă componente + Descărcările sunt comune cu pagina Componente. Alege mai sus o versiune descărcată pentru a o folosi la următoarea lansare a jocului. + Alege o versiune de descărcat + Dezvoltator + Editor + Lansare + Principal + + Extra + Completist + %1$s h + %1$d min + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5f23a21f8..868f82fc6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -15,8 +15,10 @@ Сессия Отображается, пока открыта сессия Steam или рабочего стола Steam запущен + Идет загрузка в Steam Сессия приостановлена — нажмите «Продолжить» Продолжить + Приостановить сейчас Остановить сессию Генерация кадров Выберите режим генерации кадров @@ -182,6 +184,8 @@ ID приложения org.ppsspp.PPSSPP Недействительный ID приложения Flathub + Или файл .flatpak — для приложения, которого нет в Flathub. + Выбрать файл .flatpak Источник На рабочем столе Новое приложение @@ -251,6 +255,10 @@ Экранные элементы управления Steam + QAM Автоматический режим показывает экранные элементы управления, если контроллер не подключен. + Сенсорное управление Steam (бета) + Сенсорное управление Steam включится со следующей сессии Steam + Сенсорная раскладка Steam + Перемещайте, меняйте размер или скрывайте сенсорные кнопки этой игры; сохраняется в Steam Контроллер Как Steam распознает ваш контроллер. Вступит в силу в следующей сессии. Контроллер Steam Deck @@ -260,12 +268,16 @@ Контроллер Xbox 360 Deck: кнопка быстрого доступа и гироскоп. Xbox 360: используйте Guide+A для быстрого доступа. Назад + Меню → QAM + QAM → Меню Сессия Поведение в фоне Поведение при скрытии приложения или выключении экрана. Вручную Штатный Авто: пауза и возобновление при показе. Штатный: сначала подготавливает Steam к переходу в режим сна. Вручную: ждет нажатия «Продолжить». Никогда: сессия продолжает работать. + Не прерывать загрузки + Откладывает приостановку в фоне, пока Steam что-то скачивает. Авто: пауза в фоне и возобновление при показе. Вручную: ждет нажатия «Продолжить». Никогда: сессия продолжает работать. Запуск Запускать Steam при открытии DroidDeck @@ -302,6 +314,52 @@ Выберите exe: Это расположение недоступно в сеансе Проверьте этот exe + Компоненты Windows + LB / RB — переход между разделами. + Нет + DLL для Windows из списка компонентов, которых нет в Proton. Включите те, что нужны этой игре: они попадают в собственную папку Windows игры (ее файлы не затрагиваются) и сохраняются при любой версии Proton. + Не удалось получить список компонентов. Уже скачанные компоненты можно включать и выключать. + Рекомендуется для этой игры + Проверка, что нужно этой игре… + Доступно при подключении к сети + + Включение %1$d компонента… + Включение %1$d компонентов… + Включение %1$d компонентов… + Включение %1$d компонента… + + Включено для этой игры + Steam устанавливает %1$s вместе с %2$s (приложение %3$s) + Из списка GOG для этой игры + Из требований Epic для этой игры + Из шагов установки Amazon для этой игры + Steam устанавливает %1$s с приложением %2$s (совпадение по имени) + Также в списке + В списке %1$s; здесь это не компонент + Steam + GOG + Epic Games + Amazon Games + Игра поставляется с %1$s + В игре есть %1$s (часто работает как есть) + Уже есть в этой игре + Ничто в папке игры не указывает на нужный компонент. + Установлено для этой игры (%1$d) + Все компоненты (%1$d) + Нужен установщик Windows (%1$d) + Это отдельные программы установки: внутри нет пакета, который можно разобрать, и пока нет записи того, что они устанавливают. В основном это старые среды выполнения, которые уже заменяет Mono внутри Proton; записи добавляются по мере появления. + Показать + Скрыть + Запускает свою программу установки (записи пока нет) + Здесь пока не поддерживается + Загрузка компонентов… + Не удалось установить: %1$s + Скачано + Загрузка %1$s + Установка: %1$s + Отключение… + Загрузка: %1$s + Вступит в силу при следующем запуске игры. Растянуть игры 16:9 На экранах уже 16:9 растягивает игры на весь экран вместо черных полос. Вступит в силу в следующей сессии. Аудио @@ -359,6 +417,44 @@ Проверить новые пакеты в Nightlies Proton Выбранный здесь Proton запускает все игры, для которых в Steam не выбран свой; изменение, сделанное в Steam, тоже отображается здесь. При обновлении Steam заменяет Proton от Valve; оригинальные файлы каждой сборки сохраняются. + Конфигурация + Настроить профиль + Профиль + Профиль по умолчанию + Автоматически используется играми без переопределений. + Сохраненные настройки, которые автоматически применяются при запуске %1$s. + Выберите только те настройки, которые нужны этой игре. Все остальное по-прежнему берется из профиля по умолчанию. + Пресет FEX + Используется пресет из профиля по умолчанию. + Для этой игры сохранен свой пресет. + Свои переменные FEX + По умолчанию + Совместимость игры + Параметры среды Proton, DXVK, VKD3D и синхронизации для этой игры. + Используется Proton из профиля по умолчанию. + Для этой игры сохранен свой Proton через настройку совместимости Steam. + Без Proton + DXVK + VKD3D-Proton + Использует пакет из профиля по умолчанию от %1$s, если для этой игры не выбран другой пакет. + Использует пакет компонентов из профиля по умолчанию. + Использует %1$s только для этой игры. + Параметры среды выполнения + Другие параметры среды Proton, DXVK, VKD3D-Proton и синхронизации для отдельной игры. + Профиль %1$s сохранен и применится при следующем запуске игры. + Запуск без Steam + Для игры, которая сразу закрывается: запускает её без настройки Steam от Proton. Функции Steam, такие как оверлей, для неё отключены. + Включено + Выбор Proton для %1$s сохранен и уже доступен в Steam. + Выбор Proton для %1$s сохранен. Перезапустите Steam перед запуском игры. + %1$s не может использовать выбранный Proton на этом устройстве. + Не удалось загрузить профиль %1$s: %2$s + Не удалось сохранить профиль игры: %1$s + Сбросить профиль игры + Сбросить профиль %1$s? + Удалить переопределения Proton, FEX, пакета компонентов, среды, фильтрации текстур и компонентов Windows для этой игры? Она будет использовать профиль по умолчанию. + %1$s теперь использует профиль по умолчанию. + Не удалось полностью сбросить профиль игры: %1$s %1$s теперь запускает все игры, для которых в Steam не выбран свой Proton. %1$s будет запускать все игры, для которых не выбран свой Proton, со следующего запуска Steam. %1$s не может запускать игры здесь, поэтому не может быть Proton по умолчанию. @@ -522,6 +618,21 @@ Остановить сессию? Сейчас выполняется Сейчас нет игр, запущенных через Proton + Профиль игры + Сведения о профиле пока недоступны. + Не удалось загрузить профили игр. Повторная попытка будет выполнена автоматически. + Не удалось обновить профили игр. Показаны последние доступные данные; повторная попытка будет выполнена автоматически. + Просмотр и изменение сохраненных настроек для следующего запуска. + Используется профиль по умолчанию + Свой профиль + Используется %1$s из профиля по умолчанию. + Изменения сохраняются в профиль %1$s и вступят в силу при следующем запуске игры. + + %1$d переопределение для игры + %1$d переопределения для игры + %1$d переопределений для игры + %1$d переопределения для игры + Proton не установлен. игра запущена повторно применено при запуске %1$s @@ -581,6 +692,10 @@ Автономный режим Сначала войдите в Steam Лаунчер + Язык + Steam тоже будет его использовать после следующего запуска. + Как в системе (%1$s) + Как в системе Масштаб приложения Текст и элементы управления DroidDeck %1$d%% (по умолчанию) @@ -589,6 +704,10 @@ Использовать как главный экран DroidDeck может использоваться как приложение главного экрана DroidDeck не будет доступен как приложение главного экрана + Ориентация + Альбомная (авто) + Альбомная + Перевернутая альбомная Полноэкранный режим Скрывает строку состояния и панель навигации Android Показывает строку состояния и панель навигации Android @@ -855,6 +974,12 @@ Эта страница больше не существует. Выберите раздел слева. Открыть рабочий стол Установить и открыть рабочий стол + Удалить рабочий стол + Удаление рабочего стола и встроенных в него приложений. Игры, добавленные приложения, настройки и общие библиотеки сохранятся. + Удаление рабочего стола + Рабочий стол удален + Удаление старых файлов LXQt + Обновите DroidDeck, прежде чем удалять эту версию рабочего стола. Рабочий стол Linux KDE Plasma, Firefox и ваши эмуляторы Игры, установленные в Steam, появятся здесь после завершения сессии. @@ -883,6 +1008,8 @@ Последний запуск %1$s Внутреннее хранилище Добавленная игра + Своя игра + Хранилище игр Лучшая пара для этого GPU, обновляется автоматически Выбрать комбинацию драйверов вручную Проверка драйверов… @@ -924,6 +1051,12 @@ Левый тачпад Правый тачпад Оба + Геймпад + Стик + Тачпад + Переключение между стиком и тачпадом + Другие элементы управления Deck + ПАНЕЛИ Закрыть управление вторым экраном Открыть меню Steam Слева · сверху @@ -959,6 +1092,10 @@ Установлено: %1$s В очереди по предыдущему запросу Не установлено + Доступно обновление → %1$s + Удалить старые версии %1$s + Будут удалены: %1$s. Игры, настроенные на эти версии, нужно переключить на другой Proton. + Игры, настроенные на эту версию, нужно переключить на другой Proton. Установить сейчас Убрать из очереди Клавиатура Android @@ -987,6 +1124,8 @@ Игры Proton Картинка в картинке Автоматически переходить в режим PiP + Управление на разложенном экране + В разложенном виде игра находится над тачпадами и грипсами Steam Deck. В сложенном — игра на весь экран. Приостановлено PiP станет доступен после готовности сессии. Разрешите DroidDeck использовать PiP в настройках Android. @@ -1011,8 +1150,735 @@ Выбрать папку для файлов игры фронтенда Файлы игры сохранены в %1$s Файлы фронтенда - На уровень вверх + + Дождитесь завершения операции со средой выполнения + Дождитесь завершения установки инструмента совместимости + Дождитесь завершения установки приложения рабочего стола + Дождитесь завершения установки, прежде чем запускать игру + Сначала закройте сессию Steam, чтобы игра не записала свои сохранения поверх импортированных + Импорт в %1$s + + Импортирован %1$d файл (%2$s) в %3$s + Импортировано %1$d файла (%2$s) в %3$s + Импортировано %1$d файлов (%2$s) в %3$s + Импортировано %1$d файла (%2$s) в %3$s + + + Импортирован %1$d файл (%2$s) в %3$s. Старые сохранения скопированы в Download/DroidDeck/Saves/backups + Импортировано %1$d файла (%2$s) в %3$s. Старые сохранения скопированы в Download/DroidDeck/Saves/backups + Импортировано %1$d файлов (%2$s) в %3$s. Старые сохранения скопированы в Download/DroidDeck/Saves/backups + Импортировано %1$d файла (%2$s) в %3$s. Старые сохранения скопированы в Download/DroidDeck/Saves/backups + + Выберите zip-архив сохранений для %1$s + Экспорт %1$s + + Экспортирован %1$d файл (%2$s): %3$s + Экспортировано %1$d файла (%2$s): %3$s + Экспортировано %1$d файлов (%2$s): %3$s + Экспортировано %1$d файла (%2$s): %3$s + + Выберите, куда сохранить %1$s (%2$s) + Ошибка (%1$s): %2$s + Журналов сессии пока нет: сначала запустите сессию. + Нет журналов сессии для очистки. + + Очищена %1$d папка журналов сессии. + Очищено %1$d папки журналов сессии. + Очищено %1$d папок журналов сессии. + Очищено %1$d папки журналов сессии. + + Ошибка сопряжения для отладки по Wi-Fi + Ошибка команды отладки по Wi-Fi + Отладка по Wi-Fi недоступна + Команда ADB скопирована + Выберите папку ROM + GPU не Adreno + Turnip поддерживает GPU Adreno. На %1$s в Steam может быть черный экран. Загрузка: %2$.0f МБ. + Все равно установить + Удалить среду выполнения Linux + Будут удалены среда выполнения, клиент Steam в ней и все установленные туда игры. + Применено: %1$s + Не удалось открыть %1$s на главном экране + Не удалось открыть %1$s на экране «%2$s» + Не удалось открыть %1$s на экране %2$d + Импорт + Замена + Восстановление + Отмена + Удаление + не удается прочитать файл + Импортировано: %1$s + Ожидающая замена отменена. + Выберите пакет компонентов (-linux .wcp) + Не удалось подключиться к Nightlies + Сохранено: %1$s + Ошибка загрузки: %1$s + Выберите драйвер среды выполнения Linux (-Linux zip или пакет Android + Linux) + Выберите драйвер дисплея (AdrenoTools zip или пакет Android + Linux) + Выберите zip-архив драйвера или пакет Android + Linux + %1$s: %2$s + Удаление %1$s… + Удаление %1$s из списка… + Выберите папку хранилища игр + Выберите ZIP-файл плагина Decky + Нельзя использовать: %1$s + Программа + Удаление среды выполнения Linux + Не удалось открыть настройки на нижнем экране; они открыты на главном экране. + Откройте раздел «Для разработчиков» в настройках + + Не удалось открыть настройку Android «Установка неизвестных приложений» + Не удалось проверить: %1$s + В этой сборке нет файла для этой копии DroidDeck + Сначала остановите текущую сессию, затем обновите + Загрузка + Установка + Ожидание вашего подтверждения + Ошибка обновления + Android не предложил установить обновление + + Ошибка установки Decky: %1$s + Начало импорта плагина… + Ошибка импорта плагина: %1$s + Плагин установлен. Перезапустите Steam, чтобы загрузить его. + + Еще не проверялось — нажмите «Обновить», чтобы найти новые драйверы + Android + Linux: заодно задает драйвер дисплея + Android + Linux: заодно задает драйвер среды выполнения + Turnip, встроенный в среду выполнения + системный Vulkan: для этого GPU нет встроенной сборки + %1$s (встроенный) + Вручную + %1$s · %2$s + Сейчас опубликована только одна половина + %1$.0f МБ за оба + Нет драйверов для установки: %1$s + Поиск последних драйверов… + Актуально (не удалось проверить: %1$s) + Рекомендуемые драйверы сейчас не опубликованы + Загрузка %1$s %2$s… + Обновлено до %1$s %2$s. + Обновлено до %1$s %2$s. Драйвер дисплея применится после перезапуска DroidDeck. + Выбран %1$s %2$s. + Выбран %1$s %2$s. Драйвер дисплея применится после перезапуска DroidDeck. + Используется %1$s %2$s. + Используется %1$s %2$s. Драйвер дисплея применится после перезапуска DroidDeck. + Это не zip-архив драйвера: ни драйвер AdrenoTools, ни -Linux Turnip, ни пакет Android + Linux + Ошибка импорта: %1$s + Импортированный драйвер + %1$s используется как драйвер среды выполнения и драйвер дисплея. + %1$s используется как драйвер среды выполнения и драйвер дисплея. Драйвер дисплея применится после перезапуска DroidDeck. + Удалено: %1$s (оба драйвера) + Удалено: %1$s + Последние: %1$s · проверено %2$s + Последние: %1$s · %2$s недоступно · проверено %3$s + + %1$d мин назад + %1$d мин назад + %1$d мин назад + %1$d мин назад + + + %1$d ч назад + %1$d ч назад + %1$d ч назад + %1$d ч назад + + + %1$d день назад + %1$d дня назад + %1$d дней назад + %1$d дня назад + + Проверка Banners-Turnip, WinNative и DroidDeck… + ошибка проверки + Установлен %1$s — выберите его в любом из списков, чтобы задать оба драйвера + Установлен %1$s — выберите его в меню + + Дождитесь завершения удаления среды выполнения + Игра + тачпад + прямой + Ошибка замены: %1$s + Ошибка сохранения профиля: %1$s + Рабочий стол приостановлен + %1$s на паузе + Программа приостановлена + Запуск рабочего стола + Запуск %1$s + Запуск программы + Для этой сессии нет журналов. + Не удалось создать архив журналов сессии. + Не удалось поделиться журналами сессии. + Запуск сессии… + загрузка среды выполнения Linux + загрузка среды выполнения Linux · %1$d из %2$d МБ + проверка среды выполнения Linux + распаковка среды выполнения Linux + загрузка рабочего стола + загрузка рабочего стола · %1$d из %2$d МБ + проверка рабочего стола + распаковка рабочего стола + загрузка Proton Experimental (ARM64) + загрузка Proton Experimental (ARM64) · %1$d из %2$d МБ + проверка Proton Experimental (ARM64) + распаковка Proton Experimental (ARM64) + Не удалось получить каталог сред выполнения. Проверьте подключение и снова нажмите Play. + Среда выполнения Linux не установилась. Проверьте ее на странице «Настройка» и снова нажмите Play. + Рабочий стол не установился (%1$s). Проверьте подключение и снова нажмите «Рабочий стол». + Работа %1$s неожиданно прервалась. Приложите журналы к отчету об ошибке или попробуйте снова. + Программа неожиданно завершила работу. Приложите журналы к отчету об ошибке или попробуйте снова. + Рабочий стол неожиданно завершил работу. Приложите журналы к отчету об ошибке или попробуйте снова. + Код выхода %1$d · %2$s + нет журнала + + В журнале %1$d ошибка «Function not implemented»: ускорение seccomp в proot вызывает сбой вспомогательной программы на этом устройстве. Попробуйте %2$s → «%3$s» (или «%4$s») и запустите снова. + В журнале %1$d ошибки «Function not implemented»: ускорение seccomp в proot вызывает сбой вспомогательной программы на этом устройстве. Попробуйте %2$s → «%3$s» (или «%4$s») и запустите снова. + В журнале %1$d ошибок «Function not implemented»: ускорение seccomp в proot вызывает сбой вспомогательной программы на этом устройстве. Попробуйте %2$s → «%3$s» (или «%4$s») и запустите снова. + В журнале %1$d ошибки «Function not implemented»: ускорение seccomp в proot вызывает сбой вспомогательной программы на этом устройстве. Попробуйте %2$s → «%3$s» (или «%4$s») и запустите снова. + + + Плагины + Установите ZIP-архив плагина для следующей сессии Steam. + Загрузка… + Установить из ZIP + Игры + Аудио и видео + Графика + Интернет + Офис + Разработка + Образование + Наука + Система + Утилиты + Популярное + В тренде + Новое + Обновленное + загрузка… + Настройка Flatpak + %1$s: %2$s + Не удалось проверить обновления в Flathub + установлено + папка + Графит + Графитовые поверхности; синий цвет сферы из логотипа выделяет все выбранное, активное и нажатое. + Бумага на черном + Основной цвет — белый, синий цвет значка служит акцентом для линий, колец и точек. + Синий из значка + Черный фон, белый текст, синий — для всего выбранного, активного или нажатого. + Электрик + Более яркий синий на поверхностях темно-синего, а не серого оттенка. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Пробел + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Сначала установите среду выполнения + Библиотека Steam недоступна + Предыдущая библиотека на SD-карте недоступна + Хранилище игр недоступно + Папка добавленных игр недоступна + Манифесты Steam недоступны + Множество систем + Внутреннее хранилище Система Linux + SD-карта + Хранилище + Выбрать + Свойства + Добавить в избранное + Удалить из избранного + Переименовать + Копировать + Вырезать + Действия + Избранное + Закрепить текущую папку + Избранного пока нет — закрепите папку через ее меню ⋮, чтобы быстро возвращаться к ней. + Удалить из избранного + 0 байт + байт + Расположение + Тип + Размер + Изменено + Папка + Файл + Файл %1$s + Только чтение + Не дает играм (и Wine) перезаписать или удалить этот файл. + Не удалось изменить атрибут «Только чтение» + Скрытый + Это хранилище не поддерживает атрибут «Скрытый». + Помечает файл как скрытый в Windows (атрибут hidden в Wine). + Не удалось изменить атрибут «Скрытый» в этом хранилище + Готово + Удаление... + Ошибка удаления + Перемещение... + Копирование... + Перемещение — %1$s + Копирование — %1$s + Перемещение %1$d/%2$d — %3$s + Копирование %1$d/%2$d — %3$s + Готово: %1$d, ошибок: %2$d + Готово: %1$d, пропущено: %2$d + + Перемещен %1$d элемент + Перемещено %1$d элемента + Перемещено %1$d элементов + Перемещено %1$d элемента + + + Скопирован %1$d элемент + Скопировано %1$d элемента + Скопировано %1$d элементов + Скопировано %1$d элемента + + «%1$s» уже существует + Переименование... + Ошибка переименования + Создание папки... + Не удалось создать папку + Новая папка + Имя папки + Создать + Новое имя + Удалить? + Удалить «%1$s» навсегда? + + Удалить %1$d элемент? + Удалить %1$d элемента? + Удалить %1$d элементов? + Удалить %1$d элемента? + + Это действие нельзя отменить. + + …и еще %1$d + …и еще %1$d + …и еще %1$d + …и еще %1$d + + Удаление %1$d/%2$d — %3$s + + Не удалось удалить %1$d элемент + Не удалось удалить %1$d элемента + Не удалось удалить %1$d элементов + Не удалось удалить %1$d элемента + + «Объединить» добавляет и заменяет файлы внутри существующей папки. + «Перезаписать» заменяет существующий файл. + Применить ко всем конфликтам + Объединить + Перезаписать + Оставить оба + Пропустить + Выбрать эту папку + Назад + %1$s: подключено, но сейчас недоступно для чтения + Список + Сетка + Поиск + Сортировка + Имя + Дата изменения + Просторные строки + Компактные строки + Не показывать скрытые файлы + Показывать скрытые файлы + Скрыть избранное + Показать избранное + Фильтр по этой папке + Свободно: %1$s + Выбрано: %1$d + Снять + Все + Вставить + + %1$d элемент + %1$d элемента + %1$d элементов + %1$d элемента + + Вставить сюда: %1$s + Переместить сюда: %1$s + Файлы + ХРАНИЛИЩЕ + БЫСТРЫЙ ДОСТУП + ИЗБРАННОЕ + Загрузки + Загрузки + ROMs + Журналы сессии + Изображения + Папка «%1$s» добавлена в избранное + Папка «%1$s» удалена из избранного + Пустая папка + На уровень вверх + Выберите папку + Выберите файл + Синий Steam + Небесный + Белый + Фиолетовый + Зеленый + Янтарный + Красный + Розовый + Кнопка A + Кнопка B + Кнопка X + Кнопка Y + Левый бампер + Правый бампер + Левый триггер + Правый триггер + Кнопка View + Кнопка «Меню» + View + Меню + Крестовина вверх + Крестовина вниз + Крестовина влево + Крестовина вправо + Скрыто + Перетащите для перемещения + Привязка + Сохранить + Ничего + Клавиатура + тачпад + Терминал + Грипсы и тачпады Deck + + Удаление среды выполнения Linux + + Удаление среды выполнения Linux · %1$d элемент + Удаление среды выполнения Linux · %1$d элемента + Удаление среды выполнения Linux · %1$d элементов + Удаление среды выполнения Linux · %1$d элемента + + Среда выполнения Linux удалена + Не удалось завершить удаление среды выполнения Linux. Повторите удаление на странице «Настройка». + Удаление остатков среды выполнения + Загрузка + Проверка + Распаковка + Отображается во время установки или удаления среды выполнения Linux + Установка среды выполнения Linux + Удаление среды выполнения Linux + + Среда выполнения Linux не установлена + В каталоге нет контрольной суммы для %1$s + Контрольная сумма не совпадает — ничего не изменено + Установка %1$s + Не удалось разместить AppImage + Ошибка распаковки + Ошибка установки + + Ошибка настройки Flatpak (код %1$d) + Настройка Flatpak не завершилась + Выполняется другая задача магазина + Сначала настройте Flatpak + Запуск + Flatpak завершился с кодом %1$d + Установка %1$s + Обновление %1$s + Удаление %1$s + Установка %1$s (%2$d/%3$d) + Обновление %1$s (%2$d/%3$d) + Удаление %1$s (%2$d/%3$d) + переводы + графические драйверы + медиакодеки + среда выполнения %1$s + среда выполнения %1$s %2$s + + Файл слишком мал для AppImage + Не удается прочитать файл: %1$s + Это не AppImage (это не программа Linux) + Этот AppImage собран не для ARM64 (aarch64) и не для x86-ПК + Это программа Linux, но не AppImage + Импортировать можно только AppImage типа 2 (у этого тип %1$d) + Недостаточно свободного места: для импорта нужно около %1$s + Недостаточно свободного места: для обновления нужно около %1$s + Ошибка импорта + Этот AppImage больше недоступен + Ошибка обновления + Распаковка %1$s + Не удалось распаковать AppImage + Не удалось распаковать AppImage: %1$s + + Остановите текущую сессию перед установкой Decky Loader + Ошибка загрузки Decky Loader + Размер Decky Loader не совпадает с метаданными выпуска + Не удалось скачать контрольную сумму выпуска + Контрольная сумма выпуска отсутствует + Недопустимая контрольная сумма выпуска + Контрольная сумма Decky Loader не совпадает + Файл выпуска не является совместимым 64-битным исполняемым файлом Linux + Во время загрузки запустилась сессия; остановите ее перед установкой Decky Loader + Не удалось сделать PluginLoader исполняемым + Не удалось включить удаленную отладку CEF в Steam + Не удалось подготовить PluginLoader + Не удалось сохранить текущий PluginLoader + Не удалось активировать новый PluginLoader + Остановите текущую сессию перед установкой плагина + Установите Decky Loader перед импортом плагина + Не удалось прочитать выбранный ZIP-файл + Не удалось создать папку плагинов Decky + Распаковка ZIP-архива плагина + ZIP-архив плагина должен содержать одну папку плагина верхнего уровня + В ZIP-архиве плагина нет plugin.json, package.json или dist/index.js + Не указано имя плагина + Во время установки запустилась сессия; остановите ее перед установкой плагина + Плагин установлен · перезапустите Steam, чтобы загрузить его + Ошибка установки ZIP-архива плагина + В ZIP-архиве плагина слишком много записей + ZIP-архив плагина содержит недопустимый путь + ZIP-архив плагина содержит повторяющиеся пути + ZIP-архив плагина содержит небезопасный путь + Не удалось создать папку плагина + После распаковки ZIP-архив плагина превышает лимит 1 ГиБ + Не удалось сделать исполняемым встроенный бинарный файл плагина + Недопустимая запись remote_binary + Недопустимое имя удаленного бинарного файла + URL удаленного бинарного файла должен использовать HTTPS + У удаленного бинарного файла %1$s недопустимый хеш SHA-256 + Не удалось создать папку bin плагина + Загрузка бинарного файла плагина · %1$s + Не удалось скачать бинарный файл плагина %1$s + Контрольная сумма бинарного файла плагина не совпадает · %1$s + Не удалось сделать бинарный файл плагина исполняемым · %1$s + Не удалось заменить бинарный файл плагина · %1$s + Не удалось установить бинарный файл плагина · %1$s + Папка плагина принадлежит другому плагину + Не удалось сохранить текущий плагин + Не удалось активировать импортированный плагин + Не удалось обновить настройки плагинов Decky + Не удалось восстановить прежний плагин + + Не удалось сохранить идентификатор для отладки по Wi-Fi + Ошибка команды ADB (%1$d) + Android не подтвердил изменение ограничения дочерних процессов: %1$s + Нет сохраненного сопряжения для отладки по Wi-Fi + Введите текущие IP-адрес и порт отладки по Wi-Fi + + Время ожидания окна сопряжения истекло. Повторите попытку из DroidDeck. + Не удалось найти окно сопряжения (%1$d). Убедитесь, что Wi-Fi включен. + Не удалось найти окно сопряжения: %1$s + Введите все 6 цифр кода + Сопряжение… + Код не подошел. Снова откройте окно сопряжения и введите новый код. + Сопряжено. Подключение… + Сопряжено, но порт отладки по Wi-Fi не найден. Не отключайте отладку по Wi-Fi и повторите попытку. + Применение настройки… + Ошибка команды отладки по Wi-Fi + Настройка отладки по Wi-Fi + Запрашивает код сопряжения для отладки по Wi-Fi, пока открыты Настройки + Сопряжение отладки по Wi-Fi + Включите отладку по Wi-Fi и нажмите «Сопряжение устройства с помощью кода». + Включите отладку по Wi-Fi и нажмите «Сопряжение устройства с помощью кода». Не выходите из Настроек: код вводится в этом уведомлении. + 6-значный код сопряжения + Нажмите «Ввести код» и введите 6 цифр, показанных в Настройках. + Введите код сопряжения Wi-Fi + Введите код еще раз + Ввести код + Настройка Steam + Steam готов + Нажмите, чтобы вернуться в DroidDeck. Теперь отладку по Wi-Fi можно отключить. + Настройка отладки по Wi-Fi остановлена + + Сервер загрузки ответил HTTP %1$d + Размер загруженного файла не совпал с опубликованным APK, файл удален — повторите попытку + Загруженный файл не совпал с контрольной суммой и был удален — повторите попытку + Обновление предназначено для пакета Android %1$s, а не %2$s + В метаданных обновления не указан ключ подписи релизов DroidDeck + Android не удалось прочитать загруженный APK + Загруженный APK предназначен для %1$s, а не %2$s + В загруженном APK указан versionCode %1$d, а опубликован %2$d + Android не установит versionCode %1$d поверх установленного %2$d + В загруженном APK нет сведений о подписи + Загруженный APK не подписан ключом релизов DroidDeck + Каталог обновлений ответил HTTP %1$d + + Запуск сессии… + Прошло %1$d:%2$02d · работа продолжается + Загрузка обновления клиента Steam · %1$d%% + Загрузка клиента Steam · %1$s + + Инструмент совместимости уже устанавливается + Остановите текущую сессию перед установкой инструментов совместимости + Не найден выпуск %1$s для ARM64 + Для установки %1$s нужно не менее 4 ГБ свободного места плюс размер загрузки + Загрузка %1$s %2$s + Повторная загрузка · попытка %1$d из %2$d + Загрузка прервана + Ошибка загрузки; частично скачанный файл сохранен, чтобы повторная попытка продолжила с того же места + Загрузка превысила размер выпуска и была удалена + Загрузка не завершена; частично скачанный файл сохранен, чтобы ее можно было продолжить + Проверка загрузки + Не удалось прочитать контрольную сумму выпуска; повторите попытку + %1$s %2$s не публикует контрольную сумму; ничего не установлено + Контрольная сумма не совпадает; загруженный файл удален + Во время загрузки запустилась сессия; остановите ее перед установкой инструментов совместимости + Установка %1$s + Ошибка установки %1$s (код выхода %2$d) + Не удалось установить %1$s + Загрузка пакета droiddeck-esync + + с помощниками Linux + только DLL + Оригинал (Proton обновлен после последней замены) + Изменено вне «Компонентов» + отсутствует + Оригинал %1$s + Оригинал из %1$s + %1$s %2$s будет помещен в %3$s после закрытия запущенной на нем игры. + %1$s %2$s теперь в %3$s. Вступит в силу при следующем запуске игры. + Этот оригинальный набор не сохранен + %1$s в %2$s вернется к оригиналу после закрытия запущенной на нем игры. + %1$s в %2$s восстановлен из оригинала %3$s. + %1$s используется в %2$s. Сначала замените его в этом Proton на другой. + %1$s не сохранен + Удалено: %1$s. + Оригинал установленной сборки сохраняется: через него можно вернуться. + Не сохранено + Удален оригинал %2$s из сборки %1$s. + Загрузки доступны только из выпусков Nightlies + Этот список пакетов создан до появления контрольных сумм — обновите его и повторите попытку + Контрольная сумма не совпадает + + Линейный + Ближайший + Snapdragon GSR (качество) + Только резкость + Авто (по масштабированию) + + Без эффектов. + Игровая четкость + Резкость с легким добавлением контраста — на каждый день. + Яркий + Сильно усиленные цвета. Удаление полос включено, чтобы небо не шло полосами. + Кинематографичный + Глубже контраст, холоднее цвета, сглаженные края. + Соревновательный + Высветленные тени и сильная резкость — замечайте противников первым. + Адаптивная резкость + Только резкость, цвета не меняются. + Пленочный + Кривая как у кинопленки, слегка приглушенная. + Аркада + Броско и сочно. + Ретро-CRT + Эффекты кинескопа CRT и аналогового сигнала вместе. + Четкое увеличение + Для работы ниже разрешения экрана. + Чистые пиксели + Четкие пиксели для 2D и старых игр. + Аниме-контуры + Мультяшные контуры плюс резкость. + + GPU не Adreno + Поддерживается + Экспериментально: его драйверы — тестовые сборки + Ниже проверенного оборудования (Adreno 725 и новее): может не работать + Не поддерживается: DroidDeck требует GPU Adreno (Snapdragon) + + Превышен лимит запросов GitHub — повторите попытку позже + GitHub ответил HTTP %1$d + сервер загрузки ответил HTTP %1$d + Этот список драйверов создан до появления контрольных сумм — обновите его и повторите попытку + Контрольная сумма не совпадает — загруженный файл удален + Не драйвер AdrenoTools: %1$s + нет meta.json + не удается прочитать meta.json (%1$s) + это драйвер среды выполнения Linux (импортируйте его в разделе «Драйвер среды выполнения Linux») + это драйвер игр для Wayland, который этому приложению не нужен + в meta.json не указан libraryName, поэтому AdrenoTools нечего передать + в meta.json указан %1$s, которого нет в zip-архиве + %1$s не является 64-битной общей библиотекой AArch64 ELF + %1$s связан с glibc — это Turnip «-Linux», его место в разделе «Драйвер среды выполнения Linux» + В этом zip-архиве нет libvulkan_freedreno*.so. Zip-архив Turnip для Android (AdrenoTools) или -Wayland не является драйвером среды выполнения Linux. + %1$s не является 64-битной общей библиотекой AArch64 ELF. + %1$s не является драйвером glibc — он связан с libc Android. Среде выполнения Linux нужен zip-архив «-Linux»; обычный Turnip или Turnip «-Wayland» клиент Steam вообще не может загрузить. + Не пакет драйверов: нет manifest.json с указанием драйверов для Android и Linux + Не пакет драйверов: %1$s указывает за его пределы + Не пакет драйверов: после распаковки занимает больше %1$d МБ + + SD-карта + Папка + + Поделиться журналами сессии + + требуется Android 10 или новее (слои дисплея с цветовым пространством) + этот дисплей не сообщает о поддержке HDR + этот дисплей поддерживает %1$s, но не HDR10 + + этот GPU + Ограничение дочерних процессов включено + Ограничение дочерних процессов не задано + Не удалось проверить ограничение дочерних процессов + Ограничение дочерних процессов отключено + Отключать ограничение дочерних процессов не нужно + %1$s Если это невозможно, подключите устройство к компьютеру с ADB и выполните команду ниже. Затем вернитесь сюда и проверьте снова. + Сессии Steam могут запускаться. + Эта версия Android не использует это ограничение. + Включите «Отключить ограничение дочерних процессов» в разделе «Для разработчиков» и оставьте этот раздел включенным. + В этой версии Android нет такого переключателя. Измените настройку с этого устройства через отладку по Wi-Fi. + В этой сборке нет загрузки для этой копии DroidDeck. + Android не установит ее поверх этой сборки, так как ее versionCode %1$d ниже установленного %2$d. + Android не устанавливает более старую версию поверх новой. Оставайтесь на этой сборке, пока канал не догонит ее. + Эта копия DroidDeck подписана другим ключом (локальная сборка?), поэтому Android не может обновить ее на месте. + Недостаточно свободного места для обновления. + Обновление отменено. + Обновление не установлено: %1$s + Обновление не установлено. + Архив %1$s + zip-архив + Игра %1$s еще не запускалась + Файлы сохранений для %1$s не найдены + В %1$s нет папки drive_c, поэтому это не архив сохранений GameHub или Winlator + невозможно создать папки в %1$s + невозможно записать в %1$s + невозможно записать в %1$s (%2$s) + Папка недоступна + Имя %1$s уже занято другим файлом + Папка обложек недоступна + В папке уже есть чужой список синхронизации + Не удалось удалить %1$s + Файл выходит за пределы папки экспорта + FEX (сборка без названия версии) + нет profile.json в %1$s + %1$s не является пакетом компонента Linux (тип %2$s) + %1$s · свободно %2$s + Авто + Каталог обновлений не прошел проверку (%1$s) + Настройка дочерних процессов DroidDeck + Вход выполнен · автономный режим + Вход выполнен + ВСТРОЕННЫЙ + СКАЧАННЫЙ + ИМПОРТИРОВАННЫЙ + ANDROID + LINUX + %1$s. %2$s\n\n%3$s Экран сессии Разрешение Игры 16:9 @@ -1029,6 +1895,7 @@ Частота кадров %1$s Устанавливает масштабирование: %2$s. По размеру экрана (%1$d×%2$d) + По экрану (меняется при складывании и раскладывании) Steam Показать сведения об устройстве Скрыть сведения об устройстве @@ -1036,6 +1903,188 @@ Копировать Скопируйте эти сведения в отчет об ошибке. Скопировано. + Принудительный SSBS + Удерживает включенной настройку скорости CPU, которую Wine постоянно отключает. Может ускорить игры Windows на Snapdragon 8 Elite; на старых чипах разница почти не заметна. Вступит в силу при следующем запуске игры. + + Магазины + Steam + Свои + Магазины + Открывать магазин на + Уровень скорости загрузки + Движок магазинов + ● Движок готов + Не собран + Сбалансированный + Быстрый + Максимальный + Магазины + Настройки магазинов + Настройки + (вход не выполнен) + Магазин + Установлено + Библиотека + Все + Загрузки + %1$s (%2$d) + Войти + %1$s · выйти + Выйти из %1$s? + Установленные игры останутся. Библиотека загрузится снова, когда вы войдете. + Не выходить + Выйти + Поиск в каталоге %1$s… + Поиск установленных игр… + Поиск в библиотеке… + Поиск + Ничего не установлено + Загрузка + Нет игр + Загрузка магазина… + Нет каталога + Новинки + Скидки + Бесплатные игры + Популярное + Ваша библиотека + Играть + Установить + Продолжить установку + Прервано + Установить · %1$s + Получить бесплатно + Открыть в %1$s + Купить · %1$s + Установлено + В библиотеке + Бесплатно + Продолжить + Обновить + Показывать контент для взрослых + Epic + EOS + Офлайн + Выкл. + Вход в Epic (EOS) + Запускать офлайн + Устранить проблему входа в Epic + Облачные сохранения + Загрузить в облако + Скачать из облака + Нет облачных сохранений + Конфликт с облаком + Конфликт с облаком · файлов: %1$d + … + Оставить облачные + Оставить локальные + Проверка облака… + Последняя синхронизация: %1$s + Еще не синхронизировано + Файлов: %1$d + установлено + в вашей библиотеке + магазин + Достижения + Получено %1$d из %2$d + %1$d / %2$d + Получены · %1$d + Не получены · %1$d + Скрытые · %1$d + Скрытое достижение + Подробности появятся после получения. + Все достижения получены + Загрузка… + Войдите в %1$s, чтобы увидеть свои достижения. + Не удалось связаться с %1$s. Проверьте снова, когда будете в сети. + В %1$s нет достижений для этой игры. + Показать все (%1$d) + Достижения · %1$s + ✓ В Steam + Удалить + Удаление… + Удалить %1$s? + Освободит %1$s и уберет игру из Steam. + Удаляет файлы игры и ее ярлык в Steam. + Steam подхватит игру при следующем запуске; запустите ее оттуда или запустите Steam сейчас. + Proton + Выбор Steam для каждой игры + Аудио + DirectAudio + Компоненты + FEX, DXVK, VKD3D + Движок + %1$s · нативный + %1$s · встроенный + Как запускается + Получено из + %1$s, с вашим аккаунтом + Установлено в + Хранилище игр (Games/%1$s/%2$s) + Запускается через + Steam и Proton, так же как игры Steam + Steam и Proton · вход в Epic при каждом запуске, оверлей EOS отключен + Steam и Proton · заданы переменные Amazon Games Services + Steam + добавляется в Steam при установке (интерфейс Deck + Steam Input) + Нет загрузок + Приостановлено + В очереди + В очереди · №%1$d + Отменено + Ошибка + Ошибка · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Пауза + Продолжить + Отмена + Повторить + Убрать + Убрать завершенные + Манифест + Загрузка + Проверка + Установка + Готово + Проверка + Загрузка + Удалить загрузку? + Ошибка · %1$s сохранено + Проверка + Установка + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + Файлы: %1$d/%2$d + Менеджер загрузок + Одновременных загрузок + Установить в + Установка %1$s: + Внутреннее хранилище + Свободно: %1$s + Загрузки магазинов + Показывает ход загрузки игр и продолжает загрузки в фоне + Загрузка + Загрузок: %1$d + Подготовка… + Завершенные загрузки магазинов + Установлено: %1$s + Ошибка загрузки %1$s + Удалить + Оставить + Вход в %1$s + Вход через сторонний аккаунт + Перезагрузить + Закрыть + Повторить + Завершение входа… + Не удалось загрузить страницу входа: %1$s + Этот сервис не разрешает вход со встроенной страницы. Войдите с паролем от магазина. + Страница входа вернула ошибку (HTTP %1$d). + Подключение к странице входа не защищено. + Не удалось подтвердить вход. Повторите попытку. + Ошибка входа. Повторите попытку. Закрыть Назад Изменить @@ -1073,21 +2122,128 @@ %1$d уже в «Играх» %1$d уже в «Играх» - Достижения - Получено %1$d из %2$d - %1$d / %2$d - Получены · %1$d - Не получены · %1$d - Скрытые · %1$d - Скрытое достижение - Подробности появятся после получения. - Все достижения получены - Загрузка… - Войдите в %1$s, чтобы увидеть свои достижения. - Не удалось связаться с %1$s. Проверьте снова, когда будете в сети. - В %1$s нет достижений для этой игры. - Показать все (%1$d) - Достижения · %1$s + B · Назад к %1$s + игре + настройкам запуска + Графические драйверы + Этот Proton + Все Proton + Для всех Proton + Доступно · %1$d + Проверено %1$s + Проверить новые компоненты и драйверы + Получить + %1$s · DXVK, VKD3D-Proton и FEX ниже относятся к этому Proton + %1$s · проверено + %1$s · не проверено + %1$s · не поддерживается + Поддерживает рекомендуемую пару актуальной + Авто · %1$s + Драйверы среды выполнения и дисплея по отдельности + Использовать %1$s? + Удалить %1$s? + Диагностика + Готово к игре + Пока не готово к игре + среда выполнения %1$s + ограничение процессов отключено + вход как %1$s + Когда требуется ваше участие + Внешний вид + Это устройство + Обложки библиотеки + ПРИЛОЖЕНИЯ LINUX + Flathub + Приложения для рабочего стола Linux, запускаются на рабочем столе. + Настроить Flathub + Удалить %1$s? + Его настройки и файлы сохранятся. + Оставить + Размер на диске + Удалить рабочий стол? + Все + Достижения GOG + Синхронизирует достижения для поддерживаемых игр GOG. Применится при следующем запуске. + Достижения отключены + Достижения включены + Войдите в GOG снова + Достижения отправлены + Отправка достижения в ожидании + Включите достижения GOG в настройках магазинов + Достижения недоступны; проверьте журналы сессии + Достижение открыто + Вставить + Крестовина + Левый стик + Правый стик + Меню + Клавиатура + Крестовина вверх + Крестовина вниз + Крестовина влево + Крестовина вправо + Сенсорное управление + Сенсорный контроллер + Мышь + Вкл. + Выкл. + Медленно + Обычно + Быстро + Изменить раскладку + Меню DroidDeck + Перетащите элементы управления на экран + Расположить + Отменить + Сохранение… + Значок + Ш− + Ш+ + В− + В+ + Скрыть + Цвет и непрозрачность… + Скопировать эту раскладку + Раскладка скопирована; вставьте её в сенсорную раскладку любой игры + Вставить скопированную раскладку + Восстановить раскладку Steam + У этого элемента нет привязки в Steam, чтобы назначить значок + Значок для %1$s + Цвет сенсорной раскладки + Готово + Не удалось сохранить сенсорную раскладку + Сенсорная раскладка сохранена в Steam + Приложение %1$d + Подпись (необязательно) + Значки Steam не найдены + + %1$d значок из Steam + %1$d значка из Steam + %1$d значков из Steam + %1$d значка из Steam + + Цвет значка + Цвет кнопки + Без значка + + Проверка входа… + Получение вашей библиотеки… + Получение списка игр… + Загрузка сведений об играх… (%1$d/%2$d) + + Синхронизация %1$d игры… + Синхронизация %1$d игр… + Синхронизация %1$d игр… + Синхронизация %1$d игры… + + Синхронизация… %1$d/%2$d + Получение обложек… + Синхронизация не удалась + В вашей библиотеке %1$s игр не найдено + Amazon Games не ответил насчёт вашей библиотеки: обновите или войдите снова + Не удалось связаться с GOG + Не удалось прочитать вашу библиотеку GOG + Регистрация в Steam… Разработчик Издатель Дата выхода @@ -1096,4 +2252,34 @@ Полностью %1$s ч %1$d мин - \ No newline at end of file + Cardboard VR + Режим Cardboard + Изображения для левого и правого глаза для VR-очков под телефон. + Изображение + Виртуальный экран + Стереопара 3D + Очки + Cardboard + Cardboard (2014) + Без линз + Расстояние между глазами + Ширина экрана + Расстояние до экрана + Отслеживание головы + На этом устройстве нет датчика поворота. + Отцентрировать вид + Отцентрировать + SteamVR (экспериментально) + Установить + Скачивает SteamVR от Valve для ARM64 (571 МБ). + Скачивание и распаковка SteamVR… + Запустить + Запускает SteamVR, где гарнитурой служит ваша голова; изображение переключается на стереопару 3D. + Остановить + SteamVR запущен. + Недоступно в этой сессии. + SteamVR не отвечает; проверьте журнал сессии. + Скачать компоненты + Загрузки общие со страницей «Компоненты». Выберите выше скачанную версию, чтобы использовать её при следующем запуске игры. + Выберите версию для скачивания + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml new file mode 100644 index 000000000..76c1eb376 --- /dev/null +++ b/app/src/main/res/values-sv/strings.xml @@ -0,0 +1,2235 @@ + + + DroidDeck + + Första starten laddar ned Steam. Det kan ta några minuter. + Som standard pausas Steam när du lämnar appen. Det fortsätter där det slutade när du kommer tillbaka. + Din handkontroll visas som en Xbox 360-kontroll. Annars kan du använda skärmkontrollerna. + + + Första starten konfigurerar skrivbordet. Det kan ta några minuter. + Som standard pausas sessionen när du lämnar appen. Den fortsätter där den slutade när du kommer tillbaka. + Pekskärmen fungerar som en pekplatta: dra för att flytta pekaren, tryck för att klicka. + Din handkontroll visas som en Xbox 360-kontroll. Annars kan du använda skärmkontrollerna. + + Session + Visas medan en Steam- eller skrivbordssession är öppen + Steam körs + Steam-nedladdning pågår + Sessionen är pausad - tryck på Återuppta när du är redo + Återuppta + Pausa nu + Stoppa sessionen + Bildrutegenerering + Välj läge för bildrutegenerering + Av + Win-FG %1$d× + LSFG %1$d× + LSFG adaptiv %1$d + Kräver Lossless Scaling på Steam + Installera Lossless Scaling i Steam eller importera Lossless.dll + Importera Lossless.dll… + Använder kopian från Steam + Använder en importerad kopia + Välj Lossless.dll + Lossless.dll har importerats + Den här Lossless.dll används redan + Lossless Scaling ägs inte på det här Steam-kontot + Filen är inte Lossless.dll från Lossless Scaling + Kunde inte förbereda LSFG från den här Lossless.dll + LSFG kräver Lossless Scaling från Steam eller en importerad Lossless.dll + LSFG kunde inte förbereda sina shaders från Lossless.dll + %1$s: stöds inte + %1$s: motorn misslyckades + %1$s: kräver Lossless Scaling + %1$s %2$s · startar + Lägger till ett Steam-bibliotek och söker igenom det efter Windows-spel. Spel du äger använder sina Steam-poster; resten blir icke-Steam-genvägar. + Spelmiljö + Miljövariabler för Proton-spel. + Redigera + Alla Proton-spel + Profil + Steam-app-ID + Öppna + Spel %1$s + Lägg till variabel + Variabelnamn + Värde + Spara + Ta bort + Ärv igen + Rensa miljöåsidosättningar + Ta bort värdet i den här profilen + Inga variabler angivna. + Ärvd + Anpassad variabel + Kunde inte läsa in eller spara. Kontrollera ledigt lagringsutrymme och försök igen. + Använd ett giltigt namn och ett värde på högst 8192 tecken. + Den vanliga Proton-körmiljön läser inte den här variabeln. + Läser in… + Åsidosätter D3D12-funktionsnivån. 12_2 rapporterar DirectX 12 Ultimate-funktioner; funktioner som inte stöds kan misslyckas. Lämna tomt för automatisk identifiering. + Åsidosätter D3D12-shadermodellen. 6_9 kräver en tillräckligt ny VKD3D och matchande GPU-funktioner. Lämna tomt för automatisk identifiering. + Behåll Mesas shadercache aktiverad för att minska upprepad kompilering. false aktiverar cachning. + Största storlek på Mesas shadercache, t.ex. 1G. Större cache använder mer lagringsutrymme. + VKD3D-alternativ för D3D12. nodxr inaktiverar strålspårning; använd bara för ett spel som behöver det. + Ange 1 för en diagnostiklogg från Proton. Loggning kan sänka prestandan och tar lagringsutrymme. + Ange 1 för att använda OpenGL-baserade WineD3D i stället för DXVK vid kompatibilitetstester för D3D9–11. + Ange 1 för att hålla spelet borta från ntsync (droiddeck-ntsync eller en kärndrivrutin); det använder droiddeck-esync där ett paket finns, annars droiddeck-fsync. + Ange 1 för att stänga av droiddeck-esync för det här spelet; det använder droiddeck-fsync i stället, eller droiddeck-ntsync där även fsync är av. + Ange 1 för att hålla spelet borta från droiddeck-fsync; det använder droiddeck-esync där ett paket finns, annars droiddeck-ntsync. + Ange 1 för att köra spelet på droiddeck-fsync, eller 0 för att prioritera droiddeck-esync där ett paket passar dess Proton. + Ange 0 för att inaktivera Protons hjälp för handkontrollsnavigering i det här spelet. + DXVK-överlägg för D3D9–11. fps visar bildfrekvensen; utan värde döljs det. + DXVK-inställningar avgränsade med semikolon. Nyare versioner använder dxvk.maxFrameRate för bildbegränsning; använd hellre sessionens bildbegränsare vid vanligt spelande. + Gräns för bildfrekvens i D3D12. 0 inaktiverar gränsen. + Trådning för Mesa OpenGL. true kan hjälpa CPU-begränsade OpenGL-spel; testa för varje spel. + Wine-synk + Hur spel håller sina trådar i takt. Gäller från nästa session. + esync + ntsync + fsync + wineserver + FEX-förinställning + Gäller nästa gång ett spel startas. + Spel + FEX-standard + Använder FEX:s standardinställningar. + Stabilitet + Säkraste och långsammaste inställningarna. + Kompatibilitet + Full TSO med multiblock. + Mellannivå + Balanserad TSO med sänkt x87-precision. + Prestanda + All TSO inaktiverad; multiblock. + Prestanda + TSO + Prestandainställningar med huvud-TSO aktiverad. + Extrem + Inaktiverar kontroller av självmodifierande kod. JIT-spel och DRM kan sluta fungera. + Extrem + TSO + Extrema inställningar med huvud-TSO aktiverad. + Extrem-gn + Prestanda med TSC- och metadatatips. Prova före Extrem. + Denuvo + Prestanda med fullständiga kodkontroller och dold hypervisor-bit. + Anpassat värde + Välj värde + Emulatorer & appar + Lägg till + Din egen app eller ditt eget spel + Skript + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d %% + Startar… + %1$s har lagts till + %1$s har tagits bort + Skrivbord + Appen finns inte längre. Välj den igen från skrivbordet. + Öppna %1$s + Ta bort + Tryck igen för att ta bort + Stoppa sessionen för att ta bort + Kör %1$s från sin mapp, som länkas in i sessionen där den ligger. Om du tar bort appen lämnas mappen orörd. + Kör %1$s från en kopia av mappen i DroidDeck. Om du tar bort appen raderas kopian, inte originalet. + Installera Linux-körmiljön först + Filen finns inte längre + Inte tillräckligt med ledigt utrymme: att kopiera mappen kräver %1$s + Kopierar %1$s + Det gick inte att lägga till appen + Söker efter en utgåva av %1$s + Kunde inte läsa utgåvorna av %1$s + Ingen utgåva av %1$s har en AppImage för ARM64 eller x86_64 + Laddar ned %1$s + Nedladdningen misslyckades + Det gick inte att uppdatera appen + Ikonen kunde inte läsas + %1$s har sparats + Redigera + Sök efter uppdateringar + Söker efter uppdateringar… + Uppdatera till %1$s + Senaste versionen · %1$s + %1$s har uppdaterats + %1$s · %2$s + Lägg till en app + Skript + AppImage + GitHub + Flatpak + En .sh-fil. Den körs från sin egen mapp, i helskärm under gamescope. + En ARM64-AppImage (aarch64), eller en x86_64-AppImage som körs via FEX. Den packas upp i Linux-körmiljön en gång. + Installerar ARM64-AppImagen från repots senaste utgåva, eller dess x86_64-variant via FEX. + Använd FEX + Auto + På + Av + FEX kör Linux-program för x86 på den här ARM64-enheten. Auto använder det när programmet är byggt för x86. + %1$s via FEX + Programmet är byggt för %1$s-datorer och körs via FEX, som inte har konfigurerats än. Öppna Steam en gång medan du är inloggad: DroidDeck låter Steam ladda ned FEX och den Steam Linux Runtime som behövs, sedan öppnas detta. + Det här programmet + %1$s är byggt för x86-datorer och körs via FEX, som inte är installerat än. Starta Steam en gång medan du är inloggad och låt det ladda ned FEX och Steam Linux Runtime. Starta sedan detta igen. + Ett app-ID från Flathub eller appens Flathub-sida. Flatpak konfigureras först om det inte redan är gjort. + Fil + Ingen fil vald + Välj fil + Ändra + Välj ett skript + Välj en AppImage + Välj en ikon + Repo + ägare/repo eller en github.com-länk + Inte ett GitHub-repo + App-ID + org.ppsspp.PPSSPP + Inte ett app-ID från Flathub + Eller en .flatpak-fil, för en app som inte finns på Flathub. + Välj en .flatpak-fil + Källa + På ditt skrivbord + Ny app + Namn + Från appen + Ikon + Standardikon + Välj ikon + Rensa + Från repot + Söker efter ikoner… + Standard + Redigera %1$s + Spara + Kontrollerar mappen… + Mappen länkas in i sessionen: %1$s + Mappen innehåller Linux-program, som inte kan köras från delad lagring, så den kopieras till DroidDeck (%1$s). + Lägg till + Avbryt + Installera Linux-körmiljön från Konfiguration först. + Vänta tills %1$s är klart. + Kompatibel + Versioner märkta ”%1$s” i Steams kompatibilitetsmeny har DroidDecks prefixkorrigeringar och startar spel här. Standard är Proton Experimental ARM64 (%1$s), eller Proton 11.0 ARM64 (%1$s) när den nyare versionen är installerad. Valves egna Proton-poster utan märkningen kan inte starta spel på den här enheten. + Installerad %1$s · %2$s + Avbryt + Auto + Aldrig + Alltid + Avinstallera + Kontrollera + Uppdatera + Kontrollerar… + Arbetar… + Glöm + Lägg till… + Använd + Gäller från nästa session. + Steam-session + Skrivbordssession + Skärm + Upplösning + Upp till 720p + Upp till 900p + Upp till 1080p + Inbyggd + %1$s (standard) + Anpassad · %1$d×%2$d + Anpassad… + 720p kan få menyer att reagera snabbare. + Bildförhållande + Bestäms av den anpassade upplösningen. + Auto använder minst 16:9. + Bildgräns + Begränsar hela sessionen. Skärmen byter till en uppdateringsfrekvens som är en jämn multipel av gränsen. + HDR + HDR10-utgång + Inte tillgängligt: %1$s. + Starta om appen för att tillämpa. + Drivrutiner + GPU-drivrutiner + Delas av Steam och skrivbordet. Ställs in på sidan Komponenter. + Pekskärm & kontroller + Pekskärm + Pekplatta + Direkt + Auto: pekplatta på skrivbordet, direkt i Steam. + Skärmkontroller + Steam + QAM + Auto visar kontrollerna när ingen handkontroll är ansluten. + Steam-pekstyrning (beta) + Steams pekstyrning startar med nästa Steam-session + Steams peklayout + Flytta, ändra storlek på eller dölj spelets pekkontroller; sparas i Steam + Handkontroll + Hur Steam ser din handkontroll. Gäller från nästa session. + Steam Deck-kontroll + Steam Controller + DualShock 4 + 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 + Meny → QAM + QAM → Meny + Session + Bakgrundsbeteende + När appen är dold eller skärmen stängs av. + Manuell + Inbyggd + Auto pausar och återupptar när den visas. Inbyggd förbereder Steam för vila först. Manuell väntar på Återuppta. Aldrig fortsätter köra. + Håll nedladdningar igång + Skjut upp bakgrundspausning medan Steam aktivt laddar ned. + Auto pausar i bakgrunden och återupptar när den visas. Manuell väntar på Återuppta. Aldrig fortsätter köra. + Uppstart + Starta Steam med DroidDeck + Öppnar Steam-sessionen när DroidDeck startar. + Decky + Söker efter utgåvor… + Redo att installera %1$s. + Inget kompatibelt bygge hittades. Försök igen senare. + Installerad · %1$s + Uppdatering tillgänglig · %1$s + Senaste versionen · %1$s + Loader + Installera senaste + Startar med Steam. Medan det är på kan andra appar på enheten eventuellt styra Steam. + Av. Steams fjärrfelsökningsport förblir stängd. + Avinstallera Decky Loader? + Tar bort inläsaren. Dina tillägg och inställningar behålls. + Klient + Prestandaöverlägg + Stäng av om spel kraschar eller blir svarta i Deck-läge. Gäller från nästa session. + Reparera Steam-klienten + Laddar ned Steam-klienten igen nästa gång Steam startar. Använd det om Steam fastnar på sin inläsningsskärm. Spel, inloggning och inställningar behålls. + Steam-klienten laddas ned igen nästa gång Steam startar. + Reparera + Grafik + SteamGridDB API-nyckel + Din nyckel + Redigera… + Fyller i saknad grafik från Steam, sedan SteamGridDB. Din egen cover.jpg, hero.jpg, logo.png eller icon.png i spelmappen har företräde. + Ta bort %1$s + Ta bort? + Lägg till spel + Lägg till ett spel: + Välj exe-filen: + Den här platsen är inte tillgänglig i sessionen + Kontrollera den här exe-filen + Windows-komponenter + LB/RB hoppar mellan avsnitten. + Ingen + Windows-DLL:er som Proton inte innehåller, från komponentlistan. Slå på de som det här spelet behöver: de hamnar i spelets egen Windows-mapp, aldrig bland dess filer, och finns kvar oavsett vilken Proton det körs på. + Kunde inte nå komponentlistan. Komponenter som redan laddats ned kan fortfarande växlas. + Rekommenderas för det här spelet + Kontrollerar vad det här spelet behöver… + Tillgängligt när du är uppkopplad + + Slår på %1$d komponent… + Slår på %1$d komponenter… + + På för det här spelet + Steam installerar %1$s med %2$s (app %3$s) + Från GOG:s lista för det här spelet + Från Epics förutsättningar för det här spelet + Från Amazons installationssteg för det här spelet + Steam installerar %1$s med app %2$s, matchad på namn + Listas även + Listad av %1$s; inte en komponent här + Steam + GOG + Epic Games + Amazon Games + Spelet levereras med %1$s + Spelet har %1$s (fungerar ofta som det är) + Redan i det här spelet + Inget i spelets mapp pekar på en komponent. + Installerade för det här spelet (%1$d) + Alla komponenter (%1$d) + Behöver ett Windows-installationsprogram (%1$d) + Dessa är egna installationsprogram utan något paket inuti att plocka isär, och det finns ännu ingen uppteckning av vad de installerar. De flesta är gamla körmiljöer som Mono inuti Proton redan ersätter; uppteckningar läggs till när de görs. + Visa + Dölj + Körs med sitt eget installationsprogram (ingen uppteckning än) + Stöds inte här än + Läser in komponenterna… + Kunde inte installera: %1$s + Nedladdad + Laddar ned %1$s + Installerar: %1$s + Stänger av… + %1$s nedladdning + Börjar gälla nästa gång spelet startar. + Sträck ut 16:9-spel + På skärmar som är fyrkantigare än 16:9 sträcks spel ut för att fylla skärmen i stället för att visa svarta ränder. Gäller från nästa session. + Ljud + DirectAudio för spel + Kringgår PulseAudio för lägre latens. + Mikrofon + För röstchatt. + Nätverk + Wi-Fi-identifiering + Visa Wi-Fi-namn och nätverk i närheten i Steam. + Platstjänster är av. + Platsbehörighet har inte beviljats. + Öppna inställningar + Fortsätt + Aktivera Wi-Fi-identifiering? + Android kräver platsbehörighet för att läsa Wi-Fi-namn och upptäcka nätverk i närheten. DroidDeck använder den här åtkomsten för att visa dessa nätverk i Steam. Vi spårar inte din plats. Internetåtkomst fungerar utan den här behörigheten. + Spellagring + Automatiskt – SD-kortet när det finns, plus dina Games-mappar + Automatiskt – dina Games-mappar, plus SD-kortet när det finns (inget nu) + Endast internt + Mapp: %1$s + Andra biblioteket + Spel som strömmar tillgångar från SD-kortet eller delad lagring kan hacka. + Lagringsdiagnostik + Registrerar allokeringsresultat och uppmätta lagringstider i Dela loggar. Kan göra installationer långsammare medan funktionen är aktiverad. Gäller från nästa session. + Välj en mapp… + Anpassad upplösning + Ersätter gränsen och bildförhållandet. En storlek som inte matchar skärmen får ränder. + Bredd + Höjd + Mellan 320×240 och 3840×2160. + + %1$d spel + %1$d spel + + OK + Ta bort + Återställ + Ladda ned + Ta bort %1$s + %1$.1f MB + Körmiljödrivrutin + Används av Steam, spel och skrivbordet. Gäller från nästa session. + Importera Turnip-zip… + Skärmdrivrutin + Ritar bildrutor på skärmen i båda lägena. Starta om appen för att verkställa. + Importera en AdrenoTools-zip… + aldrig + Nattbyggena kontrollerade %1$s + Komponenter + Om komponenter + Sök efter nya drivrutiner + Sök i nattbyggena efter nya paket + Proton + Den Proton som väljs här kör varje spel som inte har valt sin egen i Steam, och en ändring som görs i Steam visas även här. Steam ersätter Valves Proton när den uppdateras; originalen behålls för varje bygge. + Konfiguration + Konfigurera profil + Profil + Standardprofil + Används automatiskt av spel utan åsidosättningar. + Sparade inställningar som används automatiskt när %1$s startar. + Välj bara de inställningar det här spelet behöver. Allt annat fortsätter att använda standardprofilen. + FEX-förinställning + Använder standardprofilens förinställning. + Anpassad förinställning sparad för det här spelet. + Anpassade FEX-variabler + Använd standard + Spelkompatibilitet + Miljöalternativ för Proton, DXVK, VKD3D och synkronisering för det här spelet. + Använder standardprofilens Proton. + Anpassad Proton sparad för det här spelet via Steams kompatibilitetsmappning. + Ingen Proton + DXVK + VKD3D-Proton + Använder standardprofilens paket från %1$s om inte det här spelet väljer ett annat paket. + Använder standardprofilens komponentpaket. + Använder %1$s bara för det här spelet. + Körmiljöalternativ + Övriga miljöalternativ per spel för Proton, DXVK, VKD3D-Proton och synkronisering. + %1$s:s profil är sparad för nästa spelstart. + Starta utan Steam + För ett spel som stängs direkt: startar det utan Protons Steam-inställning. Steam-funktioner som overlay är avstängda för det. + På + %1$s:s Proton-val är sparat och tillgängligt i Steam nu. + %1$s:s Proton-val är sparat. Starta om Steam innan du startar spelet. + %1$s kan inte använda den valda Proton på den här enheten. + Kunde inte läsa in %1$s:s profil: %2$s + Spelprofilen kunde inte sparas: %1$s + Återställ spelprofil + Återställ %1$s:s profil? + Ta bort det här spelets Proton, FEX, komponentpaket, miljö, texturfiltrering och Windows-komponentåsidosättningar? Det kommer att använda standardprofilen. + %1$s använder nu standardprofilen. + Spelprofilen kunde inte återställas helt: %1$s + %1$s körs nu för varje spel som inte har valt sin egen Proton i Steam. + %1$s kommer att köras för varje spel som inte har valt sin egen Proton, från nästa gång Steam startar. + %1$s kan inte starta spel här och kan därför inte vara standard. + Standard-Proton kunde inte sparas: %1$s + %1$s · %2$d utbytta + %1$s · allt är original + %1$s · spel körs + %1$s · droiddeck-esync-paket + %1$s · inget droiddeck-esync-paket än + %1$s · esync inbyggt, inget paket behövs + GPU-drivrutiner + %1$s · återapplicerad vid start %2$s + Nästa: %1$s · efter att spelet stängs + %1$s används: %2$s + Väntar tills spelet stängs + Gäller nästa gång ett spel startar + Importera .zip + Läser in Proton-versioner… + Ingen Proton installerad än. Starta Steam en gång för att ladda ned dess ARM64-Proton, eller lägg till GE-Proton, CachyOS eller DroidDeck-Proton från Konfiguration. + Original · %1$s + %1$s · det här bygget + %1$s · tidigare bygge + Installerade · %1$d + Inget sparat för %1$s än. Den här Protonens egna filer sparas som dess original. + Ett spel körs på den här Proton, så detta väntar tills det stängs. + Gäller nästa gång ett spel startar. + Byt ut %1$s? + Lägg %1$s i %2$s? De ursprungliga filerna förblir sparade. %3$s + Återställ %1$s? + Lägg tillbaka originalet av %1$s i %2$s? %3$s + Tar bort paketet från appen. Du kan ladda ned det igen. + Ta bort det här originalet? + Det här originalet av %1$s är från ett tidigare bygge av %2$s. Originalet från det aktuella bygget behålls alltid. + I nattbyggena · %1$d + Uppdatera för att lista paket i nattbyggena. + Inget nytt sedan senaste kontrollen. + Importera .wcp + Återställ den här Protonens original + Lägg tillbaka %1$s:s original %2$s? + Byt + FEX, DXVK och VKD3D-Proton för varje Proton som Steam kör spel med.\n\nVälj en installerad rad för att byta till den. Det börjar gälla nästa gång ett spel startar, eller efter att det körande spelet stängs.\n\nDina val kontrolleras igen före varje start och återställs om något har ändrat dem. Varje Protons originalfiler behålls, så Steam-uppdateringar förlorar dem aldrig. + Original + I bruk + Ny + Sparad + Prestanda + CPU-kärnor, sessionsfixar och värdnamnet. Gäller från nästa session. + Steam-klientens kärnor + Åsidosätt Steams kärnval + Fäster Steam, dess UI-hjälpprocess och gamescope vid kärnorna nedan. + Klientkärnor + Kärnor för Steam. + Slå på åsidosättningen för att välja. + Spelkärnor + Spelkärnor + Kärnor för spel. Med alla valda schemalägger Android fritt. + Klientgränssnitt + Trådad GL + Kan göra Steams menyer mer responsiva. + Zink: lata deskriptorer + Rekommenderas för drivrutiner utan deskriptorbuffertar. + Hoppa över GL-felkontroller + Stänger av GL-validering per anrop. + gamescope: GPU-kö i realtid + Prioriterar kompositorns GPU-arbete. Kan jämna ut bildtakten till viss kostnad för spelen. + GPU + Håll GPU:n på dess högsta klockfrekvens + Bara Adreno. Färre hack, högre batteriförbrukning och mer värme. + Sessionsfixar + Turnip: sysmem-rendering + Snabbare för Steam och de flesta spel. Krävs på Adreno 710/720/722. + Hoppa över Steams xalia-hjälp + droiddeck-ntsync (experimentell) + En ersättning i användarutrymmet för Linux ntsync-drivrutinen, för testning. Medan det är på använder Proton det i stället för droiddeck-esync och droiddeck-fsync. Fungerar med Proton Experimental ARM64, GE-Proton och proton-cachyos. Gäller från nästa session. + droiddeck-esync + droiddeck-fsync först + Kör Protons egen fsync på de ursprungliga Proton-filerna, med futex_waitv tillhandahållen i användarutrymmet, så det behöver inget paket och fortsätter fungera så snart Steam uppdaterar Proton. Medan det är på använder spel det före droiddeck-esync, förutom de få som Proton håller borta från fsync. Gäller från nästa session. + Spelens Windows-trådar väntar på varandra via eventfd och delat minne i stället för tur-och-returer genom wineserver. Använder ett droiddeck-esync-paket som är gjort för exakt det Proton-bygget; paket hämtas automatiskt. Tills ett sådant finns för ett nytt Proton-bygge används droiddeck-fsync i stället. Gäller från nästa session. + Inaktiverar Protons hjälp för handkontrollsnavigering, vilket kostar CPU i alla spel. + Kör proot utan seccomp + Kan åtgärda fel med saknade systemanrop på vissa Linux-kärnor. Kan minska prestandan. + proot snabbväg + Kräver proots seccomp-filter. + Snabbare filuppslagningar och mindre hack. Stäng av om ett program inte hittar sina filer. + Sessionsidentitet + Värdnamn + Namnet Steam ser för den här enheten. Lämna tomt för att återställa till %1$s. + Endast bokstäver, siffror och bindestreck inuti, upp till 63 tecken. + Android är inställt på att avsluta den här sessionen + Installerad + Tillgänglig att ladda ned + Inget nytt sedan senaste kontrollen. + Återställ inbyggda drivrutiner + Inbyggd i appen: detta tar bort dess uppackade filer och döljer den. Återställ inbyggda drivrutiner tar tillbaka den. + Hälften av ett Android + Linux-paket: båda halvorna tas bort. En drivrutin som används faller tillbaka till standard. + Dess filer tas bort från appen. + Dess filer tas bort. Den används, så standarden tar över. + Skärm + Effekter + Kontroller + Komponenter + Handkontroll + Inställningar + %1$s-sidan + Steam + Skrivbord + Steam-meny + Snabbåtkomst + Öppna snabbåtkomstmenyn + Föregående sida + Nästa sida + Prestanda-HUD + Skalningsläge + Skalningsskärpa + Skärmeffekter + Live. Kostar lite GPU-tid medan någon effekt är på. + Förinställningar + Anpassad + Avancerat + Expanderad + Hopfälld + Skärpning (CAS) + Skärpningsnivå + Falsk HDR + Avbandning + Avbandningsstyrka + Ljusstyrka + Kontrast + Gamma + Mättnad + FXAA (kantutjämning) + Toon-konturer + CRT + NTSC + Texturfiltrering + DirectX 9–11-spel, från nästa start. + Anisotropisk filtrering + Texturskärpa + En negativ mip-bias: skarpare texturer, lite skimmer. Auto matchar hur mycket sessionen förstoras till skärmen. + Auto (%1$s) + Tangentbord + PC-tangentbord + Android + Andra skärmen + Läge + Visar + Android-appar + Starta en app + Dölj + Visa + Inga startbara appar + Sessionsbeteende + Gäller nu och för framtida sessioner i det här läget. + Nästa session + Support + Sessionsloggar + Bifoga till en felrapport + Dela loggar + Bakgrund + Stoppa + Stoppa sessionen? + Körs nu + Inget spel körs på en Proton + Spelprofil + Profildetaljer är inte tillgängliga än. + Kunde inte läsa in spelprofiler. Försöker igen automatiskt. + Kunde inte uppdatera spelprofiler. Visar senast tillgängliga data; försöker igen automatiskt. + Visa och redigera de sparade inställningarna som används vid nästa start. + Använder standardprofilen + Anpassad profil + Använder %1$s från standardprofilen. + Ändringar sparas i %1$s:s profil och gäller nästa gång spelet startar. + + %1$d spelåsidosättning + %1$d spelåsidosättningar + + Ingen Proton är installerad än. + spel körs + återapplicerad vid start %1$s + Original + Importera och ta bort på sidan Komponenter. + Klart + Behöver åtgärdas + Arbetar + Kontrollera IP-adressen och porten för Trådlös felsökning, eller para ihop enheten igen om Android tog bort den. + Installera + Hantera + Översikt + Handkontroll + Session + Startprogram + Konfiguration + ● Allt klart + %1$d av %2$d klara + Systemkontroll + Enheten stöds + Otestad GPU + GPU stöds inte + %1$s · %2$s + %1$s: %2$s + Steam behöver en Adreno-drivrutin; %1$s kan visa en svart skärm + Linux-körmiljö + %1$s · %2$d %% + Inte installerad · cirka 3 GB, installeras första gången du trycker på Play + Installerad + %1$s · uppdatering tillgänglig + %1$s · senaste versionen + Gräns för underprocesser + På · Android kan stänga Steam. Det tar en minut att stänga av den. + Inte inställd · Android kan stänga Steam. Det tar en minut att stänga av den. + Kunde inte kontrolleras · det tar en minut att stänga av den. + Åtgärda + Detaljer + Utvecklaralternativ + Använd Trådlös felsökning + Kontrollera igen + Slå på gränsen + Steam-konto + Inloggad som %1$s · offlineläge + Inloggad som %1$s + Tryck på Play och logga in på Steam + Verktyg + Handkontroll + Handkontrollsinställningar är inte tillgängliga. + Session + Sessionsloggar + Sparas efter varje session + Felsökningsverktyg + Senaste sessionsloggar + Sparade sessionsloggar + Behåller de %1$d senaste + Rensa loggar + Offlineläge + Logga in på Steam först + Startprogram + Språk + Steam använder det också, från nästa gång det startar. + Systemstandard (%1$s) + Systemstandard + Appskala + DroidDecks text och kontroller + %1$d %% (Standard) + Tema + Startprogrammets utseende + Använd som startskärm + DroidDeck kan vara startskärmsappen + DroidDeck erbjuds aldrig som startskärmsapp + Orientering + Automatiskt liggande + Liggande + Liggande, upp och ned + Helskärm + Döljer Androids statusfält och navigeringsfält + Visar Androids statusfält och navigeringsfält + Standardapp för startskärmen + Välj en app för startskärmen + Välj + Linux-appar (beta) + Filer + Bläddra och hantera filer + Proton-versioner + Installera ARM64-byggen av Proton + Prestanda + Tilldelning av CPU-kärnor + ROM-mapp + Där emulatorspel lagras + Komponenter + FEX, DXVK, VKD3D + Kontroller + Knappmappning + Upptäck + Sök + Installerade + %1$s · %2$d + Butik + Flathub · ARM64 + ● Flatpak redo + Installera Linux-körmiljön från Konfiguration först. + Konfigurera Flatpak + Lägger till Flatpak och ansluter det till Flathub (cirka 20 MB). Den första appen laddar även ned sin körmiljö. + Konfigurerar… + Flatpak + Uppdateringar + %1$s · %2$s + %1$s · %2$s · %3$d %% + Startar… + Kategorier + Kunde inte nå Flathub. Kontrollera din anslutning och försök igen. + Försök igen + Sök på Flathub + Firefox, spel, emulatorer… + Sök + Alla + Söker… + Sök efter namn eller välj en kategori. Bara ARM64-appar listas. + Inga ARM64-appar på Flathub matchar det. + Resultat + Sök efter uppdateringar + Uppdatera alla (%1$d) + ● Allt är uppdaterat + Inga appar än. Hitta några under Upptäck eller Sök. + Konfigurera Flatpak för att installera appar. + Uppdatering tillgänglig · %1$s + Öppna + Detaljer + Ta bort + Tryck igen för att ta bort + Installerar… + Installera + Konfigurera Flatpak först + %1$s nedladdning + ● Installerad + Kunde inte läsa in appens Flathub-sida. + Läser in från Flathub… + Inget ARM64-bygge på Flathub, så den kan inte installeras här. + Skärmbilder + Om + App-ID + Licens + Körmiljö + Installerad storlek + %1$s (plus dess delade körmiljö) + Webbplats + Verifierad + Uppdateringar + DroidDeck %1$s · %2$s + Kontrollerad %1$s + Inte kontrollerad än + Låt DroidDeck uppdatera sig själv + På nästa skärm, slå på Tillåt från den här källan och kom sedan tillbaka. Uppdateringen fortsätter själv. + Öppna inställningar + Inte nu + Signerad annorlunda + Kan inte uppdateras på plats + Den här kopian är inte signerad med DroidDecks utgivningsnyckel, så uppdateringar kan inte installeras över den. Avinstallera den för att byta. + Uppdatering tillgänglig + Kan inte installeras + Redo att byta + Före Stabil + Du ligger före Stabil + Det här bygget är nyare än senaste Stabil. Du flyttas till Stabil vid dess nästa utgåva. + Test avslutat + Det här testet har avslutats + Dess fix har slagits samman eller slopats. Följ Förhandsversion för de senaste fixarna. + Senaste versionen + Du har den senaste %1$s + · %1$s + %1$s… %2$d %% + %1$s… + Uppdatera%1$s + Installera%1$s + Installera Stabil %1$s trots allt + Följ Förhandsversion + Stoppa den körande sessionen för att uppdatera. + Mindre + Mer + DroidDeck %1$s + Nytt bygge av Förhandsversion + Testbygge för PR #%1$d + %1$d MB + UPPDATERINGSKANAL + Stabil + Testade utgåvor, för de flesta + %1$s · %2$s + Förhandsversion + De senaste fixarna, före Stabil + Senaste versionen + Testbyggen + Prova en fix innan den släpps + Inga just nu + PR #%1$d + Inte byggd för den här kopian av DroidDeck + Uppdaterad %1$s + testet för PR #%1$d + för ett tag sedan + just nu + + %1$d att prova + %1$d att prova + + + Ändringar sedan ditt bygge · %1$d bygge efter + Ändringar sedan ditt bygge · %1$d byggen efter + + + Senaste ändringar · %1$d bygge + Senaste ändringar · %1$d byggen + + En Android-inställning att ändra + Steam + Android stoppar appar som startar många bakgrundsprocesser, och Steam startar dussintals. Tills detta ändras stängs spel utan felmeddelande. + Fixa det åt mig + Slå på ”Inaktivera begränsningar för underprocesser” och kom sedan tillbaka. Lämna Utvecklaralternativ på, annars stängs omkopplaren av igen. + Öppna Utvecklaralternativ + Den här Android-versionen har ingen omkopplare för det, så DroidDeck ändrar det via Trådlös felsökning. Tar ungefär en minut, ingen dator behövs. + Trådlös felsökning kräver Wi-Fi. Alla nätverk fungerar; det behöver inte internet. + Ihopkopplingskoden skickas i en avisering. Tillåt aviseringar för DroidDeck, eller använd Andra sätt. + Tillåt aviseringar + Öppna Inställningar → Utvecklaralternativ → Trådlös felsökning och slå på det + Tryck på ”Para ihop enhet med ihopkopplingskod” + Skriv in koden i DroidDeck-aviseringen + Väntar på popup-fönstret för ihopkoppling… + Popup-fönster hittat. Ange dess kod i aviseringen. + Status + Kontrollerar var 2 sekunder. Den här sidan stängs när det är av. + Andra sätt + Trådlös felsökning, manuell adress, dator-ADB + Manuell adress, dator-ADB, Utvecklaralternativ + Använd Trådlös felsökning i stället + Vissa ROM:ar lägger till omkopplaren trots allt. Leta efter ”underprocess” i Utvecklaralternativ. + Om aviseringen inte fungerar, ange popup-fönstrets adress och kod själv. + Ange adress manuellt + Från en dator med ADB: + Kopiera kommando + Välj vilken skärm Android-inställningar ska öppnas på. + Huvudskärm + Ihopkopplad. Letar efter porten för Trådlös felsökning… + Ihopkopplad. Ange porten från Inställningar. + Enhet hittad. Tillämpar inställningen… + Kunde inte tillämpas automatiskt. Kontrollera adressen nedan och försök igen. %1$s + Trådlös felsökning + Gränsen för underprocesser har uppdaterats + Ändra gränsen för underprocesser + Om alternativet saknas, para ihop via Trådlös felsökning. + Ihopkopplad. Kontrollera IP-adressen och porten i Trådlös felsökning. + Gränsen för underprocesser har uppdaterats. + Använd steg 2 om Utvecklaralternativ inte har någon inställning för underprocesser. Öppna Utvecklaralternativ → Trådlös felsökning → Para ihop enhet med ihopkopplingskod. + Ihopkopplad. Använd adressen nedan, eller uppdatera den från Trådlös felsökning → IP-adress & port. + Gränsen för underprocesser uppdaterades via Trådlös felsökning. + Android bekräftade att gränsen är på. + Android bekräftade att gränsen är av. + 2 · Para ihop Trådlös felsökning + Ihopkopplingsrutans IP-adress & port + Ihopkopplingskod (PIN) + 6 siffror + Para ihop + 2 · Tillämpa via Trådlös felsökning + Anslutningens IP-adress & port + Para ihop igen + Stäng av gränsen + Andra sätt att ändra det + 1 · Utvecklaralternativ (prova först) + Slå på ”Begränsa underprocesser”. + Slå av ”Begränsa underprocesser”. + 3 · Dator-ADB (sista utväg) + Bara om Trådlös felsökning inte är tillgänglig. + 3 · Dator-ADB (sista utväg) + När skärmkontrollerna visas i en session + Färg + Nyans på skärmknapparna + Anpassad + Opacitet + %1$d %% + Knappstorlek + 100 % är standardstorleken + Spakklick + Dubbeltryck och håll en spak för L3 eller R3 + Adaptiva spakar + Spelvibration + Steam-knappen + QAM-knappen + Tangentbordsknappen + Tryck på Select för att öppna Steam-menyn + I stället för View/Select. + Animationer + Spakarna visas där du trycker och döljs när du släpper + Layout + Anpassade positioner sparade + Anpassad till den här skärmen + Redigera + Återställ + Knappmappning + Varje knapp skickar sin egen inmatning + %1$d av %2$d knappar ommappade + Konfigurera + Återställ handkontroll + Återställer alla handkontrollsinställningar och layouten + Vad varje skärmknapp skickar till spelet. Dold tar bort knappen. + Skärmknappar + Standardvärden + Återställ mappning + Android-appar + %1$d appar + Inga startbara Android-appar hittades. + ● Skrivbord installerat + Installeras vid första öppningen + Tillgänglig att installera + Inte installerad. + %1$s · inte installerad + Öppna %1$s + Spel + Välj en ROM-mapp. + Bläddra till /root/ROMs i RetroArch. + Lägg till %1$s-spel i ROMs/%2$s. + Installera %1$s + Stoppa sessionen för att installera + Körmiljö krävs + Läser in installationsdetaljer… + Installationsdetaljer är inte tillgängliga. Försök igen senare. + Installera Linux-körmiljön från Konfiguration först. + Stoppa den körande sessionen för att installera skrivbordsappar. + Installerar %1$s i Linux-skrivbordet. + %1$s · %2$d %% + Att dölja tar bort den från Skrivbord. Dess filer finns kvar i Linux-körmiljön. + Det spelet finns inte längre i ROM-mappen. + Skrivbord · %1$s + Starta i %1$s + Även i %1$s + Den sidan finns inte längre. Välj ett avsnitt till vänster. + Öppna skrivbordet + Installera & öppna skrivbordet + Avinstallera skrivbordet + Ta bort skrivbordet och dess medföljande appar. Behåll spel, tillagda appar, inställningar och delade bibliotek. + Tar bort skrivbordet + Skrivbordet har tagits bort + Tar bort gamla LXQt-filer + Uppdatera DroidDeck innan du tar bort den här skrivbordsversionen. + Linux-skrivbord + KDE Plasma, Firefox och dina emulatorer + Spel du installerar i Steam visas här efter sessionen. + Spela Steam + Startinställningar + Starta + Spelfiler + Proton-prefix + Letar efter sparfiler… + Ingen sparmapp hittades efter namn. Export tar hela användarmappen. + %1$d sparmappar + %1$s · %2$d filer · %3$s + %1$d KB + Hantera sparfiler + Sparfiler + Importera sparfiler… + Exportera som GameHub-zip… + Exportera som Winlator-zip… + Öppna sparmappen + Aldrig spelat + Körmiljö upptagen · %1$d %% + Körmiljö upptagen + Körmiljön installeras första gången du trycker på Play + Uppdatering av körmiljön tillgänglig + ● Körmiljö redo + Senast spelad %1$s + Internt lagringsutrymme + Tillagt spel + Anpassat spel + Spellagring + Det bästa paret för den här GPU:n, hålls uppdaterat + Välj ett par själv + Söker efter drivrutiner… + Inga drivrutiner listade än. Uppdatera för att söka efter dem. + Dölj drivrutiner för andra GPU:er + Visa drivrutiner för andra GPU:er + Avancerat + Ställs in tillsammans med skärmdrivrutinen: %1$s innehåller båda. + Ställs in tillsammans med körmiljödrivrutinen: %1$s innehåller båda. + Används av spel och skrivbordet. + Standard för körmiljön + Ritar bildrutor på skärmen. Gäller efter att DroidDeck har startats om. + Auto – valt efter GPU + Ladda ned, importera eller ta bort… + I BRUK + Drivrutinerna som är inbyggda i DroidDeck + Körmiljö- och skärmdrivrutiner inställda tillsammans som ett matchat par. + Rekommenderas för den här GPU:n: %1$s. + Uppdatera för att söka efter drivrutiner för den här GPU:n. + REKOMMENDERAS + Inte tillgänglig + Linux-körmiljö + Förbereder Linux-körmiljön + Session + Startar skärm och ljud + Steam-klient + Kontrollerar Steam och dess kompatibilitetsverktyg + Öppnar Steam + Väntar på Steams första bildruta + Skrivbord + Förbereder skrivbordet + Öppnar + Väntar på den första bildrutan + Startar Steam + Sessionen avslutades + Steg %1$d av %2$d + Steam är pausat + Grepp + Vänster platta + Höger platta + Båda + Handkontroll + Styrspak + Styrplatta + Växla mellan styrspak och styrplatta + Fler Deck-kontroller + PANELER + Stäng kontrollerna på andra skärmen + Öppna Steam-menyn + Vänster · övre + Vänster · nedre + Höger · övre + Höger · nedre + Klick + Höger styrplatta + Vänster styrplatta + Styrplatta + Visa tangentbord + Dölj tangentbord + Linux-terminal + Linux-sessionen är inte redo för en terminal än. + Steam + Spel + Skrivbord + Butik + Komponenter + Appar + Konfiguration + Uppdateringar + %1$d %% + Återuppta %1$s + Visas som /root/ROMs i varje session. Det interna lagringsutrymmet finns på /root/Storage. + Ingen mapp vald + Gäller från nästa session. SD-kort fungerar. + Välj mapp + Byt mapp + Installera en ARM64-version av Proton och välj den per spel i Steam → Egenskaper → Kompatibilitet. + Stoppa den pågående sessionen för att installera eller ta bort ett kompatibilitetsverktyg. + Tillgängliga versioner + Installerad %1$s + Väntar sedan en tidigare begäran + Inte installerad + Uppdatering tillgänglig → %1$s + Ta bort gamla %1$s-versioner + Tar bort %1$s. Spel som är inställda på dessa versioner måste bytas till en annan Proton. + Spel som är inställda på den här versionen måste bytas till en annan Proton. + Installera nu + Avbryt kön + Android-tangentbord + Dölj ✕ + Starta %1$s + Välj en skärm. Sekundär: %1$s. + Den sekundära skärmen är inte längre tillgänglig. + Primär skärm + Sekundär skärm + Föregående flik + Nästa flik + På + Alla %1$d + Inga + %1$d av %2$d + Av + Nedre skärm + Gäller från nästa spelstart. Ett spels profil åsidosätter den delade. + Ett annat spel via app-ID… + Klar + Standard + Av + Ändra… + Ändra variabel + Tillbaka till standard + Proton-spel + Bild-i-bild + Öppna bild-i-bild automatiskt + Kontroller på den uppfällda skärmen + Uppfälld ligger spelet ovanför Steam Decks styrplattor och greppknappar. Hopfälld visas spelet i helskärm. + Pausad + Bild-i-bild blir tillgängligt när sessionen är redo. + Tillåt bild-i-bild för DroidDeck i Android-inställningarna. + Android kunde inte öppna bild-i-bild. + Genväg + Lägg till på hemskärmen + Välj spel + Söker igenom installerade spel… + Kopiera startlänk + Startlänken har kopierats + Den här launchern kan inte lägga till genvägar. Använd Kopiera startlänk i stället. + Ogiltig startlänk för spelet. + Det här spelet finns inte längre i ditt DroidDeck-bibliotek. + Stoppa den aktuella sessionen innan du startar ett Steam-spel. + Kunde inte skicka spelet till Steam. Försök starta det igen. + Försök ta bort igen + Tar bort + Borttagningen är inte klar + Exportera spelfil + Synka alla spelfiler till mapp + Sluta synka spelfiler + Välj en mapp för frontend-spelfiler + Spelfiler sparade till %1$s + Frontend-filer + + Vänta tills åtgärden på körmiljön är klar + Vänta tills installationen av kompatibilitetsverktyget är klar + Vänta tills installationen av skrivbordsappen är klar + Vänta tills installationen är klar innan du startar ett spel + Stäng Steam-sessionen först, så att spelet inte skriver över importen + Importerar till %1$s + + Importerade %1$d fil från %2$s till %3$s + Importerade %1$d filer från %2$s till %3$s + + + Importerade %1$d fil från %2$s till %3$s. Gamla sparfiler säkerhetskopierades till Download/DroidDeck/Saves/backups + Importerade %1$d filer från %2$s till %3$s. Gamla sparfiler säkerhetskopierades till Download/DroidDeck/Saves/backups + + Välj en zip-fil med sparfiler för %1$s + Exporterar %1$s + + Exporterade %1$d fil som %2$s: %3$s + Exporterade %1$d filer som %2$s: %3$s + + Välj var %1$s ska sparas (%2$s) + %1$s misslyckades: %2$s + Inga sessionsloggar än: kör en session först. + Inga sessionsloggar att rensa. + + Rensade %1$d sessionsloggmapp. + Rensade %1$d sessionsloggmappar. + + Parkopplingen för trådlös felsökning misslyckades + Kommandot för trådlös felsökning misslyckades + Trådlös felsökning är inte tillgänglig + ADB-kommandot har kopierats + Välj ROM-mappen + Inte en Adreno-GPU + Turnip stöder Adreno-GPU:er. På %1$s kan Steam visa en svart skärm. Nedladdning: %2$.0f MB. + Installera trots det + Ta bort Linux-körmiljön + Det här tar bort körmiljön, Steam-klienten i den och alla spel som är installerade där. + Tillämpat: %1$s + Kunde inte öppna %1$s på den primära skärmen + Kunde inte öppna %1$s på %2$s + Kunde inte öppna %1$s på skärm %2$d + Importerar + Byter + Återställer + Avbryter + Tar bort + kan inte läsa filen + Importerade %1$s + Det väntande bytet avbröts. + Välj ett komponentpaket (-linux .wcp) + Nightlies kunde inte nås + Sparade %1$s + Nedladdningen misslyckades: %1$s + Välj en drivrutin för Linux-körmiljön (-Linux-zip eller Android + Linux-paket) + Välj en skärmdrivrutin (AdrenoTools-zip eller Android + Linux-paket) + Välj en drivrutins-zip eller ett Android + Linux-paket + %1$s: %2$s + Tar bort %1$s… + Glömmer %1$s… + Välj mappen för spellagring + Välj en Decky-plugin-zip + Inte användbar: %1$s + Program + Tar bort Linux-körmiljön + Kunde inte öppna Inställningar på den nedre skärmen. Öppnade den på huvudskärmen i stället. + Öppna Utvecklaralternativ i Inställningar + + Det gick inte att öppna Androids inställning ”Installera okända appar” + Kunde inte kontrollera: %1$s + Den här versionen har ingen nedladdning för den här kopian av DroidDeck + Stoppa den pågående sessionen innan du uppdaterar + Laddar ned + Installerar + Väntar på att du bekräftar + Uppdateringen misslyckades + Android erbjöd inte att installera uppdateringen + + Decky-installationen misslyckades: %1$s + Startar import av plugin… + Import av plugin misslyckades: %1$s + Plugin installerad. Starta om Steam för att läsa in den. + + Inte kontrollerat än - tryck på uppdatera för att söka efter nya drivrutiner + Android + Linux: ställer in skärmdrivrutinen samtidigt + Android + Linux: ställer in körmiljödrivrutinen samtidigt + Turnip som är inbyggd i körmiljön + systemets Vulkan: ingen paketerad version för den här GPU:n + %1$s (paketerad) + Manuell + %1$s · %2$s + Bara hälften är publicerad just nu + %1$.0f MB för båda + Inga drivrutiner att ställa in: %1$s + Söker efter de senaste drivrutinerna… + Senaste versionen (kunde inte kontrollera: %1$s) + De rekommenderade drivrutinerna är inte publicerade just nu + Laddar ned %1$s %2$s… + Uppdaterad till %1$s %2$s. + Uppdaterad till %1$s %2$s. Skärmdrivrutinen tillämpas efter att DroidDeck har startats om. + Bytte till %1$s %2$s. + Bytte till %1$s %2$s. Skärmdrivrutinen tillämpas efter att DroidDeck har startats om. + Använder %1$s %2$s. + Använder %1$s %2$s. Skärmdrivrutinen tillämpas efter att DroidDeck har startats om. + Inte en drivrutins-zip: varken en AdrenoTools-drivrutin, en -Linux-Turnip eller ett Android + Linux-paket + Importen misslyckades: %1$s + Importerad drivrutin + Använder %1$s som både körmiljö- och skärmdrivrutin. + Använder %1$s som både körmiljö- och skärmdrivrutin. Skärmdrivrutinen tillämpas efter att DroidDeck har startats om. + Tog bort %1$s (båda drivrutinerna) + Tog bort %1$s + Senaste: %1$s · kontrollerad %2$s + Senaste: %1$s · %2$s gick inte att nå · kontrollerad %3$s + + för %1$d min sedan + för %1$d min sedan + + + för %1$d h sedan + för %1$d h sedan + + + för %1$d dag sedan + för %1$d dagar sedan + + Kontrollerar Banners-Turnip, WinNative och DroidDeck… + kontrollen misslyckades + Installerad %1$s - välj den i endera listan för att ställa in båda drivrutinerna + Installerad %1$s - välj den i menyn + + Vänta tills borttagningen av körmiljön är klar + Spel + pekplatta + direkt + Bytet misslyckades: %1$s + Kunde inte spara profilen: %1$s + Skrivbordet är pausat + %1$s är pausad + Programmet är pausat + Startar skrivbordet + Startar %1$s + Startar programmet + Inga loggar för den här sessionen. + Kunde inte skapa arkivet med sessionsloggar. + Kunde inte dela sessionsloggarna. + Startar sessionen… + laddar ned Linux-körmiljön + laddar ned Linux-körmiljön · %1$d av %2$d MB + kontrollerar Linux-körmiljön + packar upp Linux-körmiljön + laddar ned skrivbordet + laddar ned skrivbordet · %1$d av %2$d MB + kontrollerar skrivbordet + packar upp skrivbordet + laddar ned Proton Experimental (ARM64) + laddar ned Proton Experimental (ARM64) · %1$d av %2$d MB + kontrollerar Proton Experimental (ARM64) + packar upp Proton Experimental (ARM64) + Kunde inte nå körmiljökatalogen. Kontrollera anslutningen och tryck på Spela igen. + Linux-körmiljön installerades inte. Kontrollera körmiljön i Konfiguration och tryck på Spela igen. + Skrivbordet installerades inte (%1$s). Kontrollera anslutningen och tryck på Skrivbord igen. + %1$s stoppades oväntat. Dela loggarna i en felrapport, eller försök igen. + Programmet stoppades oväntat. Dela loggarna i en felrapport, eller försök igen. + Skrivbordet stoppades oväntat. Dela loggarna i en felrapport, eller försök igen. + Avslutningsstatus %1$d · %2$s + ingen logg + + Loggen visar %1$d ”Function not implemented”-fel: proots seccomp-acceleration misslyckas med en hjälpprocess på den här enheten. Prova %2$s → ”%3$s” (eller ”%4$s”) och starta igen. + Loggen visar %1$d ”Function not implemented”-fel: proots seccomp-acceleration misslyckas med en hjälpprocess på den här enheten. Prova %2$s → ”%3$s” (eller ”%4$s”) och starta igen. + + + Plugins + Installera en plugin-zip för nästa Steam-session. + Laddar ned… + Installera från zip + Spel + Ljud & video + Grafik + Internet + Kontor + Utveckling + Utbildning + Vetenskap + System + Verktyg + Populärt + Trendande + Nytt + Uppdaterat + läser in… + Flatpak-konfiguration + %1$s: %2$s + Kunde inte kontrollera Flathub efter uppdateringar + installerat + mapp + Grafit + Grafitgrå ytor, med den blå färgen från logotypens klot för allt som är markerat, i fokus eller nedtryckt. + Papper på svart + Vit är primärfärgen, ikonens blå är signalfärgen för linjer, ringar och punkter. + Ikonblå + Svart bakgrund, vit text, den blå färgen för allt som är markerat, i fokus eller nedtryckt. + Elektrisk marinblå + Den blå färgen klarare, på ytor som lutar åt marinblått i stället för grått. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Installera körmiljön först + Steam-biblioteket är inte tillgängligt + Det tidigare SD-biblioteket är inte tillgängligt + Spellagringen är inte tillgänglig + Mappen för tillagda spel är inte tillgänglig + Steam-manifesten är inte tillgängliga + Många system + Internt + Linux-system + SD-kort + Lagring + Markera + Egenskaper + Lägg till i Favoriter + Ta bort från Favoriter + Byt namn + Kopiera + Klipp ut + Åtgärder + Favoriter + Fäst aktuell mapp + Inga favoriter än - fäst en mapp med dess ⋮-meny för att snabbt komma tillbaka hit. + Ta bort från favoriter + 0 byte + byte + Plats + Typ + Storlek + Ändrad + Mapp + Fil + %1$s-fil + Skrivskyddad + Förhindrar att spel (och Wine) skriver över eller tar bort filen. + Kunde inte ändra Skrivskyddad + Dold + Den här lagringen kan inte spara attributet Dold. + Markerar filen som dold i Windows (Wines dolda attribut). + Kunde inte ändra Dold på den här lagringen + Klar + Tar bort... + Borttagning misslyckades + Flyttar... + Kopierar... + Flyttar - %1$s + Kopierar - %1$s + Flyttar %1$d/%2$d - %3$s + Kopierar %1$d/%2$d - %3$s + %1$d klara, %2$d misslyckades + %1$d klara, %2$d hoppades över + + %1$d objekt flyttat + %1$d objekt flyttade + + + %1$d objekt kopierat + %1$d objekt kopierade + + ”%1$s” finns redan + Byter namn... + Namnbytet misslyckades + Skapar mapp... + Kunde inte skapa mappen + Ny mapp + Mappnamn + Skapa + Nytt namn + Ta bort? + Ta bort ”%1$s” permanent? + + Ta bort %1$d objekt? + Ta bort %1$d objekt? + + Det går inte att ångra. + + …och %1$d till + …och %1$d till + + Tar bort %1$d/%2$d - %3$s + + %1$d kunde inte tas bort + %1$d kunde inte tas bort + + Sammanfoga lägger till och ersätter filer i den befintliga mappen. + Skriv över ersätter den befintliga filen. + Tillämpa på alla konflikter + Sammanfoga + Skriv över + Behåll båda + Hoppa över + Välj den här mappen + Tillbaka + %1$s är monterad men inte läsbar just nu + Listvy + Rutnätsvy + Sök + Sortera + Namn + Ändringsdatum + Bekväma rader + Kompakta rader + Dölj dolda filer + Visa dolda filer + Dölj favoriter + Visa favoriter + Filtrera den här mappen + %1$s ledigt + %1$d markerade + Inga + Alla + Klistra in + + %1$d objekt + %1$d objekt + + Klistra in %1$s här + Klistra in %1$s (flytta) här + Filer + LAGRING + SNABBT + FAVORITER + Hämtningar + Hämtn. + ROM + Sessionsloggar + Bilder + Lade till ”%1$s” i Favoriter + Tog bort ”%1$s” från Favoriter + Tom mapp + Upp en nivå + Välj en mapp + Välj en fil + Steamblå + Himmelsblå + Vit + Lila + Grön + Amber + Röd + Rosa + A-knappen + B-knappen + X-knappen + Y-knappen + Vänster kantknapp + Höger kantknapp + Vänster avtryckare + Höger avtryckare + Visa-knappen + Meny-knappen + Visa + Meny + D-pad upp + D-pad ned + D-pad vänster + D-pad höger + Dold + Dra för att flytta + Fäst + Spara + Inget + Tangentbord + styrplatta + Terminal + Deck-grepp + styrplattor + + Tar bort Linux-körmiljön + + Tar bort Linux-körmiljön · %1$d post + Tar bort Linux-körmiljön · %1$d poster + + Linux-körmiljön har tagits bort + Kunde inte slutföra borttagningen av Linux-körmiljön. Försök ta bort den igen i Konfiguration. + Tar bort rester av körmiljön + Laddar ned + Verifierar + Packar upp + Visas medan Linux-körmiljön installeras eller tas bort + Installerar Linux-körmiljön + Tar bort Linux-körmiljön + + Linux-körmiljön är inte installerad + Katalogen har ingen kontrollsumma för %1$s + Kontrollsumman stämmer inte - ingenting ändrades + Installerar %1$s + Kunde inte placera AppImage-filen + Uppackningen misslyckades + Installationen misslyckades + + Flatpak-konfigurationen misslyckades (avslutningskod %1$d) + Flatpak-konfigurationen slutfördes inte + En annan butiksåtgärd pågår + Konfigurera Flatpak först + Startar + Flatpak avslutades med status %1$d + Installerar %1$s + Uppdaterar %1$s + Tar bort %1$s + Installerar %1$s (%2$d/%3$d) + Uppdaterar %1$s (%2$d/%3$d) + Tar bort %1$s (%2$d/%3$d) + översättningar + grafikdrivrutiner + mediekodekar + %1$s-körmiljö + %1$s-körmiljö %2$s + + Filen är för liten för att vara en AppImage + Filen kan inte läsas: %1$s + Det här är inte en AppImage (det är inte ett Linux-program) + Den här AppImage-filen är byggd för varken ARM64 (aarch64) eller x86-datorer + Det här är ett Linux-program men inte en AppImage + Endast typ 2-AppImages kan importeras (den här är typ %1$d) + Inte tillräckligt med ledigt utrymme: import kräver cirka %1$s + Inte tillräckligt med ledigt utrymme: uppdatering kräver cirka %1$s + Importen misslyckades + Den AppImage-filen finns inte längre + Uppdateringen misslyckades + Packar upp %1$s + AppImage-filen kunde inte packas upp + AppImage-filen kunde inte packas upp: %1$s + + Stoppa den aktiva sessionen innan du installerar Decky Loader + Nedladdningen av Decky Loader misslyckades + Storleken på Decky Loader matchade inte utgåvans metadata + Kunde inte ladda ned utgåvans kontrollsumma + Utgåvans kontrollsumma saknas + Utgåvans kontrollsumma är ogiltig + Kontrollsumman för Decky Loader stämmer inte + Utgåvans fil är inte en kompatibel 64-bitars körbar Linux-fil + En session startade under nedladdningen. Stoppa den innan du installerar Decky Loader + Kunde inte markera PluginLoader som körbar + Kunde inte aktivera Steams CEF-fjärrfelsökning + Kunde inte förbereda PluginLoader + Kunde inte bevara den befintliga PluginLoader + Kunde inte aktivera den nya PluginLoader + Stoppa den aktiva sessionen innan du installerar en plugin + Installera Decky Loader innan du importerar en plugin + Kunde inte läsa den valda zip-filen + Kunde inte skapa mappen för Decky-plugins + Packar upp plugin-zip + Plugin-zipen måste innehålla en plugin-mapp på toppnivå + Plugin-zipen saknar plugin.json, package.json eller dist/index.js + Plugin-namnet saknas + En session startade under installationen. Stoppa den innan du installerar en plugin + Plugin installerad · starta om Steam för att läsa in den + Installationen av plugin-zipen misslyckades + Plugin-zipen innehåller för många poster + Plugin-zipen innehåller en ogiltig sökväg + Plugin-zipen innehåller dubbletter av sökvägar + Plugin-zipen innehåller en osäker sökväg + Kunde inte skapa plugin-mappen + Plugin-zipen packas upp till mer än gränsen på 1 GiB + Kunde inte markera den medföljande plugin-binären som körbar + Ogiltig remote_binary-post + Ogiltigt namn på fjärrbinär + URL:en för fjärrbinären måste använda HTTPS + Fjärrbinären %1$s har en ogiltig SHA-256-hash + Kunde inte skapa bin-mappen för pluginen + Laddar ned plugin-binär · %1$s + Kunde inte ladda ned plugin-binären %1$s + Kontrollsumman för plugin-binären stämmer inte · %1$s + Kunde inte markera plugin-binären som körbar · %1$s + Kunde inte ersätta plugin-binären · %1$s + Kunde inte installera plugin-binären · %1$s + Plugin-mappen tillhör en annan plugin + Kunde inte bevara den befintliga pluginen + Kunde inte aktivera den importerade pluginen + Kunde inte uppdatera inställningarna för Decky-plugin + Kunde inte återställa den befintliga pluginen + + Kunde inte spara identiteten för trådlös felsökning + ADB-kommandot misslyckades (%1$d) + Android bekräftade inte att gränsen för underprocesser ändrades: %1$s + Ingen parkoppling för trådlös felsökning är sparad + Ange den aktuella IP-adressen och porten för trådlös felsökning + + Tidsgränsen överskreds när appen väntade på popup-fönstret för parkoppling. Försök igen från DroidDeck. + Kunde inte söka efter popup-fönstret för parkoppling (%1$d). Kontrollera att Wi-Fi är på. + Kunde inte söka efter popup-fönstret för parkoppling: %1$s + Ange alla 6 siffror i koden + Parkopplar… + Den koden fungerade inte. Öppna popup-fönstret för parkoppling igen och ange den nya koden. + Parkopplad. Ansluter… + Parkopplad, men porten för trådlös felsökning hittades inte. Låt trådlös felsökning vara på och försök igen. + Tillämpar inställningen… + Kommandot för trådlös felsökning misslyckades + Konfiguration av trådlös felsökning + Frågar efter parkopplingskoden för trådlös felsökning medan Inställningar är öppen + Parkoppla trådlös felsökning + Slå på trådlös felsökning och tryck sedan på ”Parkoppla enhet med parkopplingskod”. + Slå på trådlös felsökning och tryck sedan på ”Parkoppla enhet med parkopplingskod”. Stanna kvar i Inställningar: koden skrivs in i den här aviseringen. + 6-siffrig parkopplingskod + Tryck på Ange kod och skriv de 6 siffrorna som visas i Inställningar. + Ange Wi-Fi-parkopplingskoden + Försök med koden igen + Ange kod + Konfigurerar Steam + Steam är redo + Tryck för att gå tillbaka till DroidDeck. Du kan stänga av trådlös felsökning nu. + Konfigurationen av trådlös felsökning avbröts + + Nedladdningen svarade HTTP %1$d + Nedladdningens storlek matchade inte den publicerade APK-filen och förkastades - försök igen + Nedladdningen matchade inte sin kontrollsumma och förkastades - försök igen + Uppdateringen är för Android-paketet %1$s, inte %2$s + Uppdateringens metadata anger inte DroidDecks signeringsnyckel för utgåvan + Android kunde inte läsa den nedladdade APK-filen + Den nedladdade APK-filen är för %1$s, inte %2$s + Den nedladdade APK-filen anger versionCode %1$d, inte den publicerade %2$d + Android installerar inte versionCode %1$d över den installerade %2$d + Den nedladdade APK-filen har ingen signeringsinformation + Den nedladdade APK-filen är inte signerad med DroidDecks nyckel för utgåvan + Uppdateringskatalogen svarade HTTP %1$d + + Startar sessionen… + %1$d:%2$02d förflutit · arbetar fortfarande + Laddar ned uppdateringen av Steam-klienten · %1$d %% + Laddar ned Steam-klienten · %1$s + + En installation av ett kompatibilitetsverktyg pågår redan + Stoppa den aktiva sessionen innan du installerar kompatibilitetsverktyg + Kunde inte hitta en ARM64-utgåva av %1$s + Minst 4 GB ledigt utrymme plus nedladdningens storlek krävs för att installera %1$s + Laddar ned %1$s %2$s + Försöker ladda ned igen · försök %1$d av %2$d + Nedladdningen avbröts + Nedladdningen misslyckades. Den delvisa filen behålls så att ett nytt försök kan återupptas + Nedladdningen översteg utgåvans storlek och togs bort + Nedladdningen var ofullständig. Den delvisa filen behålls för ett återupptagbart försök + Verifierar nedladdningen + Kunde inte läsa utgåvans kontrollsumma. Försök igen + %1$s %2$s publicerar ingen kontrollsumma. Inget installerades + Kontrollsumman stämmer inte. Nedladdningen togs bort + En session startade under nedladdningen. Stoppa den innan du installerar kompatibilitetsverktyg + Installerar %1$s + Installationen av %1$s misslyckades (avslutningskod %2$d) + Kunde inte installera %1$s + Hämtar droiddeck-esync-paket + + med Linux-hjälpprogram + Endast DLL-filer + Original (Proton har uppdaterats sedan senaste bytet) + Ändrad utanför Komponenter + saknas + Original %1$s + Original från %1$s + %1$s %2$s läggs in i %3$s när spelet som körs på den stängs. + %1$s %2$s finns nu i %3$s. Det börjar gälla nästa gång ett spel startar. + Det originalpaketet är inte sparat + %1$s i %2$s återgår till originalet när spelet som körs på den stängs. + %1$s i %2$s återställdes från sitt %3$s-original. + %1$s används i %2$s. Byt den Protonen till något annat först. + %1$s är inte sparad + Tog bort %1$s. + Den installerade versionens original behålls: det är vägen tillbaka. + Inte sparad + Tog bort %1$s-originalet av %2$s. + Nedladdningar kommer bara från Nightlies-utgåvorna + Den här paketlistan är från innan kontrollsummor fanns - uppdatera den och försök igen + Kontrollsumman stämmer inte + + Linjär + Närmaste + Snapdragon GSR (kvalitet) + Endast skärpa + Auto (matcha skalning) + + Inget tillämpat. + Spelklarhet + Skärpa med en nypa kontrast - vardagsvalet. + Levande + Färgen pressad hårt. Deband är på så att himlar inte strimmar. + Cinematisk + Djupare kontrast, kallare färg, kanter jämnade ut. + Tävling + Skuggor lyfta och tung skärpa - för att se motståndare snabbare. + Adaptiv skärpa + Endast skärpa, färgen orörd. + Filmisk + Filmlik kurva, lätt dämpad. + Arkad + Högljudd och kraftfull. + Retro-CRT + Simulerar både CRT-röret och den analoga signalen tillsammans. + Skarp uppskalning + För körning under skärmens upplösning. + Pixelrent + Skarpa pixlar för 2D och äldre titlar. + Anime-kant + Tecknade konturer plus skärpa. + + Inte en Adreno-GPU + Stöds + Experimentell: dess drivrutiner är testversioner + Under testad maskinvara (Adreno 725 och nyare): det kan hända att det inte fungerar + Stöds inte: DroidDeck behöver en Adreno-GPU (Snapdragon) + + GitHubs hastighetsgräns nåddes - försök igen senare + GitHub svarade HTTP %1$d + nedladdningen svarade HTTP %1$d + Den här drivrutinslistan är från innan kontrollsummor fanns - uppdatera den och försök igen + Kontrollsumman stämmer inte - nedladdningen förkastades + Inte en AdrenoTools-drivrutin: %1$s + ingen meta.json + meta.json kan inte läsas (%1$s) + det här är en drivrutin för Linux-körmiljön (importera den under ”Linux-körmiljödrivrutin”) + det här är en spel-drivrutin för Wayland, som den här appen inte har någon användning för + meta.json anger inget libraryName, så ingenting kunde skickas till AdrenoTools + meta.json anger %1$s, som inte finns i zip-filen + %1$s är inte ett 64-bitars delat ELF-bibliotek för AArch64 + %1$s länkar mot glibc - en ”-Linux”-Turnip, som hör hemma under ”Linux-körmiljödrivrutin” + Ingen libvulkan_freedreno*.so i den här zip-filen. En Android-zip (AdrenoTools) eller -Wayland-Turnip-zip är inte en Linux-körmiljödrivrutin. + %1$s är inte ett 64-bitars delat ELF-bibliotek för AArch64. + %1$s är inte en glibc-drivrutin - den länkar mot Androids libc. Linux-körmiljön behöver en ”-Linux”-zip; en vanlig eller ”-Wayland”-Turnip kan inte läsas in av Steam-klienten alls. + Inte ett drivrutinspaket: ingen manifest.json som anger en android- och en linux-drivrutin + Inte ett drivrutinspaket: %1$s pekar utanför det + Inte ett drivrutinspaket: det packas upp till mer än %1$d MB + + SD-kort + Mapp + + Dela sessionsloggar + + kräver Android 10 eller senare (skärmlager med en färgrymd) + den här skärmen rapporterar inget HDR-stöd + den här skärmen stöder %1$s, inte HDR10 + + den här GPU:n + Gränsen för underprocesser är på + Gränsen för underprocesser är inte inställd + Gränsen för underprocesser kunde inte kontrolleras + Gränsen för underprocesser är av + Gränsen för underprocesser krävs inte + %1$s Om det inte är möjligt ansluter du den här enheten till en dator med ADB och kör kommandot nedan. Kom tillbaka hit och kontrollera igen. + Steam-sessioner kan starta. + Den här Android-versionen använder inte den här gränsen. + Slå på ”Inaktivera begränsningar för underprocesser” i Utvecklaralternativ, och låt Utvecklaralternativ vara kvar på efteråt. + Den här Android-versionen har ingen växel för det. Använd trådlös felsökning för att ändra det från den här enheten. + Den här versionen har ingen nedladdning för den här kopian av DroidDeck. + Android installerar inte den över den här versionen eftersom dess versionCode %1$d är lägre än den installerade %2$d. + Android installerar inte en äldre version över en nyare. Stanna på den här versionen tills kanalen hinner i kapp. + Den här kopian av DroidDeck är signerad på ett annat sätt (en lokal version?), så Android uppdaterar den inte på plats. + Det finns inte tillräckligt med ledigt utrymme för uppdateringen. + Uppdateringen avbröts. + Uppdateringen installerades inte: %1$s + Uppdateringen installerades inte. + %1$s-zip + zip + %1$s har inte startats än + Inga sparfiler hittades för %1$s + %1$s har ingen drive_c-mapp, så det är inte en sparfils-zip från GameHub eller Winlator + kan inte skapa mappar i %1$s + kan inte skriva i %1$s + kan inte skriva i %1$s (%2$s) + Mappen är inte tillgänglig + En annan fil använder redan %1$s + Mappen för omslagsbilder är inte tillgänglig + Mappen har en orelaterad synkinventering + Kunde inte ta bort %1$s + Filen hamnar utanför exportmappen + FEX (version utan versionsnamn) + ingen profile.json i %1$s + %1$s är inte ett Linux-komponentpaket (typ %2$s) + %1$s · %2$s ledigt + Automatiskt + Uppdateringskatalogen klarade inte sina kontroller (%1$s) + DroidDecks inställning för underprocesser + Inloggad · offlineläge + Inloggad + PAKETERAD + NEDLADDAD + IMPORTERAD + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Sessionsskärm + Upplösning + 16:9-spelanpassning + Hur 16:9-spel anpassas till den här skärmen. Nästa session. + Välj Matcha skärmen eller en anpassad storlek som är smalare än 16:9. + Behåll proportionerna + Sträck ut till skärmen + Bildskalning + Skalningsfilter + Delas av Steam och skrivbordet. + Tillämpas direkt. Delas av Steam och skrivbordet. + FSR, GSR och NIS tillämpas när sessionens bild förstoras. Endast skärpa fungerar även utan förstoring. + Filterskärpa + Bildfrekvens + %1$s Ställer in skalning till %2$s. + Matcha skärmen (%1$d×%2$d) + Följ skärmen (ändrar storlek vid vikning) + Steam + Visa enhetsinformation + Dölj enhetsinformation + Läser av enheten… + Kopiera + Kopiera denna information till en felrapport. + Kopierat. + Tvinga SSBS + Håller kvar en CPU-hastighetsinställning som Wine annars stänger av. Kan göra Windows-spel snabbare på Snapdragon 8 Elite; liten skillnad på äldre chip. Gäller från nästa spelstart. + + Butiker + Steam + Egna + Butiker + Öppna en butik på + Nedladdningshastighet + Butiksmotor + ● Motor redo + Inte byggd + Balanserad + Snabb + Max + Butiker + Butiksinställningar + Inställningar + (inte inloggad) + Butik + Installerade + Bibliotek + Alla + Nedladdningar + %1$s (%2$d) + Logga in + %1$s · logga ut + Logga ut från %1$s? + Installerade spel finns kvar. Biblioteket hämtas igen när du loggar in på nytt. + Fortsätt vara inloggad + Logga ut + Sök i %1$s-katalogen… + Sök installerade spel… + Sök i ditt bibliotek… + Sök + Inget installerat + Läser in + Inga spel + Läser in butiken… + Ingen katalog + Nytt + Erbjudanden + Gratisspel + Populärt nu + Ditt bibliotek + Spela + Installera + Återuppta installation + Avbruten + Installation · %1$s + Hämta gratis + Visa på %1$s + Köp · %1$s + Installerat + I biblioteket + Gratis + Återuppta + Uppdatera + Visa innehåll för vuxna + Epic + EOS + Offline + Av + Epic-inloggning (EOS) + Starta offline + Lös Epic-inloggningen + Molnsparfiler + Ladda upp till molnet + Hämta från molnet + Inga molnsparfiler + Molnkonflikt + Molnkonflikt · %1$d filer + … + Behåll molnet + Behåll lokal + Kontrollerar molnet… + Senast synkad %1$s + Inte synkad än + %1$d filer + installerat + i ditt bibliotek + butik + Prestationer + %1$d av %2$d upplåsta + %1$d / %2$d + Upplåsta · %1$d + Låsta · %1$d + Dolda · %1$d + Dold prestation + Detaljer visas när den är upplåst. + Alla prestationer upplåsta + Läser in… + Logga in på %1$s för att se dina prestationer. + Kunde inte nå %1$s. Kontrollera igen när du är online. + %1$s listar inga prestationer för det här spelet. + Visa alla %1$d + Prestationer · %1$s + ✓ I Steam + Avinstallera + Avinstallerar… + Avinstallera %1$s? + Frigör %1$s och tar bort det från Steam. + Tar bort dess filer och Steam-genväg. + Steam upptäcker spelet nästa gång det startar; starta det därifrån, eller starta Steam nu. + Proton + Steams val per spel + Ljud + DirectAudio + Komponenter + FEX, DXVK, VKD3D + Motor + %1$s · nativ + %1$s · inbyggd + Så körs det + Hämtat från + %1$s, med ditt konto + Installerat till + Spellagring (Games/%1$s/%2$s) + Startas av + Steam och Proton, samma väg som Steam-spel + Steam och Proton · Epic-inloggning per start, EOS-overlay av + Steam och Proton · Amazon Games Services-variabler inställda + Steam + läggs till i Steam vid installation (Deck-gränssnitt + Steam Input) + Inga nedladdningar + Pausad + I kö + I kö · #%1$d + Avbruten + Misslyckades + Misslyckades · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Pausa + Återuppta + Avbryt + Försök igen + Rensa + Rensa avslutade + Manifest + Nedladdning + Verifiering + Installation + Klart + Kontrollerar + Laddar ned + Ta bort nedladdning? + Misslyckades · %1$s behålls + Verifierar + Installerar + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d filer + Nedladdningshanterare + Samtidiga nedladdningar + Installera till + Installera %1$s: + Internminne + %1$s ledigt + Butiksnedladdningar + Visar nedladdningsförlopp för spel och håller nedladdningar igång i bakgrunden + Laddar ned + %1$d nedladdningar + Förbereder… + Avslutade butiksnedladdningar + %1$s installerat + Nedladdningen av %1$s misslyckades + Ta bort + Behåll + Logga in på %1$s + Logga in med din leverantör + Läs in på nytt + Stäng + Försök igen + Slutför inloggningen… + Inloggningssidan kunde inte läsas in: %1$s + Den här leverantören vägrar logga in från en inbäddad sida. Logga in med ditt butikslösenord istället. + Inloggningssidan svarade med ett fel (HTTP %1$d). + Anslutningen till inloggningssidan är inte säker. + Inloggningen kunde inte verifieras. Försök igen. + Inloggningen misslyckades. Försök igen. + Stäng + Tillbaka + Redigera + Lägg till alla spel i den här mappen + Inget nytt i den här mappen + Namn + Mål + Starta i + Startalternativ + Bildmaterial + Annan exe… + Ta bort + Omslag + Bakgrund + Logotyp + Ikon + Spelmapp + Steam + SteamGridDB + Din fil + Från exe-filen + Automatiskt + Välj bild… + Välj en bild: %1$s + Återställ till automatiskt + + %1$d spel tillagt + %1$d spel tillagda + + + %1$d redan i Spel + %1$d redan i Spel + + B · Tillbaka till %1$s + spelet + Spelinställningar + Grafikdrivrutiner + Denna Proton + Alla Proton + För alla Proton + Tillgängliga · %1$d + Kontrollerade %1$s + Sök efter nya komponenter och drivrutiner + Hämta + %1$s · DXVK, VKD3D-Proton och FEX nedan hör till denna Proton + %1$s · testad + %1$s · otestad + %1$s · stöds inte + Håller det rekommenderade paret uppdaterat + Auto · %1$s + Runtime- och visningsdrivrutiner separat + Använda %1$s? + Ta bort %1$s? + Diagnostik + Redo att spela + Inte redo att spela än + runtime %1$s + processgräns av + inloggad som %1$s + När något behöver dig + Utseende + Den här enheten + Biblioteksbilder + LINUX-APPAR + Flathub + Skrivbordsappar för Linux-skrivbordet, öppnas från Skrivbordsläge. + Konfigurera Flathub + Ta bort %1$s? + Dess inställningar och filer behålls. + Behåll + Storlek på disk + Avinstallera skrivbordet? + Alla + GOG-prestationer + Synkroniserar prestationer för GOG-spel som stöds. Gäller från nästa start. + Prestationer av + Prestationer på + Logga in på GOG igen + Prestationer uppladdade + Uppladdning av prestation väntar + Aktivera GOG-prestationer i butiksinställningarna + Prestationer inte tillgängliga; kontrollera sessionsloggarna + Prestation upplåst + Klistra in + Styrkors + Vänster styrspak + Höger styrspak + Meny + Tangentbord + Styrkors upp + Styrkors ner + Styrkors vänster + Styrkors höger + Pekskärmskontroller + Pekskärmskontroll + Mus + På + Av + Långsam + Normal + Snabb + Redigera layout + DroidDeck-meny + Dra kontroller till skärmen + Ordna + Kasta + Sparar… + Ikon + B− + B+ + H− + H+ + Dölj + Färg och opacitet… + Kopiera denna layout + Layouten kopierad; klistra in den i pekskärmslayouten för valfritt spel + Klistra in kopierad layout + Återställ till Steams standard + Den här kontrollen har ingen bindning i Steam att ge en ikon + Ikon för %1$s + Färg på pekskärmslayout + Klar + Det gick inte att spara pekskärmslayouten + Pekskärmslayout sparad till Steam + App %1$d + Etikett (valfritt) + Steams ikoner hittades inte + + %1$d ikon från Steam + %1$d ikoner från Steam + + Ikonfärg + Knappfärg + Ingen ikon + + Kontrollerar inloggning… + Hämtar ditt bibliotek… + Hämtar spellista… + Läser in speldetaljer… (%1$d/%2$d) + + Synkroniserar %1$d spel… + Synkroniserar %1$d spel… + + Synkroniserar… %1$d/%2$d + Hämtar omslagsbilder… + Synkroniseringen misslyckades + Inga spel hittades i ditt %1$s-bibliotek + Amazon Games svarade inte för ditt bibliotek: uppdatera, eller logga in igen + Det gick inte att nå GOG + Det gick inte att läsa ditt GOG-bibliotek + Registrerar hos Steam… + Cardboard VR + Cardboard-läge + Bilder för vänster och höger öga till ett VR-headset för mobilen. + Bild + Virtuell skärm + Sida vid sida-3D + Glasögon + Cardboard + Cardboard (2014) + Ingen lins + Ögonavstånd + Skärmbredd + Skärmavstånd + Huvudspårning + Den här enheten saknar rotationssensor. + Centrera vyn + Centrera + SteamVR (experimentellt) + Installera + Laddar ned Valves SteamVR för ARM64 (571 MB). + Laddar ned och packar upp SteamVR… + Starta + Kör SteamVR med ditt huvud som headset; bilden växlar till sida vid sida-3D. + Stoppa + SteamVR körs. + Inte tillgängligt i den här sessionen. + SteamVR svarade inte; kontrollera sessionsloggen. + Ladda ned komponenter + Nedladdningar delas med sidan Komponenter. Välj en nedladdad version ovan för att använda den nästa gång ett spel startas. + Välj en version att ladda ned + Utvecklare + Utgivare + Släppt + Huvudspel + + Extra + Komplettering + %1$s h + %1$d min + diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml new file mode 100644 index 000000000..082c55548 --- /dev/null +++ b/app/src/main/res/values-th/strings.xml @@ -0,0 +1,2210 @@ + + + DroidDeck + + การเริ่มใช้งานครั้งแรกจะดาวน์โหลด Steam ซึ่งอาจใช้เวลาสักครู่ + ตามค่าเริ่มต้น การออกจากแอปจะหยุด Steam ไว้ชั่วคราว แล้วกลับมาทำงานต่อจากจุดเดิมเมื่อคุณเปิดแอปอีกครั้ง + คอนโทรลเลอร์ของคุณจะปรากฏเป็นจอย Xbox 360 หากไม่เช่นนั้นให้ใช้ปุ่มควบคุมบนหน้าจอ + + + การเริ่มใช้งานครั้งแรกจะตั้งค่าเดสก์ท็อป ซึ่งอาจใช้เวลาสักครู่ + ตามค่าเริ่มต้น การออกจากแอปจะหยุดเซสชันไว้ชั่วคราว แล้วกลับมาทำงานต่อจากจุดเดิมเมื่อคุณเปิดแอปอีกครั้ง + การสัมผัสหน้าจอทำงานเหมือนทัชแพด ลากเพื่อเลื่อนตัวชี้ แตะเพื่อคลิก + คอนโทรลเลอร์ของคุณจะปรากฏเป็นจอย Xbox 360 หากไม่เช่นนั้นให้ใช้ปุ่มควบคุมบนหน้าจอ + + เซสชัน + แสดงขณะมีเซสชัน Steam หรือเดสก์ท็อปเปิดอยู่ + Steam กำลังทำงาน + กำลังดาวน์โหลดใน Steam + หยุดเซสชันไว้ชั่วคราว - แตะดำเนินการต่อเมื่อพร้อม + ดำเนินการต่อ + หยุดชั่วคราวตอนนี้ + หยุดเซสชัน + การสร้างเฟรม + เลือกโหมดการสร้างเฟรม + ปิด + Win-FG %1$d× + LSFG %1$d× + LSFG แบบปรับอัตโนมัติ %1$d + ต้องมี Lossless Scaling บน Steam + ติดตั้ง Lossless Scaling ใน Steam หรือนำเข้า Lossless.dll + นำเข้า Lossless.dll… + ใช้ชุดที่มาจาก Steam + ใช้ชุดที่นำเข้ามา + เลือก Lossless.dll + นำเข้า Lossless.dll แล้ว + Lossless.dll นี้ถูกใช้งานอยู่แล้ว + บัญชี Steam นี้ไม่ได้เป็นเจ้าของ Lossless Scaling + ไฟล์นี้ไม่ใช่ Lossless.dll ของ Lossless Scaling + ไม่สามารถเตรียม LSFG จาก Lossless.dll นี้ได้ + LSFG ต้องใช้ Lossless Scaling จาก Steam หรือ Lossless.dll ที่นำเข้ามา + LSFG ไม่สามารถเตรียมเชดเดอร์จาก Lossless.dll ได้ + %1$s: ไม่รองรับ + %1$s: เอนจินล้มเหลว + %1$s: ต้องใช้ Lossless Scaling + %1$s %2$s · กำลังเริ่ม + เพิ่มไลบรารี Steam และสแกนหาเกม Windows เกมที่เป็นเจ้าของอยู่แล้วจะใช้รายการ Steam เดิม ส่วนที่เหลือจะกลายเป็นช็อตคัตแบบไม่ใช่ Steam + สภาพแวดล้อมของเกม + ตัวแปรสภาพแวดล้อมสำหรับเกม Proton + แก้ไข + เกม Proton ทั้งหมด + โปรไฟล์ + รหัสแอป Steam + เปิด + เกม %1$s + เพิ่มตัวแปร + ชื่อตัวแปร + ค่า + บันทึก + ลบ + รับค่าสืบทอดอีกครั้ง + ล้างการปรับแต่งสภาพแวดล้อม + ยกเลิกสำหรับโปรไฟล์นี้ + ไม่มีตัวแปรที่ตั้งไว้ + สืบทอดมา + ตัวแปรกำหนดเอง + โหลดหรือบันทึกไม่ได้ ตรวจสอบพื้นที่จัดเก็บว่าง แล้วลองใหม่ + ใช้ชื่อที่ถูกต้องและค่าที่มีความยาวไม่เกิน 8192 ตัวอักษร + รันไทม์ Proton มาตรฐานไม่อ่านตัวแปรนี้ + กำลังโหลด… + การปรับแต่งระดับคุณสมบัติ D3D12 12_2 รายงานความสามารถ DirectX 12 Ultimate คุณสมบัติที่ไม่รองรับอาจทำงานล้มเหลว เว้นค่าไว้เพื่อให้ตรวจหาโดยอัตโนมัติ + การปรับแต่งโมเดลเชดเดอร์ D3D12 6_9 ต้องใช้ VKD3D รุ่นใหม่พอและคุณสมบัติ GPU ที่ตรงกัน เว้นค่าไว้เพื่อให้ตรวจหาโดยอัตโนมัติ + เปิดใช้แคชเชดเดอร์ของ Mesa เพื่อลดการคอมไพล์ซ้ำ false จะเปิดใช้แคช + ขนาดสูงสุดของแคชเชดเดอร์ Mesa เช่น 1G แคชที่ใหญ่ขึ้นจะใช้พื้นที่จัดเก็บมากขึ้น + ตัวเลือก VKD3D สำหรับ D3D12 nodxr ปิดการเรย์เทรซิง ใช้เฉพาะกับเกมที่จำเป็นต้องปิด + ตั้งเป็น 1 เพื่อบันทึกล็อกวินิจฉัยของ Proton การบันทึกล็อกอาจลดประสิทธิภาพและใช้พื้นที่จัดเก็บ + ตั้งเป็น 1 เพื่อใช้ WineD3D แบบ OpenGL แทน DXVK สำหรับทดสอบความเข้ากันได้ของ D3D9–11 + ตั้งเป็น 1 เพื่อให้เกมนี้ไม่ใช้ ntsync (droiddeck-ntsync หรือไดรเวอร์เคอร์เนล) โดยจะใช้ droiddeck-esync เมื่อมีแพ็กที่เหมาะสม มิฉะนั้นใช้ droiddeck-fsync + ตั้งเป็น 1 เพื่อปิด droiddeck-esync สำหรับเกมนี้ โดยจะใช้ droiddeck-fsync แทน หรือ droiddeck-ntsync เมื่อ fsync ปิดอยู่ด้วย + ตั้งเป็น 1 เพื่อให้เกมนี้ไม่ใช้ droiddeck-fsync โดยจะใช้ droiddeck-esync เมื่อมีแพ็กที่เหมาะสม มิฉะนั้นใช้ droiddeck-ntsync + ตั้งเป็น 1 เพื่อให้เกมนี้ทำงานบน droiddeck-fsync หรือ 0 เพื่อให้ droiddeck-esync มาก่อนเมื่อมีแพ็กที่เข้ากับ Proton นี้ + ตั้งเป็น 0 เพื่อปิดตัวช่วยควบคุมด้วยคอนโทรลเลอร์ของ Proton สำหรับเกมนี้ + โอเวอร์เลย์ DXVK สำหรับ D3D9–11 fps จะแสดงอัตราเฟรม หากไม่ตั้งค่าจะซ่อนไว้ + ตั้งค่า DXVK คั่นด้วยเซมิโคลอน รุ่นใหม่ใช้ dxvk.maxFrameRate สำหรับจำกัดเฟรมเรต ควรใช้ตัวจำกัดเฟรมของเซสชันสำหรับการเล่นทั่วไป + ขีดจำกัดอัตราเฟรม D3D12 0 คือไม่จำกัด + การทำเธรด OpenGL ของ Mesa true อาจช่วยเกม OpenGL ที่ติดขัดจาก CPU ทดสอบดูตามแต่ละเกม + การซิงค์ของ Wine + วิธีที่เกมประสานเธรดให้ตรงกัน จะมีผลในเซสชันถัดไป + esync + ntsync + fsync + wineserver + ค่าตั้งล่วงหน้าของ FEX + จะมีผลในการเปิดเกมครั้งถัดไป + เกม + ค่าเริ่มต้นของ FEX + ใช้ค่าเริ่มต้นของ FEX + เสถียรภาพ + ตั้งค่าที่ปลอดภัยที่สุดแต่ช้าที่สุด + ความเข้ากันได้ + TSO เต็มรูปแบบพร้อมมัลติบล็อก + ระดับกลาง + TSO แบบสมดุลพร้อมลดความแม่นยำ x87 + ประสิทธิภาพ + ปิด TSO ทั้งหมด พร้อมมัลติบล็อก + ประสิทธิภาพ + TSO + ค่าตั้งประสิทธิภาพพร้อมเปิด TSO หลัก + สูงสุด + ปิดการตรวจสอบโค้ดที่ปรับเปลี่ยนตัวเอง เกมที่ใช้ JIT และ DRM อาจทำงานผิดพลาด + สูงสุด + TSO + ค่าตั้งระดับสูงสุดพร้อมเปิด TSO หลัก + สูงสุด-gn + ประสิทธิภาพพร้อมคำแนะนำ TSC และข้อมูลเมตา ควรลองก่อนสูงสุด + Denuvo + ประสิทธิภาพพร้อมตรวจสอบโค้ดเต็มรูปแบบและซ่อนบิตไฮเปอร์ไวเซอร์ + ค่ากำหนดเอง + เลือกค่า + โปรแกรมจำลองและแอป + เพิ่ม + แอปหรือเกมของคุณเอง + สคริปต์ + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + กำลังเริ่ม… + เพิ่ม %1$s แล้ว + ลบ %1$s แล้ว + เดสก์ท็อป + แอปนั้นหายไปแล้ว เลือกใหม่จากเดสก์ท็อป + เปิด %1$s + ลบ + กดอีกครั้งเพื่อลบ + หยุดเซสชันก่อนเพื่อลบ + เรียกใช้ %1$s จากโฟลเดอร์ของมัน โดยเชื่อมโยงเข้าสู่เซสชันในตำแหน่งเดิม การลบแอปจะไม่กระทบโฟลเดอร์นั้น + เรียกใช้ %1$s จากสำเนาโฟลเดอร์ของมันภายใน DroidDeck การลบแอปจะลบสำเนานี้ ไม่ใช่ต้นฉบับ + ติดตั้งรันไทม์ Linux ก่อน + ไฟล์หายไปแล้ว + พื้นที่ว่างไม่พอ: การคัดลอกโฟลเดอร์ต้องใช้ %1$s + กำลังคัดลอก %1$s + เพิ่มแอปไม่สำเร็จ + กำลังค้นหารุ่นที่เผยแพร่ของ %1$s + ไม่สามารถอ่านรุ่นที่เผยแพร่ของ %1$s ได้ + รุ่นที่เผยแพร่ของ %1$s ไม่มี AppImage แบบ ARM64 หรือ x86_64 + กำลังดาวน์โหลด %1$s + ดาวน์โหลดไม่สำเร็จ + อัปเดตแอปไม่สำเร็จ + ไม่สามารถอ่านไอคอนนั้นได้ + บันทึก %1$s แล้ว + แก้ไข + ตรวจสอบการอัปเดต + กำลังตรวจสอบการอัปเดต… + อัปเดตเป็น %1$s + เป็นรุ่นล่าสุดแล้ว · %1$s + อัปเดต %1$s แล้ว + %1$s · %2$s + เพิ่มแอป + สคริปต์ + AppImage + GitHub + Flatpak + ไฟล์ .sh จะรันจากโฟลเดอร์ของตัวเอง แบบเต็มจอภายใต้ gamescope + AppImage แบบ ARM64 (aarch64) หรือแบบ x86_64 ที่รันผ่าน FEX จะถูกแตกไฟล์เข้าสู่รันไทม์ Linux เพียงครั้งเดียว + ติดตั้ง AppImage แบบ ARM64 จากรุ่นที่เผยแพร่ล่าสุดของที่เก็บ หรือแบบ x86_64 ผ่าน FEX + ใช้ FEX + อัตโนมัติ + เปิด + ปิด + FEX รันโปรแกรม Linux x86 บนอุปกรณ์ ARM64 นี้ อัตโนมัติจะใช้งานเมื่อโปรแกรมถูกสร้างมาสำหรับ x86 + %1$s ผ่าน FEX + โปรแกรมนี้สร้างมาสำหรับพีซี %1$s และรันผ่าน FEX ซึ่งยังไม่ได้ตั้งค่า เปิด Steam สักครั้งขณะลงชื่อเข้าใช้: DroidDeck จะดาวน์โหลด FEX และ Steam Linux Runtime ที่จำเป็น จากนั้นโปรแกรมนี้จะเปิดขึ้น + โปรแกรมนี้ + %1$s สร้างมาสำหรับพีซี x86 และรันผ่าน FEX ซึ่งยังไม่ได้ติดตั้ง เริ่ม Steam สักครั้งขณะลงชื่อเข้าใช้และให้ดาวน์โหลด FEX และ Steam Linux Runtime จากนั้นเริ่มโปรแกรมนี้อีกครั้ง + รหัสแอป Flathub หรือหน้า Flathub ของมัน จะตั้งค่า Flatpak ก่อนหากยังไม่ได้ตั้งค่า + ไฟล์ + ยังไม่เลือกไฟล์ + เลือกไฟล์ + เปลี่ยน + เลือกสคริปต์ + เลือก AppImage + เลือกไอคอน + ที่เก็บ + owner/repo หรือลิงก์ github.com + ไม่ใช่ที่เก็บ GitHub + รหัสแอป + org.ppsspp.PPSSPP + ไม่ใช่รหัสแอป Flathub + หรือไฟล์ .flatpak สำหรับแอปที่ไม่มีใน Flathub + เลือกไฟล์ .flatpak + แหล่งที่มา + บนเดสก์ท็อปของคุณ + แอปใหม่ + ชื่อ + จากตัวแอป + ไอคอน + ไอคอนเริ่มต้น + เลือกไอคอน + ล้าง + จากที่เก็บ + กำลังค้นหาไอคอน… + ค่าเริ่มต้น + แก้ไข %1$s + บันทึก + กำลังตรวจสอบโฟลเดอร์… + โฟลเดอร์ของมันเชื่อมโยงเข้าสู่เซสชัน: %1$s + โฟลเดอร์ของมันมีโปรแกรม Linux ซึ่งพื้นที่จัดเก็บที่ใช้ร่วมกันไม่สามารถรันได้ จึงถูกคัดลอกเข้าสู่ DroidDeck (%1$s) + เพิ่ม + ยกเลิก + ติดตั้งรันไทม์ Linux จากการตั้งค่าก่อน + รอให้ %1$s เสร็จก่อน + เข้ากันได้ + บิลด์ที่มีเครื่องหมาย “%1$s” ในเมนู Compatibility ของ Steam มีการแก้ไข prefix ของ DroidDeck และสามารถเริ่มเกมบนอุปกรณ์นี้ได้ ค่าเริ่มต้นคือ Proton Experimental ARM64 (%1$s) หรือ Proton 11.0 ARM64 (%1$s) เมื่อติดตั้งบิลด์ใหม่กว่านั้นแล้ว รายการ Proton ของ Valve เองที่ไม่มีเครื่องหมายนี้ไม่สามารถเริ่มเกมบนอุปกรณ์นี้ได้ + ติดตั้งแล้ว %1$s · %2$s + ยกเลิก + อัตโนมัติ + ไม่เลย + เสมอ + ถอนการติดตั้ง + ตรวจสอบ + อัปเดต + กำลังตรวจสอบ… + กำลังดำเนินการ… + ลืม + เพิ่ม… + ใช้ + จะมีผลในเซสชันถัดไป + เซสชัน Steam + เซสชันเดสก์ท็อป + หน้าจอ + ความละเอียด + สูงสุด 720p + สูงสุด 900p + สูงสุด 1080p + ความละเอียดจริง + %1$s (ค่าเริ่มต้น) + กำหนดเอง · %1$d×%2$d + กำหนดเอง… + 720p อาจทำให้เมนูตอบสนองไวขึ้น + อัตราส่วนหน้าจอ + กำหนดโดยความละเอียดกำหนดเอง + อัตโนมัติใช้อย่างน้อย 16:9 + ขีดจำกัดเฟรม + จำกัดทั้งเซสชัน หน้าจอจะเปลี่ยนไปใช้อัตรารีเฟรชที่หารขีดจำกัดได้พอดี + HDR + เอาต์พุต HDR10 + ไม่พร้อมใช้งาน: %1$s + รีสตาร์ทแอปเพื่อให้มีผล + ไดรเวอร์ + ไดรเวอร์ GPU + ใช้ร่วมกันระหว่าง Steam และเดสก์ท็อป ตั้งค่าได้ที่หน้า Components + การสัมผัสและการควบคุม + การสัมผัส + ทัชแพด + โดยตรง + อัตโนมัติ: ทัชแพดในเดสก์ท็อป โดยตรงใน Steam + ปุ่มควบคุมบนหน้าจอ + Steam + QAM + อัตโนมัติจะแสดงปุ่มควบคุมเมื่อไม่มีคอนโทรลเลอร์เชื่อมต่ออยู่ + การสัมผัสของ Steam (ทดลอง) + ปุ่มสัมผัสของ Steam จะเริ่มใช้งานในเซสชัน Steam ถัดไป + รูปแบบปุ่มสัมผัสของ Steam + ย้าย ปรับขนาด หรือซ่อนปุ่มสัมผัสของเกมนี้ บันทึกไปยัง Steam + คอนโทรลเลอร์ + วิธีที่ Steam มองเห็นคอนโทรลเลอร์ของคุณ จะมีผลในเซสชันถัดไป + คอนโทรลเลอร์ Steam Deck + Steam Controller + DualShock 4 + DualSense + คอนโทรลเลอร์ Xbox 360 + Deck: ปุ่ม Quick Access และไจโร Steam Controller: เหมือนกัน พร้อมเซนเซอร์แรงจับที่ปุ่มด้านหลัง DualShock 4 และ DualSense: ไจโร และแทร็กแพดทำหน้าที่เป็นทัชแพด Xbox 360: เปิด Quick Access ด้วย Guide+A + กลับ + เมนู → QAM + QAM → เมนู + เซสชัน + การทำงานในพื้นหลัง + เมื่อซ่อนแอปหรือปิดหน้าจอ + กำหนดเอง + ดั้งเดิม + อัตโนมัติจะหยุดชั่วคราวและดำเนินการต่อเมื่อแสดงผล ดั้งเดิมเตรียม Steam เข้าสู่โหมดพักก่อน กำหนดเองรอการกดดำเนินการต่อ ไม่มีวันทำงานต่อเนื่องตลอด + ให้การดาวน์โหลดทำงานต่อไป + เลื่อนการหยุดพื้นหลังชั่วคราวออกไปขณะ Steam กำลังดาวน์โหลดอยู่ + อัตโนมัติจะหยุดชั่วคราวเมื่ออยู่ในพื้นหลังและดำเนินการต่อเมื่อแสดงผล กำหนดเองรอการกดดำเนินการต่อ ไม่มีวันทำงานต่อเนื่องตลอด + การเริ่มทำงาน + เริ่ม Steam พร้อมกับ DroidDeck + เปิดเซสชัน Steam เมื่อ DroidDeck เริ่มทำงาน + Decky + กำลังค้นหารุ่นที่เผยแพร่… + พร้อมติดตั้ง %1$s + ไม่พบบิลด์ที่เข้ากันได้ ลองใหม่อีกครั้งในหลัง + ติดตั้งแล้ว · %1$s + มีอัปเดต · %1$s + เป็นรุ่นล่าสุดแล้ว · %1$s + ตัวโหลด + ติดตั้งรุ่นล่าสุด + เริ่มพร้อมกับ Steam เมื่อเปิดใช้งาน แอปอื่นบนอุปกรณ์นี้อาจสามารถควบคุม Steam ได้ + ปิดอยู่ พอร์ตดีบักระยะไกลของ Steam ยังคงปิดอยู่ + ถอนการติดตั้ง Decky Loader? + ลบตัวโหลดออก พลักอินและการตั้งค่าของคุณจะยังคงอยู่ + ไคลเอนต์ + โอเวอร์เลย์แสดงประสิทธิภาพ + ปิดหากเกมขัดข้องหรือจอดำในโหมด Deck จะมีผลในเซสชันถัดไป + ซ่อมแซมไคลเอนต์ Steam + ดาวน์โหลดไคลเอนต์ Steam ใหม่อีกครั้งในครั้งถัดไปที่ Steam เริ่มทำงาน ใช้เมื่อ Steam ค้างอยู่ที่หน้าจอโหลด เกม การลงชื่อเข้าใช้ และการตั้งค่ายังคงอยู่ + ไคลเอนต์ Steam จะถูกดาวน์โหลดใหม่อีกครั้งในครั้งถัดไปที่ Steam เริ่มทำงาน + ซ่อมแซม + ภาพปก + คีย์ API ของ SteamGridDB + คีย์ของคุณ + แก้ไข… + เติมภาพปกที่ขาดไปจาก Steam จากนั้นจาก SteamGridDB ไฟล์ cover.jpg, hero.jpg, logo.png หรือ icon.png ของคุณเองในโฟลเดอร์เกมจะมีสิทธิ์ก่อน + ลบ %1$s + ลบหรือไม่? + เพิ่มเกม + เพิ่มเกม: + เลือกไฟล์ exe: + ตำแหน่งนี้ไม่พร้อมใช้งานในเซสชัน + ตรวจสอบไฟล์ exe นี้ + คอมโพเนนต์ของ Windows + LB / RB สลับระหว่างส่วนต่าง ๆ + ไม่มี + DLL ของ Windows ที่ Proton ไม่มีให้ จากรายการคอมโพเนนต์ เปิดใช้รายการที่เกมนี้ต้องการ: รายการเหล่านั้นจะถูกใส่ไว้ในโฟลเดอร์ Windows ของเกมเอง ไม่ใช่ไฟล์ของเกม และจะคงอยู่ไม่ว่าเกมจะรันบน Proton รุ่นใด + ไม่สามารถเข้าถึงรายการคอมโพเนนต์ได้ คอมโพเนนต์ที่ดาวน์โหลดไว้แล้วยังสามารถสลับได้ + แนะนำสำหรับเกมนี้ + กำลังตรวจสอบว่าเกมนี้ต้องการอะไร… + พร้อมใช้งานเมื่อออนไลน์ + + กำลังเปิดใช้คอมโพเนนต์ %1$d รายการ… + + เปิดใช้สำหรับเกมนี้ + Steam ติดตั้ง %1$s ร่วมกับ %2$s (แอป %3$s) + จากรายการของ GOG สำหรับเกมนี้ + จากข้อกำหนดเบื้องต้นของ Epic สำหรับเกมนี้ + จากขั้นตอนติดตั้งของ Amazon สำหรับเกมนี้ + Steam ติดตั้ง %1$s ร่วมกับแอป %2$s โดยจับคู่ตามชื่อ + ยังมีการแนะนำเพิ่ม + แนะนำโดย %1$s; ไม่ใช่คอมโพเนนต์ในที่นี้ + Steam + GOG + Epic Games + Amazon Games + เกมนี้มี %1$s มาด้วย + เกมนี้มี %1$s (ส่วนมากใช้งานได้ตามเดิม) + มีอยู่ในเกมนี้แล้ว + ไม่มีสิ่งใดในโฟลเดอร์เกมที่ชี้ไปยังคอมโพเนนต์ + ติดตั้งสำหรับเกมนี้ (%1$d) + คอมโพเนนต์ทั้งหมด (%1$d) + ต้องใช้ตัวติดตั้งของ Windows (%1$d) + รายการเหล่านี้เป็นโปรแกรมติดตั้งของตัวเอง ไม่มีแพ็กเกจภายในให้แยกส่วน และยังไม่มีการบันทึกสิ่งที่ติดตั้ง ส่วนใหญ่เป็นรันไทม์รุ่นเก่าที่ Mono ภายใน Proton ทำหน้าที่แทนอยู่แล้ว การบันทึกจะถูกเพิ่มเมื่อจัดทำแล้ว + แสดง + ซ่อน + รันโปรแกรมติดตั้งของตัวเอง (ยังไม่มีการบันทึก) + ยังไม่รองรับในที่นี้ + กำลังโหลดคอมโพเนนต์… + ติดตั้งไม่ได้: %1$s + ดาวน์โหลดแล้ว + กำลังดาวน์โหลด %1$s + กำลังติดตั้ง: %1$s + กำลังปิดใช้… + ดาวน์โหลด %1$s + จะมีผลในครั้งถัดไปที่เริ่มเกม + ยืดเกม 16:9 + บนหน้าจอที่เป็นสี่เหลี่ยมจัตุรัสกว่า 16:9 จะยืดเกมให้เต็มจอแทนการแสดงแถบดำ จะมีผลในเซสชันถัดไป + เสียง + DirectAudio สำหรับเกม + ข้าม PulseAudio เพื่อลดความหน่วง + ไมโครโฟน + สำหรับการแชทด้วยเสียง + เครือข่าย + การค้นหา Wi-Fi + แสดงชื่อ Wi-Fi และเครือข่ายใกล้เคียงใน Steam + บริการตำแหน่งปิดอยู่ + ไม่ได้รับสิทธิ์ตำแหน่ง + เปิดการตั้งค่า + ดำเนินการต่อ + เปิดการค้นหา Wi-Fi? + Android กำหนดให้ต้องมีสิทธิ์ตำแหน่งเพื่ออ่านชื่อ Wi-Fi และค้นหาเครือข่ายใกล้เคียง DroidDeck ใช้สิทธิ์นี้เพื่อแสดงเครือข่ายเหล่านั้นใน Steam เราไม่ติดตามตำแหน่งของคุณ การเข้าถึงอินเทอร์เน็ตทำงานได้โดยไม่ต้องมีสิทธิ์นี้ + พื้นที่จัดเก็บเกม + อัตโนมัติ - การ์ด SD เมื่อมีอยู่ รวมกับโฟลเดอร์ Games ของคุณ + อัตโนมัติ - โฟลเดอร์ Games ของคุณ รวมกับการ์ด SD เมื่อมีอยู่ (ยังไม่มีในขณะนี้) + พื้นที่ภายในเท่านั้น + โฟลเดอร์: %1$s + ไลบรารีที่สอง + เกมที่สตรีมข้อมูลจาก SD หรือพื้นที่จัดเก็บที่ใช้ร่วมกันอาจกระตุก + การวินิจฉัยพื้นที่จัดเก็บ + บันทึกผลการจัดสรรและเวลาพื้นที่จัดเก็บที่สุ่มตรวจไว้ในล็อกที่แชร์ อาจทำให้การติดตั้งช้าลงขณะเปิดใช้งาน จะมีผลในเซสชันถัดไป + เลือกโฟลเดอร์… + ความละเอียดกำหนดเอง + แทนที่ขีดจำกัดและอัตราส่วน ขนาดที่ไม่ตรงกับหน้าจอจะมีแถบดำ + ความกว้าง + ความสูง + ระหว่าง 320×240 ถึง 3840×2160 + + %1$d เกม + + ตกลง + ลบ + คืนค่า + ดาวน์โหลด + ลบ %1$s + %1$.1f MB + ไดรเวอร์รันไทม์ + ใช้โดย Steam เกม และเดสก์ท็อป จะมีผลในเซสชันถัดไป + นำเข้า Turnip zip… + ไดรเวอร์แสดงผล + วาดเฟรมบนหน้าจอในทั้งสองโหมด รีสตาร์ทแอปเพื่อให้มีผล + นำเข้า AdrenoTools zip… + ไม่เคย + ตรวจสอบ Nightlies ครั้งล่าสุด %1$s + Components + เกี่ยวกับ Components + ตรวจสอบไดรเวอร์ใหม่ + ตรวจสอบแพ็กเกจใหม่ใน Nightlies + Proton + Proton ที่เลือกไว้ที่นี่จะรันทุกเกมที่ยังไม่ได้เลือก Proton ของตัวเองใน Steam และการเปลี่ยนแปลงใน Steam ก็จะแสดงที่นี่ด้วย Steam จะแทนที่ Proton ของ Valve เมื่ออัปเดต ต้นฉบับจะถูกเก็บไว้ตามแต่ละบิลด์ + การตั้งค่า + ตั้งค่าโปรไฟล์ + โปรไฟล์ + โปรไฟล์เริ่มต้น + ใช้โดยอัตโนมัติกับเกมที่ไม่มีการปรับแต่งเฉพาะ + การตั้งค่าที่บันทึกไว้จะใช้โดยอัตโนมัติเมื่อ %1$s เปิดขึ้น + เลือกเฉพาะการตั้งค่าที่เกมนี้ต้องการ ส่วนที่เหลือจะยังใช้โปรไฟล์เริ่มต้นต่อไป + ค่าตั้งล่วงหน้าของ FEX + ใช้ค่าตั้งล่วงหน้าจากโปรไฟล์เริ่มต้น + บันทึกค่าตั้งล่วงหน้ากำหนดเองสำหรับเกมนี้แล้ว + ตัวแปร FEX กำหนดเอง + ใช้ค่าเริ่มต้น + ความเข้ากันได้ของเกม + ตัวเลือกสภาพแวดล้อมของ Proton, DXVK, VKD3D และการซิงค์สำหรับเกมนี้ + ใช้ Proton จากโปรไฟล์เริ่มต้น + บันทึก Proton กำหนดเองสำหรับเกมนี้ผ่านการจับคู่ความเข้ากันได้ของ Steam แล้ว + ไม่มี Proton + DXVK + VKD3D-Proton + ใช้แพ็กเกจของโปรไฟล์เริ่มต้นจาก %1$s เว้นแต่เกมนี้เลือกแพ็กเกจอื่น + ใช้แพ็กเกจคอมโพเนนต์ของโปรไฟล์เริ่มต้น + ใช้ %1$s เฉพาะสำหรับเกมนี้เท่านั้น + ตัวเลือกรันไทม์ + ตัวเลือกสภาพแวดล้อมอื่น ๆ ของ Proton, DXVK, VKD3D-Proton และการซิงค์เฉพาะเกม + บันทึกโปรไฟล์ของ %1$s ไว้สำหรับการเปิดเกมครั้งถัดไป + เริ่มโดยไม่ใช้ Steam + สำหรับเกมที่ปิดตัวทันที ใช้เริ่มเกมโดยไม่ตั้งค่า Steam ของ Proton ฟีเจอร์ของ Steam เช่นโอเวอร์เลย์จะถูกปิดสำหรับเกมนี้ + เปิด + บันทึกการเลือก Proton ของ %1$s แล้ว และพร้อมใช้งานใน Steam ตอนนี้ + บันทึกการเลือก Proton ของ %1$s แล้ว รีสตาร์ท Steam ก่อนเปิดเกม + %1$s ไม่สามารถใช้ Proton ที่เลือกบนอุปกรณ์นี้ได้ + ไม่สามารถโหลดโปรไฟล์ของ %1$s ได้: %2$s + ไม่สามารถบันทึกโปรไฟล์เกมได้: %1$s + รีเซ็ตโปรไฟล์เกม + รีเซ็ตโปรไฟล์ของ %1$s? + ลบการปรับแต่ง Proton, FEX, แพ็กเกจคอมโพเนนต์, สภาพแวดล้อม, การกรองพื้นผิว และคอมโพเนนต์ Windows ของเกมนี้หรือไม่? เกมจะใช้โปรไฟล์เริ่มต้นแทน + %1$s ใช้โปรไฟล์เริ่มต้นแล้วตอนนี้ + ไม่สามารถรีเซ็ตโปรไฟล์เกมได้ทั้งหมด: %1$s + %1$s รันทุกเกมที่ยังไม่ได้เลือก Proton ของตัวเองใน Steam แล้วตอนนี้ + %1$s จะรันทุกเกมที่ยังไม่ได้เลือก Proton ของตัวเอง ตั้งแต่ครั้งถัดไปที่ Steam เริ่มทำงาน + %1$s ไม่สามารถเริ่มเกมบนอุปกรณ์นี้ได้ จึงไม่สามารถเป็นค่าเริ่มต้นได้ + ไม่สามารถบันทึก Proton เริ่มต้นได้: %1$s + %1$s · สลับแล้ว %2$d รายการ + %1$s · เป็นต้นฉบับทั้งหมด + %1$s · เกมกำลังทำงาน + %1$s · แพ็ก droiddeck-esync + %1$s · ยังไม่มีแพ็ก droiddeck-esync + %1$s · มี esync อยู่ในตัวแล้ว ไม่ต้องใช้แพ็ก + ไดรเวอร์ GPU + %1$s · นำมาใช้ใหม่เมื่อเปิด %2$s + ถัดไป: %1$s · หลังจากปิดเกม + กำลังใช้ %1$s: %2$s + รอจนกว่าเกมจะปิด + จะมีผลในครั้งถัดไปที่เกมเริ่มทำงาน + นำเข้า .zip + กำลังอ่าน Proton… + ยังไม่มี Proton ติดตั้งอยู่ เริ่ม Steam สักครั้งเพื่อดาวน์โหลด Proton ARM64 ของมัน หรือเพิ่ม GE-Proton, CachyOS หรือ DroidDeck-Proton จากการตั้งค่า + ต้นฉบับ · %1$s + %1$s · บิลด์นี้ + %1$s · บิลด์ก่อนหน้า + ติดตั้งแล้ว · %1$d + ยังไม่มีอะไรบันทึกไว้สำหรับ %1$s ไฟล์ดั้งเดิมของ Proton นี้ถูกบันทึกเป็นต้นฉบับ + มีเกมกำลังทำงานบน Proton นี้ ดังนั้นจะรอจนกว่าเกมจะปิด + จะมีผลในครั้งถัดไปที่เกมเริ่มทำงาน + สลับ %1$s? + ใส่ %1$s เข้าไปใน %2$s หรือไม่? ไฟล์ต้นฉบับจะยังถูกบันทึกไว้ %3$s + คืนค่า %1$s? + นำต้นฉบับของ %1$s กลับมาไว้ใน %2$s หรือไม่? %3$s + ลบแพ็กเกจออกจากแอป คุณสามารถดาวน์โหลดใหม่ได้อีก + ลบต้นฉบับนี้? + ต้นฉบับ %1$s นี้มาจากบิลด์ก่อนหน้าของ %2$s ต้นฉบับของบิลด์ปัจจุบันจะถูกเก็บไว้เสมอ + ใน Nightlies · %1$d + รีเฟรชเพื่อแสดงรายการแพ็กเกจใน Nightlies + ไม่มีอะไรใหม่ตั้งแต่การตรวจสอบครั้งล่าสุด + นำเข้า .wcp + คืนค่าต้นฉบับของ Proton นี้ + นำต้นฉบับ %2$s ของ %1$s กลับมาหรือไม่? + สลับ + FEX, DXVK และ VKD3D-Proton สำหรับ Proton แต่ละตัวที่ Steam ใช้รันเกม\n\nเลือกแถวที่ติดตั้งไว้เพื่อสลับเข้าใช้งาน จะมีผลในครั้งถัดไปที่เกมเริ่มทำงาน หรือหลังจากเกมที่กำลังทำงานปิดลง\n\nตัวเลือกของคุณจะถูกตรวจสอบใหม่ก่อนการเริ่มเกมทุกครั้ง และนำกลับมาใช้หากมีสิ่งใดเปลี่ยนไป ไฟล์ต้นฉบับของ Proton แต่ละตัวจะถูกเก็บไว้ ดังนั้นการอัปเดตของ Steam จะไม่ทำให้สูญหาย + ต้นฉบับ + ใช้งานอยู่ + ใหม่ + บันทึกไว้ + ประสิทธิภาพ + คอร์ CPU การแก้ไขเซสชัน และชื่อโฮสต์ จะมีผลในเซสชันถัดไป + คอร์สำหรับไคลเอนต์ Steam + กำหนดคอร์เองแทนการเลือกของ Steam + ตรึง Steam ตัวช่วย UI และ gamescope ไว้ที่คอร์ด้านล่าง + คอร์ไคลเอนต์ + คอร์สำหรับ Steam + เปิดการกำหนดเองเพื่อเลือก + คอร์สำหรับเกม + คอร์เกม + คอร์สำหรับเกม เมื่อเลือกทั้งหมด Android จะจัดสรรได้อย่างอิสระ + อินเทอร์เฟซไคลเอนต์ + GL แบบเธรด + อาจทำให้เมนู Steam ตอบสนองไวขึ้น + Zink: descriptor แบบเลซี่ + แนะนำสำหรับไดรเวอร์ที่ไม่มี descriptor buffer + ข้ามการตรวจสอบข้อผิดพลาด GL + ปิดการตรวจสอบความถูกต้องของ GL ในทุกการเรียก + gamescope: คิว GPU แบบเรียลไทม์ + ให้ความสำคัญกับงาน GPU ของตัวประกอบภาพก่อน อาจทำให้การเรียงเฟรมราบรื่นขึ้นโดยแลกกับประสิทธิภาพของเกมบางส่วน + GPU + ตรึง GPU ไว้ที่ความเร็วสัญญาณนาฬิกาสูงสุด + สำหรับ Adreno เท่านั้น สะดุดน้อยลง แต่ใช้แบตเตอรี่และเกิดความร้อนมากขึ้น + การแก้ไขเซสชัน + Turnip: การเรนเดอร์ sysmem + เร็วขึ้นสำหรับ Steam และเกมส่วนใหญ่ จำเป็นสำหรับ Adreno 710/720/722 + ข้ามตัวช่วย xalia ของ Steam + droiddeck-ntsync (ทดลอง) + ตัวแทนในระดับยูสเซอร์สเปซของไดรเวอร์ ntsync ของ Linux สำหรับการทดสอบ เมื่อเปิดใช้ Proton จะใช้สิ่งนี้แทน droiddeck-esync และ droiddeck-fsync ใช้ได้กับ Proton Experimental ARM64, GE-Proton และ proton-cachyos จะมีผลในเซสชันถัดไป + droiddeck-esync + droiddeck-fsync ก่อน + รัน fsync ของ Proton เองบนไฟล์ Proton มาตรฐาน โดยมี futex_waitv ที่จัดเตรียมในระดับยูสเซอร์สเปซ จึงไม่ต้องใช้แพ็กและยังคงทำงานได้ทันทีที่ Steam อัปเดต Proton เมื่อเปิดใช้ เกมจะใช้สิ่งนี้ก่อน droiddeck-esync ยกเว้นบางส่วนที่ Proton ปิด fsync ไว้ จะมีผลในเซสชันถัดไป + เธรด Windows ของเกมรอกันผ่าน eventfd และหน่วยความจำที่ใช้ร่วมกัน แทนการเดินทางไปกลับผ่าน wineserver ใช้แพ็ก droiddeck-esync ที่สร้างไว้สำหรับบิลด์ Proton นั้นโดยเฉพาะ แพ็กจะถูกดึงมาโดยอัตโนมัติ จนกว่าจะมีแพ็กสำหรับบิลด์ Proton ใหม่ droiddeck-fsync จะทำหน้าที่แทน จะมีผลในเซสชันถัดไป + ปิดตัวช่วยควบคุมด้วยคอนโทรลเลอร์ของ Proton ซึ่งกินพลัง CPU ในทุกเกม + รัน proot โดยไม่ใช้ seccomp + อาจแก้ข้อผิดพลาด syscall ที่ขาดหายบนบางเคอร์เนล อาจลดประสิทธิภาพ + เส้นทางลัดของ proot + ต้องใช้ตัวกรอง seccomp ของ proot + ค้นหาไฟล์เร็วขึ้นและสะดุดน้อยลง ปิดหากโปรแกรมหาไฟล์ของตัวเองไม่พบ + ข้อมูลประจำตัวของเซสชัน + ชื่อโฮสต์ + ชื่อที่ Steam มองเห็นสำหรับอุปกรณ์นี้ เว้นว่างไว้เพื่อคืนค่าเป็น %1$s + ใช้ได้เฉพาะตัวอักษร ตัวเลข และยัติภังค์ตรงกลาง ไม่เกิน 63 ตัวอักษร + Android ถูกตั้งให้ปิดเซสชันนี้ + ติดตั้งแล้ว + พร้อมดาวน์โหลด + ไม่มีอะไรใหม่ตั้งแต่การตรวจสอบครั้งล่าสุด + คืนค่าไดรเวอร์ที่มีในตัว + มีอยู่ในตัวแอป: การลบนี้จะลบไฟล์ที่แตกไว้ออกและซ่อนมัน คืนค่าไดรเวอร์ที่มีในตัวจะนำกลับมา + ครึ่งหนึ่งของแพ็ก Android + Linux: ทั้งสองครึ่งจะถูกลบ ไดรเวอร์ที่ใช้งานอยู่จะกลับไปใช้ค่าเริ่มต้น + ไฟล์ของมันจะถูกลบออกจากแอป + ไฟล์ของมันถูกลบออก เนื่องจากกำลังใช้งานอยู่ ค่าเริ่มต้นจะเข้ามาแทนที่ + หน้าจอ + เอฟเฟกต์ + การควบคุม + Components + คอนโทรลเลอร์ + การตั้งค่า + หน้า %1$s + Steam + เดสก์ท็อป + เมนู Steam + Quick Access + เปิดเมนู Quick Access + หน้าก่อนหน้า + หน้าถัดไป + HUD ประสิทธิภาพ + โหมดการปรับขนาด + ความคมชัดของการปรับขนาด + เอฟเฟกต์หน้าจอ + แบบเรียลไทม์ ใช้เวลา GPU เล็กน้อยขณะเปิดเอฟเฟกต์ใด ๆ อยู่ + ค่าที่กำหนดไว้ล่วงหน้า + กำหนดเอง + ขั้นสูง + ขยายแล้ว + ย่อแล้ว + ความคมชัด (CAS) + ระดับความคมชัด + HDR จำลอง + ลดลายแถบสี + ความเข้มของการลดลายแถบสี + ความสว่าง + ความเข้มสี + แกมมา + ความอิ่มตัวของสี + FXAA (ลบขอบหยัก) + เส้นขอบแบบการ์ตูน + CRT + NTSC + การกรองพื้นผิว + เกม DirectX 9–11 มีผลตั้งแต่ครั้งถัดไปที่เปิดเกม + การกรองแบบ Anisotropic + ความคมชัดของพื้นผิว + ค่าไบแอส mip ติดลบ: พื้นผิวคมขึ้น มีประกายวูบวาบบ้าง อัตโนมัติจะปรับตามระดับที่เซสชันถูกขยายให้พอดีหน้าจอ + อัตโนมัติ (%1$s) + แป้นพิมพ์ + แป้นพิมพ์พีซี + Android + หน้าจอที่สอง + โหมด + แสดง + แอป Android + เปิดแอป + ซ่อน + แสดง + ไม่มีแอปที่เปิดได้ + การทำงานของเซสชัน + มีผลตั้งแต่ตอนนี้และกับเซสชันในอนาคตในโหมดนี้ + เซสชันถัดไป + การสนับสนุน + ล็อกเซสชัน + แนบไปกับรายงานข้อบกพร่อง + แชร์ล็อก + พื้นหลัง + หยุด + หยุดเซสชัน? + กำลังทำงานอยู่ตอนนี้ + ไม่มีเกมกำลังทำงานบน Proton + โปรไฟล์เกม + รายละเอียดโปรไฟล์ยังไม่พร้อมใช้งาน + โหลดโปรไฟล์เกมไม่ได้ กำลังลองใหม่โดยอัตโนมัติ + รีเฟรชโปรไฟล์เกมไม่ได้ แสดงข้อมูลล่าสุดที่มีอยู่ กำลังลองใหม่โดยอัตโนมัติ + ดูและแก้ไขการตั้งค่าที่บันทึกไว้สำหรับการเปิดครั้งถัดไป + ใช้โปรไฟล์เริ่มต้น + โปรไฟล์กำหนดเอง + ใช้ %1$s จากโปรไฟล์เริ่มต้น + การเปลี่ยนแปลงจะถูกบันทึกในโปรไฟล์ของ %1$s และมีผลในครั้งถัดไปที่เกมเปิดขึ้น + + การปรับแต่งเกม %1$d รายการ + + ยังไม่มี Proton ติดตั้งอยู่ + เกมกำลังทำงาน + นำมาใช้ใหม่เมื่อเปิด %1$s + ต้นฉบับ + นำเข้าและลบได้ที่หน้า Components + เสร็จแล้ว + ต้องดำเนินการ + กำลังดำเนินการ + ตรวจสอบที่อยู่ IP และพอร์ตของ Wireless debugging หรือจับคู่ใหม่หาก Android ลบอุปกรณ์นี้ไปแล้ว + ติดตั้ง + จัดการ + ภาพรวม + คอนโทรลเลอร์ + เซสชัน + ตัวเปิดแอป + การตั้งค่า + ● พร้อมทั้งหมด + พร้อม %1$d จาก %2$d + ตรวจสอบระบบ + รองรับอุปกรณ์นี้ + GPU ที่ยังไม่ผ่านการทดสอบ + ไม่รองรับ GPU นี้ + %1$s · %2$s + %1$s: %2$s + Steam ต้องใช้ไดรเวอร์ Adreno; %1$s อาจแสดงจอดำ + รันไทม์ Linux + %1$s · %2$d%% + ยังไม่ติดตั้ง · ประมาณ 3 GB จะติดตั้งเมื่อกด Play ครั้งแรก + ติดตั้งแล้ว + %1$s · มีอัปเดต + %1$s · เป็นรุ่นล่าสุดแล้ว + ขีดจำกัดโพรเซสลูก + เปิดอยู่ · Android อาจปิด Steam การปิดใช้งานใช้เวลาประมาณหนึ่งนาที + ยังไม่ตั้งค่า · Android อาจปิด Steam การปิดใช้งานใช้เวลาประมาณหนึ่งนาที + ไม่สามารถตรวจสอบได้ · การปิดใช้งานใช้เวลาประมาณหนึ่งนาที + แก้ไข + รายละเอียด + ตัวเลือกสำหรับนักพัฒนา + ใช้ Wireless debugging + ตรวจสอบอีกครั้ง + เปิดขีดจำกัด + บัญชี Steam + ลงชื่อเข้าใช้เป็น %1$s · โหมดออฟไลน์ + ลงชื่อเข้าใช้เป็น %1$s + กด Play แล้วลงชื่อเข้าใช้ Steam + เครื่องมือ + คอนโทรลเลอร์ + การตั้งค่าคอนโทรลเลอร์ไม่พร้อมใช้งาน + เซสชัน + ล็อกเซสชัน + บันทึกไว้หลังจบแต่ละเซสชัน + เครื่องมือดีบัก + ล็อกเซสชันล่าสุด + ล็อกเซสชันที่บันทึกไว้ + เก็บ %1$d รายการล่าสุด + ล้างล็อก + โหมดออฟไลน์ + ลงชื่อเข้าใช้ Steam ก่อน + ตัวเปิดแอป + ภาษา + Steam ก็ใช้ภาษานี้ด้วย ตั้งแต่ครั้งถัดไปที่เริ่มทำงาน + ค่าเริ่มต้นของระบบ (%1$s) + ค่าเริ่มต้นของระบบ + ขนาดแอป + ข้อความและการควบคุมของ DroidDeck + %1$d%% (ค่าเริ่มต้น) + ธีม + รูปลักษณ์ของตัวเปิดแอป + ใช้เป็นหน้าจอหลัก + DroidDeck สามารถเป็นแอปหน้าจอหลักได้ + DroidDeck จะไม่ถูกเสนอให้เป็นแอปหน้าจอหลัก + การวางแนว + แนวนอนอัตโนมัติ + แนวนอน + แนวนอนกลับด้าน + เต็มจอ + ซ่อนแถบสถานะและแถบนำทางของ Android + แสดงแถบสถานะและแถบนำทางของ Android + แอปหน้าจอหลักเริ่มต้น + เลือกแอปหน้าจอหลัก + เลือก + แอป Linux (ทดลอง) + ไฟล์ + เรียกดูและจัดการไฟล์ + รุ่นของ Proton + ติดตั้งบิลด์ Proton แบบ ARM64 + ประสิทธิภาพ + การกำหนดคอร์ CPU + โฟลเดอร์ ROM + ตำแหน่งที่เก็บเกมของโปรแกรมจำลอง + Components + FEX, DXVK, VKD3D + การควบคุม + การกำหนดปุ่ม + ค้นพบ + ค้นหา + ติดตั้งแล้ว + %1$s · %2$d + สโตร์ + Flathub · ARM64 + ● Flatpak พร้อมใช้งาน + ติดตั้งรันไทม์ Linux จากการตั้งค่าก่อน + ตั้งค่า Flatpak + เพิ่ม Flatpak และเชื่อมต่อกับ Flathub (ประมาณ 20 MB) แอปแรกจะดาวน์โหลดรันไทม์ของตัวเองด้วย + กำลังตั้งค่า… + Flatpak + การอัปเดต + %1$s · %2$s + %1$s · %2$s · %3$d%% + กำลังเริ่ม… + หมวดหมู่ + ไม่สามารถเชื่อมต่อ Flathub ได้ ตรวจสอบการเชื่อมต่อแล้วลองใหม่ + ลองใหม่ + ค้นหาใน Flathub + Firefox, เกม, โปรแกรมจำลอง… + ค้นหา + ทั้งหมด + กำลังค้นหา… + ค้นหาตามชื่อ หรือเลือกหมวดหมู่ แสดงเฉพาะแอป ARM64 + ไม่มีแอป ARM64 บน Flathub ที่ตรงกับคำค้นนี้ + ผลลัพธ์ + ตรวจสอบการอัปเดต + อัปเดตทั้งหมด (%1$d) + ● ทุกอย่างเป็นรุ่นล่าสุดแล้ว + ยังไม่มีแอป ค้นหาได้จากค้นพบหรือค้นหา + ตั้งค่า Flatpak ก่อนเพื่อติดตั้งแอป + มีอัปเดต · %1$s + เปิด + รายละเอียด + ลบ + กดอีกครั้งเพื่อลบ + กำลังติดตั้ง… + ติดตั้ง + ตั้งค่า Flatpak ก่อน + ดาวน์โหลด %1$s + ● ติดตั้งแล้ว + ไม่สามารถโหลดหน้า Flathub ของแอปนี้ได้ + กำลังโหลดจาก Flathub… + ไม่มีบิลด์ ARM64 บน Flathub จึงติดตั้งที่นี่ไม่ได้ + ภาพหน้าจอ + เกี่ยวกับ + รหัสแอป + สัญญาอนุญาต + รันไทม์ + ขนาดที่ติดตั้ง + %1$s (รวมรันไทม์ที่ใช้ร่วมกัน) + เว็บไซต์ + ยืนยันแล้ว + การอัปเดต + DroidDeck %1$s · %2$s + ตรวจสอบล่าสุด %1$s + ยังไม่ตรวจสอบ + ให้ DroidDeck อัปเดตตัวเองได้ + ในหน้าจอถัดไป เปิด “อนุญาตจากแหล่งนี้” แล้วกลับมาที่นี่ การอัปเดตจะดำเนินต่อไปเอง + เปิดการตั้งค่า + ไม่ใช่ตอนนี้ + ลงนามต่างกัน + อัปเดตทับไม่ได้ + สำเนานี้ไม่ได้ลงนามด้วยคีย์รุ่นวางจำหน่ายของ DroidDeck จึงไม่สามารถติดตั้งอัปเดตทับได้ ถอนการติดตั้งเพื่อเปลี่ยน + มีอัปเดต + ติดตั้งไม่ได้ + พร้อมเปลี่ยน + นำหน้า Stable + คุณนำหน้า Stable อยู่ + บิลด์นี้ใหม่กว่า Stable รุ่นล่าสุด คุณจะเปลี่ยนไปใช้ Stable เมื่อมีการเผยแพร่รุ่นถัดไป + การทดสอบสิ้นสุดแล้ว + การทดสอบนี้สิ้นสุดแล้ว + การแก้ไขของมันถูกรวมเข้าหรือถูกยกเลิกไปแล้ว ติดตาม Preview เพื่อรับการแก้ไขล่าสุด + เป็นรุ่นล่าสุดแล้ว + คุณมี %1$s รุ่นล่าสุดแล้ว + · %1$s + %1$s… %2$d%% + %1$s… + อัปเดต%1$s + ติดตั้ง%1$s + ติดตั้ง Stable %1$s อยู่ดี + ติดตาม Preview + หยุดเซสชันที่กำลังทำงานอยู่เพื่ออัปเดต + น้อยลง + เพิ่มเติม + DroidDeck %1$s + บิลด์ Preview ใหม่ + บิลด์ทดสอบ PR #%1$d + %1$d MB + ช่องทางอัปเดต + Stable + รุ่นที่ผ่านการทดสอบแล้ว เหมาะกับคนส่วนใหญ่ + %1$s · %2$s + Preview + การแก้ไขล่าสุด ก่อนถึง Stable + เป็นรุ่นล่าสุดแล้ว + บิลด์ทดสอบ + ลองการแก้ไขก่อนเผยแพร่จริง + ไม่มีในขณะนี้ + PR #%1$d + ไม่ได้สร้างไว้สำหรับสำเนานี้ของ DroidDeck + อัปเดตแล้ว %1$s + การทดสอบ PR #%1$d + สักพักที่แล้ว + เมื่อสักครู่ + + %1$d รายการให้ลอง + + + การเปลี่ยนแปลงตั้งแต่บิลด์ของคุณ · ล้าหลัง %1$d บิลด์ + + + การเปลี่ยนแปลงล่าสุด · %1$d บิลด์ + + มีการตั้งค่า Android หนึ่งอย่างที่ต้องเปลี่ยน + Steam + Android หยุดแอปที่เริ่มโพรเซสพื้นหลังจำนวนมาก และ Steam เริ่มหลายสิบโพรเซส จนกว่าจะเปลี่ยนค่านี้ เกมจะปิดตัวโดยไม่มีข้อผิดพลาด + แก้ให้ฉันเลย + เปิด “Disable child process restrictions” แล้วกลับมาที่นี่ เปิดตัวเลือกสำหรับนักพัฒนาไว้เสมอ ไม่เช่นนั้นสวิตช์จะปิดลงอีก + เปิดตัวเลือกสำหรับนักพัฒนา + Android รุ่นนี้ไม่มีสวิตช์สำหรับสิ่งนี้ ดังนั้น DroidDeck จะเปลี่ยนค่าผ่าน Wireless debugging ใช้เวลาประมาณหนึ่งนาที ไม่ต้องใช้คอมพิวเตอร์ + Wireless debugging ต้องใช้ Wi-Fi เครือข่ายใดก็ได้ ไม่จำเป็นต้องมีอินเทอร์เน็ต + รหัสจับคู่จะแสดงในการแจ้งเตือน อนุญาตการแจ้งเตือนสำหรับ DroidDeck หรือใช้วิธีอื่น + อนุญาตการแจ้งเตือน + เปิดการตั้งค่า → ตัวเลือกสำหรับนักพัฒนา → Wireless debugging แล้วเปิดใช้งาน + แตะ “Pair device with pairing code” + พิมพ์รหัสลงในการแจ้งเตือนของ DroidDeck + กำลังรอป๊อปอัปการจับคู่… + พบป๊อปอัปแล้ว กรอกรหัสในการแจ้งเตือน + สถานะ + กำลังตรวจสอบทุก 2 วินาที หน้านี้จะปิดเมื่อปิดใช้งานแล้ว + วิธีอื่น + Wireless debugging, ที่อยู่แบบกำหนดเอง, ADB จากคอมพิวเตอร์ + ที่อยู่แบบกำหนดเอง, ADB จากคอมพิวเตอร์, ตัวเลือกสำหรับนักพัฒนา + ใช้ Wireless debugging แทน + บาง ROM เพิ่มสวิตช์นี้มาเองเลย ลองหา “child process” ในตัวเลือกสำหรับนักพัฒนา + หากการแจ้งเตือนไม่ทำงาน ให้กรอกที่อยู่และรหัสจากป๊อปอัปด้วยตัวเอง + กรอกที่อยู่ด้วยตัวเอง + จากคอมพิวเตอร์ที่มี ADB: + คัดลอกคำสั่ง + เลือกหน้าจอที่จะเปิดการตั้งค่า Android + หน้าจอหลัก + จับคู่แล้ว กำลังค้นหาพอร์ตของ Wireless debugging… + จับคู่แล้ว กรอกพอร์ตจากการตั้งค่า + พบอุปกรณ์แล้ว กำลังปรับใช้การตั้งค่า… + ไม่สามารถปรับใช้โดยอัตโนมัติได้ ตรวจสอบที่อยู่ด้านล่างแล้วลองใหม่ %1$s + Wireless debugging + อัปเดตขีดจำกัดโพรเซสลูกแล้ว + เปลี่ยนขีดจำกัดโพรเซสลูก + หากไม่พบตัวเลือกนี้ ให้จับคู่ผ่าน Wireless debugging + จับคู่แล้ว ตรวจสอบที่อยู่ IP และพอร์ตใน Wireless debugging + อัปเดตขีดจำกัดโพรเซสลูกแล้ว + ใช้ขั้นตอนที่ 2 หากตัวเลือกสำหรับนักพัฒนาไม่มีการตั้งค่าโพรเซสลูก เปิดตัวเลือกสำหรับนักพัฒนา → Wireless debugging → Pair device with pairing code + จับคู่แล้ว ใช้ที่อยู่ด้านล่าง หรืออัปเดตจาก Wireless debugging → IP address & Port + อัปเดตขีดจำกัดโพรเซสลูกผ่าน Wireless debugging แล้ว + Android ยืนยันแล้วว่าขีดจำกัดเปิดอยู่ + Android ยืนยันแล้วว่าขีดจำกัดปิดอยู่ + 2 · จับคู่ Wireless debugging + ที่อยู่ IP และพอร์ตของป๊อปอัปการจับคู่ + รหัสจับคู่ (PIN) + 6 หลัก + จับคู่ + 2 · ปรับใช้ผ่าน Wireless debugging + ที่อยู่ IP และพอร์ตสำหรับเชื่อมต่อ + จับคู่อีกครั้ง + ปิดขีดจำกัด + วิธีอื่นในการเปลี่ยนค่านี้ + 1 · ตัวเลือกสำหรับนักพัฒนา (ลองก่อน) + เปิด “Restrict child processes” + ปิด “Restrict child processes” + 3 · ADB จากคอมพิวเตอร์ (ทางเลือกสุดท้าย) + ใช้เฉพาะเมื่อ Wireless debugging ใช้งานไม่ได้ + 3 · ADB จากคอมพิวเตอร์ (ทางเลือกสุดท้าย) + เมื่อปุ่มสัมผัสจะปรากฏในเซสชัน + สี + สีของปุ่มบนหน้าจอ + กำหนดเอง + ความโปร่งใส + %1$d%% + ขนาดปุ่ม + 100% คือขนาดมาตรฐาน + การกดสติ๊ก + แตะสองครั้งแล้วกดสติ๊กค้างไว้สำหรับ L3 หรือ R3 + สติ๊กแบบปรับอัตโนมัติ + การสั่นของเกม + ปุ่ม Steam + ปุ่ม QAM + ปุ่มแป้นพิมพ์ + กด Select เพื่อเปิดเมนู Steam + แทน View/Select + ภาพเคลื่อนไหว + สติ๊กจะปรากฏที่ตำแหน่งที่คุณแตะและซ่อนเมื่อปล่อย + การจัดวาง + บันทึกตำแหน่งกำหนดเองไว้แล้ว + ปรับให้พอดีกับหน้าจอนี้ + แก้ไข + รีเซ็ต + การกำหนดปุ่ม + ทุกปุ่มส่งอินพุตของตัวเอง + กำหนดใหม่ %1$d จาก %2$d ปุ่ม + ตั้งค่า + รีเซ็ตคอนโทรลเลอร์ + คืนค่าการตั้งค่าคอนโทรลเลอร์ทั้งหมดและการจัดวาง + สิ่งที่ปุ่มบนหน้าจอแต่ละปุ่มส่งไปยังเกม ซ่อนจะเอาปุ่มนั้นออก + ปุ่มบนหน้าจอ + ค่าเริ่มต้น + รีเซ็ตการกำหนดปุ่ม + แอป Android + %1$d แอป + ไม่พบแอป Android ที่เปิดได้ + ● ติดตั้งเดสก์ท็อปแล้ว + ติดตั้งเมื่อเปิดครั้งแรก + พร้อมติดตั้ง + ยังไม่ติดตั้ง + %1$s · ยังไม่ติดตั้ง + เปิด %1$s + เกม + เลือกโฟลเดอร์ ROM + ไปที่ /root/ROMs ใน RetroArch + เพิ่มเกม %1$s ลงใน ROMs/%2$s + ติดตั้ง %1$s + หยุดเซสชันเพื่อติดตั้ง + ต้องมีรันไทม์ + กำลังโหลดรายละเอียดการติดตั้ง… + รายละเอียดการติดตั้งไม่พร้อมใช้งาน ลองใหม่ในหลัง + ติดตั้งรันไทม์ Linux จากการตั้งค่าก่อน + หยุดเซสชันที่กำลังทำงานเพื่อติดตั้งแอปเดสก์ท็อป + ติดตั้ง %1$s ลงในเดสก์ท็อป Linux + %1$s · %2$d%% + การซ่อนจะเอาออกจากเดสก์ท็อป ไฟล์ของมันยังอยู่ในรันไทม์ Linux + เกมนั้นไม่มีอยู่ในโฟลเดอร์ ROM แล้ว + เดสก์ท็อป · %1$s + เปิดใน %1$s + มีอยู่ใน %1$s ด้วย + หน้านั้นหายไปแล้ว เลือกส่วนทางด้านซ้าย + เปิดเดสก์ท็อป + ติดตั้งและเปิดเดสก์ท็อป + ถอนการติดตั้งเดสก์ท็อป + ลบเดสก์ท็อปและแอปที่มาพร้อมกัน เกม แอปที่เพิ่มเข้ามา การตั้งค่า และไลบรารีที่ใช้ร่วมกันจะยังคงอยู่ + กำลังลบเดสก์ท็อป + ลบเดสก์ท็อปแล้ว + กำลังลบไฟล์ LXQt เก่า + อัปเดต DroidDeck ก่อนลบเดสก์ท็อปรุ่นนี้ + เดสก์ท็อป Linux + KDE Plasma, Firefox และโปรแกรมจำลองของคุณ + เกมที่คุณติดตั้งใน Steam จะปรากฏที่นี่หลังจากจบเซสชัน + เล่น Steam + การตั้งค่าการเปิดเกม + เปิดเกม + ไฟล์เกม + Prefix ของ Proton + กำลังค้นหาเซฟ… + ไม่พบโฟลเดอร์เซฟที่ตรงชื่อ การส่งออกจะนำทั้งโฟลเดอร์ผู้ใช้ไป + %1$d โฟลเดอร์เซฟ + %1$s · %2$d ไฟล์ · %3$s + %1$d KB + จัดการเซฟ + เซฟของเกม + นำเข้าเซฟ… + ส่งออกเป็น GameHub zip… + ส่งออกเป็น Winlator zip… + เปิดโฟลเดอร์เซฟ + ยังไม่เคยเล่น + รันไทม์กำลังทำงาน · %1$d%% + รันไทม์กำลังทำงาน + รันไทม์จะติดตั้งเมื่อกด Play ครั้งแรก + มีอัปเดตรันไทม์ + ● รันไทม์พร้อมใช้งาน + เล่นล่าสุด %1$s + พื้นที่จัดเก็บภายใน + เกมที่เพิ่ม + เกมกำหนดเอง + พื้นที่จัดเก็บเกม + คู่ที่ดีที่สุดสำหรับ GPU นี้ อัปเดตให้ล่าสุดเสมอ + เลือกคู่ด้วยตัวเอง + กำลังตรวจสอบไดรเวอร์… + ยังไม่มีไดรเวอร์ในรายการ รีเฟรชเพื่อค้นหา + ซ่อนไดรเวอร์สำหรับ GPU อื่น + แสดงไดรเวอร์สำหรับ GPU อื่น + ขั้นสูง + ตั้งค่าร่วมกับไดรเวอร์แสดงผล: %1$s รวมทั้งสองอย่าง + ตั้งค่าร่วมกับไดรเวอร์รันไทม์: %1$s รวมทั้งสองอย่าง + ใช้โดยเกมและเดสก์ท็อป + ค่าเริ่มต้นของรันไทม์ + วาดเฟรมบนหน้าจอ จะมีผลหลัง DroidDeck รีสตาร์ท + อัตโนมัติ - เลือกตาม GPU + ดาวน์โหลด นำเข้า หรือลบ… + กำลังใช้งาน + ไดรเวอร์ที่มีในตัว DroidDeck + ไดรเวอร์รันไทม์และแสดงผลตั้งค่าร่วมกันเป็นคู่ที่เข้ากัน + แนะนำสำหรับ GPU นี้: %1$s + รีเฟรชเพื่อค้นหาไดรเวอร์สำหรับ GPU นี้ + แนะนำ + ไม่พร้อมใช้งาน + รันไทม์ Linux + กำลังเตรียมรันไทม์ Linux + เซสชัน + กำลังเริ่มการแสดงผลและเสียง + ไคลเอนต์ Steam + กำลังตรวจสอบ Steam และเครื่องมือความเข้ากันได้ + กำลังเปิด Steam + กำลังรอเฟรมแรกของ Steam + เดสก์ท็อป + กำลังเตรียมเดสก์ท็อป + กำลังเปิด + กำลังรอเฟรมแรก + กำลังเริ่ม Steam + เซสชันสิ้นสุดแล้ว + ขั้นตอนที่ %1$d จาก %2$d + Steam หยุดชั่วคราว + ปุ่มด้านหลัง + แพดซ้าย + แพดขวา + ทั้งสองข้าง + เกมแพด + สติ๊ก + แทร็กแพด + สลับระหว่างสติ๊กและแทร็กแพด + การควบคุม Deck เพิ่มเติม + แผง + ปิดการควบคุมหน้าจอที่สอง + เปิดเมนู Steam + ซ้าย · บน + ซ้าย · ล่าง + ขวา · บน + ขวา · ล่าง + คลิก + แทร็กแพดขวา + แทร็กแพดซ้าย + แทร็กแพด + แสดงแป้นพิมพ์ + ซ่อนแป้นพิมพ์ + เทอร์มินัล Linux + เซสชัน Linux ยังไม่พร้อมสำหรับเทอร์มินัล + Steam + เกม + เดสก์ท็อป + สโตร์ + Components + แอป + การตั้งค่า + การอัปเดต + %1$d%% + ดำเนินการต่อ %1$s + แสดงเป็น /root/ROMs ในแต่ละเซสชัน พื้นที่จัดเก็บภายในอยู่ที่ /root/Storage + ยังไม่เลือกโฟลเดอร์ + มีผลในเซสชันถัดไป ใช้การ์ด SD ได้ + เลือกโฟลเดอร์ + เปลี่ยนโฟลเดอร์ + ติดตั้งบิลด์ Proton แบบ ARM64 แล้วเลือกให้แต่ละเกมใน Steam → Properties → Compatibility + หยุดเซสชันที่กำลังทำงานเพื่อติดตั้งหรือลบเครื่องมือความเข้ากันได้ + บิลด์ที่พร้อมใช้งาน + ติดตั้ง %1$s แล้ว + รอดำเนินการจากคำขอก่อนหน้า + ยังไม่ติดตั้ง + มีอัปเดต → %1$s + ลบรุ่นเก่าของ %1$s + ลบ %1$s เกมที่ตั้งไว้ให้ใช้รุ่นเหล่านี้ต้องเปลี่ยนไปใช้ Proton อื่น + เกมที่ตั้งไว้ให้ใช้รุ่นนี้ต้องเปลี่ยนไปใช้ Proton อื่น + ติดตั้งตอนนี้ + ยกเลิกรายการที่รออยู่ + แป้นพิมพ์ Android + ซ่อน ✕ + เปิด %1$s + เลือกหน้าจอ หน้าจอที่สอง: %1$s + หน้าจอที่สองไม่พร้อมใช้งานแล้ว + หน้าจอหลัก + หน้าจอที่สอง + แท็บก่อนหน้า + แท็บถัดไป + เปิด + ทั้งหมด %1$d + ไม่มี + %1$d จาก %2$d + ปิด + หน้าจอด้านล่าง + มีผลในการเปิดเกมครั้งถัดไป โปรไฟล์ของเกมจะมีสิทธิ์เหนือค่าที่ใช้ร่วมกัน + เกมอื่นตามรหัสแอป… + เสร็จสิ้น + ค่าเริ่มต้น + ปิด + เปลี่ยน… + เปลี่ยนตัวแปร + กลับไปใช้ค่าเริ่มต้น + เกม Proton + ภาพซ้อนภาพ + เข้าสู่ภาพซ้อนภาพโดยอัตโนมัติ + การควบคุมบนหน้าจอที่กางออก + เมื่อกางออก เกมจะอยู่เหนือแทร็กแพดและปุ่มด้านหลังของ Steam Deck เมื่อพับ เกมจะแสดงเต็มจอ + หยุดชั่วคราว + ภาพซ้อนภาพจะพร้อมใช้งานเมื่อเซสชันพร้อม + อนุญาตภาพซ้อนภาพสำหรับ DroidDeck ในการตั้งค่า Android + Android ไม่สามารถเปิดภาพซ้อนภาพได้ + ช็อตคัต + เพิ่มไปยังหน้าจอหลัก + เลือกเกม + กำลังสแกนเกมที่ติดตั้งไว้… + คัดลอกลิงก์เปิดเกม + คัดลอกลิงก์เปิดเกมแล้ว + ตัวเปิดแอปนี้ไม่สามารถเพิ่มช็อตคัตได้ ใช้คัดลอกลิงก์เปิดเกมแทน + ลิงก์เปิดเกมไม่ถูกต้อง + เกมนี้ไม่มีอยู่ในไลบรารี DroidDeck ของคุณแล้ว + หยุดเซสชันปัจจุบันก่อนเปิดเกม Steam + ไม่สามารถส่งเกมไปยัง Steam ได้ ลองเปิดอีกครั้ง + ลองลบใหม่ + กำลังลบ + การลบไม่สมบูรณ์ + ส่งออกไฟล์เกม + ซิงค์ไฟล์เกมทั้งหมดไปยังโฟลเดอร์ + หยุดการซิงค์ไฟล์เกม + เลือกโฟลเดอร์สำหรับไฟล์เกมของฟรอนต์เอนด์ + บันทึกไฟล์เกมไปยัง %1$s แล้ว + ไฟล์ของฟรอนต์เอนด์ + + รอให้การดำเนินการของรันไทม์เสร็จสิ้น + รอให้การติดตั้งเครื่องมือความเข้ากันได้เสร็จสิ้น + รอให้การติดตั้งแอปเดสก์ท็อปเสร็จสิ้น + รอให้การติดตั้งเสร็จสิ้นก่อนเปิดเกม + ปิดเซสชัน Steam ก่อน เพื่อไม่ให้เกมบันทึกทับการนำเข้า + กำลังนำเข้าสู่ %1$s + + นำเข้า %1$d ไฟล์จาก %2$s สู่ %3$s แล้ว + + + นำเข้า %1$d ไฟล์จาก %2$s สู่ %3$s แล้ว เซฟเก่าสำรองไว้ที่ Download/DroidDeck/Saves/backups + + เลือกไฟล์ zip เซฟสำหรับ %1$s + กำลังส่งออก %1$s + + ส่งออก %1$d ไฟล์เป็น %2$s: %3$s แล้ว + + เลือกตำแหน่งบันทึก %1$s (%2$s) + %1$s ล้มเหลว: %2$s + ยังไม่มีล็อกเซสชัน: รันเซสชันก่อน + ไม่มีล็อกเซสชันให้ล้าง + + ล้างโฟลเดอร์ล็อกเซสชัน %1$d โฟลเดอร์แล้ว + + การจับคู่ Wireless debugging ล้มเหลว + คำสั่ง Wireless debugging ล้มเหลว + Wireless debugging ไม่พร้อมใช้งาน + คัดลอกคำสั่ง ADB แล้ว + เลือกโฟลเดอร์ ROM + ไม่ใช่ GPU Adreno + Turnip รองรับ GPU Adreno บน %1$s Steam อาจแสดงจอดำ ดาวน์โหลด: %2$.0f MB + ติดตั้งอยู่ดี + ลบรันไทม์ Linux + การลบนี้จะลบรันไทม์ ไคลเอนต์ Steam ภายใน และเกมทุกเกมที่ติดตั้งอยู่ในนั้น + นำมาใช้แล้ว: %1$s + ไม่สามารถเปิด %1$s บนหน้าจอหลักได้ + ไม่สามารถเปิด %1$s บน %2$s ได้ + ไม่สามารถเปิด %1$s บนหน้าจอ %2$d ได้ + กำลังนำเข้า + กำลังสลับ + กำลังคืนค่า + กำลังยกเลิก + กำลังลบ + ไม่สามารถอ่านไฟล์ได้ + นำเข้า %1$s แล้ว + ยกเลิกการสลับที่รออยู่แล้ว + เลือกแพ็กเกจคอมโพเนนต์ (-linux .wcp) + ไม่สามารถเข้าถึง Nightlies ได้ + บันทึก %1$s แล้ว + ดาวน์โหลดล้มเหลว: %1$s + เลือกไดรเวอร์รันไทม์ Linux (-Linux zip หรือแพ็ก Android + Linux) + เลือกไดรเวอร์แสดงผล (AdrenoTools zip หรือแพ็ก Android + Linux) + เลือกไฟล์ zip ไดรเวอร์ หรือแพ็ก Android + Linux + %1$s: %2$s + กำลังลบ %1$s… + กำลังลืม %1$s… + เลือกโฟลเดอร์พื้นที่จัดเก็บเกม + เลือกไฟล์ ZIP พลักอิน Decky + ใช้งานไม่ได้: %1$s + โปรแกรม + กำลังลบรันไทม์ Linux + ไม่สามารถเปิดการตั้งค่าบนหน้าจอด้านล่างได้ จึงเปิดบนหน้าจอหลักแทน + เปิดตัวเลือกสำหรับนักพัฒนาในการตั้งค่า + + ไม่สามารถเปิดการตั้งค่า “Install unknown apps” ของ Android ได้ + ไม่สามารถตรวจสอบได้: %1$s + บิลด์นี้ไม่มีให้ดาวน์โหลดสำหรับสำเนานี้ของ DroidDeck + หยุดเซสชันที่กำลังทำงานก่อน แล้วจึงอัปเดต + กำลังดาวน์โหลด + กำลังติดตั้ง + กำลังรอให้คุณยืนยัน + การอัปเดตล้มเหลว + Android ไม่ได้เสนอให้ติดตั้งอัปเดต + + ติดตั้ง Decky ล้มเหลว: %1$s + กำลังเริ่มนำเข้าพลักอิน… + นำเข้าพลักอินล้มเหลว: %1$s + ติดตั้งพลักอินแล้ว รีสตาร์ท Steam เพื่อโหลด + + ยังไม่ตรวจสอบ - แตะรีเฟรชเพื่อค้นหาไดรเวอร์ใหม่ + Android + Linux: ตั้งไดรเวอร์แสดงผลไปด้วย + Android + Linux: ตั้งไดรเวอร์รันไทม์ไปด้วย + Turnip ที่มีในตัวรันไทม์ + Vulkan ของระบบ: ไม่มีบิลด์ที่รวมไว้สำหรับ GPU นี้ + %1$s (รวมมาด้วย) + กำหนดเอง + %1$s · %2$s + มีการเผยแพร่เพียงครึ่งหนึ่งในขณะนี้ + %1$.0f MB สำหรับทั้งคู่ + ไม่มีไดรเวอร์ให้ตั้งค่า: %1$s + กำลังตรวจสอบไดรเวอร์ล่าสุด… + เป็นรุ่นล่าสุดแล้ว (ตรวจสอบไม่ได้: %1$s) + ไดรเวอร์ที่แนะนำยังไม่ได้เผยแพร่ในขณะนี้ + กำลังดาวน์โหลด %1$s %2$s… + อัปเดตเป็น %1$s %2$s แล้ว + อัปเดตเป็น %1$s %2$s แล้ว ไดรเวอร์แสดงผลจะมีผลหลัง DroidDeck รีสตาร์ท + สลับเป็น %1$s %2$s แล้ว + สลับเป็น %1$s %2$s แล้ว ไดรเวอร์แสดงผลจะมีผลหลัง DroidDeck รีสตาร์ท + กำลังใช้ %1$s %2$s + กำลังใช้ %1$s %2$s ไดรเวอร์แสดงผลจะมีผลหลัง DroidDeck รีสตาร์ท + ไม่ใช่ไฟล์ zip ไดรเวอร์: ไม่ใช่ไดรเวอร์ AdrenoTools, Turnip -Linux หรือแพ็ก Android + Linux + นำเข้าล้มเหลว: %1$s + นำเข้าไดรเวอร์แล้ว + กำลังใช้ %1$s เป็นทั้งไดรเวอร์รันไทม์และแสดงผล + กำลังใช้ %1$s เป็นทั้งไดรเวอร์รันไทม์และแสดงผล ไดรเวอร์แสดงผลจะมีผลหลัง DroidDeck รีสตาร์ท + ลบ %1$s แล้ว (ไดรเวอร์ทั้งสอง) + ลบ %1$s แล้ว + ล่าสุด: %1$s · ตรวจสอบเมื่อ %2$s + ล่าสุด: %1$s · %2$s เข้าถึงไม่ได้ · ตรวจสอบเมื่อ %3$s + + %1$d นาทีที่แล้ว + + + %1$d ชั่วโมงที่แล้ว + + + %1$d วันที่แล้ว + + กำลังตรวจสอบ Banners-Turnip, WinNative และ DroidDeck… + ตรวจสอบล้มเหลว + ติดตั้ง %1$s แล้ว - เลือกจากรายการใดก็ได้เพื่อตั้งค่าไดรเวอร์ทั้งสอง + ติดตั้ง %1$s แล้ว - เลือกจากเมนู + + รอให้การลบรันไทม์เสร็จสิ้น + เกม + ทัชแพด + โดยตรง + สลับล้มเหลว: %1$s + บันทึกโปรไฟล์ล้มเหลว: %1$s + เดสก์ท็อปหยุดชั่วคราว + %1$s หยุดชั่วคราว + โปรแกรมหยุดชั่วคราว + กำลังเริ่มเดสก์ท็อป + กำลังเริ่ม %1$s + กำลังเริ่มโปรแกรม + ไม่มีล็อกสำหรับเซสชันนี้ + ไม่สามารถสร้างไฟล์เก็บถาวรล็อกเซสชันได้ + ไม่สามารถแชร์ล็อกเซสชันได้ + กำลังเริ่มเซสชัน… + กำลังดาวน์โหลดรันไทม์ Linux + กำลังดาวน์โหลดรันไทม์ Linux · %1$d จาก %2$d MB + กำลังตรวจสอบรันไทม์ Linux + กำลังแตกไฟล์รันไทม์ Linux + กำลังดาวน์โหลดเดสก์ท็อป + กำลังดาวน์โหลดเดสก์ท็อป · %1$d จาก %2$d MB + กำลังตรวจสอบเดสก์ท็อป + กำลังแตกไฟล์เดสก์ท็อป + กำลังดาวน์โหลด Proton Experimental (ARM64) + กำลังดาวน์โหลด Proton Experimental (ARM64) · %1$d จาก %2$d MB + กำลังตรวจสอบ Proton Experimental (ARM64) + กำลังแตกไฟล์ Proton Experimental (ARM64) + ไม่สามารถเข้าถึงแคตตาล็อกรันไทม์ได้ ตรวจสอบการเชื่อมต่อแล้วกด Play อีกครั้ง + รันไทม์ Linux ติดตั้งไม่สำเร็จ ตรวจสอบรันไทม์ในการตั้งค่าแล้วกด Play อีกครั้ง + เดสก์ท็อปติดตั้งไม่สำเร็จ (%1$s) ตรวจสอบการเชื่อมต่อแล้วกดเดสก์ท็อปอีกครั้ง + %1$s หยุดทำงานกะทันหัน แชร์ล็อกพร้อมรายงานข้อบกพร่อง หรือลองใหม่ + โปรแกรมหยุดทำงานกะทันหัน แชร์ล็อกพร้อมรายงานข้อบกพร่อง หรือลองใหม่ + เดสก์ท็อปหยุดทำงานกะทันหัน แชร์ล็อกพร้อมรายงานข้อบกพร่อง หรือลองใหม่ + สถานะออก %1$d · %2$s + ไม่มีล็อก + + ล็อกแสดงข้อผิดพลาด “Function not implemented” %1$d รายการ: การเร่งความเร็ว seccomp ของ proot ทำให้ตัวช่วยบนอุปกรณ์นี้ล้มเหลว ลอง %2$s → “%3$s” (หรือ “%4$s”) แล้วเริ่มใหม่ + + + พลักอิน + ติดตั้งไฟล์ ZIP พลักอินสำหรับเซสชัน Steam ครั้งถัดไป + กำลังดาวน์โหลด… + ติดตั้งจาก ZIP + เกม + เสียงและวิดีโอ + กราฟิก + อินเทอร์เน็ต + สำนักงาน + นักพัฒนา + การศึกษา + วิทยาศาสตร์ + ระบบ + ยูทิลิตี + ได้รับความนิยม + กำลังเป็นที่นิยม + ใหม่ + อัปเดตแล้ว + กำลังโหลด… + การตั้งค่า Flatpak + %1$s: %2$s + ไม่สามารถตรวจสอบการอัปเดตจาก Flathub ได้ + ติดตั้งแล้ว + โฟลเดอร์ + Graphite + พื้นผิวสีเทาแกรไฟต์ พร้อมสีฟ้าของโลโก้สำหรับทุกสิ่งที่ถูกเลือก โฟกัส หรือกด + กระดาษบนพื้นดำ + สีขาวเป็นหลัก สีฟ้าของไอคอนเป็นสัญญาณสำหรับเส้น วงแหวน และจุด + ฟ้าไอคอน + พื้นดำ ตัวอักษรขาว สีฟ้าสำหรับทุกสิ่งที่ถูกเลือก โฟกัส หรือกด + กรมท่าไฟฟ้า + สีฟ้าสว่างขึ้น บนพื้นผิวที่เอียงไปทางกรมท่ามากกว่าเทา + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + ติดตั้งรันไทม์ก่อน + ไลบรารี Steam ไม่พร้อมใช้งาน + ไลบรารี SD ก่อนหน้าไม่พร้อมใช้งาน + พื้นที่จัดเก็บเกมไม่พร้อมใช้งาน + โฟลเดอร์เกมที่เพิ่มไม่พร้อมใช้งาน + ไฟล์ manifest ของ Steam ไม่พร้อมใช้งาน + หลายระบบ + ภายใน + ระบบ Linux + การ์ด SD + พื้นที่จัดเก็บ + เลือก + คุณสมบัติ + เพิ่มในรายการโปรด + ลบจากรายการโปรด + เปลี่ยนชื่อ + คัดลอก + ตัด + การดำเนินการ + รายการโปรด + ปักหมุดโฟลเดอร์นี้ + ยังไม่มีรายการโปรด - ปักหมุดโฟลเดอร์ด้วยเมนู ⋮ เพื่อย้อนกลับมาที่นี่ได้เร็ว + ลบออกจากรายการโปรด + 0 ไบต์ + ไบต์ + ตำแหน่ง + ชนิด + ขนาด + แก้ไขล่าสุด + โฟลเดอร์ + ไฟล์ + ไฟล์ %1$s + อ่านอย่างเดียว + ป้องกันไม่ให้เกม (และ Wine) เขียนทับหรือลบไฟล์นี้ + ไม่สามารถเปลี่ยนสถานะอ่านอย่างเดียวได้ + ซ่อน + พื้นที่จัดเก็บนี้ไม่สามารถเก็บสถานะซ่อนได้ + ตั้งค่าไฟล์เป็นซ่อนสำหรับ Windows (แอตทริบิวต์ซ่อนของ Wine) + ไม่สามารถเปลี่ยนสถานะซ่อนบนพื้นที่จัดเก็บนี้ได้ + เสร็จสิ้น + กำลังลบ... + ลบล้มเหลว + กำลังย้าย... + กำลังคัดลอก... + กำลังย้าย - %1$s + กำลังคัดลอก - %1$s + กำลังย้าย %1$d/%2$d - %3$s + กำลังคัดลอก %1$d/%2$d - %3$s + เสร็จ %1$d รายการ ล้มเหลว %2$d รายการ + เสร็จ %1$d รายการ ข้าม %2$d รายการ + + ย้าย %1$d รายการแล้ว + + + คัดลอก %1$d รายการแล้ว + + “%1$s” มีอยู่แล้ว + กำลังเปลี่ยนชื่อ... + เปลี่ยนชื่อล้มเหลว + กำลังสร้างโฟลเดอร์... + ไม่สามารถสร้างโฟลเดอร์ได้ + โฟลเดอร์ใหม่ + ชื่อโฟลเดอร์ + สร้าง + ชื่อใหม่ + ลบหรือไม่? + ลบ “%1$s” อย่างถาวรหรือไม่? + + ลบ %1$d รายการหรือไม่? + + ไม่สามารถยกเลิกการดำเนินการนี้ได้ + + …และอีก %1$d รายการ + + กำลังลบ %1$d/%2$d - %3$s + + ลบไม่ได้ %1$d รายการ + + ผสานจะเพิ่มและแทนที่ไฟล์ภายในโฟลเดอร์ที่มีอยู่ + เขียนทับจะแทนที่ไฟล์ที่มีอยู่ + ใช้กับความขัดแย้งทั้งหมด + ผสาน + เขียนทับ + เก็บไว้ทั้งสอง + ข้าม + เลือกโฟลเดอร์นี้ + กลับ + %1$s ติดตั้งอยู่แต่อ่านไม่ได้ในขณะนี้ + มุมมองรายการ + มุมมองตาราง + ค้นหา + จัดเรียง + ชื่อ + วันที่แก้ไข + แถวแบบสบาย + แถวแบบกระชับ + ซ่อนไฟล์ที่ซ่อนอยู่ + แสดงไฟล์ที่ซ่อนอยู่ + ซ่อนรายการโปรด + แสดงรายการโปรด + กรองโฟลเดอร์นี้ + เหลือ %1$s + เลือกแล้ว %1$d รายการ + ไม่มี + ทั้งหมด + วาง + + %1$d รายการ + + วาง %1$s ที่นี่ + วาง %1$s (ย้าย) ที่นี่ + ไฟล์ + พื้นที่จัดเก็บ + ด่วน + รายการโปรด + ดาวน์โหลด + ดาวน์โหลด + ROM + ล็อกเซสชัน + รูปภาพ + เพิ่ม “%1$s” ในรายการโปรดแล้ว + ลบ “%1$s” จากรายการโปรดแล้ว + ไดเรกทอรีว่างเปล่า + ขึ้นหนึ่งระดับ + เลือกโฟลเดอร์ + เลือกไฟล์ + ฟ้า Steam + ฟ้าสดใส + ขาว + ม่วง + เขียว + อำพัน + แดง + ชมพู + ปุ่ม A + ปุ่ม B + ปุ่ม X + ปุ่ม Y + ปุ่มกระดกซ้าย + ปุ่มกระดกขวา + ทริกเกอร์ซ้าย + ทริกเกอร์ขวา + ปุ่ม View + ปุ่ม Menu + View + Menu + D-pad ขึ้น + D-pad ลง + D-pad ซ้าย + D-pad ขวา + ซ่อน + ลากเพื่อย้าย + จัดให้พอดี + บันทึก + ไม่มี + แป้นพิมพ์ + แทร็กแพด + เทอร์มินัล + ปุ่มด้านหลังและแทร็กแพดของ Deck + + กำลังลบรันไทม์ Linux + + กำลังลบรันไทม์ Linux · %1$d รายการ + + ลบรันไทม์ Linux แล้ว + ไม่สามารถลบรันไทม์ Linux ให้เสร็จสมบูรณ์ได้ ลองลบใหม่ในการตั้งค่า + กำลังลบเศษไฟล์รันไทม์ที่เหลือ + กำลังดาวน์โหลด + กำลังตรวจสอบ + กำลังแตกไฟล์ + แสดงขณะติดตั้งหรือลบรันไทม์ Linux + กำลังติดตั้งรันไทม์ Linux + กำลังลบรันไทม์ Linux + + ยังไม่ติดตั้งรันไทม์ Linux + แคตตาล็อกไม่มีค่าตรวจสอบสำหรับ %1$s + ค่าตรวจสอบไม่ตรงกัน - ไม่มีการเปลี่ยนแปลงใด ๆ + กำลังติดตั้ง %1$s + ไม่สามารถวาง AppImage ได้ + การแตกไฟล์ล้มเหลว + การติดตั้งล้มเหลว + + การตั้งค่า Flatpak ล้มเหลว (ออกด้วยสถานะ %1$d) + การตั้งค่า Flatpak ไม่เสร็จสมบูรณ์ + มีงานสโตร์อื่นกำลังทำงานอยู่ + ตั้งค่า Flatpak ก่อน + กำลังเริ่ม + Flatpak ออกด้วยสถานะ %1$d + กำลังติดตั้ง %1$s + กำลังอัปเดต %1$s + กำลังลบ %1$s + กำลังติดตั้ง %1$s (%2$d/%3$d) + กำลังอัปเดต %1$s (%2$d/%3$d) + กำลังลบ %1$s (%2$d/%3$d) + คำแปล + ไดรเวอร์กราฟิก + ตัวแปลงรหัสสื่อ + รันไทม์ %1$s + รันไทม์ %1$s %2$s + + ไฟล์มีขนาดเล็กเกินกว่าจะเป็น AppImage + ไม่สามารถอ่านไฟล์ได้: %1$s + นี่ไม่ใช่ AppImage (ไม่ใช่โปรแกรม Linux) + AppImage นี้ไม่ได้สร้างมาสำหรับ ARM64 (aarch64) หรือพีซี x86 + นี่เป็นโปรแกรม Linux แต่ไม่ใช่ AppImage + นำเข้าได้เฉพาะ AppImage ประเภท 2 เท่านั้น (ไฟล์นี้เป็นประเภท %1$d) + พื้นที่ว่างไม่พอ: การนำเข้าต้องใช้ประมาณ %1$s + พื้นที่ว่างไม่พอ: การอัปเดตต้องใช้ประมาณ %1$s + นำเข้าล้มเหลว + AppImage นั้นหายไปแล้ว + อัปเดตล้มเหลว + กำลังแตกไฟล์ %1$s + ไม่สามารถแตกไฟล์ AppImage ได้ + ไม่สามารถแตกไฟล์ AppImage ได้: %1$s + + หยุดเซสชันที่กำลังทำงานก่อนติดตั้ง Decky Loader + ดาวน์โหลด Decky Loader ล้มเหลว + ขนาด Decky Loader ไม่ตรงกับข้อมูลเมตาของรุ่นที่เผยแพร่ + ไม่สามารถดาวน์โหลดค่าตรวจสอบของรุ่นที่เผยแพร่ได้ + ไม่มีค่าตรวจสอบของรุ่นที่เผยแพร่ + ค่าตรวจสอบของรุ่นที่เผยแพร่ไม่ถูกต้อง + ค่าตรวจสอบของ Decky Loader ไม่ตรงกัน + ไฟล์ของรุ่นที่เผยแพร่ไม่ใช่โปรแกรม Linux 64 บิตที่ใช้ได้ + มีเซสชันเริ่มขึ้นระหว่างการดาวน์โหลด หยุดเซสชันก่อนติดตั้ง Decky Loader + ไม่สามารถตั้งค่า PluginLoader ให้รันได้ + ไม่สามารถเปิดการดีบักระยะไกล CEF ของ Steam ได้ + ไม่สามารถเตรียม PluginLoader ได้ + ไม่สามารถเก็บ PluginLoader เดิมไว้ได้ + ไม่สามารถเปิดใช้ PluginLoader ใหม่ได้ + หยุดเซสชันที่กำลังทำงานก่อนติดตั้งพลักอิน + ติดตั้ง Decky Loader ก่อนนำเข้าพลักอิน + ไม่สามารถอ่านไฟล์ ZIP ที่เลือกได้ + ไม่สามารถสร้างไดเรกทอรีพลักอินของ Decky ได้ + กำลังแตกไฟล์ ZIP พลักอิน + ไฟล์ ZIP พลักอินต้องมีโฟลเดอร์พลักอินระดับบนสุดเพียงหนึ่งโฟลเดอร์ + ไฟล์ ZIP พลักอินไม่มี plugin.json, package.json หรือ dist/index.js + ไม่มีชื่อพลักอิน + มีเซสชันเริ่มขึ้นระหว่างการติดตั้ง หยุดเซสชันก่อนติดตั้งพลักอิน + ติดตั้งพลักอินแล้ว · รีสตาร์ท Steam เพื่อโหลด + การติดตั้งไฟล์ ZIP พลักอินล้มเหลว + ไฟล์ ZIP พลักอินมีรายการมากเกินไป + ไฟล์ ZIP พลักอินมีพาธที่ไม่ถูกต้อง + ไฟล์ ZIP พลักอินมีพาธซ้ำกัน + ไฟล์ ZIP พลักอินมีพาธที่ไม่ปลอดภัย + ไม่สามารถสร้างโฟลเดอร์พลักอินได้ + ไฟล์ ZIP พลักอินขยายเกินขีดจำกัด 1 GiB + ไม่สามารถตั้งค่าไฟล์ไบนารีพลักอินที่มาด้วยให้รันได้ + รายการ remote_binary ไม่ถูกต้อง + ชื่อไบนารีระยะไกลไม่ถูกต้อง + URL ไบนารีระยะไกลต้องใช้ HTTPS + ไบนารีระยะไกล %1$s มีแฮช SHA-256 ที่ไม่ถูกต้อง + ไม่สามารถสร้างไดเรกทอรี bin ของพลักอินได้ + กำลังดาวน์โหลดไบนารีพลักอิน · %1$s + ไม่สามารถดาวน์โหลดไบนารีพลักอิน %1$s ได้ + ค่าตรวจสอบของไบนารีพลักอินไม่ตรงกัน · %1$s + ไม่สามารถตั้งค่าไบนารีพลักอิน %1$s ให้รันได้ + ไม่สามารถแทนที่ไบนารีพลักอิน %1$s ได้ + ไม่สามารถติดตั้งไบนารีพลักอิน %1$s ได้ + โฟลเดอร์พลักอินนี้เป็นของพลักอินอื่น + ไม่สามารถเก็บพลักอินเดิมไว้ได้ + ไม่สามารถเปิดใช้พลักอินที่นำเข้าได้ + ไม่สามารถอัปเดตการตั้งค่าพลักอินของ Decky ได้ + ไม่สามารถคืนค่าพลักอินเดิมได้ + + ไม่สามารถบันทึกข้อมูลประจำตัวของ Wireless debugging ได้ + คำสั่ง ADB ล้มเหลว (%1$d) + Android ไม่ได้ยืนยันว่าขีดจำกัดโพรเซสลูกถูกเปลี่ยน: %1$s + ไม่มีการจับคู่ Wireless debugging ที่บันทึกไว้ + กรอกที่อยู่ IP และพอร์ตปัจจุบันของ Wireless debugging + + รอป๊อปอัปการจับคู่หมดเวลา ลองใหม่จาก DroidDeck + ไม่สามารถค้นหาป๊อปอัปการจับคู่ได้ (%1$d) ตรวจสอบว่าเปิด Wi-Fi อยู่ + ไม่สามารถค้นหาป๊อปอัปการจับคู่ได้: %1$s + กรอกรหัสให้ครบทั้ง 6 หลัก + กำลังจับคู่… + รหัสนั้นใช้ไม่ได้ เปิดป๊อปอัปการจับคู่อีกครั้งและกรอกรหัสใหม่ + จับคู่แล้ว กำลังเชื่อมต่อ… + จับคู่แล้ว แต่ไม่พบพอร์ตของ Wireless debugging เปิด Wireless debugging ไว้แล้วลองใหม่ + กำลังปรับใช้การตั้งค่า… + คำสั่ง Wireless debugging ล้มเหลว + การตั้งค่า Wireless debugging + ขอรหัสจับคู่ Wireless debugging ขณะเปิดการตั้งค่าอยู่ + จับคู่ Wireless debugging + เปิด Wireless debugging แล้วแตะ “Pair device with pairing code” + เปิด Wireless debugging แล้วแตะ “Pair device with pairing code” อยู่ในการตั้งค่าต่อไป: รหัสจะอยู่ในการแจ้งเตือนนี้ + รหัสจับคู่ 6 หลัก + แตะกรอกรหัสแล้วพิมพ์ตัวเลข 6 หลักที่แสดงในการตั้งค่า + กรอกรหัสจับคู่ Wi-Fi + ลองกรอกรหัสอีกครั้ง + กรอกรหัส + กำลังตั้งค่า Steam + Steam พร้อมแล้ว + แตะเพื่อกลับไปที่ DroidDeck คุณสามารถปิด Wireless debugging ได้แล้วตอนนี้ + การตั้งค่า Wireless debugging หยุดแล้ว + + การดาวน์โหลดตอบกลับด้วย HTTP %1$d + ขนาดการดาวน์โหลดไม่ตรงกับ APK ที่เผยแพร่และถูกยกเลิก - ลองใหม่ + การดาวน์โหลดไม่ตรงกับค่าตรวจสอบและถูกยกเลิก - ลองใหม่ + อัปเดตนี้สำหรับแพ็กเกจ Android %1$s ไม่ใช่ %2$s + ข้อมูลเมตาของอัปเดตไม่ได้ระบุคีย์ลงนามรุ่นวางจำหน่ายของ DroidDeck + Android ไม่สามารถอ่าน APK ที่ดาวน์โหลดได้ + APK ที่ดาวน์โหลดสำหรับ %1$s ไม่ใช่ %2$s + APK ที่ดาวน์โหลดระบุ versionCode %1$d ไม่ใช่ %2$d ที่เผยแพร่ + Android จะไม่ติดตั้ง versionCode %1$d ทับ %2$d ที่ติดตั้งอยู่ + APK ที่ดาวน์โหลดไม่มีข้อมูลการลงนาม + APK ที่ดาวน์โหลดไม่ได้ลงนามด้วยคีย์รุ่นวางจำหน่ายของ DroidDeck + แคตตาล็อกอัปเดตตอบกลับด้วย HTTP %1$d + + กำลังเริ่มเซสชัน… + ผ่านไป %1$d:%2$02d · ยังทำงานอยู่ + กำลังดาวน์โหลดอัปเดตไคลเอนต์ Steam · %1$d%% + กำลังดาวน์โหลดไคลเอนต์ Steam · %1$s + + มีการติดตั้งเครื่องมือความเข้ากันได้อื่นกำลังทำงานอยู่แล้ว + หยุดเซสชันที่กำลังทำงานก่อนติดตั้งเครื่องมือความเข้ากันได้ + ไม่พบรุ่นที่เผยแพร่แบบ ARM64 สำหรับ %1$s + ต้องมีพื้นที่ว่างอย่างน้อย 4 GB รวมกับขนาดดาวน์โหลดเพื่อติดตั้ง %1$s + กำลังดาวน์โหลด %1$s %2$s + กำลังลองดาวน์โหลดใหม่ · ครั้งที่ %1$d จาก %2$d + การดาวน์โหลดถูกขัดจังหวะ + ดาวน์โหลดล้มเหลว ไฟล์ที่ดาวน์โหลดไม่ครบถูกเก็บไว้เพื่อลองใหม่ต่อได้ + การดาวน์โหลดมีขนาดเกินรุ่นที่เผยแพร่และถูกลบ + การดาวน์โหลดไม่สมบูรณ์ ไฟล์ที่ดาวน์โหลดไม่ครบถูกเก็บไว้เพื่อลองใหม่ต่อได้ + กำลังตรวจสอบการดาวน์โหลด + ไม่สามารถอ่านค่าตรวจสอบของรุ่นที่เผยแพร่ได้ ลองใหม่ + %1$s %2$s ไม่ได้เผยแพร่ค่าตรวจสอบ จึงไม่มีการติดตั้งใด ๆ + ค่าตรวจสอบไม่ตรงกัน การดาวน์โหลดถูกลบแล้ว + มีเซสชันเริ่มขึ้นระหว่างการดาวน์โหลด หยุดเซสชันก่อนติดตั้งเครื่องมือความเข้ากันได้ + กำลังติดตั้ง %1$s + การติดตั้ง %1$s ล้มเหลว (ออกด้วยสถานะ %2$d) + ไม่สามารถติดตั้ง %1$s ได้ + กำลังดึงแพ็ก droiddeck-esync + + พร้อมตัวช่วย Linux + DLL เท่านั้น + ต้นฉบับ (Proton ถูกอัปเดตตั้งแต่การสลับครั้งล่าสุด) + ถูกเปลี่ยนแปลงนอก Components + ไม่มีอยู่ + ต้นฉบับ %1$s + ต้นฉบับจาก %1$s + %1$s %2$s จะถูกใส่เข้าไปใน %3$s เมื่อเกมที่กำลังทำงานบนนั้นปิดลง + %1$s %2$s อยู่ใน %3$s แล้วตอนนี้ จะมีผลในครั้งถัดไปที่เกมเริ่มทำงาน + ไม่มีการบันทึกชุดต้นฉบับนั้นไว้ + %1$s ของ %2$s จะกลับไปเป็นต้นฉบับเมื่อเกมที่กำลังทำงานบนนั้นปิดลง + คืนค่า %1$s ของ %2$s จากต้นฉบับ %3$s แล้ว + %1$s กำลังใช้งานอยู่ใน %2$s สลับ Proton นั้นไปเป็นอย่างอื่นก่อน + ไม่มีการบันทึก %1$s ไว้ + ลบ %1$s แล้ว + ต้นฉบับของบิลด์ที่ติดตั้งไว้ถูกเก็บไว้: นี่คือทางกลับ + ไม่ได้บันทึกไว้ + ลบต้นฉบับ %1$s ของ %2$s แล้ว + การดาวน์โหลดมาจากรุ่นที่เผยแพร่ใน Nightlies เท่านั้น + รายการแพ็กเกจนี้เก่ากว่าระบบค่าตรวจสอบ - รีเฟรชแล้วลองใหม่ + ค่าตรวจสอบไม่ตรงกัน + + Linear + Nearest + Snapdragon GSR (คุณภาพ) + เพิ่มความคมชัดเท่านั้น + อัตโนมัติ (ตามอัตราขยาย) + + ไม่ใช้เอฟเฟกต์ใด ๆ + ความชัดของเกม + เพิ่มความคมชัดพร้อมคอนทราสต์เล็กน้อย - ใช้งานทั่วไป + สดใส + เน้นสีจัดจ้าน เปิดลดลายแถบสีเพื่อไม่ให้ท้องฟ้าเป็นลาย + ภาพยนตร์ + คอนทราสต์เข้มขึ้น สีเย็นลง ขอบภาพเรียบขึ้น + แข่งขัน + ยกเงาให้สว่างขึ้นและเพิ่มความคมชัดมาก - มองเห็นคนก่อนใคร + เพิ่มความคมชัดแบบปรับอัตโนมัติ + เพิ่มความคมชัดเท่านั้น ไม่แตะสี + ฟิล์ม + โทนภาพแบบฟิล์ม นวลลงเล็กน้อย + อาร์เคด + จัดจ้านและมีพลัง + CRT เรโทร + หลอด CRT และสัญญาณอนาล็อกมาด้วยกัน + ขยายภาพแบบคม + สำหรับการรันที่ต่ำกว่าความละเอียดจอ + พิกเซลคมชัด + พิกเซลคมชัดสำหรับเกม 2D และเกมรุ่นเก่า + ขอบแอนิเมะ + เส้นขอบแบบการ์ตูนพร้อมเพิ่มความคมชัด + + ไม่ใช่ GPU Adreno + รองรับ + ทดลอง: ไดรเวอร์เป็นบิลด์ทดสอบ + ต่ำกว่าฮาร์ดแวร์ที่ทดสอบแล้ว (Adreno 725 และรุ่นใหม่กว่า): อาจรันไม่ได้ + ไม่รองรับ: DroidDeck ต้องใช้ GPU Adreno (Snapdragon) + + ติดขีดจำกัดอัตราของ GitHub - ลองใหม่ในหลัง + GitHub ตอบกลับด้วย HTTP %1$d + การดาวน์โหลดตอบกลับด้วย HTTP %1$d + รายการไดรเวอร์นี้เก่ากว่าระบบค่าตรวจสอบ - รีเฟรชแล้วลองใหม่ + ค่าตรวจสอบไม่ตรงกัน - การดาวน์โหลดถูกยกเลิก + ไม่ใช่ไดรเวอร์ AdrenoTools: %1$s + ไม่มี meta.json + meta.json อ่านไม่ได้ (%1$s) + นี่เป็นไดรเวอร์รันไทม์ Linux (นำเข้าภายใต้ “ไดรเวอร์รันไทม์ Linux” แทน) + นี่เป็นไดรเวอร์เกม Wayland ซึ่งแอปนี้ไม่ได้ใช้ + meta.json ไม่ได้ระบุ libraryName จึงไม่มีอะไรส่งให้ AdrenoTools ได้ + meta.json ระบุ %1$s ซึ่งไม่มีอยู่ในไฟล์ zip + %1$s ไม่ใช่ไลบรารีที่ใช้ร่วมแบบ ELF AArch64 64 บิต + %1$s เชื่อมโยงกับ glibc - เป็น Turnip แบบ “-Linux” ซึ่งควรอยู่ภายใต้ “ไดรเวอร์รันไทม์ Linux” + ไม่มี libvulkan_freedreno*.so ในไฟล์ zip นี้ ไฟล์ zip แบบ Android (AdrenoTools) หรือ -Wayland Turnip ไม่ใช่ไดรเวอร์รันไทม์ Linux + %1$s ไม่ใช่ไลบรารีที่ใช้ร่วมแบบ ELF AArch64 64 บิต + %1$s ไม่ใช่ไดรเวอร์ glibc - ไฟล์นี้เชื่อมโยงกับ libc ของ Android รันไทม์ Linux ต้องใช้ไฟล์ zip แบบ “-Linux” ส่วน Turnip แบบปกติหรือ “-Wayland” ไคลเอนต์ Steam ไม่สามารถโหลดได้เลย + ไม่ใช่แพ็กไดรเวอร์: ไม่มี manifest.json ที่ระบุไดรเวอร์ android และ linux + ไม่ใช่แพ็กไดรเวอร์: %1$s ชี้ไปนอกแพ็ก + ไม่ใช่แพ็กไดรเวอร์: แตกไฟล์แล้วมีขนาดเกิน %1$d MB + + การ์ด SD + โฟลเดอร์ + + แชร์ล็อกเซสชัน + + ต้องใช้ Android 10 หรือใหม่กว่า (เลเยอร์แสดงผลที่มีพื้นที่สี) + หน้าจอนี้ไม่รองรับ HDR + หน้าจอนี้รองรับ %1$s ไม่ใช่ HDR10 + + GPU นี้ + ขีดจำกัดโพรเซสลูกเปิดอยู่ + ยังไม่ตั้งค่าขีดจำกัดโพรเซสลูก + ไม่สามารถตรวจสอบขีดจำกัดโพรเซสลูกได้ + ขีดจำกัดโพรเซสลูกปิดอยู่ + ไม่จำเป็นต้องใช้ขีดจำกัดโพรเซสลูก + %1$s หากทำไม่ได้ ให้เชื่อมต่ออุปกรณ์นี้กับคอมพิวเตอร์ที่มี ADB แล้วรันคำสั่งด้านล่าง กลับมาที่นี่แล้วตรวจสอบอีกครั้ง + เซสชัน Steam สามารถเริ่มได้ + Android รุ่นนี้ไม่ใช้ขีดจำกัดนี้ + เปิด “Disable child process restrictions” ในตัวเลือกสำหรับนักพัฒนา และเปิดตัวเลือกสำหรับนักพัฒนาไว้ต่อไปหลังจากนั้น + Android รุ่นนี้ไม่มีสวิตช์สำหรับสิ่งนี้ ใช้ Wireless debugging เพื่อเปลี่ยนค่าจากอุปกรณ์นี้ + บิลด์นี้ไม่มีให้ดาวน์โหลดสำหรับสำเนานี้ของ DroidDeck + Android จะไม่ติดตั้งทับบิลด์นี้ เพราะ versionCode %1$d ต่ำกว่า %2$d ที่ติดตั้งอยู่ + Android จะไม่ติดตั้งรุ่นเก่าทับรุ่นใหม่ ใช้บิลด์นี้ต่อไปจนกว่าช่องทางนี้จะตามทัน + สำเนานี้ของ DroidDeck ลงนามต่างกัน (อาจเป็นบิลด์ที่สร้างเอง) จึง Android จะไม่อัปเดตทับให้ + มีพื้นที่ว่างไม่พอสำหรับการอัปเดต + การอัปเดตถูกยกเลิก + อัปเดตไม่สามารถติดตั้งได้: %1$s + อัปเดตไม่สามารถติดตั้งได้ + zip ของ %1$s + zip + %1$s ยังไม่ถูกเปิด + ไม่พบไฟล์เซฟสำหรับ %1$s + %1$s ไม่มีโฟลเดอร์ drive_c จึงไม่ใช่ไฟล์ zip เซฟของ GameHub หรือ Winlator + ไม่สามารถสร้างโฟลเดอร์ใน %1$s ได้ + ไม่สามารถเขียนใน %1$s ได้ + ไม่สามารถเขียนใน %1$s ได้ (%2$s) + โฟลเดอร์ไม่พร้อมใช้งาน + มีไฟล์อื่นใช้ชื่อ %1$s อยู่แล้ว + โฟลเดอร์ภาพปกไม่พร้อมใช้งาน + โฟลเดอร์มีรายการซิงค์ที่ไม่เกี่ยวข้อง + ไม่สามารถลบ %1$s ได้ + ไฟล์ออกไปนอกโฟลเดอร์ส่งออก + FEX (บิลด์ที่ไม่มีชื่อรุ่น) + ไม่มี profile.json ใน %1$s + %1$s ไม่ใช่แพ็กเกจคอมโพเนนต์ Linux (ประเภท %2$s) + %1$s · เหลือ %2$s + อัตโนมัติ + แคตตาล็อกอัปเดตไม่ผ่านการตรวจสอบ (%1$s) + การตั้งค่าโพรเซสลูกของ DroidDeck + ลงชื่อเข้าใช้แล้ว · โหมดออฟไลน์ + ลงชื่อเข้าใช้แล้ว + มีในตัว + ดาวน์โหลดแล้ว + นำเข้าแล้ว + ANDROID + LINUX + %1$s %2$s\n\n%3$s + การแสดงผลของเซสชัน + ความละเอียด + การแสดงเกม 16:9 ให้พอดี + วิธีที่เกม 16:9 แสดงพอดีกับหน้าจอนี้ มีผลในเซสชันถัดไป + เลือกพอดีหน้าจอ หรือขนาดกำหนดเองที่แคบกว่า 16:9 + คงสัดส่วนไว้ + ยืดให้พอดีหน้าจอ + การปรับขนาดภาพ + ตัวกรองการปรับขนาด + ใช้ร่วมกันระหว่าง Steam และเดสก์ท็อป + มีผลทันที ใช้ร่วมกันระหว่าง Steam และเดสก์ท็อป + FSR, GSR และ NIS จะมีผลเมื่อขยายภาพของเซสชัน เพิ่มความคมชัดเท่านั้นทำงานได้โดยไม่ต้องขยายภาพด้วย + ความคมชัดของตัวกรอง + อัตราเฟรม + %1$s ตั้งการปรับขนาดเป็น %2$s + พอดีหน้าจอ (%1$d×%2$d) + ตามหน้าจอ (ปรับขนาดเมื่อพับหรือกาง) + Steam + แสดงรายละเอียดอุปกรณ์ + ซ่อนรายละเอียดอุปกรณ์ + กำลังอ่านข้อมูลอุปกรณ์… + คัดลอก + คัดลอกรายละเอียดเหล่านี้ลงในรายงานข้อบกพร่อง + คัดลอกแล้ว + บังคับ SSBS + คงการตั้งค่าความเร็ว CPU ที่ Wine มักปิดอยู่เสมอ อาจทำให้เกม Windows เร็วขึ้นบน Snapdragon 8 Elite แต่เห็นความเปลี่ยนแปลงน้อยบนชิปรุ่นเก่า มีผลในการเปิดเกมครั้งถัดไป + + สโตร์ + Steam + กำหนดเอง + สโตร์ + เปิดสโตร์บน + ระดับความเร็วการดาวน์โหลด + เอนจินสโตร์ + ● เอนจินพร้อมใช้งาน + ยังไม่สร้าง + สมดุล + เร็ว + สูงสุด + สโตร์ + การตั้งค่าสโตร์ + การตั้งค่า + (ยังไม่ลงชื่อเข้าใช้) + สโตร์ + ติดตั้งแล้ว + ไลบรารี + ทั้งหมด + การดาวน์โหลด + %1$s (%2$d) + ลงชื่อเข้าใช้ + %1$s · ลงชื่อออก + ลงชื่อออกจาก %1$s? + เกมที่ติดตั้งไว้จะยังคงอยู่ ไลบรารีจะถูกดึงมาใหม่เมื่อคุณลงชื่อเข้าใช้อีกครั้ง + ลงชื่อเข้าใช้ต่อไป + ลงชื่อออก + ค้นหาแคตตาล็อกของ %1$s… + ค้นหาเกมที่ติดตั้งไว้… + ค้นหาไลบรารีของคุณ… + ค้นหา + ไม่มีสิ่งที่ติดตั้งไว้ + กำลังโหลด + ไม่มีเกม + กำลังโหลดสโตร์… + ไม่มีแคตตาล็อก + มีอะไรใหม่ + ดีล + เกมฟรี + กำลังเป็นที่นิยม + ไลบรารีของคุณ + เล่น + ติดตั้ง + ดำเนินการติดตั้งต่อ + ถูกขัดจังหวะ + ติดตั้ง · %1$s + รับไปเลยฟรี + ดูบน %1$s + ซื้อ · %1$s + ติดตั้งแล้ว + อยู่ในไลบรารี + ฟรี + ดำเนินการต่อ + รีเฟรช + แสดงเนื้อหาสำหรับผู้ใหญ่ + Epic + EOS + ออฟไลน์ + ปิด + ลงชื่อเข้าใช้ Epic (EOS) + เปิดแบบออฟไลน์ + แก้ไขการลงชื่อเข้าใช้ Epic + เซฟบนคลาวด์ + อัปโหลดขึ้นคลาวด์ + ดาวน์โหลดจากคลาวด์ + ไม่มีเซฟบนคลาวด์ + ข้อขัดแย้งบนคลาวด์ + ข้อขัดแย้งบนคลาวด์ · %1$d ไฟล์ + … + เก็บของคลาวด์ + เก็บของเครื่อง + กำลังตรวจสอบคลาวด์… + ซิงค์ล่าสุด %1$s + ยังไม่ได้ซิงค์ + %1$d ไฟล์ + ติดตั้งแล้ว + อยู่ในไลบรารีของคุณ + สโตร์ + ความสำเร็จ + ปลดล็อกแล้ว %1$d จาก %2$d + %1$d / %2$d + ปลดล็อกแล้ว · %1$d + ล็อกอยู่ · %1$d + ซ่อนอยู่ · %1$d + ความสำเร็จที่ซ่อนอยู่ + รายละเอียดจะแสดงเมื่อปลดล็อกแล้ว + ปลดล็อกความสำเร็จครบทุกรายการ + กำลังโหลด… + ลงชื่อเข้าใช้ %1$s เพื่อดูความสำเร็จของคุณ + ไม่สามารถเชื่อมต่อ %1$s ได้ ลองตรวจสอบอีกครั้งเมื่อออนไลน์ + %1$s ไม่มีรายการความสำเร็จสำหรับเกมนี้ + แสดงทั้งหมด %1$d รายการ + ความสำเร็จ · %1$s + ✓ อยู่ใน Steam + ถอนการติดตั้ง + กำลังถอนการติดตั้ง… + ถอนการติดตั้ง %1$s? + คืนพื้นที่ %1$s และลบออกจาก Steam + ลบไฟล์และช็อตคัตใน Steam ของเกมนี้ + Steam จะรับเกมนี้เข้ามาเมื่อเริ่มครั้งถัดไป เปิดจากที่นั่น หรือเริ่ม Steam ตอนนี้ + Proton + การเลือกของ Steam ต่อเกม + เสียง + DirectAudio + Components + FEX, DXVK, VKD3D + เอนจิน + %1$s · เนทีฟ + %1$s · มีในตัว + วิธีที่ทำงาน + ดึงมาจาก + %1$s ด้วยบัญชีของคุณ + ติดตั้งไปที่ + พื้นที่จัดเก็บเกม (Games/%1$s/%2$s) + เปิดโดย + Steam และ Proton เส้นทางเดียวกับเกม Steam + Steam และ Proton · ลงชื่อเข้าใช้ Epic ทุกครั้งที่เปิด ปิดโอเวอร์เลย์ EOS + Steam และ Proton · ตั้งค่าตัวแปรของ Amazon Games Services แล้ว + Steam + เพิ่มเข้า Steam เมื่อติดตั้ง (Deck UI + Steam Input) + ไม่มีการดาวน์โหลด + หยุดชั่วคราว + อยู่ในคิว + อยู่ในคิว · #%1$d + ยกเลิกแล้ว + ล้มเหลว + ล้มเหลว · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + หยุดชั่วคราว + ดำเนินการต่อ + ยกเลิก + ลองใหม่ + ล้าง + ล้างรายการที่เสร็จแล้ว + Manifest + ดาวน์โหลด + ตรวจสอบ + ติดตั้ง + เสร็จสิ้น + กำลังตรวจสอบ + กำลังดาวน์โหลด + ลบการดาวน์โหลด? + ล้มเหลว · เก็บ %1$s ไว้ + กำลังตรวจสอบ + กำลังติดตั้ง + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d ไฟล์ + ตัวจัดการดาวน์โหลด + การดาวน์โหลดพร้อมกัน + ติดตั้งไปที่ + ติดตั้ง %1$s: + ภายใน + เหลือ %1$s + การดาวน์โหลดสโตร์ + แสดงความคืบหน้าการดาวน์โหลดเกมและให้การดาวน์โหลดทำงานต่อในพื้นหลัง + กำลังดาวน์โหลด + กำลังดาวน์โหลด %1$d รายการ + กำลังเตรียม… + การดาวน์โหลดสโตร์เสร็จสิ้น + ติดตั้ง %1$s แล้ว + ดาวน์โหลด %1$s ล้มเหลว + ลบ + เก็บไว้ + ลงชื่อเข้าใช้ %1$s + ลงชื่อเข้าใช้ด้วยผู้ให้บริการของคุณ + โหลดใหม่ + ปิด + ลองใหม่ + กำลังเสร็จสิ้นการลงชื่อเข้าใช้… + ไม่สามารถโหลดหน้าลงชื่อเข้าใช้ได้: %1$s + ผู้ให้บริการนี้ปฏิเสธการลงชื่อเข้าใช้จากหน้าที่ฝังอยู่ ให้ลงชื่อเข้าใช้ด้วยรหัสผ่านของสโตร์แทน + หน้าลงชื่อเข้าใช้ตอบกลับด้วยข้อผิดพลาด (HTTP %1$d) + การเชื่อมต่อไปยังหน้าลงชื่อเข้าใช้ไม่ปลอดภัย + ไม่สามารถยืนยันการลงชื่อเข้าใช้ได้ ลองใหม่ + ลงชื่อเข้าใช้ล้มเหลว ลองใหม่ + ปิด + กลับ + แก้ไข + เพิ่มเกมทั้งหมดในโฟลเดอร์นี้ + ไม่มีอะไรใหม่ในโฟลเดอร์นี้ + ชื่อ + เป้าหมาย + เริ่มที่ + ตัวเลือกการเปิด + ภาพปก + exe อื่น… + ลบ + ภาพปก + พื้นหลัง + โลโก้ + ไอคอน + โฟลเดอร์เกม + Steam + SteamGridDB + ไฟล์ของคุณ + จากไฟล์ exe + อัตโนมัติ + เลือกภาพ… + เลือกภาพ: %1$s + กลับไปใช้อัตโนมัติ + + เพิ่มเกมแล้ว %1$d เกม + + + มีอยู่ใน Games แล้ว %1$d เกม + + B · กลับไปที่ %1$s + เกม + การตั้งค่า Play + ไดรเวอร์กราฟิก + Proton นี้ + Proton ทุกตัว + สำหรับ Proton ทุกตัว + พร้อมใช้งาน · %1$d + ตรวจสอบล่าสุด %1$s + ตรวจสอบคอมโพเนนต์และไดรเวอร์ใหม่ + รับ + %1$s · DXVK, VKD3D-Proton และ FEX ด้านล่างเป็นของ Proton นี้ + %1$s · ทดสอบแล้ว + %1$s · ยังไม่ทดสอบ + %1$s · ไม่รองรับ + คงคู่ที่แนะนำให้เป็นรุ่นล่าสุดเสมอ + อัตโนมัติ · %1$s + ไดรเวอร์รันไทม์และแสดงผลแยกกัน + ใช้ %1$s? + ลบ %1$s? + การวินิจฉัย + พร้อมเล่นแล้ว + ยังไม่พร้อมเล่น + รันไทม์ %1$s + ปิดขีดจำกัดโพรเซส + ลงชื่อเข้าใช้เป็น %1$s + เมื่อต้องการให้คุณดำเนินการ + รูปลักษณ์ + อุปกรณ์นี้ + ภาพปกไลบรารี + แอป LINUX + Flathub + แอปเดสก์ท็อปสำหรับเดสก์ท็อป Linux เปิดจากเดสก์ท็อป + ตั้งค่า Flathub + ลบ %1$s? + การตั้งค่าและไฟล์ของมันจะยังคงอยู่ + เก็บไว้ + ขนาดบนดิสก์ + ถอนการติดตั้งเดสก์ท็อป? + ทั้งหมด + ความสำเร็จของ GOG + ซิงค์ความสำเร็จสำหรับเกม GOG ที่รองรับ มีผลในการเปิดครั้งถัดไป + ปิดความสำเร็จ + เปิดใช้ความสำเร็จ + ลงชื่อเข้าใช้ GOG อีกครั้ง + อัปโหลดความสำเร็จแล้ว + กำลังรออัปโหลดความสำเร็จ + เปิดใช้ความสำเร็จของ GOG ในการตั้งค่า Stores + ไม่พบความสำเร็จ ตรวจสอบบันทึกเซสชัน + ปลดล็อกความสำเร็จแล้ว + วาง + ปุ่มกากบาท + อนาล็อกซ้าย + อนาล็อกขวา + เมนู + แป้นพิมพ์ + ปุ่มกากบาทขึ้น + ปุ่มกากบาทลง + ปุ่มกากบาทซ้าย + ปุ่มกากบาทขวา + การควบคุมแบบสัมผัส + คอนโทรลเลอร์สัมผัส + เมาส์ + เปิด + ปิด + ช้า + ปกติ + เร็ว + แก้ไขเค้าโครง + เมนู DroidDeck + ลากปุ่มควบคุมไปวางบนหน้าจอ + จัดเรียง + ละทิ้ง + กำลังบันทึก… + ไอคอน + ก− + ก+ + ส− + ส+ + ซ่อน + สีและความโปร่งใส… + คัดลอกเค้าโครงนี้ + คัดลอกเค้าโครงแล้ว นำไปวางในเค้าโครงสัมผัสของเกมใดก็ได้ + วางเค้าโครงที่คัดลอก + รีเซ็ตเป็นค่าเริ่มต้นของ Steam + ปุ่มควบคุมนี้ไม่มีการผูกคำสั่งใน Steam ที่จะใส่ไอคอนให้ได้ + ไอคอนสำหรับ %1$s + สีของเค้าโครงสัมผัส + เสร็จสิ้น + ไม่สามารถบันทึกเค้าโครงสัมผัสได้ + บันทึกเค้าโครงสัมผัสลง Steam แล้ว + แอป %1$d + ป้ายชื่อ (ไม่บังคับ) + ไม่พบไอคอนของ Steam + + ไอคอน %1$d รายการจาก Steam + + สีไอคอน + สีปุ่ม + ไม่มีไอคอน + + กำลังตรวจสอบการลงชื่อเข้าใช้… + กำลังดึงข้อมูลคลังเกมของคุณ… + กำลังดึงรายการเกม… + กำลังโหลดรายละเอียดเกม… (%1$d/%2$d) + + กำลังซิงค์เกม %1$d เกม… + + กำลังซิงค์… %1$d/%2$d + กำลังดึงภาพปก… + การซิงค์ล้มเหลว + ไม่พบเกมในคลังเกม %1$s ของคุณ + Amazon Games ไม่ตอบสนองสำหรับคลังเกมของคุณ: รีเฟรช หรือลงชื่อเข้าใช้อีกครั้ง + เชื่อมต่อกับ GOG ไม่ได้ + ไม่สามารถอ่านคลังเกม GOG ของคุณได้ + กำลังลงทะเบียนกับ Steam… + Cardboard VR + โหมด Cardboard + ภาพสำหรับตาซ้ายและตาขวาสำหรับแว่น VR ของโทรศัพท์ + ภาพ + หน้าจอเสมือน + 3D แบบเคียงข้าง + แว่น + Cardboard + Cardboard (2014) + ไม่มีเลนส์ + ระยะห่างระหว่างตา + ความกว้างหน้าจอ + ระยะห่างหน้าจอ + การติดตามศีรษะ + อุปกรณ์นี้ไม่มีเซ็นเซอร์การหมุน + จัดมุมมองให้อยู่ตรงกลาง + จัดกึ่งกลาง + SteamVR (ทดลอง) + ติดตั้ง + ดาวน์โหลด SteamVR สำหรับ ARM64 ของ Valve (571 MB) + กำลังดาวน์โหลดและแตกไฟล์ SteamVR… + เริ่ม + เรียกใช้ SteamVR โดยใช้ศีรษะของคุณเป็นชุดหูฟัง ภาพจะเปลี่ยนเป็น 3D แบบเคียงข้าง + หยุด + SteamVR กำลังทำงาน + ใช้ไม่ได้ในเซสชันนี้ + SteamVR ไม่ตอบสนอง ตรวจสอบบันทึกเซสชัน + ดาวน์โหลดคอมโพเนนต์ + ไฟล์ที่ดาวน์โหลดใช้ร่วมกับหน้า Components เลือกเวอร์ชันที่ดาวน์โหลดแล้วด้านบนเพื่อใช้ในการเปิดเกมครั้งถัดไป + เลือกเวอร์ชันที่จะดาวน์โหลด + ผู้พัฒนา + ผู้จัดจำหน่าย + วางจำหน่าย + เนื้อเรื่องหลัก + + ส่วนเสริม + เก็บครบ + %1$s ชม. + %1$d นาที + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 000000000..75ec7fa18 --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,2235 @@ + + + DroidDeck + + İlk çalıştırmada Steam indirilir. Bu işlem birkaç dakika sürebilir. + Varsayılan olarak uygulamadan çıkınca Steam duraklatılır; geri döndüğünüzde kaldığı yerden devam eder. + Kumandanız Xbox 360 kumandası olarak görünür. Kumanda yoksa ekran üstü kontrolleri kullanın. + + + İlk çalıştırmada masaüstü kurulur. Bu işlem birkaç dakika sürebilir. + Varsayılan olarak uygulamadan çıkınca oturum duraklatılır; geri döndüğünüzde kaldığı yerden devam eder. + Dokunma, dokunmatik yüzey gibi çalışır: işaretçiyi taşımak için sürükleyin, tıklamak için dokunun. + Kumandanız Xbox 360 kumandası olarak görünür. Kumanda yoksa ekran üstü kontrolleri kullanın. + + Oturum + Steam veya masaüstü oturumu açıkken gösterilir + Steam çalışıyor + Steam indirmesi sürüyor + Oturum duraklatıldı - hazır olduğunuzda Devam et\'e dokunun + Devam et + Şimdi askıya al + Oturumu durdur + Kare oluşturma + Kare oluşturma modunu seçin + Kapalı + Win-FG %1$d× + LSFG %1$d× + LSFG Uyarlanabilir %1$d + Steam\'de Lossless Scaling gerekli + Lossless Scaling\'i Steam\'den yükleyin veya Lossless.dll dosyasını içe aktarın + Lossless.dll içe aktar… + Steam\'deki kopya kullanılıyor + İçe aktarılan kopya kullanılıyor + Lossless.dll seçin + Lossless.dll içe aktarıldı + Bu Lossless.dll zaten kullanılıyor + Bu Steam hesabı Lossless Scaling\'e sahip değil + Bu dosya Lossless Scaling\'in Lossless.dll dosyası değil + Bu Lossless.dll ile LSFG hazırlanamadı + LSFG için Steam\'den Lossless Scaling veya içe aktarılmış bir Lossless.dll gerekir + LSFG, gölgelendiricilerini Lossless.dll dosyasından hazırlayamadı + %1$s: desteklenmiyor + %1$s: motor hatası + %1$s: Lossless Scaling gerekli + %1$s %2$s · başlatılıyor + Bir Steam kütüphanesi ekler ve Windows oyunları için tarar. Sahip olduğunuz oyunlar Steam kayıtlarını kullanır; geri kalanlar Steam dışı kısayollar olur. + Oyun ortamı + Proton oyunları için ortam değişkenleri. + Düzenle + Tüm Proton oyunları + Profil + Steam uygulama kimliği + Aç + Oyun %1$s + Değişken ekle + Değişken adı + Değer + Kaydet + Kaldır + Yeniden devral + Ortam geçersiz kılmalarını temizle + Bu profilde tanımsız bırak + Ayarlanmış değişken yok. + Devralındı + Özel değişken + Yüklenemedi veya kaydedilemedi. Boş depolama alanını kontrol edip tekrar deneyin. + Geçerli bir ad ve en fazla 8192 karakterlik bir değer kullanın. + Standart Proton çalışma zamanı bu değişkeni okumaz. + Yükleniyor… + D3D12 özellik düzeyi geçersiz kılma. 12_2, DirectX 12 Ultimate özelliklerini bildirir; desteklenmeyen özellikler hata verebilir. Otomatik algılama için boş bırakın. + D3D12 gölgelendirici modeli geçersiz kılma. 6_9 için yeterince yeni bir VKD3D ve uygun GPU özellikleri gerekir. Otomatik algılama için boş bırakın. + Tekrarlanan derlemeyi azaltmak için Mesa gölgelendirici önbelleğini açık tutun. false önbelleği etkinleştirir. + Azami Mesa gölgelendirici önbelleği boyutu, ör. 1G. Daha büyük önbellekler daha fazla depolama alanı kullanır. + D3D12 için VKD3D seçenekleri. nodxr ışın izlemeyi kapatır; yalnızca buna ihtiyaç duyan bir oyunda kullanın. + Proton tanılama günlüğü için 1 olarak ayarlayın. Günlük kaydı performansı düşürebilir ve depolama alanı kullanır. + D3D9–11 uyumluluk testi için DXVK yerine OpenGL tabanlı WineD3D kullanmak üzere 1 olarak ayarlayın. + Bu oyunda ntsync\'i (droiddeck-ntsync veya çekirdek sürücüsü) kapatmak için 1 olarak ayarlayın; paket varsa droiddeck-esync, yoksa droiddeck-fsync kullanılır. + Bu oyunda droiddeck-esync\'i kapatmak için 1 olarak ayarlayın; yerine droiddeck-fsync, fsync de kapalıysa droiddeck-ntsync kullanılır. + Bu oyunda droiddeck-fsync\'i kapatmak için 1 olarak ayarlayın; paket varsa droiddeck-esync, yoksa droiddeck-ntsync kullanılır. + Bu oyunu droiddeck-fsync ile çalıştırmak için 1, Proton sürümüne uyan bir paket varsa droiddeck-esync\'i öne almak için 0 olarak ayarlayın. + Bu oyunda Proton’un kumandayla gezinme yardımcısını kapatmak için 0 olarak ayarlayın. + D3D9–11 için DXVK katmanı. fps kare hızını gösterir; boş bırakılırsa gizlenir. + Noktalı virgülle ayrılmış DXVK ayarları. Yeni sürümler kare sınırlama için dxvk.maxFrameRate kullanır; normal oyun için oturumun kare sınırlayıcısını tercih edin. + D3D12 kare hızı sınırı. 0 sınırı kaldırır. + Mesa OpenGL iş parçacığı kullanımı. true, CPU sınırlı OpenGL oyunlarına yardımcı olabilir; her oyunda ayrı deneyin. + Wine senkronizasyonu + Oyunların iş parçacıklarını nasıl eşzamanladığı. Sonraki oturumda uygulanır. + esync + ntsync + fsync + wineserver + FEX ön ayarı + Oyun bir sonraki başlatıldığında uygulanır. + Oyunlar + FEX varsayılanları + FEX varsayılanlarını kullanır. + Kararlılık + En güvenli, en yavaş ayarlar. + Uyumluluk + Multiblock ile tam TSO. + Orta + Düşürülmüş x87 hassasiyetiyle dengeli TSO. + Performans + Tüm TSO kapalı; multiblock. + Performans + TSO + Ana TSO açık Performans ayarları. + Ekstrem + Kendini değiştiren kod denetimlerini kapatır. JIT kullanan oyunlar ve DRM bozulabilir. + Ekstrem + TSO + Ana TSO açık Ekstrem ayarları. + Ekstrem-gn + TSC ve meta veri ipuçlarıyla Performans. Ekstrem\'den önce deneyin. + Denuvo + Tam kod denetimleri ve gizlenmiş hipervizör bitiyle Performans. + Özel değer + Değer seçin + Emülatörler ve uygulamalar + Ekle + Kendi uygulamanız veya oyununuz + Betik + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %%%3$d + Başlatılıyor… + %1$s eklendi + %1$s kaldırıldı + Masaüstü + Bu uygulama artık yok. Masaüstünden yeniden seçin. + %1$s uygulamasını aç + Kaldır + Kaldırmak için tekrar basın + Kaldırmak için oturumu durdurun + %1$s kendi klasöründen çalışır; klasör bulunduğu yerden oturuma bağlanır. Uygulamayı kaldırmak o klasöre dokunmaz. + %1$s, klasörünün DroidDeck içindeki bir kopyasından çalışır. Uygulamayı kaldırmak orijinali değil, kopyayı siler. + Önce Linux çalışma zamanını yükleyin + Dosya artık yok + Yeterli boş alan yok: klasörü kopyalamak için %1$s gerekli + %1$s kopyalanıyor + Uygulama eklenemedi + %1$s için sürüm aranıyor + %1$s sürümleri okunamadı + %1$s sürümlerinin hiçbirinde ARM64 veya x86_64 AppImage yok + %1$s indiriliyor + İndirme başarısız oldu + Uygulama güncellenemedi + Bu simge okunamadı + %1$s kaydedildi + Düzenle + Güncellemeleri kontrol et + Güncellemeler kontrol ediliyor… + %1$s sürümüne güncelle + Güncel · %1$s + %1$s güncellendi + %1$s · %2$s + Uygulama ekle + Betik + AppImage + GitHub + Flatpak + Bir .sh dosyası. Kendi klasöründen, gamescope altında tam ekran çalışır. + ARM64 (aarch64) bir AppImage ya da FEX ile çalışan x86_64 bir AppImage. Linux çalışma zamanına bir kez çıkarılır. + Deponun en yeni sürümündeki ARM64 AppImage dosyasını ya da FEX ile çalışacak x86_64 dosyasını yükler. + FEX kullan + Otomatik + Açık + Kapalı + FEX, x86 Linux programlarını bu ARM64 cihazda çalıştırır. Otomatik, program x86 için derlenmişse FEX\'i kullanır. + FEX ile %1$s + Bu program %1$s PC\'ler için derlenmiş ve FEX ile çalışır, ancak FEX henüz kurulmadı. Oturum açmışken Steam\'i bir kez açın: DroidDeck, FEX\'i ve gereken Steam Linux Runtime\'ı indirtir, ardından bu program açılır. + Bu program + %1$s x86 PC\'ler için derlenmiş ve FEX ile çalışır, ancak FEX henüz yüklü değil. Oturum açmışken Steam\'i bir kez başlatıp FEX\'i ve Steam Linux Runtime\'ı indirmesini bekleyin, ardından bunu yeniden başlatın. + Bir Flathub uygulama kimliği veya Flathub sayfası. Flatpak henüz kurulu değilse önce o kurulur. + Dosya + Dosya seçilmedi + Dosya seç + Değiştir + Bir betik seçin + Bir AppImage seçin + Bir simge seçin + Depo + sahip/depo veya github.com bağlantısı + GitHub deposu değil + Uygulama kimliği + org.ppsspp.PPSSPP + Flathub uygulama kimliği değil + Ya da Flathub\'da olmayan bir uygulama için .flatpak dosyası. + Bir .flatpak dosyası seçin + Kaynak + Masaüstünüzde + Yeni uygulama + Ad + Uygulamadan + Simge + Varsayılan simge + Simge seç + Temizle + Depodan + Simgeler aranıyor… + Varsayılan + Düzenle: %1$s + Kaydet + Klasör kontrol ediliyor… + Klasörü oturuma bağlanır: %1$s + Klasöründe paylaşılan depolamanın çalıştıramadığı Linux programları var, bu yüzden DroidDeck içine kopyalanır (%1$s). + Ekle + İptal + Önce Kurulum sayfasından Linux çalışma zamanını yükleyin. + %1$s bitene kadar bekleyin. + Uyumlu + Steam’in Uyumluluk menüsünde “%1$s” ile işaretli sürümler DroidDeck’in önek düzeltmelerini içerir ve oyunları burada başlatır. Varsayılan Proton Experimental ARM64’tür (%1$s); daha yeni Proton 11.0 ARM64 (%1$s) yüklüyse o kullanılır. Valve’ın bu işareti taşımayan kendi Proton girdileri bu cihazda oyun başlatamaz. + Yüklü: %1$s · %2$s + İptal + Otomatik + Asla + Her zaman + Yüklemeyi kaldır + Kontrol et + Güncelle + Kontrol ediliyor… + İşleniyor… + Unut + Ekle… + Kullan + Sonraki oturumda uygulanır. + Steam oturumu + Masaüstü oturumu + Ekran + Çözünürlük + En fazla 720p + En fazla 900p + En fazla 1080p + Yerel + %1$s (varsayılan) + Özel · %1$d×%2$d + Özel… + 720p, menülerin daha hızlı tepki vermesini sağlayabilir. + Ekran oranı + Özel çözünürlüğe göre ayarlanır. + Otomatik en az 16:9 kullanır. + Kare sınırı + Tüm oturumu sınırlar. Ekran, sınırın tam katı olan bir yenileme hızına geçer. + HDR + HDR10 çıkışı + Kullanılamıyor: %1$s. + Uygulamak için uygulamayı yeniden başlatın. + Sürücüler + GPU sürücüleri + Steam ve Masaüstü tarafından paylaşılır. Bileşenler sayfasından ayarlanır. + Dokunma ve kontroller + Dokunma + Dokunmatik yüzey + Doğrudan + Otomatik: Masaüstünde dokunmatik yüzey, Steam\'de doğrudan. + Ekran üstü kontroller + Steam + QAM + Otomatik, kumanda bağlı değilken kontrolleri gösterir. + Steam dokunmatik (beta) + Steam\'in dokunmatik kontrolleri sonraki Steam oturumunda başlar + Steam dokunmatik düzeni + Bu oyunun dokunmatik kontrollerini taşıyın, boyutlandırın veya gizleyin; Steam\'e kaydedilir + Kumanda + Steam\'in kumandanızı nasıl gördüğü. Sonraki oturumda uygulanır. + Steam Deck kumandası + Steam Controller + DualShock 4 + DualSense + Xbox 360 kumandası + Deck: Hızlı Erişim düğmesi ve jiroskop. Steam Controller: aynısı, ayrıca arka düğmelerde kavrama algılama. DualShock 4 ve DualSense: jiroskop; dokunmatik yüzeyler kumandanın dokunmatik paneli olur. Xbox 360: Hızlı Erişim\'i Guide+A ile açın. + Geri + Menü → QAM + QAM → Menü + Oturum + Arka plan davranışı + Uygulama gizlendiğinde veya ekran kapandığında. + Manuel + Yerel + Otomatik: duraklatır, gösterilince devam eder. Yerel: önce Steam\'i uykuya hazırlar. Manuel: Devam et\'i bekler. Asla: çalışmaya devam eder. + İndirmeleri sürdür + Steam etkin olarak indirme yaparken arka planda askıya almayı erteler. + Otomatik: arka planda duraklatır, gösterilince devam eder. Manuel: Devam et\'i bekler. Asla: çalışmaya devam eder. + Başlangıç + Steam\'i DroidDeck ile başlat + DroidDeck açıldığında Steam oturumunu açar. + Decky + Sürümler kontrol ediliyor… + %1$s yüklenmeye hazır. + Uyumlu derleme bulunamadı. Daha sonra tekrar deneyin. + Yüklü · %1$s + Güncelleme var · %1$s + Güncel · %1$s + Yükleyici + En yenisini yükle + Steam ile başlar. Açıkken bu cihazdaki diğer uygulamalar Steam\'i kontrol edebilir. + Kapalı. Steam\'in uzaktan hata ayıklama bağlantı noktası kapalı kalır. + Decky Loader kaldırılsın mı? + Yükleyiciyi kaldırır. Eklentileriniz ve ayarlarınız korunur. + İstemci + Performans katmanı + Oyunlar Deck modunda çökerse veya ekran kararırsa kapatın. Sonraki oturumda uygulanır. + Steam istemcisini onar + Steam bir sonraki başlatıldığında Steam istemcisini yeniden indirir. Steam yükleme ekranında takılı kalırsa kullanın. Oyunlar, oturum bilgileri ve ayarlar korunur. + Steam istemcisi, Steam bir sonraki başlatıldığında yeniden indirilecek. + Onar + Görseller + SteamGridDB API anahtarı + Kendi anahtarınız + Düzenle… + Eksik görselleri önce Steam\'den, sonra SteamGridDB\'den tamamlar. Oyun klasöründeki kendi cover.jpg, hero.jpg, logo.png veya icon.png dosyalarınız önceliklidir. + Kaldır: %1$s + Kaldırılsın mı? + Oyun ekle + Oyun ekle: + Exe dosyasını seçin: + Bu konum oturumda kullanılamıyor + Bu exe dosyasını kontrol et + Windows bileşenleri + LB / RB ile bölümler arasında geçin. + Yok + Proton\'un içermediği Windows DLL\'leri, bileşen listesinden. Bu oyunun ihtiyaç duyduklarını açın: oyunun dosyalarına değil, kendi Windows klasörüne yüklenir ve oyun hangi Proton ile çalışırsa çalışsın yerinde kalır. + Bileşen listesine ulaşılamadı. Önceden indirilmiş bileşenler yine de açılıp kapatılabilir. + Bu oyun için önerilenler + Bu oyunun ihtiyaçları kontrol ediliyor… + Çevrimiçi olunca kullanılabilir + + %1$d bileşen açılıyor… + %1$d bileşen açılıyor… + + Bu oyun için açık + Steam, %1$s bileşenini %2$s ile yükler (uygulama %3$s) + Bu oyun için GOG listesinden + Bu oyun için Epic ön koşullarından + Bu oyun için Amazon kurulum adımlarından + Steam, %1$s bileşenini %2$s uygulamasıyla yükler (adla eşleştirildi) + Ayrıca listelenenler + %1$s tarafından listelendi; burada bir bileşen değil + Steam + GOG + Epic Games + Amazon Games + Oyun %1$s ile birlikte gelir + Oyunda %1$s var (çoğu zaman olduğu gibi çalışır) + Bu oyunda zaten var + Oyunun klasöründe bir bileşene işaret eden bir şey yok. + Bu oyun için yüklü (%1$d) + Tüm bileşenler (%1$d) + Windows yükleyicisi gerekenler (%1$d) + Bunlar, içinde açılacak bir paket bulunmayan kendi kurulum programlarıdır ve yükledikleri şeylerin henüz bir kaydı yok. Çoğu, Proton içindeki Mono\'nun zaten yerini tuttuğu eski çalışma zamanlarıdır; kayıtlar hazırlandıkça eklenir. + Göster + Gizle + Kendi kurulum programını çalıştırır (henüz kayıt yok) + Burada henüz desteklenmiyor + Bileşenler yükleniyor… + Yüklenemedi: %1$s + İndirildi + %1$s indiriliyor + Yükleniyor: %1$s + Kapatılıyor… + İndirme: %1$s + Oyun bir sonraki başlatıldığında etkili olur. + 16:9 oyunları uzat + 16:9\'dan daha kare ekranlarda oyunları siyah şerit göstermek yerine ekranı dolduracak şekilde uzatır. Sonraki oturumda uygulanır. + Ses + Oyunlar için DirectAudio + Daha düşük gecikme için PulseAudio\'yu atlar. + Mikrofon + Sesli sohbet için. + Ağ + Wi-Fi keşfi + Steam\'de Wi-Fi adlarını ve yakındaki ağları göster. + Konum hizmetleri kapalı. + Konum izni verilmedi. + Ayarları aç + Devam + Wi-Fi keşfi etkinleştirilsin mi? + Android, Wi-Fi adlarını okumak ve yakındaki ağları keşfetmek için Konum izni gerektirir. DroidDeck bu erişimi, bu ağları Steam\'de göstermek için kullanır. Konumunuzu takip etmeyiz. İnternet erişimi bu izin olmadan da çalışır. + Oyun depolama alanı + Otomatik - varsa SD kart, ayrıca Games klasörleriniz + Otomatik - Games klasörleriniz, ayrıca varsa SD kart (şu an yok) + Yalnızca dahili + Klasör: %1$s + İkinci kütüphane + Varlıkları SD karttan veya paylaşılan depolamadan akışla yükleyen oyunlarda takılmalar olabilir. + Depolama tanılaması + Ayırma sonuçlarını ve örneklenmiş depolama sürelerini “Günlükleri paylaş” çıktısına ekler. Açıkken yüklemeleri yavaşlatabilir. Sonraki oturumda uygulanır. + Klasör seçin… + Özel çözünürlük + Sınırın ve oranın yerine geçer. Ekrana uymayan bir boyutta siyah şeritler görünür. + Genişlik + Yükseklik + 320×240 ile 3840×2160 arasında. + + %1$d oyun + %1$d oyun + + Tamam + Sil + Geri yükle + İndir + Sil: %1$s + %1$.1f MB + Çalışma zamanı sürücüsü + Steam, oyunlar ve masaüstü tarafından kullanılır. Sonraki oturumda uygulanır. + Turnip zip dosyası içe aktar… + Ekran sürücüsü + Her iki modda kareleri ekrana çizer. Uygulamak için uygulamayı yeniden başlatın. + AdrenoTools zip dosyası içe aktar… + hiçbir zaman + Nightlies son kontrol: %1$s + Bileşenler + Bileşenler hakkında + Yeni sürücüleri kontrol et + Yeni paketler için Nightlies\'i kontrol et + Proton + Burada seçilen Proton, Steam\'de kendi Proton\'unu seçmemiş her oyunu çalıştırır; Steam\'de yapılan bir değişiklik de burada görünür. Steam güncellenince Valve\'ın Proton\'unu değiştirir; orijinaller her derleme için saklanır. + Yapılandırma + Profili yapılandır + Profil + Varsayılan profil + Geçersiz kılma ayarı olmayan oyunlarda otomatik kullanılır. + %1$s başlatıldığında otomatik kullanılan kayıtlı ayarlar. + Yalnızca bu oyunun ihtiyaç duyduğu ayarları seçin. Geri kalan her şey Varsayılan profili kullanmaya devam eder. + FEX ön ayarı + Varsayılan profilin ön ayarı kullanılıyor. + Bu oyun için özel ön ayar kaydedildi. + Özel FEX değişkenleri + Varsayılanı kullan + Oyun uyumluluğu + Bu oyun için Proton, DXVK, VKD3D ve senkronizasyon ortam seçenekleri. + Varsayılan profilin Proton\'u kullanılıyor. + Bu oyun için özel Proton, Steam uyumluluk eşlemesiyle kaydedildi. + Proton yok + DXVK + VKD3D-Proton + Bu oyun başka bir paket seçmedikçe %1$s kaynağındaki Varsayılan profil paketini kullanır. + Varsayılan profilin bileşen paketini kullanır. + Yalnızca bu oyun için %1$s kullanır. + Çalışma zamanı seçenekleri + Oyuna özel diğer Proton, DXVK, VKD3D-Proton ve senkronizasyon ortam seçenekleri. + %1$s profili kaydedildi; oyun bir sonraki başlatıldığında uygulanır. + Steam olmadan başlat + Hemen kapanan bir oyun için: Proton\'un Steam kurulumu olmadan başlatır. Overlay gibi Steam özellikleri bu oyun için kapalıdır. + Açık + %1$s için Proton seçimi kaydedildi ve şimdi Steam\'de kullanılabilir. + %1$s için Proton seçimi kaydedildi. Oyunu başlatmadan önce Steam\'i yeniden başlatın. + %1$s bu cihazda seçili Proton\'u kullanamaz. + %1$s profili yüklenemedi: %2$s + Oyun profili kaydedilemedi: %1$s + Oyun profilini sıfırla + %1$s profili sıfırlansın mı? + Bu oyunun Proton, FEX, bileşen paketi, ortam, doku filtreleme ve Windows bileşeni geçersiz kılmaları kaldırılsın mı? Oyun Varsayılan profili kullanır. + %1$s artık Varsayılan profili kullanıyor. + Oyun profili tamamen sıfırlanamadı: %1$s + %1$s artık Steam\'de kendi Proton\'unu seçmemiş her oyunu çalıştırıyor. + %1$s, Steam bir sonraki başlatıldığından itibaren kendi Proton\'unu seçmemiş her oyunu çalıştıracak. + %1$s burada oyun başlatamaz, bu yüzden varsayılan olamaz. + Varsayılan Proton kaydedilemedi: %1$s + %1$s · %2$d değiştirildi + %1$s · tümü orijinal + %1$s · oyun çalışıyor + %1$s · droiddeck-esync paketi + %1$s · henüz droiddeck-esync paketi yok + %1$s · esync yerleşik, paket gerekmez + GPU sürücüleri + %1$s · başlatmada yeniden uygulandı %2$s + Sıradaki: %1$s · oyun kapandıktan sonra + %1$s kullanımda: %2$s + Oyun kapanana kadar bekler + Bir oyun bir sonraki başlatıldığında uygulanır + .zip içe aktar + Proton sürümleri okunuyor… + Henüz Proton yüklü değil. ARM64 Proton\'u indirmesi için Steam\'i bir kez başlatın veya Kurulum sayfasından GE-Proton, CachyOS ya da DroidDeck-Proton ekleyin. + Orijinal · %1$s + %1$s · bu derleme + %1$s · önceki derleme + Yüklü · %1$d + %1$s için henüz saklanan bir şey yok. Bu Proton\'un kendi dosyaları orijinali olarak kaydedilir. + Bu Proton\'da bir oyun çalışıyor, bu yüzden oyun kapanana kadar beklenir. + Bir oyun bir sonraki başlatıldığında uygulanır. + %1$s ile değiştirilsin mi? + %1$s, %2$s içine yerleştirilsin mi? Orijinal dosyaları kayıtlı kalır. %3$s + %1$s geri yüklensin mi? + %1$s orijinali %2$s içine geri konulsun mu? %3$s + Paketi uygulamadan kaldırır. Yeniden indirebilirsiniz. + Bu orijinal silinsin mi? + Bu %1$s orijinali, %2$s sürümünün önceki bir derlemesine ait. Geçerli derlemenin orijinali her zaman saklanır. + Nightlies içinde · %1$d + Nightlies paketlerini listelemek için yenileyin. + Son kontrolden beri yeni bir şey yok. + .wcp içe aktar + Bu Proton\'un orijinalini geri yükle + %1$s için orijinal %2$s geri yüklensin mi? + Değiştir + Steam\'in oyunları çalıştırdığı her Proton için FEX, DXVK ve VKD3D-Proton.\n\nDeğiştirmek için yüklü bir satırı seçin. Bir oyun bir sonraki başlatıldığında ya da çalışan oyun kapandıktan sonra uygulanır.\n\nSeçimleriniz her başlatmadan önce yeniden kontrol edilir ve bir şey onları değiştirdiyse geri konur. Her Proton\'un orijinal dosyaları saklanır, böylece Steam güncellemeleri onları asla kaybettirmez. + Orijinal + Kullanımda + Yeni + Saklandı + Performans + CPU çekirdekleri, oturum düzeltmeleri ve ana bilgisayar adı. Sonraki oturumda uygulanır. + Steam istemcisi çekirdekleri + Steam\'in çekirdek seçimini geçersiz kıl + Steam\'i, arayüz yardımcısını ve gamescope\'u aşağıdaki çekirdeklere sabitler. + İstemci çekirdekleri + Steam için çekirdekler. + Seçmek için geçersiz kılmayı açın. + Oyun çekirdekleri + Oyun çekirdekleri + Oyunlar için çekirdekler. Tümü seçiliyse Android serbestçe zamanlar. + İstemci arayüzü + İş parçacıklı GL + Steam menülerinin daha hızlı tepki vermesini sağlayabilir. + Zink: tembel tanımlayıcılar + Tanımlayıcı arabelleği olmayan sürücüler için önerilir. + GL hata denetimlerini atla + Çağrı başına GL doğrulamasını kapatır. + gamescope: gerçek zamanlı GPU kuyruğu + Bileştiricinin GPU işine öncelik verir. Oyunlardan biraz feda ederek kare zamanlamasını yumuşatabilir. + GPU + GPU\'yu en yüksek saat hızında tut + Yalnızca Adreno. Daha az takılma, daha fazla pil tüketimi ve ısınma. + Oturum düzeltmeleri + Turnip: sysmem işleme + Steam ve çoğu oyun için daha hızlı. Adreno 710/720/722\'de gereklidir. + Steam\'in xalia yardımcısını atla + droiddeck-ntsync (deneysel) + Linux ntsync sürücüsünün test amaçlı, kullanıcı alanında çalışan bir yedeği. Açıkken Proton, droiddeck-esync ve droiddeck-fsync yerine bunu kullanır. Proton Experimental ARM64, GE-Proton ve proton-cachyos ile çalışır. Sonraki oturumda uygulanır. + droiddeck-esync + Önce droiddeck-fsync + Proton\'un kendi fsync\'ini standart Proton dosyalarıyla, futex_waitv kullanıcı alanında sağlanarak çalıştırır; bu yüzden paket gerektirmez ve Steam Proton\'u güncellediği anda da çalışmaya devam eder. Açıkken oyunlar, Proton\'un fsync dışında tuttuğu birkaç oyun hariç, bunu droiddeck-esync\'ten önce kullanır. Sonraki oturumda uygulanır. + Oyunların Windows iş parçacıkları, wineserver üzerinden gidip gelmek yerine eventfd ve paylaşılan bellek aracılığıyla birbirini bekler. Tam olarak o Proton derlemesi için hazırlanmış bir droiddeck-esync paketi kullanır; paketler otomatik indirilir. Yeni bir Proton derlemesi için paket çıkana kadar droiddeck-fsync devreye girer. Sonraki oturumda uygulanır. + Proton\'un her oyunda CPU harcayan kumandayla gezinme yardımcısını kapatır. + proot\'u seccomp olmadan çalıştır + Bazı çekirdeklerde eksik sistem çağrısı hatalarını düzeltebilir. Performansı düşürebilir. + proot hızlı yolu + proot\'un seccomp filtresi gerekir. + Daha hızlı dosya aramaları ve daha az takılma. Bir program dosyalarını bulamıyorsa kapatın. + Oturum kimliği + Ana bilgisayar adı + Steam\'in bu cihaz için gördüğü ad. Boş bırakılırsa %1$s geri yüklenir. + Yalnızca harf, rakam ve arada kısa çizgi; en fazla 63 karakter. + Android bu oturumu sonlandıracak şekilde ayarlı + Yüklü + İndirilebilir + Son kontrolden beri yeni bir şey yok. + Yerleşik sürücüleri geri yükle + Uygulamada yerleşik: bu işlem açılmış dosyalarını kaldırır ve sürücüyü gizler. “Yerleşik sürücüleri geri yükle” onu geri getirir. + Bir Android + Linux paketinin yarısı: iki yarı da kaldırılır. Kullanımdaki bir sürücü varsayılana döner. + Dosyaları uygulamadan kaldırılır. + Dosyaları kaldırılır. Kullanımda olduğu için varsayılan devreye girer. + Ekran + Efektler + Kontroller + Bileşenler + Kumanda + Ayarlar + %1$s sayfası + Steam + Masaüstü + Steam menüsü + Hızlı erişim + Hızlı Erişim Menüsünü aç + Önceki sayfa + Sonraki sayfa + Performans HUD\'u + Ölçekleme modu + Ölçekleme keskinliği + Ekran efektleri + Anında uygulanır. Herhangi bir efekt açıkken biraz GPU süresi harcar. + Ön ayarlar + Özel + Gelişmiş + Genişletildi + Daraltıldı + Keskinleştirme (CAS) + Keskinleştirme düzeyi + Sahte HDR + Bant giderme + Bant giderme gücü + Parlaklık + Kontrast + Gama + Doygunluk + FXAA (kenar yumuşatma) + Çizgi film konturları + CRT + NTSC + Doku filtreleme + DirectX 9–11 oyunları, bir sonraki başlatmadan itibaren. + Anizotropik filtreleme + Doku keskinliği + Negatif mip sapması: daha keskin dokular, biraz titreşim. Otomatik, oturumun ekrana ne kadar büyütüldüğüne göre ayarlanır. + Otomatik (%1$s) + Klavye + PC klavyesi + Android + İkinci ekran + Mod + Gösterilen + Android uygulamaları + Uygulama başlat + Gizle + Göster + Başlatılabilir uygulama yok + Oturum davranışı + Hemen ve bu moddaki sonraki oturumlarda uygulanır. + Sonraki oturum + Destek + Oturum günlükleri + Hata raporuna ekleyin + Günlükleri paylaş + Arka plan + Durdur + Oturum durdurulsun mu? + Şu anda çalışan + Proton üzerinde çalışan oyun yok + Oyun profili + Profil ayrıntıları henüz kullanılamıyor. + Oyun profilleri yüklenemedi. Otomatik olarak yeniden deneniyor. + Oyun profilleri yenilenemedi. Son mevcut veriler gösteriliyor; otomatik olarak yeniden deneniyor. + Bir sonraki başlatmada kullanılacak kayıtlı ayarları görüntüleyin ve düzenleyin. + Varsayılan profil kullanılıyor + Özel profil + Varsayılan profildeki %1$s kullanılıyor. + Değişiklikler %1$s profiline kaydedilir ve oyun bir sonraki başlatıldığında uygulanır. + + %1$d oyun geçersiz kılması + %1$d oyun geçersiz kılması + + Henüz Proton yüklü değil. + oyun çalışıyor + başlatmada yeniden uygulandı %1$s + Orijinal + İçe aktarma ve silme işlemleri Bileşenler sayfasında yapılır. + Tamamlandı + Dikkat gerekiyor + İşleniyor + Kablosuz hata ayıklama IP adresini ve bağlantı noktasını kontrol edin ya da Android bu cihazı kaldırdıysa yeniden eşleyin. + Yükle + Yönet + Genel bakış + Kumanda + Oturum + Başlatıcı + Kurulum + ● Her şey hazır + %1$d / %2$d hazır + Sistem kontrolü + Cihaz destekleniyor + Test edilmemiş GPU + GPU desteklenmiyor + %1$s · %2$s + %1$s: %2$s + Steam için Adreno sürücüsü gerekir; %1$s siyah ekran gösterebilir + Linux çalışma zamanı + %1$s · %%%2$d + Yüklü değil · yaklaşık 3 GB, ilk kez Oyna\'ya basınca yüklenir + Yüklü + %1$s · güncelleme var + %1$s · güncel + Alt süreç sınırı + Açık · Android Steam\'i kapatabilir. Kapatmak bir dakika sürer. + Ayarlanmadı · Android Steam\'i kapatabilir. Kapatmak bir dakika sürer. + Kontrol edilemedi · kapatmak bir dakika sürer. + Düzelt + Ayrıntılar + Geliştirici seçenekleri + Kablosuz hata ayıklamayı kullan + Tekrar kontrol et + Sınırı aç + Steam hesabı + %1$s olarak oturum açıldı · çevrimdışı mod + %1$s olarak oturum açıldı + Oyna\'ya basın ve Steam\'de oturum açın + Araçlar + Kumanda + Kumanda ayarları kullanılamıyor. + Oturum + Oturum günlükleri + Her oturumdan sonra kaydedilir + Hata ayıklama araçları + Son oturum günlükleri + Kayıtlı oturum günlükleri + En yeni %1$d tanesini saklar + Günlükleri temizle + Çevrimdışı mod + Önce Steam\'de oturum açın + Başlatıcı + Dil + Steam de bir sonraki başlatılışından itibaren bunu kullanır. + Sistem varsayılanı (%1$s) + Sistem varsayılanı + Uygulama ölçeği + DroidDeck metinleri ve kontrolleri + %%%1$d (Varsayılan) + Tema + Başlatıcı görünümü + Ana ekran olarak kullan + DroidDeck ana ekran uygulaması olabilir + DroidDeck hiçbir zaman ana ekran uygulaması olarak sunulmaz + Yön + Otomatik yatay + Yatay + Ters yatay + Tam ekran + Android durum ve gezinme çubuklarını gizler + Android durum ve gezinme çubuklarını gösterir + Varsayılan ana ekran uygulaması + Ana ekran uygulaması seçin + Seç + Linux uygulamaları (beta) + Dosyalar + Dosyalara göz atın ve yönetin + Proton sürümleri + ARM64 Proton derlemelerini yükleyin + Performans + CPU çekirdeği ataması + ROM klasörü + Emülatör oyunlarının saklandığı yer + Bileşenler + FEX, DXVK, VKD3D + Kontroller + Düğme eşleme + Keşfet + Ara + Yüklü + %1$s · %2$d + Mağaza + Flathub · ARM64 + ● Flatpak hazır + Önce Kurulum sayfasından Linux çalışma zamanını yükleyin. + Flatpak\'i kur + Flatpak\'i ekler ve Flathub\'a bağlar (yaklaşık 20 MB). İlk uygulama kendi çalışma zamanını da indirir. + Kuruluyor… + Flatpak + Güncellemeler + %1$s · %2$s + %1$s · %2$s · %%%3$d + Başlatılıyor… + Kategoriler + Flathub\'a ulaşılamadı. Bağlantınızı kontrol edip tekrar deneyin. + Tekrar dene + Flathub\'da ara + Firefox, oyunlar, emülatörler… + Ara + Tümü + Aranıyor… + Ada göre arayın veya bir kategori seçin. Yalnızca ARM64 uygulamaları listelenir. + Flathub\'da bununla eşleşen ARM64 uygulaması yok. + Sonuçlar + Güncellemeleri kontrol et + Tümünü güncelle (%1$d) + ● Her şey güncel + Henüz uygulama yok. Keşfet veya Ara bölümünden bulabilirsiniz. + Uygulama yüklemek için Flatpak\'i kurun. + Güncelleme var · %1$s + Aç + Ayrıntılar + Kaldır + Kaldırmak için tekrar basın + Yükleniyor… + Yükle + Önce Flatpak\'i kurun + İndirme: %1$s + ● Yüklü + Bu uygulamanın Flathub sayfası yüklenemedi. + Flathub\'dan yükleniyor… + Flathub\'da ARM64 derlemesi yok, bu yüzden burada yüklenemez. + Ekran görüntüleri + Hakkında + Uygulama kimliği + Lisans + Çalışma zamanı + Yüklü boyut + %1$s (ayrıca paylaşılan çalışma ortamı) + Web sitesi + Doğrulandı + Güncellemeler + DroidDeck %1$s · %2$s + Kontrol edildi %1$s + Henüz kontrol edilmedi + DroidDeck kendini güncellesin + Sonraki ekranda “Bu kaynağa izin ver” seçeneğini açın, sonra buraya dönün. Güncelleme kendiliğinden sürer. + Ayarları aç + Şimdi değil + Farklı imzalanmış + Yerinde güncellenemez + Bu kopya DroidDeck\'in yayın anahtarıyla imzalanmamış, bu yüzden güncellemeler üzerine yüklenemez. Geçmek için önce kaldırın. + Güncelleme var + Yüklenemiyor + Geçişe hazır + Kararlı\'nın önünde + Kararlı\'nın önündesiniz + Bu derleme en yeni Kararlı sürümden daha yeni. Bir sonraki sürümüyle Kararlı\'ya geçeceksiniz. + Test sona erdi + Bu test sona erdi + Düzeltmesi birleştirildi veya bırakıldı. En yeni düzeltmeler için Önizleme\'yi takip edin. + Güncel + En yeni %1$s\'e sahipsiniz + · %1$s + %1$s… %2$d%% + %1$s… + Güncelle%1$s + Yükle%1$s + Yine de Kararlı %1$s\'i yükle + Önizleme\'yi takip et + Güncellemek için çalışan oturumu durdurun. + Daha az + Daha fazla + DroidDeck %1$s + Yeni Önizleme derlemesi + PR #%1$d test derlemesi + %1$d MB + GÜNCELLEME KANALI + Kararlı + Test edilmiş sürümler, çoğu kişi için + %1$s · %2$s + Önizleme + En yeni düzeltmeler, Kararlı\'dan önce + Güncel + Test derlemeleri + Yayınlanmadan önce bir düzeltmeyi deneyin + Şu anda yok + PR #%1$d + Bu DroidDeck kopyası için derlenmedi + Güncellendi %1$s + PR #%1$d testi + bir süre önce + az önce + + %1$d denenecek + %1$d denenecek + + + Derlemenizden sonraki değişiklikler · %1$d derleme geride + Derlemenizden sonraki değişiklikler · %1$d derleme geride + + + Son değişiklikler · %1$d derleme + Son değişiklikler · %1$d derleme + + Değiştirilecek bir Android ayarı + Steam + Android, çok sayıda arka plan süreci başlatan uygulamaları durdurur; Steam da onlarca süreç başlatır. Bu değişmeden oyunlar hata vermeden kapanır. + Benim için düzelt + “Alt süreç kısıtlamalarını devre dışı bırak” seçeneğini açın, sonra buraya dönün. Geliştirici seçeneklerini açık bırakın, yoksa anahtar yeniden kapanır. + Geliştirici seçeneklerini aç + Bu Android sürümünde bunun için bir anahtar yok, bu yüzden DroidDeck bunu Kablosuz hata ayıklama üzerinden değiştirir. Yaklaşık bir dakika sürer, bilgisayar gerekmez. + Kablosuz hata ayıklama Wi-Fi gerektirir. Herhangi bir ağ işe yarar; internet gerekmez. + Eşleştirme kodu bir bildirime gelir. DroidDeck için bildirimlere izin verin veya Diğer yollar\'ı kullanın. + Bildirimlere izin ver + Ayarlar → Geliştirici seçenekleri → Kablosuz hata ayıklama\'yı açın + “Eşleştirme koduyla cihaz eşle” seçeneğine dokunun + Kodu DroidDeck bildirimine yazın + Eşleştirme açılır penceresi bekleniyor… + Açılır pencere bulundu. Kodunu bildirime girin. + Durum + Her 2 saniyede bir kontrol ediliyor. Kapandığında bu sayfa kapanır. + Diğer yollar + Kablosuz hata ayıklama, elle adres girme, bilgisayardan ADB + Elle adres girme, bilgisayardan ADB, Geliştirici seçenekleri + Bunun yerine Kablosuz hata ayıklamayı kullan + Bazı ROM\'lar bu anahtarı yine de ekler. Geliştirici seçenekleri\'nde “alt süreç” ifadesini arayın. + Bildirim işe yaramazsa açılır penceredeki adresi ve kodu kendiniz girin. + Adresi elle girin + ADB\'li bir bilgisayardan: + Komutu kopyala + Android Ayarları\'nın hangi ekranda açılacağını seçin. + Ana ekran + Eşleşti. Kablosuz hata ayıklama bağlantı noktası bulunuyor… + Eşleşti. Bağlantı noktasını Ayarlar\'dan girin. + Cihaz bulundu. Ayar uygulanıyor… + Otomatik olarak uygulanamadı. Aşağıdaki adresi kontrol edip yeniden deneyin. %1$s + Kablosuz hata ayıklama + Alt süreç sınırı güncellendi + Alt süreç sınırını değiştir + Seçenek yoksa Kablosuz hata ayıklama üzerinden eşleyin. + Eşleşti. IP adresini ve bağlantı noktasını Kablosuz hata ayıklama\'da kontrol edin. + Alt süreç sınırı güncellendi. + Geliştirici seçenekleri\'nde alt süreç ayarı yoksa 2. adımı kullanın. Geliştirici seçenekleri → Kablosuz hata ayıklama → Eşleştirme koduyla cihaz eşle\'yi açın. + Eşleşti. Aşağıdaki adresi kullanın veya Kablosuz hata ayıklama → IP adresi ve bağlantı noktası\'ndan güncelleyin. + Alt süreç sınırı Kablosuz hata ayıklama üzerinden güncellendi. + Android sınırın açık olduğunu onayladı. + Android sınırın kapalı olduğunu onayladı. + 2 · Kablosuz hata ayıklamayı eşle + Eşleştirme açılır penceresinin IP adresi ve bağlantı noktası + Eşleştirme kodu (PIN) + 6 hane + Eşle + 2 · Kablosuz hata ayıklama üzerinden uygula + Bağlantı IP adresi ve bağlantı noktası + Yeniden eşle + Sınırı kapat + Değiştirmenin diğer yolları + 1 · Geliştirici seçenekleri (önce bunu deneyin) + “Alt süreçleri kısıtla” seçeneğini açık olarak ayarlayın. + “Alt süreçleri kısıtla” seçeneğini kapalı olarak ayarlayın. + 3 · Bilgisayardan ADB (son yöntem) + Yalnızca Kablosuz hata ayıklama kullanılamıyorsa. + 3 · Bilgisayardan ADB (son yöntem) + Dokunmatik kontrollerin bir oturumda ne zaman görüneceği + Renk + Ekrandaki düğmelerin rengi + Özel + Opaklık + %1$d%% + Düğme boyutu + %100 standart boyuttur + Çubuk tıklaması + L3 veya R3 için bir çubuğa çift dokunup basılı tutun + Uyarlanabilir çubuklar + Oyun titreşimi + Steam düğmesi + QAM düğmesi + Klavye düğmesi + Steam menüsünü açmak için Select\'e basın + Görünüm/Select yerine. + Animasyonlar + Çubuklar dokunduğunuz yerde görünür, bıraktığınızda gizlenir + Düzen + Özel konumlar kaydedildi + Bu ekrana uyarlandı + Düzenle + Sıfırla + Düğme eşlemesi + Her düğme kendi girdisini gönderir + %2$d düğmeden %1$d\'i yeniden eşlendi + Yapılandır + Kumandayı sıfırla + Tüm kumanda ayarlarını ve düzeni geri yükler + Ekrandaki her düğmenin oyuna gönderdiği girdi. Gizli, düğmeyi kaldırır. + Ekrandaki düğmeler + Varsayılanlar + Eşlemeyi sıfırla + Android uygulamaları + %1$d uygulama + Başlatılabilir Android uygulaması bulunamadı. + ● Masaüstü yüklü + İlk açılışta yüklenir + Yüklenebilir + Yüklü değil. + %1$s · yüklü değil + %1$s\'i aç + Oyunlar + Bir ROM\'lar klasörü seçin. + RetroArch\'ta /root/ROMs\'a göz atın. + %1$s oyunlarını ROMs/%2$s\'e ekleyin. + %1$s\'i yükle + Yüklemek için oturumu durdurun + Çalışma ortamı gerekli + Yükleme ayrıntıları yükleniyor… + Yükleme ayrıntıları kullanılamıyor. Daha sonra tekrar deneyin. + Önce Kurulum\'dan Linux çalışma ortamını yükleyin. + Masaüstü uygulamalarını yüklemek için çalışan oturumu durdurun. + %1$s\'i Linux masaüstüne yükler. + %1$s · %2$d%% + Gizlemek onu Masaüstü\'nden kaldırır. Dosyaları Linux çalışma ortamında kalır. + Bu oyun artık ROM\'lar klasöründe değil. + Masaüstü · %1$s + %1$s\'de başlat + %1$s\'de de var + Bu sayfa artık yok. Soldan bir bölüm seçin. + Masaüstünü aç + Masaüstünü yükle ve aç + Masaüstünü kaldır + Masaüstünü ve birlikte gelen uygulamaları kaldırır. Oyunlar, eklenen uygulamalar, ayarlar ve paylaşılan kitaplıklar saklanır. + Masaüstü kaldırılıyor + Masaüstü kaldırıldı + Eski LXQt dosyaları kaldırılıyor + Bu masaüstü sürümünü kaldırmadan önce DroidDeck\'i güncelleyin. + Linux masaüstü + KDE Plasma, Firefox ve emülatörleriniz + Steam\'de yüklediğiniz oyunlar oturumdan sonra burada görünür. + Steam\'de oyna + Başlatma ayarları + Başlat + Oyun dosyaları + Proton öneki + Kayıtlar aranıyor… + Ada göre bir kayıt klasörü bulunamadı. Dışa aktarma tüm kullanıcı klasörünü alır. + %1$d kayıt klasörü + %1$s · %2$d dosya · %3$s + %1$d KB + Kayıtları yönet + Oyun kayıtları + Kayıtları içe aktar… + GameHub zip\'i olarak dışa aktar… + Winlator zip\'i olarak dışa aktar… + Kayıt klasörünü aç + Hiç oynanmadı + Çalışma ortamı meşgul · %1$d%% + Çalışma ortamı meşgul + Çalışma ortamı ilk Oyna\'da yüklenir + Çalışma ortamı güncellemesi var + ● Çalışma ortamı hazır + Son oynanma %1$s + Dahili depolama + Eklenen oyun + Özel oyun + Oyun depolama alanı + Bu GPU için en iyi çift, güncel tutulur + Çifti kendiniz seçin + Sürücüler kontrol ediliyor… + Henüz listelenen sürücü yok. Aramak için yenileyin. + Diğer GPU\'lara ait sürücüleri gizle + Diğer GPU\'lara ait sürücüleri göster + Gelişmiş + Ekran sürücüsüyle birlikte ayarlanır: %1$s ikisini de taşır. + Çalışma ortamı sürücüsüyle birlikte ayarlanır: %1$s ikisini de taşır. + Oyunlar ve masaüstü tarafından kullanılır. + Çalışma ortamı varsayılanı + Kareleri ekrana çizer. DroidDeck yeniden başlatıldıktan sonra uygulanır. + Otomatik - GPU\'ya göre seçilir + İndir, içe aktar veya sil… + KULLANIMDA + DroidDeck\'e yerleşik sürücüler + Çalışma ortamı ve ekran sürücüleri eşleşen bir çift olarak birlikte ayarlanır. + Bu GPU için önerilir: %1$s. + Bu GPU için sürücü aramak için yenileyin. + ÖNERİLİR + Kullanılamıyor + Linux çalışma ortamı + Linux çalışma ortamı hazırlanıyor + Oturum + Ekran ve ses başlatılıyor + Steam istemcisi + Steam ve uyumluluk araçları kontrol ediliyor + Steam açılıyor + Steam\'in ilk karesi bekleniyor + Masaüstü + Masaüstü hazırlanıyor + Açılıyor + İlk kare bekleniyor + Steam başlatılıyor + Oturum sona erdi + Adım %1$d / %2$d + Steam duraklatıldı + Tutamaklar + Sol taraf + Sağ taraf + Her ikisi + Oyun kolu + Çubuk + Dokunmatik yüzey + Çubuk ile dokunmatik yüzey arasında geçiş yap + Diğer Deck kontrolleri + PANELLER + İkinci ekran kontrollerini kapat + Steam menüsünü aç + Sol · üst + Sol · alt + Sağ · üst + Sağ · alt + Tıkla + Sağ dokunmatik yüzey + Sol dokunmatik yüzey + Dokunmatik yüzey + Klavyeyi göster + Klavyeyi gizle + Linux terminali + Linux oturumu henüz terminal için hazır değil. + Steam + Oyunlar + Masaüstü + Mağaza + Bileşenler + Uygulamalar + Kurulum + Güncellemeler + %%%1$d + Devam et: %1$s + Her oturumda /root/ROMs olarak görünür. Dahili depolama /root/Storage konumundadır. + Klasör seçilmedi + Sonraki oturumda uygulanır. SD kartlar da çalışır. + Klasör seç + Klasörü değiştir + Bir ARM64 Proton derlemesi yükleyin, ardından Steam → Özellikler → Uyumluluk bölümünde oyun bazında seçin. + Uyumluluk aracı yüklemek veya kaldırmak için çalışan oturumu durdurun. + Mevcut derlemeler + Yüklü: %1$s + Önceki bir istekten bekliyor + Yüklü değil + Güncelleme mevcut → %1$s + Eski %1$s sürümlerini kaldır + %1$s kaldırılır. Bu sürümlere ayarlı oyunlar başka bir Proton sürümüne geçirilmelidir. + Bu sürüme ayarlı oyunlar başka bir Proton sürümüne geçirilmelidir. + Şimdi yükle + Sıradakini iptal et + Android klavyesi + Gizle ✕ + Başlat: %1$s + Bir ekran seçin. İkincil: %1$s. + İkincil ekran artık kullanılamıyor. + Birincil ekran + İkincil ekran + Önceki sekme + Sonraki sekme + Açık + Tümü (%1$d) + Hiçbiri + %1$d / %2$d + Kapalı + Alt ekran + Oyun bir sonraki başlatıldığında uygulanır. Oyunun profili ortak profili geçersiz kılar. + Uygulama kimliğiyle başka bir oyun… + Bitti + Varsayılan + Kapalı + Değiştir… + Değişkeni değiştir + Varsayılana dön + Proton oyunları + Pencere içinde pencere + Otomatik olarak PiP moduna geç + Açılmış ekrandaki kontroller + Cihaz açıkken oyun, Steam Deck dokunmatik yüzeylerinin ve arka tutamaklarının üstünde durur. Katlıyken oyun tam ekran olur. + Askıya alındı + Pencere içinde pencere, oturum hazır olduğunda kullanılabilir. + Android ayarlarında DroidDeck için pencere içinde pencereye izin verin. + Android, pencere içinde pencereyi açamadı. + Kısayol + Ana ekrana ekle + Oyun seç + Yüklü oyunlar taranıyor… + Başlatma bağlantısını kopyala + Başlatma bağlantısı kopyalandı + Bu başlatıcı kısayol ekleyemiyor. Bunun yerine “Başlatma bağlantısını kopyala” seçeneğini kullanın. + Geçersiz oyun başlatma bağlantısı. + Bu oyun artık DroidDeck kütüphanenizde değil. + Bir Steam oyunu başlatmadan önce mevcut oturumu durdurun. + Oyun Steam\'e gönderilemedi. Yeniden başlatmayı deneyin. + Kaldırmayı yeniden dene + Kaldırılıyor + Kaldırma tamamlanmadı + Oyun dosyasını dışa aktar + Tüm oyun dosyalarını klasörle eşitle + Oyun dosyalarını eşitlemeyi durdur + Ön yüz oyun dosyaları için bir klasör seçin + Oyun dosyaları şuraya kaydedildi: %1$s + Ön yüz dosyaları + + Çalışma ortamı işleminin bitmesini bekleyin + Uyumluluk aracı yüklemesinin bitmesini bekleyin + Masaüstü uygulaması yüklemesinin bitmesini bekleyin + Oyun başlatmadan önce yüklemenin bitmesini bekleyin + Önce Steam oturumunu kapatın; böylece oyun, içe aktarılan kayıtların üzerine yazamaz + İçe aktarılıyor: %1$s + + %2$s içinden %1$d dosya %3$s oyununa aktarıldı + %2$s içinden %1$d dosya %3$s oyununa aktarıldı + + + %2$s içinden %1$d dosya %3$s oyununa aktarıldı. Eski kayıtlar Download/DroidDeck/Saves/backups klasörüne yedeklendi + %2$s içinden %1$d dosya %3$s oyununa aktarıldı. Eski kayıtlar Download/DroidDeck/Saves/backups klasörüne yedeklendi + + %1$s için bir kayıt zip dosyası seçin + Dışa aktarılıyor: %1$s + + %1$d dosya %2$s olarak dışa aktarıldı: %3$s + %1$d dosya %2$s olarak dışa aktarıldı: %3$s + + %1$s kayıtlarının konumunu seçin (%2$s) + İşlem başarısız (%1$s): %2$s + Henüz oturum günlüğü yok: önce bir oturum çalıştırın. + Temizlenecek oturum günlüğü yok. + + %1$d oturum günlüğü klasörü temizlendi. + %1$d oturum günlüğü klasörü temizlendi. + + Kablosuz hata ayıklama eşlemesi başarısız oldu + Kablosuz hata ayıklama komutu başarısız oldu + Kablosuz hata ayıklama kullanılamıyor + ADB komutu kopyalandı + ROM klasörünü seçin + Adreno GPU değil + Turnip, Adreno GPU\'larını destekler. %1$s üzerinde Steam siyah ekran gösterebilir. İndirme: %2$.0f MB. + Yine de yükle + Linux çalışma ortamını kaldır + Bu işlem çalışma ortamını, içindeki Steam istemcisini ve oraya yüklenen tüm oyunları siler. + Uygulandı: %1$s + %1$s birincil ekranda açılamadı + %1$s, %2$s üzerinde açılamadı + %1$s, %2$d numaralı ekranda açılamadı + İçe aktarılıyor + Değiştiriliyor + Geri yükleniyor + İptal ediliyor + Siliniyor + dosya okunamıyor + İçe aktarıldı: %1$s + Bekleyen değişim iptal edildi. + Bir bileşen paketi seçin (-linux .wcp) + Nightlies kaynağına ulaşılamadı + Kaydedildi: %1$s + İndirme başarısız oldu: %1$s + Bir Linux çalışma ortamı sürücüsü seçin (-Linux zip veya Android + Linux paketi) + Bir ekran sürücüsü seçin (AdrenoTools zip veya Android + Linux paketi) + Bir sürücü zip dosyası veya Android + Linux paketi seçin + %1$s: %2$s + %1$s kaldırılıyor… + %1$s unutuluyor… + Oyun depolama klasörünü seçin + Bir Decky eklentisi ZIP dosyası seçin + Kullanılamaz: %1$s + Program + Linux çalışma ortamı kaldırılıyor + Ayarlar alt ekranda açılamadı; ana ekranda açıldı. + Ayarlar\'da Geliştirici seçeneklerini açın + + Android\'in “Bilinmeyen uygulamaları yükle” ayarı açılamadı + Kontrol edilemedi: %1$s + Bu sürümde DroidDeck\'in bu kopyası için indirme yok + Önce çalışan oturumu durdurun, ardından güncelleyin + İndiriliyor + Yükleniyor + Onayınız bekleniyor + Güncelleme başarısız oldu + Android güncellemeyi yüklemeyi önermedi + + Decky yüklemesi başarısız oldu: %1$s + Eklenti içe aktarma başlatılıyor… + Eklenti içe aktarma başarısız oldu: %1$s + Eklenti yüklendi. Etkinleştirmek için Steam\'i yeniden başlatın. + + Henüz kontrol edilmedi - yeni sürücüleri aramak için yenile düğmesine dokunun + Android + Linux: ekran sürücüsünü de birlikte ayarlar + Android + Linux: çalışma ortamı sürücüsünü de birlikte ayarlar + çalışma ortamına yerleşik Turnip + sistem Vulkan sürücüsü: bu GPU için dahili derleme yok + %1$s (dahili) + Manuel + %1$s · %2$s + Şu anda yalnızca bir yarısı yayımlanmış + İkisi için %1$.0f MB + Ayarlanacak sürücü yok: %1$s + En son sürücüler kontrol ediliyor… + Güncel (kontrol edilemedi: %1$s) + Önerilen sürücüler şu anda yayımlanmış değil + %1$s %2$s indiriliyor… + %1$s %2$s sürümüne güncellendi. + %1$s %2$s sürümüne güncellendi. Ekran sürücüsü, DroidDeck yeniden başlatıldıktan sonra uygulanır. + %1$s %2$s sürümüne geçildi. + %1$s %2$s sürümüne geçildi. Ekran sürücüsü, DroidDeck yeniden başlatıldıktan sonra uygulanır. + Kullanılan: %1$s %2$s. + Kullanılan: %1$s %2$s. Ekran sürücüsü, DroidDeck yeniden başlatıldıktan sonra uygulanır. + Sürücü zip dosyası değil: ne AdrenoTools sürücüsü, ne -Linux Turnip ne de Android + Linux paketi + İçe aktarma başarısız oldu: %1$s + İçe aktarılan sürücü + %1$s hem çalışma ortamı hem de ekran sürücüsü olarak kullanılıyor. + %1$s hem çalışma ortamı hem de ekran sürücüsü olarak kullanılıyor. Ekran sürücüsü, DroidDeck yeniden başlatıldıktan sonra uygulanır. + Silindi: %1$s (iki sürücü de) + Silindi: %1$s + En son: %1$s · %2$s kontrol edildi + En son: %1$s · %2$s ulaşılamıyor · %3$s kontrol edildi + + %1$d dk önce + %1$d dk önce + + + %1$d sa önce + %1$d sa önce + + + %1$d gün önce + %1$d gün önce + + Banners-Turnip, WinNative ve DroidDeck kontrol ediliyor… + kontrol başarısız oldu + %1$s yüklendi - iki sürücüyü de ayarlamak için listelerden birinde seçin + %1$s yüklendi - menüden seçin + + Çalışma ortamının kaldırılmasının bitmesini bekleyin + Oyun + dokunmatik yüzey + doğrudan + Değiştirme başarısız oldu: %1$s + Profil kaydedilemedi: %1$s + Masaüstü duraklatıldı + %1$s duraklatıldı + Program duraklatıldı + Masaüstü başlatılıyor + %1$s başlatılıyor + Program başlatılıyor + Bu oturum için günlük yok. + Oturum günlüğü arşivi oluşturulamadı. + Oturum günlükleri paylaşılamadı. + Oturum başlatılıyor… + Linux çalışma ortamı indiriliyor + Linux çalışma ortamı indiriliyor · %1$d / %2$d MB + Linux çalışma ortamı kontrol ediliyor + Linux çalışma ortamı ayıklanıyor + masaüstü indiriliyor + masaüstü indiriliyor · %1$d / %2$d MB + masaüstü kontrol ediliyor + masaüstü ayıklanıyor + Proton Experimental (ARM64) indiriliyor + Proton Experimental (ARM64) indiriliyor · %1$d / %2$d MB + Proton Experimental (ARM64) kontrol ediliyor + Proton Experimental (ARM64) ayıklanıyor + Çalışma ortamı kataloğuna ulaşılamadı. Bağlantıyı kontrol edip yeniden Oyna\'ya basın. + Linux çalışma ortamı yüklenmedi. Kurulum bölümünde çalışma ortamını kontrol edip yeniden Oyna\'ya basın. + Masaüstü yüklenmedi (%1$s). Bağlantıyı kontrol edip yeniden Masaüstü\'ne basın. + %1$s beklenmedik şekilde durdu. Günlükleri bir hata raporuyla paylaşın veya yeniden deneyin. + Program beklenmedik şekilde durdu. Günlükleri bir hata raporuyla paylaşın veya yeniden deneyin. + Masaüstü beklenmedik şekilde durdu. Günlükleri bir hata raporuyla paylaşın veya yeniden deneyin. + Çıkış durumu %1$d · %2$s + günlük yok + + Günlükte %1$d “Function not implemented” hatası görünüyor: proot\'un seccomp hızlandırması bu cihazda bir yardımcı işlemi bozuyor. %2$s → “%3$s” (veya “%4$s”) seçeneğini deneyip yeniden başlatın. + Günlükte %1$d “Function not implemented” hatası görünüyor: proot\'un seccomp hızlandırması bu cihazda bir yardımcı işlemi bozuyor. %2$s → “%3$s” (veya “%4$s”) seçeneğini deneyip yeniden başlatın. + + + Eklentiler + Sonraki Steam oturumu için bir eklenti ZIP dosyası yükleyin. + İndiriliyor… + ZIP dosyasından yükle + Oyunlar + Ses ve Video + Grafik + İnternet + Ofis + Geliştirici + Eğitim + Bilim + Sistem + Araçlar + Popüler + Gündemde + Yeni + Güncellenen + yükleniyor… + Flatpak kurulumu + %1$s: %2$s + Flathub\'da güncellemeler kontrol edilemedi + yüklü + klasör + Grafit + Grafit yüzeyler; seçilen, odaklanan veya basılan her şey için logonun kendi küre mavisi. + Siyah üzerine kâğıt + Ana renk beyaz; simgenin mavisi çizgiler, halkalar ve noktalar için vurgu rengi. + Simge mavisi + Siyah zemin, beyaz yazı; seçilen, odaklanan veya basılan her şey için mavi. + Elektrik laciverti + Daha parlak mavi; gri yerine laciverte çalan yüzeylerde. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Boşluk + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Önce çalışma ortamını yükleyin + Steam kütüphanesi kullanılamıyor + Önceki SD kütüphanesi kullanılamıyor + Oyun depolama alanı kullanılamıyor + Eklenen oyunlar klasörü kullanılamıyor + Steam manifest dosyaları kullanılamıyor + Birçok sistem + Dahili + Linux sistemi + SD kart + Depolama + Seç + Özellikler + Favorilere ekle + Favorilerden kaldır + Yeniden adlandır + Kopyala + Kes + İşlemler + Favoriler + Geçerli klasörü sabitle + Henüz favori yok - buraya hızlıca dönmek için bir klasörü ⋮ menüsünden sabitleyin. + Favorilerden kaldır + 0 bayt + bayt + Konum + Tür + Boyut + Değiştirilme + Klasör + Dosya + %1$s dosyası + Salt okunur + Oyunların (ve Wine\'ın) bu dosyanın üzerine yazmasını veya onu silmesini engeller. + Salt okunur ayarı değiştirilemedi + Gizli + Bu depolama alanı gizli işaretini saklayamaz. + Dosyayı Windows\'ta gizli olarak işaretler (Wine\'ın gizli özniteliği). + Bu depolama alanında Gizli ayarı değiştirilemedi + Bitti + Siliniyor... + Silinemedi + Taşınıyor... + Kopyalanıyor... + Taşınıyor - %1$s + Kopyalanıyor - %1$s + Taşınıyor %1$d/%2$d - %3$s + Kopyalanıyor %1$d/%2$d - %3$s + %1$d tamamlandı, %2$d başarısız + %1$d tamamlandı, %2$d atlandı + + %1$d öğe taşındı + %1$d öğe taşındı + + + %1$d öğe kopyalandı + %1$d öğe kopyalandı + + “%1$s” zaten var + Yeniden adlandırılıyor... + Yeniden adlandırılamadı + Klasör oluşturuluyor... + Klasör oluşturulamadı + Yeni Klasör + Klasör adı + Oluştur + Yeni ad + Silinsin mi? + “%1$s” kalıcı olarak silinsin mi? + + %1$d öğe silinsin mi? + %1$d öğe silinsin mi? + + Bu işlem geri alınamaz. + + …ve %1$d tane daha + …ve %1$d tane daha + + Siliniyor %1$d/%2$d - %3$s + + %1$d öğe silinemedi + %1$d öğe silinemedi + + Birleştir, mevcut klasörün içine dosya ekler ve dosyaları değiştirir. + Üzerine yaz, mevcut dosyayı değiştirir. + Tüm çakışmalara uygula + Birleştir + Üzerine yaz + İkisini de koru + Atla + Bu klasörü seç + Geri + %1$s bağlı ancak şu anda okunamıyor + Liste görünümü + Izgara görünümü + Ara + Sırala + Ad + Değiştirilme tarihi + Geniş satırlar + Kompakt satırlar + Gizli dosyaları gizle + Gizli dosyaları göster + Favorileri gizle + Favorileri göster + Bu klasörü filtrele + %1$s boş + %1$d seçildi + Hiçbiri + Tümü + Yapıştır + + %1$d öğe + %1$d öğe + + Buraya yapıştır: %1$s + Buraya yapıştır (taşı): %1$s + Dosyalar + DEPOLAMA + HIZLI ERİŞİM + FAVORİLER + İndirilenler + İndirilen + ROM\'lar + Oturum günlükleri + Resimler + “%1$s” Favorilere eklendi + “%1$s” Favorilerden kaldırıldı + Boş klasör + Bir üst düzey + Bir klasör seçin + Bir dosya seçin + Steam mavisi + Gök mavisi + Beyaz + Mor + Yeşil + Kehribar + Kırmızı + Pembe + A düğmesi + B düğmesi + X düğmesi + Y düğmesi + Sol omuz tuşu + Sağ omuz tuşu + Sol tetik + Sağ tetik + Görünüm düğmesi + Menü düğmesi + Görünüm + Menü + Yön tuşu yukarı + Yön tuşu aşağı + Yön tuşu sol + Yön tuşu sağ + Gizli + Taşımak için sürükleyin + Hizala + Kaydet + Hiçbiri + Klavye + dokunmatik yüzey + Terminal + Deck tutamakları + dokunmatik yüzeyler + + Linux çalışma ortamı kaldırılıyor + + Linux çalışma ortamı kaldırılıyor · %1$d öğe + Linux çalışma ortamı kaldırılıyor · %1$d öğe + + Linux çalışma ortamı kaldırıldı + Linux çalışma ortamının kaldırılması tamamlanamadı. Kurulum bölümünden kaldırmayı yeniden deneyin. + Çalışma ortamı kalıntıları kaldırılıyor + İndiriliyor + Doğrulanıyor + Ayıklanıyor + Linux çalışma ortamı yüklenirken veya kaldırılırken gösterilir + Linux çalışma ortamı yükleniyor + Linux çalışma ortamı kaldırılıyor + + Linux çalışma ortamı yüklü değil + Katalogda %1$s için sağlama toplamı yok + Sağlama toplamı uyuşmuyor - hiçbir şey değiştirilmedi + %1$s yükleniyor + AppImage yerleştirilemedi + Ayıklama başarısız oldu + Yükleme başarısız oldu + + Flatpak kurulumu başarısız oldu (çıkış %1$d) + Flatpak kurulumu tamamlanmadı + Başka bir mağaza görevi çalışıyor + Önce Flatpak\'i kurun + Başlatılıyor + Flatpak %1$d durum koduyla çıktı + %1$s yükleniyor + %1$s güncelleniyor + %1$s kaldırılıyor + %1$s yükleniyor (%2$d/%3$d) + %1$s güncelleniyor (%2$d/%3$d) + %1$s kaldırılıyor (%2$d/%3$d) + çeviriler + grafik sürücüleri + medya kodekleri + %1$s çalışma ortamı + %1$s çalışma ortamı %2$s + + Dosya, AppImage olamayacak kadar küçük + Dosya okunamıyor: %1$s + Bu bir AppImage değil (Linux programı değil) + Bu AppImage ne ARM64 (aarch64) ne de x86 bilgisayarlar için derlenmiş + Bu bir Linux programı ama AppImage değil + Yalnızca Tür 2 AppImage dosyaları içe aktarılabilir (bu dosya Tür %1$d) + Yeterli boş alan yok: içe aktarma için yaklaşık %1$s gerekiyor + Yeterli boş alan yok: güncelleme için yaklaşık %1$s gerekiyor + İçe aktarma başarısız oldu + Bu AppImage artık yok + Güncelleme başarısız oldu + %1$s ayıklanıyor + AppImage ayıklanamadı + AppImage ayıklanamadı: %1$s + + Decky Loader\'ı yüklemeden önce etkin oturumu durdurun + Decky Loader indirilemedi + Decky Loader boyutu yayın meta verileriyle eşleşmedi + Yayının sağlama toplamı indirilemedi + Yayının sağlama toplamı eksik + Yayının sağlama toplamı geçersiz + Decky Loader sağlama toplamı uyuşmuyor + Yayın dosyası uyumlu bir 64 bit Linux çalıştırılabilir dosyası değil + İndirme sırasında bir oturum başladı; Decky Loader\'ı yüklemeden önce durdurun + PluginLoader çalıştırılabilir olarak işaretlenemedi + Steam CEF uzaktan hata ayıklaması etkinleştirilemedi + PluginLoader hazırlanamadı + Mevcut PluginLoader korunamadı + Yeni PluginLoader etkinleştirilemedi + Bir eklenti yüklemeden önce etkin oturumu durdurun + Bir eklenti içe aktarmadan önce Decky Loader\'ı yükleyin + Seçilen ZIP dosyası okunamadı + Decky eklentiler dizini oluşturulamadı + Eklenti ZIP\'i ayıklanıyor + Eklenti ZIP\'i en üst düzeyde tek bir eklenti klasörü içermelidir + Eklenti ZIP\'inde plugin.json, package.json veya dist/index.js eksik + Eklenti adı eksik + Yükleme sırasında bir oturum başladı; bir eklenti yüklemeden önce durdurun + Eklenti yüklendi · yüklemek için Steam\'i yeniden başlatın + Eklenti ZIP\'i yüklemesi başarısız oldu + Eklenti ZIP\'i çok fazla öğe içeriyor + Eklenti ZIP\'i geçersiz bir yol içeriyor + Eklenti ZIP\'i yinelenen yollar içeriyor + Eklenti ZIP\'i güvenli olmayan bir yol içeriyor + Eklenti klasörü oluşturulamadı + Eklenti ZIP\'i 1 GiB sınırının üzerine çıkıyor + Pakete dahil eklenti ikili dosyası çalıştırılabilir olarak işaretlenemedi + Geçersiz remote_binary girdisi + Geçersiz uzak ikili dosya adı + Uzak ikili dosya URL\'si HTTPS kullanmalıdır + Uzak ikili dosya %1$s geçersiz bir SHA-256 karmasına sahip + Eklenti bin dizini oluşturulamadı + Eklenti ikili dosyası indiriliyor · %1$s + Eklenti ikili dosyası %1$s indirilemedi + Eklenti ikili dosyası sağlama toplamı uyuşmuyor · %1$s + Eklenti ikili dosyası çalıştırılabilir olarak işaretlenemedi · %1$s + Eklenti ikili dosyası değiştirilemedi · %1$s + Eklenti ikili dosyası yüklenemedi · %1$s + Eklenti klasörü başka bir eklentiye ait + Mevcut eklenti korunamadı + İçe aktarılan eklenti etkinleştirilemedi + Decky eklenti ayarları güncellenemedi + Mevcut eklenti geri yüklenemedi + + Kablosuz hata ayıklama kimliği kaydedilemedi + ADB komutu başarısız oldu (%1$d) + Android, alt süreç sınırının değiştirildiğini onaylamadı: %1$s + Kayıtlı bir Kablosuz hata ayıklama eşleşmesi yok + Geçerli Kablosuz hata ayıklama IP adresini ve bağlantı noktasını girin + + Eşleştirme açılır penceresi beklenirken zaman aşımına uğradı. DroidDeck\'ten yeniden deneyin. + Eşleştirme açılır penceresi aranamadı (%1$d). Wi-Fi\'nin açık olduğunu kontrol edin. + Eşleştirme açılır penceresi aranamadı: %1$s + Kodun 6 hanesinin tamamını girin + Eşleştiriliyor… + O kod işe yaramadı. Eşleştirme açılır penceresini yeniden açıp yeni kodunu girin. + Eşleşti. Bağlanıyor… + Eşleşti, ancak Kablosuz hata ayıklama bağlantı noktası bulunamadı. Kablosuz hata ayıklamayı açık tutup yeniden deneyin. + Ayar uygulanıyor… + Kablosuz hata ayıklama komutu başarısız oldu + Kablosuz hata ayıklama kurulumu + Ayarlar açıkken Kablosuz hata ayıklama eşleştirme kodunu ister + Kablosuz hata ayıklamayı eşle + Kablosuz hata ayıklamayı açın, sonra “Eşleştirme koduyla cihaz eşle” seçeneğine dokunun. + Kablosuz hata ayıklamayı açın, sonra “Eşleştirme koduyla cihaz eşle” seçeneğine dokunun. Ayarlar\'dan çıkmayın: kod bu bildirime gelir. + 6 haneli eşleştirme kodu + Kodu gir\'e dokunun ve Ayarlar\'da gösterilen 6 haneyi yazın. + Wi-Fi eşleştirme kodunu girin + Kodu yeniden deneyin + Kodu gir + Steam kuruluyor + Steam hazır + DroidDeck\'e geri dönmek için dokunun. Kablosuz hata ayıklamayı şimdi kapatabilirsiniz. + Kablosuz hata ayıklama kurulumu durdu + + İndirme HTTP %1$d yanıtı verdi + İndirme boyutu yayınlanan APK ile eşleşmedi ve atıldı - yeniden deneyin + İndirme, sağlama toplamıyla eşleşmedi ve atıldı - yeniden deneyin + Güncelleme, %2$s değil %1$s Android paketi içindir + Güncelleme meta verileri DroidDeck\'in yayın imzalama anahtarını belirtmiyor + Android indirilen APK\'yı okuyamadı + İndirilen APK, %2$s değil %1$s içindir + İndirilen APK, yayınlanan %2$d değil versionCode %1$d diyor + Android, yüklü %2$d üzerine versionCode %1$d\'i yüklemez + İndirilen APK\'da imza bilgisi yok + İndirilen APK, DroidDeck\'in yayın anahtarıyla imzalanmamış + Güncelleme kataloğu HTTP %1$d yanıtı verdi + + Oturum başlatılıyor… + %1$d:%2$02d geçti · hâlâ çalışıyor + Steam istemcisi güncellemesi indiriliyor · %1$d%% + Steam istemcisi indiriliyor · %1$s + + Zaten çalışan bir uyumluluk aracı yüklemesi var + Uyumluluk araçlarını yüklemeden önce etkin oturumu durdurun + %1$s için bir ARM64 yayını bulunamadı + %1$s\'i yüklemek için indirme boyutuna ek olarak en az 4 GB boş alan gerekir + %1$s %2$s indiriliyor + İndirme yeniden deneniyor · %2$d denemenin %1$d\'i + İndirme kesintiye uğradı + İndirme başarısız oldu; yeniden deneme devam edebilsin diye yarım dosya tutuluyor + İndirme, yayın boyutunu aştı ve silindi + İndirme tamamlanmadı; devam edilebilir bir yeniden deneme için yarım dosya tutuluyor + İndirme doğrulanıyor + Yayının sağlama toplamı okunamadı; yeniden deneyin + %1$s %2$s sağlama toplamı yayınlamıyor; hiçbir şey yüklenmedi + Sağlama toplamı uyuşmuyor; indirme silindi + İndirme sırasında bir oturum başladı; uyumluluk araçlarını yüklemeden önce durdurun + %1$s yükleniyor + %1$s yüklemesi başarısız oldu (çıkış %2$d) + %1$s yüklenemedi + droiddeck-esync paketi getiriliyor + + Linux yardımcılarıyla + Yalnızca DLL\'ler + Orijinal (son değişimden sonra Proton güncellendi) + Bileşenler\'in dışında değiştirildi + yok + Orijinal %1$s + %1$s\'ten orijinal + %1$s %2$s, üzerinde çalışan oyun kapandığında %3$s\'e geçer. + %1$s %2$s artık %3$s\'te. Bir sonraki oyun başlatıldığında uygulanır. + Bu orijinal paket saklanmıyor + %2$s\'nin %1$s\'i, üzerinde çalışan oyun kapandığında orijinaline döner. + %2$s\'nin %1$s\'i %3$s orijinalinden geri yüklendi. + %1$s, %2$s\'te kullanımda. Önce o Proton\'u başka bir şeyle değiştirin. + %1$s saklanmıyor + %1$s silindi. + Yüklü derlemenin orijinali saklanır: geri dönüş yolu budur. + Saklanmıyor + %2$s\'nin %1$s orijinali silindi. + İndirmeler yalnızca Nightlies yayınlarından gelir + Bu paket listesi sağlama toplamlarından önceki bir tarihe ait - yenileyip yeniden deneyin + Sağlama toplamı uyuşmuyor + + Doğrusal + En yakın + Snapdragon GSR (kalite) + Yalnızca keskinleştirme + Otomatik (ölçeklemeye göre) + + Hiçbir şey uygulanmadı. + Oyun Netliği + Hafif kontrastla keskinleştirme - günlük kullanım için. + Canlı + Renk sonuna kadar zorlanır. Gökyüzü çizgilenmesin diye bant giderme açık. + Sinematik + Daha derin kontrast, daha soğuk renk, yumuşatılmış kenarlar. + Rekabetçi + Gölgeler açılır ve ağır keskinleştirme - önce insanları fark edin. + Uyarlanabilir Keskinleştirme + Yalnızca keskinleştirme, renk dokunulmadan kalır. + Filmsel + Film benzeri eğri, hafifçe kısık. + Arcade + Abartılı ve çarpıcı. + Retro CRT + CRT tüpü ve analog sinyal geçişleri bir arada. + Keskin Büyütme + Panel çözünürlüğünün altında çalıştırmak için. + Temiz Piksel + 2D ve eski oyunlar için net pikseller. + Anime Kenar + Çizgi film konturları ve keskinleştirme. + + Adreno GPU değil + Destekleniyor + Deneysel: sürücüleri test derlemeleri + Test edilen donanımın altında (Adreno 725 ve daha yenisi): çalışmayabilir + Desteklenmiyor: DroidDeck bir Adreno (Snapdragon) GPU\'su gerektirir + + GitHub\'ın hız sınırına ulaşıldı - daha sonra yeniden deneyin + GitHub HTTP %1$d yanıtı verdi + indirme HTTP %1$d yanıtı verdi + Bu sürücü listesi sağlama toplamlarından önceki bir tarihe ait - yenileyip yeniden deneyin + Sağlama toplamı uyuşmuyor - indirme atıldı + Bir AdrenoTools sürücüsü değil: %1$s + meta.json yok + meta.json okunamıyor (%1$s) + bu bir Linux çalışma ortamı sürücüsü (“Linux çalışma ortamı sürücüsü” altında içe aktarın) + bu bir Wayland oyun sürücüsü, bu uygulamanın buna bir kullanımı yok + meta.json bir libraryName belirtmiyor, bu yüzden AdrenoTools\'a hiçbir şey verilemedi + meta.json, zip içinde olmayan %1$s\'i belirtiyor + %1$s 64 bit AArch64 ELF paylaşılan kitaplığı değil + %1$s glibc\'ye bağlanıyor - bir “-Linux” Turnip\'i, “Linux çalışma ortamı sürücüsü” altına ait + Bu zip içinde libvulkan_freedreno*.so yok. Bir Android (AdrenoTools) veya -Wayland Turnip zip\'i bir Linux çalışma ortamı sürücüsü değildir. + %1$s 64 bit AArch64 ELF paylaşılan kitaplığı değil. + %1$s bir glibc sürücüsü değil - Android\'in libc\'sine bağlanıyor. Linux çalışma ortamı bir “-Linux” zip\'i gerektirir; düz veya “-Wayland” bir Turnip, Steam istemcisi tarafından hiç yüklenemez. + Bir sürücü paketi değil: bir android ve bir linux sürücüsü belirten manifest.json yok + Bir sürücü paketi değil: %1$s dışına işaret ediyor + Bir sürücü paketi değil: %1$d MB\'den fazla alana açılıyor + + SD kart + Klasör + + Oturum günlüklerini paylaş + + Android 10 veya daha yenisi gerekir (renk uzayına sahip ekran katmanları) + bu ekran HDR desteği bildirmiyor + bu ekran HDR10 değil, %1$s destekliyor + + bu GPU + Alt süreç sınırı açık + Alt süreç sınırı ayarlanmamış + Alt süreç sınırı kontrol edilemedi + Alt süreç sınırı kapalı + Alt süreç sınırı gerekli değil + %1$s Bu mümkün değilse bu cihazı ADB\'li bir bilgisayara bağlayıp aşağıdaki komutu çalıştırın. Buraya dönüp yeniden kontrol edin. + Steam oturumları başlayabilir. + Bu Android sürümü bu sınırı kullanmaz. + Geliştirici seçenekleri\'nde “Alt süreç kısıtlamalarını devre dışı bırak” seçeneğini açın ve sonrasında Geliştirici seçeneklerini açık tutun. + Bu Android sürümünde bunun için bir anahtar yok. Bu cihazdan değiştirmek için Kablosuz hata ayıklamayı kullanın. + Bu derlemede bu DroidDeck kopyası için bir indirme yok. + Android, versionCode %1$d yüklü %2$d\'nin altında olduğu için bunu bu derlemenin üzerine yüklemez. + Android, daha eski bir sürümü daha yeni birinin üzerine yüklemez. Kanal yetişene kadar bu derlemede kalın. + Bu DroidDeck kopyası farklı imzalanmış (yerel bir derleme mi?), bu yüzden Android onu yerinde güncellemez. + Güncelleme için yeterli boş alan yok. + Güncelleme iptal edildi. + Güncelleme yüklenmedi: %1$s + Güncelleme yüklenmedi. + %1$s zip\'i + zip + %1$s henüz başlatılmadı + %1$s için kayıt dosyası bulunamadı + %1$s\'de drive_c klasörü yok, bu yüzden bir GameHub veya Winlator kayıt zip\'i değil + %1$s içinde klasör oluşturulamıyor + %1$s içine yazılamıyor + %1$s içine yazılamıyor (%2$s) + Klasör kullanılamıyor + Başka bir dosya zaten %1$s\'i kullanıyor + Görsel klasörü kullanılamıyor + Klasörde ilgisiz bir eşitleme envanteri var + %1$s kaldırılamadı + Dosya dışa aktarma klasörünün dışına çıkıyor + FEX (sürüm adı olmayan derleme) + %1$s içinde profile.json yok + %1$s bir Linux bileşen paketi değil (tür %2$s) + %1$s · %2$s boş + Otomatik + Güncelleme kataloğu kontrollerini geçemedi (%1$s) + DroidDeck alt süreç ayarı + Oturum açıldı · çevrimdışı mod + Oturum açıldı + PAKETLİ + İNDİRİLDİ + İÇE AKTARILDI + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Oturum ekranı + Çözünürlük + 16:9 oyun sığdırma + 16:9 oyunların bu ekrana nasıl sığacağı. Sonraki oturumda. + Ekrana Uydur\'u veya 16:9\'dan daha dar özel bir boyutu seçin. + Oranları koru + Ekrana uzat + Görüntü ölçekleme + Ölçekleme filtresi + Steam ve Masaüstü tarafından paylaşılır. + Anında uygulanır. Steam ve Masaüstü tarafından paylaşılır. + FSR, GSR ve NIS, oturum görüntüsü büyütülürken uygulanır. Yalnızca keskinleştirme büyütme olmadan da çalışır. + Filtre keskinliği + Kare hızı + %1$s Ölçeklemeyi %2$s olarak ayarlar. + Ekrana Uydur (%1$d×%2$d) + Ekranı takip et (katlanıp açıldığında yeniden boyutlanır) + Steam + Cihaz ayrıntılarını göster + Cihaz ayrıntılarını gizle + Cihaz okunuyor… + Kopyala + Bu ayrıntıları bir hata raporuna kopyalayın. + Kopyalandı. + SSBS\'i zorla + Wine\'ın sürekli kapattığı bir CPU hız ayarını açık tutar. Snapdragon 8 Elite\'te Windows oyunlarını hızlandırabilir; daha eski yongalarda pek fark görülmez. Bir sonraki oyun başlatmasında uygulanır. + + Mağazalar + Steam + Özel + Mağazalar + Bir mağazayı şurada aç + İndirme hızı katmanı + Mağazalar motoru + ● Motor hazır + Derlenmedi + Dengeli + Hızlı + Maks + Mağazalar + Mağaza ayarları + Ayarlar + (oturum açılmadı) + Mağaza + Yüklü + Kütüphane + Tümü + İndirmeler + %1$s (%2$d) + Oturum aç + %1$s · oturumu kapat + %1$s\'ten oturum kapatılsın mı? + Yüklü oyunlar kalır. Tekrar oturum açtığınızda kütüphane yeniden getirilir. + Oturumu açık tut + Oturumu kapat + %1$s kataloğunda ara… + Yüklü oyunlarda ara… + Kütüphanenizde ara… + Ara + Yüklü bir şey yok + Yükleniyor + Oyun yok + Mağaza yükleniyor… + Katalog yok + Yenilikler + Fırsatlar + Ücretsiz oyunlar + Gündemde + Kütüphaneniz + Oyna + Yükle + Yüklemeyi sürdür + Kesintiye uğradı + Yükle · %1$s + Ücretsiz al + %1$s\'te görüntüle + Satın al · %1$s + Yüklü + Kütüphanede + Ücretsiz + Sürdür + Yenile + Yetişkin içeriği göster + Epic + EOS + Çevrimdışı + Kapalı + Epic oturum açma (EOS) + Çevrimdışı başlat + Epic oturum açmayı düzelt + Bulut kayıtları + Buluta yükle + Buluttan indir + Bulut kaydı yok + Bulut çakışması + Bulut çakışması · %1$d dosya + … + Bulutu koru + Yerel olanı koru + Bulut kontrol ediliyor… + Son eşitleme %1$s + Henüz eşitlenmedi + %1$d dosya + yüklü + kütüphanenizde + mağaza + Başarımlar + %2$d başarımdan %1$d açıldı + %1$d / %2$d + Açık · %1$d + Kilitli · %1$d + Gizli · %1$d + Gizli başarım + Kilidi açıldığında ayrıntılar gösterilir. + Tüm başarımların kilidi açıldı + Yükleniyor… + Başarımlarını görmek için %1$s hesabına giriş yap. + %1$s\'e bağlanılamadı. Çevrimiçi olduğunda tekrar kontrol et. + %1$s bu oyun için başarım listelemiyor. + Tümünü göster (%1$d) + Başarımlar · %1$s + ✓ Steam\'de + Yüklemeyi kaldır + Kaldırılıyor… + %1$s kaldırılsın mı? + %1$s\'lık yer açar ve onu Steam\'den kaldırır. + Dosyalarını ve Steam kısayolunu kaldırır. + Steam bir sonraki başladığında oyunu fark eder; oradan başlatın veya Steam\'i şimdi başlatın. + Proton + Steam\'in oyun başına seçimi + Ses + DirectAudio + Bileşenler + FEX, DXVK, VKD3D + Motor + %1$s · yerel + %1$s · yerleşik + Nasıl çalışır + Şuradan getirildi + %1$s, hesabınızla + Şuraya yüklendi + Oyun depolama alanı (Games/%1$s/%2$s) + Şununla başlatılır + Steam ve Proton, Steam oyunlarıyla aynı yol + Steam ve Proton · her başlatmada Epic oturum açma, EOS kaplaması kapalı + Steam ve Proton · Amazon Games Services değişkenleri ayarlı + Steam + yüklemede Steam\'e eklenir (Deck arayüzü + Steam Input) + İndirme yok + Duraklatıldı + Kuyrukta + Kuyrukta · #%1$d + İptal edildi + Başarısız oldu + Başarısız oldu · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Duraklat + Sürdür + İptal et + Yeniden dene + Temizle + Bitenleri temizle + Manifest + İndirme + Doğrulama + Yükleme + Tamamlandı + Kontrol ediliyor + İndiriliyor + İndirme silinsin mi? + Başarısız oldu · %1$s tutuldu + Doğrulanıyor + Yükleniyor + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d dosya + İndirme yöneticisi + Aynı anda indirme sayısı + Şuraya yükle + %1$s\'i yükle: + Dahili + %1$s boş + Mağaza indirmeleri + Oyun indirme ilerlemesini gösterir ve indirmelerin arka planda sürmesini sağlar + İndiriliyor + %1$d indirme + Hazırlanıyor… + Tamamlanan mağaza indirmeleri + %1$s yüklendi + %1$s indirmesi başarısız oldu + Sil + Tut + %1$s\'e oturum aç + Sağlayıcınızla oturum açın + Yeniden yükle + Kapat + Yeniden dene + Oturum açma tamamlanıyor… + Oturum açma sayfası yüklenemedi: %1$s + Bu sağlayıcı yerleşik bir sayfadan oturum açmayı reddediyor. Bunun yerine mağaza parolanızla oturum açın. + Oturum açma sayfası bir hatayla yanıt verdi (HTTP %1$d). + Oturum açma sayfasına bağlantı güvenli değil. + Oturum açma doğrulanamadı. Yeniden deneyin. + Oturum açma başarısız oldu. Yeniden deneyin. + Kapat + Geri + Düzenle + Bu klasördeki tüm oyunları ekle + Bu klasörde yeni bir şey yok + Ad + Hedef + Başlangıç yeri + Başlatma seçenekleri + Görsel + Diğer exe… + Kaldır + Kapak + Arka plan + Logo + Simge + Oyun klasörü + Steam + SteamGridDB + Dosyanız + Exe\'den + Otomatik + Görsel seç… + Bir görsel seçin: %1$s + Otomatiğe sıfırla + + %1$d oyun eklendi + %1$d oyun eklendi + + + %1$d zaten Oyunlar\'da + %1$d zaten Oyunlar\'da + + B · %1$s geri dön + oyuna + Oyna ayarlarına + Grafik sürücüleri + Bu Proton + Her Proton + Her Proton için + Kullanılabilir · %1$d + Kontrol edildi %1$s + Yeni bileşenleri ve sürücüleri kontrol et + Al + %1$s · aşağıdaki DXVK, VKD3D-Proton ve FEX bu Proton\'a ait + %1$s · test edildi + %1$s · test edilmedi + %1$s · desteklenmiyor + Önerilen çifti güncel tutar + Otomatik · %1$s + Çalışma ortamı ve ekran sürücülerini ayrı ayrı + %1$s kullanılsın mı? + %1$s kaldırılsın mı? + Tanılama + Oynamaya hazır + Henüz oynamaya hazır değil + çalışma ortamı %1$s + alt süreç sınırı kapalı + %1$s olarak oturum açıldı + Sizden bir şey istendiğinde + Görünüm + Bu cihaz + Kütüphane görselleri + LINUX UYGULAMALARI + Flathub + Linux masaüstü için masaüstü uygulamaları, Masaüstü\'nden açılır. + Flathub\'ı kur + %1$s kaldırılsın mı? + Ayarları ve dosyaları saklanır. + Tut + Diskteki boyut + Masaüstü kaldırılsın mı? + Tümü + GOG başarımları + Desteklenen GOG oyunları için başarımları eşitler. Sonraki başlatmada uygulanır. + Başarımlar kapalı + Başarımlar açık + GOG\'a tekrar giriş yap + Başarımlar yüklendi + Başarım yüklemesi beklemede + Mağaza ayarlarında GOG başarımlarını aç + Başarımlar kullanılamıyor; oturum günlüklerini kontrol et + Başarımın kilidi açıldı + Yapıştır + Yön tuşu + Sol analog çubuk + Sağ analog çubuk + Menü + Klavye + Yön tuşu yukarı + Yön tuşu aşağı + Yön tuşu sol + Yön tuşu sağ + Dokunmatik kontroller + Dokunmatik kontrolcü + Fare + Açık + Kapalı + Yavaş + Normal + Hızlı + Düzeni düzenle + DroidDeck menüsü + Kontrolleri ekrana sürükle + Düzenle + Vazgeç + Kaydediliyor… + Simge + G− + G+ + Y− + Y+ + Gizle + Renk ve opaklık… + Bu düzeni kopyala + Düzen kopyalandı; herhangi bir oyunun dokunmatik düzenine yapıştırabilirsin + Kopyalanan düzeni yapıştır + Steam\'in varsayılanına sıfırla + Bu kontrolün simge eklenebilecek bir Steam ataması yok + %1$s için simge + Dokunmatik düzen rengi + Bitti + Dokunmatik düzen kaydedilemedi + Dokunmatik düzen Steam\'e kaydedildi + Uygulama %1$d + Etiket (isteğe bağlı) + Steam\'in simgeleri bulunamadı + + Steam\'den %1$d simge + Steam\'den %1$d simge + + Simge rengi + Düğme rengi + Simge yok + + Oturum açma durumu kontrol ediliyor… + Kitaplığın alınıyor… + Oyun listesi alınıyor… + Oyun bilgileri yükleniyor… (%1$d/%2$d) + + %1$d oyun eşitleniyor… + %1$d oyun eşitleniyor… + + Eşitleniyor… %1$d/%2$d + Kapak görselleri alınıyor… + Eşitleme başarısız oldu + %1$s kitaplığında oyun bulunamadı + Amazon Games kitaplığın için yanıt vermedi: yenile veya yeniden oturum aç + GOG\'a bağlanılamadı + GOG kitaplığın okunamadı + Steam\'e kaydediliyor… + Cardboard VR + Cardboard modu + Telefon VR gözlüğü için sol ve sağ göz görüntüleri. + Görüntü + Sanal ekran + Yan yana 3D + Gözlük + Cardboard + Cardboard (2014) + Lens yok + Göz arası mesafe + Ekran genişliği + Ekran uzaklığı + Kafa takibi + Bu cihazda dönüş sensörü yok. + Görünümü ortala + Ortala + SteamVR (deneysel) + Yükle + Valve\'ın ARM64 SteamVR\'ını indirir (571 MB). + SteamVR indiriliyor ve açılıyor… + Başlat + SteamVR\'ı başınızı başlık olarak kullanarak çalıştırır; görüntü yan yana 3D\'ye geçer. + Durdur + SteamVR çalışıyor. + Bu oturumda kullanılamaz. + SteamVR yanıt vermedi; oturum günlüğünü kontrol et. + Bileşenleri indir + İndirmeler Bileşenler sayfasıyla paylaşılır. Bir sonraki oyun başlatmada kullanmak için yukarıdan indirilmiş bir sürüm seç. + İndirilecek sürümü seç + Geliştirici + Yayıncı + Çıkış + Ana hikâye + + Ekstralar + Tamamlayıcı + %1$s sa + %1$d dk + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 000000000..6bee54cd2 --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,2285 @@ + + + DroidDeck + + Під час першого запуску завантажується Steam. Це може зайняти кілька хвилин. + За замовчуванням вихід із додатка ставить Steam на паузу; коли ви повернетеся, усе продовжиться з того самого місця. + Ваш контролер відображається як геймпад Xbox 360. Якщо контролера немає, скористайтеся екранним керуванням. + + + Під час першого запуску налаштовується робочий стіл. Це може зайняти кілька хвилин. + За замовчуванням вихід із додатка ставить сеанс на паузу; коли ви повернетеся, усе продовжиться з того самого місця. + Екран працює як сенсорна панель: проводьте пальцем, щоб рухати вказівник, і торкайтеся, щоб клацнути. + Ваш контролер відображається як геймпад Xbox 360. Якщо контролера немає, скористайтеся екранним керуванням. + + Сеанс + Відображається, поки відкрито сеанс Steam або робочого столу + Steam працює + Триває завантаження в Steam + Сеанс призупинено — торкніться «Відновити», коли будете готові + Відновити + Призупинити зараз + Завершити сеанс + Генерація кадрів + Виберіть режим генерації кадрів + Вимк. + Win-FG %1$d× + LSFG %1$d× + LSFG адаптивний %1$d + Потрібен Lossless Scaling зі Steam + Установіть Lossless Scaling у Steam або імпортуйте Lossless.dll + Імпортувати Lossless.dll… + Використовується копія зі Steam + Використовується імпортована копія + Виберіть Lossless.dll + Lossless.dll імпортовано + Цей Lossless.dll уже використовується + У цьому обліковому записі Steam немає Lossless Scaling + Цей файл не є Lossless.dll від Lossless Scaling + Не вдалося підготувати LSFG із цього Lossless.dll + Для LSFG потрібен Lossless Scaling зі Steam або імпортований Lossless.dll + LSFG не вдалося підготувати шейдери з Lossless.dll + %1$s: не підтримується + %1$s: збій рушія + %1$s: потрібен Lossless Scaling + %1$s %2$s · запуск + Додає бібліотеку Steam і шукає в ній ігри для Windows. Придбані ігри використовують свої записи в Steam, решта стають ярликами сторонніх ігор. + Середовище гри + Змінні середовища для ігор Proton. + Змінити + Усі ігри Proton + Профіль + ID додатка Steam + Відкрити + Гра %1$s + Додати змінну + Назва змінної + Значення + Зберегти + Видалити + Знову успадковувати + Скинути перевизначення середовища + Не задавати в цьому профілі + Змінних не задано. + Успадковано + Власна змінна + Не вдалося завантажити або зберегти. Перевірте вільне місце й спробуйте ще раз. + Укажіть коректну назву та значення до 8192 символів. + Стандартне середовище Proton не читає цю змінну. + Завантаження… + Перевизначення рівня функцій D3D12. 12_2 повідомляє можливості DirectX 12 Ultimate; непідтримувані функції можуть давати збої. Не задавайте для автоматичного визначення. + Перевизначення моделі шейдерів D3D12. Для 6_9 потрібні досить новий VKD3D і відповідні можливості GPU. Не задавайте для автоматичного визначення. + Залиште кешування шейдерів Mesa ввімкненим, щоб зменшити повторну компіляцію. false вмикає кешування. + Максимальний розмір кешу шейдерів Mesa, наприклад 1G. Більший кеш займає більше місця. + Параметри VKD3D для D3D12. nodxr вимикає трасування променів; використовуйте лише для гри, якій це потрібно. + Установіть 1, щоб вести діагностичний журнал Proton. Журналювання може знизити продуктивність і займає місце. + Установіть 1, щоб використовувати WineD3D на основі OpenGL замість DXVK для перевірки сумісності D3D9–11. + Установіть 1, щоб ця гра не використовувала ntsync (droiddeck-ntsync або драйвер ядра); тоді вона працює на droiddeck-esync, якщо є пакет, інакше на droiddeck-fsync. + Установіть 1, щоб вимкнути droiddeck-esync для цієї гри; замість нього використовується droiddeck-fsync або droiddeck-ntsync, якщо fsync теж вимкнено. + Установіть 1, щоб ця гра не використовувала droiddeck-fsync; тоді вона працює на droiddeck-esync, якщо є пакет, інакше на droiddeck-ntsync. + Установіть 1, щоб запускати цю гру на droiddeck-fsync, або 0, щоб надавати перевагу droiddeck-esync, якщо є пакет для її Proton. + Установіть 0, щоб вимкнути в цій грі помічник Proton для навігації геймпадом. + Оверлей DXVK для D3D9–11. fps показує частоту кадрів; якщо не задано, оверлей приховано. + Параметри DXVK, розділені крапкою з комою. Нові збірки обмежують частоту кадрів через dxvk.maxFrameRate; для звичайної гри краще використовувати обмеження кадрів сеансу. + Обмеження частоти кадрів D3D12. 0 вимикає обмеження. + Багатопотоковість OpenGL у Mesa. true може допомогти іграм OpenGL, які впираються в CPU; перевіряйте для кожної гри окремо. + Синхронізація Wine + Як ігри синхронізують свої потоки. Застосовується в наступному сеансі. + esync + ntsync + fsync + wineserver + Пресет FEX + Застосовується під час наступного запуску гри. + Ігри + Стандартний FEX + Використовує стандартні налаштування FEX. + Стабільність + Найбезпечніші й найповільніші налаштування. + Сумісність + Повний TSO і multiblock. + Середній + Збалансований TSO зі зниженою точністю x87. + Продуктивність + TSO повністю вимкнено; multiblock. + Продуктивність + TSO + Налаштування «Продуктивність» з увімкненим основним TSO. + Екстрим + Вимикає перевірки самозмінного коду. Ігри з JIT і DRM можуть не працювати. + Екстрим + TSO + Налаштування «Екстрим» з увімкненим основним TSO. + Екстрим-gn + «Продуктивність» із підказками TSC і метаданих. Спробуйте його перед «Екстримом». + Denuvo + «Продуктивність» з повною перевіркою коду та прихованим бітом гіпервізора. + Власне значення + Виберіть значення + Емулятори та додатки + Додати + Ваш власний додаток або гра + Скрипт + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Запуск… + %1$s додано + %1$s видалено + Робочий стіл + Цього додатка вже немає. Виберіть його знову на робочому столі. + Відкрити %1$s + Видалити + Натисніть ще раз, щоб видалити + Завершіть сеанс, щоб видалити + Запускає %1$s з його папки, яку підключено до сеансу там, де вона лежить. Видалення додатка не зачіпає цю папку. + Запускає %1$s з копії його папки всередині DroidDeck. Видалення додатка видаляє копію, а не оригінал. + Спершу встановіть середовище Linux + Файлу більше немає + Недостатньо вільного місця: для копіювання папки потрібно %1$s + Копіювання %1$s + Не вдалося додати додаток + Пошук випуску %1$s + Не вдалося прочитати випуски %1$s + Жоден випуск %1$s не містить AppImage для ARM64 або x86_64 + Завантаження %1$s + Помилка завантаження + Не вдалося оновити додаток + Не вдалося прочитати цей значок + %1$s збережено + Змінити + Перевірити оновлення + Перевірка оновлень… + Оновити до %1$s + Актуальна версія · %1$s + %1$s оновлено + %1$s · %2$s + Додавання додатка + Скрипт + AppImage + GitHub + Flatpak + Файл .sh. Запускається зі своєї папки на весь екран через gamescope. + AppImage для ARM64 (aarch64) або для x86_64, що працює через FEX. Його один раз розпаковують у середовище Linux. + Установлює AppImage для ARM64 з найновішого випуску репозиторію або версію для x86_64 через FEX. + Використовувати FEX + Авто + Увімк. + Вимк. + FEX запускає програми Linux для x86 на цьому пристрої ARM64. У режимі «Авто» він вмикається, якщо програму зібрано для x86. + %1$s через FEX + Цю програму зібрано для ПК з архітектурою %1$s, і вона працює через FEX, який ще не налаштовано. Один раз відкрийте Steam, увійшовши в обліковий запис: DroidDeck завантажить FEX і потрібний йому Steam Linux Runtime, після чого програма відкриється. + Ця програма + %1$s зібрано для ПК x86; для запуску потрібен FEX, який ще не встановлено. Один раз запустіть Steam, увійшовши в обліковий запис, дайте йому завантажити FEX і Steam Linux Runtime, а потім запустіть це знову. + ID додатка Flathub або його сторінка на Flathub. Якщо Flatpak ще не налаштовано, його буде налаштовано спочатку. + Файл + Файл не вибрано + Вибрати файл + Змінити + Виберіть скрипт + Виберіть AppImage + Виберіть значок + Репозиторій + власник/репозиторій або посилання на github.com + Це не репозиторій GitHub + ID додатка + org.ppsspp.PPSSPP + Це не ID додатка Flathub + Або файл .flatpak для додатка, якого немає на Flathub. + Виберіть файл .flatpak + Джерело + На вашому робочому столі + Новий додаток + Назва + З додатка + Значок + Стандартний значок + Вибрати значок + Очистити + З репозиторію + Пошук значків… + Стандартний + Змінити %1$s + Зберегти + Перевірка папки… + Папку додатка підключено до сеансу: %1$s + Папка містить програми Linux, які не можна запускати зі спільного сховища, тому її буде скопійовано в DroidDeck (%1$s). + Додати + Скасувати + Спершу встановіть середовище Linux у розділі «Налаштування». + Зачекайте, поки завершиться %1$s. + Сумісно + Збірки з позначкою «%1$s» у меню «Сумісність» Steam мають виправлення префіксів від DroidDeck і запускають тут ігри. За замовчуванням використовується Proton Experimental ARM64 (%1$s) або Proton 11.0 ARM64 (%1$s), щойно встановлено цю новішу збірку. Власні збірки Proton від Valve без цієї позначки не можуть запустити гру на цьому пристрої. + Установлено %1$s · %2$s + Скасувати + Авто + Ніколи + Завжди + Видалити + Перевірити + Оновити + Перевірка… + Виконується… + Забути + Додати… + Використати + Застосовується в наступному сеансі. + Сеанс Steam + Сеанс робочого столу + Екран + Роздільна здатність + До 720p + До 900p + До 1080p + Рідна + %1$s (за замовчуванням) + Власна · %1$d×%2$d + Власна… + З 720p меню можуть реагувати швидше. + Співвідношення сторін + Визначається власною роздільною здатністю. + «Авто» використовує щонайменше 16:9. + Обмеження кадрів + Обмежує весь сеанс. Екран перемикається на частоту оновлення, кратну цьому обмеженню. + HDR + Вивід HDR10 + Недоступно: %1$s. + Перезапустіть додаток, щоб застосувати. + Драйвери + Драйвери GPU + Спільні для Steam і робочого столу. Налаштовуються на сторінці «Компоненти». + Сенсор і керування + Сенсорне введення + Сенсорна панель + Пряме + «Авто»: сенсорна панель на робочому столі, пряме введення в Steam. + Екранне керування + Steam + QAM + «Авто» показує екранне керування, коли контролер не під’єднано. + Сенсорне керування Steam (бета) + Сенсорне керування Steam увімкнеться з наступним сеансом Steam + Розкладка сенсорного керування Steam + Переміщуйте, змінюйте розмір або приховуйте сенсорні елементи керування цієї гри; зберігається в Steam + Контролер + Яким Steam бачить ваш контролер. Застосовується в наступному сеансі. + Контролер Steam Deck + Steam Controller + DualShock 4 + DualSense + Контролер Xbox 360 + Deck: кнопка швидкого доступу й гіроскоп. Steam Controller: те саме плюс датчики хвату на задніх кнопках. DualShock 4 і DualSense: гіроскоп, а трекпади працюють як їхня сенсорна панель. Xbox 360: меню швидкого доступу відкривається комбінацією Guide+A. + Кнопка Back + Меню → QAM + QAM → Меню + Сеанс + Робота у фоні + Коли додаток приховано або екран вимикається. + Вручну + Нативний + «Авто» призупиняє сеанс і відновлює його, коли додаток знову на екрані. «Нативний» спершу готує Steam до сну. «Вручну» чекає на «Відновити». «Ніколи» — сеанс працює далі. + Не переривати завантаження + Відкладає призупинення у фоні, поки Steam активно щось завантажує. + «Авто» призупиняє сеанс у фоні й відновлює, коли додаток знову на екрані. «Вручну» чекає на «Відновити». «Ніколи» — сеанс працює далі. + Запуск + Запускати Steam разом із DroidDeck + Відкриває сеанс Steam під час запуску DroidDeck. + Decky + Пошук випусків… + Можна встановити %1$s. + Сумісну збірку не знайдено. Спробуйте пізніше. + Установлено · %1$s + Доступне оновлення · %1$s + Актуальна версія · %1$s + Завантажувач + Установити останню версію + Запускається разом зі Steam. Поки його ввімкнено, інші додатки на цьому пристрої можуть керувати Steam. + Вимкнено. Порт віддаленого налагодження Steam залишається закритим. + Видалити Decky Loader? + Видаляє завантажувач. Ваші плагіни й налаштування зберігаються. + Клієнт + Оверлей продуктивності + Вимкніть, якщо в режимі Deck ігри вилітають або показують чорний екран. Застосовується в наступному сеансі. + Відновити клієнт Steam + Повторно завантажує клієнт Steam під час наступного запуску Steam. Скористайтеся цим, якщо Steam не виходить з екрана завантаження. Ігри, вхід і налаштування зберігаються. + Клієнт Steam буде повторно завантажено під час наступного запуску Steam. + Відновити + Графіка + Ключ API SteamGridDB + Ваш ключ + Змінити… + Доповнює відсутню графіку зі Steam, а потім із SteamGridDB. Ваші власні cover.jpg, hero.jpg, logo.png або icon.png у папці гри мають пріоритет. + Видалити %1$s + Видалити? + Додати гру + Додайте гру: + Виберіть exe-файл: + Це розташування недоступне в сеансі + Перевірити цей exe-файл + Компоненти Windows + LB / RB — перехід між розділами. + Немає + Бібліотеки DLL Windows, яких немає в Proton, зі списку компонентів. Увімкніть ті, що потрібні цій грі: вони потрапляють у власну папку Windows гри, а не до її файлів, і залишаються, хоч на якому Proton вона працює. + Не вдалося отримати список компонентів. Уже завантажені компоненти можна вмикати й вимикати. + Рекомендовано для цієї гри + Перевірка, що потрібно цій грі… + Доступно, коли є інтернет + + Вмикається %1$d компонент… + Вмикаються %1$d компоненти… + Вмикається %1$d компонентів… + Вмикається %1$d компонента… + + Увімкнено для цієї гри + Steam установлює %1$s разом із %2$s (додаток %3$s) + Зі списку GOG для цієї гри + Із необхідних компонентів Epic для цієї гри + Із кроків установлення Amazon для цієї гри + Steam установлює %1$s разом із додатком %2$s (збіг за назвою) + Також у списку + Є в списку %1$s; тут це не компонент + Steam + GOG + Epic Games + Amazon Games + Гра постачається з %1$s + У грі є %1$s (часто працює як є) + Уже є в цій грі + Ніщо в папці гри не вказує на якийсь компонент. + Установлено для цієї гри (%1$d) + Усі компоненти (%1$d) + Потрібен інсталятор Windows (%1$d) + Це окремі програми встановлення без пакета всередині, який можна розібрати, і для них ще немає запису того, що вони встановлюють. Більшість — старі середовища виконання, які вже замінює Mono у складі Proton; записи додаються в міру створення. + Показати + Приховати + Запускає власну програму встановлення (запису ще немає) + Тут поки не підтримується + Завантаження компонентів… + Не вдалося встановити: %1$s + Завантажено + Завантаження %1$s + Установлення: %1$s + Вимкнення… + %1$s для завантаження + Набуде чинності під час наступного запуску гри. + Розтягувати ігри 16:9 + На екранах, ближчих до квадрата, ніж 16:9, розтягує ігри на весь екран замість смуг. Застосовується в наступному сеансі. + Звук + DirectAudio для ігор + Обходить PulseAudio для меншої затримки. + Мікрофон + Для голосового чату. + Мережа + Пошук Wi-Fi + Показувати в Steam назви Wi-Fi і мережі поблизу. + Геодані вимкнено. + Дозвіл на доступ до геоданих не надано. + Відкрити налаштування + Продовжити + Увімкнути пошук Wi-Fi? + Android вимагає дозволу на доступ до геоданих, щоб читати назви Wi-Fi і шукати мережі поблизу. DroidDeck використовує цей доступ, щоб показувати ці мережі в Steam. Ми не відстежуємо ваше місцезнаходження. Доступ до інтернету працює і без цього дозволу. + Сховище ігор + Автоматично — SD-карта, якщо вставлена, плюс ваші папки Games + Автоматично — ваші папки Games, плюс SD-карта, якщо вставлена (зараз немає) + Лише внутрішня пам’ять + Папка: %1$s + Друга бібліотека + Ігри, які підвантажують ресурси з SD-карти або спільного сховища, можуть працювати ривками. + Діагностика сховища + Записує результати виділення місця та вибіркові заміри швидкості сховища в «Поділитися журналами». Поки ввімкнено, встановлення може сповільнюватися. Застосовується в наступному сеансі. + Вибрати папку… + Власна роздільна здатність + Замінює обмеження та співвідношення сторін. Якщо розмір не збігається з екраном, з’являться смуги. + Ширина + Висота + Від 320×240 до 3840×2160. + + %1$d гра + %1$d гри + %1$d ігор + %1$d гри + + OK + Видалити + Відновити + Завантажити + Видалити %1$s + %1$.1f MB + Драйвер середовища + Використовується Steam, іграми та робочим столом. Застосовується в наступному сеансі. + Імпортувати zip Turnip… + Драйвер відображення + Виводить кадри на екран в обох режимах. Перезапустіть додаток, щоб застосувати. + Імпортувати zip AdrenoTools… + ніколи + Нічні збірки перевірено %1$s + Компоненти + Про компоненти + Шукати нові драйвери + Шукати нові пакети в нічних збірках + Proton + Вибраний тут Proton запускає кожну гру, для якої в Steam не вибрано власний, а зміна, зроблена в Steam, теж відображається тут. Під час оновлення Steam замінює Proton від Valve; оригінали зберігаються для кожної збірки. + Конфігурація + Налаштувати профіль + Профіль + Основний профіль + Автоматично використовується іграми без перевизначень. + Збережені налаштування, які автоматично застосовуються під час запуску %1$s. + Вибирайте лише ті налаштування, які потрібні цій грі. Усе інше й надалі береться з основного профілю. + Пресет FEX + Використовується пресет основного профілю. + Для цієї гри збережено власний пресет. + Власні змінні FEX + Як в основному + Сумісність гри + Параметри середовища Proton, DXVK, VKD3D і синхронізації для цієї гри. + Використовується Proton основного профілю. + Для цієї гри збережено власний Proton через зіставлення сумісності Steam. + Немає Proton + DXVK + VKD3D-Proton + Використовує пакет основного профілю з %1$s, якщо ця гра не вибирає інший пакет. + Використовує пакет компонентів основного профілю. + Використовує %1$s лише для цієї гри. + Параметри середовища виконання + Інші параметри середовища Proton, DXVK, VKD3D-Proton і синхронізації для окремої гри. + Профіль %1$s збережено для наступного запуску гри. + Запустити без Steam + Для гри, яка відразу закривається: запускає її без налаштування Steam від Proton. Функції Steam, як-от оверлей, для неї вимкнені. + Увімкнено + Вибір Proton для %1$s збережено, і він уже доступний у Steam. + Вибір Proton для %1$s збережено. Перезапустіть Steam перед запуском гри. + %1$s не може використовувати вибраний Proton на цьому пристрої. + Не вдалося завантажити профіль %1$s: %2$s + Не вдалося зберегти профіль гри: %1$s + Скинути профіль гри + Скинути профіль %1$s? + Видалити для цієї гри перевизначення Proton, FEX, пакета компонентів, середовища, фільтрації текстур і компонентів Windows? Гра використовуватиме основний профіль. + %1$s тепер використовує основний профіль. + Не вдалося повністю скинути профіль гри: %1$s + %1$s тепер запускає кожну гру, для якої в Steam не вибрано власний Proton. + %1$s запускатиме кожну гру, для якої не вибрано власний Proton, починаючи з наступного запуску Steam. + %1$s не може запускати тут ігри, тому його не можна вибрати за замовчуванням. + Не вдалося зберегти Proton за замовчуванням: %1$s + %1$s · замінено: %2$d + %1$s · усе оригінальне + %1$s · гра запущена + %1$s · пакет droiddeck-esync + %1$s · пакета droiddeck-esync ще немає + %1$s · esync вбудовано, пакет не потрібен + Драйвери GPU + %1$s · повторно застосовано під час запуску %2$s + Далі: %1$s · після закриття гри + Використовується %1$s: %2$s + Очікує, доки гру буде закрито + Застосовується під час наступного запуску гри + Імпортувати .zip + Читання версій Proton… + Proton ще не встановлено. Один раз запустіть Steam, щоб завантажити його Proton для ARM64, або додайте GE-Proton, CachyOS чи DroidDeck-Proton у розділі «Налаштування». + Оригінал · %1$s + %1$s · ця збірка + %1$s · попередня збірка + Установлено · %1$d + Для %1$s ще нічого не збережено. Власні файли цього Proton зберігаються як його оригінал. + На цьому Proton запущено гру, тож зміна чекатиме, доки її закриють. + Застосовується під час наступного запуску гри. + Замінити на %1$s? + Помістити %1$s у %2$s? Оригінальні файли залишаться збереженими. %3$s + Відновити %1$s? + Повернути оригінал %1$s у %2$s? %3$s + Видаляє пакет із додатка. Ви зможете завантажити його знову. + Видалити цей оригінал? + Цей оригінал %1$s походить із попередньої збірки %2$s. Оригінал поточної збірки зберігається завжди. + У нічних збірках · %1$d + Оновіть, щоб побачити пакети з нічних збірок. + Нічого нового з часу останньої перевірки. + Імпортувати .wcp + Відновити оригінал цього Proton + Повернути оригінальний %2$s у %1$s? + Замінити + FEX, DXVK і VKD3D-Proton для кожного Proton, на якому Steam запускає ігри.\n\nВиберіть установлений рядок, щоб підставити його. Зміна застосовується під час наступного запуску гри або після закриття запущеної гри.\n\nВаш вибір перевіряється перед кожним запуском і відновлюється, якщо щось його змінило. Оригінальні файли кожного Proton зберігаються, тож оновлення Steam ніколи їх не втратять. + Оригінал + Використовується + Нове + Збережено + Продуктивність + Ядра CPU, виправлення сеансу та ім’я хоста. Застосовується в наступному сеансі. + Ядра клієнта Steam + Перевизначити вибір ядер Steam + Прив’язує Steam, його допоміжний процес інтерфейсу та gamescope до ядер нижче. + Ядра клієнта + Ядра для Steam. + Увімкніть перевизначення, щоб вибрати. + Ядра для ігор + Ядра для ігор + Ядра для ігор. Якщо вибрано всі, Android розподіляє навантаження вільно. + Інтерфейс клієнта + Багатопотоковий GL + Меню Steam можуть реагувати швидше. + Zink: ліниві дескриптори + Рекомендовано для драйверів без буферів дескрипторів. + Пропускати перевірку помилок GL + Вимикає перевірку GL для кожного виклику. + gamescope: черга GPU реального часу + Надає пріоритет роботі композитора на GPU. Може вирівняти темп кадрів ціною частини продуктивності ігор. + GPU + Тримати GPU на максимальній частоті + Лише Adreno. Менше запинок, вища витрата заряду та нагрів. + Виправлення сеансу + Turnip: рендеринг через sysmem + Швидше для Steam і більшості ігор. Потрібно для Adreno 710/720/722. + Пропускати помічник Steam xalia + droiddeck-ntsync (експериментально) + Програмна заміна драйвера ntsync ядра Linux у просторі користувача, для тестування. Поки це увімкнено, Proton використовує її замість droiddeck-esync і droiddeck-fsync. Працює з Proton Experimental ARM64, GE-Proton і proton-cachyos. Застосовується в наступному сеансі. + droiddeck-esync + droiddeck-fsync першим + Запускає власний fsync Proton на стандартних файлах Proton, а futex_waitv надається в просторі користувача, тому не потрібен пакет і все працює відразу після оновлення Proton у Steam. Поки це увімкнено, ігри використовують його раніше за droiddeck-esync, окрім кількох, де Proton тримає fsync вимкненим. Застосовується в наступному сеансі. + Потоки Windows у грах чекають одне одного через eventfd і спільну пам’ять замість звернень до wineserver. Використовує пакет droiddeck-esync, створений саме для цієї збірки Proton; пакети завантажуються автоматично. Доки такого пакета немає для нової збірки Proton, його замінює droiddeck-fsync. Застосовується в наступному сеансі. + Вимикає помічник Proton для навігації геймпадом, який споживає ресурси CPU в кожній грі. + Запускати proot без seccomp + Може усунути помилки відсутніх системних викликів на деяких ядрах. Може знизити продуктивність. + Швидкий шлях proot + Потребує фільтра seccomp у proot. + Швидший пошук файлів і менше запинок. Вимкніть, якщо програма не може знайти свої файли. + Ідентифікація сеансу + Ім’я хоста + Ім’я, яке Steam бачить для цього пристрою. Порожнє значення відновлює %1$s. + Лише літери, цифри та дефіси всередині, до 63 символів. + Android налаштовано завершувати цей сеанс + Встановлено + Доступно для завантаження + Нічого нового з часу останньої перевірки. + Відновити вбудовані драйвери + Вбудовано в додаток: це видаляє його розпаковані файли та приховує його. «Відновити вбудовані драйвери» повертає його. + Половина набору Android + Linux: видаляються обидві половини. Драйвер, що використовується, повертається до стандартного. + Його файли видаляються з додатка. + Його файли видаляються. Він зараз використовується, тому його замінює стандартний драйвер. + Екран + Ефекти + Керування + Компоненти + Контролер + Налаштування + Сторінка «%1$s» + Steam + Робочий стіл + Меню Steam + Швидкий доступ + Відкрити меню швидкого доступу + Попередня сторінка + Наступна сторінка + HUD продуктивності + Режим масштабування + Чіткість масштабування + Ефекти екрана + У реальному часі. Трохи навантажує GPU, поки увімкнено будь-який ефект. + Пресети + Власний + Додатково + Розгорнуто + Згорнуто + Чіткість (CAS) + Рівень чіткості + Імітація HDR + Усунення смугастості + Сила усунення смугастості + Яскравість + Контраст + Гамма + Насиченість + FXAA (згладжування країв) + Мультиплікаційні контури + CRT + NTSC + Фільтрація текстур + Ігри на DirectX 9–11, з наступного запуску. + Анізотропна фільтрація + Чіткість текстур + Негативне зміщення mip-рівня: чіткіші текстури, трохи мерехтіння. «Авто» підлаштовується під те, наскільки сеанс збільшено до розміру екрана. + Авто (%1$s) + Клавіатура + Клавіатура ПК + Android + Другий екран + Режим + Показує + Додатки Android + Запустити додаток + Приховати + Показати + Немає додатків для запуску + Поведінка сеансу + Застосовується зараз і до майбутніх сеансів у цьому режимі. + Наступний сеанс + Підтримка + Журнали сеансу + Додайте до звіту про помилку + Поділитися журналами + Фон + Завершити + Завершити сеанс? + Зараз виконується + Жодна гра не запущена на Proton + Профіль гри + Дані профілю ще не доступні. + Не вдалося завантажити профілі гри. Повторна спроба виконується автоматично. + Не вдалося оновити профілі гри. Показано останні доступні дані; повторна спроба виконується автоматично. + Переглядайте та редагуйте збережені налаштування, які застосуються під час наступного запуску. + Використовується профіль «Стандартний» + Власний профіль + Використовується %1$s зі стандартного профілю. + Зміни зберігаються в профілі %1$s і застосовуються під час наступного запуску гри. + + %1$d перевизначення гри + %1$d перевизначення гри + %1$d перевизначень гри + %1$d перевизначення гри + + Proton ще не встановлено. + гра запущена + повторно застосовано під час запуску %1$s + Оригінал + Імпортуйте та видаляйте на сторінці «Компоненти». + Готово + Потрібна увага + Виконується + Перевірте IP-адресу та порт бездротового налагодження або повторіть поєднання, якщо Android видалив цей пристрій. + Установити + Керувати + Огляд + Контролер + Сеанс + Лаунчер + Налаштування + ● Усе готово + %1$d з %2$d готово + Перевірка системи + Пристрій підтримується + GPU не перевірено + GPU не підтримується + %1$s · %2$s + %1$s: %2$s + Steam потребує драйвера Adreno; %1$s може показувати чорний екран + Середовище Linux + %1$s · %2$d%% + Не встановлено · приблизно 3 GB, встановлюється під час першого натискання «Грати» + Встановлено + %1$s · є оновлення + %1$s · актуальна версія + Обмеження дочірніх процесів + Увімкнено · Android може закрити Steam. Вимкнення займає хвилину. + Не задано · Android може закрити Steam. Вимкнення займає хвилину. + Не вдалося перевірити · вимкнення займає хвилину. + Виправити + Подробиці + Параметри розробника + Використати бездротове налагодження + Перевірити ще раз + Увімкнути обмеження + Обліковий запис Steam + Увійшли як %1$s · офлайн-режим + Увійшли як %1$s + Натисніть «Грати» та увійдіть у Steam + Інструменти + Контролер + Налаштування контролера недоступні. + Сеанс + Журнали сеансу + Зберігаються після кожного сеансу + Засоби діагностики + Останні журнали сеансу + Збережені журнали сеансу + Зберігає %1$d найновіших + Очистити журнали + Офлайн-режим + Спершу увійдіть у Steam + Лаунчер + Мова + Steam теж використовує її, з наступного запуску. + Системна мова (%1$s) + Системна мова + Масштаб додатка + Текст і елементи керування DroidDeck + %1$d%% (стандартно) + Тема + Вигляд лаунчера + Використовувати як головний екран + DroidDeck може бути додатком головного екрана + DroidDeck ніколи не пропонується як додаток головного екрана + Орієнтація + Автоматично альбомна + Альбомна + Перевернута альбомна + Повний екран + Приховує рядок стану та панель навігації Android + Показує рядок стану та панель навігації Android + Стандартний додаток головного екрана + Виберіть додаток головного екрана + Вибрати + Додатки Linux (бета) + Файли + Перегляд і керування файлами + Версії Proton + Встановлення збірок Proton для ARM64 + Продуктивність + Розподіл ядер CPU + Папка ROMs + Де зберігаються ігри для емуляторів + Компоненти + FEX, DXVK, VKD3D + Керування + Призначення кнопок + Огляд + Пошук + Встановлено + %1$s · %2$d + Магазин + Flathub · ARM64 + ● Flatpak готовий + Спершу встановіть середовище Linux у розділі «Налаштування». + Налаштувати Flatpak + Додає Flatpak і підключає його до Flathub (приблизно 20 MB). Перший додаток також завантажує своє середовище виконання. + Налаштування… + Flatpak + Оновлення + %1$s · %2$s + %1$s · %2$s · %3$d%% + Запуск… + Категорії + Не вдалося з’єднатися з Flathub. Перевірте з’єднання й спробуйте ще раз. + Спробувати ще раз + Пошук у Flathub + Firefox, ігри, емулятори… + Пошук + Усі + Пошук… + Шукайте за назвою або виберіть категорію. У списку лише додатки ARM64. + На Flathub немає відповідних додатків ARM64. + Результати + Перевірити оновлення + Оновити всі (%1$d) + ● Усе актуально + Поки немає додатків. Знайдіть їх у розділах «Огляд» або «Пошук». + Налаштуйте Flatpak, щоб встановлювати додатки. + Є оновлення · %1$s + Відкрити + Подробиці + Видалити + Натисніть ще раз, щоб видалити + Встановлення… + Установити + Спершу налаштуйте Flatpak + Завантаження %1$s + ● Встановлено + Не вдалося завантажити сторінку цього додатка на Flathub. + Завантаження з Flathub… + На Flathub немає збірки ARM64, тому встановити це тут не можна. + Знімки екрана + Про додаток + ID додатка + Ліцензія + Середовище виконання + Розмір після встановлення + %1$s (плюс спільне середовище виконання) + Веб-сайт + Перевірено + Оновлення + DroidDeck %1$s · %2$s + Перевірено %1$s + Ще не перевірено + Дозволити DroidDeck оновлюватися самостійно + На наступному екрані увімкніть «Дозволити з цього джерела», а потім поверніться. Оновлення продовжиться самостійно. + Відкрити налаштування + Не зараз + Підписано інакше + Неможливо оновити на місці + Цю копію не підписано релізним ключем DroidDeck, тому оновлення не можуть установитися над нею. Щоб перейти на іншу, видаліть цю копію. + Є оновлення + Неможливо встановити + Готово до переходу + Попереду стабільного + Ви попереду стабільного каналу + Ця збірка новіша за останню стабільну. Ви перейдете на стабільний канал з його наступним випуском. + Тест завершено + Цей тест завершено + Його виправлення об’єднано або відхилено. Перейдіть на попередній канал, щоб отримувати найновіші виправлення. + Актуальна версія + У вас найновіша версія %1$s + · %1$s + %1$s… %2$d%% + %1$s… + Оновити%1$s + Установити%1$s + Усе одно встановити стабільну %1$s + Перейти на попередній канал + Завершіть поточний сеанс, щоб оновити. + Згорнути + Розгорнути + DroidDeck %1$s + Нова попередня збірка + Тестова збірка PR #%1$d + %1$d MB + КАНАЛ ОНОВЛЕНЬ + Стабільний + Перевірені випуски, для більшості людей + %1$s · %2$s + Попередній + Найновіші виправлення, до стабільного каналу + Актуальна версія + Тестові збірки + Спробуйте виправлення до його випуску + Поки що немає + PR #%1$d + Не зібрано для цієї копії DroidDeck + Оновлено %1$s + тест PR #%1$d + деякий час тому + щойно + + %1$d на спробу + %1$d на спробу + %1$d на спробу + %1$d на спробу + + + Зміни з часу вашої збірки · позаду на %1$d збірку + Зміни з часу вашої збірки · позаду на %1$d збірки + Зміни з часу вашої збірки · позаду на %1$d збірок + Зміни з часу вашої збірки · позаду на %1$d збірки + + + Останні зміни · %1$d збірка + Останні зміни · %1$d збірки + Останні зміни · %1$d збірок + Останні зміни · %1$d збірки + + Один параметр Android, який потрібно змінити + Steam + Android зупиняє додатки, які запускають багато фонових процесів, а Steam запускає їх десятки. Доки це не змінити, ігри закриватимуться без помилки. + Виправити за мене + Увімкніть «Вимкнути обмеження дочірніх процесів», а потім поверніться. Залиште «Параметри розробника» увімкненими, інакше перемикач вимкнеться знову. + Відкрити «Параметри розробника» + У цій версії Android немає перемикача для цього, тож DroidDeck змінює налаштування через бездротове налагодження. Займає приблизно хвилину, комп’ютер не потрібен. + Бездротове налагодження потребує Wi-Fi. Підійде будь-яка мережа; інтернет не потрібен. + Код поєднання приходить у сповіщенні. Дозвольте сповіщення для DroidDeck або скористайтеся розділом «Інші способи». + Дозволити сповіщення + Відкрийте «Налаштування» → «Параметри розробника» → «Бездротове налагодження» і увімкніть його + Натисніть «Поєднати пристрій за кодом» + Введіть код у сповіщення DroidDeck + Очікування вікна поєднання… + Вікно знайдено. Введіть його код у сповіщенні. + Статус + Перевірка кожні 2 секунди. Ця сторінка закриється, коли це вимкнено. + Інші способи + Бездротове налагодження, адреса вручну, ADB з комп’ютера + Адреса вручну, ADB з комп’ютера, параметри розробника + Використати бездротове налагодження замість цього + Деякі прошивки все одно додають цей перемикач. Пошукайте «дочірній процес» у параметрах розробника. + Якщо сповіщення не працює, введіть адресу та код із вікна вручну. + Ввести адресу вручну + З комп’ютера з ADB: + Копіювати команду + Виберіть, на якому екрані відкрити налаштування Android. + Головний екран + Поєднано. Пошук порту бездротового налагодження… + Поєднано. Введіть порт із налаштувань. + Пристрій знайдено. Застосування налаштування… + Не вдалося застосувати автоматично. Перевірте адресу нижче і повторіть спробу. %1$s + Бездротове налагодження + Обмеження дочірніх процесів оновлено + Змінити обмеження дочірніх процесів + Якщо цього параметра немає, поєднайтеся через бездротове налагодження. + Поєднано. Перевірте IP-адресу і порт у бездротовому налагодженні. + Обмеження дочірніх процесів оновлено. + Використайте крок 2, якщо в параметрах розробника немає налаштування дочірніх процесів. Відкрийте «Параметри розробника» → «Бездротове налагодження» → «Поєднати пристрій за кодом». + Поєднано. Використайте адресу нижче або оновіть її в розділі «Бездротове налагодження» → «IP-адреса та порт». + Обмеження дочірніх процесів оновлено через бездротове налагодження. + Android підтвердив, що обмеження увімкнено. + Android підтвердив, що обмеження вимкнено. + 2 · Поєднати бездротове налагодження + IP-адреса і порт із вікна поєднання + Код поєднання (PIN) + 6 цифр + Поєднати + 2 · Застосувати через бездротове налагодження + IP-адреса і порт з’єднання + Поєднати ще раз + Вимкнути обмеження + Інші способи змінити це + 1 · Параметри розробника (спробуйте спершу) + Увімкніть «Обмежити дочірні процеси». + Вимкніть «Обмежити дочірні процеси». + 3 · ADB з комп’ютера (останній варіант) + Лише якщо бездротове налагодження недоступне. + 3 · ADB з комп’ютера (останній варіант) + Коли сенсорне керування з’являється в сеансі + Колір + Відтінок екранних кнопок + Власний + Непрозорість + %1$d%% + Розмір кнопок + 100% — стандартний розмір + Натискання стика + Двічі торкніться і затримайте стик, щоб отримати L3 або R3 + Адаптивні стики + Вібрація гри + Кнопка Steam + Кнопка QAM + Кнопка клавіатури + Натисніть Select, щоб відкрити меню Steam + Замість View/Select. + Анімації + Стики з’являються там, де ви торкаєтеся, і зникають після відпускання + Розкладка + Збережено власне розташування + Підлаштовано під цей екран + Змінити + Скинути + Призначення кнопок + Кожна кнопка надсилає власне введення + Перепризначено %1$d з %2$d кнопок + Налаштувати + Скинути контролер + Відновлює всі налаштування контролера та розкладку + Що кожна екранна кнопка надсилає грі. «Приховано» прибирає кнопку. + Екранні кнопки + Стандартні + Скинути призначення + Додатки Android + %1$d додатків + Не знайдено жодного додатка Android для запуску. + ● Робочий стіл встановлено + Встановлюється під час першого відкриття + Доступно для встановлення + Не встановлено. + %1$s · не встановлено + Відкрити %1$s + Ігри + Виберіть папку ROMs. + Перейдіть до /root/ROMs у RetroArch. + Додайте ігри %1$s у ROMs/%2$s. + Установити %1$s + Завершіть сеанс, щоб установити + Потрібне середовище Linux + Завантаження відомостей про встановлення… + Відомості про встановлення недоступні. Спробуйте пізніше. + Спершу встановіть середовище Linux у розділі «Налаштування». + Завершіть поточний сеанс, щоб установлювати додатки робочого столу. + Установлює %1$s у робочий стіл Linux. + %1$s · %2$d%% + Приховування вилучає його з робочого столу. Його файли залишаються в середовищі Linux. + Цієї гри вже немає в папці ROMs. + Робочий стіл · %1$s + Запустити в %1$s + Також у %1$s + Цієї сторінки більше немає. Виберіть розділ зліва. + Відкрити робочий стіл + Установити й відкрити робочий стіл + Видалити робочий стіл + Видаляє робочий стіл та вбудовані в нього додатки. Ігри, додані додатки, налаштування та спільні бібліотеки залишаються. + Видалення робочого столу + Робочий стіл видалено + Видалення старих файлів LXQt + Оновіть DroidDeck, перш ніж видаляти цю версію робочого столу. + Робочий стіл Linux + KDE Plasma, Firefox і ваші емулятори + Ігри, які ви встановите в Steam, з’являться тут після сеансу. + Грати в Steam + Налаштування запуску + Запустити + Файли гри + Префікс Proton + Пошук збережень… + Папку збережень за назвою не знайдено. Експорт візьме всю папку користувача. + %1$d папок збережень + %1$s · %2$d файлів · %3$s + %1$d KB + Керування збереженнями + Збереження гри + Імпортувати збереження… + Експортувати як zip GameHub… + Експортувати як zip Winlator… + Відкрити папку збережень + Ще не грали + Середовище зайняте · %1$d%% + Середовище зайняте + Середовище встановлюється під час першого натискання «Грати» + Є оновлення середовища + ● Середовище готове + Останній раз грали %1$s + Внутрішнє сховище + Додана гра + Власна гра + Сховище ігор + Найкраща пара для цього GPU, завжди в актуальному стані + Виберіть пару самостійно + Перевірка драйверів… + Драйверів у списку ще немає. Оновіть, щоб їх знайти. + Приховати драйвери для інших GPU + Показати драйвери для інших GPU + Додатково + Задається разом із драйвером дисплея: %1$s містить обидва. + Задається разом із драйвером середовища: %1$s містить обидва. + Використовується іграми та робочим столом. + Стандартний для середовища + Виводить кадри на екран. Застосовується після перезапуску DroidDeck. + Авто — вибирається за GPU + Завантажити, імпортувати або видалити… + ВИКОРИСТОВУЄТЬСЯ + Драйвери, вбудовані в DroidDeck + Драйвери середовища і дисплея задаються разом як узгоджена пара. + Рекомендовано для цього GPU: %1$s. + Оновіть, щоб знайти драйвери для цього GPU. + РЕКОМЕНДОВАНО + Недоступно + Середовище Linux + Підготовка середовища Linux + Сеанс + Запуск відео та звуку + Клієнт Steam + Перевірка Steam та його засобів сумісності + Відкриття Steam + Очікування першого кадру Steam + Робочий стіл + Підготовка робочого столу + Відкриття + Очікування першого кадру + Запуск Steam + Сеанс завершено + Крок %1$d з %2$d + Steam призупинено + Задні кнопки + Ліва сторона + Права сторона + Обидві + Геймпад + Стік + Трекпад + Перемкнути між стіком і трекпадом + Більше елементів керування Deck + ПАНЕЛІ + Закрити керування на другому екрані + Відкрити меню Steam + Ліва · верхня + Ліва · нижня + Права · верхня + Права · нижня + Натискання + Правий трекпад + Лівий трекпад + Трекпад + Показати клавіатуру + Сховати клавіатуру + Термінал Linux + Сеанс Linux ще не готовий до роботи з терміналом. + Steam + Ігри + Робочий стіл + Магазин + Компоненти + Додатки + Налаштування + Оновлення + %1$d%% + Продовжити %1$s + У кожному сеансі з’являється як /root/ROMs. Внутрішня пам’ять доступна в /root/Storage. + Папку не вибрано + Діє з наступного сеансу. SD-карти підтримуються. + Вибрати папку + Змінити папку + Встановіть збірку Proton для ARM64, а потім виберіть її для гри в Steam → Властивості → Сумісність. + Зупиніть поточний сеанс, щоб встановити або видалити інструмент сумісності. + Доступні збірки + Встановлено %1$s + Очікує з попереднього запиту + Не встановлено + Доступне оновлення → %1$s + Видалити старі версії %1$s + Буде видалено %1$s. Для ігор, налаштованих на ці версії, доведеться вибрати інший Proton. + Для ігор, налаштованих на цю версію, доведеться вибрати інший Proton. + Встановити зараз + Прибрати з черги + Клавіатура Android + Сховати ✕ + Запустити %1$s + Виберіть екран. Додатковий: %1$s. + Додатковий дисплей більше не доступний. + Основний екран + Додатковий екран + Попередня вкладка + Наступна вкладка + Увімк. + Усі %1$d + Жодного + %1$d з %2$d + Вимк. + Нижній екран + Діє з наступного запуску гри. Профіль гри має пріоритет над спільним. + Інша гра за ID додатка… + Готово + За умовчанням + Вимк. + Змінити… + Змінити змінну + Повернути значення за умовчанням + Ігри Proton + Картинка в картинці + Автоматично вмикати режим PiP + Керування на розгорнутому екрані + У розгорнутому стані гра розташована над трекпадами й задніми кнопками Steam Deck. У складеному — гра на весь екран. + Призупинено + Режим «Картинка в картинці» стане доступним, щойно сеанс буде готовий. + Дозвольте DroidDeck режим «Картинка в картинці» в налаштуваннях Android. + Android не вдалося відкрити режим «Картинка в картинці». + Ярлик + Додати на головний екран + Вибрати гру + Пошук встановлених ігор… + Копіювати посилання для запуску + Посилання для запуску скопійовано + Цей лаунчер не підтримує ярлики. Скористайтеся пунктом «Копіювати посилання для запуску». + Недійсне посилання для запуску гри. + Цієї гри більше немає у вашій бібліотеці DroidDeck. + Зупиніть поточний сеанс, перш ніж запускати гру Steam. + Не вдалося передати гру в Steam. Спробуйте запустити її ще раз. + Повторити видалення + Видалення + Видалення не завершено + Експортувати файл гри + Синхронізувати всі файли ігор із папкою + Зупинити синхронізацію файлів ігор + Виберіть папку для файлів ігор для фронтендів + Файли ігор збережено в %1$s + Файли для фронтендів + + Дочекайтеся завершення операції із середовищем + Дочекайтеся завершення встановлення інструмента сумісності + Дочекайтеся завершення встановлення програми для робочого столу + Дочекайтеся завершення встановлення, перш ніж запускати гру + Спершу закрийте сеанс Steam, щоб гра не перезаписала імпортовані збереження + Імпорт у %1$s + + Імпортовано %1$d файл з %2$s у %3$s + Імпортовано %1$d файли з %2$s у %3$s + Імпортовано %1$d файлів з %2$s у %3$s + Імпортовано %1$d файлу з %2$s у %3$s + + + Імпортовано %1$d файл з %2$s у %3$s. Резервну копію старих збережень записано в Download/DroidDeck/Saves/backups + Імпортовано %1$d файли з %2$s у %3$s. Резервну копію старих збережень записано в Download/DroidDeck/Saves/backups + Імпортовано %1$d файлів з %2$s у %3$s. Резервну копію старих збережень записано в Download/DroidDeck/Saves/backups + Імпортовано %1$d файлу з %2$s у %3$s. Резервну копію старих збережень записано в Download/DroidDeck/Saves/backups + + Виберіть zip-архів збережень для %1$s + Експорт %1$s + + Експортовано %1$d файл як %2$s: %3$s + Експортовано %1$d файли як %2$s: %3$s + Експортовано %1$d файлів як %2$s: %3$s + Експортовано %1$d файлу як %2$s: %3$s + + Виберіть, куди зберегти %1$s (%2$s) + %1$s — помилка: %2$s + Журналів сеансів ще немає: спершу запустіть сеанс. + Немає журналів сеансів для очищення. + + Очищено %1$d папку журналів сеансів. + Очищено %1$d папки журналів сеансів. + Очищено %1$d папок журналів сеансів. + Очищено %1$d папки журналів сеансів. + + Не вдалося створити пару через бездротове налагодження + Помилка команди бездротового налагодження + Бездротове налагодження недоступне + Команду ADB скопійовано + Виберіть папку ROMs + GPU не Adreno + Turnip підтримує GPU Adreno. На %1$s Steam може показувати чорний екран. Завантаження: %2$.0f MB. + Усе одно встановити + Видалити середовище Linux + Буде видалено середовище, клієнт Steam у ньому та всі встановлені там ігри. + Застосовано: %1$s + Не вдалося відкрити %1$s на основному екрані + Не вдалося відкрити %1$s на %2$s + Не вдалося відкрити %1$s на дисплеї %2$d + Імпорт + Заміна + Відновлення + Скасування + Видалення + не вдається прочитати файл + Імпортовано %1$s + Заміну в очікуванні скасовано. + Виберіть пакет компонента (-linux .wcp) + Сервер Nightlies недоступний + Збережено %1$s + Помилка завантаження: %1$s + Виберіть драйвер для середовища Linux (zip -Linux або набір Android + Linux) + Виберіть драйвер дисплея (zip AdrenoTools або набір Android + Linux) + Виберіть zip-файл драйвера або набір Android + Linux + %1$s: %2$s + Видалення %1$s… + Вилучення %1$s зі списку… + Виберіть папку для зберігання ігор + Виберіть ZIP плагіна Decky + Не можна використати: %1$s + Програма + Видалення середовища Linux + Не вдалося відкрити налаштування на нижньому екрані, тож їх відкрито на основному. + Відкрийте «Параметри розробника» в налаштуваннях + + Не вдалося відкрити налаштування Android «Встановлення невідомих додатків» + Не вдалося перевірити: %1$s + У цій збірці немає завантаження для вашої копії DroidDeck + Спершу зупиніть поточний сеанс, потім оновлюйте + Завантаження + Встановлення + Очікування вашого підтвердження + Не вдалося оновити + Android не запропонував встановити оновлення + + Не вдалося встановити Decky: %1$s + Початок імпорту плагіна… + Не вдалося імпортувати плагін: %1$s + Плагін встановлено. Перезапустіть Steam, щоб завантажити його. + + Ще не перевірено — натисніть «Оновити», щоб пошукати нові драйвери + Android + Linux: заодно встановлює драйвер дисплея + Android + Linux: заодно встановлює драйвер середовища + вбудований у середовище Turnip + системний Vulkan: немає вбудованої збірки для цього GPU + %1$s (вбудований) + Вручну + %1$s · %2$s + Наразі опубліковано лише одну частину + %1$.0f MB за обидва + Немає драйверів для встановлення: %1$s + Пошук найновіших драйверів… + Актуальна версія (не вдалося перевірити: %1$s) + Рекомендовані драйвери зараз не опубліковано + Завантаження %1$s %2$s… + Оновлено до %1$s %2$s. + Оновлено до %1$s %2$s. Драйвер дисплея запрацює після перезапуску DroidDeck. + Перемкнено на %1$s %2$s. + Перемкнено на %1$s %2$s. Драйвер дисплея запрацює після перезапуску DroidDeck. + Використовується %1$s %2$s. + Використовується %1$s %2$s. Драйвер дисплея запрацює після перезапуску DroidDeck. + Це не zip-файл драйвера: не драйвер AdrenoTools, не Turnip -Linux і не набір Android + Linux + Не вдалося імпортувати: %1$s + Імпортований драйвер + %1$s використовується і як драйвер середовища, і як драйвер дисплея. + %1$s використовується і як драйвер середовища, і як драйвер дисплея. Драйвер дисплея запрацює після перезапуску DroidDeck. + Видалено %1$s (обидва драйвери) + Видалено %1$s + Найновіші: %1$s · перевірено %2$s + Найновіші: %1$s · %2$s недоступно · перевірено %3$s + + %1$d хв тому + %1$d хв тому + %1$d хв тому + %1$d хв тому + + + %1$d год тому + %1$d год тому + %1$d год тому + %1$d год тому + + + %1$d день тому + %1$d дні тому + %1$d днів тому + %1$d дня тому + + Перевірка Banners-Turnip, WinNative і DroidDeck… + помилка перевірки + Встановлено %1$s — виберіть його в будь-якому зі списків, щоб задати обидва драйвери + Встановлено %1$s — виберіть його в меню + + Дочекайтеся завершення видалення середовища + Гра + тачпад + прямий + Не вдалося замінити: %1$s + Не вдалося зберегти профіль: %1$s + Робочий стіл призупинено + %1$s призупинено + Програму призупинено + Запуск робочого столу + Запуск %1$s + Запуск програми + Для цього сеансу немає журналів. + Не вдалося створити архів журналів сеансу. + Не вдалося поділитися журналами сеансу. + Запуск сеансу… + завантаження середовища Linux + завантаження середовища Linux · %1$d з %2$d MB + перевірка середовища Linux + розпакування середовища Linux + завантаження робочого столу + завантаження робочого столу · %1$d з %2$d MB + перевірка робочого столу + розпакування робочого столу + завантаження Proton Experimental (ARM64) + завантаження Proton Experimental (ARM64) · %1$d з %2$d MB + перевірка Proton Experimental (ARM64) + розпакування Proton Experimental (ARM64) + Не вдалося отримати каталог середовищ. Перевірте з’єднання й натисніть «Грати» ще раз. + Середовище Linux не встановилося. Перевірте його в розділі «Налаштування» й натисніть «Грати» ще раз. + Робочий стіл не встановився (%1$s). Перевірте з’єднання й натисніть «Робочий стіл» ще раз. + Роботу %1$s несподівано завершено. Додайте журнали до звіту про помилку або спробуйте ще раз. + Програма несподівано зупинилася. Додайте журнали до звіту про помилку або спробуйте ще раз. + Робочий стіл несподівано зупинився. Додайте журнали до звіту про помилку або спробуйте ще раз. + Код завершення %1$d · %2$s + немає журналу + + У журналі %1$d помилка «Function not implemented»: прискорення seccomp у proot порушує роботу допоміжної програми на цьому пристрої. Спробуйте %2$s → «%3$s» (або «%4$s») і запустіть знову. + У журналі %1$d помилки «Function not implemented»: прискорення seccomp у proot порушує роботу допоміжної програми на цьому пристрої. Спробуйте %2$s → «%3$s» (або «%4$s») і запустіть знову. + У журналі %1$d помилок «Function not implemented»: прискорення seccomp у proot порушує роботу допоміжної програми на цьому пристрої. Спробуйте %2$s → «%3$s» (або «%4$s») і запустіть знову. + У журналі %1$d помилки «Function not implemented»: прискорення seccomp у proot порушує роботу допоміжної програми на цьому пристрої. Спробуйте %2$s → «%3$s» (або «%4$s») і запустіть знову. + + + Плагіни + Встановіть плагін із ZIP для наступного сеансу Steam. + Завантаження… + Встановити із ZIP + Ігри + Аудіо й відео + Графіка + Інтернет + Офіс + Розробка + Освіта + Наука + Система + Утиліти + Популярне + У тренді + Нове + Оновлене + завантаження… + Налаштування Flatpak + %1$s: %2$s + Не вдалося перевірити оновлення на Flathub + встановлено + папка + Графіт + Графітові поверхні й синій колір кулі з логотипа для всього вибраного, у фокусі чи натиснутого. + Папір на чорному + Основний колір — білий, а синій з іконки позначає лінії, кільця й точки. + Синій з іконки + Чорне тло, білий текст і синій для всього вибраного, у фокусі чи натиснутого. + Електричний темно-синій + Яскравіший синій на поверхнях із темно-синім відтінком замість сірого. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Пробіл + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Спершу встановіть середовище + Бібліотека Steam недоступна + Попередня бібліотека на SD-карті недоступна + Сховище ігор недоступне + Папка доданих ігор недоступна + Маніфести Steam недоступні + Багато систем + Внутрішня пам’ять + Система Linux + SD-карта + Сховище + Вибрати + Властивості + Додати в обране + Вилучити з обраного + Перейменувати + Копіювати + Вирізати + Дії + Обране + Закріпити поточну папку + Обраного ще немає — закріпіть папку через її меню ⋮, щоб швидко повертатися до неї. + Вилучити з обраного + 0 Б + Б + Розташування + Тип + Розмір + Змінено + Папка + Файл + Файл %1$s + Лише читання + Не дає іграм (і Wine) перезаписати або видалити цей файл. + Не вдалося змінити атрибут «Лише читання» + Прихований + Це сховище не підтримує атрибут «Прихований». + Позначає файл як прихований у Windows (атрибут «Прихований» у Wine). + Не вдалося змінити атрибут «Прихований» у цьому сховищі + Готово + Видалення... + Не вдалося видалити + Переміщення... + Копіювання... + Переміщення — %1$s + Копіювання — %1$s + Переміщення %1$d/%2$d — %3$s + Копіювання %1$d/%2$d — %3$s + Виконано: %1$d, не вдалося: %2$d + Виконано: %1$d, пропущено: %2$d + + Переміщено %1$d елемент + Переміщено %1$d елементи + Переміщено %1$d елементів + Переміщено %1$d елемента + + + Скопійовано %1$d елемент + Скопійовано %1$d елементи + Скопійовано %1$d елементів + Скопійовано %1$d елемента + + «%1$s» уже існує + Перейменування... + Не вдалося перейменувати + Створення папки... + Не вдалося створити папку + Нова папка + Назва папки + Створити + Нова назва + Видалити? + Остаточно видалити «%1$s»? + + Видалити %1$d елемент? + Видалити %1$d елементи? + Видалити %1$d елементів? + Видалити %1$d елемента? + + Цю дію не можна скасувати. + + …і ще %1$d + …і ще %1$d + …і ще %1$d + …і ще %1$d + + Видалення %1$d/%2$d — %3$s + + Не вдалося видалити %1$d елемент + Не вдалося видалити %1$d елементи + Не вдалося видалити %1$d елементів + Не вдалося видалити %1$d елемента + + «Об’єднати» додає та замінює файли в наявній папці. + «Перезаписати» замінює наявний файл. + Застосувати до всіх конфліктів + Об’єднати + Перезаписати + Залишити обидва + Пропустити + Вибрати цю папку + Назад + %1$s: підключено, але читання зараз недоступне + Список + Сітка + Пошук + Сортування + Назва + Дата зміни + Просторі рядки + Компактні рядки + Сховати приховані файли + Показати приховані файли + Сховати обране + Показати обране + Фільтрувати цю папку + Вільно: %1$s + Вибрано: %1$d + Нічого + Усі + Вставити + + %1$d елемент + %1$d елементи + %1$d елементів + %1$d елемента + + Вставити %1$s сюди + Вставити %1$s сюди (перемістити) + Файли + СХОВИЩЕ + ШВИДКИЙ ДОСТУП + ОБРАНЕ + Завантаження + Завант. + ROMs + Журнали сеансів + Зображення + «%1$s» додано в обране + «%1$s» вилучено з обраного + Порожня папка + На рівень вище + Виберіть папку + Виберіть файл + Синій Steam + Небесний + Білий + Фіолетовий + Зелений + Бурштиновий + Червоний + Рожевий + Кнопка A + Кнопка B + Кнопка X + Кнопка Y + Лівий бампер + Правий бампер + Лівий тригер + Правий тригер + Кнопка «Перегляд» + Кнопка «Меню» + Перегляд + Меню + Хрестовина вгору + Хрестовина вниз + Хрестовина вліво + Хрестовина вправо + Приховано + Перетягніть, щоб перемістити + Прив’язка + Зберегти + Нічого + Клавіатура + трекпад + Термінал + Задні кнопки + трекпади Deck + + Видалення середовища Linux + + Видалення середовища Linux · %1$d запис + Видалення середовища Linux · %1$d записи + Видалення середовища Linux · %1$d записів + Видалення середовища Linux · %1$d запису + + Середовище Linux видалено + Не вдалося завершити видалення середовища Linux. Повторіть видалення в розділі «Налаштування». + Видалення залишків середовища + Завантаження + Перевірка + Розпакування + Показується під час встановлення або видалення середовища Linux + Встановлення середовища Linux + Видалення середовища Linux + + Середовище Linux не встановлено + У каталозі немає контрольної суми для %1$s + Контрольна сума не збігається — нічого не змінено + Встановлення %1$s + Не вдалося розмістити AppImage + Не вдалося розпакувати + Не вдалося встановити + + Не вдалося налаштувати Flatpak (код %1$d) + Налаштування Flatpak не завершено + Виконується інше завдання магазину + Спершу налаштуйте Flatpak + Запуск + Flatpak завершився з кодом %1$d + Встановлення %1$s + Оновлення %1$s + Видалення %1$s + Встановлення %1$s (%2$d/%3$d) + Оновлення %1$s (%2$d/%3$d) + Видалення %1$s (%2$d/%3$d) + перекладів + графічних драйверів + медіакодеків + середовища виконання %1$s + середовища виконання %1$s %2$s + + Файл замалий для AppImage + Не вдається прочитати файл: %1$s + Це не AppImage (це не програма для Linux) + Цей AppImage зібрано не для ARM64 (aarch64) і не для x86-ПК + Це програма для Linux, але не AppImage + Можна імпортувати лише AppImage типу 2 (цей має тип %1$d) + Недостатньо вільного місця: для імпорту потрібно близько %1$s + Недостатньо вільного місця: для оновлення потрібно близько %1$s + Не вдалося імпортувати + Цього AppImage більше немає + Не вдалося оновити + Розпакування %1$s + Не вдалося розпакувати AppImage + Не вдалося розпакувати AppImage: %1$s + + Зупиніть активний сеанс, перш ніж встановлювати Decky Loader + Не вдалося завантажити Decky Loader + Розмір Decky Loader не збігся з метаданими випуску + Не вдалося завантажити контрольну суму випуску + Контрольна сума випуску відсутня + Контрольна сума випуску недійсна + Контрольна сума Decky Loader не збіглася + Файл випуску не є сумісним 64-бітним виконуваним файлом Linux + Під час завантаження розпочався сеанс; зупиніть його, перш ніж встановлювати Decky Loader + Не вдалося позначити PluginLoader як виконуваний файл + Не вдалося увімкнути віддалене налагодження Steam CEF + Не вдалося підготувати PluginLoader + Не вдалося зберегти наявний PluginLoader + Не вдалося активувати новий PluginLoader + Зупиніть активний сеанс, перш ніж встановлювати плагін + Встановіть Decky Loader, перш ніж імпортувати плагін + Не вдалося прочитати вибраний zip-файл + Не вдалося створити папку плагінів Decky + Розпакування zip-файлу плагіна + Zip-файл плагіна повинен містити одну папку плагіна верхнього рівня + У zip-файлі плагіна відсутні plugin.json, package.json або dist/index.js + Назва плагіна відсутня + Під час встановлення розпочався сеанс; зупиніть його, перш ніж встановлювати плагін + Плагін встановлено · перезапустіть Steam, щоб завантажити його + Встановлення zip-файлу плагіна не вдалося + Zip-файл плагіна містить занадто багато елементів + Zip-файл плагіна містить недійсний шлях + Zip-файл плагіна містить шляхи, що повторюються + Zip-файл плагіна містить небезпечний шлях + Не вдалося створити папку плагіна + Zip-файл плагіна розпаковується в обсяг понад обмеження 1 GiB + Не вдалося позначити вбудований бінарний файл плагіна як виконуваний + Недійсний запис remote_binary + Недійсна назва віддаленого бінарного файлу + URL-адреса віддаленого бінарного файлу повинна використовувати HTTPS + Віддалений бінарний файл %1$s має недійсний хеш SHA-256 + Не вдалося створити папку bin плагіна + Завантаження бінарного файлу плагіна · %1$s + Не вдалося завантажити бінарний файл плагіна %1$s + Контрольна сума бінарного файлу плагіна не збіглася · %1$s + Не вдалося позначити бінарний файл плагіна як виконуваний · %1$s + Не вдалося замінити бінарний файл плагіна · %1$s + Не вдалося встановити бінарний файл плагіна · %1$s + Папка плагіна належить іншому плагіну + Не вдалося зберегти наявний плагін + Не вдалося активувати імпортований плагін + Не вдалося оновити налаштування плагінів Decky + Не вдалося відновити наявний плагін + + Не вдалося зберегти ідентифікаційні дані бездротового налагодження + Команда ADB завершилася помилкою (%1$d) + Android не підтвердив зміну обмеження дочірніх процесів: %1$s + Немає збереженої прив’язки бездротового налагодження + Введіть поточну IP-адресу та порт бездротового налагодження + + Час очікування вікна прив’язки минув. Спробуйте ще раз із DroidDeck. + Не вдалося знайти вікно прив’язки (%1$d). Перевірте, що Wi-Fi увімкнено. + Не вдалося знайти вікно прив’язки: %1$s + Введіть усі 6 цифр коду + Прив’язка… + Цей код не підійшов. Відкрийте вікно прив’язки знову і введіть новий код. + Прив’язано. Підключення… + Прив’язано, але порт бездротового налагодження не знайдено. Залиште бездротове налагодження увімкненим і спробуйте ще раз. + Застосування налаштування… + Помилка команди бездротового налагодження + Налаштування бездротового налагодження + Запитує код прив’язки бездротового налагодження, коли відкрито «Налаштування» + Прив’язати бездротове налагодження + Увімкніть бездротове налагодження, потім торкніться «Прив’язати пристрій за кодом прив’язки». + Увімкніть бездротове налагодження, потім торкніться «Прив’язати пристрій за кодом прив’язки». Залишайтеся в «Налаштуваннях»: код з’явиться в цьому сповіщенні. + 6-значний код прив’язки + Торкніться «Ввести код» і введіть 6 цифр, показаних у «Налаштуваннях». + Введіть код прив’язки Wi-Fi + Спробуйте код ще раз + Ввести код + Налаштування Steam + Steam готовий + Торкніться, щоб повернутися до DroidDeck. Тепер можна вимкнути бездротове налагодження. + Налаштування бездротового налагодження зупинено + + Завантаження відповіло HTTP %1$d + Розмір завантаження не збігся з опублікованим APK, тож його відхилено - спробуйте ще раз + Завантаження не збіглося з контрольною сумою, тож його відхилено - спробуйте ще раз + Оновлення призначене для пакета Android %1$s, а не %2$s + Метадані оновлення не вказують ключ підпису випуску DroidDeck + Android не вдалося прочитати завантажений APK + Завантажений APK призначений для %1$s, а не %2$s + Завантажений APK має versionCode %1$d, а не опублікований %2$d + Android не встановить versionCode %1$d над встановленим %2$d + Завантажений APK не містить інформації про підпис + Завантажений APK не підписано ключем випуску DroidDeck + Каталог оновлень відповів HTTP %1$d + + Запуск сеансу… + %1$d:%2$02d минуло · досі триває + Завантаження оновлення клієнта Steam · %1$d%% + Завантаження клієнта Steam · %1$s + + Встановлення інструмента сумісності вже триває + Зупиніть активний сеанс, перш ніж встановлювати інструменти сумісності + Не вдалося знайти випуск ARM64 для %1$s + Щоб встановити %1$s, потрібно щонайменше 4 GB вільного місця плюс розмір завантаження + Завантаження %1$s %2$s + Повтор завантаження · спроба %1$d з %2$d + Завантаження перервано + Помилка завантаження; частковий файл збережено для відновлення спроби + Завантаження перевищило розмір випуску і було видалено + Завантаження не завершилося; частковий файл збережено для відновлюваної повторної спроби + Перевірка завантаження + Не вдалося прочитати контрольну суму випуску; спробуйте ще раз + %1$s %2$s не публікує контрольну суму; нічого не встановлено + Контрольна сума не збіглася; завантаження видалено + Під час завантаження розпочався сеанс; зупиніть його, перш ніж встановлювати інструменти сумісності + Встановлення %1$s + Встановлення %1$s не вдалося (код виходу %2$d) + Не вдалося встановити %1$s + Отримання пакета droiddeck-esync + + з допоміжними засобами Linux + лише DLL + Оригінал (Proton оновлено з часу останньої заміни) + Змінено поза розділом «Компоненти» + відсутній + Оригінал %1$s + Оригінал з %1$s + %1$s %2$s буде встановлено в %3$s, коли гра, що на ньому працює, закриється. + %1$s %2$s тепер у %3$s. Застосується під час наступного запуску гри. + Цей оригінальний набір не збережено + %1$s із %2$s повернеться до оригіналу, коли гра, що на ньому працює, закриється. + %1$s із %2$s відновлено з оригіналу %3$s. + %1$s використовується в %2$s. Спершу замініть цей Proton на інший. + %1$s не збережено + Видалено %1$s. + Оригінал встановленої збірки збережено: це шлях назад. + Не збережено + Видалено оригінал %2$s зі збірки %1$s. + Завантаження надходять лише з випусків Nightlies + Цей список пакетів створено до появи контрольних сум - оновіть його і спробуйте ще раз + Контрольна сума не збіглася + + Лінійний + Найближчий + Snapdragon GSR (якість) + Лише різкість + Автоматично (відповідно до масштабування) + + Нічого не застосовано. + Ігрова чіткість + Різкість із ноткою контрасту - варіант на щодень. + Яскравий + Колір підсилено максимально. Усунення смугастості увімкнено, щоб небо не смугастилося. + Кінематографічний + Глибший контраст, холодніші кольори, згладжені краї. + Змагальний + Підняті тіні та посилена різкість - щоб першим помічати суперників. + Адаптивна різкість + Лише різкість, колір не змінюється. + Фільмовий + Крива в стилі кіноплівки, трохи приглушена. + Аркадний + Соковито та яскраво. + Ретро CRT + Ефекти трубки CRT та аналогового сигналу разом. + Різке масштабування + Для запуску в роздільній здатності, нижчій за панель. + Чисті пікселі + Чіткі пікселі для 2D та старих ігор. + Аніме-контур + Контури у стилі мультфільму плюс різкість. + + Не GPU Adreno + Підтримується + Експериментально: його драйвери - тестові збірки + Нижче за протестоване обладнання (Adreno 725 і новіші): може не запрацювати + Не підтримується: DroidDeck потребує GPU Adreno (Snapdragon) + + Перевищено ліміт запитів GitHub - спробуйте пізніше + GitHub відповів HTTP %1$d + завантаження відповіло HTTP %1$d + Цей список драйверів створено до появи контрольних сум - оновіть його і спробуйте ще раз + Контрольна сума не збіглася - завантаження відхилено + Не драйвер AdrenoTools: %1$s + немає meta.json + meta.json неможливо прочитати (%1$s) + це драйвер середовища Linux (імпортуйте його в розділі «Драйвер середовища Linux») + це ігровий драйвер Wayland, який цьому додатку не потрібен + meta.json не вказує libraryName, тож нічого не можна передати AdrenoTools + meta.json вказує %1$s, якого немає в zip-файлі + %1$s не є 64-бітною спільною бібліотекою ELF для AArch64 + %1$s пов’язаний з glibc - це Turnip «-Linux», який належить до розділу «Драйвер середовища Linux» + У цьому zip немає libvulkan_freedreno*.so. Zip Android (AdrenoTools) або -Wayland Turnip не є драйвером середовища Linux. + %1$s не є 64-бітною спільною бібліотекою ELF для AArch64. + %1$s не є драйвером glibc - він пов’язаний з libc Android. Середовищу Linux потрібен zip «-Linux»; звичайний Turnip або Turnip «-Wayland» клієнт Steam взагалі не може завантажити. + Не є набором драйверів: немає manifest.json із вказаними драйверами android і linux + Не є набором драйверів: %1$s вказує за його межі + Не є набором драйверів: розпаковується в обсяг понад %1$d MB + + SD-карта + Папка + + Поділитися журналами сеансу + + потрібен Android 10 або новіший (шари дисплея з кольоровим простором) + цей дисплей не повідомляє про підтримку HDR + цей дисплей підтримує %1$s, а не HDR10 + + цей GPU + Обмеження дочірніх процесів увімкнено + Обмеження дочірніх процесів не задано + Не вдалося перевірити обмеження дочірніх процесів + Обмеження дочірніх процесів вимкнено + Обмеження дочірніх процесів не потрібне + %1$s Якщо це неможливо, підключіть цей пристрій до комп’ютера з ADB і виконайте команду нижче. Потім поверніться сюди і перевірте ще раз. + Сеанси Steam можуть запускатися. + Ця версія Android не використовує це обмеження. + Увімкніть «Вимкнути обмеження дочірніх процесів» у розділі «Параметри розробника» і залишіть «Параметри розробника» увімкненими після цього. + У цій версії Android немає перемикача для цього. Змініть це з цього пристрою за допомогою бездротового налагодження. + У цій збірці немає завантаження для цієї копії DroidDeck. + Android не встановить це над цією збіркою, бо її versionCode %1$d нижчий за встановлений %2$d. + Android не встановить старішу версію над новішою. Залишайтеся на цій збірці, доки канал не наздожене. + Цю копію DroidDeck підписано інакше (локальна збірка?), тож Android не оновить її на місці. + Недостатньо вільного місця для оновлення. + Оновлення скасовано. + Оновлення не встановилося: %1$s + Оновлення не встановилося. + zip-архів %1$s + zip-архів + %1$s ще не запускали + Не знайдено файлів збережень для %1$s + %1$s не містить папки drive_c, тож це не zip-архів збережень GameHub або Winlator + не вдається створити папки в %1$s + не вдається записати в %1$s + не вдається записати в %1$s (%2$s) + Папка недоступна + Інший файл уже використовує %1$s + Папка графіки недоступна + Папка містить непов’язаний перелік синхронізації + Не вдалося видалити %1$s + Файл виходить за межі папки експорту + FEX (збірка без назви версії) + немає profile.json у %1$s + %1$s не є пакетом компонента Linux (тип %2$s) + %1$s · %2$s вільно + Автоматично + Каталог оновлень не пройшов перевірку (%1$s) + Параметр дочірніх процесів DroidDeck + Виконано вхід · офлайн-режим + Виконано вхід + ВБУДОВАНО + ЗАВАНТАЖЕНО + ІМПОРТОВАНО + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Дисплей сеансу + Роздільна здатність + Вписування гри 16:9 + Як ігри 16:9 вписуються в цей дисплей. З наступного сеансу. + Виберіть «Під екран» або власний розмір, вужчий за 16:9. + Зберігати пропорції + Розтягнути на весь дисплей + Масштабування зображення + Фільтр масштабування + Спільний для Steam і Робочого столу. + Застосовується відразу. Спільний для Steam і Робочого столу. + FSR, GSR і NIS застосовуються під час збільшення зображення сеансу. «Лише різкість» працює і без збільшення. + Різкість фільтра + Частота кадрів + %1$s Встановлює масштабування на %2$s. + Під екран (%1$d×%2$d) + Слідувати за екраном (змінює розмір при згортанні чи розгортанні) + Steam + Показати відомості про пристрій + Сховати відомості про пристрій + Читання даних пристрою… + Копіювати + Скопіюйте ці відомості у звіт про помилку. + Скопійовано. + Примусовий SSBS + Утримує увімкненим налаштування швидкості CPU, яке Wine постійно вимикає. Може прискорити ігри Windows на Snapdragon 8 Elite; на старших чипах зміни майже непомітні. Застосовується під час наступного запуску гри. + + Магазини + Steam + Власна + Магазини + Відкривати магазин на + Рівень швидкості завантаження + Рушій магазинів + ● Рушій готовий + Не зібрано + Збалансований + Швидкий + Максимальний + Магазини + Налаштування магазинів + Налаштування + (вхід не виконано) + Магазин + Встановлені + Бібліотека + Усі + Завантаження + %1$s (%2$d) + Увійти + %1$s · вийти + Вийти з %1$s? + Встановлені ігри залишаються. Бібліотеку буде отримано знову, коли ви увійдете знову. + Не виходити + Вийти + Пошук у каталозі %1$s… + Пошук встановлених ігор… + Пошук у вашій бібліотеці… + Пошук + Нічого не встановлено + Завантаження + Немає ігор + Завантаження магазину… + Немає каталогу + Нове + Знижки + Безкоштовні ігри + Популярне + Ваша бібліотека + Грати + Встановити + Продовжити встановлення + Перервано + Встановити · %1$s + Отримати безкоштовно + Переглянути на %1$s + Купити · %1$s + Встановлено + У бібліотеці + Безкоштовно + Продовжити + Оновити + Показувати контент для дорослих + Epic + EOS + Офлайн + Вимк. + Вхід Epic (EOS) + Запускати офлайн + Виправити вхід Epic + Хмарні збереження + Завантажити в хмару + Завантажити з хмари + Немає хмарних збережень + Конфлікт у хмарі + Конфлікт у хмарі · %1$d файлів + … + Залишити хмарну + Залишити локальну + Перевірка хмари… + Остання синхронізація %1$s + Ще не синхронізовано + %1$d файлів + встановлено + у вашій бібліотеці + магазин + Досягнення + %1$d з %2$d відкрито + %1$d / %2$d + Відкрито · %1$d + Заблоковано · %1$d + Сховано · %1$d + Сховане досягнення + Деталі з\'являться після відкриття. + Усі досягнення відкрито + Завантаження… + Увійдіть у %1$s, щоб побачити свої досягнення. + Не вдалося з\'єднатися з %1$s. Перевірте ще раз, коли будете онлайн. + %1$s не має досягнень для цієї гри. + Показати всі %1$d + Досягнення · %1$s + ✓ У Steam + Видалити + Видалення… + Видалити %1$s? + Звільнить %1$s і видалить гру зі Steam. + Видалить її файли та ярлик у Steam. + Steam побачить гру під час наступного запуску; запустіть її звідти або відкрийте Steam зараз. + Proton + Вибір Steam для кожної гри + Звук + DirectAudio + Компоненти + FEX, DXVK, VKD3D + Рушій + %1$s · нативний + %1$s · вбудований + Як це працює + Отримано з + %1$s, з вашим обліковим записом + Встановлено в + Сховище ігор (Games/%1$s/%2$s) + Запускається через + Steam і Proton, так само як ігри Steam + Steam і Proton · вхід Epic під час кожного запуску, оверлей EOS вимкнено + Steam і Proton · встановлено змінні Amazon Games Services + Steam + додається до Steam під час встановлення (інтерфейс Deck + Steam Input) + Немає завантажень + Призупинено + У черзі + У черзі · #%1$d + Скасовано + Помилка + Помилка · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Пауза + Продовжити + Скасувати + Повторити + Очистити + Очистити завершені + Маніфест + Завантаження + Перевірка + Встановлення + Готово + Перевірка + Завантаження + Видалити завантаження? + Помилка · %1$s збережено + Перевіряється + Встановлюється + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d файлів + Менеджер завантажень + Одночасних завантажень + Встановити в + Встановити %1$s: + Внутрішня пам’ять + %1$s вільно + Завантаження магазинів + Показує перебіг завантаження ігор і підтримує завантаження у фоновому режимі + Завантаження + %1$d завантажень + Підготовка… + Завершені завантаження магазинів + %1$s встановлено + Помилка завантаження %1$s + Видалити + Залишити + Увійти в %1$s + Увійдіть через свого провайдера + Перезавантажити + Закрити + Спробувати ще раз + Завершення входу… + Не вдалося завантажити сторінку входу: %1$s + Цей провайдер не дозволяє вхід із вбудованої сторінки. Увійдіть за допомогою пароля магазину. + Сторінка входу відповіла помилкою (HTTP %1$d). + З’єднання зі сторінкою входу не захищене. + Не вдалося підтвердити вхід. Спробуйте ще раз. + Вхід не вдався. Спробуйте ще раз. + Закрити + Назад + Редагувати + Додати всі ігри в цій папці + Нічого нового в цій папці + Назва + Ціль + Робоча папка + Параметри запуску + Графіка + Інший exe… + Вилучити + Обкладинка + Фон + Логотип + Значок + Папка гри + Steam + SteamGridDB + Ваш файл + З exe-файлу + Автоматично + Вибрати зображення… + Виберіть зображення: %1$s + Скинути до автоматичного + + Додано %1$d гру + Додано %1$d гри + Додано %1$d ігор + Додано %1$d гри + + + %1$d вже в Іграх + %1$d вже в Іграх + %1$d вже в Іграх + %1$d вже в Іграх + + B · Назад до %1$s + гри + налаштувань запуску + Графічні драйвери + Цей Proton + Кожен Proton + Для кожного Proton + Доступно · %1$d + Перевірено %1$s + Перевірити наявність нових компонентів і драйверів + Отримати + %1$s · DXVK, VKD3D-Proton і FEX нижче належать цьому Proton + %1$s · протестовано + %1$s · не протестовано + %1$s · не підтримується + Підтримує рекомендовану пару актуальною + Авто · %1$s + Драйвери середовища і дисплея окремо + Використати %1$s? + Видалити %1$s? + Діагностика + Готово до гри + Ще не готово до гри + середовище %1$s + обмеження дочірніх процесів вимкнено + вхід виконано як %1$s + Коли потрібна ваша увага + Вигляд + Цей пристрій + Графіка бібліотеки + ПРОГРАМИ LINUX + Flathub + Програми для стільниці Linux, які відкриваються з Робочого столу. + Налаштувати Flathub + Видалити %1$s? + Його налаштування та файли зберігаються. + Залишити + Розмір на диску + Видалити Робочий стіл? + Усі + Досягнення GOG + Синхронізує досягнення для підтримуваних ігор GOG. Застосується під час наступного запуску. + Досягнення вимкнено + Досягнення увімкнено + Увійдіть у GOG знову + Досягнення завантажено + Завантаження досягнення очікує + Увімкніть досягнення GOG у налаштуваннях магазинів + Досягнення недоступні; перевірте журнали сесії + Досягнення відкрито + Вставити + Хрестовина + Лівий стик + Правий стик + Меню + Клавіатура + Хрестовина вгору + Хрестовина вниз + Хрестовина вліво + Хрестовина вправо + Сенсорне керування + Сенсорний контролер + Миша + Увімк. + Вимк. + Повільна + Звичайна + Швидка + Змінити розкладку + Меню DroidDeck + Перетягніть елементи керування на екран + Розташувати + Відхилити + Збереження… + Значок + Ш− + Ш+ + В− + В+ + Сховати + Колір і непрозорість… + Копіювати цю розкладку + Розкладку скопійовано; вставте її в сенсорну розкладку будь-якої гри + Вставити скопійовану розкладку + Відновити типову розкладку Steam + У цього елемента немає прив\'язки в Steam, щоб призначити йому значок + Значок для %1$s + Колір сенсорної розкладки + Готово + Не вдалося зберегти сенсорну розкладку + Сенсорну розкладку збережено в Steam + Застосунок %1$d + Підпис (необов\'язково) + Значки Steam не знайдено + + %1$d значок зі Steam + %1$d значки зі Steam + %1$d значків зі Steam + %1$d значка зі Steam + + Колір значка + Колір кнопки + Без значка + + Перевірка входу… + Отримання вашої бібліотеки… + Отримання списку ігор… + Завантаження відомостей про ігри… (%1$d/%2$d) + + Синхронізація %1$d гри… + Синхронізація %1$d ігор… + Синхронізація %1$d ігор… + Синхронізація %1$d гри… + + Синхронізація… %1$d/%2$d + Отримання обкладинок… + Синхронізація не вдалася + У вашій бібліотеці %1$s не знайдено ігор + Amazon Games не відповів щодо вашої бібліотеки: оновіть або увійдіть знову + Не вдалося з\'єднатися з GOG + Не вдалося прочитати вашу бібліотеку GOG + Реєстрація в Steam… + Cardboard VR + Режим Cardboard + Зображення для лівого й правого ока для VR-окулярів під телефон. + Зображення + Віртуальний екран + Стереопара 3D + Окуляри + Cardboard + Cardboard (2014) + Без лінз + Відстань між очима + Ширина екрана + Відстань до екрана + Відстеження голови + На цьому пристрої немає датчика повороту. + Відцентрувати вигляд + Відцентрувати + SteamVR (експериментально) + Установити + Завантажує SteamVR від Valve для ARM64 (571 МБ). + Завантаження й розпакування SteamVR… + Запустити + Запускає SteamVR, де гарнітурою слугує ваша голова; зображення перемикається на стереопару 3D. + Зупинити + SteamVR працює. + Недоступно в цій сесії. + SteamVR не відповідає; перевірте журнал сесії. + Завантажити компоненти + Завантаження спільні зі сторінкою «Компоненти». Виберіть вище завантажену версію, щоб використати її під час наступного запуску гри. + Виберіть версію для завантаження + Розробник + Видавець + Дата виходу + Сюжет + + Додатково + 100% + %1$s год + %1$d хв + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 000000000..e481ecfb4 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,2210 @@ + + + DroidDeck + + Lần chạy đầu tải Steam. Quá trình này có thể mất vài phút. + Theo mặc định, rời khỏi ứng dụng sẽ tạm dừng Steam; nó tiếp tục từ nơi bạn dừng lại khi bạn quay lại. + Tay cầm của bạn hiện ra như tay cầm Xbox 360. Nếu không, hãy dùng các nút điều khiển trên màn hình. + + + Lần chạy đầu thiết lập desktop. Quá trình này có thể mất vài phút. + Theo mặc định, rời khỏi ứng dụng sẽ tạm dừng phiên; nó tiếp tục từ nơi bạn dừng lại khi bạn quay lại. + Chạm hoạt động như touchpad: kéo để di chuyển con trỏ, chạm để nhấp. + Tay cầm của bạn hiện ra như tay cầm Xbox 360. Nếu không, hãy dùng các nút điều khiển trên màn hình. + + Phiên + Hiển thị khi có phiên Steam hoặc desktop đang mở + Steam đang chạy + Đang tải Steam + Phiên đã tạm dừng - nhấn Tiếp tục khi sẵn sàng + Tiếp tục + Tạm dừng ngay + Dừng phiên + Tạo khung hình + Chọn chế độ tạo khung hình + Tắt + Win-FG %1$d× + LSFG %1$d× + LSFG thích ứng %1$d + Cần Lossless Scaling trên Steam + Cài Lossless Scaling trong Steam hoặc nhập Lossless.dll + Nhập Lossless.dll… + Đang dùng bản sao từ Steam + Đang dùng bản đã nhập + Chọn Lossless.dll + Đã nhập Lossless.dll + Lossless.dll này đã đang được dùng + Tài khoản Steam này không sở hữu Lossless Scaling + Tệp này không phải là Lossless.dll của Lossless Scaling + Không thể chuẩn bị LSFG từ Lossless.dll này + LSFG cần Lossless Scaling từ Steam hoặc một Lossless.dll đã nhập + LSFG không thể chuẩn bị shader từ Lossless.dll + %1$s: không hỗ trợ + %1$s: engine lỗi + %1$s: cần Lossless Scaling + %1$s %2$s · đang khởi động + Thêm một thư viện Steam và quét để tìm game Windows. Game đã sở hữu dùng mục Steam của nó; phần còn lại trở thành shortcut ngoài Steam. + Môi trường game + Biến môi trường cho game Proton. + Sửa + Tất cả game Proton + Hồ sơ + ID ứng dụng Steam + Mở + Game %1$s + Thêm biến + Tên biến + Giá trị + Lưu + Xóa + Kế thừa lại + Xóa các ghi đè môi trường + Bỏ đặt cho hồ sơ này + Chưa có biến nào được đặt. + Được kế thừa + Biến tùy chỉnh + Không thể tải hoặc lưu. Kiểm tra dung lượng trống và thử lại. + Dùng tên hợp lệ và giá trị tối đa 8192 ký tự. + Runtime Proton chuẩn không đọc biến này. + Đang tải… + Ghi đè cấp tính năng D3D12. 12_2 báo cáo khả năng DirectX 12 Ultimate; các tính năng không được hỗ trợ có thể gây lỗi. Để trống để tự động phát hiện. + Ghi đè shader model D3D12. 6_9 cần VKD3D đủ mới và tính năng GPU phù hợp. Để trống để tự động phát hiện. + Giữ bộ nhớ đệm shader của Mesa luôn bật để giảm biên dịch lặp lại. false sẽ bật bộ nhớ đệm. + Kích thước tối đa của bộ nhớ đệm shader Mesa, ví dụ 1G. Bộ nhớ đệm lớn hơn dùng nhiều dung lượng hơn. + Tùy chọn VKD3D cho D3D12. nodxr tắt dò tia; chỉ dùng cho game cần nó. + Đặt thành 1 để có log chẩn đoán Proton. Ghi log có thể giảm hiệu năng và dùng dung lượng. + Đặt thành 1 để dùng WineD3D dựa trên OpenGL thay cho DXVK khi kiểm tra tương thích D3D9–11. + Đặt thành 1 để giữ game này không dùng ntsync (droiddeck-ntsync hoặc driver kernel); nó sẽ dùng droiddeck-esync nếu có pack phù hợp, nếu không thì droiddeck-fsync. + Đặt thành 1 để tắt droiddeck-esync cho game này; nó sẽ dùng droiddeck-fsync thay vào đó, hoặc droiddeck-ntsync nếu fsync cũng tắt. + Đặt thành 1 để giữ game này không dùng droiddeck-fsync; nó sẽ dùng droiddeck-esync nếu có pack phù hợp, nếu không thì droiddeck-ntsync. + Đặt thành 1 để chạy game này trên droiddeck-fsync, hoặc 0 để ưu tiên droiddeck-esync nếu có pack phù hợp với Proton. + Đặt thành 0 để tắt trình hỗ trợ điều hướng bằng tay cầm của Proton cho game này. + Lớp phủ DXVK cho D3D9–11. fps hiển thị tốc độ khung hình; để trống để ẩn. + Các thiết lập DXVK ngăn cách bằng dấu chấm phẩy. Các bản build gần đây dùng dxvk.maxFrameRate để giới hạn khung hình; nên dùng bộ giới hạn khung hình của phiên khi chơi thông thường. + Giới hạn tốc độ khung hình D3D12. 0 tắt giới hạn. + Luồng OpenGL của Mesa. true có thể giúp các game OpenGL bị giới hạn bởi CPU; hãy thử trên từng game. + Đồng bộ Wine + Cách game giữ các luồng đồng bộ. Áp dụng từ phiên sau. + esync + ntsync + fsync + wineserver + Thiết lập trước FEX + Áp dụng vào lần khởi chạy game kế tiếp. + Game + Mặc định FEX + Dùng thiết lập mặc định của FEX. + Ổn định + Thiết lập an toàn nhất, chậm nhất. + Tương thích + TSO đầy đủ với multiblock. + Trung bình + TSO cân bằng với độ chính xác x87 giảm. + Hiệu năng + Tắt toàn bộ TSO; multiblock. + Hiệu năng + TSO + Thiết lập hiệu năng với TSO chính được bật. + Cực hạn + Tắt kiểm tra mã tự sửa đổi. Game dùng JIT và DRM có thể lỗi. + Cực hạn + TSO + Thiết lập cực hạn với TSO chính được bật. + Cực hạn-gn + Hiệu năng với gợi ý TSC và metadata. Thử trước khi dùng Cực hạn. + Denuvo + Hiệu năng với kiểm tra mã đầy đủ và ẩn bit hypervisor. + Giá trị tùy chỉnh + Chọn giá trị + Trình giả lập & ứng dụng + Thêm + Ứng dụng hoặc game của riêng bạn + Script + AppImage + Flatpak + %1$s · %2$s + %1$s · %2$s · %3$d%% + Đang khởi động… + Đã thêm %1$s + Đã xóa %1$s + Desktop + Ứng dụng đó không còn nữa. Chọn lại từ Desktop. + Mở %1$s + Xóa + Nhấn lại để xóa + Dừng phiên để xóa + Chạy %1$s từ thư mục của nó, được liên kết vào phiên tại chính vị trí đó. Xóa ứng dụng sẽ không ảnh hưởng đến thư mục đó. + Chạy %1$s từ một bản sao thư mục của nó bên trong DroidDeck. Xóa ứng dụng sẽ xóa bản sao, không phải bản gốc. + Cài runtime Linux trước + Tệp không còn tồn tại + Không đủ dung lượng trống: sao chép thư mục cần %1$s + Đang sao chép %1$s + Thêm ứng dụng không thành công + Đang tìm bản phát hành của %1$s + Không thể đọc bản phát hành của %1$s + Không có bản phát hành nào của %1$s có AppImage ARM64 hoặc x86_64 + Đang tải %1$s + Tải xuống không thành công + Cập nhật ứng dụng không thành công + Không thể đọc biểu tượng đó + Đã lưu %1$s + Sửa + Kiểm tra cập nhật + Đang kiểm tra cập nhật… + Cập nhật lên %1$s + Đã mới nhất · %1$s + Đã cập nhật %1$s + %1$s · %2$s + Thêm ứng dụng + Script + AppImage + GitHub + Flatpak + Một tệp .sh. Nó chạy từ thư mục của chính nó, toàn màn hình dưới gamescope. + Một AppImage ARM64 (aarch64), hoặc một bản x86_64 chạy qua FEX. Nó được giải nén vào runtime Linux một lần. + Cài AppImage ARM64 từ bản phát hành mới nhất của repository, hoặc bản x86_64 của nó qua FEX. + Dùng FEX + Tự động + Bật + Tắt + FEX chạy các chương trình Linux x86 trên thiết bị ARM64 này. Tự động sẽ dùng nó khi chương trình được build cho x86. + %1$s qua FEX + Chương trình này được build cho PC %1$s và chạy qua FEX, hiện chưa được thiết lập. Mở Steam một lần khi đã đăng nhập: DroidDeck sẽ tải FEX và Steam Linux Runtime cần thiết, sau đó ứng dụng này sẽ mở. + Chương trình này + %1$s được build cho PC x86 và chạy qua FEX, hiện chưa được cài. Khởi động Steam một lần khi đã đăng nhập và để nó tải FEX cùng Steam Linux Runtime, sau đó khởi động lại ứng dụng này. + Một app ID Flathub hoặc trang Flathub của nó. Flatpak sẽ được thiết lập trước nếu chưa có. + Tệp + Chưa chọn tệp + Chọn tệp + Đổi + Chọn một script + Chọn một AppImage + Chọn một biểu tượng + Repository + owner/repo hoặc một liên kết github.com + Không phải một repository GitHub + ID ứng dụng + org.ppsspp.PPSSPP + Không phải một app ID Flathub + Hoặc một tệp .flatpak, cho ứng dụng không có trên Flathub. + Chọn một tệp .flatpak + Nguồn + Trên Desktop của bạn + Ứng dụng mới + Tên + Từ ứng dụng + Biểu tượng + Biểu tượng mặc định + Chọn biểu tượng + Xóa + Từ repository + Đang tìm biểu tượng… + Mặc định + Sửa %1$s + Lưu + Đang kiểm tra thư mục… + Thư mục của nó được liên kết vào phiên: %1$s + Thư mục của nó chứa các chương trình Linux, mà bộ nhớ dùng chung không thể chạy, nên nó được sao chép vào DroidDeck (%1$s). + Thêm + Hủy + Cài runtime Linux từ Thiết lập trước. + Đợi %1$s hoàn tất. + Tương thích + Các bản build được đánh dấu “%1$s” trong menu Compatibility của Steam có các bản sửa prefix của DroidDeck và chạy game được ở đây. Mặc định là Proton Experimental ARM64 (%1$s), hoặc Proton 11.0 ARM64 (%1$s) khi bản mới hơn đó đã được cài. Các mục Proton gốc của Valve không có dấu này không thể chạy game trên thiết bị này. + Đã cài %1$s · %2$s + Hủy + Tự động + Không bao giờ + Luôn luôn + Gỡ cài đặt + Kiểm tra + Cập nhật + Đang kiểm tra… + Đang xử lý… + Quên + Thêm… + Dùng + Áp dụng từ phiên sau. + Phiên Steam + Phiên Desktop + Hiển thị + Độ phân giải + Tối đa 720p + Tối đa 900p + Tối đa 1080p + Gốc + %1$s (mặc định) + Tùy chỉnh · %1$d×%2$d + Tùy chỉnh… + 720p có thể giúp menu phản hồi nhanh hơn. + Tỷ lệ màn hình + Được đặt theo độ phân giải tùy chỉnh. + Tự động dùng tối thiểu 16:9. + Giới hạn khung hình + Giới hạn toàn bộ phiên. Màn hình chuyển sang tốc độ làm mới mà giới hạn chia hết. + HDR + Xuất HDR10 + Không khả dụng: %1$s. + Khởi động lại ứng dụng để áp dụng. + Driver + Driver GPU + Dùng chung cho Steam và Desktop. Đặt ở trang Components. + Chạm & điều khiển + Chạm + Touchpad + Trực tiếp + Tự động: touchpad ở Desktop, trực tiếp ở Steam. + Điều khiển trên màn hình + Steam + QAM + Tự động hiện các nút điều khiển khi không có tay cầm kết nối. + Chạm Steam (beta) + Các nút chạm của Steam bắt đầu từ phiên Steam kế tiếp + Bố cục chạm Steam + Di chuyển, đổi kích thước hoặc ẩn các nút chạm của game này; được lưu vào Steam + Tay cầm + Cách Steam nhận dạng tay cầm của bạn. Áp dụng từ phiên sau. + Tay cầm Steam Deck + Steam Controller + DualShock 4 + DualSense + Tay cầm Xbox 360 + Deck: nút Quick Access và con quay hồi chuyển. Steam Controller: tương tự, cộng với cảm biến nắm ở các nút phía sau. DualShock 4 và DualSense: con quay hồi chuyển, và touchpad dùng làm bàn di chuột. Xbox 360: mở Quick Access bằng Guide+A. + Quay lại + Menu → QAM + QAM → Menu + Phiên + Hành vi khi ở nền + Khi ứng dụng bị ẩn hoặc màn hình tắt. + Thủ công + Gốc + Tự động tạm dừng và tiếp tục khi được hiển thị lại. Gốc chuẩn bị Steam để ngủ trước. Thủ công chờ nhấn Tiếp tục. Không bao giờ giữ nó luôn chạy. + Giữ tải xuống tiếp tục chạy + Trì hoãn tạm dừng nền khi Steam đang tải xuống. + Tự động tạm dừng ở nền và tiếp tục khi hiển thị lại. Thủ công chờ nhấn Tiếp tục. Không bao giờ giữ nó luôn chạy. + Khởi động + Khởi động Steam cùng DroidDeck + Mở phiên Steam khi DroidDeck khởi chạy. + Decky + Đang kiểm tra bản phát hành… + Sẵn sàng cài %1$s. + Không tìm thấy bản build tương thích. Hãy thử lại sau. + Đã cài · %1$s + Có cập nhật · %1$s + Đã mới nhất · %1$s + Loader + Cài bản mới nhất + Khởi động cùng Steam. Khi đang bật, các ứng dụng khác trên thiết bị này có thể điều khiển được Steam. + Tắt. Cổng gỡ lỗi từ xa của Steam luôn đóng. + Gỡ cài đặt Decky Loader? + Gỡ bỏ loader. Plugin và thiết lập của bạn được giữ lại. + Client + Lớp phủ hiệu năng + Tắt nếu game bị crash hoặc màn hình đen ở Chế độ Deck. Áp dụng từ phiên sau. + Sửa chữa client Steam + Tải lại client Steam vào lần Steam khởi động kế tiếp. Dùng khi Steam bị kẹt ở màn hình tải. Game, đăng nhập và thiết lập được giữ lại. + Client Steam sẽ được tải lại vào lần Steam khởi động kế tiếp. + Sửa chữa + Hình ảnh + API key SteamGridDB + Key của bạn + Sửa… + Điền hình ảnh còn thiếu từ Steam, sau đó từ SteamGridDB. Tệp cover.jpg, hero.jpg, logo.png hoặc icon.png của riêng bạn trong thư mục game được ưu tiên. + Xóa %1$s + Xóa? + Thêm game + Thêm một game: + Chọn tệp exe: + Vị trí này không khả dụng trong phiên + Kiểm tra tệp exe này + Thành phần Windows + LB / RB để chuyển giữa các mục. + Không có + Các DLL Windows mà Proton không bao gồm, từ danh sách thành phần. Bật những thành phần game này cần: chúng được đặt vào thư mục Windows riêng của game, không phải tệp của nó, và được giữ lại dù nó chạy trên Proton nào. + Không thể kết nối danh sách thành phần. Các thành phần đã tải trước đó vẫn có thể chuyển đổi. + Được đề xuất cho game này + Đang kiểm tra game này cần gì… + Khả dụng khi có mạng + + Đang bật %1$d thành phần… + + Đang bật cho game này + Steam cài %1$s cùng %2$s (app %3$s) + Từ danh sách của GOG cho game này + Từ các yêu cầu của Epic cho game này + Từ các bước cài đặt của Amazon cho game này + Steam cài %1$s cùng app %2$s, khớp theo tên + Cũng được liệt kê + Được %1$s liệt kê; không phải thành phần ở đây + Steam + GOG + Epic Games + Amazon Games + Game đi kèm %1$s + Game có %1$s (thường hoạt động được ngay) + Đã có trong game này + Không có gì trong thư mục game chỉ đến một thành phần. + Đã cài cho game này (%1$d) + Tất cả thành phần (%1$d) + Cần bộ cài Windows (%1$d) + Đây là các chương trình cài đặt riêng, không có gói bên trong để phân tích, và chưa có ghi nhận về những gì chúng cài. Hầu hết là các runtime cũ mà Mono trong Proton đã thay thế; các ghi nhận được bổ sung khi có. + Hiện + Ẩn + Chạy chương trình cài đặt riêng (chưa có ghi nhận) + Chưa được hỗ trợ ở đây + Đang tải các thành phần… + Không thể cài: %1$s + Đã tải + Đang tải %1$s + Đang cài: %1$s + Đang tắt… + Tải %1$s + Có hiệu lực vào lần game khởi động kế tiếp. + Kéo giãn game 16:9 + Trên màn hình vuông hơn 16:9, kéo giãn game để lấp đầy thay vì hiện viền đen. Áp dụng từ phiên sau. + Âm thanh + DirectAudio cho game + Bỏ qua PulseAudio để giảm độ trễ. + Microphone + Dùng cho chat bằng giọng nói. + Mạng + Dò tìm Wi-Fi + Hiện tên Wi-Fi và các mạng gần đó trong Steam. + Dịch vụ vị trí đang tắt. + Chưa cấp quyền vị trí. + Mở thiết lập + Tiếp tục + Bật dò tìm Wi-Fi? + Android yêu cầu quyền Vị trí để đọc tên Wi-Fi và dò tìm các mạng gần đó. DroidDeck dùng quyền này để hiện các mạng đó trong Steam. Chúng tôi không theo dõi vị trí của bạn. Truy cập Internet vẫn hoạt động mà không cần quyền này. + Lưu trữ game + Tự động - thẻ SD khi có, cùng các thư mục Games của bạn + Tự động - các thư mục Games của bạn, cùng thẻ SD khi có (hiện không có) + Chỉ bộ nhớ trong + Thư mục: %1$s + Thư viện thứ hai + Game truyền dữ liệu từ thẻ SD hoặc bộ nhớ dùng chung có thể bị giật. + Chẩn đoán lưu trữ + Ghi lại kết quả cấp phát và thời gian lưu trữ mẫu vào Share logs. Có thể làm chậm cài đặt khi đang bật. Áp dụng từ phiên sau. + Chọn một thư mục… + Độ phân giải tùy chỉnh + Thay thế giới hạn và tỷ lệ. Kích thước không khớp màn hình sẽ có viền đen. + Chiều rộng + Chiều cao + Từ 320×240 đến 3840×2160. + + %1$d game + + OK + Xóa + Khôi phục + Tải xuống + Xóa %1$s + %1$.1f MB + Driver runtime + Dùng bởi Steam, game và desktop. Áp dụng từ phiên sau. + Nhập tệp zip Turnip… + Driver hiển thị + Vẽ khung hình lên màn hình ở cả hai chế độ. Khởi động lại ứng dụng để áp dụng. + Nhập tệp zip AdrenoTools… + chưa bao giờ + Đã kiểm tra Nightlies %1$s + Thành phần + Về Thành phần + Kiểm tra driver mới + Kiểm tra Nightlies để tìm gói mới + Proton + Proton được chọn ở đây chạy mọi game chưa chọn Proton riêng trong Steam, và một thay đổi thực hiện trong Steam cũng hiện ở đây. Steam thay thế Proton của Valve khi nó cập nhật; các bản gốc được giữ lại theo từng build. + Cấu hình + Cấu hình hồ sơ + Hồ sơ + Hồ sơ mặc định + Được dùng tự động cho game không có ghi đè. + Thiết lập đã lưu được dùng tự động khi %1$s khởi chạy. + Chỉ chọn những thiết lập game này cần. Mọi thứ khác tiếp tục dùng Hồ sơ mặc định. + Thiết lập trước FEX + Đang dùng thiết lập trước của Hồ sơ mặc định. + Thiết lập trước tùy chỉnh đã lưu cho game này. + Biến FEX tùy chỉnh + Dùng Mặc định + Tương thích game + Các tùy chọn môi trường Proton, DXVK, VKD3D và đồng bộ hóa cho game này. + Đang dùng Proton của Hồ sơ mặc định. + Proton tùy chỉnh đã lưu cho game này qua ánh xạ tương thích của Steam. + Không có Proton + DXVK + VKD3D-Proton + Dùng gói của Hồ sơ mặc định từ %1$s trừ khi game này chọn gói khác. + Dùng gói thành phần của Hồ sơ mặc định. + Chỉ dùng %1$s cho game này. + Tùy chọn runtime + Các tùy chọn môi trường Proton, DXVK, VKD3D-Proton và đồng bộ hóa khác cho từng game. + Hồ sơ của %1$s được lưu cho lần khởi chạy game kế tiếp. + Khởi động không cần Steam + Dành cho game đóng ngay lập tức: khởi động game mà không cần thiết lập Steam của Proton. Các tính năng Steam như overlay sẽ tắt đối với game này. + Bật + Lựa chọn Proton của %1$s đã được lưu và khả dụng ngay trong Steam. + Lựa chọn Proton của %1$s đã được lưu. Khởi động lại Steam trước khi chạy game. + %1$s không thể dùng Proton đã chọn trên thiết bị này. + Không thể tải hồ sơ của %1$s: %2$s + Không thể lưu hồ sơ game: %1$s + Đặt lại hồ sơ game + Đặt lại hồ sơ của %1$s? + Xóa Proton, FEX, gói thành phần, môi trường, lọc texture và các ghi đè thành phần Windows của game này? Nó sẽ dùng Hồ sơ mặc định. + %1$s giờ dùng Hồ sơ mặc định. + Không thể đặt lại hoàn toàn hồ sơ game: %1$s + %1$s giờ chạy mọi game chưa chọn Proton riêng trong Steam. + %1$s sẽ chạy mọi game chưa chọn Proton riêng, từ lần Steam khởi động kế tiếp. + %1$s không thể khởi chạy game ở đây, nên không thể là mặc định. + Không thể lưu Proton mặc định: %1$s + %1$s · %2$d đã thay + %1$s · toàn bộ gốc + %1$s · game đang chạy + %1$s · pack droiddeck-esync + %1$s · chưa có pack droiddeck-esync + %1$s · esync có sẵn, không cần pack + Driver GPU + %1$s · đã áp dụng lại lúc khởi chạy %2$s + Tiếp theo: %1$s · sau khi game đóng + %1$s đang dùng: %2$s + Chờ đến khi game đóng + Áp dụng vào lần game khởi động kế tiếp + Nhập tệp .zip + Đang đọc Proton… + Chưa cài Proton nào. Khởi động Steam một lần để tải Proton ARM64 của nó, hoặc thêm GE-Proton, CachyOS hay DroidDeck-Proton từ Thiết lập. + Gốc · %1$s + %1$s · build này + %1$s · build trước + Đã cài · %1$d + Chưa có gì được lưu cho %1$s. Các tệp gốc của Proton này được lưu làm bản gốc. + Một game đang chạy trên Proton này, nên việc này chờ đến khi nó đóng. + Áp dụng vào lần game khởi động kế tiếp. + Thay %1$s? + Đưa %1$s vào %2$s? Các tệp gốc của nó vẫn được giữ lại. %3$s + Khôi phục %1$s? + Đưa bản gốc %1$s trở lại %2$s? %3$s + Xóa gói khỏi ứng dụng. Bạn có thể tải lại nó. + Xóa bản gốc này? + Bản gốc %1$s này là từ một build trước của %2$s. Bản gốc của build hiện tại luôn được giữ lại. + Trên Nightlies · %1$d + Làm mới để liệt kê các gói trên Nightlies. + Không có gì mới từ lần kiểm tra trước. + Nhập tệp .wcp + Khôi phục bản gốc của Proton này + Đưa %2$s gốc của %1$s trở lại? + Thay + FEX, DXVK và VKD3D-Proton cho mỗi Proton mà Steam dùng để chạy game.\n\nChọn một dòng đã cài để thay vào. Áp dụng vào lần game khởi động kế tiếp, hoặc sau khi game đang chạy đóng lại.\n\nCác lựa chọn của bạn được kiểm tra lại trước mỗi lần khởi chạy và được đặt lại nếu có gì thay đổi chúng. Các tệp gốc của mỗi Proton được giữ lại, nên các bản cập nhật Steam không bao giờ làm mất chúng. + Gốc + Đang dùng + Mới + Đã lưu + Hiệu năng + Lõi CPU, các bản sửa phiên và tên máy chủ. Áp dụng từ phiên sau. + Lõi client Steam + Ghi đè lựa chọn lõi của Steam + Ghim Steam, trình trợ giúp giao diện của nó và gamescope vào các lõi dưới đây. + Lõi client + Lõi dành cho Steam. + Bật ghi đè để chọn. + Lõi game + Lõi game + Lõi dành cho game. Khi chọn tất cả, Android tự do lập lịch. + Giao diện client + GL đa luồng + Có thể giúp menu Steam phản hồi nhanh hơn. + Zink: descriptor trễ (lazy) + Khuyến nghị cho driver không có descriptor buffer. + Bỏ qua kiểm tra lỗi GL + Tắt kiểm tra hợp lệ GL theo từng lệnh gọi. + gamescope: hàng đợi GPU thời gian thực + Ưu tiên công việc GPU của compositor. Có thể làm nhịp khung hình mượt hơn với một phần chi phí cho game. + GPU + Giữ GPU ở xung nhịp cao nhất + Chỉ dành cho Adreno. Ít giật hơn, dùng nhiều pin và nhiệt hơn. + Các bản sửa phiên + Turnip: render sysmem + Nhanh hơn cho Steam và hầu hết game. Bắt buộc trên Adreno 710/720/722. + Bỏ qua trình trợ giúp xalia của Steam + droiddeck-ntsync (thử nghiệm) + Một bản thay thế ở không gian người dùng cho driver ntsync của Linux, dùng để thử nghiệm. Khi đang bật, Proton dùng nó thay cho droiddeck-esync và droiddeck-fsync. Hoạt động với Proton Experimental ARM64, GE-Proton và proton-cachyos. Áp dụng từ phiên sau. + droiddeck-esync + droiddeck-fsync trước + Chạy fsync riêng của Proton trên các tệp Proton gốc, với futex_waitv được cung cấp ở không gian người dùng, nên không cần pack và vẫn hoạt động ngay khi Steam cập nhật Proton. Khi đang bật, game dùng nó trước droiddeck-esync, trừ một số ít Proton giữ fsync tắt. Áp dụng từ phiên sau. + Các luồng Windows của game chờ nhau qua eventfd và bộ nhớ dùng chung, thay vì đi vòng qua wineserver. Dùng pack droiddeck-esync được làm riêng cho đúng bản build Proton đó; pack được tự động tải về. Cho đến khi có pack cho một bản build Proton mới, droiddeck-fsync sẽ thay thế. Áp dụng từ phiên sau. + Tắt trình hỗ trợ điều hướng bằng tay cầm của Proton, vốn tốn CPU trong mọi game. + Chạy proot không có seccomp + Có thể sửa lỗi thiếu syscall trên một số kernel. Có thể giảm hiệu năng. + Fast path của proot + Cần bộ lọc seccomp của proot. + Tra cứu tệp nhanh hơn và ít giật hơn. Tắt nếu một chương trình không tìm thấy tệp của nó. + Danh tính phiên + Tên máy chủ + Tên mà Steam thấy cho thiết bị này. Để trống để khôi phục %1$s. + Chỉ chữ, số và gạch ngang ở giữa, tối đa 63 ký tự. + Android được đặt để tắt phiên này + Đã cài + Có thể tải + Không có gì mới từ lần kiểm tra trước. + Khôi phục driver tích hợp + Được tích hợp trong ứng dụng: việc này xóa các tệp đã giải nén của nó và ẩn nó đi. Khôi phục driver tích hợp sẽ đưa nó trở lại. + Một nửa của gói Android + Linux: cả hai nửa đều bị xóa. Driver đang dùng sẽ chuyển về mặc định. + Các tệp của nó bị xóa khỏi ứng dụng. + Các tệp của nó bị xóa. Nó đang được dùng, nên mặc định sẽ thay thế. + Hiển thị + Hiệu ứng + Điều khiển + Thành phần + Tay cầm + Thiết lập + Trang %1$s + Steam + Desktop + Menu Steam + Truy cập nhanh + Mở Quick Access Menu + Trang trước + Trang sau + HUD hiệu năng + Chế độ chia tỷ lệ + Độ sắc nét khi chia tỷ lệ + Hiệu ứng màn hình + Thời gian thực. Tốn một ít thời gian GPU khi có hiệu ứng đang bật. + Thiết lập dựng sẵn + Tùy chỉnh + Nâng cao + Mở rộng + Thu gọn + Làm nét (CAS) + Mức làm nét + HDR giả + Khử dải màu + Cường độ khử dải màu + Độ sáng + Độ tương phản + Gamma + Độ bão hòa + FXAA (làm mượt cạnh) + Viền hoạt hình + CRT + NTSC + Lọc texture + Game DirectX 9–11, từ lần khởi chạy kế tiếp. + Lọc dị hướng (anisotropic) + Độ sắc nét texture + Một độ lệch mip âm: texture sắc nét hơn, có thể nhấp nháy nhẹ. Tự động khớp với mức phiên được phóng to so với màn hình. + Tự động (%1$s) + Bàn phím + Bàn phím PC + Android + Màn hình thứ hai + Chế độ + Hiển thị + Ứng dụng Android + Khởi chạy một ứng dụng + Ẩn + Hiện + Không có ứng dụng nào để khởi chạy + Hành vi phiên + Áp dụng ngay và cho các phiên sau ở chế độ này. + Phiên sau + Hỗ trợ + Log phiên + Đính kèm vào báo cáo lỗi + Chia sẻ log + Nền + Dừng + Dừng phiên? + Đang chạy + Không có game nào đang chạy trên một Proton + Hồ sơ game + Chi tiết hồ sơ chưa khả dụng. + Không thể tải hồ sơ game. Đang tự động thử lại. + Không thể làm mới hồ sơ game. Đang hiện dữ liệu khả dụng gần nhất; đang tự động thử lại. + Xem và sửa các thiết lập đã lưu được dùng ở lần khởi chạy kế tiếp. + Đang dùng Hồ sơ mặc định + Hồ sơ tùy chỉnh + Đang dùng %1$s từ Hồ sơ mặc định. + Thay đổi được lưu vào hồ sơ của %1$s và áp dụng vào lần game khởi chạy kế tiếp. + + %1$d ghi đè game + + Chưa cài Proton nào. + game đang chạy + đã áp dụng lại lúc khởi chạy %1$s + Gốc + Nhập và xóa ở trang Components. + Hoàn tất + Cần chú ý + Đang xử lý + Kiểm tra địa chỉ IP và cổng của Gỡ lỗi qua mạng không dây, hoặc ghép nối lại nếu Android đã gỡ thiết bị này. + Cài + Quản lý + Tổng quan + Tay cầm + Phiên + Launcher + Thiết lập + ● Đã sẵn sàng + %1$d trong %2$d đã sẵn sàng + Kiểm tra hệ thống + Thiết bị được hỗ trợ + GPU chưa được kiểm thử + GPU không được hỗ trợ + %1$s · %2$s + %1$s: %2$s + Steam cần driver Adreno; %1$s có thể hiện màn hình đen + Runtime Linux + %1$s · %2$d%% + Chưa cài · khoảng 3 GB, được cài vào lần Play đầu tiên + Đã cài + %1$s · có cập nhật + %1$s · đã mới nhất + Giới hạn tiến trình con + Đang bật · Android có thể đóng Steam. Tắt nó mất khoảng một phút. + Chưa đặt · Android có thể đóng Steam. Tắt nó mất khoảng một phút. + Không thể kiểm tra · tắt nó mất khoảng một phút. + Khắc phục + Chi tiết + Tùy chọn cho nhà phát triển + Dùng Gỡ lỗi qua mạng không dây + Kiểm tra lại + Bật giới hạn + Tài khoản Steam + Đã đăng nhập với %1$s · chế độ ngoại tuyến + Đã đăng nhập với %1$s + Nhấn Play và đăng nhập vào Steam + Công cụ + Tay cầm + Thiết lập tay cầm không khả dụng. + Phiên + Log phiên + Được lưu sau mỗi phiên + Công cụ gỡ lỗi + Log phiên gần nhất + Log phiên đã lưu + Giữ lại %1$d bản mới nhất + Xóa log + Chế độ ngoại tuyến + Đăng nhập Steam trước + Launcher + Ngôn ngữ + Steam cũng dùng ngôn ngữ này, từ lần khởi động kế tiếp. + Theo hệ thống (%1$s) + Theo hệ thống + Tỷ lệ ứng dụng + Văn bản và điều khiển của DroidDeck + %1$d%% (Mặc định) + Giao diện + Giao diện launcher + Dùng làm màn hình chính + DroidDeck có thể là ứng dụng Màn hình chính + DroidDeck không bao giờ được đề xuất làm ứng dụng Màn hình chính + Hướng màn hình + Ngang tự động + Ngang + Ngang ngược + Toàn màn hình + Ẩn thanh trạng thái và thanh điều hướng của Android + Hiện thanh trạng thái và thanh điều hướng của Android + Ứng dụng Màn hình chính mặc định + Chọn một ứng dụng Màn hình chính + Chọn + Ứng dụng Linux (beta) + Tệp + Duyệt và quản lý tệp + Phiên bản Proton + Cài các bản build Proton ARM64 + Hiệu năng + Phân bổ lõi CPU + Thư mục ROM + Nơi lưu game của trình giả lập + Thành phần + FEX, DXVK, VKD3D + Điều khiển + Gán nút + Khám phá + Tìm kiếm + Đã cài + %1$s · %2$d + Store + Flathub · ARM64 + ● Flatpak đã sẵn sàng + Cài runtime Linux từ Thiết lập trước. + Thiết lập Flatpak + Thêm Flatpak và kết nối nó với Flathub (khoảng 20 MB). Ứng dụng đầu tiên cũng tải runtime của nó. + Đang thiết lập… + Flatpak + Cập nhật + %1$s · %2$s + %1$s · %2$s · %3$d%% + Đang khởi động… + Danh mục + Không thể kết nối Flathub. Kiểm tra kết nối của bạn và thử lại. + Thử lại + Tìm trên Flathub + Firefox, game, trình giả lập… + Tìm kiếm + Tất cả + Đang tìm… + Tìm theo tên hoặc chọn một danh mục. Chỉ các ứng dụng ARM64 được liệt kê. + Không có ứng dụng ARM64 nào trên Flathub khớp với nội dung đó. + Kết quả + Kiểm tra cập nhật + Cập nhật tất cả (%1$d) + ● Mọi thứ đã mới nhất + Chưa có ứng dụng nào. Tìm vài ứng dụng ở Khám phá hoặc Tìm kiếm. + Thiết lập Flatpak để cài ứng dụng. + Có cập nhật · %1$s + Mở + Chi tiết + Xóa + Nhấn lại để xóa + Đang cài… + Cài + Thiết lập Flatpak trước + Tải %1$s + ● Đã cài + Không thể tải trang Flathub của ứng dụng này. + Đang tải từ Flathub… + Không có bản build ARM64 trên Flathub, nên không thể cài ở đây. + Ảnh chụp màn hình + Giới thiệu + ID ứng dụng + Giấy phép + Runtime + Kích thước đã cài + %1$s (cộng với runtime dùng chung của nó) + Trang web + Đã xác minh + Cập nhật + DroidDeck %1$s · %2$s + Đã kiểm tra %1$s + Chưa kiểm tra + Cho phép DroidDeck tự cập nhật + Ở màn hình kế tiếp, bật Cho phép từ nguồn này, rồi quay lại. Việc cập nhật sẽ tự tiếp tục. + Mở thiết lập + Không phải bây giờ + Ký khác nhau + Không thể cập nhật tại chỗ + Bản này không được ký bằng khóa phát hành của DroidDeck, nên bản cập nhật không thể cài chồng lên nó. Gỡ cài đặt nó để chuyển. + Có cập nhật + Không thể cài + Sẵn sàng chuyển + Trước Stable + Bạn đang trước Stable + Bản build này mới hơn bản Stable gần nhất. Bạn sẽ chuyển sang Stable ở bản phát hành kế tiếp của nó. + Thử nghiệm đã kết thúc + Thử nghiệm này đã kết thúc + Bản sửa của nó đã được gộp hoặc bỏ. Theo dõi Preview để có các bản sửa mới nhất. + Đã mới nhất + Bạn đã có bản %1$s mới nhất + · %1$s + %1$s… %2$d%% + %1$s… + Cập nhật%1$s + Cài%1$s + Vẫn cài Stable %1$s + Theo dõi Preview + Dừng phiên đang chạy để cập nhật. + Ít hơn + Thêm + DroidDeck %1$s + Bản Preview mới + Bản thử nghiệm PR #%1$d + %1$d MB + KÊNH CẬP NHẬT + Stable + Các bản phát hành đã kiểm thử, dành cho hầu hết người dùng + %1$s · %2$s + Preview + Các bản sửa mới nhất, trước Stable + Đã mới nhất + Bản thử nghiệm + Thử một bản sửa trước khi nó được phát hành + Hiện không có + PR #%1$d + Không được build cho bản DroidDeck này + Đã cập nhật %1$s + thử nghiệm PR #%1$d + một lúc trước + vừa xong + + %1$d để thử + + + Thay đổi từ bản của bạn · chậm %1$d bản build + + + Thay đổi gần đây · %1$d bản build + + Một thiết lập Android cần đổi + Steam + Android dừng các ứng dụng khởi chạy nhiều tiến trình nền, và Steam khởi chạy hàng chục tiến trình. Cho đến khi thay đổi điều này, game sẽ đóng mà không có lỗi. + Khắc phục giúp tôi + Bật “Disable child process restrictions”, rồi quay lại. Giữ Tùy chọn cho nhà phát triển luôn mở, nếu không công tắc sẽ tắt lại. + Mở Tùy chọn cho nhà phát triển + Phiên bản Android này không có công tắc cho việc đó, nên DroidDeck thay đổi nó qua Gỡ lỗi qua mạng không dây. Mất khoảng một phút, không cần máy tính. + Gỡ lỗi qua mạng không dây cần Wi-Fi. Mạng nào cũng được; không cần Internet. + Mã ghép nối nằm trong một thông báo. Hãy cho phép thông báo cho DroidDeck, hoặc dùng Cách khác. + Cho phép thông báo + Mở Settings → Developer options → Wireless debugging và bật nó lên + Nhấn “Pair device with pairing code” + Nhập mã vào thông báo của DroidDeck + Đang chờ cửa sổ bật lên ghép nối… + Đã tìm thấy cửa sổ bật lên. Nhập mã của nó vào thông báo. + Trạng thái + Đang kiểm tra mỗi 2 giây. Trang này sẽ đóng khi nó tắt. + Cách khác + Gỡ lỗi qua mạng không dây, địa chỉ thủ công, ADB từ máy tính + Địa chỉ thủ công, ADB từ máy tính, Tùy chọn cho nhà phát triển + Dùng Gỡ lỗi qua mạng không dây thay vào đó + Một số ROM vẫn thêm công tắc này. Tìm “child process” trong Tùy chọn cho nhà phát triển. + Nếu thông báo không hoạt động, hãy tự nhập địa chỉ và mã của cửa sổ bật lên. + Nhập địa chỉ thủ công + Từ một máy tính có ADB: + Sao chép lệnh + Chọn màn hình để mở Android Settings. + Màn hình chính + Đã ghép nối. Đang tìm cổng Gỡ lỗi qua mạng không dây… + Đã ghép nối. Nhập cổng từ Settings. + Đã tìm thấy thiết bị. Đang áp dụng thiết lập… + Không thể áp dụng tự động. Kiểm tra địa chỉ dưới đây và thử lại. %1$s + Gỡ lỗi qua mạng không dây + Đã cập nhật giới hạn tiến trình con + Đổi giới hạn tiến trình con + Nếu thiếu tùy chọn này, hãy ghép nối qua Gỡ lỗi qua mạng không dây. + Đã ghép nối. Kiểm tra địa chỉ IP và cổng trong Gỡ lỗi qua mạng không dây. + Giới hạn tiến trình con đã được cập nhật. + Dùng bước 2 nếu Tùy chọn cho nhà phát triển không có thiết lập tiến trình con. Mở Developer options → Wireless debugging → Pair device with pairing code. + Đã ghép nối. Dùng địa chỉ dưới đây, hoặc cập nhật nó từ Wireless debugging → IP address & Port. + Giới hạn tiến trình con đã được cập nhật qua Gỡ lỗi qua mạng không dây. + Android xác nhận giới hạn đang bật. + Android xác nhận giới hạn đang tắt. + 2 · Ghép nối Gỡ lỗi qua mạng không dây + Địa chỉ IP & cổng của cửa sổ bật lên ghép nối + Mã ghép nối (PIN) + 6 số + Ghép nối + 2 · Áp dụng qua Gỡ lỗi qua mạng không dây + Địa chỉ IP & cổng kết nối + Ghép nối lại + Tắt giới hạn + Cách khác để đổi nó + 1 · Tùy chọn cho nhà phát triển (thử trước) + Đặt “Restrict child processes” thành bật. + Đặt “Restrict child processes” thành tắt. + 3 · ADB từ máy tính (phương án cuối) + Chỉ khi Gỡ lỗi qua mạng không dây không khả dụng. + 3 · ADB từ máy tính (phương án cuối) + Khi các nút điều khiển chạm xuất hiện trong một phiên + Màu + Màu sắc của các nút trên màn hình + Tùy chỉnh + Độ mờ + %1$d%% + Kích thước nút + 100% là kích thước chuẩn + Nhấn cần + Chạm hai lần và giữ một cần để dùng L3 hoặc R3 + Cần thích ứng + Rung khi chơi game + Nút Steam + Nút QAM + Nút bàn phím + Nhấn Select để mở menu Steam + Thay cho View/Select. + Hiệu ứng chuyển động + Cần xuất hiện ở nơi bạn chạm và ẩn khi thả ra + Bố cục + Vị trí tùy chỉnh đã lưu + Vừa khớp với màn hình này + Sửa + Đặt lại + Gán nút + Mỗi nút gửi tín hiệu riêng của nó + %1$d trong %2$d nút đã được gán lại + Cấu hình + Đặt lại tay cầm + Khôi phục mọi thiết lập tay cầm và bố cục + Mỗi nút trên màn hình gửi gì đến game. Ẩn sẽ xóa nút đó. + Nút trên màn hình + Mặc định + Đặt lại gán nút + Ứng dụng Android + %1$d ứng dụng + Không tìm thấy ứng dụng Android nào có thể khởi chạy. + ● Đã cài desktop + Cài ở lần mở đầu tiên + Có thể cài + Chưa cài. + %1$s · chưa cài + Mở %1$s + Games + Chọn một thư mục ROM. + Duyệt đến /root/ROMs trong RetroArch. + Thêm game %1$s vào ROMs/%2$s. + Cài %1$s + Dừng phiên để cài + Cần runtime + Đang tải chi tiết cài đặt… + Chi tiết cài đặt không khả dụng. Thử lại sau. + Cài runtime Linux từ Thiết lập trước. + Dừng phiên đang chạy để cài ứng dụng desktop. + Cài %1$s vào desktop Linux. + %1$s · %2$d%% + Ẩn sẽ gỡ nó khỏi Desktop. Các tệp của nó vẫn ở trong runtime Linux. + Game đó không còn trong thư mục ROM. + Desktop · %1$s + Khởi chạy trong %1$s + Cũng có trong %1$s + Trang đó không còn nữa. Chọn một mục ở bên trái. + Mở desktop + Cài & mở desktop + Gỡ cài đặt desktop + Gỡ desktop và các ứng dụng đi kèm. Giữ lại game, ứng dụng đã thêm, thiết lập và thư viện dùng chung. + Đang gỡ desktop + Đã gỡ desktop + Đang xóa tệp LXQt cũ + Cập nhật DroidDeck trước khi gỡ phiên bản desktop này. + Desktop Linux + KDE Plasma, Firefox và các trình giả lập của bạn + Game bạn cài trong Steam sẽ xuất hiện ở đây sau phiên. + Chơi trên Steam + Thiết lập khởi chạy + Khởi chạy + Tệp game + Prefix Proton + Đang tìm các lần lưu… + Không tìm thấy thư mục lưu theo tên. Xuất sẽ lấy toàn bộ thư mục người dùng. + %1$d thư mục lưu + %1$s · %2$d tệp · %3$s + %1$d KB + Quản lý tệp lưu + Tệp lưu game + Nhập tệp lưu… + Xuất dưới dạng zip GameHub… + Xuất dưới dạng zip Winlator… + Mở thư mục lưu + Chưa chơi bao giờ + Runtime đang bận · %1$d%% + Runtime đang bận + Runtime sẽ cài ở lần Play đầu tiên + Có cập nhật runtime + ● Runtime đã sẵn sàng + Chơi lần cuối %1$s + Bộ nhớ trong + Game đã thêm + Game tùy chỉnh + Lưu trữ game + Cặp tốt nhất cho GPU này, luôn được cập nhật + Tự chọn một cặp + Đang kiểm tra driver… + Chưa có driver nào được liệt kê. Làm mới để tìm. + Ẩn driver cho GPU khác + Hiện driver cho GPU khác + Nâng cao + Được đặt cùng với driver hiển thị: %1$s mang cả hai. + Được đặt cùng với driver runtime: %1$s mang cả hai. + Dùng bởi game và desktop. + Mặc định runtime + Vẽ khung hình lên màn hình. Áp dụng sau khi DroidDeck khởi động lại. + Tự động - chọn theo GPU + Tải, nhập hoặc xóa… + ĐANG DÙNG + Các driver tích hợp trong DroidDeck + Driver runtime và hiển thị được đặt cùng nhau thành một cặp khớp. + Được đề xuất cho GPU này: %1$s. + Làm mới để tìm driver cho GPU này. + ĐỀ XUẤT + Không khả dụng + Runtime Linux + Đang chuẩn bị runtime Linux + Phiên + Đang khởi động hiển thị và âm thanh + Client Steam + Đang kiểm tra Steam và các công cụ tương thích của nó + Đang mở Steam + Đang chờ khung hình đầu tiên của Steam + Desktop + Đang chuẩn bị desktop + Đang mở + Đang chờ khung hình đầu tiên + Đang khởi động Steam + Phiên đã kết thúc + Bước %1$d trong %2$d + Steam đang tạm dừng + Grip + Pad trái + Pad phải + Cả hai + Gamepad + Cần + Trackpad + Chuyển giữa cần và trackpad + Thêm điều khiển Deck + BẢNG + Đóng điều khiển màn hình phụ + Mở menu Steam + Trái · trên + Trái · dưới + Phải · trên + Phải · dưới + Nhấn + Trackpad phải + Trackpad trái + Trackpad + Hiện bàn phím + Ẩn bàn phím + Terminal Linux + Phiên Linux chưa sẵn sàng cho terminal. + Steam + Games + Desktop + Store + Components + Ứng dụng + Thiết lập + Cập nhật + %1$d%% + Tiếp tục %1$s + Hiển thị như /root/ROMs trong mỗi phiên. Bộ nhớ trong ở /root/Storage. + Chưa chọn thư mục + Áp dụng từ phiên sau. Thẻ SD hoạt động được. + Chọn thư mục + Đổi thư mục + Cài một bản build Proton ARM64, sau đó chọn nó cho từng game trong Steam → Properties → Compatibility. + Dừng phiên đang chạy để cài hoặc gỡ một công cụ tương thích. + Các bản build khả dụng + Đã cài %1$s + Đang chờ từ một yêu cầu trước + Chưa cài + Có cập nhật → %1$s + Xóa các phiên bản %1$s cũ + Xóa %1$s. Game đang đặt dùng các phiên bản này phải chuyển sang một Proton khác. + Game đang đặt dùng phiên bản này phải chuyển sang một Proton khác. + Cài ngay + Hủy hàng chờ + Bàn phím Android + Ẩn ✕ + Khởi chạy %1$s + Chọn một màn hình. Phụ: %1$s. + Màn hình phụ không còn khả dụng. + Màn hình chính + Màn hình phụ + Tab trước + Tab sau + Bật + Tất cả %1$d + Không có + %1$d trong %2$d + Tắt + Màn hình dưới + Áp dụng vào lần game khởi động kế tiếp. Hồ sơ của một game sẽ ghi đè hồ sơ dùng chung. + Một game khác theo app ID… + Xong + Mặc định + Tắt + Đổi… + Đổi biến + Quay về mặc định + Game Proton + Hình trong hình + Tự động vào hình trong hình + Điều khiển trên màn hình mở + Khi mở, game nằm phía trên trackpad và grip phía sau của Steam Deck. Khi gập, game chiếm toàn màn hình. + Đã tạm dừng + Hình trong hình chỉ khả dụng khi phiên đã sẵn sàng. + Cho phép hình trong hình cho DroidDeck trong thiết lập Android. + Android không thể mở hình trong hình. + Shortcut + Thêm vào màn hình chính + Chọn game + Đang quét game đã cài… + Sao chép liên kết khởi chạy + Đã sao chép liên kết khởi chạy + Launcher này không thể thêm shortcut. Hãy dùng Sao chép liên kết khởi chạy. + Liên kết khởi chạy game không hợp lệ. + Game này không còn trong thư viện DroidDeck của bạn. + Dừng phiên hiện tại trước khi khởi chạy một game Steam. + Không thể gửi game đến Steam. Hãy thử khởi chạy lại. + Thử gỡ lại + Đang gỡ + Gỡ chưa hoàn tất + Xuất tệp game + Đồng bộ tất cả tệp game vào thư mục + Ngừng đồng bộ tệp game + Chọn một thư mục cho các tệp game frontend + Đã lưu tệp game vào %1$s + Tệp frontend + + Chờ hoạt động runtime hoàn tất + Chờ việc cài công cụ tương thích hoàn tất + Chờ việc cài ứng dụng desktop hoàn tất + Chờ việc cài hoàn tất trước khi khởi chạy một game + Hãy đóng phiên Steam trước, để game không ghi đè lên tệp vừa nhập + Đang nhập vào %1$s + + Đã nhập %1$d tệp từ %2$s vào %3$s + + + Đã nhập %1$d tệp từ %2$s vào %3$s. Tệp lưu cũ đã được sao lưu vào Download/DroidDeck/Saves/backups + + Chọn một tệp zip lưu cho %1$s + Đang xuất %1$s + + Đã xuất %1$d tệp dưới dạng %2$s: %3$s + + Chọn nơi lưu %1$s (%2$s) + %1$s thất bại: %2$s + Chưa có log phiên: hãy chạy một phiên trước. + Không có log phiên để xóa. + + Đã xóa %1$d thư mục log phiên. + + Ghép nối Gỡ lỗi qua mạng không dây thất bại + Lệnh Gỡ lỗi qua mạng không dây thất bại + Gỡ lỗi qua mạng không dây không khả dụng + Đã sao chép lệnh ADB + Chọn thư mục ROM + Không phải GPU Adreno + Turnip hỗ trợ GPU Adreno. Trên %1$s, Steam có thể hiện màn hình đen. Tải xuống: %2$.0f MB. + Vẫn cài + Gỡ runtime Linux + Việc này xóa runtime, client Steam bên trong nó, và mọi game đã cài ở đó. + Đã áp dụng: %1$s + Không thể mở %1$s trên màn hình chính + Không thể mở %1$s trên %2$s + Không thể mở %1$s trên màn hình %2$d + Đang nhập + Đang thay + Đang khôi phục + Đang hủy + Đang xóa + không thể đọc tệp + Đã nhập %1$s + Lượt thay đang chờ đã bị hủy. + Chọn một gói thành phần (-linux .wcp) + Không thể kết nối Nightlies + Đã lưu %1$s + Tải xuống thất bại: %1$s + Chọn một driver runtime Linux (zip -Linux hoặc gói Android + Linux) + Chọn một driver hiển thị (zip AdrenoTools hoặc gói Android + Linux) + Chọn một zip driver hoặc một gói Android + Linux + %1$s: %2$s + Đang gỡ %1$s… + Đang quên %1$s… + Chọn thư mục lưu trữ game + Chọn một tệp ZIP plugin Decky + Không thể dùng: %1$s + Chương trình + Đang gỡ runtime Linux + Không thể mở Settings trên màn hình dưới; đã mở nó trên màn hình chính. + Mở Developer options trong Settings + + Không thể mở thiết lập \"Install unknown apps\" của Android + Không thể kiểm tra: %1$s + Bản build này không có tệp tải cho bản DroidDeck này + Dừng phiên đang chạy trước, rồi cập nhật + Đang tải + Đang cài + Đang chờ bạn xác nhận + Cập nhật thất bại + Android không đề nghị cài bản cập nhật + + Cài Decky thất bại: %1$s + Đang bắt đầu nhập plugin… + Nhập plugin thất bại: %1$s + Đã cài plugin. Khởi động lại Steam để tải nó. + + Chưa kiểm tra - nhấn làm mới để tìm driver mới + Android + Linux: cùng đặt driver hiển thị + Android + Linux: cùng đặt driver runtime + Turnip tích hợp trong runtime + Vulkan hệ thống: không có bản build đi kèm cho GPU này + %1$s (đi kèm) + Thủ công + %1$s · %2$s + Hiện chỉ một nửa được phát hành + %1$.0f MB cho cả hai + Không có driver để đặt: %1$s + Đang kiểm tra driver mới nhất… + Đã mới nhất (không thể kiểm tra: %1$s) + Các driver được đề xuất hiện không được phát hành + Đang tải %1$s %2$s… + Đã cập nhật lên %1$s %2$s. + Đã cập nhật lên %1$s %2$s. Driver hiển thị áp dụng sau khi DroidDeck khởi động lại. + Đã chuyển sang %1$s %2$s. + Đã chuyển sang %1$s %2$s. Driver hiển thị áp dụng sau khi DroidDeck khởi động lại. + Đang dùng %1$s %2$s. + Đang dùng %1$s %2$s. Driver hiển thị áp dụng sau khi DroidDeck khởi động lại. + Không phải zip driver: không phải driver AdrenoTools, Turnip -Linux, cũng không phải gói Android + Linux + Nhập thất bại: %1$s + Driver đã nhập + Đang dùng %1$s làm cả driver runtime và hiển thị. + Đang dùng %1$s làm cả driver runtime và hiển thị. Driver hiển thị áp dụng sau khi DroidDeck khởi động lại. + Đã xóa %1$s (cả hai driver) + Đã xóa %1$s + Mới nhất: %1$s · đã kiểm tra %2$s + Mới nhất: %1$s · %2$s không thể kết nối · đã kiểm tra %3$s + + %1$d phút trước + + + %1$d giờ trước + + + %1$d ngày trước + + Đang kiểm tra Banners-Turnip, WinNative và DroidDeck… + kiểm tra thất bại + Đã cài %1$s - chọn nó ở một trong hai danh sách để đặt cả hai driver + Đã cài %1$s - chọn nó trong menu + + Chờ việc gỡ runtime hoàn tất + Game + touchpad + trực tiếp + Thay thất bại: %1$s + Lưu hồ sơ thất bại: %1$s + Desktop đang tạm dừng + %1$s đang tạm dừng + Chương trình đang tạm dừng + Đang khởi động desktop + Đang khởi động %1$s + Đang khởi động chương trình + Không có log cho phiên này. + Không thể tạo tệp lưu trữ log phiên. + Không thể chia sẻ log phiên. + Đang khởi động phiên… + đang tải runtime Linux + đang tải runtime Linux · %1$d trong %2$d MB + đang kiểm tra runtime Linux + đang giải nén runtime Linux + đang tải desktop + đang tải desktop · %1$d trong %2$d MB + đang kiểm tra desktop + đang giải nén desktop + đang tải Proton Experimental (ARM64) + đang tải Proton Experimental (ARM64) · %1$d trong %2$d MB + đang kiểm tra Proton Experimental (ARM64) + đang giải nén Proton Experimental (ARM64) + Không thể kết nối danh mục runtime. Kiểm tra kết nối và nhấn Play lại. + Runtime Linux chưa cài được. Kiểm tra runtime trong Thiết lập và nhấn Play lại. + Desktop chưa cài được (%1$s). Kiểm tra kết nối và nhấn Desktop lại. + %1$s đã dừng ngoài ý muốn. Hãy chia sẻ log cùng báo cáo lỗi, hoặc thử lại. + Chương trình đã dừng ngoài ý muốn. Hãy chia sẻ log cùng báo cáo lỗi, hoặc thử lại. + Desktop đã dừng ngoài ý muốn. Hãy chia sẻ log cùng báo cáo lỗi, hoặc thử lại. + Mã thoát %1$d · %2$s + không có log + + Log cho thấy %1$d lỗi \"Function not implemented\": khả năng tăng tốc seccomp của proot đang làm hỏng một helper trên thiết bị này. Hãy thử %2$s → \"%3$s\" (hoặc \"%4$s\") rồi khởi động lại. + + + Plugin + Cài một tệp ZIP plugin cho phiên Steam kế tiếp. + Đang tải… + Cài từ ZIP + Games + Âm thanh & Video + Đồ họa + Internet + Văn phòng + Nhà phát triển + Giáo dục + Khoa học + Hệ thống + Công cụ + Phổ biến + Xu hướng + Mới + Đã cập nhật + đang tải… + Thiết lập Flatpak + %1$s: %2$s + Không thể kiểm tra cập nhật trên Flathub + đã cài + thư mục + Than chì + Các bề mặt than chì, với màu xanh quả cầu riêng của logo cho mọi thứ được chọn, lấy nét hoặc nhấn. + Giấy trên đen + Trắng là màu chính, xanh của biểu tượng là tín hiệu cho các đường kẻ, vòng và điểm. + Xanh biểu tượng + Nền đen, chữ trắng, xanh cho mọi thứ được chọn, lấy nét hoặc nhấn. + Xanh điện navy + Màu xanh sáng hơn, trên các bề mặt nghiêng về navy thay vì xám. + Esc + Ins + Del + Home + End + PgUp + PgDn + ⌫ Backspace + Tab ⇥ + Caps + Enter ⏎ + ⇧ Shift + Ctrl + Super + Alt + Space + AltGr + Tab + %1$s ✓ + Ctrl+C + Ctrl+D + Cài runtime trước + Thư viện Steam không khả dụng + Thư viện SD trước đó không khả dụng + Lưu trữ game không khả dụng + Thư mục game đã thêm không khả dụng + Manifest của Steam không khả dụng + Nhiều hệ máy + Nội bộ + Hệ thống Linux + Thẻ SD + Lưu trữ + Chọn + Thuộc tính + Thêm vào Yêu thích + Xóa khỏi Yêu thích + Đổi tên + Sao chép + Cắt + Hành động + Yêu thích + Ghim thư mục hiện tại + Chưa có mục yêu thích nào - ghim một thư mục bằng menu ⋮ của nó để quay lại đây nhanh. + Bỏ khỏi mục yêu thích + 0 byte + byte + Vị trí + Loại + Kích thước + Đã sửa đổi + Thư mục + Tệp + Tệp %1$s + Chỉ đọc + Ngăn game (và Wine) ghi đè hoặc xóa tệp này. + Không thể đổi Chỉ đọc + Ẩn + Bộ lưu trữ này không thể lưu cờ ẩn. + Đánh dấu tệp là ẩn trong Windows (thuộc tính hidden của Wine). + Không thể đổi Ẩn trên bộ lưu trữ này + Xong + Đang xóa... + Xóa thất bại + Đang di chuyển... + Đang sao chép... + Đang di chuyển - %1$s + Đang sao chép - %1$s + Đang di chuyển %1$d/%2$d - %3$s + Đang sao chép %1$d/%2$d - %3$s + %1$d xong, %2$d thất bại + %1$d xong, %2$d đã bỏ qua + + %1$d mục đã di chuyển + + + %1$d mục đã sao chép + + \"%1$s\" đã tồn tại + Đang đổi tên... + Đổi tên thất bại + Đang tạo thư mục... + Không thể tạo thư mục + Thư mục mới + Tên thư mục + Tạo + Tên mới + Xóa? + Xóa \"%1$s\" vĩnh viễn? + + Xóa %1$d mục? + + Không thể hoàn tác việc này. + + …và %1$d mục nữa + + Đang xóa %1$d/%2$d - %3$s + + %1$d mục không thể xóa + + Gộp sẽ thêm và thay thế các tệp trong thư mục hiện có. + Ghi đè sẽ thay thế tệp hiện có. + Áp dụng cho mọi xung đột + Gộp + Ghi đè + Giữ cả hai + Bỏ qua + Chọn thư mục này + Quay lại + %1$s đã được gắn nhưng hiện không đọc được + Xem dạng danh sách + Xem dạng lưới + Tìm kiếm + Sắp xếp + Tên + Ngày sửa đổi + Hàng thoải mái + Hàng gọn + Ẩn tệp ẩn + Hiện tệp ẩn + Ẩn mục yêu thích + Hiện mục yêu thích + Lọc thư mục này + %1$s trống + %1$d đã chọn + Không chọn + Tất cả + Dán + + %1$d mục + + Dán %1$s vào đây + Dán %1$s (di chuyển) vào đây + Tệp + LƯU TRỮ + NHANH + YÊU THÍCH + Downloads + Downlds + ROMs + Log phiên + Pictures + Đã thêm \"%1$s\" vào Yêu thích + Đã xóa \"%1$s\" khỏi Yêu thích + Thư mục trống + Lên một cấp + Chọn một thư mục + Chọn một tệp + Xanh Steam + Xanh da trời + Trắng + Tím + Xanh lá + Hổ phách + Đỏ + Hồng + Nút A + Nút B + Nút X + Nút Y + Bumper trái + Bumper phải + Trigger trái + Trigger phải + Nút View + Nút Menu + View + Menu + D-pad lên + D-pad xuống + D-pad trái + D-pad phải + Ẩn + Kéo để di chuyển + Hít vào lưới (snap) + Lưu + Không có + Bàn phím + trackpad + Terminal + Grip + trackpad Deck + + Đang gỡ runtime Linux + + Đang gỡ runtime Linux · %1$d mục + + Đã gỡ runtime Linux + Không thể hoàn tất việc gỡ runtime Linux. Thử gỡ lại trong Thiết lập. + Đang xóa phần còn lại của runtime + Đang tải + Đang xác minh + Đang giải nén + Hiển thị khi runtime Linux đang được cài hoặc gỡ + Đang cài runtime Linux + Đang gỡ runtime Linux + + Runtime Linux chưa được cài + Danh mục không có checksum cho %1$s + Checksum không khớp - không có gì bị thay đổi + Đang cài %1$s + Không thể đặt AppImage + Giải nén thất bại + Cài thất bại + + Thiết lập Flatpak thất bại (mã thoát %1$d) + Thiết lập Flatpak chưa hoàn tất + Một tác vụ store khác đang chạy + Thiết lập Flatpak trước + Đang khởi động + Flatpak đã thoát với mã %1$d + Đang cài %1$s + Đang cập nhật %1$s + Đang gỡ %1$s + Đang cài %1$s (%2$d/%3$d) + Đang cập nhật %1$s (%2$d/%3$d) + Đang gỡ %1$s (%2$d/%3$d) + bản dịch + driver đồ họa + codec media + runtime %1$s + runtime %1$s %2$s + + Tệp này quá nhỏ để là một AppImage + Không thể đọc tệp: %1$s + Đây không phải AppImage (không phải chương trình Linux) + AppImage này không được build cho ARM64 (aarch64) hay PC x86 + Đây là chương trình Linux nhưng không phải AppImage + Chỉ có thể nhập AppImage loại 2 (tệp này là loại %1$d) + Không đủ dung lượng trống: nhập cần khoảng %1$s + Không đủ dung lượng trống: cập nhật cần khoảng %1$s + Nhập thất bại + AppImage đó không còn nữa + Cập nhật thất bại + Đang giải nén %1$s + Không thể giải nén AppImage + Không thể giải nén AppImage: %1$s + + Dừng phiên đang hoạt động trước khi cài Decky Loader + Tải Decky Loader thất bại + Kích thước Decky Loader không khớp với metadata của bản phát hành + Không thể tải checksum của bản phát hành + Thiếu checksum của bản phát hành + Checksum của bản phát hành không hợp lệ + Checksum Decky Loader không khớp + Tài sản phát hành không phải là file thực thi Linux 64-bit tương thích + Một phiên đã bắt đầu trong khi tải; hãy dừng nó trước khi cài Decky Loader + Không thể đánh dấu PluginLoader có thể thực thi + Không thể bật gỡ lỗi CEF từ xa của Steam + Không thể dàn (stage) PluginLoader + Không thể giữ lại PluginLoader hiện có + Không thể kích hoạt PluginLoader mới + Dừng phiên đang hoạt động trước khi cài một plugin + Cài Decky Loader trước khi nhập một plugin + Không thể đọc tệp ZIP đã chọn + Không thể tạo thư mục plugin Decky + Đang giải nén ZIP plugin + ZIP plugin phải chứa một thư mục plugin cấp cao nhất + ZIP plugin thiếu plugin.json, package.json, hoặc dist/index.js + Thiếu tên plugin + Một phiên đã bắt đầu trong khi cài đặt; hãy dừng nó trước khi cài một plugin + Đã cài plugin · khởi động lại Steam để tải nó + Cài ZIP plugin thất bại + ZIP plugin chứa quá nhiều mục + ZIP plugin chứa một đường dẫn không hợp lệ + ZIP plugin chứa các đường dẫn trùng lặp + ZIP plugin chứa một đường dẫn không an toàn + Không thể tạo thư mục plugin + ZIP plugin giải nén vượt quá giới hạn 1 GiB + Không thể đánh dấu tệp nhị phân plugin đi kèm có thể thực thi + Mục remote_binary không hợp lệ + Tên tệp nhị phân từ xa không hợp lệ + URL tệp nhị phân từ xa phải dùng HTTPS + Tệp nhị phân từ xa %1$s có mã băm SHA-256 không hợp lệ + Không thể tạo thư mục bin của plugin + Đang tải tệp nhị phân plugin · %1$s + Không thể tải tệp nhị phân plugin %1$s + Checksum tệp nhị phân plugin không khớp · %1$s + Không thể đánh dấu tệp nhị phân plugin có thể thực thi · %1$s + Không thể thay thế tệp nhị phân plugin · %1$s + Không thể cài tệp nhị phân plugin · %1$s + Thư mục plugin thuộc về một plugin khác + Không thể giữ lại plugin hiện có + Không thể kích hoạt plugin đã nhập + Không thể cập nhật thiết lập plugin Decky + Không thể khôi phục plugin hiện có + + Không thể lưu danh tính Gỡ lỗi qua mạng không dây + Lệnh ADB thất bại (%1$d) + Android không xác nhận giới hạn tiến trình con đã được đổi: %1$s + Chưa có ghép nối Gỡ lỗi qua mạng không dây nào được lưu + Nhập địa chỉ IP & cổng Gỡ lỗi qua mạng không dây hiện tại + + Hết thời gian chờ cửa sổ bật lên ghép nối. Hãy thử lại từ DroidDeck. + Không thể tìm cửa sổ bật lên ghép nối (%1$d). Kiểm tra Wi-Fi đã bật. + Không thể tìm cửa sổ bật lên ghép nối: %1$s + Nhập đủ 6 số của mã + Đang ghép nối… + Mã đó không hoạt động. Mở lại cửa sổ bật lên ghép nối và nhập mã mới của nó. + Đã ghép nối. Đang kết nối… + Đã ghép nối, nhưng không tìm thấy cổng Gỡ lỗi qua mạng không dây. Giữ Gỡ lỗi qua mạng không dây luôn mở và thử lại. + Đang áp dụng thiết lập… + Lệnh Gỡ lỗi qua mạng không dây thất bại + Thiết lập Gỡ lỗi qua mạng không dây + Hỏi mã ghép nối Gỡ lỗi qua mạng không dây khi Settings đang mở + Ghép nối Gỡ lỗi qua mạng không dây + Bật Gỡ lỗi qua mạng không dây, rồi nhấn “Pair device with pairing code”. + Bật Gỡ lỗi qua mạng không dây, rồi nhấn “Pair device with pairing code”. Ở lại trong Settings: mã sẽ hiện trong thông báo này. + Mã ghép nối 6 số + Nhấn Enter code và nhập 6 số hiện trong Settings. + Nhập mã ghép nối Wi-Fi + Thử lại mã + Nhập mã + Đang thiết lập Steam + Steam đã sẵn sàng + Nhấn để quay lại DroidDeck. Bạn có thể tắt Gỡ lỗi qua mạng không dây ngay bây giờ. + Thiết lập Gỡ lỗi qua mạng không dây đã dừng + + Lượt tải trả về HTTP %1$d + Kích thước tải xuống không khớp với APK đã phát hành và đã bị loại bỏ - hãy thử lại + Tệp tải xuống không khớp checksum và đã bị loại bỏ - hãy thử lại + Bản cập nhật này dành cho package Android %1$s, không phải %2$s + Metadata cập nhật không ghi tên khóa ký phát hành của DroidDeck + Android không thể đọc APK đã tải xuống + APK đã tải xuống dành cho %1$s, không phải %2$s + APK đã tải xuống ghi versionCode %1$d, không phải %2$d đã phát hành + Android sẽ không cài versionCode %1$d chồng lên %2$d đã cài + APK đã tải xuống không có thông tin ký + APK đã tải xuống không được ký bằng khóa phát hành của DroidDeck + Danh mục cập nhật trả về HTTP %1$d + + Đang khởi động phiên… + %1$d:%2$02d đã trôi qua · vẫn đang xử lý + Đang tải bản cập nhật client Steam · %1$d%% + Đang tải client Steam · %1$s + + Một lượt cài công cụ tương thích đang chạy + Dừng phiên đang hoạt động trước khi cài công cụ tương thích + Không tìm thấy bản phát hành ARM64 cho %1$s + Cần ít nhất 4 GB trống cộng với kích thước tải xuống để cài %1$s + Đang tải %1$s %2$s + Đang thử lại tải · lần %1$d trong %2$d + Tải xuống bị gián đoạn + Tải xuống thất bại; tệp dở được giữ lại để lần thử lại có thể tiếp tục + Tải xuống vượt quá kích thước bản phát hành và đã bị xóa + Tải xuống chưa hoàn tất; tệp dở được giữ lại để thử lại tiếp tục + Đang xác minh tải xuống + Không thể đọc checksum bản phát hành; hãy thử lại + %1$s %2$s không công bố checksum; không có gì được cài + Checksum không khớp; tệp tải xuống đã bị xóa + Một phiên đã bắt đầu trong khi tải; hãy dừng nó trước khi cài công cụ tương thích + Đang cài %1$s + Cài %1$s thất bại (mã thoát %2$d) + Không thể cài %1$s + Đang lấy pack droiddeck-esync + + có trợ giúp Linux + chỉ DLL + Gốc (Proton đã cập nhật từ lần thay gần nhất) + Đã thay đổi bên ngoài Components + không có + Gốc %1$s + Gốc từ %1$s + %1$s %2$s sẽ vào %3$s khi game đang chạy trên đó đóng lại. + %1$s %2$s giờ đã ở trong %3$s. Áp dụng vào lần game khởi động kế tiếp. + Gói gốc đó không được lưu + %1$s của %2$s sẽ trở về bản gốc khi game đang chạy trên đó đóng lại. + %1$s của %2$s đã được khôi phục từ bản gốc %3$s. + %1$s đang được dùng trong %2$s. Hãy thay Proton đó sang thứ khác trước. + %1$s không được lưu + Đã xóa %1$s. + Bản gốc của build đã cài được giữ lại: đó là đường quay về. + Chưa được lưu + Đã xóa bản gốc %1$s của %2$s. + Lượt tải chỉ đến từ các bản phát hành Nightlies + Danh sách gói này có trước checksum - hãy làm mới và thử lại + Checksum không khớp + + Tuyến tính + Gần nhất + Snapdragon GSR (chất lượng) + Chỉ làm nét + Tự động (khớp tỷ lệ) + + Không áp dụng gì. + Game Clarity + Làm nét với một chút tương phản - lựa chọn hàng ngày. + Vivid + Màu sắc được đẩy mạnh. Khử dải màu được bật để bầu trời không bị sọc. + Cinematic + Tương phản sâu hơn, màu lạnh hơn, cạnh được làm mượt. + Competitive + Vùng tối được nâng lên và làm nét mạnh - phát hiện người trước tiên. + Adaptive Sharpen + Chỉ làm nét, không đổi màu sắc. + Filmic + Đường cong giống phim, hơi trầm. + Arcade + Mạnh mẽ và rực rỡ. + Retro CRT + Ống CRT và tín hiệu analog cùng áp dụng. + Upscale Sharp + Dùng khi chạy dưới độ phân giải màn hình. + Pixel Clean + Pixel sắc nét cho game 2D và các game cũ. + Anime Edge + Viền hoạt hình cộng làm nét. + + Không phải GPU Adreno + Được hỗ trợ + Thử nghiệm: driver của nó là bản build thử nghiệm + Dưới mức phần cứng đã kiểm thử (Adreno 725 và mới hơn): có thể không chạy được + Không được hỗ trợ: DroidDeck cần GPU Adreno (Snapdragon) + + Đã đạt giới hạn tốc độ của GitHub - hãy thử lại sau + GitHub trả về HTTP %1$d + tải xuống trả về HTTP %1$d + Danh sách driver này có trước checksum - hãy làm mới và thử lại + Checksum không khớp - tệp tải xuống đã bị loại bỏ + Không phải driver AdrenoTools: %1$s + không có meta.json + meta.json không đọc được (%1$s) + đây là driver runtime Linux (hãy nhập nó dưới \"Linux runtime driver\") + đây là driver game Wayland, mà ứng dụng này không dùng đến + meta.json không ghi tên libraryName, nên không có gì để gửi cho AdrenoTools + meta.json ghi tên %1$s, nhưng không có trong zip + %1$s không phải là thư viện chia sẻ ELF AArch64 64-bit + %1$s liên kết glibc - một Turnip \"-Linux\", thuộc về mục \"Linux runtime driver\" + Không có libvulkan_freedreno*.so trong zip này. Một zip Turnip Android (AdrenoTools) hoặc -Wayland không phải là driver runtime Linux. + %1$s không phải là thư viện chia sẻ ELF AArch64 64-bit. + %1$s không phải là driver glibc - nó liên kết libc của Android. Runtime Linux cần một zip \"-Linux\"; một Turnip thường hoặc \"-Wayland\" hoàn toàn không thể được client Steam tải. + Không phải gói driver: không có manifest.json ghi tên một driver android và một driver linux + Không phải gói driver: %1$s trỏ ra ngoài nó + Không phải gói driver: nó giải nén vượt quá %1$d MB + + Thẻ SD + Thư mục + + Chia sẻ log phiên + + cần Android 10 hoặc mới hơn (lớp hiển thị có không gian màu) + màn hình này báo không hỗ trợ HDR + màn hình này hỗ trợ %1$s, không phải HDR10 + + GPU này + Giới hạn tiến trình con đang bật + Giới hạn tiến trình con chưa được đặt + Không thể kiểm tra giới hạn tiến trình con + Giới hạn tiến trình con đang tắt + Không cần giới hạn tiến trình con + %1$s Nếu không thể, hãy kết nối thiết bị này với một máy tính có ADB và chạy lệnh dưới đây. Quay lại đây và kiểm tra lại. + Phiên Steam có thể khởi động. + Phiên bản Android này không dùng giới hạn này. + Bật “Disable child process restrictions” trong Developer options, và giữ Developer options luôn mở sau đó. + Phiên bản Android này không có công tắc cho việc đó. Dùng Gỡ lỗi qua mạng không dây để đổi nó từ thiết bị này. + Bản build này không có tệp tải cho bản DroidDeck này. + Android sẽ không cài nó chồng lên bản này vì versionCode %1$d của nó thấp hơn %2$d đã cài. + Android sẽ không cài một phiên bản cũ hơn chồng lên phiên bản mới hơn. Hãy giữ nguyên bản này cho đến khi kênh bắt kịp. + Bản DroidDeck này được ký khác (có thể là bản build cục bộ?), nên Android sẽ không cập nhật tại chỗ. + Không đủ dung lượng trống cho bản cập nhật. + Bản cập nhật đã bị hủy. + Bản cập nhật không cài được: %1$s + Bản cập nhật không cài được. + zip %1$s + zip + %1$s chưa được khởi chạy + Không tìm thấy tệp lưu nào cho %1$s + %1$s không có thư mục drive_c, nên nó không phải tệp zip lưu GameHub hoặc Winlator + không thể tạo thư mục trong %1$s + không thể ghi trong %1$s + không thể ghi trong %1$s (%2$s) + Thư mục không khả dụng + Một tệp khác đã dùng tên %1$s + Thư mục hình ảnh không khả dụng + Thư mục có một inventory đồng bộ không liên quan + Không thể xóa %1$s + Tệp nằm ngoài thư mục xuất + FEX (bản build không có tên phiên bản) + không có profile.json trong %1$s + %1$s không phải là gói thành phần Linux (loại %2$s) + %1$s · %2$s trống + Tự động + Danh mục cập nhật không qua được kiểm tra (%1$s) + Thiết lập tiến trình con của DroidDeck + Đã đăng nhập · chế độ ngoại tuyến + Đã đăng nhập + ĐI KÈM + ĐÃ TẢI + ĐÃ NHẬP + ANDROID + LINUX + %1$s. %2$s\n\n%3$s + Hiển thị phiên + Độ phân giải + Khớp game 16:9 + Cách game 16:9 khớp với màn hình này. Từ phiên sau. + Chọn Khớp màn hình hoặc một kích thước tùy chỉnh hẹp hơn 16:9. + Giữ tỷ lệ + Kéo giãn để khớp màn hình + Chia tỷ lệ hình ảnh + Bộ lọc chia tỷ lệ + Dùng chung cho Steam và Desktop. + Áp dụng ngay. Dùng chung cho Steam và Desktop. + FSR, GSR và NIS áp dụng khi phóng to hình ảnh phiên. Chỉ làm nét cũng hoạt động mà không cần phóng to. + Độ sắc nét bộ lọc + Tốc độ khung hình + %1$s Đặt chia tỷ lệ thành %2$s. + Khớp màn hình (%1$d×%2$d) + Theo màn hình (đổi kích thước khi gập hoặc mở) + Steam + Hiện chi tiết thiết bị + Ẩn chi tiết thiết bị + Đang đọc thiết bị… + Sao chép + Sao chép các chi tiết này vào báo cáo lỗi. + Đã sao chép. + Buộc SSBS + Giữ một thiết lập tốc độ CPU mà Wine liên tục tắt. Có thể giúp game Windows nhanh hơn trên Snapdragon 8 Elite; thay đổi ít thấy trên chip cũ hơn. Áp dụng vào lần game khởi động kế tiếp. + + Stores + Steam + Tùy chỉnh + Stores + Mở một store trên + Mức tốc độ tải + Engine stores + ● Engine sẵn sàng + Chưa build + Cân bằng + Nhanh + Tối đa + Stores + Thiết lập Stores + Thiết lập + (chưa đăng nhập) + Store + Đã cài + Thư viện + Tất cả + Downloads + %1$s (%2$d) + Đăng nhập + %1$s · đăng xuất + Đăng xuất khỏi %1$s? + Game đã cài vẫn giữ. Thư viện sẽ được tải lại khi bạn đăng nhập lại. + Vẫn đăng nhập + Đăng xuất + Tìm trong danh mục %1$s… + Tìm game đã cài… + Tìm trong thư viện của bạn… + Tìm kiếm + Chưa cài gì + Đang tải + Không có game + Đang tải store… + Không có danh mục + Mới + Ưu đãi + Game miễn phí + Xu hướng + Thư viện của bạn + Chơi + Cài + Tiếp tục cài + Bị gián đoạn + Cài · %1$s + Lấy miễn phí + Xem trên %1$s + Mua · %1$s + Đã cài + Trong thư viện + Miễn phí + Tiếp tục + Làm mới + Hiện nội dung người lớn + Epic + EOS + Ngoại tuyến + Tắt + Đăng nhập Epic (EOS) + Khởi chạy ngoại tuyến + Giải quyết đăng nhập Epic + Tệp lưu trên cloud + Tải lên cloud + Tải xuống từ cloud + Không có tệp lưu trên cloud + Xung đột cloud + Xung đột cloud · %1$d tệp + … + Giữ bản cloud + Giữ bản cục bộ + Đang kiểm tra cloud… + Đồng bộ lần cuối %1$s + Chưa đồng bộ + %1$d tệp + đã cài + trong thư viện của bạn + store + Thành tích + Đã mở khóa %1$d/%2$d + %1$d / %2$d + Đã mở khóa · %1$d + Đang khóa · %1$d + Đang ẩn · %1$d + Thành tích ẩn + Chi tiết sẽ hiện ra khi mở khóa. + Đã mở khóa mọi thành tích + Đang tải… + Đăng nhập %1$s để xem thành tích của bạn. + Không thể kết nối tới %1$s. Hãy kiểm tra lại khi có mạng. + %1$s không liệt kê thành tích nào cho game này. + Hiện tất cả %1$d + Thành tích · %1$s + ✓ Trong Steam + Gỡ cài đặt + Đang gỡ cài đặt… + Gỡ cài đặt %1$s? + Giải phóng %1$s và gỡ nó khỏi Steam. + Xóa các tệp của nó và shortcut Steam của nó. + Steam sẽ nhận ra game vào lần khởi động kế tiếp; hãy khởi chạy từ đó, hoặc mở Steam ngay. + Proton + Lựa chọn của Steam cho mỗi game + Âm thanh + DirectAudio + Components + FEX, DXVK, VKD3D + Engine + %1$s · gốc + %1$s · tích hợp + Cách nó chạy + Lấy từ + %1$s, bằng tài khoản của bạn + Cài vào + Lưu trữ game (Games/%1$s/%2$s) + Khởi chạy bởi + Steam và Proton, cùng đường dẫn với game Steam + Steam và Proton · đăng nhập Epic mỗi lần khởi chạy, lớp phủ EOS tắt + Steam và Proton · đã đặt biến Amazon Games Services + Steam + được thêm vào Steam khi cài (Deck UI + Steam Input) + Không có lượt tải + Đã tạm dừng + Đang chờ + Đang chờ · #%1$d + Đã hủy + Thất bại + Thất bại · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + Tạm dừng + Tiếp tục + Hủy + Thử lại + Xóa + Xóa các mục đã xong + Manifest + Tải xuống + Xác minh + Cài + Xong + Đang kiểm tra + Đang tải + Xóa lượt tải? + Thất bại · %1$s được giữ lại + Đang xác minh + Đang cài + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d tệp + Quản lý tải xuống + Số lượt tải cùng lúc + Cài vào + Cài %1$s: + Nội bộ + %1$s trống + Tải xuống Store + Hiển thị tiến trình tải game và giữ các lượt tải tiếp tục chạy ở nền + Đang tải + %1$d lượt tải + Đang chuẩn bị… + Đã hoàn tất tải Store + Đã cài %1$s + Tải %1$s thất bại + Xóa + Giữ + Đăng nhập vào %1$s + Đăng nhập bằng nhà cung cấp của bạn + Tải lại + Đóng + Thử lại + Đang hoàn tất đăng nhập… + Không thể tải trang đăng nhập: %1$s + Nhà cung cấp này từ chối đăng nhập từ một trang nhúng. Hãy đăng nhập bằng mật khẩu store của bạn thay vào đó. + Trang đăng nhập trả về lỗi (HTTP %1$d). + Kết nối đến trang đăng nhập không an toàn. + Không thể xác minh đăng nhập. Hãy thử lại. + Đăng nhập thất bại. Hãy thử lại. + Đóng + Quay lại + Sửa + Thêm tất cả game trong thư mục này + Không có gì mới trong thư mục này + Tên + Đích + Khởi chạy trong + Tùy chọn khởi chạy + Hình ảnh + Exe khác… + Xóa + Ảnh bìa + Hình nền + Logo + Biểu tượng + Thư mục game + Steam + SteamGridDB + Tệp của bạn + Từ exe + Tự động + Chọn hình ảnh… + Chọn một hình ảnh: %1$s + Đặt lại về tự động + + %1$d game đã được thêm + + + %1$d đã có trong Games + + B · Về lại %1$s + game đó + Thiết lập Play + Driver đồ họa + Proton này + Mọi Proton + Cho mọi Proton + Khả dụng · %1$d + Đã kiểm tra %1$s + Kiểm tra thành phần và driver mới + Lấy + %1$s · DXVK, VKD3D-Proton và FEX dưới đây thuộc về Proton này + %1$s · đã kiểm thử + %1$s · chưa kiểm thử + %1$s · không hỗ trợ + Giữ cặp được đề xuất luôn mới + Tự động · %1$s + Driver runtime và hiển thị riêng biệt + Dùng %1$s? + Xóa %1$s? + Chẩn đoán + Sẵn sàng để chơi + Chưa sẵn sàng để chơi + runtime %1$s + giới hạn tiến trình đã tắt + đã đăng nhập với %1$s + Khi có gì cần bạn + Giao diện + Thiết bị này + Hình ảnh thư viện + ỨNG DỤNG LINUX + Flathub + Ứng dụng desktop cho desktop Linux, mở từ Desktop. + Thiết lập Flathub + Xóa %1$s? + Thiết lập và tệp của nó được giữ lại. + Giữ + Dung lượng trên đĩa + Gỡ cài đặt desktop? + Tất cả + Thành tích GOG + Đồng bộ thành tích cho các game GOG được hỗ trợ. Áp dụng từ lần khởi động kế tiếp. + Đã tắt thành tích + Đã bật thành tích + Đăng nhập lại GOG + Đã tải thành tích lên + Đang chờ tải thành tích lên + Bật thành tích GOG trong phần cài đặt Stores + Không có thành tích; hãy kiểm tra nhật ký phiên + Đã mở khóa thành tích + Dán + Phím chữ thập + Cần analog trái + Cần analog phải + Menu + Bàn phím + Phím chữ thập lên + Phím chữ thập xuống + Phím chữ thập trái + Phím chữ thập phải + Điều khiển chạm + Bộ điều khiển chạm + Chuột + Bật + Tắt + Chậm + Bình thường + Nhanh + Chỉnh sửa bố cục + Menu DroidDeck + Kéo các nút điều khiển vào màn hình + Sắp xếp + Hủy bỏ + Đang lưu… + Biểu tượng + R− + R+ + C− + C+ + Ẩn + Màu và độ mờ… + Sao chép bố cục này + Đã sao chép bố cục; hãy dán vào bố cục chạm của bất kỳ trò chơi nào + Dán bố cục đã sao chép + Đặt lại về mặc định của Steam + Nút điều khiển này không có liên kết nào trong Steam để gắn biểu tượng + Biểu tượng cho %1$s + Màu bố cục chạm + Xong + Không thể lưu bố cục chạm + Đã lưu bố cục chạm vào Steam + Ứng dụng %1$d + Nhãn (không bắt buộc) + Không tìm thấy biểu tượng của Steam + + %1$d biểu tượng từ Steam + + Màu biểu tượng + Màu nút + Không có biểu tượng + + Đang kiểm tra đăng nhập… + Đang lấy thư viện của bạn… + Đang lấy danh sách trò chơi… + Đang tải chi tiết trò chơi… (%1$d/%2$d) + + Đang đồng bộ %1$d trò chơi… + + Đang đồng bộ… %1$d/%2$d + Đang lấy ảnh bìa… + Đồng bộ không thành công + Không tìm thấy trò chơi nào trong thư viện %1$s của bạn + Amazon Games không phản hồi về thư viện của bạn: hãy làm mới, hoặc đăng nhập lại + Không thể kết nối với GOG + Không thể đọc thư viện GOG của bạn + Đang đăng ký với Steam… + Cardboard VR + Chế độ Cardboard + Hình cho mắt trái và mắt phải dành cho kính VR điện thoại. + Hình ảnh + Màn hình ảo + 3D cạnh nhau + Kính + Cardboard + Cardboard (2014) + Không có thấu kính + Khoảng cách mắt + Chiều rộng màn hình + Khoảng cách màn hình + Theo dõi đầu + Thiết bị này không có cảm biến xoay. + Căn giữa lại góc nhìn + Căn giữa lại + SteamVR (thử nghiệm) + Cài đặt + Tải SteamVR ARM64 của Valve (571 MB). + Đang tải và giải nén SteamVR… + Bắt đầu + Chạy SteamVR với đầu bạn làm kính; hình ảnh chuyển sang 3D cạnh nhau. + Dừng + SteamVR đang chạy. + Không khả dụng trong phiên này. + SteamVR không phản hồi; hãy xem nhật ký phiên. + Tải thành phần + Bản tải xuống được dùng chung với trang Components. Chọn một phiên bản đã tải ở trên để dùng vào lần khởi chạy game kế tiếp. + Chọn phiên bản để tải + Nhà phát triển + Nhà phát hành + Phát hành + Cốt truyện chính + + Nội dung phụ + Hoàn thành 100% + %1$s giờ + %1$d phút + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b3ec6abd5..b1852e70d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -15,8 +15,10 @@ 会话 在 Steam 或桌面会话打开时显示 Steam 正在运行 + Steam 正在下载 会话已暂停 — 准备好后点按恢复 恢复 + 立即暂停 停止会话 帧生成 选择帧生成模式 @@ -182,6 +184,8 @@ 应用 ID org.ppsspp.PPSSPP 不是 Flathub 应用 ID + 或选择 .flatpak 文件,用于不在 Flathub 上的应用。 + 选择 .flatpak 文件 来源 在你的桌面上 新应用 @@ -251,6 +255,10 @@ 屏幕控制 Steam + QAM 自动模式会在未连接手柄时显示屏幕控制。 + Steam 触控(测试版) + Steam 的触控按键将从下次 Steam 会话开始生效 + Steam 触控布局 + 移动、调整大小或隐藏此游戏的触控按键;保存到 Steam 手柄 决定 Steam 把你的手柄识别为哪种类型,将在下次会话生效。 Steam Deck 手柄 @@ -268,6 +276,8 @@ 手动 原生 自动会暂停并在显示时恢复。原生会先让 Steam 做好睡眠准备。手动会等待点按恢复。“从不”则保持运行。 + 后台继续下载 + Steam 正在下载时推迟后台暂停。 自动会在后台暂停并在显示时恢复。手动会等待点按恢复。“从不”则保持运行。 启动 随 DroidDeck 启动 Steam @@ -331,6 +341,7 @@ 游戏含有 %1$s(通常可直接使用) 此游戏中已有 游戏文件夹中没有指向任何组件的内容。 + 已为此游戏安装(%1$d) 全部组件(%1$d) 需要 Windows 安装程序(%1$d) 这些组件需要在前缀中运行真正的安装程序(.NET、PhysX、XNA、完整的 Visual C++ 安装程序等)。DroidDeck 暂时无法做到,接下来会支持。 @@ -341,6 +352,9 @@ 正在加载组件… 无法安装:%1$s 已下载 + 正在下载 %1$s + 正在安装:%1$s + 正在关闭… 下载 %1$s 将在下次启动游戏时生效。 拉伸 16:9 比例的游戏 @@ -397,6 +411,44 @@ 检查 Nightlies 上的新软件包 Proton 此处选择的 Proton 会运行所有未在 Steam 中单独选择 Proton 的游戏,在 Steam 中所做的更改也会显示在这里。Steam 更新时会替换 Valve 的 Proton;每个构建的原件都会保留。 + 配置 + 设置配置档案 + 配置档案 + 默认配置档案 + 没有覆盖设置的游戏会自动使用。 + %1$s 启动时自动使用的已保存设置。 + 只选择此游戏需要的设置。其余设置仍使用默认配置档案。 + FEX 预设 + 正在使用默认配置档案的预设。 + 已为此游戏保存自定义预设。 + 自定义 FEX 变量 + 使用默认 + 游戏兼容性 + 此游戏的 Proton、DXVK、VKD3D 及同步环境选项。 + 正在使用默认配置档案的 Proton。 + 已通过 Steam 兼容性映射为此游戏保存自定义 Proton。 + 无 Proton + DXVK + VKD3D-Proton + 使用默认配置档案中来自 %1$s 的组件包,除非此游戏另选了其他组件包。 + 使用默认配置档案的组件包。 + 仅此游戏使用 %1$s。 + 运行时选项 + 其他按游戏设置的 Proton、DXVK、VKD3D-Proton 及同步环境选项。 + %1$s 的配置档案已保存,将在下次启动游戏时生效。 + 不启动 Steam 直接开始 + 适用于一开就立即关闭的游戏:不经过 Proton 的 Steam 设置来启动。叠加层等 Steam 功能对该游戏会关闭。 + 开 + %1$s 的 Proton 选择已保存,现已在 Steam 中可用。 + %1$s 的 Proton 选择已保存。启动游戏前请重启 Steam。 + %1$s 无法在此设备上使用所选的 Proton。 + 无法加载 %1$s 的配置档案:%2$s + 无法保存游戏配置档案:%1$s + 重置游戏配置档案 + 重置 %1$s 的配置档案? + 移除此游戏的 Proton、FEX、组件包、环境变量、纹理过滤和 Windows 组件覆盖设置?之后它将使用默认配置档案。 + %1$s 现在使用默认配置档案。 + 无法完全重置游戏配置档案:%1$s %1$s 现在将运行所有未在 Steam 中单独选择 Proton 的游戏。 从下次 Steam 启动起,%1$s 将运行所有未单独选择 Proton 的游戏。 %1$s 无法在此设备启动游戏,因此不能设为默认。 @@ -560,6 +612,18 @@ 停止会话? 当前正在运行 没有游戏在 Proton 上运行 + 游戏配置档案 + 配置档案详情暂不可用。 + 无法加载游戏配置档案。正在自动重试。 + 无法刷新游戏配置档案。正在显示上次可用的数据,并自动重试。 + 查看和编辑下次启动时使用的已保存设置。 + 正在使用默认配置档案 + 自定义配置档案 + 正在使用默认配置档案中的 %1$s。 + 更改会保存到 %1$s 的配置档案,并在下次启动游戏时生效。 + + %1$d 项游戏覆盖设置 + 尚未安装 Proton。 游戏运行中 于启动 %1$s 时重新应用 @@ -631,6 +695,10 @@ 用作主屏幕 DroidDeck 可以设为主屏幕应用 系统不会把 DroidDeck 作为主屏幕应用提供 + 屏幕方向 + 自动横屏 + 横屏 + 反向横屏 全屏 隐藏 Android 状态栏和导航栏 显示 Android 状态栏和导航栏 @@ -888,6 +956,12 @@ 该页面已不存在。请在左侧选择一个分区。 打开桌面 安装并打开桌面 + 卸载桌面 + 移除桌面及其自带应用。游戏、已添加的应用、设置和共享库会保留。 + 正在移除桌面 + 桌面已移除 + 正在移除旧的 LXQt 文件 + 请先更新 DroidDeck,再移除此桌面版本。 Linux 桌面 KDE Plasma、Firefox 和你的模拟器 你在 Steam 安装的游戏会在会话结束后显示在这里。 @@ -916,6 +990,8 @@ 上次游玩于 %1$s 内部存储 已添加游戏 + 自定义游戏 + 游戏存储 为此 GPU 挑选的最佳组合,并保持更新 自行挑选驱动组合 正在检查驱动… @@ -957,6 +1033,12 @@ 左触控板 右触控板 两侧 + 手柄 + 摇杆 + 触控板 + 在摇杆与触控板之间切换 + 更多 Deck 控制 + 面板 关闭副屏控制 打开 Steam 菜单 左 · 上 @@ -992,6 +1074,10 @@ 已安装 %1$s 来自较早请求的待处理项 未安装 + 有可用更新 → %1$s + 移除旧版 %1$s + 将移除 %1$s。设为使用这些版本的游戏必须改用其他 Proton。 + 设为使用此版本的游戏必须改用其他 Proton。 立即安装 取消排队 Android 键盘 @@ -1020,6 +1106,8 @@ Proton 游戏 画中画 自动进入画中画 + 展开屏幕上的控制 + 展开时,游戏画面位于 Steam Deck 触控板和背键上方。折叠时,游戏全屏显示。 已暂停 会话就绪后即可使用画中画。 请在 Android 设置中允许 DroidDeck 的画中画。 @@ -1046,17 +1134,14 @@ 前端文件 - 请等待运行时操作完成 请等待兼容性工具安装完成 请等待桌面应用安装完成 请等待安装完成后再启动游戏 请先关闭 Steam 会话,以免游戏存档覆盖导入的内容 - 正在导入到 %1$s - 已从 %2$s 导入 %1$d 个文件到 %3$s @@ -1065,18 +1150,14 @@ 已从 %2$s 导入 %1$d 个文件到 %3$s。旧存档已备份到 Download/DroidDeck/Saves/backups 为 %1$s 选择存档 zip 包 - 正在导出 %1$s - 已将 %1$d 个文件导出为 %2$s:%3$s - 选择 %1$s 的保存位置(%2$s) - %1$s失败:%2$s 还没有会话日志:请先运行一次会话。 @@ -1090,23 +1171,18 @@ 已复制 ADB 命令 选择 ROM 文件夹 不是 Adreno GPU - Turnip 支持 Adreno GPU。在 %1$s 上,Steam 可能显示黑屏。需下载:%2$.0f MB。 仍要安装 移除 Linux 运行时 这会删除运行时、其中的 Steam 客户端,以及安装在其中的所有游戏。 - 已应用:%1$s 无法在主屏幕上打开 %1$s - 无法在 %2$s 上打开 %1$s - 无法在显示器 %2$d 上打开 %1$s - 正在导入 正在替换 @@ -1114,20 +1190,17 @@ 正在取消 正在删除 无法读取该文件 - 已导入 %1$s 等待中的替换已取消。 选择组件包(-linux .wcp) 无法连接 Nightlies - 已存储 %1$s 下载失败:%1$s 选择 Linux 运行时驱动(-Linux zip 包或 Android + Linux 组合包) 选择显示驱动(AdrenoTools zip 包或 Android + Linux 组合包) 选择驱动 zip 包或 Android + Linux 组合包 - %1$s:%2$s 正在移除 %1$s… @@ -1135,7 +1208,6 @@ 选择游戏存储文件夹 选择 Decky 插件 ZIP 包 无法使用:%1$s - 程序 正在移除 Linux 运行时 @@ -1147,7 +1219,6 @@ 无法检查:%1$s 此构建没有适用于此 DroidDeck 副本的下载 请先停止正在运行的会话,再进行更新 - 正在下载 正在安装 @@ -1169,18 +1240,15 @@ 系统 Vulkan:没有适用于此 GPU 的内置构建 %1$s(内置) 手动 - %1$s · %2$s 目前只发布了其中一半 两者共 %1$.0f MB - 没有可设置的驱动:%1$s 正在检查最新驱动… 已是最新(无法检查:%1$s) 推荐的驱动目前尚未发布 - 正在下载 %1$s %2$s… 已更新到 %1$s %2$s。 @@ -1196,10 +1264,8 @@ 正在将 %1$s 同时用作运行时驱动和显示驱动。显示驱动将在 DroidDeck 重启后生效。 已删除 %1$s(两个驱动) 已删除 %1$s - 最新:%1$s · %2$s检查 - 最新:%1$s · 无法连接 %2$s · %3$s检查 @@ -1213,30 +1279,25 @@ 正在检查 Banners-Turnip、WinNative 和 DroidDeck… 检查失败 - 已安装 %1$s — 在任一列表中选择它即可同时设置两个驱动 已安装 %1$s — 请在菜单中选择它 请等待运行时移除完成 - 游戏 - 触控板 - 直接 替换失败:%1$s + 配置档案保存失败:%1$s 桌面已暂停 - %1$s 已暂停 程序已暂停 正在启动桌面 - 正在启动 %1$s 正在启动程序 @@ -1244,7 +1305,6 @@ 无法创建会话日志压缩包。 无法分享会话日志。 正在启动会话… - 正在下载 Linux 运行时 正在下载 Linux 运行时 · %1$d / %2$d MB @@ -1260,32 +1320,26 @@ 正在解包 Proton Experimental(ARM64) 无法连接运行时目录。请检查网络连接,然后再次点“游玩”。 Linux 运行时未能安装。请在“配置”中检查运行时,然后再次点“游玩”。 - 桌面未能安装(%1$s)。请检查网络连接,然后再次点“桌面”。 - %1$s 意外停止。请将日志随错误报告一起分享,或重试。 程序意外停止。请将日志随错误报告一起分享,或重试。 桌面意外停止。请将日志随错误报告一起分享,或重试。 - 退出状态 %1$d · %2$s 无日志 - 日志中有 %1$d 个“Function not implemented”错误:proot 的 seccomp 加速在此设备上导致某个辅助进程失败。请尝试 %2$s → “%3$s”(或“%4$s”),然后重新启动。 - 插件 安装插件 ZIP 包,将在下次 Steam 会话中生效。 正在下载… 从 ZIP 安装 - 游戏 影音 @@ -1299,27 +1353,20 @@ 实用工具 热门 趋势 - 新上架 - 最近更新 加载中… Flatpak 配置 - %1$s:%2$s 无法从 Flathub 检查更新 - - 已安装 - 文件夹 - 石墨 石墨色界面,选中、聚焦或按下的元素均使用标志自带的球形蓝色。 @@ -1329,7 +1376,6 @@ 黑色背景、白色文字,选中、聚焦或按下的元素使用蓝色。 电光藏青 更明亮的蓝色,配以偏藏青而非灰色的界面。 - Esc Ins @@ -1348,15 +1394,12 @@ Alt 空格 AltGr - Tab - %1$s ✓ Ctrl+C Ctrl+D - 请先安装运行时 Steam 库不可用 @@ -1364,10 +1407,8 @@ 游戏存储不可用 已添加游戏的文件夹不可用 Steam 清单文件不可用 - 多种平台 - 内部存储 Linux 系统 @@ -1386,7 +1427,6 @@ 还没有收藏 — 用文件夹的 ⋮ 菜单固定它,之后即可快速跳回。 从收藏夹移除 0 字节 - 字节 位置 @@ -1395,7 +1435,6 @@ 修改时间 文件夹 文件 - %1$s 文件 只读 @@ -1410,18 +1449,14 @@ 删除失败 正在移动… 正在复制… - 正在移动 - %1$s 正在复制 - %1$s - 正在移动 %1$d/%2$d - %3$s 正在复制 %1$d/%2$d - %3$s - %1$d 项完成,%2$d 项失败 - %1$d 项完成,%2$d 项已跳过 @@ -1430,7 +1465,6 @@ 已复制 %1$d 项 - “%1$s”已存在 正在重命名… @@ -1442,19 +1476,16 @@ 创建 新名称 删除? - 要永久删除“%1$s”吗? 删除 %1$d 项? 此操作无法撤销。 - …以及其他 %1$d 项 - 正在删除 %1$d/%2$d - %3$s @@ -1469,7 +1500,6 @@ 跳过 选择此文件夹 返回 - %1$s 已挂载,但当前无法读取 列表视图 @@ -1485,50 +1515,40 @@ 隐藏收藏夹 显示收藏夹 筛选此文件夹 - 可用 %1$s 已选择 %1$d 项 - 不选 - 全选 粘贴 %1$d 项 - 将 %1$s 粘贴到此处 - 将 %1$s 移动到此处 文件 存储 - 快速访问 收藏夹 下载 - 下载 ROM 会话日志 图片 - 已将“%1$s”添加到收藏夹 - 已将“%1$s”从收藏夹移除 空文件夹 返回上一级 选择文件夹 选择文件 - Steam 蓝 天蓝 @@ -1538,7 +1558,6 @@ 琥珀 红色 粉色 - A 键 B 键 @@ -1550,25 +1569,20 @@ 右扳机 视图键 菜单键 - - 视图 - 菜单 方向键上 方向键下 方向键左 方向键右 - 隐藏 拖动以移动 对齐 保存 - 无 键盘 + 触控板 @@ -1576,7 +1590,6 @@ Deck 背键 + 触控板 - 正在移除 Linux 运行时 @@ -1594,11 +1607,9 @@ 未安装 Linux 运行时 - 目录中没有 %1$s 的校验和 校验和不匹配 — 未做任何更改 - 正在安装 %1$s 无法放置 AppImage @@ -1612,51 +1623,41 @@ 请先配置 Flatpak 启动中 Flatpak 已退出,状态码 %1$d - 正在安装 %1$s 正在更新 %1$s 正在移除 %1$s - 正在安装 %1$s(%2$d/%3$d) 正在更新 %1$s(%2$d/%3$d) 正在移除 %1$s(%2$d/%3$d) - 翻译 图形驱动 媒体编解码器 - %1$s 运行时 - %1$s 运行时 %2$s 文件太小,不可能是 AppImage - 无法读取文件:%1$s 这不是 AppImage(不是 Linux 程序) 此 AppImage 既不是为 ARM64(aarch64)构建,也不是为 x86 PC 构建 这是 Linux 程序,但不是 AppImage 只能导入类型 2 的 AppImage(此文件为类型 %1$d) - 可用空间不足:导入约需 %1$s - 可用空间不足:更新约需 %1$s 导入失败 该 AppImage 已不存在 更新失败 - 正在解压 %1$s 无法解压该 AppImage - 无法解压该 AppImage:%1$s @@ -1696,26 +1697,19 @@ 无效的 remote_binary 条目 无效的远程二进制文件名 远程二进制文件的 URL 必须使用 HTTPS - 远程二进制文件 %1$s 的 SHA-256 哈希无效 无法创建插件 bin 目录 - 正在下载插件二进制文件 · %1$s - 无法下载插件二进制文件 %1$s - 插件二进制文件校验和不匹配 · %1$s - 无法将插件二进制文件设为可执行 · %1$s - 无法替换插件二进制文件 · %1$s - 无法安装插件二进制文件 · %1$s 该插件文件夹属于另一个插件 @@ -1726,10 +1720,8 @@ 无法保存无线调试身份信息 - ADB 命令执行失败(%1$d) - Android 未确认子进程限制已更改:%1$s 未保存任何无线调试配对 @@ -1737,10 +1729,8 @@ 等待配对弹窗超时。请从 DroidDeck 重试。 - 无法查找配对弹窗(%1$d)。请检查 Wi-Fi 是否已开启。 - 无法查找配对弹窗:%1$s 请输入完整的 6 位配对码 @@ -1755,13 +1745,11 @@ 配对无线调试 开启无线调试,然后点按“使用配对码配对设备”。 开启无线调试,然后点按“使用配对码配对设备”。请停留在“设置”中:配对码在此通知中输入。 - 6 位配对码 点按“输入配对码”,然后输入“设置”中显示的 6 位数字。 输入 Wi-Fi 配对码 请重新输入配对码 - 输入配对码 正在配置 Steam @@ -1773,12 +1761,10 @@ 下载服务器返回 HTTP %1$d 下载的文件大小与发布的 APK 不符,已丢弃 — 请重试 下载的文件与校验和不符,已丢弃 — 请重试 - 此更新适用于 Android 软件包 %1$s,而不是 %2$s 更新元数据中未指明 DroidDeck 的发布签名密钥 Android 无法读取下载的 APK - 下载的 APK 适用于 %1$s,而不是 %2$s 下载的 APK 的 versionCode 为 %1$d,而不是已发布的 %2$d @@ -1788,14 +1774,11 @@ 更新目录返回 HTTP %1$d - 正在启动会话… - 已用时 %1$d:%2$02d · 仍在处理 正在下载 Steam 客户端更新 · %1$d%% - 正在下载 Steam 客户端 · %1$s @@ -1816,7 +1799,6 @@ 校验和不匹配;已删除下载的文件 下载期间有会话启动;请先停止它,再安装兼容性工具 正在安装 %1$s - %1$s 安装失败(退出代码 %2$d) 无法安装 %1$s @@ -1827,30 +1809,24 @@ 仅 DLL 原件(上次替换后 Proton 已更新) 已在“组件”之外被更改 - 不存在 - 原件 %1$s 原件来自 %1$s %1$s %2$s 将在 %3$s 上运行的游戏关闭后放入其中。 %1$s %2$s 已放入 %3$s,将在下次启动游戏时生效。 未存储该原件包 - %2$s 的 %1$s 将在其上运行的游戏关闭后恢复为原件。 - 已从 %3$s 原件还原 %2$s 的 %1$s。 - %1$s 正在 %2$s 中使用。请先将该 Proton 替换为其他版本。 %1$s 未存储 已删除 %1$s。 已安装构建的原件会保留:它是还原的途径。 未存储 - 已删除 %2$s 的 %1$s 原件。 只能从 Nightlies 发布版本下载 @@ -1902,7 +1878,6 @@ 下载返回 HTTP %1$d 此驱动列表早于校验和功能 — 请刷新后重试 校验和不匹配 — 已丢弃下载的文件 - 不是 AdrenoTools 驱动:%1$s 缺少 meta.json @@ -1910,7 +1885,6 @@ 这是 Linux 运行时驱动(请在“运行时驱动”下导入) 这是 Wayland 游戏驱动,本应用用不到它 meta.json 未指定 libraryName,因此无法交给 AdrenoTools - meta.json 指定了 %1$s,但它不在 zip 包中 %1$s 不是 64 位 AArch64 ELF 共享库 @@ -1924,22 +1898,19 @@ SD 卡 - 文件夹 - 分享会话日志 + 分享会话日志 需要 Android 10 或更高版本(支持色彩空间的显示图层) 此显示器未报告支持 HDR - 此显示器支持 %1$s,但不支持 HDR10 - 此 GPU 子进程限制已开启 @@ -1947,7 +1918,6 @@ 无法检查子进程限制 子进程限制已关闭 无需更改子进程限制 - %1$s 如果无法做到,请将此设备连接到装有 ADB 的电脑并运行下方命令,然后返回此处重新检查。 可以启动 Steam 会话了。 @@ -1955,81 +1925,60 @@ 在开发者选项中打开“禁用子进程限制”,之后请保持开发者选项开启。 此 Android 版本没有该开关。请使用无线调试在此设备上更改。 此构建没有适用于此 DroidDeck 副本的下载。 - Android 不会用它覆盖此构建,因为它的 versionCode %1$d 低于已安装的 %2$d。 Android 不会用旧版本覆盖新版本。请继续使用此构建,直到该通道的版本跟上。 此 DroidDeck 副本的签名不同(可能是本地构建?),因此 Android 无法就地更新。 可用空间不足,无法更新。 更新已取消。 - 更新未能安装:%1$s 更新未能安装。 - %1$s zip 包 - zip 包 - %1$s 尚未启动过 未找到 %1$s 的存档文件 - %1$s 中没有 drive_c 文件夹,因此不是 GameHub 或 Winlator 存档 zip 包 - 无法在 %1$s 中创建文件夹 无法写入 %1$s - 无法写入 %1$s(%2$s) 文件夹不可用 - 已有另一个文件使用 %1$s 美术图文件夹不可用 文件夹中有无关的同步清单 - 无法移除 %1$s 文件超出了导出文件夹 - FEX(无版本名称的构建) - %1$s 中没有 profile.json - %1$s 不是 Linux 组件包(类型为 %2$s) - %1$s · 可用 %2$s - 自动 - 更新目录未通过校验(%1$s) - DroidDeck 子进程设置 - 已登录 · 离线模式 已登录 - 内置 已下载 已导入 - Android + Linux - %1$s。%2$s\n\n%3$s 会话显示 @@ -2048,6 +1997,7 @@ 帧率 %1$s 将缩放设为 %2$s。 匹配屏幕(%1$d×%2$d) + 跟随屏幕(折叠或展开时调整大小) Steam 显示设备详情 隐藏设备详情 @@ -2057,6 +2007,186 @@ 已复制。 强制 SSBS 保持开启一项 Wine 会不断关闭的 CPU 速度设置。在骁龙 8 至尊版上可能让 Windows 游戏更快;在较旧芯片上变化不大。下次启动游戏时生效。 + + 游戏商店 + Steam + 自定义 + 游戏商店 + 打开商店时显示 + 下载速度档位 + 商店引擎 + ● 引擎就绪 + 未构建 + 均衡 + 快速 + 最高 + 游戏商店 + 游戏商店设置 + 设置 + (未登录) + 商店 + 已安装 + 库 + 全部 + 下载 + %1$s(%2$d) + 登录 + %1$s · 退出登录 + 要从 %1$s 退出登录吗? + 已安装的游戏会保留。重新登录后将再次获取游戏库。 + 保持登录 + 退出登录 + 搜索 %1$s 目录… + 搜索已安装的游戏… + 搜索你的库… + 搜索 + 尚未安装任何内容 + 加载中 + 没有游戏 + 正在加载商店… + 无目录 + 新上架 + 特惠 + 免费游戏 + 趋势 + 你的库 + 游玩 + 安装 + 继续安装 + 已中断 + 安装 · %1$s + 免费领取 + 在 %1$s 上查看 + 购买 · %1$s + 已安装 + 已在库中 + 免费 + 继续 + 刷新 + 显示成人内容 + Epic + EOS + 离线 + 关闭 + Epic 登录(EOS) + 离线启动 + 解决 Epic 登录问题 + 云存档 + 上传到云端 + 从云端下载 + 无云存档 + 云存档冲突 + 云存档冲突 · %1$d 个文件 + … + 保留云端版本 + 保留本地版本 + 正在检查云存档… + 上次同步:%1$s + 尚未同步 + %1$d 个文件 + 已安装 + 在你的库中 + 商店 + 成就 + 已解锁 %1$d / %2$d + %1$d / %2$d + 已解锁 · %1$d + 未解锁 · %1$d + 隐藏 · %1$d + 隐藏成就 + 解锁后显示详情。 + 已解锁全部成就 + 读取中… + 登录 %1$s 以查看你的成就。 + 无法连接到 %1$s。请在联网后再查看。 + %1$s 未列出此游戏的成就。 + 显示全部 %1$d 项 + 成就 · %1$s + ✓ 已在 Steam 中 + 卸载 + 正在卸载… + 卸载 %1$s? + 将释放 %1$s,并从 Steam 中移除。 + 将移除其文件和 Steam 快捷方式。 + Steam 下次启动时会识别该游戏;请从 Steam 中启动它,或立即启动 Steam。 + Proton + 由 Steam 按游戏选择 + 音频 + DirectAudio + 组件 + FEX、DXVK、VKD3D + 引擎 + %1$s · 原生 + %1$s · 内置 + 运行方式 + 获取来源 + %1$s(使用你的账户) + 安装位置 + 游戏存储(Games/%1$s/%2$s) + 启动方式 + Steam 和 Proton,与 Steam 游戏方式相同 + Steam 和 Proton · 每次启动时登录 Epic,关闭 EOS 覆盖层 + Steam 和 Proton · 已设置 Amazon Games Services 变量 + Steam + 安装时添加到 Steam(Deck 界面 + Steam 输入) + 没有下载 + 已暂停 + 排队中 + 排队中 · 第 %1$d 位 + 已取消 + 失败 + 失败 · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + 暂停 + 继续 + 取消 + 重试 + 清除 + 清除已完成 + 清单 + 下载 + 校验 + 安装 + 完成 + 检查中 + 下载中 + 删除下载? + 失败 · 已保留 %1$s + 校验中 + 安装中 + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d 个文件 + 下载管理 + 同时下载数 + 安装到 + 安装 %1$s: + 内部存储 + 可用 %1$s + 商店下载 + 显示游戏下载进度,并让下载在后台继续进行 + 正在下载 + %1$d 个下载 + 正在准备… + 已完成的商店下载 + %1$s 已安装 + %1$s 下载失败 + 删除 + 保留 + 登录 %1$s + 使用第三方账号登录 + 重新加载 + 关闭 + 重试 + 正在完成登录… + 无法加载登录页面:%1$s + 该第三方登录方式不允许在内嵌页面中登录。请改用商店密码登录。 + 登录页面返回了错误(HTTP %1$d)。 + 与登录页面的连接不安全。 + 无法验证登录。请重试。 + 登录失败。请重试。 关闭 返回 编辑 @@ -2088,21 +2218,122 @@ %1$d 个已在游戏中 - 成就 - 已解锁 %1$d / %2$d - %1$d / %2$d - 已解锁 · %1$d - 未解锁 · %1$d - 隐藏 · %1$d - 隐藏成就 - 解锁后显示详情。 - 已解锁全部成就 - 读取中… - 登录 %1$s 以查看你的成就。 - 无法连接到 %1$s。请在联网后再查看。 - %1$s 未列出此游戏的成就。 - 显示全部 %1$d 项 - 成就 · %1$s + B · 返回%1$s + 游戏 + 游玩设置 + 图形驱动 + 此 Proton + 所有 Proton + 适用于所有 Proton + 可用 · %1$d + 上次检查:%1$s + 检查新组件和驱动 + 获取 + %1$s · 下方的 DXVK、VKD3D-Proton 和 FEX 属于此 Proton + %1$s · 已测试 + %1$s · 未测试 + %1$s · 不受支持 + 让推荐的驱动组合保持最新 + 自动 · %1$s + 分别设置运行时驱动和显示驱动 + 使用 %1$s? + 移除 %1$s? + 诊断 + 可以游玩了 + 暂时还不能游玩 + 运行时 %1$s + 进程限制已关闭 + 已登录为 %1$s + 需要你处理 + 外观 + 此设备 + 游戏库美术图 + LINUX 应用 + Flathub + 适用于 Linux 桌面的桌面应用,从“桌面”打开。 + 配置 Flathub + 移除 %1$s? + 其设置和文件会保留。 + 保留 + 占用空间 + 卸载桌面? + 全部 + GOG 成就 + 为受支持的 GOG 游戏同步成就。下次启动时生效。 + 成就已关闭 + 成就已开启 + 重新登录 GOG + 成就已上传 + 成就上传待处理 + 在「商店」设置中开启 GOG 成就 + 成就不可用;请查看会话日志 + 成就已解锁 + 粘贴 + 方向键 + 左摇杆 + 右摇杆 + 菜单 + 键盘 + 方向键上 + 方向键下 + 方向键左 + 方向键右 + 触控 + 触控控制器 + 鼠标 + 开 + 关 + 慢 + 正常 + 快 + 编辑布局 + DroidDeck 菜单 + 将控件拖到屏幕上 + 排列 + 放弃 + 正在保存… + 图标 + 宽− + 宽+ + 高− + 高+ + 隐藏 + 颜色和不透明度… + 复制此布局 + 布局已复制,可粘贴到任意游戏的触控布局中 + 粘贴已复制的布局 + 恢复为 Steam 的默认布局 + 此控件在 Steam 中没有绑定,无法设置图标 + %1$s 的图标 + 触控布局颜色 + 完成 + 无法保存触控布局 + 触控布局已保存到 Steam + 应用 %1$d + 标签(可选) + 未找到 Steam 的图标 + + 来自 Steam 的 %1$d 个图标 + + 图标颜色 + 按钮颜色 + 无图标 + + 正在检查登录状态… + 正在获取你的游戏库… + 正在获取游戏列表… + 正在加载游戏详情…(%1$d/%2$d) + + 正在同步 %1$d 个游戏… + + 正在同步… %1$d/%2$d + 正在获取封面图片… + 同步失败 + 在你的 %1$s 游戏库中未找到游戏 + Amazon Games 未返回你的游戏库信息:请刷新,或重新登录 + 无法连接到 GOG + 无法读取你的 GOG 游戏库 + 正在向 Steam 注册… 开发商 发行商 发行日期 @@ -2111,4 +2342,34 @@ 全收集 %1$s 小时 %1$d 分钟 + Cardboard VR + Cardboard 模式 + 为手机 VR 眼镜提供左右眼画面。 + 画面 + 虚拟屏幕 + 左右并排 3D + 眼镜 + Cardboard + Cardboard(2014) + 无镜片 + 瞳距 + 屏幕宽度 + 屏幕距离 + 头部追踪 + 此设备没有旋转传感器。 + 重置视角 + 重置 + SteamVR(实验性) + 安装 + 下载 Valve 的 ARM64 版 SteamVR(571 MB)。 + 正在下载并解压 SteamVR… + 启动 + 以你的头部作为头显运行 SteamVR;画面会切换为左右并排 3D。 + 停止 + SteamVR 正在运行。 + 此会话中不可用。 + SteamVR 未响应;请查看会话日志。 + 下载组件 + 下载内容与“组件”页面共享。在上方选择已下载的版本,即可在下次启动游戏时使用。 + 选择要下载的版本 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index c74140c50..027e05b42 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -15,8 +15,10 @@ 會話 Steam 或桌面會話開啟時顯示 Steam 正在執行 + Steam 正在下載 會話已暫停 - 準備好後請輕按「繼續」 繼續 + 立即暫停 結束會話 幀生成 選擇幀生成模式 @@ -182,6 +184,8 @@ 應用程式 ID org.ppsspp.PPSSPP 不是 Flathub 應用程式 ID + 亦可使用 .flatpak 檔案,適用於 Flathub 上沒有的應用程式。 + 選擇 .flatpak 檔案 來源 顯示在桌面 新應用程式 @@ -251,6 +255,10 @@ 螢幕控制 Steam + QAM 「自動」會在未連接控制器時顯示螢幕控制。 + Steam 觸控(測試版) + Steam 的觸控控制會在下次 Steam 會話啟用 + Steam 觸控佈局 + 移動、調整大小或隱藏此遊戲的觸控控制;會儲存至 Steam 控制器 Steam 如何識別你的控制器。下次會話生效。 Steam Deck 控制器 @@ -268,6 +276,8 @@ 手動 原生 「自動」會暫停,並在重新顯示時繼續;「原生」會先讓 Steam 準備好休眠;「手動」會等你按「繼續」;「從不」會保持執行。 + 保持下載進行 + Steam 正在下載時,延遲在背景暫停。 「自動」會在背景時暫停,並在重新顯示時繼續;「手動」會等你按「繼續」;「從不」會保持執行。 啟動 隨 DroidDeck 啟動 Steam @@ -331,6 +341,7 @@ 遊戲含有 %1$s(通常可直接使用) 這款遊戲中已有 遊戲資料夾內沒有指向任何組件的內容。 + 已為這款遊戲安裝(%1$d) 所有組件(%1$d) 需要 Windows 安裝程式(%1$d) 這些組件需要在前綴內執行真正的安裝程式(.NET、PhysX、XNA、完整的 Visual C++ 安裝程式等)。DroidDeck 暫時未能做到,下一步就會支援。 @@ -341,6 +352,9 @@ 正在載入組件… 無法安裝:%1$s 已下載 + 正在下載 %1$s + 正在安裝:%1$s + 正在關閉… 下載 %1$s 會在下次啟動遊戲時生效。 拉伸 16:9 遊戲 @@ -397,6 +411,44 @@ 在 Nightlies 檢查新軟件包 Proton 在此選擇的 Proton 會執行所有未在 Steam 中自選 Proton 的遊戲;在 Steam 中所作的變更也會在此顯示。Steam 更新時會取代 Valve 的 Proton;每個版本的原始檔案都會保留。 + 配置 + 配置設定檔 + 設定檔 + 預設設定檔 + 沒有覆寫設定的遊戲會自動使用。 + %1$s 啟動時會自動使用的已儲存設定。 + 只需選擇這款遊戲需要的設定,其餘設定會繼續使用預設設定檔。 + FEX 預設 + 正在沿用預設設定檔的 FEX 預設。 + 已為這款遊戲儲存自訂 FEX 預設。 + 自訂 FEX 變數 + 使用預設 + 遊戲兼容性 + 這款遊戲的 Proton、DXVK、VKD3D 及同步環境選項。 + 正在使用預設設定檔的 Proton。 + 已透過 Steam 兼容性對應為這款遊戲儲存自訂 Proton。 + 無 Proton + DXVK + VKD3D-Proton + 使用預設設定檔中來自 %1$s 的套件,除非這款遊戲選擇了其他套件。 + 使用預設設定檔的元件套件。 + 只在這款遊戲使用 %1$s。 + 運行環境選項 + 其他個別遊戲的 Proton、DXVK、VKD3D-Proton 及同步環境選項。 + 已儲存 %1$s 的設定檔,下次啟動遊戲時生效。 + 不啟動 Steam 直接開始 + 適用於一開就立即關閉的遊戲:不經過 Proton 的 Steam 設定來啟動。疊加層等 Steam 功能對該遊戲會關閉。 + 開 + 已儲存 %1$s 的 Proton 選擇,現已可在 Steam 中使用。 + 已儲存 %1$s 的 Proton 選擇。請先重新啟動 Steam,再啟動遊戲。 + %1$s 無法在此裝置上使用所選的 Proton。 + 無法載入 %1$s 的設定檔:%2$s + 無法儲存遊戲設定檔:%1$s + 重設遊戲設定檔 + 重設 %1$s 的設定檔? + 移除這款遊戲的 Proton、FEX、元件套件、環境、紋理過濾及 Windows 組件覆寫設定?遊戲會改用預設設定檔。 + %1$s 現已使用預設設定檔。 + 無法完全重設遊戲設定檔:%1$s %1$s 現在會執行所有未在 Steam 中自選 Proton 的遊戲。 從下次啟動 Steam 起,%1$s 會執行所有未自選 Proton 的遊戲。 %1$s 無法在此啟動遊戲,因此不能設為預設。 @@ -560,6 +612,18 @@ 停止會話? 正在執行 沒有遊戲正在 Proton 上執行 + 遊戲設定檔 + 暫時未有設定檔詳情。 + 無法載入遊戲設定檔,正在自動重試。 + 無法重新整理遊戲設定檔。正在顯示最後可用的資料,並會自動重試。 + 查看及編輯下次啟動時使用的已儲存設定。 + 正在使用預設設定檔 + 自訂設定檔 + 正在使用預設設定檔中的 %1$s。 + 變更會儲存至 %1$s 的設定檔,並在下次啟動遊戲時生效。 + + %1$d 項遊戲覆寫設定 + 尚未安裝 Proton。 遊戲執行中 於 %1$s 啟動時重新套用 @@ -631,6 +695,10 @@ 用作主畫面 DroidDeck 可用作主畫面應用程式 DroidDeck 不會作為主畫面應用程式的選項 + 螢幕方向 + 自動橫向 + 橫向 + 反向橫向 全螢幕 隱藏 Android 狀態列和導覽列 顯示 Android 狀態列和導覽列 @@ -888,6 +956,12 @@ 該頁面已不存在,請在左側選擇一個部分。 開啟桌面 安裝並開啟桌面 + 解除安裝桌面 + 移除桌面及其內置應用程式。遊戲、已新增的應用程式、設定及共用程式庫會保留。 + 正在移除桌面 + 已移除桌面 + 正在移除舊的 LXQt 檔案 + 請先更新 DroidDeck,再移除此桌面版本。 Linux 桌面 KDE Plasma、Firefox 和你的模擬器 你在 Steam 安裝的遊戲會在會話結束後顯示在這裡。 @@ -916,6 +990,8 @@ 上次遊玩:%1$s 內部儲存空間 已新增的遊戲 + 自訂遊戲 + 遊戲儲存位置 此 GPU 的最佳組合,並會保持最新 自行選擇組合 正在檢查驅動程式… @@ -957,6 +1033,12 @@ 左側觸控板 右側觸控板 兩側 + 控制器 + 搖桿 + 觸控板 + 在搖桿和觸控板之間切換 + 更多 Deck 控制 + 面板 關閉第二螢幕控制 開啟 Steam 選單 左側 · 上方 @@ -992,6 +1074,10 @@ 已安裝 %1$s 先前的要求尚待處理 未安裝 + 有可用更新 → %1$s + 移除舊版 %1$s + 將移除 %1$s。設為使用這些版本的遊戲須改用其他 Proton。 + 設為使用此版本的遊戲須改用其他 Proton。 立即安裝 取消排隊 Android 鍵盤 @@ -1020,6 +1106,8 @@ Proton 遊戲 畫中畫 自動進入畫中畫 + 展開螢幕時的控制 + 展開時,遊戲會顯示在 Steam Deck 觸控板和背面握把上方;摺疊時,遊戲會全螢幕顯示。 已暫停 會話準備就緒後即可使用畫中畫。 請在 Android 設定中允許 DroidDeck 使用畫中畫。 @@ -1046,17 +1134,14 @@ 前端檔案 - 請等待運行環境操作完成 請等待兼容性工具安裝完成 請等待桌面應用程式安裝完成 請等待安裝完成後再啟動遊戲 請先關閉 Steam 會話,以免遊戲存檔覆蓋匯入的內容 - 正在匯入至 %1$s - 已從 %2$s 匯入 %1$d 個檔案至 %3$s @@ -1065,18 +1150,14 @@ 已從 %2$s 匯入 %1$d 個檔案至 %3$s。舊存檔已備份至 Download/DroidDeck/Saves/backups 為 %1$s 選擇存檔 zip - 正在匯出 %1$s - 已將 %1$d 個檔案匯出為 %2$s:%3$s - 選擇 %1$s 的儲存位置(%2$s) - 操作失敗(%1$s):%2$s 尚未有會話記錄:請先執行一次會話。 @@ -1090,23 +1171,18 @@ 已複製 ADB 指令 選擇 ROM 資料夾 不是 Adreno GPU - Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。 仍要安裝 移除 Linux 運行環境 此操作會刪除運行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。 - 已套用:%1$s 無法在主螢幕開啟 %1$s - 無法在 %2$s 開啟 %1$s - 無法在顯示器 %2$d 開啟 %1$s - 正在匯入 正在替換 @@ -1114,20 +1190,17 @@ 正在取消 正在刪除 無法讀取檔案 - 已匯入 %1$s 已取消等待中的替換。 選擇元件套件(-linux .wcp) 無法連接 Nightlies - 已儲存 %1$s 下載失敗:%1$s 選擇 Linux 運行環境驅動程式(-Linux zip 或 Android + Linux 組合包) 選擇顯示驅動程式(AdrenoTools zip 或 Android + Linux 組合包) 選擇驅動程式 zip 或 Android + Linux 組合包 - %1$s:%2$s 正在移除 %1$s… @@ -1135,7 +1208,6 @@ 選擇遊戲儲存資料夾 選擇 Decky 插件 ZIP 無法使用:%1$s - 程式 正在移除 Linux 運行環境 @@ -1147,7 +1219,6 @@ 無法檢查:%1$s 此版本沒有適用於此 DroidDeck 版本的下載 請先停止正在執行的會話,然後再更新 - 正在下載 正在安裝 @@ -1169,18 +1240,15 @@ 系統 Vulkan:沒有適用於此 GPU 的內置版本 %1$s(內置) 手動 - %1$s · %2$s 目前只發佈了其中一半 兩者合共 %1$.0f MB - 沒有可設定的驅動程式:%1$s 正在檢查最新驅動程式… 已是最新(無法檢查:%1$s) 建議的驅動程式目前尚未發佈 - 正在下載 %1$s %2$s… 已更新至 %1$s %2$s。 @@ -1196,10 +1264,8 @@ 正在使用 %1$s 作為運行環境及顯示驅動程式。顯示驅動程式會在 DroidDeck 重新啟動後生效。 已刪除 %1$s(兩個驅動程式) 已刪除 %1$s - 最新:%1$s · %2$s檢查 - 最新:%1$s · 無法連接 %2$s · %3$s檢查 @@ -1213,30 +1279,25 @@ 正在檢查 Banners-Turnip、WinNative 和 DroidDeck… 檢查失敗 - 已安裝 %1$s - 在任一清單中選擇即可同時設定兩個驅動程式 已安裝 %1$s - 請在選單中選擇 請等待運行環境移除完成 - 遊戲 - 觸控板 - 直接 替換失敗:%1$s + 設定檔儲存失敗:%1$s 桌面已暫停 - %1$s 已暫停 程式已暫停 正在啟動桌面 - 正在啟動 %1$s 正在啟動程式 @@ -1244,7 +1305,6 @@ 無法建立會話記錄封存檔。 無法分享會話記錄。 正在啟動會話… - 正在下載 Linux 運行環境 正在下載 Linux 運行環境 · %1$d/%2$d MB @@ -1260,32 +1320,26 @@ 正在解壓 Proton Experimental (ARM64) 無法連接運行環境目錄。請檢查網絡連線,然後再按「開啟 Steam」。 Linux 運行環境未能安裝。請在「設定」中檢查運行環境,然後再按「開啟 Steam」。 - 桌面未能安裝(%1$s)。請檢查網絡連線,然後再按「桌面」。 - %1$s 意外停止。請在錯誤報告中附上記錄,或再試一次。 程式意外停止。請在錯誤報告中附上記錄,或再試一次。 桌面意外停止。請在錯誤報告中附上記錄,或再試一次。 - 結束狀態 %1$d · %2$s 沒有記錄 - 記錄顯示 %1$d 個「Function not implemented」錯誤:proot 的 seccomp 加速令此裝置上的一個輔助程式失敗。請嘗試 %2$s →「%3$s」(或「%4$s」),然後重新啟動。 - 插件 安裝插件 ZIP,在下次 Steam 會話使用。 正在下載… 從 ZIP 安裝 - 遊戲 音訊及影片 @@ -1299,27 +1353,20 @@ 公用程式 熱門 趨勢 - 最新 - 最近更新 正在載入… Flatpak 設定 - %1$s:%2$s 無法在 Flathub 檢查更新 - - 已安裝 - 資料夾 - 石墨 石墨色介面,所有已選取、焦點所在或按下的項目都使用標誌本身的球體藍色。 @@ -1329,7 +1376,6 @@ 黑色背景、白色文字,所有已選取、焦點所在或按下的項目都使用藍色。 電光海軍藍 更明亮的藍色,搭配偏海軍藍而非灰色的介面。 - Esc Ins @@ -1348,15 +1394,12 @@ Alt 空格 AltGr - Tab - %1$s ✓ Ctrl+C Ctrl+D - 請先安裝運行環境 無法使用 Steam 收藏庫 @@ -1364,10 +1407,8 @@ 無法使用遊戲儲存位置 無法使用已新增遊戲的資料夾 無法使用 Steam 清單檔案 - 多個系統 - 內部 Linux 系統 @@ -1386,7 +1427,6 @@ 尚未有我的最愛 - 使用資料夾的 ⋮ 選單將其釘選,即可快速返回。 從我的最愛移除 0 位元組 - 位元組 位置 @@ -1395,7 +1435,6 @@ 修改日期 資料夾 檔案 - %1$s 檔案 唯讀 @@ -1410,18 +1449,14 @@ 刪除失敗 正在移動... 正在複製... - 正在移動 - %1$s 正在複製 - %1$s - 正在移動 %1$d/%2$d - %3$s 正在複製 %1$d/%2$d - %3$s - %1$d 個完成,%2$d 個失敗 - %1$d 個完成,%2$d 個已略過 @@ -1430,7 +1465,6 @@ 已複製 %1$d 個項目 - 「%1$s」已存在 正在重新命名... @@ -1442,19 +1476,16 @@ 建立 新名稱 刪除? - 要永久刪除「%1$s」嗎? 刪除 %1$d 個項目? 此操作無法復原。 - …及另外 %1$d 個 - 正在刪除 %1$d/%2$d - %3$s @@ -1469,7 +1500,6 @@ 略過 選擇此資料夾 返回 - %1$s 已掛載,但目前無法讀取 清單檢視 @@ -1485,50 +1515,40 @@ 隱藏我的最愛 顯示我的最愛 篩選此資料夾 - 可用 %1$s 已選取 %1$d 個 - 無 - 全部 貼上 %1$d 個項目 - 在此貼上 %1$s - 移動 %1$s 至此 檔案 儲存空間 - 快速 我的最愛 下載 - 下載 ROMs 會話記錄 圖片 - 已將「%1$s」加入我的最愛 - 已將「%1$s」從我的最愛移除 空白目錄 返回上一層 選擇資料夾 選擇檔案 - Steam 藍 天藍 @@ -1538,7 +1558,6 @@ 琥珀色 紅色 粉紅色 - A 鍵 B 鍵 @@ -1550,25 +1569,20 @@ 右扳機鍵 檢視按鈕 選單按鈕 - - 檢視 - 選單 十字鍵上 十字鍵下 十字鍵左 十字鍵右 - 隱藏 拖曳以移動 對齊 儲存 - 無 鍵盤 + 觸控板 @@ -1576,7 +1590,6 @@ Deck 握把 + 觸控板 - 正在移除 Linux 運行環境 @@ -1594,11 +1607,9 @@ 尚未安裝 Linux 運行環境 - 目錄中沒有 %1$s 的校驗和 校驗和不符 - 未作任何更改 - 正在安裝 %1$s 無法放置 AppImage @@ -1612,51 +1623,41 @@ 請先設定 Flatpak 正在開始 Flatpak 已結束,狀態為 %1$d - 正在安裝 %1$s 正在更新 %1$s 正在移除 %1$s - 正在安裝 %1$s(%2$d/%3$d) 正在更新 %1$s(%2$d/%3$d) 正在移除 %1$s(%2$d/%3$d) - 翻譯 圖形驅動程式 媒體編解碼器 - %1$s 運行環境 - %1$s 運行環境 %2$s 檔案太小,不可能是 AppImage - 無法讀取檔案:%1$s 這不是 AppImage(它不是 Linux 程式) 此 AppImage 既不是為 ARM64 (aarch64) 亦不是為 x86 電腦而設 這是 Linux 程式,但不是 AppImage 只可匯入第 2 類 AppImage(此檔案為第 %1$d 類) - 可用空間不足:匯入約需 %1$s - 可用空間不足:更新約需 %1$s 匯入失敗 該 AppImage 已不存在 更新失敗 - 正在解壓縮 %1$s 無法解壓縮 AppImage - 無法解壓縮 AppImage:%1$s @@ -1696,26 +1697,19 @@ remote_binary 項目無效 遠端二進位檔名稱無效 遠端二進位檔網址必須使用 HTTPS - 遠端二進位檔 %1$s 的 SHA-256 雜湊值無效 無法建立插件 bin 目錄 - 正在下載插件二進位檔 · %1$s - 無法下載插件二進位檔 %1$s - 插件二進位檔校驗和不符 · %1$s - 無法將插件二進位檔設為可執行 · %1$s - 無法取代插件二進位檔 · %1$s - 無法安裝插件二進位檔 · %1$s 插件資料夾已屬於另一個插件 @@ -1726,10 +1720,8 @@ 無法儲存無線偵錯身分 - ADB 指令失敗(%1$d) - Android 未確認子進程限制已更改:%1$s 沒有已儲存的無線偵錯配對 @@ -1737,10 +1729,8 @@ 等待配對彈出式視窗逾時,請在 DroidDeck 重試。 - 無法尋找配對彈出式視窗(%1$d)。請確認 Wi-Fi 已開啟。 - 無法尋找配對彈出式視窗:%1$s 請輸入完整的 6 位數配對碼 @@ -1755,13 +1745,11 @@ 配對無線偵錯 開啟無線偵錯,然後輕按「使用配對碼配對裝置」。 開啟無線偵錯,然後輕按「使用配對碼配對裝置」。請留在「設定」中:配對碼需在此通知中輸入。 - 6 位數配對碼 輕按「輸入配對碼」,然後輸入「設定」中顯示的 6 位數字。 輸入 Wi-Fi 配對碼 請再次輸入配對碼 - 輸入配對碼 正在設定 Steam @@ -1773,12 +1761,10 @@ 下載回應 HTTP %1$d 下載的大小與已發佈的 APK 不符,已被捨棄 - 請再試一次 下載的檔案與其校驗和不符,已被捨棄 - 請再試一次 - 此更新屬於 Android 套件 %1$s,而非 %2$s 更新中繼資料未指明 DroidDeck 的發佈簽署金鑰 Android 無法讀取已下載的 APK - 已下載的 APK 屬於 %1$s,而非 %2$s 已下載的 APK 的 versionCode 為 %1$d,而非已發佈的 %2$d @@ -1788,14 +1774,11 @@ 更新目錄回應 HTTP %1$d - 正在啟動會話… - 已過 %1$d:%2$02d · 仍在處理 正在下載 Steam 用戶端更新 · %1$d%% - 正在下載 Steam 用戶端 · %1$s @@ -1816,7 +1799,6 @@ 校驗和不符;已刪除下載的檔案 下載期間有會話啟動;請先停止該會話,再安裝兼容性工具 正在安裝 %1$s - %1$s 安裝失敗(結束代碼 %2$d) 無法安裝 %1$s @@ -1827,30 +1809,24 @@ 只有 DLL 原始(上次替換後 Proton 已更新) 已在「元件」以外更改 - 不存在 - 原始 %1$s 原始版本來自 %1$s %1$s %2$s 會在 %3$s 上執行的遊戲關閉後放入其中。 %1$s %2$s 現已放入 %3$s,會在下次啟動遊戲時生效。 未儲存該原始組合包 - %2$s 的 %1$s 會在其上執行的遊戲關閉後還原為原始版本。 - 已從 %3$s 的原始版本還原 %2$s 的 %1$s。 - %1$s 正在 %2$s 中使用。請先將該 Proton 替換為其他版本。 未儲存 %1$s 已刪除 %1$s。 已安裝版本的原始版本會保留,以便還原。 未儲存 - 已刪除 %2$s 的 %1$s 原始版本。 只會從 Nightlies 發佈版本下載 @@ -1902,7 +1878,6 @@ 下載回應 HTTP %1$d 此驅動程式清單早於校驗和推出 - 請重新整理後再試一次 校驗和不符 - 已捨棄下載的檔案 - 不是 AdrenoTools 驅動程式:%1$s 沒有 meta.json @@ -1910,7 +1885,6 @@ 這是 Linux 運行環境驅動程式(請在「運行環境驅動程式」下匯入) 這是 Wayland 遊戲驅動程式,此應用程式用不上 meta.json 沒有指明 libraryName,因此無法交給 AdrenoTools - meta.json 指明了 %1$s,但 zip 中沒有此檔案 %1$s 不是 64 位元 AArch64 ELF 共用程式庫 @@ -1924,22 +1898,19 @@ SD 卡 - 資料夾 - 分享會話記錄 + 分享會話記錄 需要 Android 10 或更新版本(支援色彩空間的顯示圖層) 此螢幕沒有回報支援 HDR - 此螢幕支援 %1$s,但不支援 HDR10 - 此 GPU 子進程限制已開啟 @@ -1947,7 +1918,6 @@ 無法檢查子進程限制 子進程限制已關閉 無需理會子進程限制 - %1$s 如無法這樣做,請將此裝置連接到已安裝 ADB 的電腦,並執行以下指令,然後返回這裡再檢查一次。 可以啟動 Steam 會話。 @@ -1955,81 +1925,60 @@ 在開發人員選項中開啟「停用子進程限制」,之後請保持開發人員選項開啟。 此 Android 版本沒有此開關。請使用無線偵錯,直接在此裝置上更改。 此版本沒有適用於此 DroidDeck 版本的下載。 - Android 不會以此版本覆蓋安裝,因為其 versionCode %1$d 低於已安裝的 %2$d。 Android 不會以舊版本覆蓋較新的版本。請繼續使用此版本,直至該頻道追上為止。 此 DroidDeck 的簽署方式不同(可能是本機自行建置的版本?),因此 Android 無法直接更新。 可用空間不足,無法更新。 已取消更新。 - 更新未能安裝:%1$s 更新未能安裝。 - %1$s zip - zip - %1$s 尚未啟動過 找不到 %1$s 的存檔 - %1$s 沒有 drive_c 資料夾,因此不是 GameHub 或 Winlator 存檔 zip - 無法在 %1$s 中建立資料夾 無法寫入 %1$s - 無法寫入 %1$s(%2$s) 無法使用資料夾 - 另一個檔案已使用 %1$s 無法使用圖片資料夾 資料夾中有不相關的同步清單 - 無法移除 %1$s 檔案超出匯出資料夾範圍 - FEX(沒有版本名稱的版本) - %1$s 中沒有 profile.json - %1$s 不是 Linux 元件套件(類型為 %2$s) - %1$s · 可用 %2$s - 自動 - 更新目錄未通過檢查(%1$s) - DroidDeck 子進程設定 - 已登入 · 離線模式 已登入 - 內置 已下載 已匯入 - ANDROID + LINUX - %1$s。%2$s\n\n%3$s 會話顯示 @@ -2048,6 +1997,7 @@ 幀率 %1$s 將縮放設為 %2$s。 配合螢幕(%1$d×%2$d) + 跟隨螢幕(摺疊或展開時調整大小) Steam 顯示裝置詳情 隱藏裝置詳情 @@ -2057,6 +2007,186 @@ 已複製。 強制 SSBS 讓一項 Wine 會不斷關閉的 CPU 速度設定保持開啟。在 Snapdragon 8 Elite 上可能令 Windows 遊戲更快;在較舊的晶片上變化不大。下次啟動遊戲時生效。 + + 商店 + Steam + 自訂 + 商店 + 開啟商店時顯示 + 下載速度等級 + 商店引擎 + ● 引擎已就緒 + 未建置 + 平衡 + 快速 + 最高 + 商店 + 商店設定 + 設定 + (未登入) + 商店 + 已安裝 + 收藏庫 + 全部 + 下載 + %1$s(%2$d) + 登入 + %1$s · 登出 + 登出 %1$s? + 已安裝的遊戲會保留。重新登入時會再次取得收藏庫。 + 保持登入 + 登出 + 搜尋 %1$s 目錄… + 搜尋已安裝的遊戲… + 搜尋你的收藏庫… + 搜尋 + 未安裝任何遊戲 + 正在載入 + 沒有遊戲 + 正在載入商店… + 沒有目錄 + 最新推出 + 優惠 + 免費遊戲 + 趨勢 + 你的收藏庫 + 開始遊戲 + 安裝 + 繼續安裝 + 已中斷 + 安裝 · %1$s + 免費領取 + 在 %1$s 查看 + 購買 · %1$s + 已安裝 + 已在收藏庫 + 免費 + 繼續 + 重新整理 + 顯示成人內容 + Epic + EOS + 離線 + 關閉 + Epic 登入(EOS) + 離線啟動 + 解決 Epic 登入問題 + 雲端存檔 + 上載至雲端 + 從雲端下載 + 沒有雲端存檔 + 雲端衝突 + 雲端衝突 · %1$d 個檔案 + … + 保留雲端版本 + 保留本機版本 + 正在檢查雲端… + 上次同步:%1$s + 尚未同步 + %1$d 個檔案 + 已安裝 + 在你的收藏庫中 + 商店 + 成就 + 已解鎖 %1$d / %2$d + %1$d / %2$d + 已解鎖 · %1$d + 未解鎖 · %1$d + 隱藏 · %1$d + 隱藏成就 + 解鎖後會顯示詳情。 + 已解鎖全部成就 + 正在讀取… + 登入 %1$s 以查看你的成就。 + 無法連接 %1$s。請在連線後再查看。 + %1$s 未列出此遊戲的成就。 + 顯示全部 %1$d 項 + 成就 · %1$s + ✓ 已在 Steam + 解除安裝 + 正在解除安裝… + 解除安裝 %1$s? + 將釋放 %1$s,並從 Steam 移除。 + 將移除其檔案及 Steam 捷徑。 + Steam 下次啟動時會加入此遊戲;請從 Steam 啟動,或立即啟動 Steam。 + Proton + 由 Steam 按遊戲選擇 + 音訊 + DirectAudio + 元件 + FEX、DXVK、VKD3D + 引擎 + %1$s · 原生 + %1$s · 內置 + 運行方式 + 取自 + %1$s(使用你的帳戶) + 安裝位置 + 遊戲儲存位置(Games/%1$s/%2$s) + 啟動方式 + Steam 及 Proton,與 Steam 遊戲相同 + Steam 及 Proton · 每次啟動時登入 Epic,EOS 疊加層關閉 + Steam 及 Proton · 已設定 Amazon Games Services 變數 + Steam + 安裝時加入 Steam(Deck 介面 + Steam Input) + 沒有下載項目 + 已暫停 + 排隊中 + 排隊中 · 第 %1$d 位 + 已取消 + 失敗 + 失敗 · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + 暫停 + 繼續 + 取消 + 重試 + 清除 + 清除已完成 + 清單 + 下載 + 驗證 + 安裝 + 完成 + 正在檢查 + 正在下載 + 刪除下載? + 失敗 · 已保留 %1$s + 正在驗證 + 正在安裝 + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d 個檔案 + 下載管理員 + 同時下載數目 + 安裝至 + 安裝 %1$s: + 內部儲存空間 + 可用 %1$s + 商店下載 + 顯示遊戲下載進度,並讓下載在背景繼續進行 + 正在下載 + %1$d 項下載 + 正在準備… + 已完成的商店下載 + 已安裝 %1$s + %1$s 下載失敗 + 刪除 + 保留 + 登入 %1$s + 透過你的帳戶提供者登入 + 重新載入 + 關閉 + 再試一次 + 正在完成登入… + 無法載入登入頁面:%1$s + 此提供者不允許從內嵌頁面登入。請改用商店密碼登入。 + 登入頁面回應錯誤(HTTP %1$d)。 + 與登入頁面的連線不安全。 + 無法驗證登入,請再試一次。 + 登入失敗,請再試一次。 關閉 返回 編輯 @@ -2088,21 +2218,122 @@ %1$d 個已在遊戲中 - 成就 - 已解鎖 %1$d / %2$d - %1$d / %2$d - 已解鎖 · %1$d - 未解鎖 · %1$d - 隱藏 · %1$d - 隱藏成就 - 解鎖後會顯示詳情。 - 已解鎖全部成就 - 正在讀取… - 登入 %1$s 以查看你的成就。 - 無法連接 %1$s。請在連線後再查看。 - %1$s 未列出此遊戲的成就。 - 顯示全部 %1$d 項 - 成就 · %1$s + B · 返回%1$s + 遊戲 + 遊玩設定 + 圖形驅動程式 + 此 Proton + 所有 Proton + 適用於所有 Proton + 可用 · %1$d + 上次檢查:%1$s + 檢查新元件及驅動程式 + 取得 + %1$s · 以下的 DXVK、VKD3D-Proton 和 FEX 屬於此 Proton + %1$s · 已測試 + %1$s · 未經測試 + %1$s · 不支援 + 讓建議的組合保持最新 + 自動 · %1$s + 分別設定運行環境及顯示驅動程式 + 使用 %1$s? + 移除 %1$s? + 診斷 + 可以開始遊戲 + 尚未可以開始遊戲 + 運行環境 %1$s + 子進程限制已關閉 + 已登入為 %1$s + 需要你處理時 + 外觀 + 此裝置 + 收藏庫圖片 + Linux 應用程式 + Flathub + Linux 桌面的桌面應用程式,可從「桌面」開啟。 + 設定 Flathub + 移除 %1$s? + 其設定和檔案會保留。 + 保留 + 佔用空間 + 解除安裝桌面? + 全部 + GOG 成就 + 為受支援的 GOG 遊戲同步成就。下次啟動時生效。 + 成就已關閉 + 成就已開啟 + 重新登入 GOG + 成就已上載 + 成就上載待處理 + 在「商店」設定中開啟 GOG 成就 + 成就不可用;請查看工作階段日誌 + 成就已解鎖 + 貼上 + 方向鍵 + 左搖桿 + 右搖桿 + 選單 + 鍵盤 + 方向鍵上 + 方向鍵下 + 方向鍵左 + 方向鍵右 + 觸控操作 + 觸控控制器 + 滑鼠 + 開 + 關 + 慢 + 正常 + 快 + 編輯佈局 + DroidDeck 選單 + 將控制項拖曳到畫面上 + 排列 + 放棄 + 儲存中… + 圖示 + 闊− + 闊+ + 高− + 高+ + 隱藏 + 顏色及不透明度… + 複製此佈局 + 佈局已複製,可貼到任何遊戲的觸控佈局中 + 貼上已複製的佈局 + 還原為 Steam 的預設佈局 + 此控制項在 Steam 中未綁定操作,無法設定圖示 + %1$s 的圖示 + 觸控佈局顏色 + 完成 + 無法儲存觸控佈局 + 觸控佈局已儲存至 Steam + 應用程式 %1$d + 標籤(選用) + 找不到 Steam 的圖示 + + 來自 Steam 的 %1$d 個圖示 + + 圖示顏色 + 按鈕顏色 + 沒有圖示 + + 正在檢查登入狀態… + 正在取得你的遊戲庫… + 正在取得遊戲清單… + 正在載入遊戲詳情…(%1$d/%2$d) + + 正在同步 %1$d 個遊戲… + + 正在同步… %1$d/%2$d + 正在取得封面圖片… + 同步失敗 + 在你的 %1$s 遊戲庫中找不到遊戲 + Amazon Games 未回應你的遊戲庫要求:請重新整理,或重新登入 + 無法連線至 GOG + 無法讀取你的 GOG 遊戲庫 + 正在向 Steam 註冊… 開發商 發行商 發行日期 @@ -2111,4 +2342,34 @@ 全收集 %1$s 小時 %1$d 分鐘 + Cardboard VR + Cardboard 模式 + 為手機 VR 眼鏡提供左右眼畫面。 + 畫面 + 虛擬螢幕 + 左右並排 3D + 眼鏡 + Cardboard + Cardboard(2014) + 無鏡片 + 瞳距 + 螢幕闊度 + 螢幕距離 + 頭部追蹤 + 此裝置沒有旋轉感應器。 + 重設視角 + 重設 + SteamVR(實驗性) + 安裝 + 下載 Valve 的 ARM64 版 SteamVR(571 MB)。 + 正在下載並解壓 SteamVR… + 啟動 + 以你的頭部作為頭戴裝置執行 SteamVR;畫面會切換為左右並排 3D。 + 停止 + SteamVR 正在執行。 + 此工作階段中無法使用。 + SteamVR 沒有回應;請查看工作階段日誌。 + 下載元件 + 下載內容與「元件」頁面共用。在上方選擇已下載的版本,即可在下次啟動遊戲時使用。 + 選擇要下載的版本 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 768c2eb4c..9957bd3e1 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -15,8 +15,10 @@ 工作階段 在 Steam 或桌面工作階段開啟時顯示 Steam 正在執行 + Steam 正在下載 工作階段已暫停 - 準備好後點擊繼續 繼續 + 立即暫停 結束此工作階段 影格生成 請選擇你要使用的影格生成模式 @@ -182,6 +184,8 @@ 應用程式 ID org.ppsspp.PPSSPP 不是有效的 Flathub 應用程式 ID + 或是 .flatpak 檔案,適用於 Flathub 上沒有的應用程式。 + 選擇 .flatpak 檔案 來源 顯示在桌面 新應用程式 @@ -251,6 +255,10 @@ 螢幕控制 Steam + QAM 自動模式會在未連接控制器時顯示螢幕控制。 + Steam 觸控(測試版) + Steam 觸控控制會在下次 Steam 工作階段啟用 + Steam 觸控版面配置 + 移動、調整大小或隱藏這款遊戲的觸控控制;會儲存到 Steam 控制器 Steam 辨識你的控制器的方式。下次工作階段生效。 Steam Deck 控制器 @@ -268,6 +276,8 @@ 手動 原生 「自動」會暫停,並在重新顯示時繼續。「原生」會先讓 Steam 做好睡眠準備。「手動」會等你點擊「繼續」。「從不」則會持續執行。 + 讓下載持續進行 + Steam 正在下載時,延後在背景暫停。 「自動」會在背景時暫停,並在重新顯示時繼續。「手動」會等你點擊「繼續」。「從不」則會持續執行。 啟動 隨 DroidDeck 啟動 Steam @@ -331,6 +341,7 @@ 遊戲含有 %1$s(通常可直接使用) 這款遊戲中已有 遊戲資料夾中沒有指向任何元件的內容。 + 已為這款遊戲安裝(%1$d) 所有元件(%1$d) 需要 Windows 安裝程式(%1$d) 這些元件需要在前綴中執行真正的安裝程式(.NET、PhysX、XNA、完整的 Visual C++ 安裝程式等)。DroidDeck 目前還做不到,接下來就會支援。 @@ -341,6 +352,9 @@ 正在載入元件… 無法安裝:%1$s 已下載 + 正在下載 %1$s + 正在安裝:%1$s + 正在關閉… 下載 %1$s 將在下次啟動遊戲時生效。 拉伸 16:9 遊戲 @@ -397,6 +411,44 @@ 檢查 Nightlies 中的新套件 Proton 這裡選擇的 Proton 會執行所有未在 Steam 中自選 Proton 的遊戲,在 Steam 中所做的變更也會顯示在這裡。Steam 更新時會取代 Valve 的 Proton;每個組建版本的原始檔案都會保留。 + 設定 + 調整設定檔 + 設定檔 + 預設設定檔 + 沒有覆寫設定的遊戲會自動使用。 + %1$s 啟動時自動使用的已儲存設定。 + 只選擇這款遊戲需要的設定,其餘設定會繼續使用預設設定檔。 + FEX 預設集 + 使用預設設定檔的預設集。 + 已為這款遊戲儲存自訂預設集。 + 自訂 FEX 變數 + 使用預設 + 遊戲相容性 + 這款遊戲的 Proton、DXVK、VKD3D 與同步環境選項。 + 使用預設設定檔的 Proton。 + 已透過 Steam 相容性對應為這款遊戲儲存自訂 Proton。 + 無 Proton + DXVK + VKD3D-Proton + 使用預設設定檔中來自 %1$s 的套件,除非這款遊戲另選其他套件。 + 使用預設設定檔的元件套件。 + 僅這款遊戲使用 %1$s。 + 執行環境選項 + 其他個別遊戲的 Proton、DXVK、VKD3D-Proton 與同步環境選項。 + 已儲存 %1$s 的設定檔,下次啟動遊戲時生效。 + 不啟動 Steam 直接開始 + 適用於一開就立即關閉的遊戲:不經過 Proton 的 Steam 設定來啟動。疊加層等 Steam 功能對這款遊戲會關閉。 + 開 + 已儲存 %1$s 的 Proton 選擇,現在即可在 Steam 中使用。 + 已儲存 %1$s 的 Proton 選擇。啟動遊戲前請重新啟動 Steam。 + %1$s 無法在這部裝置上使用所選的 Proton。 + 無法載入 %1$s 的設定檔:%2$s + 無法儲存遊戲設定檔:%1$s + 重設遊戲設定檔 + 要重設 %1$s 的設定檔嗎? + 要移除這款遊戲的 Proton、FEX、元件套件、環境、材質過濾和 Windows 元件覆寫嗎?遊戲將改用預設設定檔。 + %1$s 現在使用預設設定檔。 + 無法完全重設遊戲設定檔:%1$s %1$s 現在會執行所有未在 Steam 中自選 Proton 的遊戲。 從下次啟動 Steam 起,%1$s 會執行所有未自選 Proton 的遊戲。 %1$s 無法在此啟動遊戲,因此不能設為預設。 @@ -560,6 +612,18 @@ 停止工作階段? 目前正在執行 目前沒有使用 Proton 執行的遊戲 + 遊戲設定檔 + 尚無法取得設定檔詳細資料。 + 無法載入遊戲設定檔,正在自動重試。 + 無法重新整理遊戲設定檔。目前顯示最後取得的資料,正在自動重試。 + 檢視並編輯下次啟動時使用的已儲存設定。 + 使用預設設定檔 + 自訂設定檔 + 使用預設設定檔中的 %1$s。 + 變更會儲存到 %1$s 的設定檔,並在下次啟動遊戲時生效。 + + %1$d 項遊戲覆寫 + 尚未安裝 Proton。 遊戲執行中 已在啟動 %1$s 時重新套用 @@ -631,6 +695,10 @@ 用作主螢幕 DroidDeck 可以作為主螢幕應用程式 DroidDeck 不會作為主螢幕應用程式提供 + 螢幕方向 + 自動橫向 + 橫向 + 反向橫向 全螢幕 隱藏 Android 狀態列和導覽列 顯示 Android 狀態列和導覽列 @@ -888,6 +956,12 @@ 該頁面已不存在。請從左側選擇一個區段。 開啟桌面 安裝並開啟桌面 + 解除安裝桌面 + 移除桌面及其內建的應用程式。遊戲、已新增的應用程式、設定和共用程式庫都會保留。 + 正在移除桌面 + 已移除桌面 + 正在移除舊的 LXQt 檔案 + 請先更新 DroidDeck,再移除此桌面版本。 Linux 桌面 KDE Plasma、Firefox 和你的模擬器 在 Steam 中安裝的遊戲會在工作階段結束後顯示在這裡。 @@ -916,6 +990,8 @@ 上次執行 %1$s 內部儲存空間 已新增的遊戲 + 自訂遊戲 + 遊戲儲存空間 為此 GPU 選擇最佳驅動程式組合,並保持最新 自行選擇驅動程式組合 正在檢查驅動程式… @@ -957,6 +1033,12 @@ 左側觸控板 右側觸控板 兩側 + 手把 + 搖桿 + 觸控板 + 在搖桿與觸控板之間切換 + 更多 Deck 控制 + 面板 關閉第二螢幕控制 開啟 Steam 選單 左側 · 上方 @@ -992,6 +1074,10 @@ 已安裝 %1$s 上一次要求留下的待處理項 未安裝 + 有可用更新 → %1$s + 移除舊版 %1$s + 將移除 %1$s。設定為使用這些版本的遊戲必須改用其他 Proton。 + 設定為使用此版本的遊戲必須改用其他 Proton。 立即安裝 取消排隊 Android 鍵盤 @@ -1020,6 +1106,8 @@ Proton 遊戲 子母畫面 自動進入子母畫面 + 展開螢幕時的控制 + 展開時,遊戲會顯示在 Steam Deck 觸控板和背面握把上方。摺疊時,遊戲以全螢幕顯示。 已暫停 工作階段準備就緒後即可使用子母畫面。 請在 Android 設定中允許 DroidDeck 使用子母畫面。 @@ -1046,17 +1134,14 @@ 前端檔案 - 請等待執行環境作業完成 請等待相容性工具安裝完成 請等待桌面應用程式安裝完成 請等待安裝完成後再啟動遊戲 請先關閉 Steam 工作階段,以免遊戲存檔覆蓋匯入的內容 - 正在匯入到 %1$s - 已從 %2$s 匯入 %1$d 個檔案到 %3$s @@ -1065,18 +1150,14 @@ 已從 %2$s 匯入 %1$d 個檔案到 %3$s。舊存檔已備份到 Download/DroidDeck/Saves/backups 選擇 %1$s 的存檔壓縮檔 - 正在匯出 %1$s - 已將 %1$d 個檔案匯出為 %2$s:%3$s - 選擇 %1$s 的儲存位置(%2$s) - 失敗(%1$s):%2$s 還沒有工作階段日誌:請先執行一次工作階段。 @@ -1090,23 +1171,18 @@ 已複製 ADB 指令 選擇 ROM 資料夾 不是 Adreno GPU - Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。 仍要安裝 移除 Linux 執行環境 這會刪除執行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。 - 已套用:%1$s 無法在主螢幕上開啟 %1$s - 無法在 %2$s 上開啟 %1$s - 無法在螢幕 %2$d 上開啟 %1$s - 正在匯入 正在換入 @@ -1114,20 +1190,17 @@ 正在取消 正在刪除 無法讀取檔案 - 已匯入 %1$s 已取消等待中的換入。 選擇元件套件(-linux .wcp) 無法連線到 Nightlies - 已儲存 %1$s 下載失敗:%1$s 選擇 Linux 執行環境驅動程式(-Linux zip 或 Android + Linux 套件組合) 選擇顯示驅動程式(AdrenoTools zip 或 Android + Linux 套件組合) 選擇驅動程式 zip 或 Android + Linux 套件組合 - %1$s:%2$s 正在移除 %1$s… @@ -1135,7 +1208,6 @@ 選擇遊戲儲存資料夾 選擇 Decky 外掛程式 ZIP 無法使用:%1$s - 程式 正在移除 Linux 執行環境 @@ -1147,7 +1219,6 @@ 無法檢查:%1$s 此組建版本沒有適用於此 DroidDeck 複本的下載檔案 請先停止正在執行的工作階段,再進行更新 - 正在下載 正在安裝 @@ -1169,18 +1240,15 @@ 系統 Vulkan:沒有適用於此 GPU 的內建版本 %1$s(內建) 手動 - %1$s · %2$s 目前只發布了其中一半 兩者共 %1$.0f MB - 沒有可設定的驅動程式:%1$s 正在檢查最新驅動程式… 已是最新(無法檢查:%1$s) 建議的驅動程式目前尚未發布 - 正在下載 %1$s %2$s… 已更新到 %1$s %2$s。 @@ -1196,10 +1264,8 @@ 正在將 %1$s 同時用作執行環境驅動程式和顯示驅動程式。顯示驅動程式會在 DroidDeck 重新啟動後生效。 已刪除 %1$s(兩個驅動程式) 已刪除 %1$s - 最新:%1$s · 檢查於 %2$s - 最新:%1$s · 無法連線到 %2$s · 檢查於 %3$s @@ -1213,30 +1279,25 @@ 正在檢查 Banners-Turnip、WinNative 和 DroidDeck… 檢查失敗 - 已安裝 %1$s - 在任一清單中選擇它即可同時設定兩個驅動程式 已安裝 %1$s - 請在選單中選擇 請等待執行環境移除完成 - 遊戲 - 觸控板 - 直接 換入失敗:%1$s + 設定檔儲存失敗:%1$s 桌面已暫停 - %1$s 已暫停 程式已暫停 正在啟動桌面 - 正在啟動 %1$s 正在啟動程式 @@ -1244,7 +1305,6 @@ 無法建立工作階段日誌封存檔。 無法分享工作階段日誌。 正在啟動工作階段… - 正在下載 Linux 執行環境 正在下載 Linux 執行環境 · %1$d/%2$d MB @@ -1260,32 +1320,26 @@ 正在解壓縮 Proton Experimental(ARM64) 無法連線到執行環境目錄。請檢查網路連線,然後再按一次「執行」。 Linux 執行環境未能安裝。請在「設定」中檢查執行環境,然後再按一次「執行」。 - 桌面未能安裝(%1$s)。請檢查網路連線,然後再按一次「桌面」。 - %1$s 意外停止。請在錯誤報告中附上日誌,或再試一次。 程式意外停止。請在錯誤報告中附上日誌,或再試一次。 桌面意外停止。請在錯誤報告中附上日誌,或再試一次。 - 結束狀態 %1$d · %2$s 沒有日誌 - 日誌中出現 %1$d 個「Function not implemented」錯誤:proot 的 seccomp 加速在這部裝置上導致輔助程式失敗。請嘗試 %2$s →「%3$s」(或「%4$s」),然後重新啟動。 - 外掛程式 安裝外掛程式 ZIP,供下次 Steam 工作階段使用。 正在下載… 從 ZIP 安裝 - 遊戲 影音 @@ -1299,27 +1353,20 @@ 工具程式 熱門 趨勢 - 最新上架 - 最近更新 正在載入… Flatpak 設定 - %1$s:%2$s 無法從 Flathub 檢查更新 - - 已安裝 - 資料夾 - 石墨 石墨色介面,選取、聚焦或按下的項目都使用標誌本身的球體藍。 @@ -1329,7 +1376,6 @@ 黑色背景、白色文字,選取、聚焦或按下的項目使用藍色。 電光藏青 更亮的藍色,搭配偏藏青而非灰色的介面。 - Esc Ins @@ -1348,15 +1394,12 @@ Alt Space AltGr - Tab - %1$s ✓ Ctrl+C Ctrl+D - 請先安裝執行環境 無法使用 Steam 收藏庫 @@ -1364,10 +1407,8 @@ 無法使用遊戲儲存空間 無法使用已新增遊戲的資料夾 無法使用 Steam 資訊清單 - 多種主機 - 內部 Linux 系統 @@ -1386,7 +1427,6 @@ 還沒有我的最愛 - 透過資料夾的 ⋮ 選單釘選,即可快速回到這裡。 從我的最愛移除 0 位元組 - 位元組 位置 @@ -1395,7 +1435,6 @@ 修改日期 資料夾 檔案 - %1$s 檔案 唯讀 @@ -1410,18 +1449,14 @@ 刪除失敗 正在移動... 正在複製... - 正在移動 - %1$s 正在複製 - %1$s - 正在移動 %1$d/%2$d - %3$s 正在複製 %1$d/%2$d - %3$s - %1$d 個完成,%2$d 個失敗 - %1$d 個完成,%2$d 個略過 @@ -1430,7 +1465,6 @@ 已複製 %1$d 個項目 - 「%1$s」已存在 正在重新命名... @@ -1442,19 +1476,16 @@ 建立 新名稱 要刪除嗎? - 要永久刪除「%1$s」嗎? 要刪除 %1$d 個項目嗎? 此動作無法復原。 - …及其他 %1$d 個 - 正在刪除 %1$d/%2$d - %3$s @@ -1469,7 +1500,6 @@ 略過 選擇此資料夾 返回 - %1$s 已掛載,但目前無法讀取 清單檢視 @@ -1485,50 +1515,40 @@ 隱藏我的最愛 顯示我的最愛 篩選此資料夾 - 可用 %1$s 已選取 %1$d 個 - 全不選 - 全選 貼上 %1$d 個項目 - 將 %1$s 貼到這裡 - 將 %1$s 移動到這裡 檔案 儲存空間 - 快速存取 我的最愛 下載 - 下載 ROM 工作階段日誌 圖片 - 已將「%1$s」加入我的最愛 - 已將「%1$s」從我的最愛移除 空的資料夾 返回上一層 選擇資料夾 選擇檔案 - Steam 藍 天藍 @@ -1538,7 +1558,6 @@ 琥珀 紅色 粉紅 - A 按鈕 B 按鈕 @@ -1550,25 +1569,20 @@ 右扳機 檢視按鈕 選單按鈕 - - 檢視 - 選單 方向鍵上 方向鍵下 方向鍵左 方向鍵右 - 隱藏 拖曳以移動 對齊 儲存 - 無 鍵盤 + 觸控板 @@ -1576,7 +1590,6 @@ Deck 握把 + 觸控板 - 正在移除 Linux 執行環境 @@ -1594,11 +1607,9 @@ 尚未安裝 Linux 執行環境 - 目錄中沒有 %1$s 的校驗碼 校驗碼不符 - 未進行任何變更 - 正在安裝 %1$s 無法放置 AppImage @@ -1612,51 +1623,41 @@ 請先設定 Flatpak 正在啟動 Flatpak 已結束,狀態碼 %1$d - 正在安裝 %1$s 正在更新 %1$s 正在移除 %1$s - 正在安裝 %1$s(%2$d/%3$d) 正在更新 %1$s(%2$d/%3$d) 正在移除 %1$s(%2$d/%3$d) - 翻譯 圖形驅動程式 媒體轉碼器 - %1$s 執行環境 - %1$s 執行環境 %2$s 檔案太小,不可能是 AppImage - 無法讀取檔案:%1$s 這不是 AppImage(不是 Linux 程式) 此 AppImage 既不是為 ARM64(aarch64)也不是為 x86 電腦組建的 這是 Linux 程式,但不是 AppImage 只能匯入第 2 型 AppImage(此檔案為第 %1$d 型) - 可用空間不足:匯入需要約 %1$s - 可用空間不足:更新需要約 %1$s 匯入失敗 該 AppImage 已不存在 更新失敗 - 正在解壓縮 %1$s 無法解壓縮 AppImage - 無法解壓縮 AppImage:%1$s @@ -1696,26 +1697,19 @@ remote_binary 項目無效 遠端二進位檔名稱無效 遠端二進位檔 URL 必須使用 HTTPS - 遠端二進位檔 %1$s 的 SHA-256 雜湊值無效 無法建立外掛程式的 bin 目錄 - 正在下載外掛程式二進位檔 · %1$s - 無法下載外掛程式二進位檔 %1$s - 外掛程式二進位檔校驗碼不符 · %1$s - 無法將外掛程式二進位檔標示為可執行 · %1$s - 無法取代外掛程式二進位檔 · %1$s - 無法安裝外掛程式二進位檔 · %1$s 外掛程式資料夾屬於另一個外掛程式 @@ -1726,10 +1720,8 @@ 無法儲存無線偵錯身分 - ADB 指令失敗(%1$d) - Android 未確認子處理程序限制已變更:%1$s 尚未儲存任何無線偵錯配對 @@ -1737,10 +1729,8 @@ 等待配對彈出視窗逾時。請從 DroidDeck 再試一次。 - 無法尋找配對彈出視窗(%1$d)。請確認 Wi-Fi 已開啟。 - 無法尋找配對彈出視窗:%1$s 請輸入完整的 6 位數配對碼 @@ -1755,13 +1745,11 @@ 配對無線偵錯 開啟無線偵錯,然後點擊「使用配對碼配對裝置」。 開啟無線偵錯,然後點擊「使用配對碼配對裝置」。請留在「設定」中:配對碼要輸入在這則通知裡。 - 6 位數配對碼 點擊「輸入配對碼」,然後輸入「設定」中顯示的 6 位數字。 輸入 Wi-Fi 配對碼 請再次輸入配對碼 - 輸入配對碼 正在設定 Steam @@ -1773,12 +1761,10 @@ 下載回應 HTTP %1$d 下載的大小與發布的 APK 不符,已捨棄 - 請再試一次 下載的檔案與校驗碼不符,已捨棄 - 請再試一次 - 此更新適用於 Android 套件 %1$s,而非 %2$s 更新中繼資料未列出 DroidDeck 的發行簽署金鑰 Android 無法讀取下載的 APK - 下載的 APK 適用於 %1$s,而非 %2$s 下載的 APK 標示 versionCode 為 %1$d,與發布的 %2$d 不符 @@ -1788,14 +1774,11 @@ 更新目錄回應 HTTP %1$d - 正在啟動工作階段… - 已經過 %1$d:%2$02d · 仍在處理中 正在下載 Steam 用戶端更新 · %1$d%% - 正在下載 Steam 用戶端 · %1$s @@ -1816,7 +1799,6 @@ 校驗碼不符;已刪除下載內容 下載期間有工作階段啟動;請先停止它,再安裝相容性工具 正在安裝 %1$s - %1$s 安裝失敗(結束代碼 %2$d) 無法安裝 %1$s @@ -1827,30 +1809,24 @@ 僅 DLL 原始(上次換入後 Proton 已更新) 已在「元件」之外變更 - 不存在 - 原始 %1$s 原始版本來自 %1$s %1$s %2$s 會在 %3$s 上執行的遊戲關閉後換入。 %1$s %2$s 已換入 %3$s。下次啟動遊戲時生效。 未儲存該原始套件組合 - %2$s 的 %1$s 會在其上執行的遊戲關閉後還原為原始版本。 - 已從 %3$s 的原始版本還原 %2$s 的 %1$s。 - %2$s 正在使用 %1$s。請先為該 Proton 換入其他版本。 未儲存 %1$s 已刪除 %1$s。 已安裝組建的原始版本會保留,以便還原。 未儲存 - 已刪除 %2$s 來自 %1$s 的原始版本。 只會從 Nightlies 發行版本下載 @@ -1902,7 +1878,6 @@ 下載回應 HTTP %1$d 此驅動程式清單早於校驗碼機制 - 請重新整理後再試一次 校驗碼不符 - 已捨棄下載內容 - 不是 AdrenoTools 驅動程式:%1$s 沒有 meta.json @@ -1910,7 +1885,6 @@ 這是 Linux 執行環境驅動程式(請在「執行環境驅動程式」下匯入) 這是 Wayland 遊戲驅動程式,此應用程式用不到 meta.json 未指定 libraryName,因此無法交給 AdrenoTools - meta.json 指定了 %1$s,但 zip 中沒有此檔案 %1$s 不是 64 位元 AArch64 ELF 共用程式庫 @@ -1924,22 +1898,19 @@ SD 卡 - 資料夾 - 分享工作階段日誌 + 分享工作階段日誌 需要 Android 10 或更新版本(具備色彩空間的顯示圖層) 此顯示器未回報支援 HDR - 此顯示器支援 %1$s,但不支援 HDR10 - 此 GPU 子處理程序限制已開啟 @@ -1947,7 +1918,6 @@ 無法檢查子處理程序限制 子處理程序限制已關閉 不需要變更子處理程序限制 - %1$s 如果無法這麼做,請將此裝置連接到裝有 ADB 的電腦並執行下方指令,然後返回這裡重新檢查。 可以啟動 Steam 工作階段。 @@ -1955,81 +1925,60 @@ 在開發人員選項中開啟「停用子處理程序限制」,之後請保持開發人員選項開啟。 此 Android 版本沒有這個開關。請使用無線偵錯,直接在此裝置上變更。 此組建版本沒有適用於此 DroidDeck 複本的下載檔案。 - Android 不會以此版本覆蓋目前的組建,因為它的 versionCode %1$d 低於已安裝的 %2$d。 Android 不會以舊版本覆蓋新版本。請先留在此組建版本,等更新頻道趕上。 此 DroidDeck 複本的簽章不同(是本機組建的嗎?),因此 Android 無法直接覆寫更新。 可用空間不足,無法更新。 已取消更新。 - 未能安裝更新:%1$s 未能安裝更新。 - %1$s 壓縮檔 - 壓縮檔 - %1$s 尚未啟動過 找不到 %1$s 的存檔 - %1$s 中沒有 drive_c 資料夾,因此不是 GameHub 或 Winlator 存檔壓縮檔 - 無法在 %1$s 中建立資料夾 無法寫入 %1$s - 無法寫入 %1$s(%2$s) 無法使用資料夾 - 已有其他檔案使用 %1$s 無法使用圖片資料夾 資料夾中有不相關的同步清單 - 無法移除 %1$s 檔案位於匯出資料夾之外 - FEX(未標示版本名稱的組建) - %1$s 中沒有 profile.json - %1$s 不是 Linux 元件套件(類型 %2$s) - %1$s · 可用 %2$s - 自動 - 更新目錄未通過檢查(%1$s) - DroidDeck 子處理程序設定 - 已登入 · 離線模式 已登入 - 內建 已下載 已匯入 - ANDROID + LINUX - %1$s。%2$s\n\n%3$s 工作階段顯示 @@ -2048,6 +1997,7 @@ 影格率 %1$s 將縮放設為 %2$s。 符合螢幕(%1$d×%2$d) + 跟隨螢幕(摺疊或展開時調整大小) Steam 顯示裝置詳細資料 隱藏裝置詳細資料 @@ -2057,6 +2007,186 @@ 已複製。 強制 SSBS 讓一項 Wine 會不斷關閉的 CPU 速度設定保持開啟。在 Snapdragon 8 Elite 上可能讓 Windows 遊戲更快;在較舊的晶片上變化不大。下次啟動遊戲時生效。 + + 遊戲商店 + Steam + 自訂 + 遊戲商店 + 開啟商店時顯示 + 下載速度等級 + 商店引擎 + ● 引擎已就緒 + 尚未組建 + 平衡 + 快速 + 最高 + 遊戲商店 + 遊戲商店設定 + 設定 + (未登入) + 商店 + 已安裝 + 收藏庫 + 全部 + 下載 + %1$s(%2$d) + 登入 + %1$s · 登出 + 要登出 %1$s 嗎? + 已安裝的遊戲會保留。重新登入時會再次取得收藏庫。 + 保持登入 + 登出 + 搜尋 %1$s 目錄… + 搜尋已安裝的遊戲… + 搜尋你的收藏庫… + 搜尋 + 尚未安裝任何內容 + 正在載入 + 沒有遊戲 + 正在載入商店… + 沒有目錄 + 最新上架 + 特惠 + 免費遊戲 + 熱門趨勢 + 你的收藏庫 + 開始遊戲 + 安裝 + 繼續安裝 + 已中斷 + 安裝 · %1$s + 免費取得 + 在 %1$s 檢視 + 購買 · %1$s + 已安裝 + 已在收藏庫中 + 免費 + 繼續 + 重新整理 + 顯示成人內容 + Epic + EOS + 離線 + 關閉 + Epic 登入(EOS) + 離線啟動 + 解決 Epic 登入問題 + 雲端存檔 + 上傳到雲端 + 從雲端下載 + 沒有雲端存檔 + 雲端衝突 + 雲端衝突 · %1$d 個檔案 + … + 保留雲端版本 + 保留本機版本 + 正在檢查雲端… + 上次同步:%1$s + 尚未同步 + %1$d 個檔案 + 已安裝 + 在你的收藏庫中 + 商店 + 成就 + 已解鎖 %1$d / %2$d + %1$d / %2$d + 已解鎖 · %1$d + 未解鎖 · %1$d + 隱藏 · %1$d + 隱藏成就 + 解鎖後會顯示詳細資訊。 + 已解鎖所有成就 + 正在讀取… + 登入 %1$s 以查看你的成就。 + 無法連線到 %1$s。請在連上網路後再查看。 + %1$s 沒有列出這款遊戲的成就。 + 顯示全部 %1$d 項 + 成就 · %1$s + ✓ 已在 Steam 中 + 解除安裝 + 正在解除安裝… + 要解除安裝 %1$s 嗎? + 將釋放 %1$s,並從 Steam 中移除。 + 將移除其檔案和 Steam 捷徑。 + Steam 下次啟動時會加入這款遊戲;請從 Steam 啟動,或立即啟動 Steam。 + Proton + 由 Steam 依遊戲選擇 + 音訊 + DirectAudio + 元件 + FEX、DXVK、VKD3D + 引擎 + %1$s · 原生 + %1$s · 內建 + 執行方式 + 取得來源 + %1$s,使用你的帳號 + 安裝位置 + 遊戲儲存空間(Games/%1$s/%2$s) + 啟動方式 + 透過 Steam 和 Proton,與 Steam 遊戲的方式相同 + Steam 和 Proton · 每次啟動時登入 Epic,EOS 重疊層關閉 + Steam 和 Proton · 已設定 Amazon Games Services 變數 + Steam + 安裝時加入 Steam(Deck 介面 + Steam Input) + 沒有下載項目 + 已暫停 + 排隊中 + 排隊中 · #%1$d + 已取消 + 失敗 + 失敗 · %1$s + %1$s · %2$d%% + %1$s / %2$s · %3$d%% + 暫停 + 繼續 + 取消 + 重試 + 清除 + 清除已完成項目 + 資訊清單 + 下載 + 驗證 + 安裝 + 完成 + 正在檢查 + 正在下載 + 要刪除下載內容嗎? + 失敗 · 已保留 %1$s + 正在驗證 + 正在安裝 + %1$s %2$d%% + %1$s · %2$s + %1$s / %2$s + %1$d/%2$d 個檔案 + 下載管理員 + 同時下載數 + 安裝到 + 安裝 %1$s: + 內部 + 可用 %1$s + 商店下載 + 顯示遊戲下載進度,並讓下載在背景持續進行 + 正在下載 + %1$d 個下載項目 + 正在準備… + 已完成的商店下載 + 已安裝 %1$s + %1$s 下載失敗 + 刪除 + 保留 + 登入 %1$s + 透過你的帳號提供者登入 + 重新載入 + 關閉 + 再試一次 + 正在完成登入… + 無法載入登入頁面:%1$s + 此提供者拒絕從內嵌頁面登入。請改用商店密碼登入。 + 登入頁面回應錯誤(HTTP %1$d)。 + 與登入頁面的連線不安全。 + 無法驗證登入。請再試一次。 + 登入失敗。請再試一次。 關閉 返回 編輯 @@ -2088,21 +2218,122 @@ %1$d 個已在遊戲中 - 成就 - 已解鎖 %1$d / %2$d - %1$d / %2$d - 已解鎖 · %1$d - 未解鎖 · %1$d - 隱藏 · %1$d - 隱藏成就 - 解鎖後會顯示詳細資訊。 - 已解鎖所有成就 - 正在讀取… - 登入 %1$s 以查看你的成就。 - 無法連線到 %1$s。請在連上網路後再查看。 - %1$s 沒有列出這款遊戲的成就。 - 顯示全部 %1$d 項 - 成就 · %1$s + B · 返回%1$s + 遊戲 + 執行設定 + 圖形驅動程式 + 此 Proton + 所有 Proton + 適用於所有 Proton + 可用 · %1$d + 已檢查 %1$s + 檢查新元件和驅動程式 + 取得 + %1$s · 下方的 DXVK、VKD3D-Proton 和 FEX 屬於此 Proton + %1$s · 已測試 + %1$s · 未測試 + %1$s · 不支援 + 讓建議的驅動程式組合保持最新 + 自動 · %1$s + 分別設定執行環境驅動程式和顯示驅動程式 + 要使用 %1$s 嗎? + 要移除 %1$s 嗎? + 診斷 + 已可開始遊戲 + 尚未準備好開始遊戲 + 執行環境 %1$s + 處理程序限制已關閉 + 已登入為 %1$s + 需要你處理的項目 + 外觀 + 這部裝置 + 收藏庫圖片 + LINUX 應用程式 + Flathub + 適用於 Linux 桌面的應用程式,從「桌面」開啟。 + 設定 Flathub + 要移除 %1$s 嗎? + 其設定和檔案會保留。 + 保留 + 佔用空間 + 要解除安裝桌面嗎? + 全部 + GOG 成就 + 為支援的 GOG 遊戲同步成就。下次啟動時生效。 + 成就已關閉 + 成就已開啟 + 重新登入 GOG + 成就已上傳 + 成就上傳待處理 + 在「商店」設定中開啟 GOG 成就 + 成就無法使用;請查看工作階段記錄 + 成就已解鎖 + 貼上 + 方向鍵 + 左搖桿 + 右搖桿 + 選單 + 鍵盤 + 方向鍵上 + 方向鍵下 + 方向鍵左 + 方向鍵右 + 觸控操作 + 觸控控制器 + 滑鼠 + 開 + 關 + 慢 + 正常 + 快 + 編輯版面配置 + DroidDeck 選單 + 將控制項拖曳到畫面上 + 排列 + 放棄 + 儲存中… + 圖示 + 寬− + 寬+ + 高− + 高+ + 隱藏 + 顏色與不透明度… + 複製這個版面配置 + 版面配置已複製,可貼到任何遊戲的觸控版面配置中 + 貼上複製的版面配置 + 還原為 Steam 的預設值 + 這個控制項在 Steam 中沒有綁定,無法設定圖示 + %1$s 的圖示 + 觸控版面配置顏色 + 完成 + 無法儲存觸控版面配置 + 觸控版面配置已儲存至 Steam + 應用程式 %1$d + 標籤(選用) + 找不到 Steam 的圖示 + + 來自 Steam 的 %1$d 個圖示 + + 圖示顏色 + 按鈕顏色 + 沒有圖示 + + 正在檢查登入狀態… + 正在取得你的遊戲庫… + 正在取得遊戲清單… + 正在載入遊戲詳細資料…(%1$d/%2$d) + + 正在同步 %1$d 個遊戲… + + 正在同步… %1$d/%2$d + 正在取得封面圖片… + 同步失敗 + 在你的 %1$s 遊戲庫中找不到遊戲 + Amazon Games 未回應你的遊戲庫要求:請重新整理,或重新登入 + 無法連線到 GOG + 無法讀取你的 GOG 遊戲庫 + 正在向 Steam 註冊… 開發商 發行商 發行日期 @@ -2111,4 +2342,34 @@ 全收集 %1$s 小時 %1$d 分鐘 + Cardboard VR + Cardboard 模式 + 為手機 VR 眼鏡提供左右眼畫面。 + 畫面 + 虛擬螢幕 + 左右並排 3D + 眼鏡 + Cardboard + Cardboard(2014) + 無鏡片 + 瞳距 + 螢幕寬度 + 螢幕距離 + 頭部追蹤 + 這部裝置沒有旋轉感測器。 + 重設視角 + 重設 + SteamVR(實驗性) + 安裝 + 下載 Valve 的 ARM64 版 SteamVR(571 MB)。 + 正在下載並解壓縮 SteamVR… + 啟動 + 以你的頭部作為頭戴裝置執行 SteamVR;畫面會切換為左右並排 3D。 + 停止 + SteamVR 正在執行。 + 這個工作階段無法使用。 + SteamVR 沒有回應;請查看工作階段記錄。 + 下載元件 + 下載內容與「元件」頁面共用。在上方選擇已下載的版本,即可在下次啟動遊戲時使用。 + 選擇要下載的版本 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3c192317e..f283c4500 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2360,4 +2360,72 @@ Enable GOG achievements in Stores settings Achievements unavailable; check session logs Achievement unlocked + Paste + D-pad + Left stick + Right stick + Menu + Keyboard + D-pad up + D-pad down + D-pad left + D-pad right + Touch controls + Touch Controller + Mouse + On + Off + Slow + Normal + Fast + Edit layout + DroidDeck menu + Drag controls onto the screen + Arrange + Discard + Saving… + Icon + W− + W+ + H− + H+ + Hide + Color and opacity… + Copy this layout + Layout copied; paste it into any game\'s touch layout + Paste copied layout + Reset to Steam\'s default + This control has no binding in Steam to put an icon on + Icon for %1$s + Touch layout color + Done + Could not save the touch layout + Touch layout saved to Steam + App %1$d + Label (optional) + Steam\'s icons were not found + + %1$d icon from Steam + %1$d icons from Steam + + Icon color + Button color + No icon + + Checking sign-in… + Fetching your library… + Fetching game list… + Loading game details… (%1$d/%2$d) + + Syncing %1$d game… + Syncing %1$d games… + + Syncing… %1$d/%2$d + Fetching cover art… + Sync failed + No games found in your %1$s library + Amazon Games did not answer for your library: refresh, or sign in again + Couldn\'t reach GOG + Could not read your GOG library + Registering with Steam… diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 86bafa9b1..62fd269ce 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -3,9 +3,33 @@ AppLanguage.supported and the values-* folders. --> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/com/droiddeck/launcher/core/AppLanguageTest.kt b/app/src/test/java/com/droiddeck/launcher/core/AppLanguageTest.kt new file mode 100644 index 000000000..552e571fb --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/core/AppLanguageTest.kt @@ -0,0 +1,57 @@ +package com.droiddeck.launcher.core + +import java.io.File +import java.util.Locale +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class AppLanguageTest { + private fun tag(languageTag: String) = AppLanguage.supportedTag(Locale.forLanguageTag(languageTag)) + + private val folders = mapOf( + "id" to "in", "ms" to "ms", "cs" to "cs", "da" to "da", "de" to "de", "es" to "es", + "es-419" to "b+es+419", "fr" to "fr", "it" to "it", "hu" to "hu", "nl" to "nl", "nb" to "nb", + "pl" to "pl", "pt-BR" to "pt", "pt-PT" to "pt-rPT", "ro" to "ro", "fi" to "fi", "sv" to "sv", + "vi" to "vi", "tr" to "tr", "el" to "el", "bg" to "bg", "ru" to "ru", "uk" to "uk", "ar" to "ar", + "hi" to "hi", "th" to "th", "ja" to "ja", "ko" to "ko", "zh-CN" to "zh-rCN", "zh-TW" to "zh-rTW", + "zh-HK" to "zh-rHK", + ) + + @Test fun everyLanguageReadsAsItself() { + AppLanguage.supported.forEach { assertEquals(it, tag(it)) } + } + + @Test fun everyLanguageHasItsOwnName() { + val names = AppLanguage.supported.map(AppLanguage::nativeName) + assertEquals(names.size, names.toSet().size) + AppLanguage.supported.forEach { assertTrue(it, AppLanguage.nativeName(it) != it) } + } + + @Test fun systemLocalesFindTheirLanguage() { + assertEquals("es-419", tag("es-MX")) + assertEquals("es-419", tag("es-US")) + assertEquals("es", tag("es-ES")) + assertEquals("pt-BR", tag("pt-BR")) + assertEquals("pt-BR", tag("pt")) + assertEquals("pt-PT", tag("pt-PT")) + assertEquals("pt-PT", tag("pt-AO")) + assertEquals("id", tag("in-ID")) + assertEquals("id", tag("id-ID")) + assertEquals("de", tag("de-AT")) + assertEquals("ar", tag("ar-EG")) + assertEquals("zh-HK", tag("zh-MO")) + assertNull(tag("sw-KE")) + assertNull(tag("nn-NO")) + } + + @Test fun everyLanguageShipsStringsAndIsOfferedToAndroid() { + assertEquals(AppLanguage.supported.toSet() - "en", folders.keys) + val res = File("src/main/res") + folders.values.forEach { assertTrue(it, File(res, "values-$it/strings.xml").isFile) } + val config = File(res, "xml/locales_config.xml").readText() + val offered = Regex("""android:name="([^"]+)"""").findAll(config).map { it.groupValues[1] }.toList() + assertEquals(AppLanguage.supported, offered) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt index 4e4e30447..8046e7794 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt @@ -18,6 +18,18 @@ class SteamLanguageTest { assertEquals("tchinese", steam("zh-HK")) } + @Test fun everyAppLanguageButHindiHasSteamsOwn() { + com.droiddeck.launcher.core.AppLanguage.supported.forEach { + val steam = steam(it) + assertTrue("$it -> $steam", (steam == "english") == (it == "en" || it == "hi")) + } + assertEquals("brazilian", steam("pt-BR")) + assertEquals("portuguese", steam("pt-PT")) + assertEquals("latam", steam("es-419")) + assertEquals("indonesian", steam("id")) + assertEquals("malay", steam("ms")) + } + @Test fun chineseFollowsTheScriptBeforeTheRegion() { assertEquals("tchinese", steam("zh-Hant-CN")) assertEquals("schinese", steam("zh-Hans-HK")) From 4562d611395a7b7ab12d687df2021e50f2274777 Mon Sep 17 00:00:00 2001 From: 0x1Nawaf <73679429+0x1Nawaf@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:36:31 +0300 Subject: [PATCH 04/14] Feat/reset all settings (#696) --- .../com/droiddeck/launcher/MainActivity.kt | 25 +++++ .../launcher/session/SettingsReset.kt | 85 ++++++++++++++++ .../droiddeck/launcher/ui/FrontEndScreen.kt | 2 + .../droiddeck/launcher/ui/FrontEndSetup.kt | 2 + .../com/droiddeck/launcher/ui/MainScreen.kt | 41 ++++++++ app/src/main/res/values-es/strings.xml | 8 ++ app/src/main/res/values-fr/strings.xml | 8 ++ app/src/main/res/values-ja/strings.xml | 8 ++ app/src/main/res/values-ko/strings.xml | 8 ++ app/src/main/res/values-ru/strings.xml | 8 ++ app/src/main/res/values-zh-rCN/strings.xml | 8 ++ app/src/main/res/values-zh-rHK/strings.xml | 8 ++ app/src/main/res/values-zh-rTW/strings.xml | 8 ++ app/src/main/res/values/strings.xml | 8 ++ .../launcher/session/SettingsResetTest.kt | 96 +++++++++++++++++++ 15 files changed, 323 insertions(+) create mode 100644 app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/session/SettingsResetTest.kt diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index 6e447339d..bd7577a78 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -116,6 +116,7 @@ class MainActivity : ComponentActivity() { private var frameGenLabel by mutableStateOf("") private var lossless by mutableStateOf(Lossless.State.NONE) private var showRemove by mutableStateOf(false) + private var showResetSettings by mutableStateOf(false) private var showNonAdreno by mutableStateOf(null) private var glThread by mutableStateOf(true) private var noGlError by mutableStateOf(true) @@ -727,6 +728,10 @@ class MainActivity : ComponentActivity() { } }, "clear-logs").start() }, + onResetSettings = { + if (SessionState.running) android.widget.Toast.makeText(this, R.string.setup_reset_settings_session_running, android.widget.Toast.LENGTH_LONG).show() + else showResetSettings = true + }, onOffline = { OfflineMode.setEnabled(this, !OfflineMode.enabled(this)) offline = OfflineMode.enabled(this) @@ -872,6 +877,10 @@ class MainActivity : ComponentActivity() { onConfirm = { showRemove = false; removeRuntime() }, onDismiss = { showRemove = false }, ) + if (showResetSettings) com.droiddeck.launcher.ui.ResetSettingsDialog( + onConfirm = { gameProfiles -> resetSettings(gameProfiles) }, + onDismiss = { showResetSettings = false }, + ) flood?.let { f -> com.droiddeck.launcher.ui.LaunchFlood(f.from, onProgress = { floodProgress = it }) { launchFlooded(f) } } returning?.let { r -> com.droiddeck.launcher.ui.FloodReturn(androidx.compose.ui.graphics.Color(r.color), r.to, onProgress = { floodProgress = it }) { @@ -1750,6 +1759,22 @@ class MainActivity : ComponentActivity() { } } + /** Setup's Reset all settings; the activity is recreated so every screen reads the defaults (and the language) again. */ + private fun resetSettings(gameProfiles: Boolean) { + if (SessionState.running) return + val scopes = steamGames.map { it.profileKey } + Thread({ + val ok = com.droiddeck.launcher.session.SettingsReset.resetAll(this, gameProfiles, scopes) + ui.post { + if (isDestroyed) return@post + com.droiddeck.launcher.core.AppLanguage.setChosen(this, com.droiddeck.launcher.core.AppLanguage.SYSTEM) + if (!ok) android.widget.Toast.makeText(this, R.string.setup_reset_settings_partial, android.widget.Toast.LENGTH_LONG).show() + com.droiddeck.launcher.ui.Motion.refresh(this) + recreate() + } + }, "reset-settings").start() + } + private fun removeRuntime() { if (runtimeChangesBlocked()) return val removal = LinuxRuntimeInstaller.beginUninstall(this) ?: return diff --git a/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt b/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt new file mode 100644 index 000000000..160c0b3a3 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt @@ -0,0 +1,85 @@ +package com.droiddeck.launcher.session + +import android.content.Context +import android.util.Log +import com.droiddeck.launcher.core.GameEnvironment +import com.droiddeck.launcher.input.ControllerPrefs +import com.droiddeck.launcher.runtime.LinuxRuntime + +/** + * Setup's "Reset all settings": every choice goes back to the default its getter falls back to. + * + * Only settings are removed. The "session" file also holds the library (added games' names, exes, + * appids and art picks), migration records, the drivers Auto installed, the storage and ROMs + * folders and the one-time permission prompts; those stay, as do store sign-ins, imported drivers + * and Lossless Scaling. Game profiles (each game's and the Default profile's environment + * overrides) are cleared only when asked for. + */ +object SettingsReset { + private const val TAG = "SettingsReset" + + internal val SESSION_KEYS = setOf( + "orientation", "launcherFullscreen", "animations", "theme", "runSteamAtStartup", "addedGamesArt", + "gameStoresEnabled", "gameStoresSpeedTier", "gameStoresParallel", "storesOpenTab", "storesShowMature", + "hud", "hudDeck", "backActionsInverted", "touch", "osc", "shape", "steamController", + "gamepadLeft", "gamepadRight", "pipAutoEnter", "unfoldedControls", "stretch16x9", + "gpuDriverMode", "androidDriver", "linuxDriver", "linuxDriver.steam", "linuxDriver.desktop", + "tuSysmem", "glThread", "noGlError", "zinkLazy", "mangoapp", "gpuClockPin", "gamescopeRealtime", + "directAudio", "mic", "wifiDiscovery", "clientCpusOverride", "clientCpus", "gameCpus", + "noXalia", "fastSync", "fsyncFirst", "syncFallback", "prootNoSeccomp", "prootFastPath", + "guestHostname", "fexPreset", "forceSsbs", "logs", "storageDiagnostics", + "upscaler", "upscaleSharpness", "screenEffects", "textureAnisotropy", "textureLodBias", + "steamDownloadsInBackground", "steam_offline", + // Cleared so the Quest's one-time compatibility defaults are written again. + "questDefaultsV1", + ) + + /** Per-mode settings, stored as "". */ + internal val SESSION_PREFIXES = listOf( + "resolutionCap.", "displayResolution.", "customRes.", "hdr.", "fpsLimit.", "suspendPolicy.", + ) + + private val STEAM_TOUCH_KEYS = listOf("feedback", "autoFade", "gyro", "shake") + + internal fun isSessionSetting(key: String) = key in SESSION_KEYS || SESSION_PREFIXES.any { key.startsWith(it) } + + /** + * Resets the settings, then per-game profiles when [gameProfiles] is set ([gameScopes] are the + * library's profile keys; scopes with saved overrides are found on their own). Blocking: call + * off the main thread. Returns false if a part could not be reset; the rest is still done. + * The language is not reset here: AppLanguage.setChosen changes the process locale, so the + * caller does that on the main thread. + */ + fun resetAll(context: Context, gameProfiles: Boolean, gameScopes: Collection = emptyList()): Boolean { + var ok = true + val session = context.getSharedPreferences("session", Context.MODE_PRIVATE) + session.edit().apply { session.all.keys.filter { isSessionSetting(it) }.forEach { remove(it) } }.commit() + ControllerPrefs.resetAll(context) + context.getSharedPreferences("app-ui", Context.MODE_PRIVATE).edit().clear().commit() + context.getSharedPreferences("frame_gen", Context.MODE_PRIVATE).edit().clear().commit() + context.getSharedPreferences("steam_touch", Context.MODE_PRIVATE).edit() + .apply { STEAM_TOUCH_KEYS.forEach { remove(it) } }.commit() + com.droiddeck.launcher.agent.AgentAccess.setCommandsEnabled(context, false) + + if (gameProfiles) { + val scopes = (gameScopes + runCatching { GameEnvironmentStore.read(context).games.keys }.getOrDefault(emptySet()) + + SessionPrefs.gameTextureFiltering(context).keys) + .filter { it.isNotEmpty() && GameEnvironment.validScope(it) }.toSet() + scopes.forEach { scope -> + runCatching { GameProfileManager.reset(context, scope) } + .onFailure { ok = false; Log.w(TAG, "Could not reset the profile of $scope", it) } + } + session.edit().remove("gameTextureFiltering").commit() + runCatching { GameEnvironmentStore.save(context, GameEnvironmentStore.read(context).copy(shared = emptyMap())) } + .onFailure { ok = false; Log.w(TAG, "Could not clear the Default profile's environment", it) } + } + + QuestExperimental.initialize(context) + runCatching { OfflineMode.apply(context, LinuxRuntime.rootDir(context)) } + .onFailure { ok = false; Log.w(TAG, "Could not turn offline mode off", it) } + runCatching { GameEnvironmentStore.publish(context) } + .onFailure { ok = false; Log.w(TAG, "Could not update game environment", it) } + Log.i(TAG, "settings reset (game profiles: $gameProfiles, ok: $ok)") + return ok + } +} diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt index 5845f8549..79486e5d8 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -205,6 +205,8 @@ class FrontEndActions( val onAgentCommands: () -> Unit = {}, val onShareLogs: () -> Unit = {}, val onClearLogs: () -> Unit = {}, + /** Setup's Reset all settings: asks first, then puts every setting back to its default. */ + val onResetSettings: () -> Unit = {}, val onOffline: () -> Unit, val onPageBack: () -> Unit = {}, val onTheme: (String) -> Unit = {}, 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 5e82d9633..27e045019 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -405,6 +405,8 @@ internal fun SetupPanel( ActionRow(stringResource(R.string.setup_latest_logs), stringResource(R.string.drawer_logs_hint), stringResource(R.string.drawer_share_logs), a.onShareLogs, progress = com.droiddeck.launcher.session.SessionLogShare.progress) ActionRow(stringResource(R.string.setup_saved_logs), stringResource(R.string.setup_saved_logs_hint, com.droiddeck.launcher.session.SessionPaths.KEEP_SESSIONS), stringResource(R.string.setup_clear_logs), a.onClearLogs) + ActionRow(stringResource(R.string.setup_reset_settings), stringResource(R.string.setup_reset_settings_hint), + stringResource(R.string.setup_reset_settings_button), a.onResetSettings) } } } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt index 3d97ca656..f51d7cb14 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt @@ -2,17 +2,29 @@ package com.droiddeck.launcher.ui import com.droiddeck.launcher.R import androidx.compose.ui.res.stringResource +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import com.droiddeck.launcher.HomeApp import com.droiddeck.launcher.input.SecondScreenDisplay @@ -53,3 +65,32 @@ fun ConfirmDialog(title: String, text: String, confirm: String, onConfirm: () -> dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, ) } + +/** Setup's Reset all settings, with the choice to clear game profiles too (off unless ticked). */ +@Composable +fun ResetSettingsDialog(onConfirm: (gameProfiles: Boolean) -> Unit, onDismiss: () -> Unit) { + var gameProfiles by remember { mutableStateOf(false) } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.setup_reset_settings_title)) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(stringResource(R.string.setup_reset_settings_body)) + Row( + Modifier.fillMaxWidth().toggleable(gameProfiles, role = Role.Checkbox) { gameProfiles = it }, + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox(checked = gameProfiles, onCheckedChange = null) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.setup_reset_settings_profiles), style = MaterialTheme.typography.bodyMedium) + } + } + }, + confirmButton = { + TextButton(onClick = { onDismiss(); onConfirm(gameProfiles) }) { + Text(stringResource(R.string.setup_reset_settings_button), color = MaterialTheme.colorScheme.error) + } + }, + dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, + ) +} diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index a259331d0..5f2842f42 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -686,6 +686,14 @@ Registros de sesión guardados Conserva los %1$d más recientes Borrar registros + Restablecer todos los ajustes + Devuelve cada ajuste a su valor predeterminado + Restablecer + ¿Restablecer todos los ajustes? + Los ajustes de pantalla, rendimiento, mando, tema, idioma y lanzador vuelven a sus valores predeterminados. Se conservan tus juegos, la sesión de Steam, las cuentas de tiendas, los controladores descargados y las carpetas de almacenamiento. + Restablecer también los perfiles de juego (Proton, FEX, componentes y entorno de cada juego, y el entorno del perfil predeterminado) + Detén la sesión antes de restablecer los ajustes. + Los ajustes se restablecieron, pero algunas partes no se pudieron restablecer. Los registros tienen los detalles. Modo sin conexión Inicia sesión en Steam primero Launcher diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 52967b528..c49e0662c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -686,6 +686,14 @@ Journaux de session enregistrés Conserver les %1$d plus récents Effacer les journaux + Réinitialiser tous les paramètres + Remet chaque paramètre à sa valeur par défaut + Réinitialiser + Réinitialiser tous les paramètres ? + Les paramètres d\'affichage, de performances, de manette, de thème, de langue et du lanceur reprennent leurs valeurs par défaut. Vos jeux, votre connexion Steam, vos comptes de boutiques, les pilotes téléchargés et les dossiers de stockage sont conservés. + Réinitialiser aussi les profils de jeu (Proton, FEX, composants et environnement de chaque jeu, ainsi que l\'environnement du profil par défaut) + Arrêtez la session avant de réinitialiser les paramètres. + Les paramètres ont été réinitialisés, mais certaines parties n\'ont pas pu l\'être. Les journaux contiennent les détails. Mode hors ligne Veuillez d\'abord vous connecter à Steam Lanceur diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 8c4edf41a..1b9c0d2dd 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -680,6 +680,14 @@ 保存されたセッションログ 最新の%1$d件を保持 ログを消去 + すべての設定をリセット + すべての設定をデフォルトに戻します + リセット + すべての設定をリセットしますか? + 画面、パフォーマンス、コントローラー、テーマ、言語、ランチャーの設定がデフォルトに戻ります。ゲーム、Steamのサインイン、ストアのアカウント、ダウンロードしたドライバー、ストレージフォルダーは保持されます。 + ゲームプロファイルもリセットする(各ゲームのProton、FEX、コンポーネント、環境、およびデフォルトプロファイルの環境) + 設定をリセットする前にセッションを停止してください。 + 設定はリセットされましたが、一部をリセットできませんでした。詳細はログを確認してください。 オフラインモード 先にSteamにログインしてください ランチャー diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index fa9282a09..f0672279d 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -680,6 +680,14 @@ 저장된 세션 로그 최신 %1$d개 유지 로그 지우기 + 모든 설정 초기화 + 모든 설정을 기본값으로 되돌립니다 + 초기화 + 모든 설정을 초기화할까요? + 화면, 성능, 컨트롤러, 테마, 언어 및 런처 설정이 기본값으로 돌아갑니다. 게임, Steam 로그인, 스토어 계정, 다운로드한 드라이버 및 저장소 폴더는 유지됩니다. + 게임 프로필도 초기화 (각 게임의 Proton, FEX, 구성 요소 및 환경, 기본 프로필의 환경) + 설정을 초기화하기 전에 세션을 중지하세요. + 설정이 초기화되었지만 일부는 초기화하지 못했습니다. 자세한 내용은 로그를 확인하세요. 오프라인 모드 먼저 Steam에 로그인하세요 런처 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 868f82fc6..59dcb1741 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -689,6 +689,14 @@ Сохраненные журналы сессии Хранятся последние %1$d Очистить журналы + Сбросить все настройки + Возвращает все настройки к значениям по умолчанию + Сбросить + Сбросить все настройки? + Настройки экрана, производительности, контроллера, темы, языка и лаунчера вернутся к значениям по умолчанию. Ваши игры, вход в Steam, аккаунты магазинов, загруженные драйверы и папки хранилища сохранятся. + Также сбросить профили игр (Proton, FEX, компоненты и окружение каждой игры, а также окружение профиля по умолчанию) + Остановите сессию перед сбросом настроек. + Настройки сброшены, но некоторые части сбросить не удалось. Подробности в журналах. Автономный режим Сначала войдите в Steam Лаунчер diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b1852e70d..f93e48b38 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -680,6 +680,14 @@ 已保存的会话日志 保留最新的 %1$d 个 清除日志 + 重置所有设置 + 将所有设置恢复为默认值 + 重置 + 重置所有设置? + 显示、性能、手柄、主题、语言和启动器设置将恢复为默认值。你的游戏、Steam 登录、商店账户、已下载的驱动和存储文件夹会被保留。 + 同时重置游戏配置档案(每个游戏的 Proton、FEX、组件和环境,以及默认配置档案的环境) + 请先停止会话再重置设置。 + 设置已重置,但部分内容未能重置。详情请查看日志。 离线模式 请先登录 Steam 启动器 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 027e05b42..226b0f500 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -680,6 +680,14 @@ 已儲存的會話記錄 保留最新的 %1$d 個 清除記錄 + 重設所有設定 + 將所有設定還原為預設值 + 重設 + 要重設所有設定嗎? + 顯示、效能、控制器、主題、語言和啟動器設定將還原為預設值。你的遊戲、Steam 登入、商店帳戶、已下載的驅動程式和儲存資料夾都會保留。 + 同時重設遊戲設定檔(每個遊戲的 Proton、FEX、元件和環境,以及預設設定檔的環境) + 請先停止會話再重設設定。 + 設定已重設,但部分項目未能重設。詳情請查看記錄。 離線模式 請先登入 Steam 啟動器 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 9957bd3e1..e5fc5d76a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -680,6 +680,14 @@ 已儲存的工作階段日誌 保留最新的 %1$d 個 清除日誌 + 重設所有設定 + 將所有設定還原為預設值 + 重設 + 要重設所有設定嗎? + 顯示、效能、控制器、佈景主題、語言和啟動器設定將還原為預設值。你的遊戲、Steam 登入、商店帳戶、已下載的驅動程式和儲存資料夾都會保留。 + 同時重設遊戲設定檔(每個遊戲的 Proton、FEX、元件和環境,以及預設設定檔的環境) + 請先停止工作階段再重設設定。 + 設定已重設,但部分項目無法重設。詳細資訊請查看日誌。 離線模式 請先登入 Steam 啟動器 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f283c4500..3415cc58d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -716,6 +716,14 @@ Saved session logs Keeps the newest %1$d Clear logs + Reset all settings + Puts every setting back to its default + Reset + Reset all settings? + Display, performance, controller, theme, language and launcher settings go back to their defaults. Your games, Steam sign-in, store accounts, downloaded drivers and storage folders are kept. + Also reset game profiles (each game\'s Proton, FEX, components and environment, and the Default profile\'s environment) + Stop the session before resetting settings. + Settings were reset, but some parts could not be. The logs have the details. Offline mode Sign in to Steam first Launcher diff --git a/app/src/test/java/com/droiddeck/launcher/session/SettingsResetTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SettingsResetTest.kt new file mode 100644 index 000000000..e636bee70 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/SettingsResetTest.kt @@ -0,0 +1,96 @@ +package com.droiddeck.launcher.session + +import android.content.Context +import com.droiddeck.launcher.core.AppUiPrefs +import com.droiddeck.launcher.gpu.FrameGen +import com.droiddeck.launcher.input.ControllerPrefs +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class SettingsResetTest { + private val app get() = RuntimeEnvironment.getApplication() + private fun prefs(name: String) = app.getSharedPreferences(name, Context.MODE_PRIVATE) + private val session get() = prefs("session") + + @Before fun clear() { + listOf("session", "controller", "app-ui", "frame_gen", "steam_touch", "agent").forEach { + prefs(it).edit().clear().commit() + } + } + + @Test fun settingsGoBackToTheirDefaults() { + SessionPrefs.setTheme(app, "ocean") + SessionPrefs.setNoXalia(app, false) + SessionPrefs.setFpsLimit(app, SessionService.MODE_STEAM, 30) + SessionPrefs.setHdr(app, SessionService.MODE_DESKTOP, true) + SessionPrefs.setResolutionChoice(app, SessionService.MODE_STEAM, "1280x800") + SessionPrefs.setGuestHostname(app, "MyDeck") + ControllerPrefs.setOpacity(app, 40) + AppUiPrefs.setScale(app, 150) + FrameGen.set(app, FrameGen.Mode(FrameGen.ENGINE_WINFG, 3)) + prefs("steam_touch").edit().putBoolean("gyro", false).commit() + + assertTrue(SettingsReset.resetAll(app, gameProfiles = false)) + + assertEquals("graphite", SessionPrefs.theme(app)) + assertTrue(SessionPrefs.noXalia(app)) + assertEquals(0, SessionPrefs.fpsLimit(app, SessionService.MODE_STEAM)) + assertFalse(SessionPrefs.hdr(app, SessionService.MODE_DESKTOP)) + assertFalse(SessionPrefs.resolutionChosen(app, SessionService.MODE_STEAM)) + assertEquals(SessionPrefs.DEFAULT_GUEST_HOSTNAME, SessionPrefs.guestHostname(app)) + assertEquals(100, ControllerPrefs.read(app).opacity) + assertEquals(AppUiPrefs.DEFAULT_SCALE, AppUiPrefs.scale(app)) + assertEquals(FrameGen.Mode.OFF.engine, FrameGen.engine(app)) + assertFalse(prefs("steam_touch").contains("gyro")) + } + + @Test fun libraryAndRecordsAreKept() { + val folder = "/storage/emulated/0/Games/Foo" + SessionPrefs.setAddedGameName(app, folder, "Foo") + SessionPrefs.setAddedGameExe(app, folder, "$folder/foo.exe") + SessionPrefs.addedGameAppId(app, folder, 1234L) + SessionPrefs.setAddedGameRemoved(app, "/storage/emulated/0/Games/Bar", true) + SessionPrefs.setGamesFoldersMigrated(app) + SessionPrefs.setGpuAutoInstalled(app, setOf("turnip-a")) + SessionPrefs.setRomsDir(app, "/storage/emulated/0/ROMs") + SessionPrefs.setGameStorage(app, "/storage/ABCD-1234", "SD Card") + SessionPrefs.setMicAsked(app) + prefs("steam_touch").edit().putString("copiedLayout", "a:1,2").commit() + + SettingsReset.resetAll(app, gameProfiles = false) + + assertEquals("Foo", SessionPrefs.addedGameName(app, folder)) + assertEquals("$folder/foo.exe", SessionPrefs.addedGameExe(app, folder)) + assertEquals(1234L, SessionPrefs.addedGameStoredAppId(app, folder)) + assertEquals(listOf("/storage/emulated/0/Games/Bar"), SessionPrefs.removedAddedGames(app)) + assertTrue(SessionPrefs.gamesFoldersMigrated(app)) + assertEquals(setOf("turnip-a"), SessionPrefs.gpuAutoInstalled(app)) + assertEquals("/storage/emulated/0/ROMs", SessionPrefs.romsDir(app)) + assertEquals("/storage/ABCD-1234", SessionPrefs.gameStorage(app)) + assertTrue(SessionPrefs.micAsked(app)) + assertEquals("a:1,2", prefs("steam_touch").getString("copiedLayout", null)) + } + + @Test fun gameTextureFilteringIsKeptUnlessGameProfilesAreReset() { + session.edit().putString("gameTextureFiltering", """{"440":{"anisotropy":16}}""").commit() + SettingsReset.resetAll(app, gameProfiles = false) + assertEquals(16, SessionPrefs.gameTextureFiltering(app, "440").anisotropy) + + SettingsReset.resetAll(app, gameProfiles = true) + assertTrue(SessionPrefs.gameTextureFiltering(app).isEmpty()) + } + + @Test fun perModeKeysAreSettings() { + listOf("resolutionCap.steam", "displayResolution.desktop", "customRes.steam", "hdr.steam", "fpsLimit.desktop", "suspendPolicy.steam") + .forEach { assertTrue(it, SettingsReset.isSessionSetting(it)) } + listOf("addedName:/x", "addedAppId:/x", "gpuAutoInstalled", "romsDir", "gameStorage", "micAsked", "gameTextureFiltering") + .forEach { assertFalse(it, SettingsReset.isSessionSetting(it)) } + } +} From a12ac8134a14cd7665890176324298cd6348a7f0 Mon Sep 17 00:00:00 2001 From: Jagger15 Date: Tue, 13 Oct 2026 14:16:20 -0400 Subject: [PATCH 05/14] Add experimental Quest 2 and Adreno 6xx display-driver support (#676) --- .../com/droiddeck/launcher/SessionActivity.kt | 2 + .../launcher/gpu/DriverSetupLog.java | 30 ++++++++ .../droiddeck/launcher/gpu/TurnipDriver.java | 10 ++- .../launcher/session/QuestExperimental.kt | 19 +++-- .../launcher/session/QuestExperimentalTest.kt | 24 +++++- docs/quest.md | 20 ++++- tools/quest/build-driver.py | 73 +++++++++++++++++++ tools/release/runtime_inputs.py | 5 ++ 8 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index a8d9ccd0a..f584f69ba 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -1349,6 +1349,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { // Turnip, not the system Adreno driver: importing the dma-bufs gamescope commits needs // VK_EXT_image_drm_format_modifier, which the system driver does not implement. val turnip = TurnipDriver(this) + com.droiddeck.launcher.gpu.DriverSetupLog.write(this, "surface.driver-setup") val driverId = if (CompositorHost.isStarted) null else turnip.install() // The output size belongs to the session, not to the Surface: gamescope's display is @@ -1417,6 +1418,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { ) Log.i(TAG, "hdr: " + (if (on) "on" else if (wanted) "wanted but ${probe.reason}" else "off") + " · display ${probe.formats.ifEmpty { "SDR" }}") } + com.droiddeck.launcher.gpu.DriverSetupLog.write(this, "compositor.start driver=$driverId") CompositorHost.startOrAttach( holder.surface, runtimeDir.path, driverId?.let { turnip.driverPath(it) }, driverId?.let { turnip.libraryName(it) }, diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java b/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java new file mode 100644 index 000000000..00aca26a5 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java @@ -0,0 +1,30 @@ +package com.droiddeck.launcher.gpu; + +import android.content.Context; +import android.util.Log; +import java.io.File; +import java.io.FileWriter; + +/** Setup breadcrumbs survive failures before a session log exists. No account data. */ +public final class DriverSetupLog { + private DriverSetupLog() {} + + public static synchronized void write(Context context, String stage) { + Log.i("DriverSetup", stage); + append(context.getFilesDir(), stage); + try { append(context.getExternalFilesDir(null), stage); } + catch (Exception ignored) { /* External storage is optional. */ } + } + + private static void append(File directory, String stage) { + if (directory == null) return; + try { + directory.mkdirs(); + File file = new File(directory, "driver-setup.txt"); + // Keep the latest run's breadcrumbs bounded across repeated attempts. + try (FileWriter out = new FileWriter(file, file.length() < 131072)) { + out.write(System.currentTimeMillis() + " " + stage + "\n"); + } + } catch (Exception ignored) { /* Diagnostics must never prevent startup. */ } + } +} diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java index 27083a544..c125b531c 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java @@ -45,13 +45,16 @@ import java.util.zip.ZipInputStream; */ public final class TurnipDriver { private static final String TAG = "TurnipDriver"; + /** Pinned A6xx/A7xx upstream build; used as the A6xx offline fallback. */ + private static final String DRIVER_A6XX = "turnip26.3.0-a6xx"; /** Adreno 7xx (8 Gen 2/3, e.g. the Pocket FIT's A750). */ private static final String DRIVER_A7XX = "turnip25.1.0"; /** Adreno 8xx (8 Elite, e.g. a Fold 8's A830). */ private static final String DRIVER_A8XX = "turnip-sdk36"; /** The two the apk carries; never removable, unpacked on demand. */ public static final List BUNDLED = Collections.unmodifiableList(Arrays.asList( - DRIVER_A7XX, DRIVER_A8XX, com.droiddeck.launcher.session.QuestExperimental.DRIVER)); + DRIVER_A7XX, DRIVER_A8XX, com.droiddeck.launcher.session.QuestExperimental.DRIVER, + DRIVER_A6XX, com.droiddeck.launcher.session.QuestExperimental.DRIVER_A6XX)); /** Overrides the pick, for a device we cannot reach: a7xx, a8xx or system. */ private static final String OVERRIDE_FILE = "Download/droiddeck-driver"; /** Bundled drivers the user deleted: hidden from the list, their unpacked copy removed. */ @@ -289,12 +292,14 @@ public final class TurnipDriver { */ public String install() { String id = choose(); + DriverSetupLog.write(context, "install.begin driver=" + id); if (id == null) return null; File dir = new File(contentDir, id); if (!new File(dir, "meta.json").isFile()) { FileUtils.delete(dir); if (!TarZst.extractAsset(context, "graphics_driver/adrenotools-" + id + ".tzst", dir)) { Log.e(TAG, "could not unpack " + id); + DriverSetupLog.write(context, "install.unpack-failed driver=" + id); FileUtils.delete(dir); return null; } @@ -305,6 +310,7 @@ public final class TurnipDriver { return null; } Log.i(TAG, "graphics driver " + id + " (" + library + ")"); + DriverSetupLog.write(context, "install.ready driver=" + id + " library=" + library); return id; } @@ -338,6 +344,7 @@ public final class TurnipDriver { if (forced.startsWith("system")) return null; if (forced.startsWith("a8")) return DRIVER_A8XX; if (forced.startsWith("a7")) return DRIVER_A7XX; + if (forced.startsWith("a6")) return DRIVER_A6XX; } String model = gpuModel(); Log.i(TAG, "gpu model: " + (model == null ? "unknown" : model)); @@ -348,6 +355,7 @@ public final class TurnipDriver { char generation = m.group(1).charAt(0); if (generation >= '8') return DRIVER_A8XX; if (generation == '7') return DRIVER_A7XX; + if (generation == '6') return DRIVER_A6XX; } } // Nothing to go on: Android 16 shipped with the 8 Elite, so treat a new device as 8xx. diff --git a/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt b/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt index a58bb7f93..089f0631c 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt @@ -10,29 +10,36 @@ import java.io.File object QuestExperimental { private const val TAG = "QuestExperimental" const val DRIVER = "turnip25.1.0-quest1" + const val DRIVER_A6XX = "turnip26.3.0-quest2" + + internal fun quest2(model: String, device: String, manufacturer: String): Boolean = + model.equals("Quest 2", ignoreCase = true) || + ((manufacturer.equals("Oculus", ignoreCase = true) || manufacturer.equals("Meta", ignoreCase = true)) && + device == "hollywood") - // Quest 2 has an A6xx GPU and cannot use this bundled A7xx driver. internal fun supported(model: String, device: String, manufacturer: String): Boolean = - model.equals("Quest 3", ignoreCase = true) || model.equals("Quest 3S", ignoreCase = true) || + quest2(model, device, manufacturer) || model.equals("Quest 3", ignoreCase = true) || model.equals("Quest 3S", ignoreCase = true) || ((manufacturer.equals("Oculus", ignoreCase = true) || manufacturer.equals("Meta", ignoreCase = true)) && (device == "eureka" || device == "panther")) fun isSupported(): Boolean = supported(Build.MODEL, Build.DEVICE, Build.MANUFACTURER) + fun isQuest2(): Boolean = quest2(Build.MODEL, Build.DEVICE, Build.MANUFACTURER) fun initialize(context: Context) { if (!isSupported()) return val prefs = context.getSharedPreferences("session", Context.MODE_PRIVATE) - if (!prefs.getBoolean("questDefaultsV1", false)) { + val defaultsKey = if (isQuest2()) "quest2DefaultsV1" else "questDefaultsV1" + if (!prefs.getBoolean(defaultsKey, false)) { // Auto would download an unwrapped display driver and lose the compatibility path. // Apply once so deliberate changes made during headset testing remain possible. prefs.edit() .putString("gpuDriverMode", SessionPrefs.GPU_DRIVERS_MANUAL) - .putString("androidDriver", DRIVER) + .putString("androidDriver", if (isQuest2()) DRIVER_A6XX else DRIVER) .putString("linuxDriver", "") // The existing HDR option uses SurfaceControl presentation. This is a // corruption workaround from upstream issue #245, not a claim of HDR accuracy. .putBoolean("hdr.steam", true) - .putBoolean("questDefaultsV1", true) + .putBoolean(defaultsKey, true) .apply() } val report = buildString { @@ -51,7 +58,7 @@ object QuestExperimental { appendLine("androidDriver=${SessionPrefs.androidDriver(context)}") appendLine("steamSurfaceControlWorkaround=${SessionPrefs.hdr(context, "steam")}") appendLine("GPUOBJ fallback installed in Android driver and ARM64 libblsession") - appendLine("Experimental Quest 3/3S compatibility; 2D panel application") + appendLine("Experimental Quest 2/3/3S compatibility; 2D panel application") } Log.i(TAG, report) runCatching { File(context.filesDir, "quest-startup.txt").writeText(report) } diff --git a/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt b/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt index 5677b1519..3f19a2a2f 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt @@ -23,15 +23,33 @@ class QuestExperimentalTest { ShadowBuild.setDevice("phone") } - @Test fun otherDevicesAndQuest2KeepTheirSettings() { - for (model in listOf("Phone", "Quest 2")) { + @Test fun otherDevicesKeepTheirSettings() { + for (model in listOf("Phone", "Quest", "Quest Pro")) { ShadowBuild.setModel(model) QuestExperimental.initialize(context) assertEquals(SessionPrefs.GPU_DRIVERS_AUTO, SessionPrefs.gpuDriverMode(context)) assertEquals("", SessionPrefs.androidDriver(context)) assertFalse(SessionPrefs.hdr(context, "steam")) } - assertFalse(QuestExperimental.supported("Quest 2", "hollywood", "Oculus")) + assertFalse(QuestExperimental.supported("Phone", "hollywood", "Other")) + } + + @Test fun quest2SelectsA6xxInsteadOfQuest3DriverAndPreservesLaterChoices() { + ShadowBuild.setModel("Quest 2") + // A prior Quest 3 flag must not suppress Quest 2's own migration. + context.getSharedPreferences("session", Context.MODE_PRIVATE).edit() + .putBoolean("questDefaultsV1", true).commit() + QuestExperimental.initialize(context) + assertEquals(SessionPrefs.GPU_DRIVERS_MANUAL, SessionPrefs.gpuDriverMode(context)) + assertEquals(QuestExperimental.DRIVER_A6XX, SessionPrefs.androidDriver(context)) + assertEquals("", SessionPrefs.linuxDriver(context)) + assertTrue(SessionPrefs.hdr(context, "steam")) + assertTrue(QuestExperimental.supported("unknown", "hollywood", "Oculus")) + assertTrue(QuestExperimental.supported("unknown", "hollywood", "Meta")) + assertFalse(QuestExperimental.quest2("Quest 3", "eureka", "Meta")) + SessionPrefs.setAndroidDriver(context, "chosen-a6xx-driver") + QuestExperimental.initialize(context) + assertEquals("chosen-a6xx-driver", SessionPrefs.androidDriver(context)) } @Test fun questDefaultsSelectBothCompatibilityPathsAndAreAppliedOnlyOnce() { diff --git a/docs/quest.md b/docs/quest.md index af5871d0e..307d5f84b 100644 --- a/docs/quest.md +++ b/docs/quest.md @@ -1,7 +1,23 @@ -# Experimental Quest 3/3S support +# Experimental Quest 2/3/3S support Quest 3/3S runs DroidDeck as a 2D panel. This does not add immersive VR or -OpenXR support. Quest 2 is outside the bundled Adreno 7xx driver path. +OpenXR support. Quest 2 has a separate experimental Adreno 6xx driver path. + +Quest 2 selects `turnip26.3.0-quest2`, a pinned Banners-Turnip A6xx/A7xx build +with the same GPUOBJ adapter. It does not select the Quest 3 A7xx binary. +The first Quest 2 launch applies manual driver selection and the SurfaceControl +workaround, even if `questDefaultsV1` exists from an earlier install. Later user +choices are retained. The separate `turnip26.3.0-a6xx` bundle provides a plain +Adreno 6xx offline fallback for other devices; their driver Auto behavior remains. +Neither family-wide hardware coverage nor Quest 2 operation has been verified. + +The A6xx input URL and SHA-256 are pinned in `tools/quest/build-driver.py`. +Both A6xx bundles are prepared by the existing local/CI wrapper build step and +covered by runtime-input attestation. Quest 3/3S retains its original driver. +Early installation and compositor breadcrumbs are written to `driver-setup.txt` +in private files and, when available, the app's external files directory. They +do not require a Steam session. The existing Quest Java crash file now also +applies to Quest 2; native crashes still require Android logcat. Install the standard DroidDeck APK on Quest 3/3S. Device detection on the first launch selects manual drivers, the bundled diff --git a/tools/quest/build-driver.py b/tools/quest/build-driver.py index b421f9974..f5376f20b 100644 --- a/tools/quest/build-driver.py +++ b/tools/quest/build-driver.py @@ -7,9 +7,81 @@ import subprocess import sys import tarfile import tempfile +import hashlib +import urllib.request +import zipfile ROOT = Path(__file__).resolve().parents[2] OUTPUT = ROOT / 'app/build/prepared-assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst' +A6XX_TAG = 'v26.3.0-20261012-r2' +A6XX_SHA256 = 'bb1b2c51e6446cf428d70255d037be169ac68108c00af33aa1ba7ece711f8336' +A6XX_URL = f'https://github.com/The412Banner/Banners-Turnip/releases/download/{A6XX_TAG}/Turnip-{A6XX_TAG}.zip' + + +def pack(entries, output): + stream = io.BytesIO() + with tarfile.open(fileobj=stream, mode='w', format=tarfile.USTAR_FORMAT) as archive: + for name, content in entries: + entry = tarfile.TarInfo(name) + entry.size = len(content) + entry.mode = 0o644 + archive.addfile(entry, io.BytesIO(content)) + compressed = subprocess.run(['zstd', '-q', '-19', '-c'], input=stream.getvalue(), + capture_output=True, check=True).stdout + output.parent.mkdir(parents=True, exist_ok=True) + output.write_bytes(compressed) + print(output) + + +def build_a6xx(ndk, output_dir=OUTPUT.parent, archive_path=None): + """Pin a real A6xx driver, with a separate Quest adapter around the same bytes.""" + cached = archive_path or ROOT / 'app/build/quest-inputs' / f'Turnip-{A6XX_TAG}.zip' + if not cached.exists(): + cached.parent.mkdir(parents=True, exist_ok=True) + data = urllib.request.urlopen(A6XX_URL, timeout=120).read() + if hashlib.sha256(data).hexdigest() != A6XX_SHA256: + raise ValueError('A6xx archive checksum mismatch') + cached.write_bytes(data) + data = cached.read_bytes() + if hashlib.sha256(data).hexdigest() != A6XX_SHA256: + raise ValueError('A6xx archive checksum mismatch') + with zipfile.ZipFile(io.BytesIO(data)) as archive: + driver = archive.read('libvulkan_freedreno.so') + original_meta = json.loads(archive.read('meta.json')) + if original_meta.get('libraryName') != 'libvulkan_freedreno.so': + raise ValueError('Unexpected A6xx driver library') + hosts = list((Path(ndk) / 'toolchains/llvm/prebuilt').glob('*')) + if len(hosts) != 1: + raise ValueError('Expected one NDK host toolchain') + llvm = hosts[0] / 'bin' + provenance = (f'Banners-Turnip {A6XX_TAG}\n{A6XX_URL}\nSHA256 {A6XX_SHA256}\n' + 'Mesa source: a0ff50acf0e1d152f54949490fa41558d2e0f7a4\n' + 'Quest adapter: DroidDeck tools/quest (GPL-3.0-or-later)\n').encode() + for quest in (False, True): + meta = dict(original_meta) + meta['name'] = 'Turnip Adreno 6xx' + (' - Quest 2 GPUOBJ experimental' if quest else ' - bundled fallback') + meta['description'] += ' Bundled for Adreno 6xx; Quest 2 hardware validation pending.' + entries = [('libvulkan_freedreno.so', driver), ('SOURCE.txt', provenance)] + if quest: + with tempfile.TemporaryDirectory(prefix='quest-a6xx-') as folder: + work = Path(folder) + original = work / 'libvulkan_freedreno.so' + original.write_bytes(driver) + wrapper = work / 'libquest_turnip.so' + subprocess.run([ + str(llvm / 'aarch64-linux-android28-clang'), '-shared', '-fPIC', '-O2', + '-Wall', '-Wextra', '-Werror', '-fvisibility=hidden', + '-Wl,-z,max-page-size=16384', '-Wl,-soname,libquest_turnip.so', + '-Wl,--no-as-needed', str(original), '-Wl,--as-needed', + str(ROOT / 'tools/quest/android_wrapper.c'), str(ROOT / 'tools/quest/kgsl_compat.c'), + '-ldl', '-llog', '-o', str(wrapper), + ], check=True) + subprocess.run([str(llvm / 'llvm-strip'), '--strip-unneeded', str(wrapper)], check=True) + entries.append(('libquest_turnip.so', wrapper.read_bytes())) + meta['libraryName'] = 'libquest_turnip.so' + entries.append(('meta.json', (json.dumps(meta, indent=2) + '\n').encode())) + name = 'turnip26.3.0-quest2' if quest else 'turnip26.3.0-a6xx' + pack(entries, output_dir / f'adrenotools-{name}.tzst') def build(ndk, output=OUTPUT): @@ -58,3 +130,4 @@ def build(ndk, output=OUTPUT): if __name__ == '__main__': build(sys.argv[1]) + build_a6xx(sys.argv[1]) diff --git a/tools/release/runtime_inputs.py b/tools/release/runtime_inputs.py index c9716edf8..eac1c27c8 100644 --- a/tools/release/runtime_inputs.py +++ b/tools/release/runtime_inputs.py @@ -53,11 +53,16 @@ def outputs(root): result['assets/pulseaudio.tzst'] = 'app/build/prepared-assets/pulseaudio.tzst' result['assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst'] = \ 'app/build/prepared-assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst' + for name in ['turnip26.3.0-a6xx', 'turnip26.3.0-quest2']: + result[f'assets/graphics_driver/adrenotools-{name}.tzst'] = \ + f'app/build/prepared-assets/graphics_driver/adrenotools-{name}.tzst' return result def required(): paths = ['assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst'] + paths += [f'assets/graphics_driver/adrenotools-{name}.tzst' + for name in ['turnip26.3.0-a6xx', 'turnip26.3.0-quest2']] paths += ['assets/linuxfs/' + name for name in ['libfakeinput.so', 'libblsession.so', 'libblfastpath.so', 'libssbs.so', 'usr/local/bin/droiddeck-clipboard', 'usr/local/bin/gamescope', 'usr/local/lib/droiddeck/uruntime']] From 5e23c668f3763e903fc44a25332d8247ba7ba9a9 Mon Sep 17 00:00:00 2001 From: Kirk Date: Wed, 14 Oct 2026 02:17:31 +0800 Subject: [PATCH 06/14] fix: the Android keyboard opens on the first tap after the system dismissed it (#665) --- .../droiddeck/launcher/input/KeyboardHost.kt | 7 ++- .../launcher/input/KeyboardHostTest.kt | 55 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/com/droiddeck/launcher/input/KeyboardHostTest.kt diff --git a/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt b/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt index 48e54c791..b18279647 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt @@ -2,10 +2,12 @@ package com.droiddeck.launcher.input import android.app.Activity import android.content.Context +import android.os.Build import android.text.InputType import android.view.KeyCharacterMap import android.view.KeyEvent import android.view.View +import android.view.WindowInsets import android.view.inputmethod.BaseInputConnection import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection @@ -53,7 +55,10 @@ class KeyboardHost( return KeyConnection() } - fun toggle() = if (shown) hide() else show() + fun toggle() = if (shown && imeVisible()) hide() else show() + + private fun imeVisible() = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || + rootWindowInsets?.isVisible(WindowInsets.Type.ime()) != false fun show() { shown = true diff --git a/app/src/test/java/com/droiddeck/launcher/input/KeyboardHostTest.kt b/app/src/test/java/com/droiddeck/launcher/input/KeyboardHostTest.kt new file mode 100644 index 000000000..eff919752 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/input/KeyboardHostTest.kt @@ -0,0 +1,55 @@ +package com.droiddeck.launcher.input + +import android.app.Activity +import android.content.Context +import android.os.Looper +import android.view.inputmethod.InputMethodManager +import android.widget.FrameLayout +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class KeyboardHostTest { + private lateinit var imm: InputMethodManager + private lateinit var host: KeyboardHost + + @Before fun setUp() { + val activity = Robolectric.buildActivity(Activity::class.java).setup().get() + imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + host = KeyboardHost(activity) + activity.setContentView(FrameLayout(activity).apply { addView(host) }) + idle() + } + + @Config(sdk = [33]) + @Test fun toggleShowsAKeyboardTheSystemDismissed() { + host.show() + idle() + imm.hideSoftInputFromWindow(host.windowToken, 0) + idle() + host.toggle() + idle() + assertTrue(shadowOf(imm).isSoftInputVisible) + assertTrue(host.shown) + } + + @Config(sdk = [28]) + @Test fun toggleHidesAKeyboardThatIsShowing() { + host.show() + idle() + host.toggle() + idle() + assertFalse(shadowOf(imm).isSoftInputVisible) + assertFalse(host.shown) + } + + private fun idle() = shadowOf(Looper.getMainLooper()).idle() +} From ec537421acc72f447bcce25313dc50326c98c1a4 Mon Sep 17 00:00:00 2001 From: Kirk Date: Wed, 14 Oct 2026 03:01:40 +0800 Subject: [PATCH 07/14] fix: the Debugging tools hint points to Setup > Diagnostics (#725) --- .../main/java/com/droiddeck/launcher/agent/AgentAccess.kt | 4 ++-- .../com/droiddeck/launcher/agent/AgentSecurityTest.kt | 8 ++++++++ docs/agent-control.md | 2 +- tools/droiddeckctl | 2 +- tools/tests/test_droiddeckctl.py | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt index e43ba8669..b6828c742 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt @@ -8,7 +8,7 @@ import android.content.Context * permission). Running commands in the guest, evaluating JavaScript in the Steam client, session * environment, binary overrides and preference writes reach inside the app's sandbox - the guest * holds the Steam login - which the shell of a stock device otherwise cannot. They require - * Setup > Session > Debugging tools to be on, in every build. The bridge itself cannot turn it on. + * Setup > Diagnostics > Debugging tools to be on, in every build. The bridge itself cannot turn it on. */ object AgentAccess { const val MAX_REQUEST_CHARS = 256 * 1024 @@ -33,7 +33,7 @@ object AgentAccess { fun requireCommands(context: Context) { if (!commandsAllowed(context)) throw AgentException( "AGENT_COMMANDS_DISABLED", - "Debugging tools are off: turn on Setup > Session > Debugging tools on the device", + "Debugging tools are off: turn on Setup > Diagnostics > Debugging tools on the device", ) } diff --git a/app/src/test/java/com/droiddeck/launcher/agent/AgentSecurityTest.kt b/app/src/test/java/com/droiddeck/launcher/agent/AgentSecurityTest.kt index 499645c3e..df4bb15fc 100644 --- a/app/src/test/java/com/droiddeck/launcher/agent/AgentSecurityTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/agent/AgentSecurityTest.kt @@ -7,6 +7,7 @@ import android.util.Base64 import androidx.compose.ui.semantics.SemanticsConfiguration import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.text.AnnotatedString +import com.droiddeck.launcher.R import com.droiddeck.launcher.runtime.LinuxRuntime import org.json.JSONObject import org.junit.Assert.* @@ -68,6 +69,13 @@ class AgentSecurityTest { assertFalse(AgentAccess.commandsAllowed(context)) } + @Test fun theRefusalNamesTheSetupTabThatHoldsTheToggle() { + val refusal = refused("AGENT_COMMANDS_DISABLED") { AgentAccess.requireCommands(context) } + val tab = context.getString(R.string.setup_tab_diagnostics) + val toggle = context.getString(R.string.setup_agent_commands) + assertTrue(refusal.message, refusal.message!!.contains("Setup > $tab > $toggle")) + } + @Test fun runCannotBypassTheToggle() { refused("AGENT_COMMANDS_DISABLED") { AgentAccess.requireStart(context, JSONObject().put("mode", "run")) } AgentAccess.requireStart(context, JSONObject().put("mode", "steam")) diff --git a/docs/agent-control.md b/docs/agent-control.md index 62ad534ad..da7e78098 100644 --- a/docs/agent-control.md +++ b/docs/agent-control.md @@ -26,7 +26,7 @@ Player controls and observations work over authorized ADB on any build: `state`, `resume`, `wait`, `launch`, `quit`, `focus`, `input`, `ui`, `logs`, `screenshot`, `record`, `displays`, `wake`, and `access`. -**Setup > Session > Debugging tools** is off by default in every build, including debug builds. +**Setup > Diagnostics > Debugging tools** is off by default in every build, including debug builds. It enables `run`, `guest`, `cdp`, all `env` operations, `override`, and `prefs`. The toggle switches directly without a confirmation dialog. The bridge refuses to change it through `ui` or preference writes. The existing preference key, `setting-agent-commands` test tag, JSON command names, and diff --git a/tools/droiddeckctl b/tools/droiddeckctl index db21fa31a..d7456a81a 100755 --- a/tools/droiddeckctl +++ b/tools/droiddeckctl @@ -300,7 +300,7 @@ def start_session(adb: str, serial: str, request: dict) -> dict: def require_commands(adb: str, serial: str) -> dict: access = checked(provider_call(adb, serial, "access"), "ACCESS_FAILED") if access.get("commands") is not True: - raise ControlError("Debugging tools are off: turn on Setup > Session > Debugging tools on the device", + raise ControlError("Debugging tools are off: turn on Setup > Diagnostics > Debugging tools on the device", "AGENT_COMMANDS_DISABLED") return access diff --git a/tools/tests/test_droiddeckctl.py b/tools/tests/test_droiddeckctl.py index 22330ded0..9e3c87ac9 100644 --- a/tools/tests/test_droiddeckctl.py +++ b/tools/tests/test_droiddeckctl.py @@ -193,7 +193,7 @@ class DroiddeckctlTest(FakeAdbTest): code, out = self.run_ctl("run", "/usr/bin/sh", "--", "-c", "true") self.assertEqual(7, code) self.assertEqual("AGENT_COMMANDS_DISABLED", out["error"]["code"]) - self.assertIn("Debugging tools", out["error"]["message"]) + self.assertIn("Setup > Diagnostics > Debugging tools", out["error"]["message"]) self.assertEqual(["access"], [c.get("method") for c in self.calls()]) def test_override_checks_names_before_pushing_any_file(self): From 02888834d40e352dea48e4f7b31184d96e5192f7 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:21:01 -0400 Subject: [PATCH 08/14] fix: pace gamescope's fixed-refresh frame limiter per window by time (#753) --- tools/gamescope/PATCHES.md | 7 +++++ .../0115-limiter-paces-per-window.patch | 27 +++++++++++++++++++ tools/gamescope/release.env | 4 +-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tools/gamescope/patches/0115-limiter-paces-per-window.patch diff --git a/tools/gamescope/PATCHES.md b/tools/gamescope/PATCHES.md index acb345515..3a36c3ae0 100644 --- a/tools/gamescope/PATCHES.md +++ b/tools/gamescope/PATCHES.md @@ -44,6 +44,13 @@ library list, before anything is published. read the window's attributes first, it kept the window as override-redirect and left it out of `GAMESCOPE_FOCUSABLE_WINDOWS`, so the client kept its loading screen over the running game (Skyrim SE, A Plague Tale: Innocence, on the games' own Xwayland). MapNotify now refreshes the flag. +- `0115-limiter-paces-per-window.patch` - this app: on a fixed-refresh output the frame limiter + (Steam's Frame Limit) let a capped window's frames out only on every Nth refresh of a global grid, + so a frame finished just after its slot waited a whole extra slot. At 30 on 60 Hz one picture was + held for three or four refreshes - "small stops and ladders" with frame generation - where a game's + own limiter slips such a frame by one refresh. The fixed-refresh branch now paces each window by + time, as the VRR branch already did: its next frame goes out on the first refresh a capped interval + after its last one, with half a refresh of tolerance so an on-time window keeps every Nth refresh. Sixteen more of Armada's patches are DRM/lease/HDR-on-KMS work for a native display, which this app's Wayland-hosted gamescope never reaches, or need a newer gamescope than the runtime has. diff --git a/tools/gamescope/patches/0115-limiter-paces-per-window.patch b/tools/gamescope/patches/0115-limiter-paces-per-window.patch new file mode 100644 index 000000000..7ead84e35 --- /dev/null +++ b/tools/gamescope/patches/0115-limiter-paces-per-window.patch @@ -0,0 +1,27 @@ +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +index 5b0e300..b9b07ca 100644 +--- a/src/steamcompmgr.cpp ++++ b/src/steamcompmgr.cpp +@@ -6875,10 +6875,19 @@ static bool steamcompmgr_should_vblank_window( bool bShouldLimitFPS, uint64_t vb + { + if ( g_nSteamCompMgrTargetFPS && bShouldLimitFPS && nRefreshHz > nTargetFPS ) + { +- int nVblankDivisor = nRefreshHz / nTargetFPS; ++ if ( w && now ) ++ { ++ uint64_t schedule = w->last_commit_first_latch_time + g_SteamCompMgrLimitedAppRefreshCycle; ++ if ( now + g_SteamCompMgrAppRefreshCycle / 2 < schedule ) ++ bSendCallback = false; ++ } ++ else ++ { ++ int nVblankDivisor = nRefreshHz / nTargetFPS; + +- if ( vblank_idx % nVblankDivisor != 0 ) +- bSendCallback = false; ++ if ( vblank_idx % nVblankDivisor != 0 ) ++ bSendCallback = false; ++ } + } + } + diff --git a/tools/gamescope/release.env b/tools/gamescope/release.env index 9f895d08d..631c6b47d 100644 --- a/tools/gamescope/release.env +++ b/tools/gamescope/release.env @@ -1,3 +1,3 @@ GAMESCOPE_REPO=Droid-Deck/DroidDeck-Components -GAMESCOPE_TAG=gamescope-3.16.29-p9 -GAMESCOPE_SHA256=7123a37bdbcd0d76d4bd1d7490e5d2017221e036395fdd48c1e92458594ca769 +GAMESCOPE_TAG=gamescope-3.16.29-p10 +GAMESCOPE_SHA256=e8e2433ae618458135ec271a30af7e80b282ff41f9931286e9bbad614838eca0 From 4163b31b02cfc3734ddaede05cb5704899d0246f Mon Sep 17 00:00:00 2001 From: The412Banner <205237651+The412Banner@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:03:38 -0400 Subject: [PATCH 09/14] fix: Windows components: .NET Core / Desktop runtimes install again (#740) droiddeck-msi-install: a DuplicateFile entry onto the file itself (dotnet.exe in the .NET host MSI) is skipped like Windows Installer does, instead of stopping the whole install with SameFileError. Fixes .NET Desktop 3/6/7/8, .NET Core 3 and .NET 5. Thanks to whatsupbros for finding and tracing it. --- .../usr/local/bin/droiddeck-msi-install | 9 ++++-- tools/tests/test_msi_install.py | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install b/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install index bfa637a7f..c1fe35846 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install @@ -601,8 +601,14 @@ class Installer: def put(self, source, rel): target = self.out / rel + # A DuplicateFile row can name the file's own folder and name (the .NET host packages copy + # dotnet.exe onto itself); Windows Installer takes that as nothing to do. Found and diagnosed + # by whatsupbros on the DroidDeck Discord. + if target.exists() and Path(source).samefile(target): + return False target.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(source, target) + return True def install_files(self, unpacked): assemblies = self.assemblies() @@ -664,8 +670,7 @@ class Installer: folder = self.directory(row["DestFolder"]) if row.get("DestFolder") else self.files[row["File_"]].rsplit("/", 1)[0] name = long_name(row.get("DestName", "")) or self.files[row["File_"]].rsplit("/", 1)[1] source = self.out / self.files[row["File_"]] - if folder and source.is_file() and "/" not in name: - self.put(source, folder + "/" + name) + if folder and source.is_file() and "/" not in name and self.put(source, folder + "/" + name): self.counts["files"] += 1 if missing: self.notes.append("%d file(s) were not in the package's cabinets" % missing) diff --git a/tools/tests/test_msi_install.py b/tools/tests/test_msi_install.py index 16e8807f1..b896288eb 100644 --- a/tools/tests/test_msi_install.py +++ b/tools/tests/test_msi_install.py @@ -119,6 +119,35 @@ class LayoutTest(unittest.TestCase): inst.select() self.assertEqual(inst.included_components, {"Up", "Wide"}) + def test_a_duplicate_onto_the_file_itself_is_skipped(self): + # The .NET host packages (dotnet-host-*-win-x86.msi) duplicate dotnet.exe into its own + # folder under its own name; copying it onto itself raised SameFileError and stopped every + # .NET Core / Desktop runtime install. + with tempfile.TemporaryDirectory() as tmp: + src = Path(tmp) / "cab" / "dotnet.exe" + src.parent.mkdir() + src.write_bytes(b"MZ") + out = Path(tmp) / "out" + inst = installer({ + "Directory": rows("Directory Directory_Parent DefaultDir", + ("TARGETDIR", "", "SourceDir"), + ("ProgramFilesFolder", "TARGETDIR", "PFiles"), + ("DOTNETHOME", "ProgramFilesFolder", "dotnet"), + ("DOTNETHOME_X86", "ProgramFilesFolder", "dotnet")), + "Component": rows("Component Directory_ Attributes Condition KeyPath", + ("Host", "DOTNETHOME", "0", "", "DotnetExe")), + "Feature": rows("Feature Feature_Parent Level", ("Main", "", "1")), + "FeatureComponents": rows("Feature_ Component_", ("Main", "Host")), + "File": rows("File Component_ FileName", ("DotnetExe", "Host", "dotnet.exe")), + "DuplicateFile": rows("FileKey Component_ File_ DestName DestFolder", + ("Dup", "Host", "DotnetExe", "", "DOTNETHOME_X86")), + }, out=str(out)) + inst.select() + inst.install_files({"DotnetExe": src}) + placed = out / "drive_c/Program Files (x86)/dotnet/dotnet.exe" + self.assertEqual(placed.read_bytes(), b"MZ") + self.assertEqual(inst.counts["files"], 1) + def test_global_assembly_paths(self): with tempfile.TemporaryDirectory() as tmp: dll = Path(tmp) / "a.dll" From 83ec5708edae6ad101045a9d438e4dbd15ea877c Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:27:42 -0400 Subject: [PATCH 10/14] cleanup (#764) --- .gitattributes | 1 - .github/workflows/build-aaudio-sink.yml | 2 - .github/workflows/build-desktop-kde.yml | 9 - .github/workflows/build-gamescope.yml | 6 - .github/workflows/build.yml | 42 - .github/workflows/ci-debian-image.yml | 4 - .github/workflows/pr-release-cleanup.yml | 9 +- .github/workflows/pr-release-publish.yml | 16 - .github/workflows/release.yml | 3 - .github/workflows/update-catalog.yml | 1 - .gitignore | 8 - app/build.gradle | 63 - app/src/main/AndroidManifest.xml | 45 - app/src/main/cpp/CMakeLists.txt | 17 +- app/src/main/cpp/adrenotools/build_asm.sh | 1 - .../cpp/adrenotools/gen/bcenabler_patch.h | 2 - .../include/adrenotools/bcenabler.h | 22 +- .../adrenotools/include/adrenotools/driver.h | 45 +- .../adrenotools/include/adrenotools/priv.h | 9 +- .../lib/linkernsbypass/android_linker_ns.cpp | 20 +- .../lib/linkernsbypass/android_linker_ns.h | 28 +- .../lib/linkernsbypass/elf_soname_patcher.cpp | 7 +- .../lib/linkernsbypass/elf_soname_patcher.h | 8 +- .../lib/linkernsbypass/meson.build | 5 - app/src/main/cpp/adrenotools/meson.build | 15 - .../main/cpp/adrenotools/src/bcenabler.cpp | 21 +- .../cpp/adrenotools/src/bcenabler_patch.s | 2 +- app/src/main/cpp/adrenotools/src/driver.cpp | 16 +- .../cpp/adrenotools/src/hook/hook_impl.cpp | 12 +- .../main/cpp/adrenotools/src/hook/hook_impl.h | 1 + .../adrenotools/src/hook/hook_impl_params.h | 5 +- app/src/main/cpp/adrenotools/src/hook/kgsl.h | 926 +-- .../main/cpp/adrenotools/src/hook/meson.build | 20 - .../tools/acc-shim/vk_acc_shim.cpp | 4 - .../tools/qtimapper-shim/ext/QtiGrallocDefs.h | 34 +- .../tools/qtimapper-shim/ext/common.h | 1 - .../qtimapper-shim/ext/gr_adreno_info.cpp | 25 +- .../tools/qtimapper-shim/ext/gr_adreno_info.h | 149 +- .../tools/qtimapper-shim/ext/gr_priv_handle.h | 24 +- .../tools/qtimapper-shim/ext/gr_utils.cpp | 53 +- .../tools/qtimapper-shim/ext/gr_utils.h | 27 +- .../tools/qtimapper-shim/ext/gralloc_priv.h | 14 +- .../tools/qtimapper-shim/ext/mapper.h | 34 +- .../qtimapper-shim/ext/mapperextensions.h | 21 +- .../tools/qtimapper-shim/ext/types/common.h | 4984 +++++++---------- .../tools/qtimapper-shim/ext/types/mapper.h | 60 +- .../ext/types/mapperextensions.h | 103 +- .../adrenotools/tools/qtimapper-shim/shim.cpp | 25 +- app/src/main/cpp/deviceinfo/vkinfo.c | 5 - app/src/main/cpp/fakeinput_steam.cpp | 495 +- .../main/cpp/framegen/VulkanRendererContext.h | 328 +- app/src/main/cpp/framegen/lsfg/lsfg_alpha.cpp | 4 - app/src/main/cpp/framegen/lsfg/lsfg_alpha.hpp | 4 - app/src/main/cpp/framegen/lsfg/lsfg_beta.cpp | 4 - app/src/main/cpp/framegen/lsfg/lsfg_beta.hpp | 4 - app/src/main/cpp/framegen/lsfg/lsfg_chain.cpp | 4 - app/src/main/cpp/framegen/lsfg/lsfg_chain.hpp | 4 - .../main/cpp/framegen/lsfg/lsfg_common.cpp | 4 - .../main/cpp/framegen/lsfg/lsfg_common.hpp | 4 - app/src/main/cpp/framegen/lsfg/lsfg_delta.cpp | 4 - app/src/main/cpp/framegen/lsfg/lsfg_delta.hpp | 4 - app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp | 58 +- app/src/main/cpp/framegen/lsfg/lsfg_dll.h | 83 +- app/src/main/cpp/framegen/lsfg/lsfg_dxbc.cpp | 21 +- app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h | 32 +- .../main/cpp/framegen/lsfg/lsfg_engine.cpp | 45 +- app/src/main/cpp/framegen/lsfg/lsfg_engine.h | 60 +- app/src/main/cpp/framegen/lsfg/lsfg_gamma.cpp | 4 - app/src/main/cpp/framegen/lsfg/lsfg_gamma.hpp | 4 - .../main/cpp/framegen/lsfg/lsfg_generate.cpp | 4 - .../main/cpp/framegen/lsfg/lsfg_generate.hpp | 4 - .../main/cpp/framegen/lsfg/lsfg_governor.cpp | 58 +- .../main/cpp/framegen/lsfg/lsfg_governor.h | 36 +- app/src/main/cpp/framegen/lsfg/lsfg_jni.cpp | 7 +- .../main/cpp/framegen/lsfg/lsfg_mipmaps.cpp | 4 - .../main/cpp/framegen/lsfg/lsfg_mipmaps.hpp | 4 - app/src/main/cpp/framegen/lsfg/lsfg_pacer.cpp | 16 - app/src/main/cpp/framegen/lsfg/lsfg_pacer.hpp | 4 - app/src/main/cpp/framegen/lsfg/lsfg_probe.cpp | 23 +- app/src/main/cpp/framegen/lsfg/lsfg_probe.h | 66 +- .../main/cpp/framegen/lsfg/lsfg_shaders.cpp | 10 - .../main/cpp/framegen/lsfg/lsfg_shaders.hpp | 6 - app/src/main/cpp/framegen/lsfg/lsfg_vkd.cpp | 1 - app/src/main/cpp/framegen/lsfg/lsfg_vkd.h | 23 - app/src/main/cpp/framegen/winfg/config.hpp | 248 +- .../cpp/framegen/winfg/embedded_shaders.hpp | 4 +- app/src/main/cpp/framegen/winfg/framegen.cpp | 44 +- app/src/main/cpp/framegen/winfg/framegen.hpp | 117 +- app/src/main/cpp/framegen/winfg/log.hpp | 8 - .../main/cpp/framegen/winfg/record_impl.inc | 192 +- .../main/cpp/framegen/winfg/vk_dispatch.hpp | 32 +- .../main/cpp/framegen/winfg/winfg_engine.cpp | 31 +- .../main/cpp/framegen/winfg/winfg_engine.h | 41 +- app/src/main/cpp/framegen/winfg/winfg_vkd.cpp | 1 - app/src/main/cpp/framegen/winfg/winfg_vkd.h | 15 - app/src/main/cpp/scanout/ScanoutContext.h | 30 +- app/src/main/cpp/sdnative/sdnative.c | 8 - app/src/main/cpp/shaders/cas_frag.h | 1 - app/src/main/cpp/shaders/color_frag.h | 1 - app/src/main/cpp/shaders/crt_frag.h | 1 - app/src/main/cpp/shaders/deband_frag.h | 1 - app/src/main/cpp/shaders/fsr_easu_frag.h | 1 - app/src/main/cpp/shaders/fsr_rcas_frag.h | 1 - app/src/main/cpp/shaders/fxaa_frag.h | 1 - app/src/main/cpp/shaders/hdr_frag.h | 1 - app/src/main/cpp/shaders/nis_frag.h | 1 - app/src/main/cpp/shaders/ntsc_frag.h | 1 - app/src/main/cpp/shaders/sgsr_frag.h | 1 - app/src/main/cpp/shaders/sgsr_quality_frag.h | 1 - app/src/main/cpp/shaders/toon_frag.h | 1 - app/src/main/cpp/shaders/upscale_vert.h | 1 - app/src/main/cpp/termux/termux.c | 7 - app/src/main/cpp/thirdparty/dxbc/.gitignore | 1 - .../main/cpp/thirdparty/dxbc/CMakeLists.txt | 1 - .../dxbc/include/dxbc/dxbc_analysis.h | 30 - .../dxbc/include/dxbc/dxbc_chunk_isgn.h | 12 - .../dxbc/include/dxbc/dxbc_chunk_shex.h | 7 - .../dxbc/include/dxbc/dxbc_common.h | 29 - .../dxbc/include/dxbc/dxbc_compiler.h | 232 +- .../dxbc/include/dxbc/dxbc_decoder.h | 118 - .../thirdparty/dxbc/include/dxbc/dxbc_defs.h | 118 +- .../thirdparty/dxbc/include/dxbc/dxbc_enums.h | 89 +- .../dxbc/include/dxbc/dxbc_header.h | 18 - .../dxbc/include/dxbc/dxbc_modinfo.h | 25 - .../dxbc/include/dxbc/dxbc_module.h | 52 - .../thirdparty/dxbc/include/dxbc/dxbc_names.h | 2 +- .../dxbc/include/dxbc/dxbc_options.h | 20 - .../dxbc/include/dxbc/dxbc_reader.h | 6 - .../thirdparty/dxbc/include/dxbc/dxbc_tag.h | 6 - .../thirdparty/dxbc/include/dxbc/dxbc_util.h | 65 - .../dxbc/include/dxvk/dxvk_pipelayout.h | 50 +- .../dxbc/include/spirv/spirv_code_buffer.h | 138 - .../dxbc/include/spirv/spirv_instruction.h | 48 - .../dxbc/include/spirv/spirv_module.h | 8 - .../include/spirv/thirdparty/GLSL.std.450.h | 16 +- .../dxbc/include/spirv/thirdparty/spirv.hpp | 29 +- .../thirdparty/dxbc/include/util/log/log.h | 6 - .../thirdparty/dxbc/include/util/rc/util_rc.h | 11 - .../dxbc/include/util/rc/util_rc_ptr.h | 29 - .../thirdparty/dxbc/include/util/util_bit.h | 67 - .../thirdparty/dxbc/include/util/util_error.h | 6 - .../thirdparty/dxbc/include/util/util_math.h | 3 - .../dxbc/include/util/util_small_vector.h | 1 - .../dxbc/include/util/util_string.h | 67 - .../dxbc/src/dxbc/dxbc_analysis.cpp | 12 - .../dxbc/src/dxbc/dxbc_chunk_isgn.cpp | 1 - .../dxbc/src/dxbc/dxbc_chunk_shex.cpp | 4 - .../dxbc/src/dxbc/dxbc_compiler.cpp | 732 +-- .../thirdparty/dxbc/src/dxbc/dxbc_decoder.cpp | 40 +- .../thirdparty/dxbc/src/dxbc/dxbc_defs.cpp | 235 - .../thirdparty/dxbc/src/dxbc/dxbc_header.cpp | 11 +- .../thirdparty/dxbc/src/dxbc/dxbc_module.cpp | 4 - .../thirdparty/dxbc/src/dxbc/dxbc_names.cpp | 2 +- .../thirdparty/dxbc/src/dxbc/dxbc_util.cpp | 16 +- .../dxbc/src/spirv/spirv_code_buffer.cpp | 6 +- .../dxbc/src/spirv/spirv_module.cpp | 31 - .../thirdparty/dxbc/src/util/util_string.cpp | 11 - app/src/main/cpp/waylandcomp/CMakeLists.txt | 23 +- .../generated/banner-ahb-v1-protocol.c | 4 +- .../generated/banner-ahb-v1-server-protocol.h | 141 +- .../generated/banner-desktop-v1-protocol.c | 4 +- .../banner-desktop-v1-server-protocol.h | 74 +- .../generated/color-management-v1-protocol.c | 4 +- .../color-management-v1-server-protocol.h | 1915 +------ .../linux-dmabuf-v1-client-protocol.h | 828 +-- .../generated/linux-dmabuf-v1-protocol.c | 4 +- .../linux-dmabuf-v1-server-protocol.h | 707 +-- ...pointer-constraints-unstable-v1-protocol.c | 4 +- ...-constraints-unstable-v1-server-protocol.h | 464 +- .../generated/presentation-time-protocol.c | 4 +- .../presentation-time-server-protocol.h | 254 +- .../relative-pointer-unstable-v1-protocol.c | 4 +- ...tive-pointer-unstable-v1-server-protocol.h | 151 +- .../single-pixel-buffer-v1-protocol.c | 4 +- .../single-pixel-buffer-v1-server-protocol.h | 96 +- .../text-input-unstable-v3-protocol.c | 4 +- .../text-input-unstable-v3-server-protocol.h | 729 +-- .../generated/viewporter-protocol.c | 4 +- .../generated/viewporter-server-protocol.h | 278 +- .../wlr-data-control-unstable-v1-protocol.c | 4 +- ...data-control-unstable-v1-server-protocol.h | 352 +- .../generated/xdg-shell-client-protocol.h | 1574 +----- .../generated/xdg-shell-protocol.c | 4 +- .../generated/xdg-shell-server-protocol.h | 1595 +----- .../generated/xdg-toplevel-icon-v1-protocol.c | 4 +- .../xdg-toplevel-icon-v1-server-protocol.h | 258 +- .../prebuilt/include/wayland-server-core.h | 197 - .../include/wayland-server-protocol.h | 4142 +------------- .../prebuilt/include/wayland-server.h | 19 - .../prebuilt/include/wayland-util.h | 573 +- .../protocols/color-management-v1.xml | 1 - .../waylandcomp/protocols/linux-dmabuf-v1.xml | 1 - .../protocols/presentation-time.xml | 3 - .../protocols/text-input-unstable-v3.xml | 1 - .../wlr-data-control-unstable-v1.xml | 1 - .../cpp/waylandcomp/protocols/xdg-shell.xml | 4 - .../main/cpp/waylandcomp/src/ahb_swapchain.c | 67 +- .../main/cpp/waylandcomp/src/ahb_swapchain.h | 52 +- app/src/main/cpp/waylandcomp/src/blend.frag | 1 - app/src/main/cpp/waylandcomp/src/blend.vert | 1 - app/src/main/cpp/waylandcomp/src/blend_frag.h | 1 - app/src/main/cpp/waylandcomp/src/blend_pass.c | 14 +- app/src/main/cpp/waylandcomp/src/blend_pass.h | 15 - app/src/main/cpp/waylandcomp/src/blend_vert.h | 1 - app/src/main/cpp/waylandcomp/src/cardboard.c | 39 +- .../main/cpp/waylandcomp/src/cardboard.frag | 38 +- app/src/main/cpp/waylandcomp/src/cardboard.h | 45 +- .../main/cpp/waylandcomp/src/cardboard_frag.h | 1 - app/src/main/cpp/waylandcomp/src/compositor.c | 713 +-- .../cpp/waylandcomp/src/compositor_internal.h | 28 +- .../cpp/waylandcomp/src/droiddeck_color.h | 121 +- .../main/cpp/waylandcomp/src/droiddeck_ext.c | 12 +- .../main/cpp/waylandcomp/src/droiddeck_ext.h | 33 +- .../main/cpp/waylandcomp/src/effects_chain.c | 103 +- .../main/cpp/waylandcomp/src/effects_chain.h | 52 - .../cpp/waylandcomp/src/framegen_bridge.c | 51 +- .../cpp/waylandcomp/src/framegen_bridge.h | 57 - .../cpp/waylandcomp/src/framegen_engine.cpp | 24 +- .../cpp/waylandcomp/src/framegen_engine.h | 33 - .../waylandcomp/src/framegen_engine_stub.c | 2 - .../main/cpp/waylandcomp/src/framegen_jni.c | 5 - .../main/cpp/waylandcomp/src/hdr_compose.c | 20 +- .../main/cpp/waylandcomp/src/hdr_compose.h | 36 +- .../main/cpp/waylandcomp/src/hdr_encode.frag | 41 +- .../cpp/waylandcomp/src/hdr_encode_frag.h | 1 - .../main/cpp/waylandcomp/src/headpose_shm.h | 23 +- app/src/main/cpp/waylandcomp/src/sc_layer.c | 312 +- app/src/main/cpp/waylandcomp/src/sc_layer.h | 129 - app/src/main/cpp/waylandcomp/src/vk_loader.c | 1 - app/src/main/cpp/waylandcomp/src/vk_loader.h | 14 +- app/src/main/cpp/waylandcomp/src/vk_present.c | 400 +- app/src/main/cpp/waylandcomp/src/vk_present.h | 114 +- .../cpp/waylandcomp/src/waylandcomp_jni.c | 142 +- .../main/cpp/waylandcomp/src/wl_clipboard.c | 67 +- .../main/cpp/waylandcomp/src/wl_color_mgmt.c | 176 +- app/src/main/cpp/waylandcomp/src/wl_dmabuf.c | 59 +- .../cpp/waylandcomp/src/wl_single_pixel.c | 9 +- .../main/cpp/waylandcomp/src/wl_text_input.c | 45 +- .../cpp/waylandcomp/src/wl_toplevel_icon.c | 5 - .../main/java/com/droiddeck/launcher/App.kt | 6 - .../com/droiddeck/launcher/ComponentsMenu.kt | 7 - .../java/com/droiddeck/launcher/DeckyMenu.kt | 7 - .../com/droiddeck/launcher/DriverMenus.kt | 53 - .../com/droiddeck/launcher/HomeActivity.kt | 1 - .../java/com/droiddeck/launcher/HomeApp.kt | 9 - .../com/droiddeck/launcher/MainActivity.kt | 110 - .../java/com/droiddeck/launcher/ProtonMenu.kt | 7 - .../com/droiddeck/launcher/SessionActivity.kt | 324 +- .../com/droiddeck/launcher/UpdatesMenu.kt | 10 - .../droiddeck/launcher/agent/AgentAccess.kt | 11 - .../launcher/agent/AgentBridgeProvider.kt | 14 - .../com/droiddeck/launcher/agent/AgentEnv.kt | 23 - .../droiddeck/launcher/agent/AgentGuest.kt | 16 - .../droiddeck/launcher/agent/AgentInput.kt | 14 - .../launcher/agent/AgentStartActivity.kt | 1 - .../com/droiddeck/launcher/agent/AgentUi.kt | 12 - .../audio/DirectAudioRelayComponent.java | 40 - .../launcher/audio/PulseAudioComponent.java | 59 - .../droiddeck/launcher/core/AppLanguage.kt | 35 - .../droiddeck/launcher/core/ArchivePaths.java | 9 - .../launcher/core/CpuAllocationDefaults.kt | 2 - .../com/droiddeck/launcher/core/CpuCores.kt | 16 - .../droiddeck/launcher/core/DeviceReport.kt | 9 - .../droiddeck/launcher/core/DeviceSupport.kt | 12 - .../droiddeck/launcher/core/Downloader.java | 16 +- .../com/droiddeck/launcher/core/FexPreset.kt | 1 - .../droiddeck/launcher/core/FileUtils.java | 8 +- .../launcher/core/GameEnvironment.kt | 10 - .../com/droiddeck/launcher/core/Hashes.kt | 3 - .../launcher/core/HostEnvironment.kt | 2 - .../droiddeck/launcher/core/HostProcess.kt | 22 +- .../droiddeck/launcher/core/LogRedactor.kt | 99 - .../droiddeck/launcher/core/NetworkReport.kt | 15 - .../launcher/core/PhantomProcessLimit.kt | 8 - .../droiddeck/launcher/core/SecretScrub.kt | 19 - .../launcher/core/SessionLogCapture.kt | 32 - .../droiddeck/launcher/core/SessionPart.java | 7 - .../com/droiddeck/launcher/core/TarZst.java | 12 +- .../launcher/core/TextureFiltering.kt | 16 - .../droiddeck/launcher/core/WifiDiscovery.kt | 2 - .../droiddeck/launcher/core/WirelessAdbFix.kt | 4 - .../droiddeck/launcher/core/ZramSupport.kt | 1 - .../launcher/files/AppFilesProvider.kt | 23 - .../launcher/files/CollapsibleRail.kt | 71 +- .../launcher/files/FavoritesStore.kt | 12 - .../launcher/files/FileAttributes.kt | 25 - .../launcher/files/FileManagerActivity.kt | 1 - .../launcher/files/FileManagerItems.kt | 41 +- .../launcher/files/FileManagerModel.kt | 37 +- .../launcher/files/FileManagerScreen.kt | 112 +- .../com/droiddeck/launcher/files/FileOps.kt | 19 +- .../launcher/files/FilePickerActivity.kt | 13 - .../launcher/files/FilePropertiesDialog.kt | 17 - .../launcher/files/InAppFilePicker.kt | 9 - .../com/droiddeck/launcher/files/MenuStyle.kt | 4 - .../launcher/files/OutlinedAlertDialog.kt | 6 - .../launcher/files/PeIconExtractor.kt | 32 +- .../droiddeck/launcher/files/StorageRoots.kt | 44 - .../droiddeck/launcher/frontend/AddedExes.kt | 9 - .../launcher/frontend/AddedGameArt.kt | 56 - .../launcher/frontend/AddedGameEdits.kt | 14 - .../droiddeck/launcher/frontend/AddedGames.kt | 110 - .../launcher/frontend/AddedGamesPublisher.kt | 9 - .../launcher/frontend/AutoComponents.kt | 78 - .../droiddeck/launcher/frontend/CoverArt.kt | 38 - .../launcher/frontend/DependencyDetector.kt | 82 +- .../launcher/frontend/GameExePicker.kt | 28 - .../launcher/frontend/GameFileSync.kt | 2 - .../droiddeck/launcher/frontend/GameFiles.kt | 9 - .../launcher/frontend/GameIdentifier.kt | 114 +- .../launcher/frontend/GameLaunchLink.kt | 1 - .../launcher/frontend/GameShortcuts.kt | 1 - .../launcher/frontend/GamesFolderMigration.kt | 14 - .../droiddeck/launcher/frontend/Library.kt | 141 +- .../launcher/frontend/LibraryCache.kt | 5 - .../launcher/frontend/PeVersionInfo.kt | 48 +- .../frontend/PrefixInstalledDetector.kt | 10 - .../launcher/frontend/SessionInstalls.kt | 18 - .../launcher/frontend/SteamAppInfo.kt | 28 - .../launcher/frontend/SteamGridDb.kt | 23 - .../droiddeck/launcher/frontend/SteamMatch.kt | 17 - .../droiddeck/launcher/frontend/SteamPics.kt | 31 - .../launcher/frontend/SteamRedists.kt | 8 - .../launcher/frontend/StoreListBackfill.kt | 20 - .../launcher/frontend/WinCompSources.kt | 42 - .../com/droiddeck/launcher/gpu/Cardboard.kt | 75 +- .../com/droiddeck/launcher/gpu/DeviceInfo.kt | 12 - .../droiddeck/launcher/gpu/DriverBundle.kt | 30 - .../com/droiddeck/launcher/gpu/DriverPairs.kt | 33 - .../launcher/gpu/DriverSetupLog.java | 6 +- .../com/droiddeck/launcher/gpu/FrameGen.kt | 20 - .../com/droiddeck/launcher/gpu/GpuInfo.kt | 25 - .../com/droiddeck/launcher/gpu/HeadTracker.kt | 11 - .../launcher/gpu/LinuxVulkanDriver.java | 29 - .../gpu/LinuxVulkanDriverManager.java | 49 - .../com/droiddeck/launcher/gpu/Lossless.kt | 13 - .../droiddeck/launcher/gpu/LsfgNative.java | 7 - .../droiddeck/launcher/gpu/ScreenEffects.kt | 33 - .../com/droiddeck/launcher/gpu/SocNames.kt | 12 - .../com/droiddeck/launcher/gpu/SteamVr.kt | 15 - .../droiddeck/launcher/gpu/TurnipDriver.java | 68 - .../droiddeck/launcher/gpu/TurnipReleases.kt | 37 - .../com/droiddeck/launcher/gpu/VulkanInfo.kt | 7 - .../launcher/input/ControllerPrefs.kt | 2 - .../droiddeck/launcher/input/DeckControls.kt | 12 - .../droiddeck/launcher/input/EvdevKeys.java | 26 +- .../launcher/input/FakeInputRingPaths.java | 12 - .../launcher/input/FakeInputWriter.java | 134 +- .../droiddeck/launcher/input/KeyboardHost.kt | 19 +- .../launcher/input/OnScreenControls.kt | 1 - .../droiddeck/launcher/input/PadBridge.java | 97 - .../com/droiddeck/launcher/input/PadMotion.kt | 31 - .../com/droiddeck/launcher/input/PadState.kt | 10 - .../launcher/input/PointerGestures.kt | 20 +- .../launcher/input/SecondScreenMode.kt | 3 - .../launcher/input/SessionClipboard.kt | 7 - .../launcher/input/SteamControllerBle.kt | 31 +- .../launcher/input/SteamTouchBindings.kt | 37 - .../launcher/input/SteamTouchConfig.kt | 71 +- .../launcher/input/SteamTouchControls.kt | 122 +- .../launcher/input/SteamTouchDevice.kt | 29 +- .../launcher/input/SteamTouchIconPicker.kt | 17 - .../launcher/input/TouchpadGestures.kt | 31 - .../launcher/runtime/AppImageManager.kt | 40 - .../launcher/runtime/BwrapSpawner.kt | 98 +- .../launcher/runtime/DeckyManager.kt | 8 - .../launcher/runtime/DeckyPluginInstall.kt | 3 - .../launcher/runtime/DesktopCatalog.kt | 46 - .../launcher/runtime/DesktopFileRemoval.kt | 6 +- .../launcher/runtime/DesktopRemoval.kt | 9 - .../launcher/runtime/FlatpakManager.kt | 32 - .../launcher/runtime/GuestCommand.kt | 12 - .../runtime/LinuxNetworkLinkComponent.kt | 30 - .../launcher/runtime/LinuxRuntime.java | 121 - .../runtime/LinuxRuntimeInstaller.java | 77 +- .../launcher/runtime/ProotFastPath.kt | 24 - .../launcher/runtime/RuntimeFileTree.java | 8 - .../launcher/runtime/RuntimeInstallService.kt | 11 - .../droiddeck/launcher/runtime/UserApps.kt | 43 - .../launcher/session/BatteryComponent.kt | 23 - .../launcher/session/ComponentsManager.kt | 102 - .../launcher/session/CpuStatComponent.kt | 18 - .../launcher/session/CrashHandler.kt | 12 - .../launcher/session/GameEnvironmentStore.kt | 3 - .../session/GameProfileInventoryCache.kt | 1 - .../droiddeck/launcher/session/GameSaves.kt | 34 - .../droiddeck/launcher/session/GameStorage.kt | 24 - .../droiddeck/launcher/session/GpuClockPin.kt | 15 - .../launcher/session/GpuMemComponent.kt | 25 - .../launcher/session/GpuStatsComponent.kt | 31 - .../launcher/session/HwmonComponent.kt | 29 - .../launcher/session/LoadingState.kt | 21 - .../launcher/session/LogMigration.kt | 12 - .../droiddeck/launcher/session/OfflineMode.kt | 20 - .../launcher/session/OrphanReaper.kt | 18 - .../launcher/session/OverlayShown.kt | 6 - .../com/droiddeck/launcher/session/PerfHud.kt | 15 - .../droiddeck/launcher/session/PerfMode.kt | 30 - .../droiddeck/launcher/session/PipPolicy.kt | 1 - .../launcher/session/ProtonExtras.kt | 27 - .../launcher/session/QuestExperimental.kt | 5 - .../launcher/session/RivalClients.kt | 20 +- .../launcher/session/RumbleComponent.kt | 29 +- .../launcher/session/SecondaryLibrary.kt | 9 - .../launcher/session/SelectedGameProfile.kt | 2 - .../session/SessionAchievementGame.kt | 1 - .../launcher/session/SessionArtifacts.kt | 97 - .../launcher/session/SessionDisplay.kt | 31 - .../launcher/session/SessionEvents.kt | 4 - .../launcher/session/SessionFiles.kt | 56 - .../launcher/session/SessionLogShare.kt | 41 - .../launcher/session/SessionPaths.kt | 56 - .../launcher/session/SessionPipController.kt | 1 - .../launcher/session/SessionPrefs.kt | 242 - .../launcher/session/SessionService.kt | 297 +- .../launcher/session/SessionState.kt | 32 - .../launcher/session/SessionTerminal.kt | 1 - .../launcher/session/SettingsReset.kt | 18 - .../launcher/session/SteamDeckPad.kt | 54 - .../launcher/session/SteamDownloadMonitor.kt | 6 - .../launcher/session/SteamLanguage.kt | 10 - .../launcher/session/StorageDiagnostics.kt | 6 - .../launcher/session/WinComponentNames.kt | 6 - .../launcher/session/WinComponents.kt | 138 - .../droiddeck/launcher/store/FlathubApi.kt | 10 - .../droiddeck/launcher/store/StoreState.kt | 8 - .../droiddeck/launcher/store/UserAppsState.kt | 8 - .../stores/AchievementNotifications.kt | 2 - .../droiddeck/launcher/stores/CloudPlan.kt | 18 - .../launcher/stores/CloudSavePaths.kt | 23 - .../droiddeck/launcher/stores/CloudSaves.kt | 52 - .../launcher/stores/CloudTransport.kt | 18 - .../launcher/stores/CredentialCipher.kt | 25 +- .../droiddeck/launcher/stores/GameFacts.kt | 39 - .../droiddeck/launcher/stores/LoginFlows.kt | 8 - .../droiddeck/launcher/stores/SizeCache.kt | 6 - .../launcher/stores/SteamLiveShortcuts.kt | 25 - .../com/droiddeck/launcher/stores/Store.kt | 4 - .../launcher/stores/StoreAccounts.kt | 42 - .../launcher/stores/StoreAchievements.kt | 8 - .../com/droiddeck/launcher/stores/StoreArt.kt | 8 - .../droiddeck/launcher/stores/StoreCatalog.kt | 42 - .../com/droiddeck/launcher/stores/StoreExe.kt | 6 - .../launcher/stores/StoreGameSidecar.kt | 40 - .../launcher/stores/StoreInstallRoot.kt | 45 - .../launcher/stores/StoreInstalls.kt | 45 - .../droiddeck/launcher/stores/StoreLaunch.kt | 41 - .../launcher/stores/StoreLaunchRequests.kt | 16 +- .../launcher/stores/StoreLogFiles.kt | 13 - .../launcher/stores/StoreLoginActivity.kt | 29 - .../com/droiddeck/launcher/stores/StoreNet.kt | 23 - .../droiddeck/launcher/stores/StoreSizes.kt | 10 - .../droiddeck/launcher/stores/StoresNative.kt | 13 - .../droiddeck/launcher/stores/StoresState.kt | 65 - .../stores/amazon/AmazonApiClient.java | 13 - .../stores/amazon/AmazonAuthClient.java | 9 - .../launcher/stores/amazon/AmazonBackend.kt | 10 - .../stores/amazon/AmazonCredentialStore.kt | 6 - .../stores/amazon/AmazonDownloadManager.java | 11 - .../launcher/stores/amazon/AmazonGame.java | 8 - .../stores/amazon/AmazonLaunchHelper.java | 9 - .../launcher/stores/amazon/AmazonLibrary.kt | 8 - .../stores/amazon/AmazonManifest.java | 7 - .../launcher/stores/amazon/AmazonNative.kt | 14 - .../launcher/stores/amazon/AmazonPkce.java | 4 - .../launcher/stores/amazon/AmazonPrefs.kt | 1 - .../stores/download/DownloadModels.kt | 27 - .../launcher/stores/download/DownloadQueue.kt | 68 - .../launcher/stores/download/SavedQueue.kt | 12 - .../stores/download/StoreDownloadService.kt | 25 - .../stores/download/StoreDownloadTier.kt | 10 - .../launcher/stores/epic/EpicAchievements.kt | 2 - .../launcher/stores/epic/EpicApiClient.java | 16 - .../launcher/stores/epic/EpicAuthClient.java | 13 - .../launcher/stores/epic/EpicBackend.kt | 16 - .../stores/epic/EpicCredentialStore.kt | 7 - .../stores/epic/EpicDownloadManager.java | 69 +- .../launcher/stores/epic/EpicGame.java | 7 - .../launcher/stores/epic/EpicInstallTags.kt | 7 - .../launcher/stores/epic/EpicLaunchData.kt | 14 - .../launcher/stores/epic/EpicLibrary.kt | 8 - .../launcher/stores/epic/EpicNative.kt | 22 - .../launcher/stores/epic/EpicPrefs.kt | 5 - .../launcher/stores/epic/EpicStoreCatalog.kt | 15 - .../launcher/stores/gog/GogAchievements.kt | 3 - .../droiddeck/launcher/stores/gog/GogAuth.kt | 15 - .../launcher/stores/gog/GogBackend.kt | 6 - .../droiddeck/launcher/stores/gog/GogComet.kt | 5 - .../stores/gog/GogDownloadManager.java | 80 - .../launcher/stores/gog/GogGame.java | 4 - .../launcher/stores/gog/GogLibrary.kt | 22 - .../launcher/stores/gog/GogNative.kt | 31 - .../droiddeck/launcher/stores/gog/GogPrefs.kt | 6 - .../launcher/stores/gog/GogStoreCatalog.kt | 19 - .../com/droiddeck/launcher/ui/AddAppDialog.kt | 27 - .../droiddeck/launcher/ui/AddedGameEditor.kt | 36 - .../com/droiddeck/launcher/ui/AppScale.kt | 1 - .../droiddeck/launcher/ui/ComponentsPage.kt | 80 - .../droiddeck/launcher/ui/CursorOverlay.kt | 5 - .../launcher/ui/DeckControlsPanel.kt | 51 +- .../com/droiddeck/launcher/ui/DesktopPage.kt | 46 - .../com/droiddeck/launcher/ui/DriverPage.kt | 16 - .../com/droiddeck/launcher/ui/EmptyLibrary.kt | 38 - .../droiddeck/launcher/ui/ExePickerDialog.kt | 18 - .../java/com/droiddeck/launcher/ui/FitText.kt | 6 - .../com/droiddeck/launcher/ui/FocusGlide.kt | 58 +- .../com/droiddeck/launcher/ui/FrameGenMenu.kt | 5 - .../com/droiddeck/launcher/ui/FrontEndArt.kt | 11 - .../droiddeck/launcher/ui/FrontEndContent.kt | 47 - .../droiddeck/launcher/ui/FrontEndGames.kt | 51 - .../com/droiddeck/launcher/ui/FrontEndRail.kt | 25 - .../droiddeck/launcher/ui/FrontEndScreen.kt | 81 - .../droiddeck/launcher/ui/FrontEndSetup.kt | 36 - .../droiddeck/launcher/ui/FrontEndWidgets.kt | 17 - .../launcher/ui/GameEnvironmentEditor.kt | 12 - .../droiddeck/launcher/ui/GamepadGeometry.kt | 26 - .../droiddeck/launcher/ui/GpuDriversPanel.kt | 13 - .../java/com/droiddeck/launcher/ui/Hop.kt | 41 - .../com/droiddeck/launcher/ui/LaunchFlood.kt | 57 - .../java/com/droiddeck/launcher/ui/Layout.kt | 5 - .../com/droiddeck/launcher/ui/LiftSheet.kt | 118 - .../com/droiddeck/launcher/ui/MainScreen.kt | 1 - .../launcher/ui/ModeSettingsDialog.kt | 45 - .../com/droiddeck/launcher/ui/PackageRow.kt | 1 - .../com/droiddeck/launcher/ui/PageFlood.kt | 52 - .../com/droiddeck/launcher/ui/PcKeyboard.kt | 16 - .../droiddeck/launcher/ui/ResolutionRow.kt | 1 - .../launcher/ui/SecondScreenPresentation.kt | 2 - .../launcher/ui/SessionDrawerPage.kt | 1 - .../droiddeck/launcher/ui/SessionLoading.kt | 16 - .../droiddeck/launcher/ui/SessionOverlay.kt | 73 - .../droiddeck/launcher/ui/SettingsWidgets.kt | 49 - .../com/droiddeck/launcher/ui/SharedMotion.kt | 46 - .../com/droiddeck/launcher/ui/SheetRows.kt | 11 - .../com/droiddeck/launcher/ui/SteamHome.kt | 43 - .../java/com/droiddeck/launcher/ui/StepOut.kt | 51 - .../java/com/droiddeck/launcher/ui/Steps.kt | 8 - .../launcher/ui/StoreAchievementsUi.kt | 7 +- .../com/droiddeck/launcher/ui/StoreChips.kt | 6 - .../droiddeck/launcher/ui/StoreGameFacts.kt | 21 - .../launcher/ui/StoreGridNavigation.kt | 1 - .../com/droiddeck/launcher/ui/StorePage.kt | 63 - .../com/droiddeck/launcher/ui/StoresDetail.kt | 24 - .../droiddeck/launcher/ui/StoresDownloads.kt | 63 - .../launcher/ui/StoresInstallDialog.kt | 5 - .../com/droiddeck/launcher/ui/StoresMotion.kt | 51 - .../com/droiddeck/launcher/ui/StoresPage.kt | 177 +- .../droiddeck/launcher/ui/StoresWidgets.kt | 2 - .../com/droiddeck/launcher/ui/TestTags.kt | 5 - .../java/com/droiddeck/launcher/ui/Theme.kt | 4 - .../com/droiddeck/launcher/ui/Throbber.kt | 15 - .../com/droiddeck/launcher/ui/UpdatesPage.kt | 20 - .../launcher/ui/WinComponentsEditor.kt | 58 - .../launcher/ui/WirelessAdbFixPage.kt | 1 - .../com/droiddeck/launcher/ui/Wordmark.kt | 6 - .../droiddeck/launcher/update/AppUpdates.kt | 13 - .../launcher/update/SelfInstaller.kt | 14 - .../launcher/wayland/CompositorHost.kt | 53 - .../droiddeck/launcher/wayland/HdrSupport.kt | 7 - .../launcher/wayland/WaylandCompositor.java | 225 - app/src/main/res/anim/session_hold.xml | 1 - app/src/main/res/anim/session_rise.xml | 1 - app/src/main/res/anim/session_sink.xml | 1 - app/src/main/res/values-es/strings.xml | 165 - app/src/main/res/values-ja/strings.xml | 165 - app/src/main/res/values-ko/strings.xml | 165 - app/src/main/res/values-zh-rCN/strings.xml | 165 - app/src/main/res/values-zh-rHK/strings.xml | 165 - app/src/main/res/values-zh-rTW/strings.xml | 165 - app/src/main/res/values/strings.xml | 179 - app/src/main/res/values/themes.xml | 1 - app/src/main/res/xml/game_mode_config.xml | 9 - app/src/main/res/xml/locales_config.xml | 2 - app/src/main/rust/stores/Cargo.lock | 2 - app/src/main/rust/stores/Cargo.toml | 15 - app/src/main/rust/stores/src/amazon.rs | 102 - app/src/main/rust/stores/src/amazon/jni.rs | 22 - app/src/main/rust/stores/src/epic.rs | 31 +- app/src/main/rust/stores/src/epic/assemble.rs | 53 - app/src/main/rust/stores/src/epic/chunk.rs | 45 +- app/src/main/rust/stores/src/epic/driver.rs | 45 - app/src/main/rust/stores/src/epic/jni.rs | 34 - app/src/main/rust/stores/src/epic/manifest.rs | 108 +- app/src/main/rust/stores/src/epic/plan.rs | 49 - app/src/main/rust/stores/src/fetch_core.rs | 264 +- app/src/main/rust/stores/src/fetch_tuning.rs | 100 - app/src/main/rust/stores/src/gog.rs | 31 - app/src/main/rust/stores/src/gog/engine.rs | 153 +- app/src/main/rust/stores/src/gog/jni.rs | 25 - app/src/main/rust/stores/src/gog/plan.rs | 46 - app/src/main/rust/stores/src/lib.rs | 19 +- app/src/main/rust/stores/src/md5_small.rs | 10 - app/src/main/rust/stores/src/priority.rs | 18 - app/src/main/rust/stores/src/stores_jni.rs | 9 - .../launcher/core/LogRedactorTest.kt | 1 - .../launcher/core/SecretScrubTest.kt | 1 - .../launcher/frontend/AddedExesTest.kt | 3 - .../frontend/AddedGameArtLookupTest.kt | 4 - .../launcher/frontend/AddedGameEditsTest.kt | 7 - .../frontend/AddedGamesCandidatesTest.kt | 3 - .../launcher/frontend/AddedGamesRemoveTest.kt | 3 - .../frontend/AddedGamesStoreScanTest.kt | 2 - .../launcher/frontend/AddedGamesTest.kt | 1 - .../launcher/frontend/GameFileSyncTest.kt | 3 - .../launcher/frontend/GameFilesTest.kt | 1 - .../launcher/frontend/GameIdentifierTest.kt | 32 +- .../frontend/GamesFolderMigrationTest.kt | 4 - .../launcher/frontend/SteamPicsTest.kt | 6 - .../launcher/frontend/WinCompSourcesTest.kt | 27 - .../droiddeck/launcher/gpu/CardboardTest.kt | 21 - .../launcher/gpu/DriverBundleTest.kt | 1 - .../droiddeck/launcher/gpu/DriverPairsTest.kt | 1 - .../launcher/gpu/GpuModelFallbackTest.kt | 1 - .../droiddeck/launcher/gpu/SocNamesTest.kt | 1 - .../launcher/input/FakeInputRingPathTest.kt | 7 - .../launcher/input/PadBridgeRoutingTest.kt | 2 - .../launcher/input/SteamTouchConfigTest.kt | 1 - .../launcher/runtime/AppImageManagerTest.kt | 1 - .../launcher/runtime/BwrapSpawnerTest.kt | 6 - .../runtime/DeckyPluginInstallTest.kt | 1 - .../launcher/runtime/ProotFastPathTest.kt | 1 - .../launcher/runtime/RuntimeRemovalTest.kt | 4 - .../launcher/runtime/RuntimeRemovalUiTest.kt | 1 - .../launcher/session/BatteryComponentTest.kt | 1 - .../session/GameEnvironmentStoreTest.kt | 1 - .../launcher/session/ProtonExtrasTest.kt | 7 +- .../launcher/session/QuestExperimentalTest.kt | 1 - .../launcher/session/RumbleComponentTest.kt | 1 - .../launcher/session/SessionDisplayTest.kt | 6 - .../launcher/session/SessionLogShareTest.kt | 5 - .../session/SessionLogShareZipTest.kt | 8 - .../session/SteamDownloadMonitorTest.kt | 2 - .../launcher/session/SteamLanguageTest.kt | 1 - .../session/WinComponentsSupportTest.kt | 8 - .../launcher/store/FlathubApiTest.kt | 1 - .../launcher/stores/CloudPlanTest.kt | 5 - .../launcher/stores/CloudSavePathsTest.kt | 2 - .../launcher/stores/CloudSavesTest.kt | 5 - .../launcher/stores/GameFactsTest.kt | 2 - .../launcher/stores/InstalledCardsTest.kt | 3 - .../launcher/stores/MatureContentTest.kt | 2 - .../stores/StoreAccountsCipherTest.kt | 7 - .../launcher/stores/StoreCacheTest.kt | 5 - .../launcher/stores/StoreGameSidecarTest.kt | 12 - .../launcher/stores/StoreInstallRootTest.kt | 4 - .../droiddeck/launcher/stores/StoreLogTest.kt | 2 - .../stores/download/DownloadEntryTest.kt | 4 - .../download/DownloadNotificationTextTest.kt | 1 - .../stores/epic/EpicJsonManifestTest.kt | 1 - .../launcher/stores/gog/GogCdnTest.kt | 1 - .../droiddeck/launcher/ui/FlathubFocusTest.kt | 3 - .../launcher/ui/GamepadGeometryTest.kt | 4 - .../droiddeck/launcher/ui/LiftSheetTest.kt | 1 - .../com/droiddeck/launcher/ui/MotionTest.kt | 1 - .../com/droiddeck/launcher/ui/PageExitTest.kt | 1 - .../droiddeck/launcher/ui/PickListingTest.kt | 1 - .../droiddeck/launcher/ui/PresetScaleTest.kt | 2 - .../launcher/ui/RobolectricComposeRule.kt | 3 - .../droiddeck/launcher/ui/StoreTabsTest.kt | 2 - .../boot-animation-source/droiddeck-boot.html | 86 +- artwork/boot-animation-source/render.py | 12 +- artwork/boot-animation-source/sound.py | 20 +- docs/agent-control.md | 46 +- docs/development/stores.md | 8 +- gradle.properties | 4 - gradle/wrapper/gradle-wrapper.properties | 1 - gradlew | 26 +- gradlew.bat | 14 - tools/aaudio-sink/build.sh | 8 - tools/aaudio-sink/config.h | 3 - tools/aaudio-sink/ltdl.h | 2 - tools/aaudio-sink/module-aaudio-sink.c | 36 - tools/adb_device.py | 1 - tools/build_local.sh | 21 - tools/cardboard/test_cardboard_params.c | 7 - tools/comet/droiddeck.patch | 2 +- tools/desktop-kde/build-kwin.sh | 5 - tools/desktop-kde/build.sh | 28 +- tools/desktop-kde/closure.py | 4 - tools/desktop-kde/kwin-build-seeds.txt | 1 - tools/desktop-kde/lxqt-applications.txt | 2 - .../actions/steamdeck-gaming-return.svg | 1 - .../places/distributor-logo-steamdeck.svg | 1 - .../layouts/org.kde.plasma.desktop-layout.js | 3 - .../DroidDeck/contents/images/1920x1080.svg | 1 - .../0001-respect-nested-desktop-scale.patch | 4 +- tools/desktop-kde/removal-manifests.py | 3 - tools/desktop-kde/seeds.txt | 3 - tools/directaudio/fetch.sh | 15 - tools/directaudio/release.env | 3 - tools/droiddeck-scenario | 2 +- tools/droiddeckctl | 10 +- tools/gamescope/build-in-arch.sh | 16 +- ...019-steamcompmgr-arm64-virtual-white.patch | 6 +- .../0020-color-p3-red-is-wide-gamut.patch | 2 +- ...00-realtime-queue-and-gamepad-cursor.patch | 24 +- .../patches/0110-wayland-backend-touch.patch | 8 +- ...wayland-pointer-warps-in-passthrough.patch | 6 +- ...112-restore-iconified-game-on-resume.patch | 16 +- ...ake-override-redirect-from-mapnotify.patch | 2 +- tools/linuxfs/check-preloads.sh | 5 - tools/linuxfs/clipboard/clipboard.c | 14 +- tools/linuxfs/debian-image.env | 5 - tools/linuxfs/desktop/droiddeck-desktop | 66 - tools/linuxfs/desktop/droiddeck-desktop-gpu | 13 - tools/linuxfs/desktop/droiddeck-gpu | 11 - tools/linuxfs/desktop/firefox-droiddeck.js | 8 - tools/linuxfs/desktop/kwin_wayland_wrapper | 6 - .../overlay/usr/bin/jupiter-biosupdate | 1 - .../steamos-polkit-helpers/jupiter-biosupdate | 1 - .../jupiter-dock-updater | 4 - .../steamos-polkit-helpers/steamos-priv-write | 4 - .../steamos-select-branch | 4 - .../steamos-set-timezone | 1 - .../bin/steamos-polkit-helpers/steamos-update | 4 - .../overlay/usr/bin/steamos-select-branch | 4 - .../overlay/usr/bin/steamos-session-select | 6 - tools/linuxfs/overlay/usr/bin/steamos-update | 3 - .../overlay/usr/local/bin/droiddeck-agent | 14 +- .../usr/local/bin/droiddeck-appimage-run | 22 - .../overlay/usr/local/bin/droiddeck-bwrap | 21 - .../usr/local/bin/droiddeck-desktop-bookmarks | 5 +- .../usr/local/bin/droiddeck-desktop-clipboard | 4 - .../usr/local/bin/droiddeck-desktop-games | 9 - .../local/bin/droiddeck-desktop-window-rules | 1 - .../overlay/usr/local/bin/droiddeck-esync | 3 - .../overlay/usr/local/bin/droiddeck-fex | 5 - .../overlay/usr/local/bin/droiddeck-flatpak | 28 +- .../usr/local/bin/droiddeck-flatpak-run | 10 - .../usr/local/bin/droiddeck-flatpak-setup | 28 - .../overlay/usr/local/bin/droiddeck-fonts | 12 - .../overlay/usr/local/bin/droiddeck-game-env | 10 - .../usr/local/bin/droiddeck-gpu-window | 3 - .../usr/local/bin/droiddeck-launcher-cores | 14 +- .../overlay/usr/local/bin/droiddeck-login1 | 12 +- .../usr/local/bin/droiddeck-msi-install | 35 +- .../usr/local/bin/droiddeck-netmanager | 22 +- .../overlay/usr/local/bin/droiddeck-open | 2 - .../overlay/usr/local/bin/droiddeck-open-url | 1 - .../usr/local/bin/droiddeck-pad-defaults | 114 +- .../usr/local/bin/droiddeck-proton-extra | 34 - .../usr/local/bin/droiddeck-script-run | 5 - .../usr/local/bin/droiddeck-seed-redists | 50 - .../overlay/usr/local/bin/droiddeck-session | 294 - .../usr/local/bin/droiddeck-steam-compat | 5 - .../usr/local/bin/droiddeck-steam-desktop-ui | 1 - .../usr/local/bin/droiddeck-steam-games | 7 - .../usr/local/bin/droiddeck-steam-install | 12 - .../usr/local/bin/droiddeck-steam-language | 1 - .../usr/local/bin/droiddeck-steam-launch | 13 - .../usr/local/bin/droiddeck-steam-library | 17 - .../usr/local/bin/droiddeck-steam-shim | 4 - .../usr/local/bin/droiddeck-steam-shortcuts | 15 +- .../usr/local/bin/droiddeck-steam-ui-scale | 7 - .../overlay/usr/local/bin/droiddeck-steamvr | 21 - .../usr/local/bin/droiddeck-store-launch | 12 +- .../usr/local/bin/droiddeck-wincomponents | 15 - .../overlay/usr/local/bin/steam-compatibility | 180 +- tools/linuxfs/preload/binfmt.c | 16 - tools/linuxfs/preload/dircache.c | 21 - tools/linuxfs/preload/drm.c | 34 +- tools/linuxfs/preload/fsync.c | 3 - tools/linuxfs/preload/inputudev.c | 54 +- tools/linuxfs/preload/kgslid.c | 15 +- tools/linuxfs/preload/kgslpoll.c | 17 - tools/linuxfs/preload/links.c | 23 - tools/linuxfs/preload/lock.c | 29 +- tools/linuxfs/preload/net.c | 23 +- tools/linuxfs/preload/netif.c | 30 +- tools/linuxfs/preload/noexec.c | 9 - tools/linuxfs/preload/ntsync.c | 3 - tools/linuxfs/preload/opens.c | 29 +- tools/linuxfs/preload/pathcache.c | 16 - tools/linuxfs/preload/perms.c | 27 - tools/linuxfs/preload/quest_kgsl.c | 1 - tools/linuxfs/preload/robust.c | 10 - tools/linuxfs/preload/space.c | 24 - tools/linuxfs/preload/storage.c | 15 +- tools/linuxfs/preload/sysv.c | 45 +- tools/linuxfs/preload/tracefs.c | 14 - tools/linuxfs/preload/tracer.c | 24 +- tools/linuxfs/preload/udevmon.c | 51 +- tools/linuxfs/preload/xmode.c | 19 - tools/linuxfs/preload/zram.c | 5 - tools/linuxfs/ssbs/ssbs_adapter.c | 32 - tools/mangoapp/libtracefs-shim.c | 14 - tools/mangoapp/mangoapp | 10 - tools/mangoapp/packages.txt | 7 - tools/msitools/packages.txt | 9 - tools/proot/bench/equiv2.py | 1 - tools/proot/bench/fpbench.c | 2 - tools/proot/bench/mechbench.c | 23 +- tools/proot/bench/parstat.c | 1 - tools/proot/bench/run-device.sh | 10 - tools/proot/bench/sysbench.c | 3 - tools/proot/bench/workloads.sh | 1 - tools/proot/fastpath/audit.c | 51 +- tools/proot/fastpath/fastpath.c | 254 +- .../0004-seccomp-filter-by-argument.patch | 6 +- .../patches/0010-new-mount-api-enosys.patch | 4 +- .../patches/0011-kompat-utsname-only.patch | 18 +- .../0012-android-hardlink-denial.patch | 6 +- .../patches/0012-fake_id0-identity-only.patch | 32 +- ...ctl-by-request-and-kernel-exit-stops.patch | 58 +- .../patches/0014-fastpath-trampoline.patch | 40 +- tools/quest/android_wrapper.c | 5 +- tools/quest/build-driver.py | 1 - tools/quest/kgsl_compat.c | 8 +- tools/quest/kgsl_compat.h | 1 - tools/release/axml_rename.py | 2 - tools/release/runtime_inputs.py | 2 - tools/release/sign-apk.sh | 24 - tools/release/update_catalog.py | 2 - tools/release/variants.txt | 5 - tools/test_local.sh | 1 - tools/tests/effects_source_cache_harness.c | 2 - tools/tests/sysv_otime_harness.c | 3 - tools/tests/termux_jni_harness.c | 7 - tools/tests/test_binfmt_preload.py | 1 - tools/tests/test_cardboard.py | 3 - tools/tests/test_desktop_removal.py | 2 - tools/tests/test_dircache_preload.py | 2 - tools/tests/test_droiddeckctl.py | 5 +- tools/tests/test_effects_source_cache.py | 1 - tools/tests/test_game_environment.py | 14 - tools/tests/test_launcher_cores.py | 13 +- tools/tests/test_login1.py | 2 +- tools/tests/test_msi_install.py | 5 - tools/tests/test_netmanager.py | 5 - tools/tests/test_proton_extra.py | 2 - tools/tests/test_seed_redists.py | 3 - tools/tests/test_session_guest_command.py | 2 - tools/tests/test_session_tmp.py | 2 +- tools/tests/test_steam_desktop_launch.py | 2 +- tools/tests/test_steam_install.py | 3 - tools/tests/test_steam_language.py | 1 - tools/tests/test_steam_shortcuts.py | 1 - tools/tests/test_store_launch.py | 8 - tools/tests/test_termux_jni.py | 2 - tools/tests/test_wincomponents.py | 2 +- tools/tests/test_zram_preload.py | 2 - tools/vr/build.sh | 13 - tools/vr/src/driver_droiddeck.cpp | 58 +- tools/vr/src/openvr_driver.h | 1336 ++--- tools/vr/src/presenter.cpp | 38 +- tools/vr/src/presenter.h | 21 +- tools/vr/src/vk_layer_droiddeck_vr.c | 120 +- tools/vr/src/wayland_client_stub.c | 3 - .../third_party/wayland/wayland-client-core.h | 82 - .../wayland/wayland-client-protocol.h | 4591 +-------------- tools/vr/third_party/wayland/wayland-client.h | 9 - tools/vr/third_party/wayland/wayland-util.h | 573 +- 852 files changed, 4844 insertions(+), 46144 deletions(-) diff --git a/.gitattributes b/.gitattributes index 0e4a34658..b4e784d8f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,2 @@ -# Vendored files preserve upstream license spacing and unified-diff context lines. tools/comet/LICENSE whitespace=-blank-at-eof tools/comet/droiddeck.patch whitespace=-blank-at-eol,-blank-at-eof diff --git a/.github/workflows/build-aaudio-sink.yml b/.github/workflows/build-aaudio-sink.yml index 63133e545..b6ecd5e04 100644 --- a/.github/workflows/build-aaudio-sink.yml +++ b/.github/workflows/build-aaudio-sink.yml @@ -1,7 +1,5 @@ name: Build AAudio sink module -# A quick check of tools/aaudio-sink on its own; the APK build compiles the same module into the -# audio bundle. Runs on pushes that touch the module. on: push: paths: diff --git a/.github/workflows/build-desktop-kde.yml b/.github/workflows/build-desktop-kde.yml index 9390e1af8..5a9933abc 100644 --- a/.github/workflows/build-desktop-kde.yml +++ b/.github/workflows/build-desktop-kde.yml @@ -1,13 +1,5 @@ name: Build desktop package (KDE Plasma) -# The desktop the app installs into its Linux runtime on first use: KDE Plasma as SteamOS's desktop -# mode has it, the closure of tools/desktop-kde/seeds.txt over Arch Linux ARM minus what the runtime -# already carries, plus tools/desktop-kde/overlay (tools/desktop-kde/build.sh), every package -# signature-checked. Published as a release in Droid-Deck/DroidDeck-Components with DroidDeck's -# other components; the apk pins it in tools/desktop-kde/release.env (tag, sha256, size). A -# published tag is never rebuilt: the packages are Arch Linux ARM's current ones, so a rebuild -# would not match the pinned sha256. A changed package gets a new tag (desktop-kde-r2, ...) and a -# release.env update. on: push: paths: @@ -72,7 +64,6 @@ jobs: KDE_KWIN_OVERRIDE="$PWD/out/kwin" tools/desktop-kde/build.sh out cp out/kwin/desktop-kde.kwin.txt out/ echo "size $(stat -c %s out/desktop-kde.tar.zst) sha256 $(cat out/desktop-kde.sha256)" - # The package itself too, briefly: enough to try a build on a device before publishing it. - uses: actions/upload-artifact@v4 with: name: desktop-kde diff --git a/.github/workflows/build-gamescope.yml b/.github/workflows/build-gamescope.yml index 649c01e9f..0a91fea43 100644 --- a/.github/workflows/build-gamescope.yml +++ b/.github/workflows/build-gamescope.yml @@ -1,9 +1,5 @@ name: Build gamescope (patched) -# The runtime's own gamescope (Arch Linux ARM's 3.16.29 package) rebuilt with the patches under -# tools/gamescope/patches, in an Arch Linux ARM container on an arm64 runner, then published as a -# release asset the apk build fetches (tools/gamescope/release.env names the tag and sha). The -# hosted runtime image is never changed: the apk stages the binary over /usr/local/bin. on: push: paths: @@ -43,6 +39,4 @@ jobs: run: | set -euo pipefail gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber - # A component the apk fetches by tag, never something users install: it must not - # become the repo's Latest release or appear as a pre-release, also when re-uploaded. gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b854e59d..7bd2d1d08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,8 +22,6 @@ permissions: pull-requests: write concurrency: - # Replace older builds only for the same PR. Each push/dispatch gets its own group so one main - # build cannot cancel another before it signs and publishes. group: build-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} @@ -211,14 +209,12 @@ jobs: aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -nostdlib -ffreestanding -fno-stack-protector \ -fno-tree-loop-distribute-patterns -Wl,-z,defs -o "$D/libblaudit.so" tools/proot/fastpath/audit.c aarch64-linux-gnu-strip --strip-unneeded "$D/libblaudit.so" - # Force-SSBS adapter, preloaded into a game's Wine only when the game asks for it. aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \ -o "$D/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl aarch64-linux-gnu-strip --strip-unneeded "$D/libssbs.so" mkdir -p "$D/usr/local/bin" aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$D/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-clipboard" - # Cardboard mode's SteamVR driver and Vulkan layer (tools/vr), Vulkan headers from the runner's NDK. VK_INCLUDE=$(ls -d "${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"/toolchains/llvm/prebuilt/*/sysroot/usr/include | head -1) \ tools/vr/build.sh --native "$D/usr/local/lib/droiddeck-vr" for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do @@ -254,14 +250,12 @@ jobs: for so in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do cp -L "mango-pkgs/usr/lib/$so" "$M/$so" done - # Ours, not the package's: GPU memory without tracefs (tools/mangoapp/libtracefs-shim.c). aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \ -o "$M/libtracefs.so.1" tools/mangoapp/libtracefs-shim.c aarch64-linux-gnu-strip --strip-unneeded "$M/libtracefs.so.1" install -Dm644 tools/mangoapp/mangoapp "$D/usr/local/bin/mangoapp" rm -rf mango-pkgs ls -l "$M" - # The Windows components installer's tools (tools/msitools/packages.txt). T="$D/usr/local/lib/droiddeck-msitools" mkdir -p "$T" msi-pkgs grep -v '^#' tools/msitools/packages.txt | while read -r sum url; do @@ -273,7 +267,6 @@ jobs: done install -m644 msi-pkgs/usr/bin/msiinfo "$T/msiinfo" install -m644 msi-pkgs/usr/bin/cabextract "$T/cabextract" - # 7-Zip (the full build with its format plugin) opens installer .exe wrappers. install -m644 msi-pkgs/usr/lib/7zip/7z "$T/7z" install -m644 msi-pkgs/usr/lib/7zip/7z.so "$T/7z.so" for so in libmsi-1.0.so.0 libgsf-1.so.114 libgcab-1.0.so.0; do @@ -291,10 +284,6 @@ jobs: install -Dm644 tools/linuxfs/desktop/firefox-droiddeck.js "$D/usr/lib/firefox/defaults/pref/droiddeck.js" ls -lR "$D" - # The Debian 11 image the preloads are compiled in comes from a release on this repo - # (published once by ci-debian-image.yml, pinned in tools/linuxfs/debian-image.env): the - # public registry mirrors rate-limit the shared runner addresses and failed builds on a - # busy evening. Only if that download fails is the image pulled, with retries. - name: Fetch the Debian image the preloads build in if: steps.preloads.outputs.cache-hit != 'true' env: @@ -370,9 +359,6 @@ jobs: set -euo pipefail curl -fsSL -o pa.tar.gz https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz mkdir -p pa13 && tar -xzf pa.tar.gz -C pa13 --strip-components=1 && rm pa.tar.gz - # DirectAudio - the game driver (one set per Wine audio interface), the Android-side helper and - # the Steam client's sinks - comes from a pinned release of The412Banner/directaudio, checksummed, - # exactly like gamescope; nothing compiled for audio is kept in this repo. - name: Fetch DirectAudio (tools/directaudio/release.env) run: tools/directaudio/fetch.sh "$PWD" "$PWD/sink-out" - name: Build the AAudio sink into the audio bundle @@ -410,12 +396,6 @@ jobs: set -euo pipefail bash tools/linuxfs/check-preloads.sh app/src/main/assets/linuxfs - # The store download engines (app/src/main/rust/stores) are Rust, built by Gradle's - # buildRustStores task through cargo-ndk. rustup and cargo are on the runner image; the Android - # target and cargo-ndk are added here. Cache = cargo registry + cargo-ndk + the crate's target - # tree, keyed on the rustc version and Cargo.lock, so a source-only change rebuilds this crate - # on top of the cached dependency objects, and a lockfile change still starts from the - # previous objects (restore-keys). - name: Rust toolchain for the store engines (aarch64-linux-android) id: rust run: | @@ -474,9 +454,6 @@ jobs: python3 tools/release/runtime_inputs.py apk app/build/outputs/apk/release/app-release.apk - name: Check the store engines' library - # The exports Kotlin binds must be in the apk's libdroiddeckstores.so, or the stores fail on - # the device with an UnsatisfiedLinkError instead of here. The lockfile check is the - # reproducibility guard: cargo must not have had to re-resolve anything. run: | set -euo pipefail NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}" @@ -487,11 +464,6 @@ jobs: || { echo "::error::Cargo.lock changed during the build; commit the regenerated lockfile (uploaded as stores-cargo-lock)"; exit 1; } - name: Store engines host unit tests - # The crate's #[test] functions are pure host-side logic (manifest parsing, chunk plans, - # assembly from a cache, hashes), so a plain `cargo test` on the runner's own architecture - # runs them; the Android build above compiles none of them. Informational on this first - # run - the point is to learn whether they pass - and bounded so a hang can never hold - # the apk. Make it blocking once it is known green. timeout-minutes: 10 continue-on-error: true run: | @@ -626,13 +598,6 @@ jobs: variants: ${{ needs.build.outputs.variants }} secrets: inherit - # Each signed main build as a prerelease of the public Droid-Deck/DroidDeck-CI (main-, the - # last KEEP kept), where anyone can download it without a GitHub account, then a post in the - # Discord's #preview-builds channel linking to it. The release is made with a token of the org's - # "DroidDeck CI Publisher" app (installed on DroidDeck-CI only, Contents: write): its ID in the - # DROIDDECK_CI_APP_ID variable, its private key in the DROIDDECK_CI_APP_KEY secret. - # DISCORD_BUILDS_WEBHOOK is the channel's webhook URL. Keep this secret name when renaming - # the channel: Discord routes the existing webhook by channel ID, not its display name. announce: needs: [build, sign] if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' @@ -659,8 +624,6 @@ jobs: owner: Droid-Deck repositories: DroidDeck-CI - # What this build is, for the top of the release notes: the merged PR's title and the first - # paragraph of its description, or the commit's subject for a push made without a PR. - name: Describe the change env: GH_TOKEN: ${{ github.token }} @@ -710,15 +673,12 @@ jobs: gh release delete "$tag" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null || true gh release create "$tag" -R "$CI_REPO" --target main --prerelease \ --title "main ${GITHUB_SHA::7}" --notes "$notes" signed/* - # Keep the newest main builds by publication time. GitHub App releases can share createdAt. gh release list -R "$CI_REPO" -L 200 --json tagName,publishedAt \ --jq '[.[] | select(.tagName | startswith("main-"))] | sort_by(.publishedAt) | reverse | .['"$KEEP"':][] | .tagName' | while IFS= read -r old; do - # Never remove this run's release, even if the API returns an unexpected ordering. if [ "$old" = "$tag" ]; then continue fi - # Another main publish may have removed the same old release concurrently. if ! gh release delete "$old" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null; then if gh release view "$old" -R "$CI_REPO" >/dev/null 2>&1; then echo "::error::Could not delete old release $old" @@ -748,8 +708,6 @@ jobs: fi commit=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA") subject=$(jq -r '.commit.message | split("\n")[0]' <<<"$commit") - # A merge commit's subject is "Merge pull request #N from ..."; its body's first line is - # the PR's title, which says more. title=$(jq -r '.commit.message | split("\n") | map(select(length > 0)) | .[1] // ""' <<<"$commit") pr=$(sed -n 's/^Merge pull request #\([0-9]*\).*/\1/p' <<<"$subject") if [ -n "$pr" ]; then diff --git a/.github/workflows/ci-debian-image.yml b/.github/workflows/ci-debian-image.yml index 1635ed407..a43e6ad7b 100644 --- a/.github/workflows/ci-debian-image.yml +++ b/.github/workflows/ci-debian-image.yml @@ -1,8 +1,5 @@ name: Publish the CI Debian image -# Pulls the Debian 11 image the x86 preloads build in, once, and publishes it as a release asset -# on this repo so build.yml never depends on a registry mirror's rate limit. Run it again (with a -# new tag) only to refresh the image; tools/linuxfs/debian-image.env names the one builds use. on: workflow_dispatch: @@ -32,7 +29,6 @@ jobs: run: | set -euo pipefail image=public.ecr.aws/docker/library/debian:bullseye - # A published image is pinned by checksum in the build; never replace one by accident. if [ "$REPLACE" != true ] && gh release view "$TAG" -R "$GITHUB_REPOSITORY" --json assets -q '.assets[].name' 2>/dev/null | grep -qx debian-bullseye.tar.zst; then echo "::error::$TAG already carries debian-bullseye.tar.zst; publish to a new tag, or set replace and re-pin tools/linuxfs/debian-image.env" exit 1 diff --git a/.github/workflows/pr-release-cleanup.yml b/.github/workflows/pr-release-cleanup.yml index af4581f45..edb76a778 100644 --- a/.github/workflows/pr-release-cleanup.yml +++ b/.github/workflows/pr-release-cleanup.yml @@ -1,9 +1,9 @@ name: PR Release Cleanup -run-name: remove PR #${{ github.event.pull_request.number }}'s test build +run-name: remove PR + + + -# A merged or closed PR's test build leaves DroidDeck-CI, so the app's Updates page stops offering it -# (a merged fix is in the next Preview). pull_request_target for the publisher app's secrets; nothing -# from the PR is checked out or run. on: pull_request_target: types: [ closed ] @@ -32,7 +32,6 @@ jobs: set -euo pipefail state=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .state) echo "closed=$([ "$state" = closed ] && echo true || echo false)" >> "$GITHUB_OUTPUT" - # A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses. - uses: actions/create-github-app-token@v2 if: ${{ steps.state.outputs.closed == 'true' }} id: ci-token diff --git a/.github/workflows/pr-release-publish.yml b/.github/workflows/pr-release-publish.yml index 6c5070b27..b55d2a214 100644 --- a/.github/workflows/pr-release-publish.yml +++ b/.github/workflows/pr-release-publish.yml @@ -11,8 +11,6 @@ on: pull_request_target: types: [labeled] branches: [main] - # A PR from a branch of this repo is signed on its own after each green build: only people who - # can push here can change it. A fork's PR waits for a maintainer's publish-test label. workflow_run: workflows: [Build APK] types: [completed] @@ -53,7 +51,6 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - # Every variant a main build ships, so a PR build can be tried under any of them. - name: The packages a signed build ships as id: variants run: echo "list=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT" @@ -68,8 +65,6 @@ jobs: run: | set -euo pipefail [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid PR number '$PR_NUMBER'"; exit 1; } - # The automatic path stands down quietly, never failing: a draft, a PR labelled - # no-test-build, or a build that a later push has already overtaken. skip() { echo "::notice::Not publishing PR #$PR_NUMBER: $1"; echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; } if [ "$EVENT_NAME" = workflow_run ]; then echo "automatic=true" >> "$GITHUB_OUTPUT" @@ -94,9 +89,6 @@ jobs: head=$(jq -r '.head.sha' <<<"$pr") base=$(jq -r '.base.sha' <<<"$pr") - # The label is often added while the PR's build is still running, or just after a push - # before it has started: wait for the latest build of the head commit to finish (a PR - # build takes about 4 minutes) rather than fail and need the label added again. run_id= for attempt in $(seq 1 60); do latest=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml \ @@ -116,8 +108,6 @@ jobs: sleep 30 done [[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 30 minutes'; exit 1; } - # GitHub leaves a run's pull_requests empty when its head is in a fork, so the build is - # tied to this PR by its head repository, branch and commit instead. run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id") jq -e --arg head "$head" --arg repo "$(jq -r .head.repo.full_name <<<"$pr")" --arg ref "$(jq -r .head.ref <<<"$pr")" ' .event == "pull_request" and .head_sha == $head and .conclusion == "success" and @@ -127,8 +117,6 @@ jobs: sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}} tools=$(find "$sdk/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -1) - # A PR build is made from the PR merged onto main, so it must install over main's own - # build. main is what this job checked out; no release has to exist or be marked latest. main_code=$(sed -n 's/^[[:space:]]*versionCode[[:space:]][[:space:]]*\([0-9][0-9]*\)[[:space:]]*$/\1/p' app/build.gradle) [[ "$main_code" =~ ^[0-9]+$ ]] || { echo "::error::No numeric versionCode in main's app/build.gradle"; exit 1; } mkdir -p "$RUNNER_TEMP/pr" @@ -188,8 +176,6 @@ jobs: pattern: droiddeck-pr-signed-* merge-multiple: true path: signed - # What this build is, for the top of the release notes: the PR's title and the first - # paragraph of its description. - name: Describe the change env: GH_TOKEN: ${{ github.token }} @@ -208,7 +194,6 @@ jobs: fi } > notes-head.md cat notes-head.md - # A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses. - uses: actions/create-github-app-token@v2 id: ci-token with: @@ -250,7 +235,6 @@ jobs: echo "first=$first" >> "$GITHUB_OUTPUT" gh release upload "$tag" signed/* -R "$repo" --clobber gh release edit "$tag" -R "$repo" --title "PR #$PR_NUMBER" --notes "$notes" --prerelease - # Drop the previous build's files: anything this build did not just upload. keep=$(cd signed && ls | jq -R . | jq -sc .) gh api "repos/$repo/releases/tags/$tag" | jq -r --argjson keep "$keep" '.assets[] | select(.name as $n | $keep | index($n) | not) | .id' | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f443c710..b8fe92060 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,6 @@ jobs: artifact: droiddeck-release secrets: inherit - # The release is made here, from the apks this run signed, so it traces back to its Build APK run. publish: name: publish the release needs: [find, sign] @@ -94,7 +93,6 @@ jobs: if gh release view "$VERSION" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then echo "::error::release $VERSION already exists"; exit 1 fi - # The notes' first line is the title; media paths become raw URLs at this release's tag. title=$(sed -n '1s/^# *//p' "$notes") [ -n "$title" ] || title="DroidDeck $VERSION" raw="https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$VERSION/docs/releases/media/" @@ -110,7 +108,6 @@ jobs: --target "$GITHUB_SHA" --title "$title" --notes-file "$RUNNER_TEMP/body.md" --latest echo "Published [$title]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$VERSION)" >> "$GITHUB_STEP_SUMMARY" - # A release made with this run's token fires no release event, so the in-app catalog is refreshed here. catalog: needs: publish uses: ./.github/workflows/update-catalog.yml diff --git a/.github/workflows/update-catalog.yml b/.github/workflows/update-catalog.yml index d3a4075b5..93a74d5f9 100644 --- a/.github/workflows/update-catalog.yml +++ b/.github/workflows/update-catalog.yml @@ -20,7 +20,6 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - # Always use the current trusted generator, even when a release event points at an older tag. - uses: actions/checkout@v4 with: ref: main diff --git a/.gitignore b/.gitignore index 4293e3af4..e5bdbfffc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,24 +8,17 @@ captures/ .externalNativeBuild/ .cxx/ .kotlin/ -# Built in CI from tools/linuxfs and app/src/main/cpp/fakeinput.cpp, never committed, so the two -# copies cannot drift. app/src/main/assets/linuxfs/ app/src/main/jniLibs/arm64-v8a/libproot.so app/src/main/jniLibs/arm64-v8a/.proot-inputs app/src/main/jniLibs/arm64-v8a/.proot-outputs app/src/main/jniLibs/arm64-v8a/libproot-loader.so app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so -# The store download engines (app/src/main/rust/stores) are cross-compiled by Gradle into -# app/build/rustJniLibs; cargo's own build tree stays next to the crate. Neither is source. app/src/main/rust/stores/target/ app/src/main/assets/droiddeck-esync/ -# Kotlin compiler session files (build-tool state, never source). .kotlin/ -# Signing keys never enter the repo (DroidDeck's lives in GitHub environment secrets; see -# tools/release/make-release-key.sh). keystore/testkey.p12 is the public AOSP testkey. *.jks *.keystore *.p12 @@ -37,7 +30,6 @@ droiddeck-release-key/ password.txt __pycache__/ -# Local signing with the release key (tools/build_local.sh); holds a password. .signing.env app/src/main/assets/directaudio/ /tools/vr/out/ diff --git a/app/build.gradle b/app/build.gradle index f55175d3b..5b29d72f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,19 +49,12 @@ if (!(treeState in ['clean', 'dirty'])) { buildLabel = "${buildOrigin} ${treeState} · ${buildLabel}" def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"') -// What the in-app updater compares against the published builds: the commit this build is of (a PR -// build's head, not the merge CI makes of it), its PR, and whether CI made it at all. def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getOrNull() ?: providers.environmentVariable('GITHUB_SHA').getOrNull() ?: gitValue(['rev-parse', 'HEAD']) ?: '' def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' } def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true' -// The release key's certificate: a build signed with it, from CI or a computer, updates in place. def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim() -// When the source was committed, for "built 3 hours ago": fixed per commit, so it costs no rebuilds. def commitTime = (gitValue(['show', '-s', '--format=%ct', 'HEAD']) ?: '0').with { it.isLong() ? it : '0' } -// The desktop package this apk installs (tools/desktop-kde/release.env): published once per tag in -// DroidDeck-Components, pinned here with its checksum like gamescope and the esync packs, so the -// desktop scripts in the apk always get the package they were made for. def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines() .findAll { it.contains('=') && !it.startsWith('#') } .collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] } @@ -72,7 +65,6 @@ if (desktopRemovalSources.archives[desktopKde.DESKTOP_KDE_TAG] != desktopKde.DES if (!file('src/main/assets/desktop-removal/versions.txt').readLines().contains(desktopKde.DESKTOP_KDE_TAG)) { throw new GradleException('Regenerate desktop removal inventories for ' + desktopKde.DESKTOP_KDE_TAG) } -// tools/release/variants.txt as "package=suffix;...", so the updater picks the apk of the package it runs as. def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines() .findAll { it.trim() && !it.startsWith('#') } .collect { def f = it.trim().split(/\s+/); "${f[1]}=${f[2] == '-' ? '' : f[2]}" } @@ -81,16 +73,11 @@ def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines() android { namespace 'com.droiddeck.launcher' compileSdk 34 - // The NDK is pinned by property so CI can hand in whatever the runner already has rather than - // downloading a second copy; see gradle.properties. ndkVersion project.findProperty('ndkVersion') ?: '27.3.13750724' defaultConfig { applicationId "com.droiddeck.launcher" minSdkVersion 26 - // 28 is not a leftover. An app on targetSdk 29+ runs in a domain that may not execute a - // file out of its own data directory, and this app's entire job is to exec a rootfs: - // proot, gamescope, the Steam client and everything it launches all live under files/. targetSdkVersion 28 versionCode 13 versionName "0.4.0" @@ -125,10 +112,6 @@ android { } signingConfigs { - // AOSP testkey (android@android.com), the same key Bannerlator ships with. It is publicly - // known, so it proves nothing about who built the apk - what it buys is ONE stable - // signature: every build installs over the last one instead of asking the user to - // uninstall first. v1 (JAR) + v2 + v3 so every installer and Android version accepts it. testkey { storeFile file("${rootDir}/keystore/testkey.p12") storeType "PKCS12" @@ -153,24 +136,15 @@ android { } lint { - // targetSdk 28 is deliberate and structural: an app on 29+ may not execute a file out of - // its own data directory, and executing a rootfs is the whole of what this app does. Lint - // fails a release build over it because Google Play will not accept an sdk that old - - // true, and not something this app can trade away. Every other check stays on. disable 'ExpiredTargetSdkVersion' } packagingOptions { resources { - // Kadb's Bouncy Castle artifacts repeat the same multi-release JAR manifest. excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF'] } jniLibs { - // The PulseAudio daemon is executed from the native library directory and its - // libraries are read out of the apk by the class loader: both want the old, - // uncompressed-and-extracted layout. useLegacyPackaging = true - // Prefer the PTY helper built by this app's NDK target over Termux's prebuilt copy. pickFirsts += ['**/libtermux.so'] } } @@ -181,9 +155,7 @@ android { } testOptions { - // Plain JVM tests: a stray android.util.Log call returns a default instead of throwing. unitTests.returnDefaultValues = true - // Robolectric tests start activities whose text is now all string resources. unitTests.includeAndroidResources = true } @@ -196,8 +168,6 @@ android { } } -// Session scripts must follow this checkout even when the native asset bundle is cached. -// A stale local bundle can omit a newly required helper on a fresh runtime install. def stageSessionScripts = tasks.register('stageSessionScripts', Copy) { from(rootProject.file('tools/linuxfs/overlay')) { include 'usr/local/bin/droiddeck-*', 'usr/local/bin/steam-compatibility', 'usr/bin/**' @@ -206,8 +176,6 @@ def stageSessionScripts = tasks.register('stageSessionScripts', Copy) { } tasks.named('preBuild').configure { dependsOn(stageSessionScripts) } -// Prepared audio lives under build/, so native builds never rewrite the tracked base bundle. -// Copying into one asset root also prevents an old base bundle winning a duplicate asset merge. def runtimeAssetsDir = layout.buildDirectory.dir('runtimeAssets') def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) { dependsOn(stageSessionScripts) @@ -218,12 +186,7 @@ def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) { android.sourceSets.main.assets.setSrcDirs([runtimeAssetsDir]) tasks.named('preBuild').configure { dependsOn(stageRuntimeAssets) } -// Unit tests and previews remain usable without Docker. Packaging an APK requires the complete -// runtime payload prepared by the local helper or CI, with matching sources and output hashes. def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) { - // The session scripts are re-copied into the asset tree by stageSessionScripts; with parallel - // task execution this check once hashed a file mid-copy and failed a main build. Hash only - // once the staging has finished. mustRunAfter(stageSessionScripts, stageRuntimeAssets) workingDir rootProject.projectDir commandLine 'python3', 'tools/release/runtime_inputs.py', 'verify' @@ -232,14 +195,6 @@ tasks.configureEach { task -> if (task.name in ['packageDebug', 'packageRelease']) task.dependsOn(verifyRuntimeInputs) } -// The GOG / Epic / Amazon download engines are Rust (app/src/main/rust/stores, ported from -// Bannerlator's bl-steam-client crate). cargo-ndk cross-compiles them for arm64-v8a into the build -// directory, which is one more jniLibs source below: nothing compiled is committed, the same rule -// the preloads, proot and the DirectAudio relay follow. cargo-ndk exports the NDK clang/llvm-ar as -// the C compiler for the one C-compiling crate (ring, under rustls) - the step Bannerlator's CI -// does by hand - and reads the NDK from ANDROID_NDK_HOME, which is set to the NDK this build pins. -// -PskipRust=true builds without the library, for a checkout with no Rust toolchain: the stores -// then report themselves unavailable instead of the build failing. def rustCrateDir = layout.projectDirectory.dir('src/main/rust/stores') def rustJniLibsDir = layout.buildDirectory.dir('rustJniLibs') def skipRust = (project.findProperty('skipRust') ?: 'false').toString().toBoolean() @@ -255,9 +210,6 @@ def buildRustStores = tasks.register('buildRustStores', Exec) { }.standardOutput.asText) outputs.dir(rustJniLibsDir) workingDir rustCrateDir - // The configuration cache serialises the task, so everything it needs is resolved here, not - // in doFirst. ~/.cargo/bin is where rustup puts cargo and cargo-ndk; an IDE-launched Gradle - // does not always have it on PATH. def cargoBin = "${System.getProperty('user.home')}/.cargo/bin" environment 'PATH', "${cargoBin}${File.pathSeparator}${System.getenv('PATH')}" environment 'ANDROID_NDK_HOME', android.ndkDirectory.absolutePath @@ -265,47 +217,32 @@ def buildRustStores = tasks.register('buildRustStores', Exec) { if (!skipRust) environment 'RUSTC', providers.exec { commandLine 'rustup', 'which', '--toolchain', 'stable', 'rustc' }.standardOutput.asText.get().trim() - // --platform 26 = minSdkVersion: the engines link against the API 26 libc, like the C++ code. - // Homebrew's cargo/rustc can precede rustup on macOS and use a different sysroot, despite - // rustup reporting the Android target installed. Run both through the selected toolchain. commandLine 'rustup', 'run', 'stable', 'cargo', 'ndk', '--target', 'arm64-v8a', '--platform', '26', '--output-dir', rustJniLibsDir.get().asFile.absolutePath, 'build', '--release', '--locked' } -// Skipping Rust must omit its library even if another build left a cached .so behind. if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir) tasks.named('preBuild').configure { dependsOn(buildRustStores) } dependencies { - // The runtime tarball is a zstd-compressed tar. implementation 'com.github.luben:zstd-jni:1.5.2-3@aar' implementation 'org.apache.commons:commons-compress:1.27.1' - // Bannerlator's Windows components come as .tar.xz (WinComponents). implementation 'org.tukaani:xz:1.10' - // The UI: every menu, dialog and overlay is Compose. The session's SurfaceView and the - // on-screen pad are Views underneath it, which is what they are in every Compose app. implementation platform('androidx.compose:compose-bom:2024.02.00') implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.material3:material3' - // @Preview for the composables; the tooling that renders them is a debug-only dependency. implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' implementation 'androidx.activity:activity-compose:1.8.2' - // The File Manager, Bannerlator's: its icon set, image thumbnails and the coroutines it - // copies on. Same versions as Bannerlator so the screen stays a straight copy. implementation 'androidx.compose.material:material-icons-extended' implementation 'io.coil-kt:coil-compose:2.6.0' implementation 'com.caverock:androidsvg-aar:1.4' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' - // ADB-over-Wi-Fi pairing and authenticated shell client. Users still approve pairing from - // Android's Wireless debugging screen; DroidDeck only runs its one fixed setting command. implementation 'com.flyfishxu:kadb-android:1.2.1' - // Android terminal view/emulator used by the in-session secondary-display Linux shell. implementation 'com.termux.termux-app:terminal-view:0.118.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.16.1' testImplementation 'androidx.compose.ui:ui-test-junit4' - // The Android archive does not include the host natives used by JVM archive tests. testRuntimeOnly 'com.github.luben:zstd-jni:1.5.2-3@jar' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d833ca2ae..1518aed57 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,34 +4,19 @@ - - - - - - - - @@ -50,11 +35,6 @@ android:appCategory="game" android:isGame="true"> - - - - - - - - - SPIR-V translator (zlib licence), vendored under thirdparty/dxbc from the same -# subset upstream lsfg-vk uses. Native LSFG frame generation needs it: the Lossless Scaling -# shader chain is DXBC in every build downloadable from Steam. +# DXVK's DXBC->SPIR-V translator is zlib licensed (vendored under thirdparty/dxbc). add_subdirectory(thirdparty/dxbc) -# The Win-FG chain self-identifies by two halves: the upstream tag it was copied from (hand-set, -# UPDATE IT when re-copying) and a hash over the sources themselves, computed here, which cannot -# rot. The compositor's frame-generation leg (waylandcomp/CMakeLists.txt) stamps both in. set(WINFG_UPSTREAM "v0.3.0") set(WINFG_CHAIN_SRC framegen/winfg/framegen.cpp @@ -44,11 +33,7 @@ add_subdirectory(waylandcomp) add_library(sdnative SHARED sdnative/sdnative.c) target_link_libraries(sdnative log) -# The device info window's Vulkan section: the phone's own driver, asked once through the system -# loader (opened with dlopen, so a phone without Vulkan still loads the app). add_library(deviceinfo SHARED deviceinfo/vkinfo.c) target_link_libraries(deviceinfo dl) -# Rebuild Termux terminal-emulator's PTY JNI helper with the app's NDK instead of carrying its -# prebuilt libtermux.so with a redundant libstdc++.so dependency. add_library(termux SHARED termux/termux.c) diff --git a/app/src/main/cpp/adrenotools/build_asm.sh b/app/src/main/cpp/adrenotools/build_asm.sh index b53d25c1b..6f90a46a0 100755 --- a/app/src/main/cpp/adrenotools/build_asm.sh +++ b/app/src/main/cpp/adrenotools/build_asm.sh @@ -3,7 +3,6 @@ aarch64-linux-gnu-gcc -c src/bcenabler_patch.s -o /tmp/patch.o aarch64-linux-gnu-objcopy -O binary -j .text /tmp/patch.o /tmp/patch.bin -# Must be smaller than a single page bin2c -o gen/bcenabler_patch.h -n PatchRawData /tmp/patch.bin rm /tmp/patch.o diff --git a/app/src/main/cpp/adrenotools/gen/bcenabler_patch.h b/app/src/main/cpp/adrenotools/gen/bcenabler_patch.h index 3909a25ca..c68424524 100644 --- a/app/src/main/cpp/adrenotools/gen/bcenabler_patch.h +++ b/app/src/main/cpp/adrenotools/gen/bcenabler_patch.h @@ -1,6 +1,4 @@ -/* Generated by bin2c, do not edit manually */ -/* Contents of file /tmp/patch.bin */ const long int PatchRawData_size = 264; const unsigned char PatchRawData[264] = { 0x1F, 0x0C, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0xE0, 0x08, 0x80, 0x52, 0x3E, 0x00, 0x00, 0x14, diff --git a/app/src/main/cpp/adrenotools/include/adrenotools/bcenabler.h b/app/src/main/cpp/adrenotools/include/adrenotools/bcenabler.h index 3d3a76e7d..5f97f11c1 100644 --- a/app/src/main/cpp/adrenotools/include/adrenotools/bcenabler.h +++ b/app/src/main/cpp/adrenotools/include/adrenotools/bcenabler.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #ifdef __cplusplus @@ -11,29 +12,14 @@ extern "C" { #include -/** - * @brief Describes the level of BCeNabler support for a driver - */ enum adrenotools_bcn_type { - ADRENOTOOLS_BCN_INCOMPATIBLE, //!< Driver isn't supported by BCeNabler - ADRENOTOOLS_BCN_BLOB, //!< Driver already supports BCn textures so BCeNabler isn't necessary - ADRENOTOOLS_BCN_PATCH //!< Driver can be patched with BCeNabler to support BCn textures + ADRENOTOOLS_BCN_INCOMPATIBLE, + ADRENOTOOLS_BCN_BLOB, + ADRENOTOOLS_BCN_PATCH }; -/** - * @brief Checks the status of BCn support in the supplied driver - * @param major The major part of VkPhysicalDeviceProperties::driverVersion - * @param minor The minor part of VkPhysicalDeviceProperties::driverVersion - * @param vendorId VkPhysicalDeviceProperties::vendorId - */ enum adrenotools_bcn_type adrenotools_get_bcn_type(uint32_t major, uint32_t minor, uint32_t vendorId); -/** - * @brief Patches the Adreno graphics driver to enable support for BCn compressed formats - * @note adrenotools_get_bcn_type MUST be checked to equal ADRENOTOOLS_BCN_PATCH before calling this - * @param vkGetPhysicalDeviceFormatPropertiesFn A pointer to vkGetPhysicalDeviceFormatProperties obtained through vkGetInstanceProcAddr. This is used to find the correct function to patch - * @return If the patching succeeded, if false the driver will be in an undefined state - */ bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn); #ifdef __cplusplus diff --git a/app/src/main/cpp/adrenotools/include/adrenotools/driver.h b/app/src/main/cpp/adrenotools/include/adrenotools/driver.h index f81e8fe34..a81f98593 100644 --- a/app/src/main/cpp/adrenotools/include/adrenotools/driver.h +++ b/app/src/main/cpp/adrenotools/include/adrenotools/driver.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #ifdef __cplusplus @@ -10,60 +11,16 @@ extern "C" { #include #include "priv.h" -/** - * @brief Opens a new libvulkan.so instance according to `flags` - * @remark IMPORTANT: The app MUST be packaged w/ useLegacyPackaging = true. The reason for this is - * that we install the hook in hookLibDir; but this folder won't be set correctly when useLegacyPackaging == false - * because Android expects to read the *.so directly from the apk (instead of decompressing them into nativeLibraryDir). - * @param dlopenMode The dlopen mode to use when opening libvulkan - * @param featureFlags Which adrenotools driver features to enable - * @param tmpLibDir A writable directory to hold patched libraries, only used on api < 29 due to the lack of memfd support. If nullptr is passed and the API version is < 29 memfd usage will be attempted and if unsupported nullptr will be returned - * @param hookLibDir The directory holding the built hooks. - * IMPORTANT: This path MUST point to whatever `getApplicationInfo().nativeLibraryDir` (Kotlin code) returns. - * Failing to do so will result in this function returning a valid ptr but then the hook will fail and either the original driver is loaded as fallback, or vkEnumeratePhysicalDevices returns 0 devices. - * @param customDriverDir The directory to load a custom GPU driver named according to `customDriverName` from. Only used if ADRENOTOOLS_DRIVER_CUSTOM is set in `featureFlags`. - * IMPORTANT: This path MUST NOT be on sdcard/storage. i.e. use ANativeActivity::internalDataPath and often looks like "/data/user/0/com.example.app/files/". - * Otherwise you will get permission denied because dlopen() can't be done on *.so libraries that any random user or application can manipulate (which would be a security risk). - * @param customDriverName The soname of the custom driver to load. Only used if ADRENOTOOLS_DRIVER_CUSTOM is set in `featureFlags` - * @param fileRedirectDir The directory which to redirect all file accesses performed by the driver to. Only used if ADRENOTOOLS_DRIVER_FILE_REDIRECT is set in `featureFlags` - * @param userMappingHandle A pointer to a void* which will be set to the mapping handle if ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT is set in `featureFlags` - */ void *adrenotools_open_libvulkan(int dlopenMode, int featureFlags, const char *tmpLibDir, const char *hookLibDir, const char *customDriverDir, const char *customDriverName, const char *fileRedirectDir, void **userMappingHandle); -/** - * @brief Imports the given CPU mapped memory range into the GSL allocator. This should then be followed by a call to vkAllocateMemory with a matching size which will return a VkDeviceMemory view over the input region - * @param handle Mapping handle that was returned by adrenotools_open_libvulkan - * @param hostPtr The host pointer to import - * @param size The size of the region to import - * @return True on success - */ bool adrenotools_import_user_mem(void *handle, void *hostPtr, uint64_t size); -/** - * @brief Maps GPU memory and imports it into the GSL allocator. This should then be followed by a call to adrenotools_mem_cpu_map to map the memory on the CPU, then finally vkAllocateMemory with a matching size - * @param handle Mapping handle that was returned by adrenotools_open_libvulkan - * @param size Pointer to a variable containing the size of the region to import, will be updated to contain the required size of the region to allocate CPU side - * @return true on success - */ bool adrenotools_mem_gpu_allocate(void *handle, uint64_t *size); -/** - * @brief Maps the last mapping allocated using adrenotools_mem_gpu_allocate into the given host memory region, such that vkAllocateMemory can then be called - * @param handle Mapping handle that was returned by adrenotools_open_libvulkan - * @param hostPtr A pointer to where the mapping should be mapped - * @param size The size of the mapping. MUST be equal to the size returned by adrenotools_mem_gpu_allocate - */ bool adrenotools_mem_cpu_map(void *handle, void *hostPtr, uint64_t size); -/** - * @note This function should be called after adrenotools_open_libvulkan and Vulkan driver init to check if the mapping import hook loaded successfully - * @return True if the mapping was successfully imported (or initialization succeeded) - */ bool adrenotools_validate_gpu_mapping(void *handle); -/** - * @brief Provides a way to force the GPU to run at the maximum possible clocks (thermal constraints will still be applied) - */ void adrenotools_set_turbo(bool turbo); #ifdef __cplusplus diff --git a/app/src/main/cpp/adrenotools/include/adrenotools/priv.h b/app/src/main/cpp/adrenotools/include/adrenotools/priv.h index 742d24f24..77135d2c9 100644 --- a/app/src/main/cpp/adrenotools/include/adrenotools/priv.h +++ b/app/src/main/cpp/adrenotools/include/adrenotools/priv.h @@ -1,13 +1,11 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #include -/** - * @brief Bitfield enum of additional driver features that can be used with adrenotools_open_libvulkan - */ enum { ADRENOTOOLS_DRIVER_CUSTOM = 1 << 0, ADRENOTOOLS_DRIVER_FILE_REDIRECT = 1 << 1, @@ -16,12 +14,9 @@ enum { #define ADRENOTOOLS_GPU_MAPPING_SUCCEEDED_MAGIC 0xDEADBEEF -/** - * @brief A replacement GPU memory mapping for use with ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT - */ struct adrenotools_gpu_mapping { void *host_ptr; - uint64_t gpu_addr; //!< The GPU address of the mapping to import, if mapping import/init succeeds this will be set to ADRENOTOOLS_GPU_MAPPING_SUCCEEDED_MAGIC + uint64_t gpu_addr; uint64_t size; uint64_t flags; }; \ No newline at end of file diff --git a/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.cpp b/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.cpp index 3bd58992c..8220824eb 100644 --- a/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.cpp +++ b/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #include #include #include @@ -21,7 +22,6 @@ static loader_android_create_namespace_t loader_android_create_namespace; static bool lib_loaded; -/* Public API */ bool linkernsbypass_load_status() { return lib_loaded; } @@ -55,8 +55,6 @@ android_link_namespaces_all_libs_t android_link_namespaces_all_libs; android_link_namespaces_t android_link_namespaces; bool linkernsbypass_link_namespace_to_default_all_libs(android_namespace_t *to) { - // Creating a shared namespace with the default parent will give a copy of the default namespace that we can actually access - // This is needed since there is no way to access a direct handle to the default namespace as it's not exported static auto defaultNs{android_create_namespace_escape("default_copy", nullptr, nullptr, ANDROID_NAMESPACE_TYPE_SHARED, nullptr, nullptr)}; return android_link_namespaces_all_libs(to, defaultNs); } @@ -81,7 +79,6 @@ void *linkernsbypass_namespace_dlopen(const char *filename, int flags, android_n void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *libTargetDir, int flags, android_namespace_t *ns) { static std::array PathBuf{}; - // Used as a unique ID for overwriting soname and creating target lib files static uint16_t TargetId{}; int libTargetFd{[&] () { @@ -89,7 +86,6 @@ void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *li snprintf(PathBuf.data(), PathBuf.size(), "%s/%d_patched.so", libTargetDir, TargetId); return open(PathBuf.data(), O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); } else { - // If memfd isn't supported errno will contain ENOSYS after calling errno = 0; int fd{static_cast(syscall(__NR_memfd_create, libPath, 0))}; if (errno == ENOSYS || fd < 0) @@ -101,21 +97,18 @@ void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *li if (libTargetFd == -1) return nullptr; - // Partially overwrite soname with 3 digits (replacing lib...) with to make sure a cached so isn't loaded std::array sonameOverwrite{}; snprintf(sonameOverwrite.data(), sonameOverwrite.size(), "%03u", TargetId++); if (!elf_soname_patch(libPath, libTargetFd, sonameOverwrite.data())) return nullptr; - // Load our patched library into the hook namespace android_dlextinfo hookExtInfo{ .flags = ANDROID_DLEXT_USE_NAMESPACE | ANDROID_DLEXT_USE_LIBRARY_FD, .library_fd = libTargetFd, .library_namespace = ns }; - // Make a path that looks about right snprintf(PathBuf.data(), PathBuf.size(), "/proc/self/fd/%d", libTargetFd); return android_dlopen_ext(PathBuf.data(), flags, &hookExtInfo); @@ -125,21 +118,19 @@ static void *align_ptr(void *ptr) { return reinterpret_cast(reinterpret_cast(ptr) & ~(getpagesize() - 1)); } -/* Private */ __attribute__((constructor)) static void resolve_linker_symbols() { using loader_dlopen_t = void *(*)(const char *, int, const void *); if (android_get_device_api_level() < 28) return; - // ARM64 specific function walking to locate the internal dlopen handler auto loader_dlopen{[]() { union BranchLinked { uint32_t raw; struct { - int32_t offset : 26; //!< 26-bit branch offset - uint8_t sig : 6; //!< 6-bit signature + int32_t offset : 26; + uint8_t sig : 6; }; bool Verify() { @@ -148,10 +139,8 @@ __attribute__((constructor)) static void resolve_linker_symbols() { }; static_assert(sizeof(BranchLinked) == 4, "BranchLinked is wrong size"); - // Some devices ship with --X mapping for exexecutables so work around that mprotect(align_ptr(reinterpret_cast(&dlopen)), getpagesize(), PROT_WRITE | PROT_READ | PROT_EXEC); - // dlopen is just a wrapper for __loader_dlopen that passes the return address as the third arg hence we can just walk it to find __loader_dlopen auto blInstr{reinterpret_cast(&dlopen)}; while (!blInstr->Verify()) blInstr++; @@ -159,10 +148,8 @@ __attribute__((constructor)) static void resolve_linker_symbols() { return reinterpret_cast(blInstr + blInstr->offset); }()}; - // Protect the loader_dlopen function to remove the BTI attribute (since this is an internal function that isn't intended to be jumped indirectly to) mprotect(align_ptr(reinterpret_cast(&loader_dlopen)), getpagesize(), PROT_WRITE | PROT_READ | PROT_EXEC); - // Passing dlopen as a caller address tricks the linker into using the internal unrestricted namespace letting us access libraries that are normally forbidden in the classloader namespace imposed on apps auto ldHandle{loader_dlopen("ld-android.so", RTLD_LAZY, reinterpret_cast(&dlopen))}; if (!ldHandle) return; @@ -187,6 +174,5 @@ __attribute__((constructor)) static void resolve_linker_symbols() { if (!android_get_exported_namespace) return; - // Lib is now safe to use lib_loaded = true; } diff --git a/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.h b/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.h index 77ec4005a..cc775b0de 100644 --- a/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.h +++ b/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #ifdef __cplusplus @@ -9,7 +10,6 @@ extern "C" { #include -// https://cs.android.com/android/platform/superproject/+/0a492a4685377d41fef2b12e9af4ebfa6feef9c2:art/libnativeloader/include/nativeloader/dlext_namespaces.h;l=25;bpv=1;bpt=1 enum { ANDROID_NAMESPACE_TYPE_REGULAR = 0, ANDROID_NAMESPACE_TYPE_ISOLATED = 1, @@ -19,14 +19,8 @@ enum { ANDROID_NAMESPACE_TYPE_SHARED_ISOLATED = ANDROID_NAMESPACE_TYPE_SHARED | ANDROID_NAMESPACE_TYPE_ISOLATED, }; -/** - * @brief Checks if linkernsbypass loaded successfully and is safe to use - * @note IMPORTANT: This should be called before any calls to the rest of the library are made - * @return true if loading succeeded - */ bool linkernsbypass_load_status(); -// https://cs.android.com/android/platform/superproject/+/0a492a4685377d41fef2b12e9af4ebfa6feef9c2:art/libnativeloader/include/nativeloader/dlext_namespaces.h;l=86;bpv=1;bpt=1 struct android_namespace_t *android_create_namespace(const char *name, const char *ld_library_path, const char *default_library_path, @@ -41,39 +35,19 @@ struct android_namespace_t *android_create_namespace_escape(const char *name, const char *permitted_when_isolated_path, struct android_namespace_t *parent_namespace); -// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=3554 typedef struct android_namespace_t *(*android_get_exported_namespace_t)(const char *); extern android_get_exported_namespace_t android_get_exported_namespace; -// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=2499 typedef bool (*android_link_namespaces_all_libs_t)(struct android_namespace_t *, struct android_namespace_t *); extern android_link_namespaces_all_libs_t android_link_namespaces_all_libs; -// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=2473 typedef bool (*android_link_namespaces_t)(struct android_namespace_t *, struct android_namespace_t *, const char *); extern android_link_namespaces_t android_link_namespaces; -/** - * @brief Like android_link_namespaces_all_libs but links from the default namespace - */ bool linkernsbypass_link_namespace_to_default_all_libs(struct android_namespace_t *to); -/** - * @brief Loads a library into a namespace - * @note IMPORTANT: If `filename` is compiled with the '-z global' linker flag and RTLD_GLOBAL is supplied in `flags` the library will be added to the namespace's LD_PRELOAD list - * @param filename The name of the library to load - * @param flags The rtld flags for `filename` - * @param ns The namespace to dlopen into - */ void *linkernsbypass_namespace_dlopen(const char *filename, int flags, struct android_namespace_t *ns); -/** - * @brief Force loads a unique instance of a library into a namespace - * @param libPath The path to the library to load with hooks applied - * @param libTargetDir A temporary directory to hold the soname patched library at `libPath`, will attempt to use memfd if nullptr - * @param flags The rtld flags for `libName` - * @param ns The namespace to dlopen into - */ void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *libTargetDir, int flags, struct android_namespace_t *ns); #ifdef __cplusplus diff --git a/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.cpp b/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.cpp index db619ab53..cd5ca5c47 100644 --- a/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.cpp +++ b/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #include #include #include @@ -20,7 +21,6 @@ bool elf_soname_patch(const char *libPath, int targetFd, const char *sonamePatch if (ftruncate(targetFd, libStat.st_size) == -1) return false; - // Map the memory so we can read our elf into it auto mappedLib{reinterpret_cast(mmap(nullptr, libStat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, targetFd, 0))}; if (!mappedLib) return false; @@ -29,30 +29,25 @@ bool elf_soname_patch(const char *libPath, int targetFd, const char *sonamePatch if (libFd == false) return false; - // Read lib elf into target file if (read(libFd, mappedLib, libStat.st_size) != libStat.st_size) return false; - // No longer needed close(libFd); auto eHdr{reinterpret_cast(mappedLib)}; auto sHdrEntries{reinterpret_cast(mappedLib + eHdr->e_shoff)}; - // Iterate over section headers to find the .dynamic section for (ElfW(Half) i{}; i < eHdr->e_shnum; i++) { auto &sHdr{sHdrEntries[i]}; if (sHdr.sh_type == SHT_DYNAMIC) { auto strTab{reinterpret_cast(mappedLib + sHdrEntries[sHdr.sh_link].sh_offset)}; auto dynHdrEntries{reinterpret_cast(mappedLib + sHdr.sh_offset)}; - // Iterate over .dynamic entries to find DT_SONAME for (ElfW(Xword) k{}; k < (sHdr.sh_size / sHdr.sh_entsize); k++) { auto &dynHdrEntry{dynHdrEntries[k]}; if (dynHdrEntry.d_tag == DT_SONAME) { char *soname{strTab + dynHdrEntry.d_un.d_val}; - // Partially replace the old soname with the soname patch size_t charIdx{}; for (; soname[charIdx] != 0 && sonamePatch[charIdx] != 0; charIdx++) soname[charIdx] = sonamePatch[charIdx]; diff --git a/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.h b/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.h index 0b2c186ff..88ea58005 100644 --- a/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.h +++ b/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.h @@ -1,19 +1,13 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #ifdef __cplusplus extern "C" { #endif -/** - * @brief Overwrites a portion of the soname in an elf by loading it into shared memory and modifying .dynstr - * @note IMPORTANT: The supplied soname patch will overwrite the first strlen(sonamePatch) chars of the soname - * @param elfPath Full path to the elf to patch - * @param targetFd FD to use for storing the patched library - * @return True on success - */ bool elf_soname_patch(const char *elfPath, int targetFd, const char *newSoname); #ifdef __cplusplus diff --git a/app/src/main/cpp/adrenotools/lib/linkernsbypass/meson.build b/app/src/main/cpp/adrenotools/lib/linkernsbypass/meson.build index e666aff8a..12919c500 100644 --- a/app/src/main/cpp/adrenotools/lib/linkernsbypass/meson.build +++ b/app/src/main/cpp/adrenotools/lib/linkernsbypass/meson.build @@ -11,8 +11,3 @@ linkernsbypass_lib = static_library ( include_directories: ([linkernsbypass_inc, adrenotools_inc]), dependencies: ([libandroid_dep, libdl_dep]) ) - -# linkernsbypass_include = static_library('linkernsbypass', sources) -# target_compile_options(['linkernsbypass', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_libraries(['linkernsbypass', 'android', 'dl']) -# target_include_directories(['linkernsbypass', 'PUBLIC', '.']) diff --git a/app/src/main/cpp/adrenotools/meson.build b/app/src/main/cpp/adrenotools/meson.build index bf519bb2a..37404c268 100644 --- a/app/src/main/cpp/adrenotools/meson.build +++ b/app/src/main/cpp/adrenotools/meson.build @@ -33,18 +33,3 @@ adrenotools_lib = shared_library( ) subdir('src/hook') - -# target_include_directories(['adrenotools', 'PUBLIC', 'include']) -# target_include_directories(['adrenotools', 'PRIVATE', '.']) -# target_compile_options(['adrenotools', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_libraries(['adrenotools', 'android', 'linkernsbypass']) -# hooks_deb = subproject('src/hook') -# if build_shared_libs - # target_link_options(['adrenotools', 'PRIVATE', '-Wl,-s,--exclude-libs,liblinkernsbypass.a']) -# endif -# if gen_install_target - # configure_file(['adrenotools.pc.in', 'adrenotools.pc', '@ONLY']) - # install(['FILES', cmake_binary_dir, '/adrenotools.pc', 'DESTINATION', 'lib/pkgconfig']) - # install(['TARGETS', 'adrenotools', 'main_hook', 'file_redirect_hook', 'gsl_alloc_hook', 'hook_impl']) - # install(['DIRECTORY', 'include/', 'DESTINATION', 'include', 'FILES_MATCHING', 'PATTERN', '*.h*']) -# endif diff --git a/app/src/main/cpp/adrenotools/src/bcenabler.cpp b/app/src/main/cpp/adrenotools/src/bcenabler.cpp index 1deb41df4..6bf3e98dd 100644 --- a/app/src/main/cpp/adrenotools/src/bcenabler.cpp +++ b/app/src/main/cpp/adrenotools/src/bcenabler.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #include #include #include @@ -19,7 +20,6 @@ enum adrenotools_bcn_type adrenotools_get_bcn_type(uint32_t major, uint32_t mino return ADRENOTOOLS_BCN_PATCH; } -// Searches /proc/self/maps for the first free page after the given address static void *find_free_page(uintptr_t address) { std::ifstream procMaps("/proc/self/maps"); @@ -45,62 +45,48 @@ static void *align_ptr(void *ptr) { bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn) { union Branch { struct { - int32_t offset : 26; //!< 26-bit branch offset - uint8_t sig : 6; //!< 6-bit signature (0x25 for linked, 0x5 for jump) + int32_t offset : 26; + uint8_t sig : 6; }; uint32_t raw{}; }; static_assert(sizeof(Branch) == 4, "Branch size is invalid"); - // Find the nearest unmapped page where we can place patch code void *patchPage{find_free_page(reinterpret_cast(vkGetPhysicalDeviceFormatPropertiesFn))}; if (!patchPage) return false; - // Map patch region void *ptr{mmap(patchPage, getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0)}; if (ptr != patchPage) return false; - // Allow reading from the blob's .text section since some devices enable ---X - // Protect two pages just in case we happen to land on a page boundary if (mprotect(align_ptr(vkGetPhysicalDeviceFormatPropertiesFn), getpagesize() * 2, PROT_WRITE | PROT_READ | PROT_EXEC)) return false; - // First branch in this function is targeted at the function we want to patch Branch *blInst{reinterpret_cast(vkGetPhysicalDeviceFormatPropertiesFn)}; constexpr uint8_t BranchLinkSignature{0x25}; - // Search for first instruction with the BL signature while (blInst->sig != BranchLinkSignature) blInst++; - // Internal QGL format conversion function that we need to patch uint32_t *convFormatFn{reinterpret_cast(blInst) + blInst->offset}; - // See mprotect call above - // This time we also set PROT_WRITE so we can write our patch to the page if (mprotect(align_ptr(convFormatFn), getpagesize() * 2, PROT_WRITE | PROT_READ | PROT_EXEC)) return false; - // This would normally set the default result to 0 (error) in the format not found case constexpr uint32_t ClearResultSignature{0x2a1f03e0}; - // We replace it with a branch to our own extended if statement which adds in the extra things for BCn uint32_t *clearResultPtr{convFormatFn}; while (*clearResultPtr != ClearResultSignature) clearResultPtr++; - // Ensure we don't write out of bounds if (PatchRawData_size > getpagesize()) return false; - // Copy the patch function to our mapped page memcpy(patchPage, PatchRawData, PatchRawData_size); - // Fixup the patch code so it correctly returns back to the driver after running constexpr uint32_t PatchReturnFixupMagic{0xffffffff}; constexpr uint8_t BranchSignature{0x5}; @@ -130,6 +116,5 @@ bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn) { asm volatile("ISB"); - // Done! return true; } diff --git a/app/src/main/cpp/adrenotools/src/bcenabler_patch.s b/app/src/main/cpp/adrenotools/src/bcenabler_patch.s index 8766e7880..99fab59bc 100644 --- a/app/src/main/cpp/adrenotools/src/bcenabler_patch.s +++ b/app/src/main/cpp/adrenotools/src/bcenabler_patch.s @@ -80,4 +80,4 @@ .L18: mov w0, 0 .L3: - .word 0xffffffff // Branch fixup + .word 0xffffffff diff --git a/app/src/main/cpp/adrenotools/src/driver.cpp b/app/src/main/cpp/adrenotools/src/driver.cpp index 581e04a67..848beae8c 100644 --- a/app/src/main/cpp/adrenotools/src/driver.cpp +++ b/app/src/main/cpp/adrenotools/src/driver.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #include #include #include @@ -17,17 +18,14 @@ #include void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *tmpLibDir, const char *hookLibDir, const char *customDriverDir, const char *customDriverName, const char *fileRedirectDir, void **userMappingHandle) { - // Bail out if linkernsbypass failed to load, this probably means we're on api < 28 if (!linkernsbypass_load_status()) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Could not load linkernsbypass\n"); return nullptr; } - // Use memfd on Q+ since it's guaranteed to work, but only if tmpLibDir wasn't set if (android_get_device_api_level() >= 29 && !tmpLibDir) tmpLibDir = nullptr; - // Verify that params for specific features are only passed if they are enabled if (!(featureFlags & ADRENOTOOLS_DRIVER_FILE_REDIRECT) && fileRedirectDir) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "ADRENOTOOLS_DRIVER_FILE_REDIRECT present but no file redirect folder found\n"); return nullptr; @@ -43,7 +41,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char * return nullptr; } - // Verify that params for enabled features are correct struct stat buf{}; if (featureFlags & ADRENOTOOLS_DRIVER_CUSTOM) { @@ -58,7 +55,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char * } } - // Verify that params for enabled features are correct if (featureFlags & ADRENOTOOLS_DRIVER_FILE_REDIRECT) { if (!fileRedirectDir) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "ADRENOTOOLS_DRIVER_REDIRECT_DIR present but no folder parameter was found\n"); @@ -71,22 +67,18 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char * } } - // Create a namespace that can isolate our hook from the classloader namespace auto hookNs{android_create_namespace("adrenotools-libvulkan", hookLibDir, nullptr, ANDROID_NAMESPACE_TYPE_SHARED, nullptr, nullptr)}; - // Link it to the default namespace so the hook can use libandroid etc if (!linkernsbypass_link_namespace_to_default_all_libs(hookNs)) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Failed to hook our namespace to the default one\n"); return nullptr; } - // Preload the hook implementation, otherwise we get a weird issue where despite being in NEEDED of the hook lib the hook's symbols will overwrite ours and cause an infinite loop auto hookImpl{linkernsbypass_namespace_dlopen("libhook_impl.so", RTLD_NOW, hookNs)}; if (!hookImpl) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Couldn't preload the hook implementation\n"); return nullptr; } - // Pass parameters to the hook implementation auto initHookParam{reinterpret_cast(dlsym(hookImpl, "init_hook_param"))}; if (!initHookParam) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Couldn't init hook params\n"); @@ -95,7 +87,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char * auto importMapping{[&]() -> adrenotools_gpu_mapping * { if (featureFlags & ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT) { - // This will be leaked, but it's not a big deal since it's only a few bytes adrenotools_gpu_mapping *mapping{new adrenotools_gpu_mapping{}}; *userMappingHandle = mapping; return mapping; @@ -107,7 +98,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char * initHookParam(new HookImplParams(featureFlags, tmpLibDir, hookLibDir, customDriverDir, customDriverName, fileRedirectDir, importMapping)); - // Load the libvulkan hook into the isolated namespace if (!linkernsbypass_namespace_dlopen("libmain_hook.so", RTLD_GLOBAL, hookNs)) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Failed to load libvulkan into the isolated namespace\n"); return nullptr; @@ -150,7 +140,7 @@ bool adrenotools_import_user_mem(void *handle, void *hostPtr, uint64_t size) { importMapping->host_ptr = hostPtr; importMapping->gpu_addr = info.gpuaddr; importMapping->size = size; - importMapping->flags = 0xc2600; //!< Unknown flags, but they are required for the mapping to work + importMapping->flags = 0xc2600; close(kgslFd); return true; @@ -189,7 +179,7 @@ bool adrenotools_mem_gpu_allocate(void *handle, uint64_t *size) { mapping->host_ptr = nullptr; mapping->gpu_addr = info.gpuaddr; mapping->size = *size; - mapping->flags = 0xc2600; //!< Unknown flags, but they are required for the mapping to work + mapping->flags = 0xc2600; close(kgslFd); return true; diff --git a/app/src/main/cpp/adrenotools/src/hook/hook_impl.cpp b/app/src/main/cpp/adrenotools/src/hook/hook_impl.cpp index 50e79a946..8fa80ee18 100644 --- a/app/src/main/cpp/adrenotools/src/hook/hook_impl.cpp +++ b/app/src/main/cpp/adrenotools/src/hook/hook_impl.cpp @@ -15,7 +15,7 @@ #define TAG "hook_impl" #define LOGI(fmt, ...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##__VA_ARGS__) -const HookImplParams *hook_params; //!< Bunch of info needed to load/patch the driver +const HookImplParams *hook_params; int (*gsl_memory_alloc_pure_sym)(uint32_t, uint32_t, void *); int (*gsl_memory_alloc_pure_64_sym)(uint64_t, uint32_t, void *); int (*gsl_memory_free_pure_sym)(void *); @@ -43,7 +43,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char LOGI("hook_android_dlopen_ext: filename: %s", filename); - // Ignore non-vulkan libraries if (!strstr(filename, "vulkan.")) return android_dlopen_ext(filename, flags, extinfo); @@ -52,7 +51,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char return fallback(); } - // customDriverDir will be empty if ADRENOTOOLS_DRIVER_CUSTOM isn't set therefore it's fine to have either way auto driverNs{android_create_namespace(filename, hook_params->customDriverDir.c_str(), hook_params->hookLibDir.c_str(), ANDROID_NAMESPACE_TYPE_SHARED, nullptr, extinfo->library_namespace)}; @@ -61,16 +59,12 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char return fallback(); } - // We depend on libandroid which is unlikely to be in the supplied driver namespace so we have to link it over android_link_namespaces(driverNs, nullptr, "libandroid.so"); - // Preload ourself, a new instance will be created since we have different linker ancestory - // If we don't preload we get a weird issue where despite being in NEEDED of the hook lib the hook's symbols will overwrite ours and cause an infinite loop auto hookImpl{linkernsbypass_namespace_dlopen("libhook_impl.so", RTLD_NOW, driverNs)}; if (!hookImpl) return nullptr; - // Pass parameters to ourself auto initHookParam{reinterpret_cast(dlsym(hookImpl, "init_hook_param"))}; if (!initHookParam) return nullptr; @@ -86,7 +80,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char LOGI("hook_android_dlopen_ext: applied libfile_redirect_hook"); } - // Use our new namespace to load the vulkan driver auto newExtinfo{*extinfo}; newExtinfo.library_namespace = driverNs; @@ -122,8 +115,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char LOGI("hook_android_dlopen_ext: hook failed: failed to apply libgsl_alloc_hook!"); } - // TODO: If there is already an instance of a Vulkan driver loaded hooks won't be applied, this will only be the case for skiavk generally - // To fix this we would need to search /proc/self/maps for the file to a loaded instance of the library in order to read it to patch the soname and load it uniquely if (hook_params->featureFlags & ADRENOTOOLS_DRIVER_CUSTOM) { LOGI("hook_android_dlopen_ext: loading custom driver: %s%s", hook_params->customDriverDir.c_str(), hook_params->customDriverName.c_str()); void *handle{android_dlopen_ext(hook_params->customDriverName.c_str(), flags, &newExtinfo)}; @@ -142,7 +133,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char __attribute__((visibility("default"))) void *hook_android_load_sphal_library(const char *filename, int flags) { LOGI("hook_android_load_sphal_library: filename: %s", filename); - // https://android.googlesource.com/platform/system/core/+/master/libvndksupport/linker.cpp for (const char *name : {"sphal", "vendor", "default"}) { if (auto vendorNs{android_get_exported_namespace(name)}) { android_dlextinfo dlextinfo{ diff --git a/app/src/main/cpp/adrenotools/src/hook/hook_impl.h b/app/src/main/cpp/adrenotools/src/hook/hook_impl.h index 2ce2471e1..77be0fd71 100644 --- a/app/src/main/cpp/adrenotools/src/hook/hook_impl.h +++ b/app/src/main/cpp/adrenotools/src/hook/hook_impl.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #ifdef __cplusplus diff --git a/app/src/main/cpp/adrenotools/src/hook/hook_impl_params.h b/app/src/main/cpp/adrenotools/src/hook/hook_impl_params.h index 8a3bd1673..7dbabd7b7 100644 --- a/app/src/main/cpp/adrenotools/src/hook/hook_impl_params.h +++ b/app/src/main/cpp/adrenotools/src/hook/hook_impl_params.h @@ -1,15 +1,12 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #include #include -/** - * @brief Holds the parameters needed for all hooks - * @note See comments for adrenotools_open_libvulkan as a reference for member variables - */ struct HookImplParams { int featureFlags; std::string tmpLibDir; diff --git a/app/src/main/cpp/adrenotools/src/hook/kgsl.h b/app/src/main/cpp/adrenotools/src/hook/kgsl.h index 6d4d93d6a..fa99b1d34 100644 --- a/app/src/main/cpp/adrenotools/src/hook/kgsl.h +++ b/app/src/main/cpp/adrenotools/src/hook/kgsl.h @@ -3,50 +3,27 @@ * Copyright (c) 2018-2021, The Linux Foundation. All rights reserved. * Copyright (c) 2022 Qualcomm Innovation Center, Inc. All rights reserved. */ - #ifndef _UAPI_MSM_KGSL_H #define _UAPI_MSM_KGSL_H #include #include -/* - * The KGSL version has proven not to be very useful in userspace if features - * are cherry picked into other trees out of order so it is frozen as of 3.14. - * It is left here for backwards compatabilty and as a reminder that - * software releases are never linear. Also, I like pie. - */ #define KGSL_VERSION_MAJOR 3 #define KGSL_VERSION_MINOR 14 -/* - * We have traditionally mixed context and issueibcmds / command batch flags - * together into a big flag stew. This worked fine until we started adding a - * lot more command batch flags and we started running out of bits. Turns out - * we have a bit of room in the context type / priority mask that we could use - * for command batches, but that means we need to split out the flags into two - * coherent sets. - * - * If any future definitions are for both context and cmdbatch add both defines - * and link the cmdbatch to the context define as we do below. Otherwise feel - * free to add exclusive bits to either set. - */ -/* --- context flags --- */ #define KGSL_CONTEXT_SAVE_GMEM 0x00000001 #define KGSL_CONTEXT_NO_GMEM_ALLOC 0x00000002 -/* This is a cmdbatch exclusive flag - use the CMDBATCH equivalent instead */ #define KGSL_CONTEXT_SUBMIT_IB_LIST 0x00000004 #define KGSL_CONTEXT_CTX_SWITCH 0x00000008 #define KGSL_CONTEXT_PREAMBLE 0x00000010 #define KGSL_CONTEXT_TRASH_STATE 0x00000020 #define KGSL_CONTEXT_PER_CONTEXT_TS 0x00000040 #define KGSL_CONTEXT_USER_GENERATED_TS 0x00000080 -/* This is a cmdbatch exclusive flag - use the CMDBATCH equivalent instead */ #define KGSL_CONTEXT_END_OF_FRAME 0x00000100 #define KGSL_CONTEXT_NO_FAULT_TOLERANCE 0x00000200 -/* This is a cmdbatch exclusive flag - use the CMDBATCH equivalent instead */ #define KGSL_CONTEXT_SYNC 0x00000400 #define KGSL_CONTEXT_PWR_CONSTRAINT 0x00000800 #define KGSL_CONTEXT_PRIORITY_MASK 0x0000F000 @@ -80,70 +57,43 @@ #define KGSL_CONTEXT_INVALID 0xffffffff -/* - * --- command batch flags --- - * The bits that are linked to a KGSL_CONTEXT equivalent are either legacy - * definitions or bits that are valid for both contexts and cmdbatches. To be - * safe the other 8 bits that are still available in the context field should be - * omitted here in case we need to share - the other bits are available for - * cmdbatch only flags as needed - */ #define KGSL_CMDBATCH_MEMLIST 0x00000001 #define KGSL_CMDBATCH_MARKER 0x00000002 -#define KGSL_CMDBATCH_SUBMIT_IB_LIST KGSL_CONTEXT_SUBMIT_IB_LIST /* 0x004 */ -#define KGSL_CMDBATCH_CTX_SWITCH KGSL_CONTEXT_CTX_SWITCH /* 0x008 */ +#define KGSL_CMDBATCH_SUBMIT_IB_LIST KGSL_CONTEXT_SUBMIT_IB_LIST +#define KGSL_CMDBATCH_CTX_SWITCH KGSL_CONTEXT_CTX_SWITCH #define KGSL_CMDBATCH_PROFILING 0x00000010 -/* - * KGSL_CMDBATCH_PROFILING must also be set for KGSL_CMDBATCH_PROFILING_KTIME - * to take effect, as the latter only affects the time data returned. - */ #define KGSL_CMDBATCH_PROFILING_KTIME 0x00000020 -#define KGSL_CMDBATCH_END_OF_FRAME KGSL_CONTEXT_END_OF_FRAME /* 0x100 */ -#define KGSL_CMDBATCH_SYNC KGSL_CONTEXT_SYNC /* 0x400 */ -#define KGSL_CMDBATCH_PWR_CONSTRAINT KGSL_CONTEXT_PWR_CONSTRAINT /* 0x800 */ -#define KGSL_CMDBATCH_SPARSE 0x1000 /* 0x1000 */ -/* RECURRING bits must be set for LSR workload with IOCTL_KGSL_RECURRING_COMMAND. */ +#define KGSL_CMDBATCH_END_OF_FRAME KGSL_CONTEXT_END_OF_FRAME +#define KGSL_CMDBATCH_SYNC KGSL_CONTEXT_SYNC +#define KGSL_CMDBATCH_PWR_CONSTRAINT KGSL_CONTEXT_PWR_CONSTRAINT +#define KGSL_CMDBATCH_SPARSE 0x1000 + #define KGSL_CMDBATCH_START_RECURRING 0x00100000 #define KGSL_CMDBATCH_STOP_RECURRING 0x00200000 -/* - * Reserve bits [16:19] and bits [28:31] for possible bits shared between - * contexts and command batches. Update this comment as new flags are added. - */ -/* - * gpu_command_object flags - these flags communicate the type of command or - * memory object being submitted for a GPU command - */ -/* Flags for GPU command objects */ #define KGSL_CMDLIST_IB 0x00000001U #define KGSL_CMDLIST_CTXTSWITCH_PREAMBLE 0x00000002U #define KGSL_CMDLIST_IB_PREAMBLE 0x00000004U -/* Flags for GPU command memory objects */ #define KGSL_OBJLIST_MEMOBJ 0x00000008U #define KGSL_OBJLIST_PROFILE 0x00000010U -/* Flags for GPU command sync points */ #define KGSL_CMD_SYNCPOINT_TYPE_TIMESTAMP 0 #define KGSL_CMD_SYNCPOINT_TYPE_FENCE 1 #define KGSL_CMD_SYNCPOINT_TYPE_TIMELINE 2 -/* --- Memory allocation flags --- */ -/* General allocation hints */ #define KGSL_MEMFLAGS_SECURE (1ULL << 3) #define KGSL_MEMFLAGS_GPUREADONLY (1ULL << 24) #define KGSL_MEMFLAGS_GPUWRITEONLY (1ULL << 25) #define KGSL_MEMFLAGS_FORCE_32BIT (1ULL << 32) -/* Flag for binding all the virt range to single phys data */ #define KGSL_SPARSE_BIND_MULTIPLE_TO_PHYS 0x400000000ULL #define KGSL_SPARSE_BIND 0x1ULL #define KGSL_SPARSE_UNBIND 0x2ULL -/* Memory caching hints */ #define KGSL_CACHEMODE_MASK 0x0C000000U #define KGSL_CACHEMODE_SHIFT 26 @@ -159,7 +109,6 @@ #define KGSL_MEMFLAGS_GUARD_PAGE (1ULL << 33) #define KGSL_MEMFLAGS_VBO (1ULL << 34) -/* Memory types for which allocations are made */ #define KGSL_MEMTYPE_MASK 0x0000FF00 #define KGSL_MEMTYPE_SHIFT 8 @@ -186,10 +135,6 @@ #define KGSL_MEMTYPE_MULTISAMPLE 20 #define KGSL_MEMTYPE_KERNEL 255 -/* - * Alignment hint, passed as the power of 2 exponent. - * i.e 4k (2^12) would be 12, 64k (2^16)would be 16. - */ #define KGSL_MEMALIGN_MASK 0x00FF0000 #define KGSL_MEMALIGN_SHIFT 16 @@ -198,22 +143,12 @@ enum kgsl_user_mem_type { KGSL_USER_MEM_TYPE_ASHMEM = 0x00000001, KGSL_USER_MEM_TYPE_ADDR = 0x00000002, KGSL_USER_MEM_TYPE_ION = 0x00000003, - /* - * ION type is retained for backwards compatibility but Ion buffers are - * dma-bufs so try to use that naming if we can - */ KGSL_USER_MEM_TYPE_DMABUF = 0x00000003, KGSL_USER_MEM_TYPE_MAX = 0x00000007, }; #define KGSL_MEMFLAGS_USERMEM_MASK 0x000000e0 #define KGSL_MEMFLAGS_USERMEM_SHIFT 5 -/* - * Unfortunately, enum kgsl_user_mem_type starts at 0 which does not - * leave a good value for allocated memory. In the flags we use - * 0 to indicate allocated memory and thus need to add 1 to the enum - * values. - */ #define KGSL_USERMEM_FLAG(x) (((x) + 1) << KGSL_MEMFLAGS_USERMEM_SHIFT) #define KGSL_MEMFLAGS_NOT_USERMEM 0 @@ -223,7 +158,6 @@ enum kgsl_user_mem_type { #define KGSL_MEMFLAGS_USERMEM_ADDR KGSL_USERMEM_FLAG(KGSL_USER_MEM_TYPE_ADDR) #define KGSL_MEMFLAGS_USERMEM_ION KGSL_USERMEM_FLAG(KGSL_USER_MEM_TYPE_ION) -/* --- generic KGSL flag values --- */ #define KGSL_FLAGS_NORMALMODE 0x00000000 #define KGSL_FLAGS_SAFEMODE 0x00000001 @@ -237,19 +171,14 @@ enum kgsl_user_mem_type { #define KGSL_FLAGS_SOFT_RESET 0x00000100 #define KGSL_FLAGS_PER_CONTEXT_TIMESTAMPS 0x00000200 -/* Server Side Sync Timeout in milliseconds */ #define KGSL_SYNCOBJ_SERVER_TIMEOUT 2000 -/* UBWC Modes */ #define KGSL_UBWC_NONE 0 #define KGSL_UBWC_1_0 1 #define KGSL_UBWC_2_0 2 #define KGSL_UBWC_3_0 3 #define KGSL_UBWC_4_0 4 -/* - * Reset status values for context - */ enum kgsl_ctx_reset_stat { KGSL_CTX_STAT_NO_ERROR = 0x00000000, KGSL_CTX_STAT_GUILTY_CONTEXT_RESET_EXT = 0x00000001, @@ -263,38 +192,13 @@ enum kgsl_ctx_reset_stat { struct kgsl_devinfo { unsigned int device_id; - /* - * chip revision id - * coreid:8 majorrev:8 minorrev:8 patch:8 - */ unsigned int chip_id; unsigned int mmu_enabled; unsigned long gmem_gpubaseaddr; - /* - * This field contains the adreno revision - * number 200, 205, 220, etc... - */ unsigned int gpu_id; __kernel_size_t gmem_sizebytes; }; -/* - * struct kgsl_devmemstore - this structure defines the region of memory - * that can be mmap()ed from this driver. The timestamp fields are volatile - * because they are written by the GPU - * @soptimestamp: Start of pipeline timestamp written by GPU before the - * commands in concern are processed - * @sbz: Unused, kept for 8 byte alignment - * @eoptimestamp: End of pipeline timestamp written by GPU after the - * commands in concern are processed - * @sbz2: Unused, kept for 8 byte alignment - * @preempted: Indicates if the context was preempted - * @sbz3: Unused, kept for 8 byte alignment - * @ref_wait_ts: Timestamp on which to generate interrupt, unused now. - * @sbz4: Unused, kept for 8 byte alignment - * @current_context: The current context the GPU is working on - * @sbz5: Unused, kept for 8 byte alignment - */ struct kgsl_devmemstore { volatile unsigned int soptimestamp; unsigned int sbz; @@ -312,14 +216,12 @@ struct kgsl_devmemstore { ((ctxt_id)*sizeof(struct kgsl_devmemstore) + \ offsetof(struct kgsl_devmemstore, field)) -/* timestamp id*/ enum kgsl_timestamp_type { - KGSL_TIMESTAMP_CONSUMED = 0x00000001, /* start-of-pipeline timestamp */ - KGSL_TIMESTAMP_RETIRED = 0x00000002, /* end-of-pipeline timestamp*/ + KGSL_TIMESTAMP_CONSUMED = 0x00000001, + KGSL_TIMESTAMP_RETIRED = 0x00000002, KGSL_TIMESTAMP_QUEUED = 0x00000003, }; -/* property types - used with kgsl_device_getproperty */ #define KGSL_PROP_DEVICE_INFO 0x1 #define KGSL_PROP_DEVICE_SHADOW 0x2 #define KGSL_PROP_DEVICE_POWER 0x3 @@ -352,30 +254,13 @@ enum kgsl_timestamp_type { #define KGSL_PROP_VK_DEVICE_ID 0x2A #define KGSL_PROP_IS_LPAC_ENABLED 0x2B -/* - * kgsl_capabilities_properties returns a list of supported properties. - * If the user passes 0 for 'count' the kernel will set it to the number of - * supported properties. The list is expected to be 'count * sizeof(__u32)' - * bytes long. The kernel will return the actual number of entries copied into - * list via 'count'. - */ struct kgsl_capabilities_properties { __u64 list; __u32 count; }; -/* - * KGSL_QUERY_CAPS_PROPERTIES returns a list of the valid properties in the - * kernel. The subtype data should be struct kgsl_capabilities_properties - */ #define KGSL_QUERY_CAPS_PROPERTIES 1 -/* - * kgsl_capabilities allows the user to query kernel capabilities. The 'data' - * type should be set appropriately for the querytype (see above). Pass 0 to - * 'size' and the kernel will set it to the expected size of 'data' that is - * appropriate for querytype (in bytes). - */ struct kgsl_capabilities { __u64 data; __u64 size; @@ -385,7 +270,7 @@ struct kgsl_capabilities { struct kgsl_shadowprop { unsigned long gpuaddr; __kernel_size_t size; - unsigned int flags; /* contains KGSL_FLAGS_ values */ + unsigned int flags; }; struct kgsl_qdss_stm_prop { @@ -437,10 +322,8 @@ struct kgsl_gpu_model { char gpu_model[32]; }; -/* Context property sub types */ #define KGSL_CONTEXT_PROP_FAULTS 1 -/* Performance counter groups */ #define KGSL_PERFCOUNTER_GROUP_CP 0x0 #define KGSL_PERFCOUNTER_GROUP_RBBM 0x1 @@ -502,7 +385,6 @@ struct kgsl_gpu_model { #define KGSL_PERFCOUNTER_NOT_USED 0xFFFFFFFF #define KGSL_PERFCOUNTER_BROKEN 0xFFFFFFFE -/* structure holds list of ibs */ struct kgsl_ibdesc { unsigned long gpuaddr; unsigned long __pad; @@ -510,21 +392,6 @@ struct kgsl_ibdesc { unsigned int ctrl; }; -/** - * struct kgsl_cmdbatch_profiling_buffer - * @wall_clock_s: Ringbuffer submission time (seconds). - * If KGSL_CMDBATCH_PROFILING_KTIME is set, time is provided - * in kernel clocks, otherwise wall clock time is used. - * @wall_clock_ns: Ringbuffer submission time (nanoseconds). - * If KGSL_CMDBATCH_PROFILING_KTIME is set time is provided - * in kernel clocks, otherwise wall clock time is used. - * @gpu_ticks_queued: GPU ticks at ringbuffer submission - * @gpu_ticks_submitted: GPU ticks when starting cmdbatch execution - * @gpu_ticks_retired: GPU ticks when finishing cmdbatch execution - * - * This structure defines the profiling buffer used to measure cmdbatch - * execution time - */ struct kgsl_cmdbatch_profiling_buffer { __u64 wall_clock_s; __u64 wall_clock_ns; @@ -533,20 +400,8 @@ struct kgsl_cmdbatch_profiling_buffer { __u64 gpu_ticks_retired; }; -/* ioctls */ #define KGSL_IOC_TYPE 0x09 -/* - * get misc info about the GPU - * type should be a value from enum kgsl_property_type - * value points to a structure that varies based on type - * sizebytes is sizeof() that structure - * for KGSL_PROP_DEVICE_INFO, use struct kgsl_devinfo - * this structure contaings hardware versioning info. - * for KGSL_PROP_DEVICE_SHADOW, use struct kgsl_shadowprop - * this is used to find mmap() offset and sizes for mapping - * struct kgsl_memstore into userspace. - */ struct kgsl_device_getproperty { unsigned int type; void __user *value; @@ -556,12 +411,7 @@ struct kgsl_device_getproperty { #define IOCTL_KGSL_DEVICE_GETPROPERTY \ _IOWR(KGSL_IOC_TYPE, 0x2, struct kgsl_device_getproperty) -/* IOCTL_KGSL_DEVICE_READ (0x3) - removed 03/2012 - */ -/* block until the GPU has executed past a given timestamp - * timeout is in milliseconds. - */ struct kgsl_device_waittimestamp { unsigned int timestamp; unsigned int timeout; @@ -579,35 +429,20 @@ struct kgsl_device_waittimestamp_ctxtid { #define IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID \ _IOW(KGSL_IOC_TYPE, 0x7, struct kgsl_device_waittimestamp_ctxtid) -/* DEPRECATED: issue indirect commands to the GPU. - * drawctxt_id must have been created with IOCTL_KGSL_DRAWCTXT_CREATE - * ibaddr and sizedwords must specify a subset of a buffer created - * with IOCTL_KGSL_SHAREDMEM_FROM_PMEM - * flags may be a mask of KGSL_CONTEXT_ values - * timestamp is a returned counter value which can be passed to - * other ioctls to determine when the commands have been executed by - * the GPU. - * - * This function is deprecated - consider using IOCTL_KGSL_SUBMIT_COMMANDS - * instead - */ struct kgsl_ringbuffer_issueibcmds { unsigned int drawctxt_id; unsigned long ibdesc_addr; unsigned int numibs; - unsigned int timestamp; /*output param */ + unsigned int timestamp; unsigned int flags; }; #define IOCTL_KGSL_RINGBUFFER_ISSUEIBCMDS \ _IOWR(KGSL_IOC_TYPE, 0x10, struct kgsl_ringbuffer_issueibcmds) -/* read the most recently executed timestamp value - * type should be a value from enum kgsl_timestamp_type - */ struct kgsl_cmdstream_readtimestamp { unsigned int type; - unsigned int timestamp; /*output param */ + unsigned int timestamp; }; #define IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_OLD \ @@ -616,11 +451,6 @@ struct kgsl_cmdstream_readtimestamp { #define IOCTL_KGSL_CMDSTREAM_READTIMESTAMP \ _IOWR(KGSL_IOC_TYPE, 0x11, struct kgsl_cmdstream_readtimestamp) -/* free memory when the GPU reaches a given timestamp. - * gpuaddr specify a memory region created by a - * IOCTL_KGSL_SHAREDMEM_FROM_PMEM call - * type should be a value from enum kgsl_timestamp_type - */ struct kgsl_cmdstream_freememontimestamp { unsigned long gpuaddr; unsigned int type; @@ -630,28 +460,18 @@ struct kgsl_cmdstream_freememontimestamp { #define IOCTL_KGSL_CMDSTREAM_FREEMEMONTIMESTAMP \ _IOW(KGSL_IOC_TYPE, 0x12, struct kgsl_cmdstream_freememontimestamp) -/* - * Previous versions of this header had incorrectly defined - * IOCTL_KGSL_CMDSTREAM_FREEMEMONTIMESTAMP as a read-only ioctl instead - * of a write only ioctl. To ensure binary compatibility, the following - * #define will be used to intercept the incorrect ioctl - */ #define IOCTL_KGSL_CMDSTREAM_FREEMEMONTIMESTAMP_OLD \ _IOR(KGSL_IOC_TYPE, 0x12, struct kgsl_cmdstream_freememontimestamp) -/* create a draw context, which is used to preserve GPU state. - * The flags field may contain a mask KGSL_CONTEXT_* values - */ struct kgsl_drawctxt_create { unsigned int flags; - unsigned int drawctxt_id; /*output param */ + unsigned int drawctxt_id; }; #define IOCTL_KGSL_DRAWCTXT_CREATE \ _IOWR(KGSL_IOC_TYPE, 0x13, struct kgsl_drawctxt_create) -/* destroy a draw context */ struct kgsl_drawctxt_destroy { unsigned int drawctxt_id; }; @@ -659,16 +479,12 @@ struct kgsl_drawctxt_destroy { #define IOCTL_KGSL_DRAWCTXT_DESTROY \ _IOW(KGSL_IOC_TYPE, 0x14, struct kgsl_drawctxt_destroy) -/* - * add a block of pmem, fb, ashmem or user allocated address - * into the GPU address space - */ struct kgsl_map_user_mem { int fd; - unsigned long gpuaddr; /*output param */ + unsigned long gpuaddr; __kernel_size_t len; __kernel_size_t offset; - unsigned long hostptr; /*input param */ + unsigned long hostptr; enum kgsl_user_mem_type memtype; unsigned int flags; }; @@ -679,7 +495,7 @@ struct kgsl_map_user_mem { struct kgsl_cmdstream_readtimestamp_ctxtid { unsigned int context_id; unsigned int type; - unsigned int timestamp; /*output param */ + unsigned int timestamp; }; #define IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID \ @@ -696,10 +512,9 @@ struct kgsl_cmdstream_freememontimestamp_ctxtid { _IOW(KGSL_IOC_TYPE, 0x17, \ struct kgsl_cmdstream_freememontimestamp_ctxtid) -/* add a block of pmem or fb into the GPU address space */ struct kgsl_sharedmem_from_pmem { int pmem_fd; - unsigned long gpuaddr; /*output param */ + unsigned long gpuaddr; unsigned int len; unsigned int offset; }; @@ -707,7 +522,6 @@ struct kgsl_sharedmem_from_pmem { #define IOCTL_KGSL_SHAREDMEM_FROM_PMEM \ _IOWR(KGSL_IOC_TYPE, 0x20, struct kgsl_sharedmem_from_pmem) -/* remove memory from the GPU's address space */ struct kgsl_sharedmem_free { unsigned long gpuaddr; }; @@ -757,15 +571,10 @@ struct kgsl_bind_gmem_shadow { #define IOCTL_KGSL_DRAWCTXT_BIND_GMEM_SHADOW \ _IOW(KGSL_IOC_TYPE, 0x22, struct kgsl_bind_gmem_shadow) -/* add a block of memory into the GPU address space */ -/* - * IOCTL_KGSL_SHAREDMEM_FROM_VMALLOC deprecated 09/2012 - * use IOCTL_KGSL_GPUMEM_ALLOC instead - */ struct kgsl_sharedmem_from_vmalloc { - unsigned long gpuaddr; /*output param */ + unsigned long gpuaddr; unsigned int hostptr; unsigned int flags; }; @@ -773,13 +582,6 @@ struct kgsl_sharedmem_from_vmalloc { #define IOCTL_KGSL_SHAREDMEM_FROM_VMALLOC \ _IOWR(KGSL_IOC_TYPE, 0x23, struct kgsl_sharedmem_from_vmalloc) -/* - * This is being deprecated in favor of IOCTL_KGSL_GPUMEM_CACHE_SYNC which - * supports both directions (flush and invalidate). This code will still - * work, but by definition it will do a flush of the cache which might not be - * what you want to have happen on a buffer following a GPU operation. It is - * safer to go with IOCTL_KGSL_GPUMEM_CACHE_SYNC - */ #define IOCTL_KGSL_SHAREDMEM_FLUSH_CACHE \ _IOW(KGSL_IOC_TYPE, 0x24, struct kgsl_sharedmem_free) @@ -795,13 +597,12 @@ struct kgsl_drawctxt_set_bin_base_offset { enum kgsl_cmdwindow_type { KGSL_CMDWINDOW_MIN = 0x00000000, KGSL_CMDWINDOW_2D = 0x00000000, - KGSL_CMDWINDOW_3D = 0x00000001, /* legacy */ + KGSL_CMDWINDOW_3D = 0x00000001, KGSL_CMDWINDOW_MMU = 0x00000002, KGSL_CMDWINDOW_ARBITER = 0x000000FF, KGSL_CMDWINDOW_MAX = 0x000000FF, }; -/* write to the command window */ struct kgsl_cmdwindow_write { enum kgsl_cmdwindow_type target; unsigned int addr; @@ -812,7 +613,7 @@ struct kgsl_cmdwindow_write { _IOW(KGSL_IOC_TYPE, 0x2e, struct kgsl_cmdwindow_write) struct kgsl_gpumem_alloc { - unsigned long gpuaddr; /* output param */ + unsigned long gpuaddr; __kernel_size_t size; unsigned int flags; }; @@ -823,49 +624,38 @@ struct kgsl_gpumem_alloc { struct kgsl_cff_syncmem { unsigned long gpuaddr; __kernel_size_t len; - unsigned int __pad[2]; /* For future binary compatibility */ + unsigned int __pad[2]; }; #define IOCTL_KGSL_CFF_SYNCMEM \ _IOW(KGSL_IOC_TYPE, 0x30, struct kgsl_cff_syncmem) -/* - * A timestamp event allows the user space to register an action following an - * expired timestamp. Note IOCTL_KGSL_TIMESTAMP_EVENT has been redefined to - * _IOWR to support fences which need to return a fd for the priv parameter. - */ struct kgsl_timestamp_event { - int type; /* Type of event (see list below) */ - unsigned int timestamp; /* Timestamp to trigger event on */ - unsigned int context_id; /* Context for the timestamp */ - void __user *priv; /* Pointer to the event specific blob */ - __kernel_size_t len; /* Size of the event specific blob */ + int type; + unsigned int timestamp; + unsigned int context_id; + void __user *priv; + __kernel_size_t len; }; #define IOCTL_KGSL_TIMESTAMP_EVENT_OLD \ _IOW(KGSL_IOC_TYPE, 0x31, struct kgsl_timestamp_event) -/* A genlock timestamp event releases an existing lock on timestamp expire */ #define KGSL_TIMESTAMP_EVENT_GENLOCK 1 struct kgsl_timestamp_event_genlock { - int handle; /* Handle of the genlock lock to release */ + int handle; }; -/* A fence timestamp event releases an existing lock on timestamp expire */ #define KGSL_TIMESTAMP_EVENT_FENCE 2 struct kgsl_timestamp_event_fence { - int fence_fd; /* Fence to signal */ + int fence_fd; }; -/* - * Set a property within the kernel. Uses the same structure as - * IOCTL_KGSL_GETPROPERTY - */ #define IOCTL_KGSL_SETPROPERTY \ _IOW(KGSL_IOC_TYPE, 0x32, struct kgsl_device_getproperty) @@ -873,70 +663,26 @@ struct kgsl_timestamp_event_fence { #define IOCTL_KGSL_TIMESTAMP_EVENT \ _IOWR(KGSL_IOC_TYPE, 0x33, struct kgsl_timestamp_event) -/** - * struct kgsl_gpumem_alloc_id - argument to IOCTL_KGSL_GPUMEM_ALLOC_ID - * @id: returned id value for this allocation. - * @flags: mask of KGSL_MEM* values requested and actual flags on return. - * @size: requested size of the allocation and actual size on return. - * @mmapsize: returned size to pass to mmap() which may be larger than 'size' - * @gpuaddr: returned GPU address for the allocation - * - * Allocate memory for access by the GPU. The flags and size fields are echoed - * back by the kernel, so that the caller can know if the request was - * adjusted. - * - * Supported flags: - * KGSL_MEMFLAGS_GPUREADONLY: the GPU will be unable to write to the buffer - * KGSL_MEMTYPE*: usage hint for debugging aid - * KGSL_MEMALIGN*: alignment hint, may be ignored or adjusted by the kernel. - * KGSL_MEMFLAGS_USE_CPU_MAP: If set on call and return, the returned GPU - * address will be 0. Calling mmap() will set the GPU address. - */ struct kgsl_gpumem_alloc_id { unsigned int id; unsigned int flags; __kernel_size_t size; __kernel_size_t mmapsize; unsigned long gpuaddr; -/* private: reserved for future use*/ unsigned long __pad[2]; }; #define IOCTL_KGSL_GPUMEM_ALLOC_ID \ _IOWR(KGSL_IOC_TYPE, 0x34, struct kgsl_gpumem_alloc_id) -/** - * struct kgsl_gpumem_free_id - argument to IOCTL_KGSL_GPUMEM_FREE_ID - * @id: GPU allocation id to free - * - * Free an allocation by id, in case a GPU address has not been assigned or - * is unknown. Freeing an allocation by id with this ioctl or by GPU address - * with IOCTL_KGSL_SHAREDMEM_FREE are equivalent. - */ struct kgsl_gpumem_free_id { unsigned int id; -/* private: reserved for future use*/ unsigned int __pad; }; #define IOCTL_KGSL_GPUMEM_FREE_ID \ _IOWR(KGSL_IOC_TYPE, 0x35, struct kgsl_gpumem_free_id) -/** - * struct kgsl_gpumem_get_info - argument to IOCTL_KGSL_GPUMEM_GET_INFO - * @gpuaddr: GPU address to query. Also set on return. - * @id: GPU allocation id to query. Also set on return. - * @flags: returned mask of KGSL_MEM* values. - * @size: returned size of the allocation. - * @mmapsize: returned size to pass mmap(), which may be larger than 'size' - * @useraddr: returned address of the userspace mapping for this buffer - * - * This ioctl allows querying of all user visible attributes of an existing - * allocation, by either the GPU address or the id returned by a previous - * call to IOCTL_KGSL_GPUMEM_ALLOC_ID. Legacy allocation ioctls may not - * return all attributes so this ioctl can be used to look them up if needed. - * - */ struct kgsl_gpumem_get_info { unsigned long gpuaddr; unsigned int id; @@ -944,27 +690,12 @@ struct kgsl_gpumem_get_info { __kernel_size_t size; __kernel_size_t mmapsize; unsigned long useraddr; -/* private: reserved for future use*/ unsigned long __pad[4]; }; #define IOCTL_KGSL_GPUMEM_GET_INFO\ _IOWR(KGSL_IOC_TYPE, 0x36, struct kgsl_gpumem_get_info) -/** - * struct kgsl_gpumem_sync_cache - argument to IOCTL_KGSL_GPUMEM_SYNC_CACHE - * @gpuaddr: GPU address of the buffer to sync. - * @id: id of the buffer to sync. Either gpuaddr or id is sufficient. - * @op: a mask of KGSL_GPUMEM_CACHE_* values - * @offset: offset into the buffer - * @length: number of bytes starting from offset to perform - * the cache operation on - * - * Sync the L2 cache for memory headed to and from the GPU - this replaces - * KGSL_SHAREDMEM_FLUSH_CACHE since it can handle cache management for both - * directions - * - */ struct kgsl_gpumem_sync_cache { unsigned long gpuaddr; unsigned int id; @@ -982,101 +713,45 @@ struct kgsl_gpumem_sync_cache { #define KGSL_GPUMEM_CACHE_FLUSH \ (KGSL_GPUMEM_CACHE_CLEAN | KGSL_GPUMEM_CACHE_INV) -/* Flag to ensure backwards compatibility of kgsl_gpumem_sync_cache struct */ #define KGSL_GPUMEM_CACHE_RANGE (1 << 31U) #define IOCTL_KGSL_GPUMEM_SYNC_CACHE \ _IOW(KGSL_IOC_TYPE, 0x37, struct kgsl_gpumem_sync_cache) -/** - * struct kgsl_perfcounter_get - argument to IOCTL_KGSL_PERFCOUNTER_GET - * @groupid: Performance counter group ID - * @countable: Countable to select within the group - * @offset: Return offset of the reserved LO counter - * @offset_hi: Return offset of the reserved HI counter - * - * Get an available performance counter from a specified groupid. The offset - * of the performance counter will be returned after successfully assigning - * the countable to the counter for the specified group. An error will be - * returned and an offset of 0 if the groupid is invalid or there are no - * more counters left. After successfully getting a perfcounter, the user - * must call kgsl_perfcounter_put(groupid, contable) when finished with - * the perfcounter to clear up perfcounter resources. - * - */ struct kgsl_perfcounter_get { unsigned int groupid; unsigned int countable; unsigned int offset; unsigned int offset_hi; -/* private: reserved for future use */ - unsigned int __pad; /* For future binary compatibility */ + + unsigned int __pad; }; #define IOCTL_KGSL_PERFCOUNTER_GET \ _IOWR(KGSL_IOC_TYPE, 0x38, struct kgsl_perfcounter_get) -/** - * struct kgsl_perfcounter_put - argument to IOCTL_KGSL_PERFCOUNTER_PUT - * @groupid: Performance counter group ID - * @countable: Countable to release within the group - * - * Put an allocated performance counter to allow others to have access to the - * resource that was previously taken. This is only to be called after - * successfully getting a performance counter from kgsl_perfcounter_get(). - * - */ struct kgsl_perfcounter_put { unsigned int groupid; unsigned int countable; -/* private: reserved for future use */ - unsigned int __pad[2]; /* For future binary compatibility */ + + unsigned int __pad[2]; }; #define IOCTL_KGSL_PERFCOUNTER_PUT \ _IOW(KGSL_IOC_TYPE, 0x39, struct kgsl_perfcounter_put) -/** - * struct kgsl_perfcounter_query - argument to IOCTL_KGSL_PERFCOUNTER_QUERY - * @groupid: Performance counter group ID - * @countable: Return active countables array - * @size: Size of active countables array - * @max_counters: Return total number counters for the group ID - * - * Query the available performance counters given a groupid. The array - * *countables is used to return the current active countables in counters. - * The size of the array is passed in so the kernel will only write at most - * size or counter->size for the group id. The total number of available - * counters for the group ID is returned in max_counters. - * If the array or size passed in are invalid, then only the maximum number - * of counters will be returned, no data will be written to *countables. - * If the groupid is invalid an error code will be returned. - * - */ struct kgsl_perfcounter_query { unsigned int groupid; - /* Array to return the current countable for up to size counters */ unsigned int __user *countables; unsigned int count; unsigned int max_counters; -/* private: reserved for future use */ - unsigned int __pad[2]; /* For future binary compatibility */ + + unsigned int __pad[2]; }; #define IOCTL_KGSL_PERFCOUNTER_QUERY \ _IOWR(KGSL_IOC_TYPE, 0x3A, struct kgsl_perfcounter_query) -/** - * struct kgsl_perfcounter_query - argument to IOCTL_KGSL_PERFCOUNTER_QUERY - * @groupid: Performance counter group IDs - * @countable: Performance counter countable IDs - * @value: Return performance counter reads - * @size: Size of all arrays (groupid/countable pair and return value) - * - * Read in the current value of a performance counter given by the groupid - * and countable. - * - */ struct kgsl_perfcounter_read_group { unsigned int groupid; @@ -1087,44 +762,23 @@ struct kgsl_perfcounter_read_group { struct kgsl_perfcounter_read { struct kgsl_perfcounter_read_group __user *reads; unsigned int count; -/* private: reserved for future use */ - unsigned int __pad[2]; /* For future binary compatibility */ + + unsigned int __pad[2]; }; #define IOCTL_KGSL_PERFCOUNTER_READ \ _IOWR(KGSL_IOC_TYPE, 0x3B, struct kgsl_perfcounter_read) -/* - * struct kgsl_gpumem_sync_cache_bulk - argument to - * IOCTL_KGSL_GPUMEM_SYNC_CACHE_BULK - * @id_list: list of GPU buffer ids of the buffers to sync - * @count: number of GPU buffer ids in id_list - * @op: a mask of KGSL_GPUMEM_CACHE_* values - * - * Sync the cache for memory headed to and from the GPU. Certain - * optimizations can be made on the cache operation based on the total - * size of the working set of memory to be managed. - */ struct kgsl_gpumem_sync_cache_bulk { unsigned int __user *id_list; unsigned int count; unsigned int op; -/* private: reserved for future use */ - unsigned int __pad[2]; /* For future binary compatibility */ + + unsigned int __pad[2]; }; #define IOCTL_KGSL_GPUMEM_SYNC_CACHE_BULK \ _IOWR(KGSL_IOC_TYPE, 0x3C, struct kgsl_gpumem_sync_cache_bulk) -/* - * struct kgsl_cmd_syncpoint_timestamp - * @context_id: ID of a KGSL context - * @timestamp: GPU timestamp - * - * This structure defines a syncpoint comprising a context/timestamp pair. A - * list of these may be passed by IOCTL_KGSL_SUBMIT_COMMANDS to define - * dependencies that must be met before the command can be submitted to the - * hardware - */ struct kgsl_cmd_syncpoint_timestamp { unsigned int context_id; unsigned int timestamp; @@ -1134,60 +788,22 @@ struct kgsl_cmd_syncpoint_fence { int fd; }; -/* - * struct kgsl_cmd_syncpoint_timeline - * @timelines: Address of an array of &struct kgsl_timeline_val - * @count: Number of entries in @timelines - * @timelines_size: Size of each entry in @timelines - * - * Define a syncpoint for a number of timelines. This syncpoint will - * be satisfied when all of the specified timelines are signaled. - */ struct kgsl_cmd_syncpoint_timeline { __u64 timelines; __u32 count; __u32 timelines_size; }; -/** - * struct kgsl_cmd_syncpoint - Define a sync point for a command batch - * @type: type of sync point defined here - * @priv: Pointer to the type specific buffer - * @size: Size of the type specific buffer - * - * This structure contains pointers defining a specific command sync point. - * The pointer and size should point to a type appropriate structure. - */ struct kgsl_cmd_syncpoint { int type; void __user *priv; __kernel_size_t size; }; -/* Flag to indicate that the cmdlist may contain memlists */ #define KGSL_IBDESC_MEMLIST 0x1 -/* Flag to point out the cmdbatch profiling buffer in the memlist */ #define KGSL_IBDESC_PROFILING_BUFFER 0x2 -/** - * struct kgsl_submit_commands - Argument to IOCTL_KGSL_SUBMIT_COMMANDS - * @context_id: KGSL context ID that owns the commands - * @flags: - * @cmdlist: User pointer to a list of kgsl_ibdesc structures - * @numcmds: Number of commands listed in cmdlist - * @synclist: User pointer to a list of kgsl_cmd_syncpoint structures - * @numsyncs: Number of sync points listed in synclist - * @timestamp: On entry the a user defined timestamp, on exist the timestamp - * assigned to the command batch - * - * This structure specifies a command to send to the GPU hardware. This is - * similar to kgsl_issueibcmds expect that it doesn't support the legacy way to - * submit IB lists and it adds sync points to block the IB until the - * dependencies are satisified. This entry point is the new and preferred way - * to submit commands to the GPU. The memory list can be used to specify all - * memory that is referrenced in the current set of commands. - */ struct kgsl_submit_commands { unsigned int context_id; @@ -1197,20 +813,12 @@ struct kgsl_submit_commands { struct kgsl_cmd_syncpoint __user *synclist; unsigned int numsyncs; unsigned int timestamp; -/* private: reserved for future use */ unsigned int __pad[4]; }; #define IOCTL_KGSL_SUBMIT_COMMANDS \ _IOWR(KGSL_IOC_TYPE, 0x3D, struct kgsl_submit_commands) -/** - * struct kgsl_device_constraint - device constraint argument - * @context_id: KGSL context ID - * @type: type of constraint i.e pwrlevel/none - * @data: constraint data - * @size: size of the constraint data - */ struct kgsl_device_constraint { unsigned int type; unsigned int context_id; @@ -1218,103 +826,55 @@ struct kgsl_device_constraint { __kernel_size_t size; }; -/* Constraint Type*/ #define KGSL_CONSTRAINT_NONE 0 #define KGSL_CONSTRAINT_PWRLEVEL 1 -/* L3 constraint Type */ #define KGSL_CONSTRAINT_L3_NONE 2 #define KGSL_CONSTRAINT_L3_PWRLEVEL 3 -/* PWRLEVEL constraint level*/ -/* set to min frequency */ #define KGSL_CONSTRAINT_PWR_MIN 0 -/* set to max frequency */ #define KGSL_CONSTRAINT_PWR_MAX 1 struct kgsl_device_constraint_pwrlevel { unsigned int level; }; -/** - * struct kgsl_syncsource_create - Argument to IOCTL_KGSL_SYNCSOURCE_CREATE - * @id: returned id for the syncsource that was created. - * - * This ioctl creates a userspace sync timeline. - */ struct kgsl_syncsource_create { unsigned int id; -/* private: reserved for future use */ unsigned int __pad[3]; }; #define IOCTL_KGSL_SYNCSOURCE_CREATE \ _IOWR(KGSL_IOC_TYPE, 0x40, struct kgsl_syncsource_create) -/** - * struct kgsl_syncsource_destroy - Argument to IOCTL_KGSL_SYNCSOURCE_DESTROY - * @id: syncsource id to destroy - * - * This ioctl creates a userspace sync timeline. - */ struct kgsl_syncsource_destroy { unsigned int id; -/* private: reserved for future use */ unsigned int __pad[3]; }; #define IOCTL_KGSL_SYNCSOURCE_DESTROY \ _IOWR(KGSL_IOC_TYPE, 0x41, struct kgsl_syncsource_destroy) -/** - * struct kgsl_syncsource_create_fence - Argument to - * IOCTL_KGSL_SYNCSOURCE_CREATE_FENCE - * @id: syncsource id - * @fence_fd: returned sync_fence fd - * - * Create a fence that may be signaled by userspace by calling - * IOCTL_KGSL_SYNCSOURCE_SIGNAL_FENCE. There are no order dependencies between - * these fences. - */ struct kgsl_syncsource_create_fence { unsigned int id; int fence_fd; -/* private: reserved for future use */ unsigned int __pad[4]; }; -/** - * struct kgsl_syncsource_signal_fence - Argument to - * IOCTL_KGSL_SYNCSOURCE_SIGNAL_FENCE - * @id: syncsource id - * @fence_fd: sync_fence fd to signal - * - * Signal a fence that was created by a IOCTL_KGSL_SYNCSOURCE_CREATE_FENCE - * call using the same syncsource id. This allows a fence to be shared - * to other processes but only signaled by the process owning the fd - * used to create the fence. - */ #define IOCTL_KGSL_SYNCSOURCE_CREATE_FENCE \ _IOWR(KGSL_IOC_TYPE, 0x42, struct kgsl_syncsource_create_fence) struct kgsl_syncsource_signal_fence { unsigned int id; int fence_fd; -/* private: reserved for future use */ unsigned int __pad[4]; }; #define IOCTL_KGSL_SYNCSOURCE_SIGNAL_FENCE \ _IOWR(KGSL_IOC_TYPE, 0x43, struct kgsl_syncsource_signal_fence) -/** - * struct kgsl_cff_sync_gpuobj - Argument to IOCTL_KGSL_CFF_SYNC_GPUOBJ - * @offset: Offset into the GPU object to sync - * @length: Number of bytes to sync - * @id: ID of the GPU object to sync - */ struct kgsl_cff_sync_gpuobj { __u64 offset; __u64 length; @@ -1324,16 +884,6 @@ struct kgsl_cff_sync_gpuobj { #define IOCTL_KGSL_CFF_SYNC_GPUOBJ \ _IOW(KGSL_IOC_TYPE, 0x44, struct kgsl_cff_sync_gpuobj) -/** - * struct kgsl_gpuobj_alloc - Argument to IOCTL_KGSL_GPUOBJ_ALLOC - * @size: Size in bytes of the object to allocate - * @flags: mask of KGSL_MEMFLAG_* bits - * @va_len: Size in bytes of the virtual region to allocate - * @mmapsize: Returns the mmap() size of the object - * @id: Returns the GPU object ID of the new object - * @metadata_len: Length of the metdata to copy from the user - * @metadata: Pointer to the user specified metadata to store for the object - */ struct kgsl_gpuobj_alloc { __u64 size; __u64 flags; @@ -1344,22 +894,11 @@ struct kgsl_gpuobj_alloc { __u64 metadata; }; -/* Let the user know that this header supports the gpuobj metadata */ #define KGSL_GPUOBJ_ALLOC_METADATA_MAX 64 #define IOCTL_KGSL_GPUOBJ_ALLOC \ _IOWR(KGSL_IOC_TYPE, 0x45, struct kgsl_gpuobj_alloc) -/** - * struct kgsl_gpuobj_free - Argument to IOCTL_KGLS_GPUOBJ_FREE - * @flags: Mask of: KGSL_GUPOBJ_FREE_ON_EVENT - * @priv: Pointer to the private object if KGSL_GPUOBJ_FREE_ON_EVENT is - * specified - * @id: ID of the GPU object to free - * @type: If KGSL_GPUOBJ_FREE_ON_EVENT is specified, the type of asynchronous - * event to free on - * @len: Length of the data passed in priv - */ struct kgsl_gpuobj_free { __u64 flags; __u64 __user priv; @@ -1373,21 +912,11 @@ struct kgsl_gpuobj_free { #define KGSL_GPU_EVENT_TIMESTAMP 1 #define KGSL_GPU_EVENT_FENCE 2 -/** - * struct kgsl_gpu_event_timestamp - Specifies a timestamp event to free a GPU - * object on - * @context_id: ID of the timestamp event to wait for - * @timestamp: Timestamp of the timestamp event to wait for - */ struct kgsl_gpu_event_timestamp { unsigned int context_id; unsigned int timestamp; }; -/** - * struct kgsl_gpu_event_fence - Specifies a fence ID to to free a GPU object on - * @fd: File descriptor for the fence - */ struct kgsl_gpu_event_fence { int fd; }; @@ -1395,15 +924,6 @@ struct kgsl_gpu_event_fence { #define IOCTL_KGSL_GPUOBJ_FREE \ _IOW(KGSL_IOC_TYPE, 0x46, struct kgsl_gpuobj_free) -/** - * struct kgsl_gpuobj_info - argument to IOCTL_KGSL_GPUOBJ_INFO - * @gpuaddr: GPU address of the object - * @flags: Current flags for the object - * @size: Size of the object - * @va_len: VA size of the object - * @va_addr: Virtual address of the object (if it is mapped) - * id - GPU object ID of the object to query - */ struct kgsl_gpuobj_info { __u64 gpuaddr; __u64 flags; @@ -1417,14 +937,6 @@ struct kgsl_gpuobj_info { _IOWR(KGSL_IOC_TYPE, 0x47, struct kgsl_gpuobj_info) -/** - * struct kgsl_gpuobj_import - argument to IOCTL_KGSL_GPUOBJ_IMPORT - * @priv: Pointer to the private data for the import type - * @priv_len: Length of the private data - * @flags: Mask of KGSL_MEMFLAG_ flags - * @type: Type of the import (KGSL_USER_MEM_TYPE_*) - * @id: Returns the ID of the new GPU object - */ struct kgsl_gpuobj_import { __u64 __user priv; __u64 priv_len; @@ -1433,18 +945,10 @@ struct kgsl_gpuobj_import { unsigned int id; }; -/** - * struct kgsl_gpuobj_import_dma_buf - import a dmabuf object - * @fd: File descriptor for the dma-buf object - */ struct kgsl_gpuobj_import_dma_buf { int fd; }; -/** - * struct kgsl_gpuobj_import_useraddr - import an object based on a useraddr - * @virtaddr: Virtual address of the object to import - */ struct kgsl_gpuobj_import_useraddr { __u64 virtaddr; }; @@ -1452,13 +956,6 @@ struct kgsl_gpuobj_import_useraddr { #define IOCTL_KGSL_GPUOBJ_IMPORT \ _IOWR(KGSL_IOC_TYPE, 0x48, struct kgsl_gpuobj_import) -/** - * struct kgsl_gpuobj_sync_obj - Individual GPU object to sync - * @offset: Offset within the GPU object to sync - * @length: Number of bytes to sync - * @id: ID of the GPU object to sync - * @op: Cache operation to execute - */ struct kgsl_gpuobj_sync_obj { __u64 offset; @@ -1467,12 +964,6 @@ struct kgsl_gpuobj_sync_obj { unsigned int op; }; -/** - * struct kgsl_gpuobj_sync - Argument for IOCTL_KGSL_GPUOBJ_SYNC - * @objs: Pointer to an array of kgsl_gpuobj_sync_obj structs - * @obj_len: Size of each item in the array - * @count: Number of items in the array - */ struct kgsl_gpuobj_sync { __u64 __user objs; @@ -1483,14 +974,6 @@ struct kgsl_gpuobj_sync { #define IOCTL_KGSL_GPUOBJ_SYNC \ _IOW(KGSL_IOC_TYPE, 0x49, struct kgsl_gpuobj_sync) -/** - * struct kgsl_command_object - GPU command object - * @offset: GPU address offset of the object - * @gpuaddr: GPU address of the object - * @size: Size of the object - * @flags: Current flags for the object - * @id - GPU command object ID - */ struct kgsl_command_object { __u64 offset; __u64 gpuaddr; @@ -1499,33 +982,12 @@ struct kgsl_command_object { unsigned int id; }; -/** - * struct kgsl_command_syncpoint - GPU syncpoint object - * @priv: Pointer to the type specific buffer - * @size: Size of the type specific buffer - * @type: type of sync point defined here - */ struct kgsl_command_syncpoint { __u64 __user priv; __u64 size; unsigned int type; }; -/** - * struct kgsl_command_object - Argument for IOCTL_KGSL_GPU_COMMAND - * @flags: Current flags for the object - * @cmdlist: List of kgsl_command_objects for submission - * @cmd_size: Size of kgsl_command_objects structure - * @numcmds: Number of kgsl_command_objects in command list - * @objlist: List of kgsl_command_objects for tracking - * @obj_size: Size of kgsl_command_objects structure - * @numobjs: Number of kgsl_command_objects in object list - * @synclist: List of kgsl_command_syncpoints - * @sync_size: Size of kgsl_command_syncpoint structure - * @numsyncs: Number of kgsl_command_syncpoints in syncpoint list - * @context_id: Context ID submittin ghte kgsl_gpu_command - * @timestamp: Timestamp for the submitted commands - */ struct kgsl_gpu_command { __u64 flags; __u64 __user cmdlist; @@ -1544,23 +1006,6 @@ struct kgsl_gpu_command { #define IOCTL_KGSL_GPU_COMMAND \ _IOWR(KGSL_IOC_TYPE, 0x4A, struct kgsl_gpu_command) -/** - * struct kgsl_preemption_counters_query - argument to - * IOCTL_KGSL_PREEMPTIONCOUNTER_QUERY - * @counters: Return preemption counters array - * @size_user: Size allocated by userspace - * @size_priority_level: Size of preemption counters for each - * priority level - * @max_priority_level: Return max number of priority levels - * - * Query the available preemption counters. The array counters - * is used to return preemption counters. The size of the array - * is passed in so the kernel will only write at most size_user - * or max available preemption counters. The total number of - * preemption counters is returned in max_priority_level. If the - * array or size passed in are invalid, then an error is - * returned back. - */ struct kgsl_preemption_counters_query { __u64 __user counters; unsigned int size_user; @@ -1571,16 +1016,6 @@ struct kgsl_preemption_counters_query { #define IOCTL_KGSL_PREEMPTIONCOUNTER_QUERY \ _IOWR(KGSL_IOC_TYPE, 0x4B, struct kgsl_preemption_counters_query) -/** - * struct kgsl_gpuobj_set_info - argument for IOCTL_KGSL_GPUOBJ_SET_INFO - * @flags: Flags to indicate which parameters to change - * @metadata: If KGSL_GPUOBJ_SET_INFO_METADATA is set, a pointer to the new - * metadata - * @id: GPU memory object ID to change - * @metadata_len: If KGSL_GPUOBJ_SET_INFO_METADATA is set, the length of the - * new metadata string - * @type: If KGSL_GPUOBJ_SET_INFO_TYPE is set, the new type of the memory object - */ #define KGSL_GPUOBJ_SET_INFO_METADATA (1 << 0) #define KGSL_GPUOBJ_SET_INFO_TYPE (1 << 1) @@ -1596,13 +1031,6 @@ struct kgsl_gpuobj_set_info { #define IOCTL_KGSL_GPUOBJ_SET_INFO \ _IOW(KGSL_IOC_TYPE, 0x4C, struct kgsl_gpuobj_set_info) -/** - * struct kgsl_sparse_phys_alloc - Argument for IOCTL_KGSL_SPARSE_PHYS_ALLOC - * @size: Size in bytes to back - * @pagesize: Pagesize alignment required - * @flags: Flags for this allocation - * @id: Returned ID for this allocation - */ struct kgsl_sparse_phys_alloc { __u64 size; __u64 pagesize; @@ -1613,10 +1041,6 @@ struct kgsl_sparse_phys_alloc { #define IOCTL_KGSL_SPARSE_PHYS_ALLOC \ _IOWR(KGSL_IOC_TYPE, 0x50, struct kgsl_sparse_phys_alloc) -/** - * struct kgsl_sparse_phys_free - Argument for IOCTL_KGSL_SPARSE_PHYS_FREE - * @id: ID to free - */ struct kgsl_sparse_phys_free { unsigned int id; }; @@ -1624,14 +1048,6 @@ struct kgsl_sparse_phys_free { #define IOCTL_KGSL_SPARSE_PHYS_FREE \ _IOW(KGSL_IOC_TYPE, 0x51, struct kgsl_sparse_phys_free) -/** - * struct kgsl_sparse_virt_alloc - Argument for IOCTL_KGSL_SPARSE_VIRT_ALLOC - * @size: Size in bytes to reserve - * @pagesize: Pagesize alignment required - * @flags: Flags for this allocation - * @id: Returned ID for this allocation - * @gpuaddr: Returned GPU address for this allocation - */ struct kgsl_sparse_virt_alloc { __u64 size; __u64 pagesize; @@ -1643,10 +1059,6 @@ struct kgsl_sparse_virt_alloc { #define IOCTL_KGSL_SPARSE_VIRT_ALLOC \ _IOWR(KGSL_IOC_TYPE, 0x52, struct kgsl_sparse_virt_alloc) -/** - * struct kgsl_sparse_virt_free - Argument for IOCTL_KGSL_SPARSE_VIRT_FREE - * @id: ID to free - */ struct kgsl_sparse_virt_free { unsigned int id; }; @@ -1654,14 +1066,6 @@ struct kgsl_sparse_virt_free { #define IOCTL_KGSL_SPARSE_VIRT_FREE \ _IOW(KGSL_IOC_TYPE, 0x53, struct kgsl_sparse_virt_free) -/** - * struct kgsl_sparse_binding_object - Argument for kgsl_sparse_bind - * @virtoffset: Offset into the virtual ID - * @physoffset: Offset into the physical ID (bind only) - * @size: Size in bytes to reserve - * @flags: Flags for this kgsl_sparse_binding_object - * @id: Physical ID to bind (bind only) - */ struct kgsl_sparse_binding_object { __u64 virtoffset; __u64 physoffset; @@ -1670,14 +1074,6 @@ struct kgsl_sparse_binding_object { unsigned int id; }; -/** - * struct kgsl_sparse_bind - Argument for IOCTL_KGSL_SPARSE_BIND - * @list: List of kgsl_sparse_bind_objects to bind/unbind - * @id: Virtual ID to bind/unbind - * @size: Size of kgsl_sparse_bind_object - * @count: Number of elements in list - * - */ struct kgsl_sparse_bind { __u64 __user list; unsigned int id; @@ -1688,20 +1084,6 @@ struct kgsl_sparse_bind { #define IOCTL_KGSL_SPARSE_BIND \ _IOW(KGSL_IOC_TYPE, 0x54, struct kgsl_sparse_bind) -/** - * struct kgsl_gpu_sparse_command - Argument for - * IOCTL_KGSL_GPU_SPARSE_COMMAND - * @flags: Current flags for the object - * @sparselist: List of kgsl_sparse_binding_object to bind/unbind - * @synclist: List of kgsl_command_syncpoints - * @sparsesize: Size of kgsl_sparse_binding_object - * @numsparse: Number of elements in list - * @sync_size: Size of kgsl_command_syncpoint structure - * @numsyncs: Number of kgsl_command_syncpoints in syncpoint list - * @context_id: Context ID submitting the kgsl_gpu_command - * @timestamp: Timestamp for the submitted commands - * @id: Virtual ID to bind/unbind - */ struct kgsl_gpu_sparse_command { __u64 flags; __u64 __user sparselist; @@ -1721,26 +1103,6 @@ struct kgsl_gpu_sparse_command { #define KGSL_GPUMEM_RANGE_OP_BIND 1 #define KGSL_GPUMEM_RANGE_OP_UNBIND 2 -/** - * struct kgsl_gpumem_bind_range - specifies a bind operation for a virtual - * buffer object - * @child_offset: Offset to the start of memory within the child buffer object - * (not used for KGSL_GPUMEM_RANGE_OP_UNBIND operations) - * @target_offset: GPU address offset within the target VBO - * @length: Amount of memory to map/unmap (in bytes) - * @child_id: The GPU buffer ID for the child object to map/unmap in the VBO - * @op: One of KGSL_GPUMEM_RANGE_OP_BIND or KGSL_GPUMEM_RANGE_OP_UNBIND - * - * This defines a specific bind operation to a virtual buffer object specified - * in &struct kgsl_gpumem_bind_ranges. When @op is KGSL_GPUMEM_RANGE_OP_BIND the - * physical memory starting at @child_offset in the memory object identified by - * @child_id will be mapped into the target virtual buffer object starting at - * @offset for @length bytes. - * - * When @op is KGSL_GPUMEM_RANGE_OP_UNBIND any entries in the target virtual - * buffer object between @offset and @length that belong to @child_id will be - * removed. - */ struct kgsl_gpumem_bind_range { __u64 child_offset; __u64 target_offset; @@ -1752,32 +1114,6 @@ struct kgsl_gpumem_bind_range { #define KGSL_GPUMEM_BIND_ASYNC (1UL << 0) #define KGSL_GPUMEM_BIND_FENCE_OUT (1UL << 1) -/** - * struct kgsl_gpumem_bind_ranges - Argument to IOCTL_KGSL_GPUMEM_BIND_RANGES to - * either map or unmap a child buffer object into a virtual buffer object. - * @ranges: User memory pointer to an array of range operations of type &struct - * kgsl_gpumem_bind_range - * @ranges_nents: Number of entries in @ranges - * @ranges_size: Size of each entry in @ranges in bytes - * @id: GPU buffer object identifier for the target virtual buffer object - * @flags: Bitmap of KGSL_GPUMEM_BIND_ASYNC and KGSL_GPUMEM_BIND_FENCE_OUT - * @fence_id: If KGSL_GPUMEM_BIND_FENCE_OUT is set in @flags contains the - * identifier for the sync fence that will be signaled after the operation - * completes - * - * Describes a number of range operations to perform on a virtual buffer object - * identified by @id. Ranges should be a __u64 representation of an array of - * &struct kgsl_gpumem_bind_range entries. @ranges_nents will contain the number - * of entries in the array, and @ranges_size will contain the size of each entry - * in the array. If KGSL_GPUMEM_BIND_ASYNC is set the operation will be - * performed asynchronously and the operation will immediately return to the - * user. Otherwise the calling context will block until the operation has - * completed. - * - * If KGSL_GPUMEM_BIND_ASYNC and KGSL_GPUMEM_BIND_FENCE_OUT are both set a sync - * fence will be created and returned in @fence_id. The fence will be signaled - * when the bind operation has completed. - */ struct kgsl_gpumem_bind_ranges { __u64 ranges; __u32 ranges_nents; @@ -1785,7 +1121,6 @@ struct kgsl_gpumem_bind_ranges { __u32 id; __u32 flags; int fence_id; - /* private: 64 bit compatibility */ __u32 padding; }; @@ -1794,81 +1129,23 @@ struct kgsl_gpumem_bind_ranges { #define KGSL_GPU_AUX_COMMAND_BIND (1 << 0) #define KGSL_GPU_AUX_COMMAND_TIMELINE (1 << 1) -/* Reuse the same flag that GPU COMMAND uses */ #define KGSL_GPU_AUX_COMMAND_SYNC KGSL_CMDBATCH_SYNC -/** - * struct kgsl_gpu_aux_command_bind - Descriptor for a GPU AUX bind command - * @rangeslist: Pointer to a list of &struct kgsl_gpumem_bind_range items - * @numranges Number of entries in @rangeslist - * @rangesize: Size of each entry in @rangeslist - * @target: The GPU memory ID for the target virtual buffer object - * - * Describe a GPU AUX command to bind ranges in a virtual buffer object. - * @rangeslist points to a &struct kgsl_gpumem_bind_ranges which is the same - * struct that is used by IOCTl_KGSL_GPUMEM_BIND_RANGES. @numrages is the size - * of the array in @rangeslist and @rangesize is the size of each entity in - * @rangeslist. @target points to the GPU ID for the target VBO object. - */ struct kgsl_gpu_aux_command_bind { __u64 rangeslist; __u64 numranges; __u64 rangesize; __u32 target; -/* private: Padding for 64 bit compatibility */ __u32 padding; }; -/** - * struct kgsl_aux_command_generic - Container for an AUX command - * @priv: Pointer to the type specific buffer - * @size: Size of the type specific buffer - * @type: type of sync point defined here - * - * Describes a generic container for GPU aux commands. @priv is a user pointer - * to the command struct matching @type of size @size. - */ struct kgsl_gpu_aux_command_generic { __u64 priv; __u64 size; __u32 type; -/* private: Padding for 64 bit compatibility */ __u32 padding; }; -/** - * struct kgsl_gpu_aux_command - Argument for IOCTL_KGSL_GPU_AUX_COMMAND - * @flags: flags for the object - * @cmdlist: List of &struct kgsl_gpu_aux_command_generic objects - * @cmd_size: Size of each entry in @cmdlist - * @numcmds: Number of entries in @cmdlist - * @synclist: List of &struct kgsl_command_syncpoint objects - * @syncsize: Size of each entry in @synclist - * @numsyncs: Number of entries in @synclist - * @context_id: ID of the context submtting the aux command - * @timestamp: Timestamp for the command submission - * - * Describe a GPU auxiliary command. Auxiliary commands are tasks that are not - * performed on hardware but can be queued like normal GPU commands. Like GPU - * commands AUX commands are assigned a timestamp and processed in order in the - * queue. They can also have standard sync objects attached. The only - * difference is that AUX commands usually perform some sort of administrative - * task in the CPU and are retired in the dispatcher. - * - * For bind operations flags must have one of the KGSL_GPU_AUX_COMMAND_* flags - * set. If sync objects are attached KGSL_GPU_AUX_COMMAND_SYNC must be set. - * @cmdlist points to an array of &struct kgsl_gpu_aux_command_generic structs - * which in turn will have a pointer to a specific command type. - * @numcmds is the number of commands in the list and @cmdsize is the size - * of each entity in @cmdlist. - * - * If KGSL_GPU_AUX_COMMAND_SYNC is specified @synclist will point to an array of - * &struct kgsl_command_syncpoint items in the same fashion as a GPU hardware - * command. @numsyncs and @syncsize describe the list. - * - * @context_id is the context that is submitting the command and @timestamp - * contains the timestamp for the operation. - */ struct kgsl_gpu_aux_command { __u64 flags; __u64 cmdlist; @@ -1884,59 +1161,24 @@ struct kgsl_gpu_aux_command { #define IOCTL_KGSL_GPU_AUX_COMMAND \ _IOWR(KGSL_IOC_TYPE, 0x57, struct kgsl_gpu_aux_command) -/** - * struct kgsl_timeline_create - Argument for IOCTL_KGSL_TIMELINE_CREATE - * @seqno: Initial sequence number for the timeline - * @id: Timeline identifier [out] - * - * Create a new semaphore timeline and return the identifier in @id. - * The identifier is global for the device and can be used to - * identify the timeline in all subsequent commands. - */ struct kgsl_timeline_create { __u64 seqno; __u32 id; -/* private: padding for 64 bit compatibility */ __u32 padding; }; #define IOCTL_KGSL_TIMELINE_CREATE \ _IOWR(KGSL_IOC_TYPE, 0x58, struct kgsl_timeline_create) -/** - * struct kgsl_timeline_val - A container to store a timeline/sequence number - * pair. - * @seqno: Sequence number to signal/query - * @timeline: The timeline identifier to signal/query - * - * A container to store a timeline/seqno pair used by the query and signal - * ioctls. - */ struct kgsl_timeline_val { __u64 seqno; __u32 timeline; -/* private: padding for 64 bit compatibility */ __u32 padding; }; #define KGSL_TIMELINE_WAIT_ALL 1 #define KGSL_TIMELINE_WAIT_ANY 2 -/** - * struct kgsl_timeline_wait - Argument for IOCTL_KGSL_TIMELINE_WAIT - * @tv_sec: Number of seconds to wait for the signal - * @tv_nsec: Number of nanoseconds to wait for the signal - * @timelines: Address of an array of &struct kgsl_timeline_val entries - * @count: Number of entries in @timeline - * @timelines_size: Size of each entry in @timelines - * @flags: One of KGSL_TIMELINE_WAIT_ALL or KGSL_TIMELINE_WAIT_ANY - * - * Wait for the timelines listed in @timelines to be signaled. If @flags is - * equal to KGSL_TIMELINE_WAIT_ALL then wait for all timelines or if - * KGSL_TIMELINE_WAIT_ANY is specified then wait for any of the timelines to - * signal. @tv_sec and @tv_nsec indicates the number of seconds and nanoseconds - * that the process should be blocked waiting for the signal. - */ struct kgsl_timeline_wait { __s64 tv_sec; __s64 tv_nsec; @@ -1944,7 +1186,6 @@ struct kgsl_timeline_wait { __u32 count; __u32 timelines_size; __u32 flags; -/* private: padding for 64 bit compatibility */ __u32 padding; }; @@ -1954,14 +1195,6 @@ struct kgsl_timeline_wait { #define IOCTL_KGSL_TIMELINE_QUERY \ _IOWR(KGSL_IOC_TYPE, 0x5A, struct kgsl_timeline_val) -/** - * struct kgsl_timeline_signal - argument for IOCTL_KGSL_TIMELINE_SIGNAL - * @timelines: Address of an array of &struct kgsl_timeline_val entries - * @count: Number of entries in @timelines - * @timelines_size: Size of each entry in @timelines - * - * Signal an array of timelines of type @struct kgsl_timeline_val. - */ struct kgsl_timeline_signal { __u64 timelines; __u32 count; @@ -1971,15 +1204,6 @@ struct kgsl_timeline_signal { #define IOCTL_KGSL_TIMELINE_SIGNAL \ _IOW(KGSL_IOC_TYPE, 0x5B, struct kgsl_timeline_signal) -/** - * struct kgsl_timeline_fence_get - argument for IOCTL_KGSL_TIMELINE_FENCE_GET - * @seqno: Sequence number for the fence - * @timeline: Timeline to create the fence on - * @handle: Contains the fence fd for a successful operation [out] - * - * Create a sync file descriptor for the seqnum on the timeline and return it in - * @handle. Can be polled and queried just like any other sync file descriptor - */ struct kgsl_timeline_fence_get { __u64 seqno; __u32 timeline; @@ -1988,34 +1212,18 @@ struct kgsl_timeline_fence_get { #define IOCTL_KGSL_TIMELINE_FENCE_GET \ _IOWR(KGSL_IOC_TYPE, 0x5C, struct kgsl_timeline_fence_get) -/** - * IOCTL_KGSL_TIMELINE_DESTROY takes a u32 identifier for the timeline to - * destroy - */ #define IOCTL_KGSL_TIMELINE_DESTROY _IOW(KGSL_IOC_TYPE, 0x5D, __u32) -/** - * struct kgsl_gpu_aux_command_timeline - An aux command for timeline signals - * @timelines: An array of &struct kgsl_timeline_val elements - * @count: The number of entries in @timelines - * @timelines_size: The size of each element in @timelines - * - * An aux command for timeline signals that can be pointed to by - * &struct kgsl_aux_command_generic when the type is - * KGSL_GPU_AUX_COMMAND_TIMELINE. - */ struct kgsl_gpu_aux_command_timeline { __u64 timelines; __u32 count; __u32 timelines_size; }; -/* Macros for fault type used in kgsl_fault structure */ #define KGSL_FAULT_TYPE_NO_FAULT 0 #define KGSL_FAULT_TYPE_PAGEFAULT 1 #define KGSL_FAULT_TYPE_MAX 2 -/* Macros to be used in kgsl_pagefault_report structure */ #define KGSL_PAGEFAULT_TYPE_NONE 0 #define KGSL_PAGEFAULT_TYPE_READ (1 << 0) #define KGSL_PAGEFAULT_TYPE_WRITE (1 << 1) @@ -2024,87 +1232,32 @@ struct kgsl_gpu_aux_command_timeline { #define KGSL_PAGEFAULT_TYPE_EXTERNAL (1 << 4) #define KGSL_PAGEFAULT_TYPE_TRANSACTION_STALLED (1 << 5) -/** - * struct kgsl_pagefault_report - Descriptor for each page fault - * @fault_addr: page fault address - * @fault_type: type of page fault - * - * Contains information about supported GPU page fault. - * Supported fault type: KGSL_PAGEFAULT_TYPE_* - */ struct kgsl_pagefault_report { __u64 fault_addr; - /* private: reserved for future use */ __u64 reserved[2]; __u32 fault_type; - /* private: padding for 64 bit compatibility */ __u32 __pad; }; -/** - * struct kgsl_fault - Descriptor for each GPU fault type - * @fault: User memory pointer to list of specific fault type - * @type: Type of gpu fault - * @count: Number of entries in @fault - * @size: Size of each entry in @fault in bytes - * - * Contains information about each GPU fault type. If user passes 0 for all the fields, KGSL - * will return the @count and @type of fault. Based on this, user can allocate a buffer for - * specific fault type, fill the @fault and specify the structure size of type specific fault - * in @size. User can walk through @fault list to parse the fault type specific information. - * - * Supported type: KGSL_FAULT_TYPE_* - */ struct kgsl_fault { __u64 fault; __u32 type; __u32 count; __u32 size; - /* private: padding for 64 bit compatibility */ __u32 padding; }; -/** - * struct kgsl_fault_report - Container for list of GPU faults - * @faultlist: User memory pointer to list of fault descriptor &struct kgsl_fault - * @faultnents: Number of entries in @faultlist. Each entry corresponds to a fault type i.e. - * KGSL_FAULT_TYPE_* - * @faultsize: Size of each entry in @faultlist in bytes - * @context_id: ID of a KGSL context - * - * Returns a list of GPU faults for a context identified by @context_id. If the user specifies - * @context_id only, then KGSL will set the @faultnents to the number of fault types it has - * for that context. - * - * User is expected to allocate an array of @struct kgsl_fault with @faultnents number of entries - * and fill the @faultlist field. On calling @IOCTL_KGSL_GET_FAULT_REPORT, KGSL will return the - * type and count for each fault. Based on this, user needs to update the @kgsl_fault structure. - * Then, it should call the @IOCTL_KGSL_GET_FAULT_REPORT again for kernel to fill the fault - * information. - */ struct kgsl_fault_report { __u64 faultlist; __u32 faultnents; __u32 faultsize; __u32 context_id; - /* private: padding for 64 bit compatibility */ __u32 padding; }; #define IOCTL_KGSL_GET_FAULT_REPORT \ _IOWR(KGSL_IOC_TYPE, 0x5E, struct kgsl_fault_report) -/** - * struct kgsl_recurring_object - Argument for IOCTL_KGSL_RECURRING_COMMAND - * @flags: Current flags for the object - * @cmdlist: List of kgsl_command_objects for submission - * @cmd_size: Size of kgsl_command_objects structure - * @numcmds: Number of kgsl_command_objects in command list - * @objlist: List of kgsl_command_objects for tracking - * @obj_size: Size of kgsl_command_objects structure - * @numobjs: Number of kgsl_command_objects in object list - * @context_id: Context ID submitting the kgsl_recurring_command - */ struct kgsl_recurring_command { __u64 flags; __u64 __user cmdlist; @@ -2114,11 +1267,10 @@ struct kgsl_recurring_command { __u32 objsize; __u32 numobjs; __u32 context_id; - /* private: Padding for 64 bit compatibility */ __u32 padding; }; #define IOCTL_KGSL_RECURRING_COMMAND \ _IOWR(KGSL_IOC_TYPE, 0x5F, struct kgsl_recurring_command) -#endif /* _UAPI_MSM_KGSL_H */ \ No newline at end of file +#endif \ No newline at end of file diff --git a/app/src/main/cpp/adrenotools/src/hook/meson.build b/app/src/main/cpp/adrenotools/src/hook/meson.build index 0bb6eb390..4ea88a7dd 100644 --- a/app/src/main/cpp/adrenotools/src/hook/meson.build +++ b/app/src/main/cpp/adrenotools/src/hook/meson.build @@ -1,32 +1,12 @@ hook_impl_files = files(['hook_impl.cpp', 'hook_impl.h', 'hook_impl_params.h']) hook_impl_inc = include_directories('../../include') -# hook_impl_lib = shared_library('hook_impl', 'hook_impl.cpp', 'hook_impl.h', 'hook_impl_params.h') -# target_compile_options(['hook_impl', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_libraries(['hook_impl', 'linkernsbypass', 'log']) -# target_include_directories(['hook_impl', 'PRIVATE', '../../include']) -# set_target_properties(['hook_impl', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden']) main_hook_files = files(['main_hook.c']) -# main_hook_lib = shared_library('main_hook', 'main_hook.c') -# target_compile_options(['main_hook', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_options(['main_hook', 'PRIVATE', '-z', 'global']) -# target_link_libraries(['main_hook', 'hook_impl']) -# set_target_properties(['main_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden']) file_redirect_hook_files = files(['file_redirect_hook.c']) -# file_redirect_hook_lib = shared_library('file_redirect_hook', 'file_redirect_hook.c') -# target_compile_options(['file_redirect_hook', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_options(['file_redirect_hook', 'PRIVATE', '-z', 'global']) -# target_link_libraries(['file_redirect_hook', 'hook_impl']) -# set_target_properties(['file_redirect_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden']) gsl_alloc_hook_files = files(['gsl_alloc_hook.c']) -# gsl_alloc_hook_lib = shared_library('gsl_alloc_hook', 'gsl_alloc_hook.c') -# target_compile_options(['gsl_alloc_hook', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_options(['gsl_alloc_hook', 'PRIVATE', '-z', 'global']) -# target_link_libraries(['gsl_alloc_hook', 'hook_impl']) -# set_target_properties(['gsl_alloc_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden']) hook_impl_lib = shared_library( 'hook_impl', diff --git a/app/src/main/cpp/adrenotools/tools/acc-shim/vk_acc_shim.cpp b/app/src/main/cpp/adrenotools/tools/acc-shim/vk_acc_shim.cpp index 2bccb0534..4c5b89ccc 100644 --- a/app/src/main/cpp/adrenotools/tools/acc-shim/vk_acc_shim.cpp +++ b/app/src/main/cpp/adrenotools/tools/acc-shim/vk_acc_shim.cpp @@ -5,7 +5,6 @@ thread_local int satisfy_driver_emutls; -// Check libllvm-glnext.so in older drivers for a full list of these, newer ones are slightly more awkward due to the split enum class OptionType : uint32_t { ArgsStringList = 1, QcArgsString = 4, @@ -21,7 +20,6 @@ struct StringListVal { struct Option { OptionType type; - // PAD union { bool boolVal; int intVal; @@ -49,7 +47,6 @@ int CompileHook(void *state, OptionsSet *opts) { Option replacementOpts[ReplacementOptsNum]; uint32_t patchedOptNum{}; - // Insert your desired args here - results will be written to stderr const char *argv[] = {"-help", "-help-hidden", "", NULL }; StringListVal listVal{ .len = 3, @@ -83,7 +80,6 @@ int CompileHook(void *state, OptionsSet *opts) { struct LlvmInterface { uint32_t unk0[3]; - // PAD void *makeContext; void *delContext; decltype(CompileHook) *compile; diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/QtiGrallocDefs.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/QtiGrallocDefs.h index e0ccbe1d2..8dcdaa5bf 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/QtiGrallocDefs.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/QtiGrallocDefs.h @@ -30,53 +30,31 @@ #ifndef __QTIGRALLOCDEFS_H__ #define __QTIGRALLOCDEFS_H__ -// From gralloc_priv.h -/* Gralloc usage bits indicating the type of allocation that should be used */ -/* Refer to BufferUsage in hardware/interfaces/graphics/common//types.hal */ -/* The bits below are in officially defined vendor space - * i.e bits 28-31 and 48-63*/ -/* Non linear, Universal Bandwidth Compression */ #define GRALLOC_USAGE_PRIVATE_ALLOC_UBWC (UINT32_C(1) << 28) -/* Set this for allocating uncached memory (using O_DSYNC), - * cannot be used with noncontiguous heaps */ #define GRALLOC_USAGE_PRIVATE_UNCACHED (UINT32_C(1) << 29) -/* This flag is used to indicate 10 bit format. - * When both GRALLOC_USAGE_PRIVATE_ALLOC_UBWC & GRALLOC_USAGE_PRIVATE_10BIT - * are set then it will indicate UBWC_TP10 format. - * When only GRALLOC_USAGE_PRIVATE_10BIT is set it will indicate linear P010 format. - */ #define GRALLOC_USAGE_PRIVATE_10BIT (UINT32_C(1) << 30) -/* This flag is used for SECURE display usecase */ #define GRALLOC_USAGE_PRIVATE_SECURE_DISPLAY (UINT32_C(1) << 31) -/* This flag is used to indicate video NV21 format */ #define GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER 1ULL << 48 -/* unused legacy flags */ #define GRALLOC_USAGE_PRIVATE_MM_HEAP 0 #define GRALLOC_USAGE_PRIVATE_IOMMU_HEAP 0 -/* This flag is set for WFD usecase */ #define GRALLOC_USAGE_PRIVATE_WFD 1ULL << 51 -/* This flag is set for HEIF usecase */ #define GRALLOC_USAGE_PRIVATE_HEIF (UINT32_C(1) << 27) -/* TODO(user): Remove when clients stop referencing this flag */ #define GRALLOC_USAGE_PRIVATE_10BIT_TP 0 -/* This flag indicates PI format is being used */ #define GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI 1ULL << 49 -/* This flag is set while CDSP accesses the buffer */ #define GRALLOC_USAGE_PRIVATE_CDSP 1ULL << 50 -// OEM specific HAL formats #define HAL_PIXEL_FORMAT_RGBA_5551 6 #define HAL_PIXEL_FORMAT_RGBA_4444 7 #define HAL_PIXEL_FORMAT_NV12_ENCODEABLE 0x102 @@ -98,7 +76,6 @@ #define HAL_PIXEL_FORMAT_RAW8 0x123 #define HAL_PIXEL_FORMAT_NV12_HEIF 0x116 -// 10 bit #define HAL_PIXEL_FORMAT_ARGB_2101010 0x117 #define HAL_PIXEL_FORMAT_RGBX_1010102 0x118 #define HAL_PIXEL_FORMAT_XRGB_2101010 0x119 @@ -106,7 +83,7 @@ #define HAL_PIXEL_FORMAT_ABGR_2101010 0x11B #define HAL_PIXEL_FORMAT_BGRX_1010102 0x11C #define HAL_PIXEL_FORMAT_XBGR_2101010 0x11D -#define HAL_PIXEL_FORMAT_YCbCr_420_P010 0x36 // HAL_PIXEL_FORMAT_YCBCR_P010 +#define HAL_PIXEL_FORMAT_YCbCr_420_P010 0x36 #define HAL_PIXEL_FORMAT_YCbCr_420_P010_UBWC 0x124 #define HAL_PIXEL_FORMAT_YCbCr_420_P010_VENUS 0x7FA30C0A @@ -115,7 +92,6 @@ #define HAL_PIXEL_FORMAT_INTERLACE 0x180 -// Camera utils format #define HAL_PIXEL_FORMAT_NV12_LINEAR_FLEX 0x125 #define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX 0x126 #define HAL_PIXEL_FORMAT_MULTIPLANAR_FLEX 0x127 @@ -123,22 +99,16 @@ #define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX_4_BATCH 0x129 #define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX_8_BATCH 0x130 -// FSC Formats #define HAL_PIXEL_FORMAT_RGB888_UBWC_FSC 0x131 #define HAL_PIXEL_FORMAT_RGB101010_UBWC_FSC 0x132 -// v4l2_fourcc('Y', 'U', 'Y', 'L'). 24 bpp YUYV 4:2:2 10 bit per component #define HAL_PIXEL_FORMAT_YCbCr_422_I_10BIT 0x4C595559 -// v4l2_fourcc('Y', 'B', 'W', 'C'). 10 bit per component. This compressed -// format reduces the memory access bandwidth #define HAL_PIXEL_FORMAT_YCbCr_422_I_10BIT_COMPRESSED 0x43574259 -// UBWC aligned Venus format #define HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC 0x7FA30C06 #define HAL_PIXEL_FORMAT_YCbCr_420_TP10_UBWC 0x7FA30C09 -// Khronos ASTC formats #define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_4x4_KHR 0x93B0 #define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_5x4_KHR 0x93B1 #define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_5x5_KHR 0x93B2 @@ -170,4 +140,4 @@ enum { BUFFER_TYPE_UI = 0, BUFFER_TYPE_VIDEO }; -#endif //__QTIGRALLOCDEFS_H__ +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/common.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/common.h index 73e80cb1d..a6dd7366d 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/common.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/common.h @@ -26,7 +26,6 @@ using namespace android::hardware; using namespace android; -/* Simplified version of hidl::base::V1_0::IBase */ struct IBase : virtual public RefBase { virtual bool isRemote() const; diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.cpp b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.cpp index 30610e810..3a77d9274 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.cpp +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.cpp @@ -80,8 +80,6 @@ AdrenoMemInfo::AdrenoMemInfo() { ALOGE(" Failed to load libadreno_utils.so"); } - // Check if the overriding property debug.gralloc.gfx_ubwc_disable - // that disables UBWC allocations for the graphics stack is set char property[PROPERTY_VALUE_MAX]; property_get(DISABLE_UBWC_PROP, property, "0"); if (!(strncmp(property, "1", PROPERTY_VALUE_MAX)) || @@ -123,16 +121,13 @@ void AdrenoMemInfo::AlignUnCompressedRGB(int width, int height, int format, int break; } - int raster_mode = 0; // Adreno unknown raster mode. - int padding_threshold = 512; // Threshold for padding surfaces. - // the function below computes aligned width and aligned height - // based on linear or macro tile mode selected. + int raster_mode = 0; + int padding_threshold = 512; + + if (LINK_adreno_compute_fmt_aligned_width_and_height) { - // We call into adreno_utils only for RGB formats. So plane_id is 0 and - // num_samples is 1 always. We may have to add uitility function to - // find out these if there is a need to call this API for YUV formats. LINK_adreno_compute_fmt_aligned_width_and_height( - width, height, 0 /*plane_id*/, GetGpuPixelFormat(format), 1 /*num_samples*/, tile_enabled, + width, height, 0 , GetGpuPixelFormat(format), 1 , tile_enabled, raster_mode, padding_threshold, reinterpret_cast(aligned_w), reinterpret_cast(aligned_h)); } else if (LINK_adreno_compute_aligned_width_and_height) { @@ -140,7 +135,7 @@ void AdrenoMemInfo::AlignUnCompressedRGB(int width, int height, int format, int width, height, bpp, tile_enabled, raster_mode, padding_threshold, reinterpret_cast(aligned_w), reinterpret_cast(aligned_h)); } else if (LINK_adreno_compute_padding) { - int surface_tile_height = 1; // Linear surface + int surface_tile_height = 1; *aligned_w = UINT(LINK_adreno_compute_padding(width, bpp, surface_tile_height, raster_mode, padding_threshold)); ALOGW("%s: Warning!! Old GFX API is used to calculate stride", __FUNCTION__); @@ -157,9 +152,9 @@ void AdrenoMemInfo::AlignCompressedRGB(int width, int height, int format, unsign unsigned int* aligned_h) { if (LINK_adreno_compute_compressedfmt_aligned_width_and_height) { int bytesPerPixel = 0; - int raster_mode = 0; // Adreno unknown raster mode. - int padding_threshold = 512; // Threshold for padding - // surfaces. + int raster_mode = 0; + int padding_threshold = 512; + LINK_adreno_compute_compressedfmt_aligned_width_and_height( width, height, format, 0, raster_mode, padding_threshold, reinterpret_cast(aligned_w), @@ -341,4 +336,4 @@ bool AdrenoMemInfo::IsPISupportedByGPU(int format, uint64_t usage) { return false; } -} // namespace gralloc +} diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.h index 590d18ba6..f5281bc1c 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.h @@ -35,11 +35,10 @@ namespace gralloc { -// Adreno Pixel Formats typedef enum { ADRENO_PIXELFORMAT_UNKNOWN = 0, ADRENO_PIXELFORMAT_R16G16B16A16_FLOAT = 10, - ADRENO_PIXELFORMAT_R10G10B10A2_UNORM = 24, // Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES + ADRENO_PIXELFORMAT_R10G10B10A2_UNORM = 24, ADRENO_PIXELFORMAT_R8G8B8A8 = 28, ADRENO_PIXELFORMAT_R8G8B8A8_SRGB = 29, ADRENO_PIXELFORMAT_D32_FLOAT = 40, @@ -55,140 +54,87 @@ typedef enum { ADRENO_PIXELFORMAT_P010 = 104, ADRENO_PIXELFORMAT_YUY2 = 107, ADRENO_PIXELFORMAT_B4G4R4A4 = 115, - ADRENO_PIXELFORMAT_NV12_EXT = 506, // NV12 with non-std alignment and offsets - ADRENO_PIXELFORMAT_R8G8B8X8 = 507, // GL_RGB8 (Internal) - ADRENO_PIXELFORMAT_R8G8B8 = 508, // GL_RGB8 - ADRENO_PIXELFORMAT_A1B5G5R5 = 519, // GL_RGB5_A1 - ADRENO_PIXELFORMAT_R8G8B8X8_SRGB = 520, // GL_SRGB8 - ADRENO_PIXELFORMAT_R8G8B8_SRGB = 521, // GL_SRGB8 + ADRENO_PIXELFORMAT_NV12_EXT = 506, + ADRENO_PIXELFORMAT_R8G8B8X8 = 507, + ADRENO_PIXELFORMAT_R8G8B8 = 508, + ADRENO_PIXELFORMAT_A1B5G5R5 = 519, + ADRENO_PIXELFORMAT_R8G8B8X8_SRGB = 520, + ADRENO_PIXELFORMAT_R8G8B8_SRGB = 521, ADRENO_PIXELFORMAT_A2B10G10R10_UNORM = 532, - // Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES ADRENO_PIXELFORMAT_R10G10B10X2_UNORM = 537, ADRENO_PIXELFORMAT_D24_UNORM_X8_UINT = 548, ADRENO_PIXELFORMAT_D24_UNORM = 549, ADRENO_PIXELFORMAT_D32_FLOAT_X24S8_UINT = 551, ADRENO_PIXELFORMAT_S8_UINT = 552, - ADRENO_PIXELFORMAT_ASTC_4X4 = 568, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_5X4 = 569, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_5X5 = 570, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_6X5 = 571, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_6X6 = 572, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X5 = 573, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X6 = 574, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X8 = 575, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X5 = 576, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X6 = 577, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X8 = 578, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X10 = 579, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_12X10 = 580, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_12X12 = 581, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_4X4_SRGB = 582, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_5X4_SRGB = 583, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_5X5_SRGB = 584, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_6X5_SRGB = 585, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_6X6_SRGB = 586, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X5_SRGB = 587, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X6_SRGB = 588, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X8_SRGB = 589, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X5_SRGB = 590, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X6_SRGB = 591, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X8_SRGB = 592, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X10_SRGB = 593, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_12X10_SRGB = 594, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_12X12_SRGB = 595, // ASTC Compressed - // Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES - ADRENO_PIXELFORMAT_R5G6B5 = 610, // RGBA version of B5G6R5 - ADRENO_PIXELFORMAT_R5G5B5A1 = 611, // RGBA version of B5G5R5A1 - ADRENO_PIXELFORMAT_R4G4B4A4 = 612, // RGBA version of B4G4R4A4 - ADRENO_PIXELFORMAT_UYVY = 614, // YUV 4:2:2 packed progressive (1 plane) + ADRENO_PIXELFORMAT_ASTC_4X4 = 568, + ADRENO_PIXELFORMAT_ASTC_5X4 = 569, + ADRENO_PIXELFORMAT_ASTC_5X5 = 570, + ADRENO_PIXELFORMAT_ASTC_6X5 = 571, + ADRENO_PIXELFORMAT_ASTC_6X6 = 572, + ADRENO_PIXELFORMAT_ASTC_8X5 = 573, + ADRENO_PIXELFORMAT_ASTC_8X6 = 574, + ADRENO_PIXELFORMAT_ASTC_8X8 = 575, + ADRENO_PIXELFORMAT_ASTC_10X5 = 576, + ADRENO_PIXELFORMAT_ASTC_10X6 = 577, + ADRENO_PIXELFORMAT_ASTC_10X8 = 578, + ADRENO_PIXELFORMAT_ASTC_10X10 = 579, + ADRENO_PIXELFORMAT_ASTC_12X10 = 580, + ADRENO_PIXELFORMAT_ASTC_12X12 = 581, + ADRENO_PIXELFORMAT_ASTC_4X4_SRGB = 582, + ADRENO_PIXELFORMAT_ASTC_5X4_SRGB = 583, + ADRENO_PIXELFORMAT_ASTC_5X5_SRGB = 584, + ADRENO_PIXELFORMAT_ASTC_6X5_SRGB = 585, + ADRENO_PIXELFORMAT_ASTC_6X6_SRGB = 586, + ADRENO_PIXELFORMAT_ASTC_8X5_SRGB = 587, + ADRENO_PIXELFORMAT_ASTC_8X6_SRGB = 588, + ADRENO_PIXELFORMAT_ASTC_8X8_SRGB = 589, + ADRENO_PIXELFORMAT_ASTC_10X5_SRGB = 590, + ADRENO_PIXELFORMAT_ASTC_10X6_SRGB = 591, + ADRENO_PIXELFORMAT_ASTC_10X8_SRGB = 592, + ADRENO_PIXELFORMAT_ASTC_10X10_SRGB = 593, + ADRENO_PIXELFORMAT_ASTC_12X10_SRGB = 594, + ADRENO_PIXELFORMAT_ASTC_12X12_SRGB = 595, + + ADRENO_PIXELFORMAT_R5G6B5 = 610, + ADRENO_PIXELFORMAT_R5G5B5A1 = 611, + ADRENO_PIXELFORMAT_R4G4B4A4 = 612, + ADRENO_PIXELFORMAT_UYVY = 614, ADRENO_PIXELFORMAT_NV21 = 619, - ADRENO_PIXELFORMAT_Y8U8V8A8 = 620, // YUV 4:4:4 packed (1 plane) - ADRENO_PIXELFORMAT_Y8 = 625, // Single 8-bit luma only channel YUV format - ADRENO_PIXELFORMAT_TP10 = 654, // YUV 4:2:0 planar 10 bits/comp (2 planes) + ADRENO_PIXELFORMAT_Y8U8V8A8 = 620, + ADRENO_PIXELFORMAT_Y8 = 625, + ADRENO_PIXELFORMAT_TP10 = 654, } ADRENOPIXELFORMAT; typedef enum { - SURFACE_TILE_MODE_DISABLE = 0x0, // used for linear surface - SURFACE_TILE_MODE_ENABLE = 0x1 // used for tiled surface + SURFACE_TILE_MODE_DISABLE = 0x0, + SURFACE_TILE_MODE_ENABLE = 0x1 } surface_tile_mode_t; class AdrenoMemInfo { public: - /* - * Function to compute aligned width and aligned height based on - * width, height, format and usage flags. - * - * @return aligned width, aligned height - */ void GetAlignedWidthAndHeight(int width, int height, int format, int usage, unsigned int* aligned_w, unsigned int* aligned_h, bool ubwc_enabled, bool tile_enabled); - /* - * Function to compute the adreno aligned width and aligned height - * based on the width and format. - * - * @return aligned width, aligned height - */ void AlignUnCompressedRGB(int width, int height, int format, int tileEnabled, unsigned int* aligned_w, unsigned int* aligned_h); - /* - * Function to compute the adreno aligned width and aligned height - * based on the width and format. - * - * @return aligned width, aligned height - */ void AlignCompressedRGB(int width, int height, int format, unsigned int* aligned_w, unsigned int* aligned_h); - /* - * Function to compute the pixel alignment requirement. - * - * @return alignment - */ uint32_t GetGpuPixelAlignment(); - /* - * Function to query whether GPU supports UBWC for given HAL format - * @return > 0 : supported - * 0 : not supported - */ bool IsUBWCSupportedByGPU(int format); - /* - * Function to check if GPU supports PI or not - */ bool IsPISupportedByGPU(int format, uint64_t usage); - /* - * Function to get the corresponding Adreno format for given HAL format - */ ADRENOPIXELFORMAT GetGpuPixelFormat(int hal_format); - /* - * Function to get graphics metadata blob size - * @return graphics metadata size - */ uint32_t AdrenoGetMetadataBlobSize(); - /* - * Function to populate the graphics metadata blob - * @return 1 : Successful - * 2 : Unsuccessful - */ int AdrenoInitMemoryLayout(void* metadata_blob, int width, int height, int depth, int format, int num_samples, int isUBWC, uint64_t usage, uint32_t num_planes); - /* - * Function to get buffer size for based on graphcis metadata - * @return buffer size - */ uint32_t AdrenoGetAlignedGpuBufferSize(void* metadata_blob); - /* - * Function to check if adreno size calculation APIs are avaliable - * @return true : Avaliable - * false : Unavaliable - */ bool AdrenoSizeAPIAvaliable(); static AdrenoMemInfo* GetInstance(); @@ -196,7 +142,6 @@ class AdrenoMemInfo { private: AdrenoMemInfo(); ~AdrenoMemInfo(); - // link(s)to adreno surface padding library. int (*LINK_adreno_compute_padding)(int width, int bpp, int surface_tile_height, int screen_tile_height, int padding_threshold) = NULL; void (*LINK_adreno_compute_aligned_width_and_height)(int width, int height, int bpp, @@ -229,6 +174,6 @@ class AdrenoMemInfo { static AdrenoMemInfo* s_instance; }; -} // namespace gralloc +} -#endif // __GR_ADRENO_INFO_H__ +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_priv_handle.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_priv_handle.h index 3efbea4b7..30a48e7ab 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_priv_handle.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_priv_handle.h @@ -57,29 +57,27 @@ struct private_handle_t { PRIV_FLAGS_CAMERA_READ = 0x00040000, PRIV_FLAGS_HW_COMPOSER = 0x00080000, PRIV_FLAGS_HW_TEXTURE = 0x00100000, - PRIV_FLAGS_ITU_R_601 = 0x00200000, // Unused from display - PRIV_FLAGS_ITU_R_601_FR = 0x00400000, // Unused from display - PRIV_FLAGS_ITU_R_709 = 0x00800000, // Unused from display + PRIV_FLAGS_ITU_R_601 = 0x00200000, + PRIV_FLAGS_ITU_R_601_FR = 0x00400000, + PRIV_FLAGS_ITU_R_709 = 0x00800000, PRIV_FLAGS_SECURE_DISPLAY = 0x01000000, PRIV_FLAGS_TILE_RENDERED = 0x02000000, PRIV_FLAGS_CPU_RENDERED = 0x04000000, PRIV_FLAGS_UBWC_ALIGNED = 0x08000000, PRIV_FLAGS_DISP_CONSUMER = 0x10000000, - PRIV_FLAGS_CLIENT_ALLOCATED = 0x20000000, // Ion buffer allocated outside of gralloc - PRIV_FLAGS_UBWC_ALIGNED_PI = 0x40000000, // PI format + PRIV_FLAGS_CLIENT_ALLOCATED = 0x20000000, + PRIV_FLAGS_UBWC_ALIGNED_PI = 0x40000000, }; - // file-descriptors dup'd over IPC int fd; int fd_metadata; - // values sent over IPC int magic; int flags; - int width; // holds width of the actual buffer allocated - int height; // holds height of the actual buffer allocated - int unaligned_width; // holds width client asked to allocate - int unaligned_height; // holds height client asked to allocate + int width; + int height; + int unaligned_width; + int unaligned_height; int format; int buffer_type; unsigned int layer_count; @@ -126,7 +124,6 @@ struct private_handle_t { numFds = kNumFds; } - // Legacy constructor used by some clients private_handle_t(int fd, unsigned int size, int usage, int buf_type, int format, int w, int h) : private_handle_t(fd, -1, PRIV_FLAGS_CLIENT_ALLOCATED, w, h, 0, 0, format, buf_type, size, static_cast(usage)) {} @@ -176,7 +173,6 @@ struct private_handle_t { unsigned int GetLayerCount() const { return layer_count; } int GetStride() const { - // In handle we currently store aligned width after allocation. return width; } @@ -187,4 +183,4 @@ struct private_handle_t { }; #pragma pack(pop) -#endif // __GR_PRIV_HANDLE_H__ +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.cpp b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.cpp index a83ef20a6..4dc3a1f4b 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.cpp +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.cpp @@ -162,7 +162,6 @@ bool CpuCanRead(uint64_t usage) { bool CpuCanWrite(uint64_t usage) { if (usage & BufferUsage::CPU_WRITE_MASK) { - // Application intends to use CPU for rendering return true; } @@ -177,7 +176,6 @@ uint32_t GetDataAlignment(int format, uint64_t usage) { if (usage & BufferUsage::PROTECTED) { if ((usage & BufferUsage::CAMERA_OUTPUT) || (usage & GRALLOC_USAGE_PRIVATE_SECURE_DISPLAY)) { - // The alignment here reflects qsee mmu V7L/V8L requirement align = SZ_2M; } else { align = SECURE_ALIGN; @@ -202,7 +200,6 @@ bool IsGPUFlagSupported(uint64_t usage) { return ret; } -// Returns the final buffer size meant to be allocated with ion unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int alignedh) { unsigned int size = 0; int format = info.format; @@ -223,7 +220,6 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int } else if (IsCompressedRGBFormat(format)) { size = alignedw * alignedh * ASTC_BLOCK_SIZE; } else { - // Below switch should be for only YUV/custom formats switch (format) { case HAL_PIXEL_FORMAT_RAW16: case HAL_PIXEL_FORMAT_Y16: @@ -237,15 +233,15 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int case HAL_PIXEL_FORMAT_Y8: size = alignedw * alignedh * 1; break; - // adreno formats - case HAL_PIXEL_FORMAT_YCrCb_420_SP_ADRENO: // NV21 + + case HAL_PIXEL_FORMAT_YCrCb_420_SP_ADRENO: size = ALIGN(alignedw * alignedh, SIZE_4K); size += (unsigned int)ALIGN(2 * ALIGN(width / 2, 32) * ALIGN(height / 2, 32), SIZE_4K); break; - case HAL_PIXEL_FORMAT_YCbCr_420_SP_TILED: // NV12 - // The chroma plane is subsampled, - // but the pitch in bytes is unchanged - // The GPU needs 4K alignment, but the video decoder needs 8K + case HAL_PIXEL_FORMAT_YCbCr_420_SP_TILED: + + + size = ALIGN(alignedw * alignedh, SIZE_8K); size += ALIGN(alignedw * (unsigned int)ALIGN(height / 2, 32), SIZE_8K); break; @@ -296,7 +292,6 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int return size; } -// Explicitly defined UBWC formats bool IsUBwcFormat(int format) { switch (format) { case HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC: @@ -309,7 +304,6 @@ bool IsUBwcFormat(int format) { } bool IsUBwcSupported(int format) { - // Existing HAL formats with UBWC support switch (format) { case HAL_PIXEL_FORMAT_BGR_565: case HAL_PIXEL_FORMAT_RGBA_8888: @@ -336,7 +330,6 @@ bool IsUBwcPISupported(int format, uint64_t usage) { return false; } - // As of now only two formats switch (format) { case HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC: case HAL_PIXEL_FORMAT_YCbCr_420_TP10_UBWC: { @@ -354,27 +347,21 @@ bool IsUBwcPISupported(int format, uint64_t usage) { } bool IsUBwcEnabled(int format, uint64_t usage) { - // Allow UBWC, if client is using an explicitly defined UBWC pixel format. if (IsUBwcFormat(format)) { return true; } - // Allow UBWC, if an OpenGL client sets UBWC usage flag and GPU plus MDP - // support the format. OR if a non-OpenGL client like Rotator, sets UBWC - // usage flag and MDP supports the format. if (((usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC) || (usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI) || (usage & BufferUsage::COMPOSER_CLIENT_TARGET)) && IsUBwcSupported(format)) { bool enable = true; - // Query GPU for UBWC only if buffer is intended to be used by GPU. if ((usage & BufferUsage::GPU_TEXTURE) || (usage & BufferUsage::GPU_RENDER_TARGET)) { if (AdrenoMemInfo::GetInstance()) { enable = AdrenoMemInfo::GetInstance()->IsUBWCSupportedByGPU(format); } } - // Allow UBWC, only if CPU usage flags are not set if (enable && !(CpuCanAccess(usage))) { return true; } @@ -418,13 +405,10 @@ unsigned int GetRgbUBwcMetaBufferSize(int width, int height, uint32_t bpp) { return size; } - // Align meta buffer height to 16 blocks meta_height = ALIGN(((height + block_height - 1) / block_height), 16); - // Align meta buffer width to 64 blocks meta_width = ALIGN(((width + block_width - 1) / block_width), 64); - // Align meta buffer size to 4K size = (unsigned int)ALIGN((meta_width * meta_height), 4096); return size; @@ -500,12 +484,10 @@ bool UseUncached(int format, uint64_t usage) { return true; } - // CPU read rarely if ((usage & BufferUsage::CPU_READ_MASK) == static_cast(BufferUsage::CPU_READ_RARELY)) { return true; } - // CPU write rarely if ((usage & BufferUsage::CPU_WRITE_MASK) == static_cast(BufferUsage::CPU_WRITE_RARELY)) { return true; @@ -576,8 +558,6 @@ uint64_t GetHandleFlags(int format, uint64_t usage) { int GetImplDefinedFormat(uint64_t usage, int format) { int gr_format = format; - // If input format is HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED then based on - // the usage bits, gralloc assigns a format. if (format == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED || format == HAL_PIXEL_FORMAT_YCbCr_420_888) { if ((usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC || usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI) && @@ -585,33 +565,30 @@ int GetImplDefinedFormat(uint64_t usage, int format) { gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC; } else if (usage & BufferUsage::VIDEO_ENCODER) { if (usage & GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER) { - gr_format = HAL_PIXEL_FORMAT_NV21_ENCODEABLE; // NV21 + gr_format = HAL_PIXEL_FORMAT_NV21_ENCODEABLE; } else if (usage & GRALLOC_USAGE_PRIVATE_HEIF) { gr_format = HAL_PIXEL_FORMAT_NV12_HEIF; } else if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) { gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS; } else { - gr_format = HAL_PIXEL_FORMAT_NV12_ENCODEABLE; // NV12 + gr_format = HAL_PIXEL_FORMAT_NV12_ENCODEABLE; } } else if (usage & BufferUsage::CAMERA_INPUT) { if (usage & BufferUsage::CAMERA_OUTPUT) { - // Assumed ZSL if both producer and consumer camera flags set - gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; // NV21 + + gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; } else { - gr_format = HAL_PIXEL_FORMAT_YCrCb_420_SP; // NV21 + gr_format = HAL_PIXEL_FORMAT_YCrCb_420_SP; } } else if (usage & BufferUsage::CAMERA_OUTPUT) { if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) { - gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; // NV21 + gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; } else { - gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS; // NV12 preview + gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS; } } else if (usage & BufferUsage::COMPOSER_OVERLAY) { - // XXX: If we still haven't set a format, default to RGBA8888 gr_format = HAL_PIXEL_FORMAT_RGBA_8888; } else if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) { - // If no other usage flags are detected, default the - // flexible YUV format to NV21_ZSL gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; } } @@ -637,7 +614,7 @@ bool HasAlphaComponent(int32_t format) { } void GetRGBPlaneInfo(const BufferInfo& info, int32_t format, int32_t width, int32_t height, - int32_t /* flags */, int* plane_count, PlaneLayoutInfo* plane_info) { + int32_t , int* plane_count, PlaneLayoutInfo* plane_info) { uint64_t usage = info.usage; *plane_count = 1; uint32_t bpp = 0; @@ -659,4 +636,4 @@ void GetRGBPlaneInfo(const BufferInfo& info, int32_t format, int32_t width, int3 plane_info->scanlines = height; } -} // namespace gralloc +} diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.h index 13b21577c..4c757a214 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.h @@ -42,7 +42,7 @@ #ifdef SLAVE_SIDE_CP #define SECURE_ALIGN SZ_1M -#else // MASTER_SIDE_CP +#else #define SECURE_ALIGN SZ_4K #endif @@ -68,59 +68,38 @@ inline Type1 ALIGN(Type1 x, Type2 align) { } enum PlaneComponent { - /* luma */ PLANE_COMPONENT_Y = 1 << 0, - /* chroma blue */ PLANE_COMPONENT_Cb = 1 << 1, - /* chroma red */ PLANE_COMPONENT_Cr = 1 << 2, - /* red */ PLANE_COMPONENT_R = 1 << 10, - /* green */ PLANE_COMPONENT_G = 1 << 11, - /* blue */ PLANE_COMPONENT_B = 1 << 12, - /* alpha */ PLANE_COMPONENT_A = 1 << 20, - /* raw data plane */ PLANE_COMPONENT_RAW = 1 << 30, - /* meta information plane */ PLANE_COMPONENT_META = 1 << 31, }; struct PlaneLayoutInfo { - /** Components represented the type of plane. */ PlaneComponent component; - /** horizontal subsampling. Must be a positive power of 2. */ uint32_t h_subsampling; - /** vertical subsampling. Must be a positive power of 2. */ uint32_t v_subsampling; - /** offset to the first byte of the top-left pixel of the plane - * and it is calculated from the start of the buffer. - * Add base of the handle with offset to get the first byte of the plane. - */ uint32_t offset; - /** step is the distance in bytes from one pixel value to the next. */ int32_t step; - /** stride of the plane in pixels */ int32_t stride; - /** stride of the plane in in bytes */ int32_t stride_bytes; - /** plane height or vertical stride */ int32_t scanlines; - /** size of the plane in bytes */ uint32_t size; }; @@ -151,6 +130,6 @@ int GetImplDefinedFormat(uint64_t usage, int format); int GetCustomFormatFlags(int format, uint64_t usage, int* custom_format, uint64_t* priv_flags); bool IsGPUFlagSupported(uint64_t usage); bool HasAlphaComponent(int32_t format); -} // namespace gralloc +} -#endif // __GR_UTILS_H__ +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gralloc_priv.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gralloc_priv.h index 8a333a6bd..0e4359e5e 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gralloc_priv.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gralloc_priv.h @@ -26,7 +26,6 @@ #include "QtiGrallocDefs.h" #include "gr_priv_handle.h" -//#pragma message "Warning: gralloc_priv.h is deprecated" #define GRALLOC_PROP_PREFIX "vendor.gralloc." #define GRALLOC_PROP(prop_name) GRALLOC_PROP_PREFIX prop_name @@ -42,8 +41,6 @@ inline int roundUpToPageSize(int x) { return (x + (getpagesize() - 1)) & ~(getpagesize() - 1); } -/* Legacy gralloc1 definitions */ -/* Some clients may still be using the old flags */ #define GRALLOC1_PRODUCER_USAGE_PRIVATE_ADSP_HEAP GRALLOC_USAGE_PRIVATE_ADSP_HEAP #define GRALLOC1_PRODUCER_USAGE_PRIVATE_ALLOC_UBWC GRALLOC_USAGE_PRIVATE_ALLOC_UBWC #define GRALLOC1_PRODUCER_USAGE_PRIVATE_UNCACHED GRALLOC_USAGE_PRIVATE_UNCACHED @@ -54,7 +51,6 @@ inline int roundUpToPageSize(int x) { #define GRALLOC1_CONSUMER_USAGE_PRIVATE_10BIT_TP GRALLOC_USAGE_PRIVATE_10BIT_TP #define GRALLOC1_PRODUCER_USAGE_PRIVATE_VIDEO_NV21_ENCODER GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER -// for PERFORM API : #define GRALLOC_MODULE_PERFORM_CREATE_HANDLE_FROM_BUFFER 1 #define GRALLOC_MODULE_PERFORM_GET_STRIDE 2 #define GRALLOC_MODULE_PERFORM_GET_CUSTOM_STRIDE_FROM_HANDLE 3 @@ -72,11 +68,9 @@ inline int roundUpToPageSize(int x) { #define GRALLOC1_MODULE_PERFORM_GET_INTERLACE_FLAG 15 #define GRALLOC_MODULE_PERFORM_GET_GRAPHICS_METADATA 16 -/* possible values for inverse gamma correction */ #define HAL_IGC_NOT_SPECIFIED 0 #define HAL_IGC_s_RGB 1 -/* Color Space: Values maps to ColorSpace_t in qdMetadata.h */ #define HAL_CSC_ITU_R_601 0 #define HAL_CSC_ITU_R_601_FR 1 #define HAL_CSC_ITU_R_709 2 @@ -84,20 +78,16 @@ inline int roundUpToPageSize(int x) { #define HAL_CSC_ITU_R_2020 4 #define HAL_CSC_ITU_R_2020_FR 5 -/* possible formats for 3D content*/ enum { HAL_NO_3D = 0x0, HAL_3D_SIDE_BY_SIDE_L_R = 0x1, HAL_3D_SIDE_BY_SIDE_R_L = 0x2, HAL_3D_TOP_BOTTOM = 0x4, - HAL_3D_IN_SIDE_BY_SIDE_L_R = 0x10000, // unused legacy format + HAL_3D_IN_SIDE_BY_SIDE_L_R = 0x10000, }; -/* Flag to determine interlaced content - * Value maps to Flags presents in types.hal of QtiMapperextensions - */ enum { LAYOUT_INTERLACED_FLAG = 1 << 0, }; -#endif // __GRALLOC_PRIV_H__ +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapper.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapper.h index ae3bdbba9..0325e8641 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapper.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapper.h @@ -60,43 +60,43 @@ struct IMapper : public IBase { int32_t height __attribute__((aligned(4))); }; - static sp getService(const std::string& /* serviceName */, const bool /* getStub */); + static sp getService(const std::string& , const bool ); }; -} // namespace V2_0 +} namespace V3_0 { struct IMapper : public IBase { - static sp getService(const std::string& /* serviceName */, const bool /* getStub */); + static sp getService(const std::string& , const bool ); }; -} // namespace V3_0 +} namespace V4_0 { struct IMapper : public IBase { - static sp getService(const std::string& /* serviceName */, const bool /* getStub */); + static sp getService(const std::string& , const bool ); }; -} // namespace V4_0 -} // namespace android::hardware::graphics::mapper +} +} namespace vendor::qti::hardware::display::mapper { namespace V4_0 { struct IQtiMapper : public IBase { - static Return> castFrom(const sp& /* parent */, - bool /* emitError */); + static Return> castFrom(const sp& , + bool ); }; -} // namespace V4_0 +} namespace V3_0 { struct IQtiMapper : public IBase { - static Return> castFrom(const sp& /* parent */, - bool /* emitError */); + static Return> castFrom(const sp& , + bool ); }; -} // namespace V3_0 +} namespace V2_0 { struct IQtiMapper : public IBase { @@ -175,9 +175,9 @@ struct IQtiMapper : public IBase { V1_0::IQtiMapperExtensions>& extensions)>; virtual Return getMapperExtensions(getMapperExtensions_cb _hidl_cb); - static Return> castFrom(const sp& /* parent */, - bool /* emitError */); + static Return> castFrom(const sp& , + bool ); }; -} // namespace V2_0 -} // namespace vendor::qti::hardware::display::mapper +} +} diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapperextensions.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapperextensions.h index 078cb90b2..1337627fb 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapperextensions.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapperextensions.h @@ -45,7 +45,7 @@ struct IQtiMapperExtensions : public IBase { IQtiMapperExtensions(){}; }; -} // namespace V1_0 +} namespace V1_1 { using Error = V1_0::Error; using PlaneLayout = V1_0::PlaneLayout; @@ -64,7 +64,6 @@ struct IQtiMapperExtensions : public IBase { std::terminate(); } - // Used, passthrough using getInterlacedFlag_cb = std::function; virtual ::android::hardware::Return getInterlacedFlag(void* buffer, @@ -79,7 +78,6 @@ struct IQtiMapperExtensions : public IBase { std::terminate(); } - // Used, passthrough using getRgbDataAddress_cb = std::function; virtual ::android::hardware::Return getRgbDataAddress(void* buffer, @@ -95,7 +93,6 @@ struct IQtiMapperExtensions : public IBase { std::terminate(); } - // Used, passhthrough using getColorSpace_cb = std::function; virtual ::android::hardware::Return getColorSpace(void* buffer, getColorSpace_cb _hidl_cb) { @@ -103,7 +100,6 @@ struct IQtiMapperExtensions : public IBase { std::terminate(); } - // Used, passthrough with hidl fixups using getYuvPlaneInfo_cb = std::function; virtual ::android::hardware::Return getFd(void* buffer, getFd_cb _hidl_cb); - // Used, cast private data with no passthrough using getWidth_cb = std::function; virtual ::android::hardware::Return getWidth(void* buffer, getWidth_cb _hidl_cb); - // Used, cast private data with no passthrough using getHeight_cb = std::function; virtual ::android::hardware::Return getHeight(void* buffer, getHeight_cb _hidl_cb); - // Used, cast private data with no passthrough using getOffset_cb = std::function; virtual ::android::hardware::Return getOffset(void* buffer, getOffset_cb _hidl_cb); - // Used, cast private data with no passthrough using getSize_cb = std::function; virtual ::android::hardware::Return getSize(void* buffer, getSize_cb _hidl_cb); @@ -179,12 +170,10 @@ struct IQtiMapperExtensions : public IBase { ::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, uint64_t usage)>; virtual ::android::hardware::Return getUsageFlags(void* buffer, getUsageFlags_cb _hidl_cb); - // Used, cast private data with no passthrough using getFormat_cb = std::function; virtual ::android::hardware::Return getFormat(void* buffer, getFormat_cb _hidl_cb); - // Used, cast private data with no passthrough using getPrivateFlags_cb = std::function; virtual ::android::hardware::Return getPrivateFlags(void* buffer, @@ -198,7 +187,6 @@ struct IQtiMapperExtensions : public IBase { std::terminate(); } - // Used, PAIN using getFormatLayout_cb = std::function getSurfaceMetadata_V1(void* buffer, void* metadata) { @@ -217,8 +204,8 @@ struct IQtiMapperExtensions : public IBase { } static Return> castFrom( - android::sp const& /* parent */, bool /* emitError */); + android::sp const& , bool ); }; -} // namespace V1_1 -} // namespace vendor::qti::hardware::display::mapperextensions +} +} diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/common.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/common.h index 6f327baf3..64daacb5d 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/common.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/common.h @@ -12,7 +12,6 @@ namespace graphics { namespace common { namespace V1_0 { -// Forward declaration for forward reference support: enum class PixelFormat : int32_t; enum class BufferUsage : uint64_t; enum class Transform : int32_t; @@ -21,1402 +20,1072 @@ enum class ColorMode : int32_t; enum class ColorTransform : int32_t; enum class Hdr : int32_t; -/** - * Common enumeration and structure definitions for all graphics HALs. - * - * - * Pixel formats for graphics buffers. - */ enum class PixelFormat : int32_t { - /** - * 32-bit format that has 8-bit R, G, B, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBA_8888 = 1 /* 0x1 */, - /** - * 32-bit format that has 8-bit R, G, B, and unused components, in that - * order, from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBX_8888 = 2 /* 0x2 */, - /** - * 24-bit format that has 8-bit R, G, and B components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGB_888 = 3 /* 0x3 */, - /** - * 16-bit packed format that has 5-bit R, 6-bit G, and 5-bit B components, - * in that order, from the most-sigfinicant bits to the least-significant - * bits. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGB_565 = 4 /* 0x4 */, - /** - * 32-bit format that has 8-bit B, G, R, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - BGRA_8888 = 5 /* 0x5 */, - /** - * Legacy formats deprecated in favor of YCBCR_420_888. - */ - YCBCR_422_SP = 16 /* 0x10 */, - YCRCB_420_SP = 17 /* 0x11 */, - YCBCR_422_I = 20 /* 0x14 */, - /** - * 64-bit format that has 16-bit R, G, B, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are signed floats, whose interpretation is defined - * by the dataspace. - */ - RGBA_FP16 = 22 /* 0x16 */, - /** - * RAW16 is a single-channel, 16-bit, little endian format, typically - * representing raw Bayer-pattern images from an image sensor, with minimal - * processing. - * - * The exact pixel layout of the data in the buffer is sensor-dependent, and - * needs to be queried from the camera device. - * - * Generally, not all 16 bits are used; more common values are 10 or 12 - * bits. If not all bits are used, the lower-order bits are filled first. - * All parameters to interpret the raw data (black and white points, - * color space, etc) must be queried from the camera device. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - strides are specified in pixels, not in bytes - * - * size = stride * height * 2 - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW16 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data, layout is as - * | defined above. - * Dataspace::DEPTH | Unprocessed implementation-dependent raw - * | depth measurements, opaque with 16 bit - * | samples. - * Other | Unsupported - */ - RAW16 = 32 /* 0x20 */, - /** - * BLOB is used to carry task-specific data which does not have a standard - * image structure. The details of the format are left to the two - * endpoints. - * - * A typical use case is for transporting JPEG-compressed images from the - * Camera HAL to the framework or to applications. - * - * Buffers of this format must have a height of 1, and width equal to their - * size in bytes. - * - * The mapping of the dataspace to buffer contents for BLOB is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::JFIF | An encoded JPEG image - * Dataspace::DEPTH | An android_depth_points buffer - * Dataspace::SENSOR | Sensor event data. - * Other | Unsupported - */ - BLOB = 33 /* 0x21 */, - /** - * A format indicating that the choice of format is entirely up to the - * allocator. - * - * The allocator should examine the usage bits passed in when allocating a - * buffer with this format, and it should derive the pixel format from - * those usage flags. This format must never be used with any of the - * BufferUsage::CPU_* usage flags. - * - * Even when the internally chosen format has an alpha component, the - * clients must assume the alpha vlaue to be 1.0. - * - * The interpretation of the component values is defined by the dataspace. - */ - IMPLEMENTATION_DEFINED = 34 /* 0x22 */, - /** - * This format allows platforms to use an efficient YCbCr/YCrCb 4:2:0 - * buffer layout, while still describing the general format in a - * layout-independent manner. While called YCbCr, it can be used to - * describe formats with either chromatic ordering, as well as - * whole planar or semiplanar layouts. - * - * This format must be accepted by the allocator when BufferUsage::CPU_* - * are set. - * - * Buffers with this format must be locked with IMapper::lockYCbCr. - * Locking with IMapper::lock must return an error. - * - * The interpretation of the component values is defined by the dataspace. - */ - YCBCR_420_888 = 35 /* 0x23 */, - /** - * RAW_OPAQUE is a format for unprocessed raw image buffers coming from an - * image sensor. The actual structure of buffers of this format is - * implementation-dependent. - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW_OPAQUE is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW_OPAQUE = 36 /* 0x24 */, - /** - * RAW10 is a single-channel, 10-bit per pixel, densely packed in each row, - * unprocessed format, usually representing raw Bayer-pattern images coming from - * an image sensor. - * - * In an image buffer with this format, starting from the first pixel of each - * row, each 4 consecutive pixels are packed into 5 bytes (40 bits). Each one - * of the first 4 bytes contains the top 8 bits of each pixel, The fifth byte - * contains the 2 least significant bits of the 4 pixels, the exact layout data - * for each 4 consecutive pixels is illustrated below (Pi[j] stands for the jth - * bit of the ith pixel): - * - * bit 7 bit 0 - * =====|=====|=====|=====|=====|=====|=====|=====| - * Byte 0: |P0[9]|P0[8]|P0[7]|P0[6]|P0[5]|P0[4]|P0[3]|P0[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 1: |P1[9]|P1[8]|P1[7]|P1[6]|P1[5]|P1[4]|P1[3]|P1[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 2: |P2[9]|P2[8]|P2[7]|P2[6]|P2[5]|P2[4]|P2[3]|P2[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 3: |P3[9]|P3[8]|P3[7]|P3[6]|P3[5]|P3[4]|P3[3]|P3[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 4: |P3[1]|P3[0]|P2[1]|P2[0]|P1[1]|P1[0]|P0[1]|P0[0]| - * =============================================== - * - * This format assumes - * - a width multiple of 4 pixels - * - an even height - * - a vertical stride equal to the height - * - strides are specified in bytes, not in pixels - * - * size = stride * height - * - * When stride is equal to width * (10 / 8), there will be no padding bytes at - * the end of each row, the entire image data is densely packed. When stride is - * larger than width * (10 / 8), padding bytes will be present at the end of each - * row (including the last row). - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW10 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW10 = 37 /* 0x25 */, - /** - * RAW12 is a single-channel, 12-bit per pixel, densely packed in each row, - * unprocessed format, usually representing raw Bayer-pattern images coming from - * an image sensor. - * - * In an image buffer with this format, starting from the first pixel of each - * row, each two consecutive pixels are packed into 3 bytes (24 bits). The first - * and second byte contains the top 8 bits of first and second pixel. The third - * byte contains the 4 least significant bits of the two pixels, the exact layout - * data for each two consecutive pixels is illustrated below (Pi[j] stands for - * the jth bit of the ith pixel): - * - * bit 7 bit 0 - * ======|======|======|======|======|======|======|======| - * Byte 0: |P0[11]|P0[10]|P0[ 9]|P0[ 8]|P0[ 7]|P0[ 6]|P0[ 5]|P0[ 4]| - * |------|------|------|------|------|------|------|------| - * Byte 1: |P1[11]|P1[10]|P1[ 9]|P1[ 8]|P1[ 7]|P1[ 6]|P1[ 5]|P1[ 4]| - * |------|------|------|------|------|------|------|------| - * Byte 2: |P1[ 3]|P1[ 2]|P1[ 1]|P1[ 0]|P0[ 3]|P0[ 2]|P0[ 1]|P0[ 0]| - * ======================================================= - * - * This format assumes: - * - a width multiple of 4 pixels - * - an even height - * - a vertical stride equal to the height - * - strides are specified in bytes, not in pixels - * - * size = stride * height - * - * When stride is equal to width * (12 / 8), there will be no padding bytes at - * the end of each row, the entire image data is densely packed. When stride is - * larger than width * (12 / 8), padding bytes will be present at the end of - * each row (including the last row). - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW12 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW12 = 38 /* 0x26 */, - /** - * 0x27 to 0x2A are reserved for flexible formats - * - * - * 32-bit packed format that has 2-bit A, 10-bit B, G, and R components, - * in that order, from the most-sigfinicant bits to the least-significant - * bits. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBA_1010102 = 43 /* 0x2B */, - /** - * 0x100 - 0x1FF - * - * This range is reserved for vendor extensions. Formats in this range - * must support BufferUsage::GPU_TEXTURE. Clients must assume they do not - * have an alpha component. - * - * - * Y8 is a YUV planar format comprised of a WxH Y plane, with each pixel - * being represented by 8 bits. It is equivalent to just the Y plane from - * YV12. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - * size = stride * height - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - Y8 = 538982489 /* 0x20203859 */, - /** - * Y16 is a YUV planar format comprised of a WxH Y plane, with each pixel - * being represented by 16 bits. It is just like Y8, but has double the - * bits per pixel (little endian). - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - strides are specified in pixels, not in bytes - * - * size = stride * height * 2 - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. When the dataspace is - * Dataspace::DEPTH, each pixel is a distance value measured by a depth - * camera, plus an associated confidence value. - */ - Y16 = 540422489 /* 0x20363159 */, - /** - * YV12 is a 4:2:0 YCrCb planar format comprised of a WxH Y plane followed - * by (W/2) x (H/2) Cr and Cb planes. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - * y_size = stride * height - * c_stride = ALIGN(stride/2, 16) - * c_size = c_stride * height/2 - * size = y_size + c_size * 2 - * cr_offset = y_size - * cb_offset = y_size + c_size - * - * This range is reserved for vendor extensions. Formats in this range - * must support BufferUsage::GPU_TEXTURE. Clients must assume they do not - * have an alpha component. - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::GPU_TEXTURE - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - YV12 = 842094169 /* 0x32315659 */, + + + + + + + + RGBA_8888 = 1 , + + + + + + + + RGBX_8888 = 2 , + + + + + + + + RGB_888 = 3 , + + + + + + + + + RGB_565 = 4 , + + + + + + + + BGRA_8888 = 5 , + + + + YCBCR_422_SP = 16 , + YCRCB_420_SP = 17 , + YCBCR_422_I = 20 , + + + + + + + + RGBA_FP16 = 22 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW16 = 32 , + + + + + + + + + + + + + + + + + + + + + + BLOB = 33 , + + + + + + + + + + + + + + + IMPLEMENTATION_DEFINED = 34 , + + + + + + + + + + + + + + + + YCBCR_420_888 = 35 , + + + + + + + + + + + + + + + + + + + + + RAW_OPAQUE = 36 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW10 = 37 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW12 = 38 , + + + + + + + + + + + + RGBA_1010102 = 43 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Y8 = 538982489 , + + + + + + + + + + + + + + + + + + + + + + + + + + Y16 = 540422489 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + YV12 = 842094169 , }; -/** - * Buffer usage definitions. - */ enum class BufferUsage : uint64_t { - /** - * bit 0-3 is an enum - */ - CPU_READ_MASK = 15ull /* 0xfULL */, - /** - * buffer is never read by CPU - */ + + + + CPU_READ_MASK = 15ull , + + + CPU_READ_NEVER = 0ull, - /** - * buffer is rarely read by CPU - */ CPU_READ_RARELY = 2ull, - /** - * buffer is often read by CPU - */ CPU_READ_OFTEN = 3ull, - /** - * bit 4-7 is an enum - */ - CPU_WRITE_MASK = 240ull /* (0xfULL << 4) */, - /** - * buffer is never written by CPU - */ - CPU_WRITE_NEVER = 0ull /* (0 << 4) */, - /** - * buffer is rarely written by CPU - */ - CPU_WRITE_RARELY = 32ull /* (2 << 4) */, - /** - * buffer is often written by CPU - */ - CPU_WRITE_OFTEN = 48ull /* (3 << 4) */, - /** - * buffer is used as a GPU texture - */ - GPU_TEXTURE = 256ull /* (1ULL << 8) */, - /** - * buffer is used as a GPU render target - */ - GPU_RENDER_TARGET = 512ull /* (1ULL << 9) */, - /** - * bit 10 must be zero - * - * - * buffer is used as a composer HAL overlay layer - */ - COMPOSER_OVERLAY = 2048ull /* (1ULL << 11) */, - /** - * buffer is used as a composer HAL client target - */ - COMPOSER_CLIENT_TARGET = 4096ull /* (1ULL << 12) */, - /** - * bit 13 must be zero - * - * - * Buffer is allocated with hardware-level protection against copying the - * contents (or information derived from the contents) into unprotected - * memory. - */ - PROTECTED = 16384ull /* (1ULL << 14) */, - /** - * buffer is used as a hwcomposer HAL cursor layer - */ - COMPOSER_CURSOR = 32768ull /* (1ULL << 15) */, - /** - * buffer is used as a video encoder input - */ - VIDEO_ENCODER = 65536ull /* (1ULL << 16) */, - /** - * buffer is used as a camera HAL output - */ - CAMERA_OUTPUT = 131072ull /* (1ULL << 17) */, - /** - * buffer is used as a camera HAL input - */ - CAMERA_INPUT = 262144ull /* (1ULL << 18) */, - /** - * bit 19 must be zero - * - * - * buffer is used as a renderscript allocation - */ - RENDERSCRIPT = 1048576ull /* (1ULL << 20) */, - /** - * bit 21 must be zero - * - * - * buffer is used as a video decoder output - */ - VIDEO_DECODER = 4194304ull /* (1ULL << 22) */, - /** - * buffer is used as a sensor direct report output - */ - SENSOR_DIRECT_DATA = 8388608ull /* (1ULL << 23) */, - /** - * buffer is used as as an OpenGL shader storage or uniform - * buffer object - */ - GPU_DATA_BUFFER = 16777216ull /* (1ULL << 24) */, - /** - * bits 25-27 must be zero and are reserved for future versions - * - * - * bits 28-31 are reserved for vendor extensions - */ - VENDOR_MASK = 4026531840ull /* (0xfULL << 28) */, - /** - * bits 32-47 must be zero and are reserved for future versions - * - * - * bits 48-63 are reserved for vendor extensions - */ - VENDOR_MASK_HI = 18446462598732840960ull /* (0xffffULL << 48) */, + + + + CPU_WRITE_MASK = 240ull , + + + + CPU_WRITE_NEVER = 0ull , + + + + CPU_WRITE_RARELY = 32ull , + + + + CPU_WRITE_OFTEN = 48ull , + + + + GPU_TEXTURE = 256ull , + + + + GPU_RENDER_TARGET = 512ull , + + + + + + + COMPOSER_OVERLAY = 2048ull , + + + + COMPOSER_CLIENT_TARGET = 4096ull , + + + + + + + + + PROTECTED = 16384ull , + + + + COMPOSER_CURSOR = 32768ull , + + + + VIDEO_ENCODER = 65536ull , + + + + CAMERA_OUTPUT = 131072ull , + + + + CAMERA_INPUT = 262144ull , + + + + + + + RENDERSCRIPT = 1048576ull , + + + + + + + VIDEO_DECODER = 4194304ull , + + + + SENSOR_DIRECT_DATA = 8388608ull , + + + + + GPU_DATA_BUFFER = 16777216ull , + + + + + + + VENDOR_MASK = 4026531840ull , + + + + + + + VENDOR_MASK_HI = 18446462598732840960ull , }; -/** - * Transformation definitions - */ enum class Transform : int32_t { - /** - * Horizontal flip. FLIP_H/FLIP_V is applied before ROT_90. - */ - FLIP_H = 1 /* (1 << 0) */, - /** - * Vertical flip. FLIP_H/FLIP_V is applied before ROT_90. - */ - FLIP_V = 2 /* (1 << 1) */, - /** - * 90 degree clockwise rotation. FLIP_H/FLIP_V is applied before ROT_90. - */ - ROT_90 = 4 /* (1 << 2) */, - /** - * Commonly used combinations. - */ - ROT_180 = 3 /* (FLIP_H | FLIP_V) */, - ROT_270 = 7 /* ((FLIP_H | FLIP_V) | ROT_90) */, + + + + FLIP_H = 1 , + + + + FLIP_V = 2 , + + + + ROT_90 = 4 , + + + + ROT_180 = 3 , + ROT_270 = 7 , }; -/** - * Dataspace Definitions - * ====================== - * - * Dataspace is the definition of how pixel values should be interpreted. - * - * For many formats, this is the colorspace of the image data, which includes - * primaries (including white point) and the transfer characteristic function, - * which describes both gamma curve and numeric range (within the bit depth). - * - * Other dataspaces include depth measurement data from a depth camera. - * - * A dataspace is comprised of a number of fields. - * - * Version - * -------- - * The top 2 bits represent the revision of the field specification. This is - * currently always 0. - * - * - * bits 31-30 29 - 0 - * +-----+----------------------------------------------------+ - * fields | Rev | Revision specific fields | - * +-----+----------------------------------------------------+ - * - * Field layout for version = 0: - * ---------------------------- - * - * A dataspace is comprised of the following fields: - * Standard - * Transfer function - * Range - * - * bits 31-30 29-27 26 - 22 21 - 16 15 - 0 - * +-----+-----+--------+--------+----------------------------+ - * fields | 0 |Range|Transfer|Standard| Legacy and custom | - * +-----+-----+--------+--------+----------------------------+ - * VV RRR TTTTT SSSSSS LLLLLLLL LLLLLLLL - * - * If range, transfer and standard fields are all 0 (e.g. top 16 bits are - * all zeroes), the bottom 16 bits contain either a legacy dataspace value, - * or a custom value. - */ enum class Dataspace : int32_t { - /** - * Default-assumption data space, when not explicitly specified. - * - * It is safest to assume the buffer is an image with sRGB primaries and - * encoding ranges, but the consumer and/or the producer of the data may - * simply be using defaults. No automatic gamma transform should be - * expected, except for a possible display gamma transform when drawn to a - * screen. - */ - UNKNOWN = 0 /* 0x0 */, - /** - * Arbitrary dataspace with manually defined characteristics. Definition - * for colorspaces or other meaning must be communicated separately. - * - * This is used when specifying primaries, transfer characteristics, - * etc. separately. - * - * A typical use case is in video encoding parameters (e.g. for H.264), - * where a colorspace can have separately defined primaries, transfer - * characteristics, etc. - */ - ARBITRARY = 1 /* 0x1 */, - /** - * Color-description aspects - * - * The following aspects define various characteristics of the color - * specification. These represent bitfields, so that a data space value - * can specify each of them independently. - */ + + + + + + + + + + UNKNOWN = 0 , + + + + + + + + + + + + ARBITRARY = 1 , + + + + + + + STANDARD_SHIFT = 16, - /** - * Standard aspect - * - * Defines the chromaticity coordinates of the source primaries in terms of - * the CIE 1931 definition of x and y specified in ISO 11664-1. - */ - STANDARD_MASK = 4128768 /* (63 << STANDARD_SHIFT) */, - /** - * Chromacity coordinates are unknown or are determined by the application. - * Implementations shall use the following suggested standards: - * - * All YCbCr formats: BT709 if size is 720p or larger (since most video - * content is letterboxed this corresponds to width is - * 1280 or greater, or height is 720 or greater). - * BT601_625 if size is smaller than 720p or is JPEG. - * All RGB formats: BT709. - * - * For all other formats standard is undefined, and implementations should use - * an appropriate standard for the data represented. - */ - STANDARD_UNSPECIFIED = 0 /* (0 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2126, KB = 0.0722 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT709 = 65536 /* (1 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - */ - STANDARD_BT601_625 = 131072 /* (2 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.222, KB = 0.071 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT601_625_UNADJUSTED = 196608 /* (3 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - */ - STANDARD_BT601_525 = 262144 /* (4 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.212, KB = 0.087 luminance interpretation - * for RGB conversion (as in SMPTE 240M). - */ - STANDARD_BT601_525_UNADJUSTED = 327680 /* (5 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2627, KB = 0.0593 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT2020 = 393216 /* (6 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2627, KB = 0.0593 luminance interpretation - * for RGB conversion using the linear domain. - */ - STANDARD_BT2020_CONSTANT_LUMINANCE = 458752 /* (7 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.21 0.71 - * blue 0.14 0.08 - * red 0.67 0.33 - * white (C) 0.310 0.316 - * - * Use the unadjusted KR = 0.30, KB = 0.11 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT470M = 524288 /* (8 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.243 0.692 - * blue 0.145 0.049 - * red 0.681 0.319 - * white (C) 0.310 0.316 - * - * Use the unadjusted KR = 0.254, KB = 0.068 luminance interpretation - * for RGB conversion. - */ - STANDARD_FILM = 589824 /* (9 << STANDARD_SHIFT) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. (DCI-P3) - * Primaries: x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - */ - STANDARD_DCI_P3 = 655360 /* (10 << STANDARD_SHIFT) */, - /** - * Adobe RGB - * Primaries: x y - * green 0.210 0.710 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - */ - STANDARD_ADOBE_RGB = 720896 /* (11 << STANDARD_SHIFT) */, + + + + + + + STANDARD_MASK = 4128768 , + + + + + + + + + + + + + + STANDARD_UNSPECIFIED = 0 , + + + + + + + + + + + STANDARD_BT709 = 65536 , + + + + + + + + + + + + + STANDARD_BT601_625 = 131072 , + + + + + + + + + + + STANDARD_BT601_625_UNADJUSTED = 196608 , + + + + + + + + + + + + + STANDARD_BT601_525 = 262144 , + + + + + + + + + + + STANDARD_BT601_525_UNADJUSTED = 327680 , + + + + + + + + + + + STANDARD_BT2020 = 393216 , + + + + + + + + + + + STANDARD_BT2020_CONSTANT_LUMINANCE = 458752 , + + + + + + + + + + + STANDARD_BT470M = 524288 , + + + + + + + + + + + STANDARD_FILM = 589824 , + + + + + + + + + STANDARD_DCI_P3 = 655360 , + + + + + + + + + STANDARD_ADOBE_RGB = 720896 , TRANSFER_SHIFT = 22, - /** - * Transfer aspect - * - * Transfer characteristics are the opto-electronic transfer characteristic - * at the source as a function of linear optical intensity (luminance). - * - * For digital signals, E corresponds to the recorded value. Normally, the - * transfer function is applied in RGB space to each of the R, G and B - * components independently. This may result in color shift that can be - * minized by applying the transfer function in Lab space only for the L - * component. Implementation may apply the transfer function in RGB space - * for all pixel formats if desired. - */ - TRANSFER_MASK = 130023424 /* (31 << TRANSFER_SHIFT) */, - /** - * Transfer characteristics are unknown or are determined by the - * application. - * - * Implementations should use the following transfer functions: - * - * For YCbCr formats: use TRANSFER_SMPTE_170M - * For RGB formats: use TRANSFER_SRGB - * - * For all other formats transfer function is undefined, and implementations - * should use an appropriate standard for the data represented. - */ - TRANSFER_UNSPECIFIED = 0 /* (0 << TRANSFER_SHIFT) */, - /** - * Transfer characteristic curve: - * E = L - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_LINEAR = 4194304 /* (1 << TRANSFER_SHIFT) */, - /** - * Transfer characteristic curve: - * - * E = 1.055 * L^(1/2.4) - 0.055 for 0.0031308 <= L <= 1 - * = 12.92 * L for 0 <= L < 0.0031308 - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_SRGB = 8388608 /* (2 << TRANSFER_SHIFT) */, - /** - * BT.601 525, BT.601 625, BT.709, BT.2020 - * - * Transfer characteristic curve: - * E = 1.099 * L ^ 0.45 - 0.099 for 0.018 <= L <= 1 - * = 4.500 * L for 0 <= L < 0.018 - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_SMPTE_170M = 12582912 /* (3 << TRANSFER_SHIFT) */, - /** - * Assumed display gamma 2.2. - * - * Transfer characteristic curve: - * E = L ^ (1/2.2) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_2 = 16777216 /* (4 << TRANSFER_SHIFT) */, - /** - * display gamma 2.6. - * - * Transfer characteristic curve: - * E = L ^ (1/2.6) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_6 = 20971520 /* (5 << TRANSFER_SHIFT) */, - /** - * display gamma 2.8. - * - * Transfer characteristic curve: - * E = L ^ (1/2.8) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_8 = 25165824 /* (6 << TRANSFER_SHIFT) */, - /** - * SMPTE ST 2084 (Dolby Perceptual Quantizer) - * - * Transfer characteristic curve: - * E = ((c1 + c2 * L^n) / (1 + c3 * L^n)) ^ m - * c1 = c3 - c2 + 1 = 3424 / 4096 = 0.8359375 - * c2 = 32 * 2413 / 4096 = 18.8515625 - * c3 = 32 * 2392 / 4096 = 18.6875 - * m = 128 * 2523 / 4096 = 78.84375 - * n = 0.25 * 2610 / 4096 = 0.1593017578125 - * L - luminance of image 0 <= L <= 1 for HDR colorimetry. - * L = 1 corresponds to 10000 cd/m2 - * E - corresponding electrical signal - */ - TRANSFER_ST2084 = 29360128 /* (7 << TRANSFER_SHIFT) */, - /** - * ARIB STD-B67 Hybrid Log Gamma - * - * Transfer characteristic curve: - * E = r * L^0.5 for 0 <= L <= 1 - * = a * ln(L - b) + c for 1 < L - * a = 0.17883277 - * b = 0.28466892 - * c = 0.55991073 - * r = 0.5 - * L - luminance of image 0 <= L for HDR colorimetry. L = 1 corresponds - * to reference white level of 100 cd/m2 - * E - corresponding electrical signal - */ - TRANSFER_HLG = 33554432 /* (8 << TRANSFER_SHIFT) */, + + + + + + + + + + + + + + TRANSFER_MASK = 130023424 , + + + + + + + + + + + + + TRANSFER_UNSPECIFIED = 0 , + + + + + + + TRANSFER_LINEAR = 4194304 , + + + + + + + + + TRANSFER_SRGB = 8388608 , + + + + + + + + + + TRANSFER_SMPTE_170M = 12582912 , + + + + + + + + + TRANSFER_GAMMA2_2 = 16777216 , + + + + + + + + + TRANSFER_GAMMA2_6 = 20971520 , + + + + + + + + + TRANSFER_GAMMA2_8 = 25165824 , + + + + + + + + + + + + + + + TRANSFER_ST2084 = 29360128 , + + + + + + + + + + + + + + + TRANSFER_HLG = 33554432 , RANGE_SHIFT = 27, - /** - * Range aspect - * - * Defines the range of values corresponding to the unit range of 0-1. - * This is defined for YCbCr only, but can be expanded to RGB space. - */ - RANGE_MASK = 939524096 /* (7 << RANGE_SHIFT) */, - /** - * Range is unknown or are determined by the application. Implementations - * shall use the following suggested ranges: - * - * All YCbCr formats: limited range. - * All RGB or RGBA formats (including RAW and Bayer): full range. - * All Y formats: full range - * - * For all other formats range is undefined, and implementations should use - * an appropriate range for the data represented. - */ - RANGE_UNSPECIFIED = 0 /* (0 << RANGE_SHIFT) */, - /** - * Full range uses all values for Y, Cb and Cr from - * 0 to 2^b-1, where b is the bit depth of the color format. - */ - RANGE_FULL = 134217728 /* (1 << RANGE_SHIFT) */, - /** - * Limited range uses values 16/256*2^b to 235/256*2^b for Y, and - * 1/16*2^b to 15/16*2^b for Cb, Cr, R, G and B, where b is the bit depth of - * the color format. - * - * E.g. For 8-bit-depth formats: - * Luma (Y) samples should range from 16 to 235, inclusive - * Chroma (Cb, Cr) samples should range from 16 to 240, inclusive - * - * For 10-bit-depth formats: - * Luma (Y) samples should range from 64 to 940, inclusive - * Chroma (Cb, Cr) samples should range from 64 to 960, inclusive - */ - RANGE_LIMITED = 268435456 /* (2 << RANGE_SHIFT) */, - /** - * Extended range is used for scRGB. Intended for use with - * floating point pixel formats. [0.0 - 1.0] is the standard - * sRGB space. Values outside the range 0.0 - 1.0 can encode - * color outside the sRGB gamut. - * Used to blend / merge multiple dataspaces on a single display. - */ - RANGE_EXTENDED = 402653184 /* (3 << RANGE_SHIFT) */, - /** - * Legacy dataspaces - * - * - * sRGB linear encoding: - * - * The red, green, and blue components are stored in sRGB space, but - * are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are encoded using the full range ([0,255] for 8-bit) for all - * components. - */ - SRGB_LINEAR = 512 /* 0x200 */, - V0_SRGB_LINEAR = 138477568 /* ((STANDARD_BT709 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * scRGB linear encoding: - * - * The red, green, and blue components are stored in extended sRGB space, - * but are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are floating point. - * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. - * Values beyond the range [0.0 - 1.0] would correspond to other colors - * spaces and/or HDR content. - */ - V0_SCRGB_LINEAR = 406913024 /* ((STANDARD_BT709 | TRANSFER_LINEAR) | RANGE_EXTENDED) */, - /** - * sRGB gamma encoding: - * - * The red, green and blue components are stored in sRGB space, and - * converted to linear space when read, using the SRGB transfer function - * for each of the R, G and B components. When written, the inverse - * transformation is performed. - * - * The alpha component, if present, is always stored in linear space and - * is left unmodified when read or written. - * - * Use full range and BT.709 standard. - */ - SRGB = 513 /* 0x201 */, - V0_SRGB = 142671872 /* ((STANDARD_BT709 | TRANSFER_SRGB) | RANGE_FULL) */, - /** - * scRGB: - * - * The red, green, and blue components are stored in extended sRGB space, - * but are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are floating point. - * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. - * Values beyond the range [0.0 - 1.0] would correspond to other colors - * spaces and/or HDR content. - */ - V0_SCRGB = 411107328 /* ((STANDARD_BT709 | TRANSFER_SRGB) | RANGE_EXTENDED) */, - /** - * YCbCr Colorspaces - * ----------------- - * - * Primaries are given using (x,y) coordinates in the CIE 1931 definition - * of x and y specified by ISO 11664-1. - * - * Transfer characteristics are the opto-electronic transfer characteristic - * at the source as a function of linear optical intensity (luminance). - * - * - * JPEG File Interchange Format (JFIF) - * - * Same model as BT.601-625, but all values (Y, Cb, Cr) range from 0 to 255 - * - * Use full range, BT.601 transfer and BT.601_625 standard. - */ - JFIF = 257 /* 0x101 */, - V0_JFIF = 146931712 /* ((STANDARD_BT601_625 | TRANSFER_SMPTE_170M) | RANGE_FULL) */, - /** - * ITU-R Recommendation 601 (BT.601) - 625-line - * - * Standard-definition television, 625 Lines (PAL) - * - * Use limited range, BT.601 transfer and BT.601_625 standard. - */ - BT601_625 = 258 /* 0x102 */, - V0_BT601_625 = 281149440 /* ((STANDARD_BT601_625 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * ITU-R Recommendation 601 (BT.601) - 525-line - * - * Standard-definition television, 525 Lines (NTSC) - * - * Use limited range, BT.601 transfer and BT.601_525 standard. - */ - BT601_525 = 259 /* 0x103 */, - V0_BT601_525 = 281280512 /* ((STANDARD_BT601_525 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * ITU-R Recommendation 709 (BT.709) - * - * High-definition television - * - * Use limited range, BT.709 transfer and BT.709 standard. - */ - BT709 = 260 /* 0x104 */, - V0_BT709 = 281083904 /* ((STANDARD_BT709 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. - * - * Digital Cinema DCI-P3 - * - * Use full range, linear transfer and D65 DCI-P3 standard - */ - DCI_P3_LINEAR = 139067392 /* ((STANDARD_DCI_P3 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. - * - * Digital Cinema DCI-P3 - * - * Use full range, gamma 2.6 transfer and D65 DCI-P3 standard - * Note: Application is responsible for gamma encoding the data as - * a 2.6 gamma encoding is not supported in HW. - */ - DCI_P3 = 155844608 /* ((STANDARD_DCI_P3 | TRANSFER_GAMMA2_6) | RANGE_FULL) */, - /** - * Display P3 - * - * Display P3 uses same primaries and white-point as DCI-P3 - * linear transfer function makes this the same as DCI_P3_LINEAR. - */ - DISPLAY_P3_LINEAR = 139067392 /* ((STANDARD_DCI_P3 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * Display P3 - * - * Use same primaries and white-point as DCI-P3 - * but sRGB transfer function. - */ - DISPLAY_P3 = 143261696 /* ((STANDARD_DCI_P3 | TRANSFER_SRGB) | RANGE_FULL) */, - /** - * Adobe RGB - * - * Use full range, gamma 2.2 transfer and Adobe RGB primaries - * Note: Application is responsible for gamma encoding the data as - * a 2.2 gamma encoding is not supported in HW. - */ - ADOBE_RGB = 151715840 /* ((STANDARD_ADOBE_RGB | TRANSFER_GAMMA2_2) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, linear transfer and BT2020 standard - */ - BT2020_LINEAR = 138805248 /* ((STANDARD_BT2020 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, BT.709 transfer and BT2020 standard - */ - BT2020 = 147193856 /* ((STANDARD_BT2020 | TRANSFER_SMPTE_170M) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, SMPTE 2084 (PQ) transfer and BT2020 standard - */ - BT2020_PQ = 163971072 /* ((STANDARD_BT2020 | TRANSFER_ST2084) | RANGE_FULL) */, - /** - * Data spaces for non-color formats - * - * - * The buffer contains depth ranging measurements from a depth camera. - * This value is valid with formats: - * HAL_PIXEL_FORMAT_Y16: 16-bit samples, consisting of a depth measurement - * and an associated confidence value. The 3 MSBs of the sample make - * up the confidence value, and the low 13 LSBs of the sample make up - * the depth measurement. - * For the confidence section, 0 means 100% confidence, 1 means 0% - * confidence. The mapping to a linear float confidence value between - * 0.f and 1.f can be obtained with - * float confidence = (((depthSample >> 13) - 1) & 0x7) / 7.0f; - * The depth measurement can be extracted simply with - * uint16_t range = (depthSample & 0x1FFF); - * HAL_PIXEL_FORMAT_BLOB: A depth point cloud, as - * a variable-length float (x,y,z, confidence) coordinate point list. - * The point cloud will be represented with the android_depth_points - * structure. - */ - DEPTH = 4096 /* 0x1000 */, - /** - * The buffer contains sensor events from sensor direct report. - * This value is valid with formats: - * HAL_PIXEL_FORMAT_BLOB: an array of sensor event structure that forms - * a lock free queue. Format of sensor event structure is specified - * in Sensors HAL. - */ - SENSOR = 4097 /* 0x1001 */, + + + + + + + RANGE_MASK = 939524096 , + + + + + + + + + + + + RANGE_UNSPECIFIED = 0 , + + + + + RANGE_FULL = 134217728 , + + + + + + + + + + + + + + RANGE_LIMITED = 268435456 , + + + + + + + + RANGE_EXTENDED = 402653184 , + + + + + + + + + + + + + + SRGB_LINEAR = 512 , + V0_SRGB_LINEAR = 138477568 , + + + + + + + + + + + + + V0_SCRGB_LINEAR = 406913024 , + + + + + + + + + + + + + + SRGB = 513 , + V0_SRGB = 142671872 , + + + + + + + + + + + + + V0_SCRGB = 411107328 , + + + + + + + + + + + + + + + + + + JFIF = 257 , + V0_JFIF = 146931712 , + + + + + + + + BT601_625 = 258 , + V0_BT601_625 = 281149440 , + + + + + + + + BT601_525 = 259 , + V0_BT601_525 = 281280512 , + + + + + + + + BT709 = 260 , + V0_BT709 = 281083904 , + + + + + + + + DCI_P3_LINEAR = 139067392 , + + + + + + + + + + DCI_P3 = 155844608 , + + + + + + + DISPLAY_P3_LINEAR = 139067392 , + + + + + + + DISPLAY_P3 = 143261696 , + + + + + + + + ADOBE_RGB = 151715840 , + + + + + + + + BT2020_LINEAR = 138805248 , + + + + + + + + BT2020 = 147193856 , + + + + + + + + BT2020_PQ = 163971072 , + + + + + + + + + + + + + + + + + + + + + + DEPTH = 4096 , + + + + + + + + SENSOR = 4097 , }; -/** - * Color modes that may be supported by a display. - * - * Definitions: - * Rendering intent generally defines the goal in mapping a source (input) - * color to a destination device color for a given color mode. - * - * It is important to keep in mind three cases where mapping may be applied: - * 1. The source gamut is much smaller than the destination (display) gamut - * 2. The source gamut is much larger than the destination gamut (this will - * ordinarily be handled using colorimetric rendering, below) - * 3. The source and destination gamuts are roughly equal, although not - * completely overlapping - * Also, a common requirement for mappings is that skin tones should be - * preserved, or at least remain natural in appearance. - * - * Colorimetric Rendering Intent (All cases): - * Colorimetric indicates that colors should be preserved. In the case - * that the source gamut lies wholly within the destination gamut or is - * about the same (#1, #3), this will simply mean that no manipulations - * (no saturation boost, for example) are applied. In the case where some - * source colors lie outside the destination gamut (#2, #3), those will - * need to be mapped to colors that are within the destination gamut, - * while the already in-gamut colors remain unchanged. - * - * Non-colorimetric transforms can take many forms. There are no hard - * rules and it's left to the implementation to define. - * Two common intents are described below. - * - * Stretched-Gamut Enhancement Intent (Source < Destination): - * When the destination gamut is much larger than the source gamut (#1), the - * source primaries may be redefined to reflect the full extent of the - * destination space, or to reflect an intermediate gamut. - * Skin-tone preservation would likely be applied. An example might be sRGB - * input displayed on a DCI-P3 capable device, with skin-tone preservation. - * - * Within-Gamut Enhancement Intent (Source >= Destination): - * When the device (destination) gamut is not larger than the source gamut - * (#2 or #3), but the appearance of a larger gamut is desired, techniques - * such as saturation boost may be applied to the source colors. Skin-tone - * preservation may be applied. There is no unique method for within-gamut - * enhancement; it would be defined within a flexible color mode. - * - */ enum class ColorMode : int32_t { - /** - * DEFAULT is the "native" gamut of the display. - * White Point: Vendor/OEM defined - * Panel Gamma: Vendor/OEM defined (typically 2.2) - * Rendering Intent: Vendor/OEM defined (typically 'enhanced') - */ NATIVE = 0, - /** - * STANDARD_BT601_625 corresponds with display - * settings that implement the ITU-R Recommendation BT.601 - * or Rec 601. Using 625 line version - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_625 = 1, - /** - * Primaries: - * x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.222, KB = 0.071 luminance interpretation - * for RGB conversion. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_625_UNADJUSTED = 2, - /** - * Primaries: - * x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_525 = 3, - /** - * Primaries: - * x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.212, KB = 0.087 luminance interpretation - * for RGB conversion (as in SMPTE 240M). - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_525_UNADJUSTED = 4, - /** - * REC709 corresponds with display settings that implement - * the ITU-R Recommendation BT.709 / Rec. 709 for high-definition television. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * HDTV REC709 Inverse Gamma Correction (IGC): V represents normalized - * (with [0 to 1] range) value of R, G, or B. - * - * if Vnonlinear < 0.081 - * Vlinear = Vnonlinear / 4.5 - * else - * Vlinear = ((Vnonlinear + 0.099) / 1.099) ^ (1/0.45) - * - * HDTV REC709 Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.5 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear) ^ 0.45 – 0.099 - */ STANDARD_BT709 = 5, - /** - * DCI_P3 corresponds with display settings that implement - * SMPTE EG 432-1 and SMPTE RP 431-2 - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - * - * Gamma: 2.6 - */ DCI_P3 = 6, - /** - * SRGB corresponds with display settings that implement - * the sRGB color space. Uses the same primaries as ITU-R Recommendation - * BT.709 - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * PC/Internet (sRGB) Inverse Gamma Correction (IGC): - * - * if Vnonlinear ≤ 0.03928 - * Vlinear = Vnonlinear / 12.92 - * else - * Vlinear = ((Vnonlinear + 0.055)/1.055) ^ 2.4 - * - * PC/Internet (sRGB) Gamma Correction (GC): - * - * if Vlinear ≤ 0.0031308 - * Vnonlinear = 12.92 * Vlinear - * else - * Vnonlinear = 1.055 * (Vlinear)^(1/2.4) – 0.055 - */ SRGB = 7, - /** - * ADOBE_RGB corresponds with the RGB color space developed - * by Adobe Systems, Inc. in 1998. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.210 0.710 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Gamma: 2.2 - */ ADOBE_RGB = 8, - /** - * DISPLAY_P3 is a color space that uses the DCI_P3 primaries, - * the D65 white point and the SRGB transfer functions. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - * - * PC/Internet (sRGB) Gamma Correction (GC): - * - * if Vlinear ≤ 0.0030186 - * Vnonlinear = 12.92 * Vlinear - * else - * Vnonlinear = 1.055 * (Vlinear)^(1/2.4) – 0.055 - * - * Note: In most cases sRGB transfer function will be fine. - */ DISPLAY_P3 = 9, }; -/** - * Color transforms that may be applied by hardware composer to the whole - * display. - */ enum class ColorTransform : int32_t { - /** - * Applies no transform to the output color - */ IDENTITY = 0, - /** - * Applies an arbitrary transform defined by a 4x4 affine matrix - */ ARBITRARY_MATRIX = 1, - /** - * Applies a transform that inverts the value or luminance of the color, but - * does not modify hue or saturation - */ VALUE_INVERSE = 2, - /** - * Applies a transform that maps all colors to shades of gray - */ GRAYSCALE = 3, - /** - * Applies a transform which corrects for protanopic color blindness - */ CORRECT_PROTANOPIA = 4, - /** - * Applies a transform which corrects for deuteranopic color blindness - */ CORRECT_DEUTERANOPIA = 5, - /** - * Applies a transform which corrects for tritanopic color blindness - */ CORRECT_TRITANOPIA = 6, }; -/** - * Supported HDR formats. Must be kept in sync with equivalents in Display.java. - */ enum class Hdr : int32_t { - /** - * Device supports Dolby Vision HDR - */ DOLBY_VISION = 1, - /** - * Device supports HDR10 - */ HDR10 = 2, - /** - * Device supports hybrid log-gamma HDR - */ HLG = 3, }; -// -// type declarations for package -// template static inline std::string toString(int32_t o); @@ -1691,9 +1360,6 @@ constexpr int32_t& operator&=(int32_t& v, return v; } -// -// type header definitions for package -// template <> inline std::string toString<::android::hardware::graphics::common::V1_0::PixelFormat>(int32_t o) { @@ -3183,15 +2849,12 @@ static inline std::string toString(::android::hardware::graphics::common::V1_0:: return os; } -} // namespace V1_0 -} // namespace common -} // namespace graphics -} // namespace hardware -} // namespace android +} +} +} +} +} -// -// global type declarations for package -// namespace android { namespace hardware { @@ -3220,9 +2883,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::PixelFormat, 2 ::android::hardware::graphics::common::V1_0::PixelFormat::Y16, ::android::hardware::graphics::common::V1_0::PixelFormat::YV12, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3254,9 +2917,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::BufferUsage, 2 ::android::hardware::graphics::common::V1_0::BufferUsage::VENDOR_MASK, ::android::hardware::graphics::common::V1_0::BufferUsage::VENDOR_MASK_HI, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3270,9 +2933,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::Transform, 5> ::android::hardware::graphics::common::V1_0::Transform::ROT_180, ::android::hardware::graphics::common::V1_0::Transform::ROT_270, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3338,9 +3001,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::Dataspace, 57> ::android::hardware::graphics::common::V1_0::Dataspace::DEPTH, ::android::hardware::graphics::common::V1_0::Dataspace::SENSOR, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3359,9 +3022,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::ColorMode, 10> ::android::hardware::graphics::common::V1_0::ColorMode::ADOBE_RGB, ::android::hardware::graphics::common::V1_0::ColorMode::DISPLAY_P3, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3377,9 +3040,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::ColorTransform ::android::hardware::graphics::common::V1_0::ColorTransform::CORRECT_DEUTERANOPIA, ::android::hardware::graphics::common::V1_0::ColorTransform::CORRECT_TRITANOPIA, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3391,11 +3054,11 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::Hdr, 3> ::android::hardware::graphics::common::V1_0::Hdr::HDR10, ::android::hardware::graphics::common::V1_0::Hdr::HLG, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} -#endif // HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_COMMON_V1_0_TYPES_H +#endif #ifndef HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_COMMON_V1_1_TYPES_H #define HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_COMMON_V1_1_TYPES_H @@ -3411,1449 +3074,1158 @@ namespace graphics { namespace common { namespace V1_1 { -// Forward declaration for forward reference support: enum class PixelFormat : int32_t; enum class BufferUsage : uint64_t; enum class Dataspace : int32_t; enum class ColorMode : int32_t; enum class RenderIntent : int32_t; -/** - * Pixel formats for graphics buffers. - */ enum class PixelFormat : int32_t { - /** - * 32-bit format that has 8-bit R, G, B, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBA_8888 = 1 /* 0x1 */, - /** - * 32-bit format that has 8-bit R, G, B, and unused components, in that - * order, from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBX_8888 = 2 /* 0x2 */, - /** - * 24-bit format that has 8-bit R, G, and B components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGB_888 = 3 /* 0x3 */, - /** - * 16-bit packed format that has 5-bit R, 6-bit G, and 5-bit B components, - * in that order, from the most-sigfinicant bits to the least-significant - * bits. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGB_565 = 4 /* 0x4 */, - /** - * 32-bit format that has 8-bit B, G, R, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - BGRA_8888 = 5 /* 0x5 */, - /** - * Legacy formats deprecated in favor of YCBCR_420_888. - */ - YCBCR_422_SP = 16 /* 0x10 */, - YCRCB_420_SP = 17 /* 0x11 */, - YCBCR_422_I = 20 /* 0x14 */, - /** - * 64-bit format that has 16-bit R, G, B, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are signed floats, whose interpretation is defined - * by the dataspace. - */ - RGBA_FP16 = 22 /* 0x16 */, - /** - * RAW16 is a single-channel, 16-bit, little endian format, typically - * representing raw Bayer-pattern images from an image sensor, with minimal - * processing. - * - * The exact pixel layout of the data in the buffer is sensor-dependent, and - * needs to be queried from the camera device. - * - * Generally, not all 16 bits are used; more common values are 10 or 12 - * bits. If not all bits are used, the lower-order bits are filled first. - * All parameters to interpret the raw data (black and white points, - * color space, etc) must be queried from the camera device. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - strides are specified in pixels, not in bytes - * - * size = stride * height * 2 - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW16 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data, layout is as - * | defined above. - * Dataspace::DEPTH | Unprocessed implementation-dependent raw - * | depth measurements, opaque with 16 bit - * | samples. - * Other | Unsupported - */ - RAW16 = 32 /* 0x20 */, - /** - * BLOB is used to carry task-specific data which does not have a standard - * image structure. The details of the format are left to the two - * endpoints. - * - * A typical use case is for transporting JPEG-compressed images from the - * Camera HAL to the framework or to applications. - * - * Buffers of this format must have a height of 1, and width equal to their - * size in bytes. - * - * The mapping of the dataspace to buffer contents for BLOB is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::JFIF | An encoded JPEG image - * Dataspace::DEPTH | An android_depth_points buffer - * Dataspace::SENSOR | Sensor event data. - * Other | Unsupported - */ - BLOB = 33 /* 0x21 */, - /** - * A format indicating that the choice of format is entirely up to the - * allocator. - * - * The allocator should examine the usage bits passed in when allocating a - * buffer with this format, and it should derive the pixel format from - * those usage flags. This format must never be used with any of the - * BufferUsage::CPU_* usage flags. - * - * Even when the internally chosen format has an alpha component, the - * clients must assume the alpha vlaue to be 1.0. - * - * The interpretation of the component values is defined by the dataspace. - */ - IMPLEMENTATION_DEFINED = 34 /* 0x22 */, - /** - * This format allows platforms to use an efficient YCbCr/YCrCb 4:2:0 - * buffer layout, while still describing the general format in a - * layout-independent manner. While called YCbCr, it can be used to - * describe formats with either chromatic ordering, as well as - * whole planar or semiplanar layouts. - * - * This format must be accepted by the allocator when BufferUsage::CPU_* - * are set. - * - * Buffers with this format must be locked with IMapper::lockYCbCr. - * Locking with IMapper::lock must return an error. - * - * The interpretation of the component values is defined by the dataspace. - */ - YCBCR_420_888 = 35 /* 0x23 */, - /** - * RAW_OPAQUE is a format for unprocessed raw image buffers coming from an - * image sensor. The actual structure of buffers of this format is - * implementation-dependent. - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW_OPAQUE is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW_OPAQUE = 36 /* 0x24 */, - /** - * RAW10 is a single-channel, 10-bit per pixel, densely packed in each row, - * unprocessed format, usually representing raw Bayer-pattern images coming from - * an image sensor. - * - * In an image buffer with this format, starting from the first pixel of each - * row, each 4 consecutive pixels are packed into 5 bytes (40 bits). Each one - * of the first 4 bytes contains the top 8 bits of each pixel, The fifth byte - * contains the 2 least significant bits of the 4 pixels, the exact layout data - * for each 4 consecutive pixels is illustrated below (Pi[j] stands for the jth - * bit of the ith pixel): - * - * bit 7 bit 0 - * =====|=====|=====|=====|=====|=====|=====|=====| - * Byte 0: |P0[9]|P0[8]|P0[7]|P0[6]|P0[5]|P0[4]|P0[3]|P0[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 1: |P1[9]|P1[8]|P1[7]|P1[6]|P1[5]|P1[4]|P1[3]|P1[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 2: |P2[9]|P2[8]|P2[7]|P2[6]|P2[5]|P2[4]|P2[3]|P2[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 3: |P3[9]|P3[8]|P3[7]|P3[6]|P3[5]|P3[4]|P3[3]|P3[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 4: |P3[1]|P3[0]|P2[1]|P2[0]|P1[1]|P1[0]|P0[1]|P0[0]| - * =============================================== - * - * This format assumes - * - a width multiple of 4 pixels - * - an even height - * - a vertical stride equal to the height - * - strides are specified in bytes, not in pixels - * - * size = stride * height - * - * When stride is equal to width * (10 / 8), there will be no padding bytes at - * the end of each row, the entire image data is densely packed. When stride is - * larger than width * (10 / 8), padding bytes will be present at the end of each - * row (including the last row). - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW10 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW10 = 37 /* 0x25 */, - /** - * RAW12 is a single-channel, 12-bit per pixel, densely packed in each row, - * unprocessed format, usually representing raw Bayer-pattern images coming from - * an image sensor. - * - * In an image buffer with this format, starting from the first pixel of each - * row, each two consecutive pixels are packed into 3 bytes (24 bits). The first - * and second byte contains the top 8 bits of first and second pixel. The third - * byte contains the 4 least significant bits of the two pixels, the exact layout - * data for each two consecutive pixels is illustrated below (Pi[j] stands for - * the jth bit of the ith pixel): - * - * bit 7 bit 0 - * ======|======|======|======|======|======|======|======| - * Byte 0: |P0[11]|P0[10]|P0[ 9]|P0[ 8]|P0[ 7]|P0[ 6]|P0[ 5]|P0[ 4]| - * |------|------|------|------|------|------|------|------| - * Byte 1: |P1[11]|P1[10]|P1[ 9]|P1[ 8]|P1[ 7]|P1[ 6]|P1[ 5]|P1[ 4]| - * |------|------|------|------|------|------|------|------| - * Byte 2: |P1[ 3]|P1[ 2]|P1[ 1]|P1[ 0]|P0[ 3]|P0[ 2]|P0[ 1]|P0[ 0]| - * ======================================================= - * - * This format assumes: - * - a width multiple of 4 pixels - * - an even height - * - a vertical stride equal to the height - * - strides are specified in bytes, not in pixels - * - * size = stride * height - * - * When stride is equal to width * (12 / 8), there will be no padding bytes at - * the end of each row, the entire image data is densely packed. When stride is - * larger than width * (12 / 8), padding bytes will be present at the end of - * each row (including the last row). - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW12 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW12 = 38 /* 0x26 */, - /** - * 0x27 to 0x2A are reserved for flexible formats - * - * - * 32-bit packed format that has 2-bit A, 10-bit B, G, and R components, - * in that order, from the most-sigfinicant bits to the least-significant - * bits. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBA_1010102 = 43 /* 0x2B */, - /** - * 0x100 - 0x1FF - * - * This range is reserved for vendor extensions. Formats in this range - * must support BufferUsage::GPU_TEXTURE. Clients must assume they do not - * have an alpha component. - * - * - * Y8 is a YUV planar format comprised of a WxH Y plane, with each pixel - * being represented by 8 bits. It is equivalent to just the Y plane from - * YV12. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - * size = stride * height - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - Y8 = 538982489 /* 0x20203859 */, - /** - * Y16 is a YUV planar format comprised of a WxH Y plane, with each pixel - * being represented by 16 bits. It is just like Y8, but has double the - * bits per pixel (little endian). - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - strides are specified in pixels, not in bytes - * - * size = stride * height * 2 - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. When the dataspace is - * Dataspace::DEPTH, each pixel is a distance value measured by a depth - * camera, plus an associated confidence value. - */ - Y16 = 540422489 /* 0x20363159 */, - /** - * YV12 is a 4:2:0 YCrCb planar format comprised of a WxH Y plane followed - * by (W/2) x (H/2) Cr and Cb planes. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - * y_size = stride * height - * c_stride = ALIGN(stride/2, 16) - * c_size = c_stride * height/2 - * size = y_size + c_size * 2 - * cr_offset = y_size - * cb_offset = y_size + c_size - * - * This range is reserved for vendor extensions. Formats in this range - * must support BufferUsage::GPU_TEXTURE. Clients must assume they do not - * have an alpha component. - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::GPU_TEXTURE - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - YV12 = 842094169 /* 0x32315659 */, - /** - * 16-bit format that has a single 16-bit depth component. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - DEPTH_16 = 48 /* 0x30 */, - /** - * 32-bit format that has a single 24-bit depth component and, optionally, - * 8 bits that are unused. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - DEPTH_24 = 49 /* 0x31 */, - /** - * 32-bit format that has a 24-bit depth component and an 8-bit stencil - * component packed into 32-bits. - * - * The depth component values are unsigned normalized to the range [0, 1], - * whose interpretation is defined by the dataspace. The stencil values are - * unsigned integers, whose interpretation is defined by the dataspace. - */ - DEPTH_24_STENCIL_8 = 50 /* 0x32 */, - /** - * 32-bit format that has a single 32-bit depth component. - * - * The component values are signed floats, whose interpretation is defined - * by the dataspace. - */ - DEPTH_32F = 51 /* 0x33 */, - /** - * Two-component format that has a 32-bit depth component, an 8-bit stencil - * component, and optionally 24-bits unused. - * - * The depth component values are signed floats, whose interpretation is - * defined by the dataspace. The stencil bits are unsigned integers, whose - * interpretation is defined by the dataspace. - */ - DEPTH_32F_STENCIL_8 = 52 /* 0x34 */, - /** - * 8-bit format that has a single 8-bit stencil component. - * - * The component values are unsigned integers, whose interpretation is - * defined by the dataspace. - */ - STENCIL_8 = 53 /* 0x35 */, - /** - * P010 is a 4:2:0 YCbCr semiplanar format comprised of a WxH Y plane - * followed immediately by a Wx(H/2) CbCr plane. Each sample is - * represented by a 16-bit little-endian value, with the lower 6 bits set - * to zero. - * - * This format assumes - * - an even height - * - a vertical stride equal to the height - * - * stride_in_bytes = stride * 2 - * y_size = stride_in_bytes * height - * cbcr_size = stride_in_bytes * (height / 2) - * cb_offset = y_size - * cr_offset = cb_offset + 2 - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::VIDEO_* - * - BufferUsage::CPU_* - * - BufferUsage::GPU_TEXTURE - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - * - * This format is appropriate for 10bit video content. - * - * Buffers with this format must be locked with IMapper::lockYCbCr - * or with IMapper::lock. - */ - YCBCR_P010 = 54 /* 0x36 */, + + + + + + + + RGBA_8888 = 1 , + + + + + + + + RGBX_8888 = 2 , + + + + + + + + RGB_888 = 3 , + + + + + + + + + RGB_565 = 4 , + + + + + + + + BGRA_8888 = 5 , + + + + YCBCR_422_SP = 16 , + YCRCB_420_SP = 17 , + YCBCR_422_I = 20 , + + + + + + + + RGBA_FP16 = 22 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW16 = 32 , + + + + + + + + + + + + + + + + + + + + + + BLOB = 33 , + + + + + + + + + + + + + + + IMPLEMENTATION_DEFINED = 34 , + + + + + + + + + + + + + + + + YCBCR_420_888 = 35 , + + + + + + + + + + + + + + + + + + + + + RAW_OPAQUE = 36 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW10 = 37 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW12 = 38 , + + + + + + + + + + + + RGBA_1010102 = 43 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Y8 = 538982489 , + + + + + + + + + + + + + + + + + + + + + + + + + + Y16 = 540422489 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + YV12 = 842094169 , + + + + + + + DEPTH_16 = 48 , + + + + + + + + DEPTH_24 = 49 , + + + + + + + + + DEPTH_24_STENCIL_8 = 50 , + + + + + + + DEPTH_32F = 51 , + + + + + + + + + DEPTH_32F_STENCIL_8 = 52 , + + + + + + + STENCIL_8 = 53 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + YCBCR_P010 = 54 , }; -/** - * Buffer usage definitions. - */ enum class BufferUsage : uint64_t { - /** - * bit 0-3 is an enum - */ - CPU_READ_MASK = 15ull /* 0xfULL */, - /** - * buffer is never read by CPU - */ + + + + CPU_READ_MASK = 15ull , + + + CPU_READ_NEVER = 0ull, - /** - * buffer is rarely read by CPU - */ CPU_READ_RARELY = 2ull, - /** - * buffer is often read by CPU - */ CPU_READ_OFTEN = 3ull, - /** - * bit 4-7 is an enum - */ - CPU_WRITE_MASK = 240ull /* (0xfULL << 4) */, - /** - * buffer is never written by CPU - */ - CPU_WRITE_NEVER = 0ull /* (0 << 4) */, - /** - * buffer is rarely written by CPU - */ - CPU_WRITE_RARELY = 32ull /* (2 << 4) */, - /** - * buffer is often written by CPU - */ - CPU_WRITE_OFTEN = 48ull /* (3 << 4) */, - /** - * buffer is used as a GPU texture - */ - GPU_TEXTURE = 256ull /* (1ULL << 8) */, - /** - * buffer is used as a GPU render target - */ - GPU_RENDER_TARGET = 512ull /* (1ULL << 9) */, - /** - * bit 10 must be zero - * - * - * buffer is used as a composer HAL overlay layer - */ - COMPOSER_OVERLAY = 2048ull /* (1ULL << 11) */, - /** - * buffer is used as a composer HAL client target - */ - COMPOSER_CLIENT_TARGET = 4096ull /* (1ULL << 12) */, - /** - * bit 13 must be zero - * - * - * Buffer is allocated with hardware-level protection against copying the - * contents (or information derived from the contents) into unprotected - * memory. - */ - PROTECTED = 16384ull /* (1ULL << 14) */, - /** - * buffer is used as a hwcomposer HAL cursor layer - */ - COMPOSER_CURSOR = 32768ull /* (1ULL << 15) */, - /** - * buffer is used as a video encoder input - */ - VIDEO_ENCODER = 65536ull /* (1ULL << 16) */, - /** - * buffer is used as a camera HAL output - */ - CAMERA_OUTPUT = 131072ull /* (1ULL << 17) */, - /** - * buffer is used as a camera HAL input - */ - CAMERA_INPUT = 262144ull /* (1ULL << 18) */, - /** - * bit 19 must be zero - * - * - * buffer is used as a renderscript allocation - */ - RENDERSCRIPT = 1048576ull /* (1ULL << 20) */, - /** - * bit 21 must be zero - * - * - * buffer is used as a video decoder output - */ - VIDEO_DECODER = 4194304ull /* (1ULL << 22) */, - /** - * buffer is used as a sensor direct report output - */ - SENSOR_DIRECT_DATA = 8388608ull /* (1ULL << 23) */, - /** - * buffer is used as as an OpenGL shader storage or uniform - * buffer object - */ - GPU_DATA_BUFFER = 16777216ull /* (1ULL << 24) */, - /** - * bits 25-27 must be zero and are reserved for future versions - * - * - * bits 28-31 are reserved for vendor extensions - */ - VENDOR_MASK = 4026531840ull /* (0xfULL << 28) */, - /** - * bits 32-47 must be zero and are reserved for future versions - * - * - * bits 48-63 are reserved for vendor extensions - */ - VENDOR_MASK_HI = 18446462598732840960ull /* (0xffffULL << 48) */, - /** - * buffer is used as a cube map texture - */ - GPU_CUBE_MAP = 33554432ull /* (1ULL << 25) */, - /** - * buffer contains a complete mipmap hierarchy - */ - GPU_MIPMAP_COMPLETE = 67108864ull /* (1ULL << 26) */, + + + + CPU_WRITE_MASK = 240ull , + + + + CPU_WRITE_NEVER = 0ull , + + + + CPU_WRITE_RARELY = 32ull , + + + + CPU_WRITE_OFTEN = 48ull , + + + + GPU_TEXTURE = 256ull , + + + + GPU_RENDER_TARGET = 512ull , + + + + + + + COMPOSER_OVERLAY = 2048ull , + + + + COMPOSER_CLIENT_TARGET = 4096ull , + + + + + + + + + PROTECTED = 16384ull , + + + + COMPOSER_CURSOR = 32768ull , + + + + VIDEO_ENCODER = 65536ull , + + + + CAMERA_OUTPUT = 131072ull , + + + + CAMERA_INPUT = 262144ull , + + + + + + + RENDERSCRIPT = 1048576ull , + + + + + + + VIDEO_DECODER = 4194304ull , + + + + SENSOR_DIRECT_DATA = 8388608ull , + + + + + GPU_DATA_BUFFER = 16777216ull , + + + + + + + VENDOR_MASK = 4026531840ull , + + + + + + + VENDOR_MASK_HI = 18446462598732840960ull , + + + + GPU_CUBE_MAP = 33554432ull , + + + + GPU_MIPMAP_COMPLETE = 67108864ull , }; enum class Dataspace : int32_t { - /** - * Default-assumption data space, when not explicitly specified. - * - * It is safest to assume the buffer is an image with sRGB primaries and - * encoding ranges, but the consumer and/or the producer of the data may - * simply be using defaults. No automatic gamma transform should be - * expected, except for a possible display gamma transform when drawn to a - * screen. - */ - UNKNOWN = 0 /* 0x0 */, - /** - * Arbitrary dataspace with manually defined characteristics. Definition - * for colorspaces or other meaning must be communicated separately. - * - * This is used when specifying primaries, transfer characteristics, - * etc. separately. - * - * A typical use case is in video encoding parameters (e.g. for H.264), - * where a colorspace can have separately defined primaries, transfer - * characteristics, etc. - */ - ARBITRARY = 1 /* 0x1 */, - /** - * Color-description aspects - * - * The following aspects define various characteristics of the color - * specification. These represent bitfields, so that a data space value - * can specify each of them independently. - */ + + + + + + + + + + UNKNOWN = 0 , + + + + + + + + + + + + ARBITRARY = 1 , + + + + + + + STANDARD_SHIFT = 16, - /** - * Standard aspect - * - * Defines the chromaticity coordinates of the source primaries in terms of - * the CIE 1931 definition of x and y specified in ISO 11664-1. - */ - STANDARD_MASK = 4128768 /* (63 << STANDARD_SHIFT) */, - /** - * Chromacity coordinates are unknown or are determined by the application. - * Implementations shall use the following suggested standards: - * - * All YCbCr formats: BT709 if size is 720p or larger (since most video - * content is letterboxed this corresponds to width is - * 1280 or greater, or height is 720 or greater). - * BT601_625 if size is smaller than 720p or is JPEG. - * All RGB formats: BT709. - * - * For all other formats standard is undefined, and implementations should use - * an appropriate standard for the data represented. - */ - STANDARD_UNSPECIFIED = 0 /* (0 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2126, KB = 0.0722 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT709 = 65536 /* (1 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - */ - STANDARD_BT601_625 = 131072 /* (2 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.222, KB = 0.071 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT601_625_UNADJUSTED = 196608 /* (3 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - */ - STANDARD_BT601_525 = 262144 /* (4 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.212, KB = 0.087 luminance interpretation - * for RGB conversion (as in SMPTE 240M). - */ - STANDARD_BT601_525_UNADJUSTED = 327680 /* (5 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2627, KB = 0.0593 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT2020 = 393216 /* (6 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2627, KB = 0.0593 luminance interpretation - * for RGB conversion using the linear domain. - */ - STANDARD_BT2020_CONSTANT_LUMINANCE = 458752 /* (7 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.21 0.71 - * blue 0.14 0.08 - * red 0.67 0.33 - * white (C) 0.310 0.316 - * - * Use the unadjusted KR = 0.30, KB = 0.11 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT470M = 524288 /* (8 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.243 0.692 - * blue 0.145 0.049 - * red 0.681 0.319 - * white (C) 0.310 0.316 - * - * Use the unadjusted KR = 0.254, KB = 0.068 luminance interpretation - * for RGB conversion. - */ - STANDARD_FILM = 589824 /* (9 << STANDARD_SHIFT) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. (DCI-P3) - * Primaries: x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - */ - STANDARD_DCI_P3 = 655360 /* (10 << STANDARD_SHIFT) */, - /** - * Adobe RGB - * Primaries: x y - * green 0.210 0.710 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - */ - STANDARD_ADOBE_RGB = 720896 /* (11 << STANDARD_SHIFT) */, + + + + + + + STANDARD_MASK = 4128768 , + + + + + + + + + + + + + + STANDARD_UNSPECIFIED = 0 , + + + + + + + + + + + STANDARD_BT709 = 65536 , + + + + + + + + + + + + + STANDARD_BT601_625 = 131072 , + + + + + + + + + + + STANDARD_BT601_625_UNADJUSTED = 196608 , + + + + + + + + + + + + + STANDARD_BT601_525 = 262144 , + + + + + + + + + + + STANDARD_BT601_525_UNADJUSTED = 327680 , + + + + + + + + + + + STANDARD_BT2020 = 393216 , + + + + + + + + + + + STANDARD_BT2020_CONSTANT_LUMINANCE = 458752 , + + + + + + + + + + + STANDARD_BT470M = 524288 , + + + + + + + + + + + STANDARD_FILM = 589824 , + + + + + + + + + STANDARD_DCI_P3 = 655360 , + + + + + + + + + STANDARD_ADOBE_RGB = 720896 , TRANSFER_SHIFT = 22, - /** - * Transfer aspect - * - * Transfer characteristics are the opto-electronic transfer characteristic - * at the source as a function of linear optical intensity (luminance). - * - * For digital signals, E corresponds to the recorded value. Normally, the - * transfer function is applied in RGB space to each of the R, G and B - * components independently. This may result in color shift that can be - * minized by applying the transfer function in Lab space only for the L - * component. Implementation may apply the transfer function in RGB space - * for all pixel formats if desired. - */ - TRANSFER_MASK = 130023424 /* (31 << TRANSFER_SHIFT) */, - /** - * Transfer characteristics are unknown or are determined by the - * application. - * - * Implementations should use the following transfer functions: - * - * For YCbCr formats: use TRANSFER_SMPTE_170M - * For RGB formats: use TRANSFER_SRGB - * - * For all other formats transfer function is undefined, and implementations - * should use an appropriate standard for the data represented. - */ - TRANSFER_UNSPECIFIED = 0 /* (0 << TRANSFER_SHIFT) */, - /** - * Transfer characteristic curve: - * E = L - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_LINEAR = 4194304 /* (1 << TRANSFER_SHIFT) */, - /** - * Transfer characteristic curve: - * - * E = 1.055 * L^(1/2.4) - 0.055 for 0.0031308 <= L <= 1 - * = 12.92 * L for 0 <= L < 0.0031308 - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_SRGB = 8388608 /* (2 << TRANSFER_SHIFT) */, - /** - * BT.601 525, BT.601 625, BT.709, BT.2020 - * - * Transfer characteristic curve: - * E = 1.099 * L ^ 0.45 - 0.099 for 0.018 <= L <= 1 - * = 4.500 * L for 0 <= L < 0.018 - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_SMPTE_170M = 12582912 /* (3 << TRANSFER_SHIFT) */, - /** - * Assumed display gamma 2.2. - * - * Transfer characteristic curve: - * E = L ^ (1/2.2) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_2 = 16777216 /* (4 << TRANSFER_SHIFT) */, - /** - * display gamma 2.6. - * - * Transfer characteristic curve: - * E = L ^ (1/2.6) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_6 = 20971520 /* (5 << TRANSFER_SHIFT) */, - /** - * display gamma 2.8. - * - * Transfer characteristic curve: - * E = L ^ (1/2.8) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_8 = 25165824 /* (6 << TRANSFER_SHIFT) */, - /** - * SMPTE ST 2084 (Dolby Perceptual Quantizer) - * - * Transfer characteristic curve: - * E = ((c1 + c2 * L^n) / (1 + c3 * L^n)) ^ m - * c1 = c3 - c2 + 1 = 3424 / 4096 = 0.8359375 - * c2 = 32 * 2413 / 4096 = 18.8515625 - * c3 = 32 * 2392 / 4096 = 18.6875 - * m = 128 * 2523 / 4096 = 78.84375 - * n = 0.25 * 2610 / 4096 = 0.1593017578125 - * L - luminance of image 0 <= L <= 1 for HDR colorimetry. - * L = 1 corresponds to 10000 cd/m2 - * E - corresponding electrical signal - */ - TRANSFER_ST2084 = 29360128 /* (7 << TRANSFER_SHIFT) */, - /** - * ARIB STD-B67 Hybrid Log Gamma - * - * Transfer characteristic curve: - * E = r * L^0.5 for 0 <= L <= 1 - * = a * ln(L - b) + c for 1 < L - * a = 0.17883277 - * b = 0.28466892 - * c = 0.55991073 - * r = 0.5 - * L - luminance of image 0 <= L for HDR colorimetry. L = 1 corresponds - * to reference white level of 100 cd/m2 - * E - corresponding electrical signal - */ - TRANSFER_HLG = 33554432 /* (8 << TRANSFER_SHIFT) */, + + + + + + + + + + + + + + TRANSFER_MASK = 130023424 , + + + + + + + + + + + + + TRANSFER_UNSPECIFIED = 0 , + + + + + + + TRANSFER_LINEAR = 4194304 , + + + + + + + + + TRANSFER_SRGB = 8388608 , + + + + + + + + + + TRANSFER_SMPTE_170M = 12582912 , + + + + + + + + + TRANSFER_GAMMA2_2 = 16777216 , + + + + + + + + + TRANSFER_GAMMA2_6 = 20971520 , + + + + + + + + + TRANSFER_GAMMA2_8 = 25165824 , + + + + + + + + + + + + + + + TRANSFER_ST2084 = 29360128 , + + + + + + + + + + + + + + + TRANSFER_HLG = 33554432 , RANGE_SHIFT = 27, - /** - * Range aspect - * - * Defines the range of values corresponding to the unit range of 0-1. - * This is defined for YCbCr only, but can be expanded to RGB space. - */ - RANGE_MASK = 939524096 /* (7 << RANGE_SHIFT) */, - /** - * Range is unknown or are determined by the application. Implementations - * shall use the following suggested ranges: - * - * All YCbCr formats: limited range. - * All RGB or RGBA formats (including RAW and Bayer): full range. - * All Y formats: full range - * - * For all other formats range is undefined, and implementations should use - * an appropriate range for the data represented. - */ - RANGE_UNSPECIFIED = 0 /* (0 << RANGE_SHIFT) */, - /** - * Full range uses all values for Y, Cb and Cr from - * 0 to 2^b-1, where b is the bit depth of the color format. - */ - RANGE_FULL = 134217728 /* (1 << RANGE_SHIFT) */, - /** - * Limited range uses values 16/256*2^b to 235/256*2^b for Y, and - * 1/16*2^b to 15/16*2^b for Cb, Cr, R, G and B, where b is the bit depth of - * the color format. - * - * E.g. For 8-bit-depth formats: - * Luma (Y) samples should range from 16 to 235, inclusive - * Chroma (Cb, Cr) samples should range from 16 to 240, inclusive - * - * For 10-bit-depth formats: - * Luma (Y) samples should range from 64 to 940, inclusive - * Chroma (Cb, Cr) samples should range from 64 to 960, inclusive - */ - RANGE_LIMITED = 268435456 /* (2 << RANGE_SHIFT) */, - /** - * Extended range is used for scRGB. Intended for use with - * floating point pixel formats. [0.0 - 1.0] is the standard - * sRGB space. Values outside the range 0.0 - 1.0 can encode - * color outside the sRGB gamut. - * Used to blend / merge multiple dataspaces on a single display. - */ - RANGE_EXTENDED = 402653184 /* (3 << RANGE_SHIFT) */, - /** - * Legacy dataspaces - * - * - * sRGB linear encoding: - * - * The red, green, and blue components are stored in sRGB space, but - * are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are encoded using the full range ([0,255] for 8-bit) for all - * components. - */ - SRGB_LINEAR = 512 /* 0x200 */, - V0_SRGB_LINEAR = 138477568 /* ((STANDARD_BT709 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * scRGB linear encoding: - * - * The red, green, and blue components are stored in extended sRGB space, - * but are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are floating point. - * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. - * Values beyond the range [0.0 - 1.0] would correspond to other colors - * spaces and/or HDR content. - */ - V0_SCRGB_LINEAR = 406913024 /* ((STANDARD_BT709 | TRANSFER_LINEAR) | RANGE_EXTENDED) */, - /** - * sRGB gamma encoding: - * - * The red, green and blue components are stored in sRGB space, and - * converted to linear space when read, using the SRGB transfer function - * for each of the R, G and B components. When written, the inverse - * transformation is performed. - * - * The alpha component, if present, is always stored in linear space and - * is left unmodified when read or written. - * - * Use full range and BT.709 standard. - */ - SRGB = 513 /* 0x201 */, - V0_SRGB = 142671872 /* ((STANDARD_BT709 | TRANSFER_SRGB) | RANGE_FULL) */, - /** - * scRGB: - * - * The red, green, and blue components are stored in extended sRGB space, - * but are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are floating point. - * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. - * Values beyond the range [0.0 - 1.0] would correspond to other colors - * spaces and/or HDR content. - */ - V0_SCRGB = 411107328 /* ((STANDARD_BT709 | TRANSFER_SRGB) | RANGE_EXTENDED) */, - /** - * YCbCr Colorspaces - * ----------------- - * - * Primaries are given using (x,y) coordinates in the CIE 1931 definition - * of x and y specified by ISO 11664-1. - * - * Transfer characteristics are the opto-electronic transfer characteristic - * at the source as a function of linear optical intensity (luminance). - * - * - * JPEG File Interchange Format (JFIF) - * - * Same model as BT.601-625, but all values (Y, Cb, Cr) range from 0 to 255 - * - * Use full range, BT.601 transfer and BT.601_625 standard. - */ - JFIF = 257 /* 0x101 */, - V0_JFIF = 146931712 /* ((STANDARD_BT601_625 | TRANSFER_SMPTE_170M) | RANGE_FULL) */, - /** - * ITU-R Recommendation 601 (BT.601) - 625-line - * - * Standard-definition television, 625 Lines (PAL) - * - * Use limited range, BT.601 transfer and BT.601_625 standard. - */ - BT601_625 = 258 /* 0x102 */, - V0_BT601_625 = 281149440 /* ((STANDARD_BT601_625 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * ITU-R Recommendation 601 (BT.601) - 525-line - * - * Standard-definition television, 525 Lines (NTSC) - * - * Use limited range, BT.601 transfer and BT.601_525 standard. - */ - BT601_525 = 259 /* 0x103 */, - V0_BT601_525 = 281280512 /* ((STANDARD_BT601_525 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * ITU-R Recommendation 709 (BT.709) - * - * High-definition television - * - * Use limited range, BT.709 transfer and BT.709 standard. - */ - BT709 = 260 /* 0x104 */, - V0_BT709 = 281083904 /* ((STANDARD_BT709 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. - * - * Digital Cinema DCI-P3 - * - * Use full range, linear transfer and D65 DCI-P3 standard - */ - DCI_P3_LINEAR = 139067392 /* ((STANDARD_DCI_P3 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. - * - * Digital Cinema DCI-P3 - * - * Use full range, gamma 2.6 transfer and D65 DCI-P3 standard - * Note: Application is responsible for gamma encoding the data as - * a 2.6 gamma encoding is not supported in HW. - */ - DCI_P3 = 155844608 /* ((STANDARD_DCI_P3 | TRANSFER_GAMMA2_6) | RANGE_FULL) */, - /** - * Display P3 - * - * Display P3 uses same primaries and white-point as DCI-P3 - * linear transfer function makes this the same as DCI_P3_LINEAR. - */ - DISPLAY_P3_LINEAR = 139067392 /* ((STANDARD_DCI_P3 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * Display P3 - * - * Use same primaries and white-point as DCI-P3 - * but sRGB transfer function. - */ - DISPLAY_P3 = 143261696 /* ((STANDARD_DCI_P3 | TRANSFER_SRGB) | RANGE_FULL) */, - /** - * Adobe RGB - * - * Use full range, gamma 2.2 transfer and Adobe RGB primaries - * Note: Application is responsible for gamma encoding the data as - * a 2.2 gamma encoding is not supported in HW. - */ - ADOBE_RGB = 151715840 /* ((STANDARD_ADOBE_RGB | TRANSFER_GAMMA2_2) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, linear transfer and BT2020 standard - */ - BT2020_LINEAR = 138805248 /* ((STANDARD_BT2020 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, BT.709 transfer and BT2020 standard - */ - BT2020 = 147193856 /* ((STANDARD_BT2020 | TRANSFER_SMPTE_170M) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, SMPTE 2084 (PQ) transfer and BT2020 standard - */ - BT2020_PQ = 163971072 /* ((STANDARD_BT2020 | TRANSFER_ST2084) | RANGE_FULL) */, - /** - * Data spaces for non-color formats - * - * - * The buffer contains depth ranging measurements from a depth camera. - * This value is valid with formats: - * HAL_PIXEL_FORMAT_Y16: 16-bit samples, consisting of a depth measurement - * and an associated confidence value. The 3 MSBs of the sample make - * up the confidence value, and the low 13 LSBs of the sample make up - * the depth measurement. - * For the confidence section, 0 means 100% confidence, 1 means 0% - * confidence. The mapping to a linear float confidence value between - * 0.f and 1.f can be obtained with - * float confidence = (((depthSample >> 13) - 1) & 0x7) / 7.0f; - * The depth measurement can be extracted simply with - * uint16_t range = (depthSample & 0x1FFF); - * HAL_PIXEL_FORMAT_BLOB: A depth point cloud, as - * a variable-length float (x,y,z, confidence) coordinate point list. - * The point cloud will be represented with the android_depth_points - * structure. - */ - DEPTH = 4096 /* 0x1000 */, - /** - * The buffer contains sensor events from sensor direct report. - * This value is valid with formats: - * HAL_PIXEL_FORMAT_BLOB: an array of sensor event structure that forms - * a lock free queue. Format of sensor event structure is specified - * in Sensors HAL. - */ - SENSOR = 4097 /* 0x1001 */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use limited range, BT.709 transfer and BT2020 standard - */ - BT2020_ITU = 281411584 /* ((STANDARD_BT2020 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * ITU-R Recommendation 2100 (BT.2100) - * - * High dynamic range television - * - * Use limited/full range, PQ/HLG transfer, and BT2020 standard - * limited range is the preferred / normative definition for BT.2100 - */ - BT2020_ITU_PQ = 298188800 /* ((STANDARD_BT2020 | TRANSFER_ST2084) | RANGE_LIMITED) */, - BT2020_ITU_HLG = 302383104 /* ((STANDARD_BT2020 | TRANSFER_HLG) | RANGE_LIMITED) */, - BT2020_HLG = 168165376 /* ((STANDARD_BT2020 | TRANSFER_HLG) | RANGE_FULL) */, + + + + + + + RANGE_MASK = 939524096 , + + + + + + + + + + + + RANGE_UNSPECIFIED = 0 , + + + + + RANGE_FULL = 134217728 , + + + + + + + + + + + + + + RANGE_LIMITED = 268435456 , + + + + + + + + RANGE_EXTENDED = 402653184 , + + + + + + + + + + + + + + SRGB_LINEAR = 512 , + V0_SRGB_LINEAR = 138477568 , + + + + + + + + + + + + + V0_SCRGB_LINEAR = 406913024 , + + + + + + + + + + + + + + SRGB = 513 , + V0_SRGB = 142671872 , + + + + + + + + + + + + + V0_SCRGB = 411107328 , + + + + + + + + + + + + + + + + + + JFIF = 257 , + V0_JFIF = 146931712 , + + + + + + + + BT601_625 = 258 , + V0_BT601_625 = 281149440 , + + + + + + + + BT601_525 = 259 , + V0_BT601_525 = 281280512 , + + + + + + + + BT709 = 260 , + V0_BT709 = 281083904 , + + + + + + + + DCI_P3_LINEAR = 139067392 , + + + + + + + + + + DCI_P3 = 155844608 , + + + + + + + DISPLAY_P3_LINEAR = 139067392 , + + + + + + + DISPLAY_P3 = 143261696 , + + + + + + + + ADOBE_RGB = 151715840 , + + + + + + + + BT2020_LINEAR = 138805248 , + + + + + + + + BT2020 = 147193856 , + + + + + + + + BT2020_PQ = 163971072 , + + + + + + + + + + + + + + + + + + + + + + DEPTH = 4096 , + + + + + + + + SENSOR = 4097 , + + + + + + + + BT2020_ITU = 281411584 , + + + + + + + + + BT2020_ITU_PQ = 298188800 , + BT2020_ITU_HLG = 302383104 , + BT2020_HLG = 168165376 , }; enum class ColorMode : int32_t { - /** - * DEFAULT is the "native" gamut of the display. - * White Point: Vendor/OEM defined - * Panel Gamma: Vendor/OEM defined (typically 2.2) - * Rendering Intent: Vendor/OEM defined (typically 'enhanced') - */ NATIVE = 0, - /** - * STANDARD_BT601_625 corresponds with display - * settings that implement the ITU-R Recommendation BT.601 - * or Rec 601. Using 625 line version - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_625 = 1, - /** - * Primaries: - * x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.222, KB = 0.071 luminance interpretation - * for RGB conversion. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_625_UNADJUSTED = 2, - /** - * Primaries: - * x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_525 = 3, - /** - * Primaries: - * x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.212, KB = 0.087 luminance interpretation - * for RGB conversion (as in SMPTE 240M). - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_525_UNADJUSTED = 4, - /** - * REC709 corresponds with display settings that implement - * the ITU-R Recommendation BT.709 / Rec. 709 for high-definition television. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * HDTV REC709 Inverse Gamma Correction (IGC): V represents normalized - * (with [0 to 1] range) value of R, G, or B. - * - * if Vnonlinear < 0.081 - * Vlinear = Vnonlinear / 4.5 - * else - * Vlinear = ((Vnonlinear + 0.099) / 1.099) ^ (1/0.45) - * - * HDTV REC709 Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.5 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear) ^ 0.45 – 0.099 - */ STANDARD_BT709 = 5, - /** - * DCI_P3 corresponds with display settings that implement - * SMPTE EG 432-1 and SMPTE RP 431-2 - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - * - * Gamma: 2.6 - */ DCI_P3 = 6, - /** - * SRGB corresponds with display settings that implement - * the sRGB color space. Uses the same primaries as ITU-R Recommendation - * BT.709 - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * PC/Internet (sRGB) Inverse Gamma Correction (IGC): - * - * if Vnonlinear ≤ 0.03928 - * Vlinear = Vnonlinear / 12.92 - * else - * Vlinear = ((Vnonlinear + 0.055)/1.055) ^ 2.4 - * - * PC/Internet (sRGB) Gamma Correction (GC): - * - * if Vlinear ≤ 0.0031308 - * Vnonlinear = 12.92 * Vlinear - * else - * Vnonlinear = 1.055 * (Vlinear)^(1/2.4) – 0.055 - */ SRGB = 7, - /** - * ADOBE_RGB corresponds with the RGB color space developed - * by Adobe Systems, Inc. in 1998. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.210 0.710 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Gamma: 2.2 - */ ADOBE_RGB = 8, - /** - * DISPLAY_P3 is a color space that uses the DCI_P3 primaries, - * the D65 white point and the SRGB transfer functions. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - * - * PC/Internet (sRGB) Gamma Correction (GC): - * - * if Vlinear ≤ 0.0030186 - * Vnonlinear = 12.92 * Vlinear - * else - * Vnonlinear = 1.055 * (Vlinear)^(1/2.4) – 0.055 - * - * Note: In most cases sRGB transfer function will be fine. - */ DISPLAY_P3 = 9, - /** - * BT2020 corresponds with display settings that implement the ITU-R - * Recommendation BT.2020 / Rec. 2020 for UHDTV. - * - * Primaries: - * x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * Inverse Gamma Correction (IGC): V represents normalized (with [0 to 1] - * range) value of R, G, or B. - * - * if Vnonlinear < b * 4.5 - * Vlinear = Vnonlinear / 4.5 - * else - * Vlinear = ((Vnonlinear + (a - 1)) / a) ^ (1/0.45) - * - * Gamma Correction (GC): - * - * if Vlinear < b - * Vnonlinear = 4.5 * Vlinear - * else - * Vnonlinear = a * Vlinear ^ 0.45 - (a - 1) - * - * where - * - * a = 1.09929682680944, b = 0.018053968510807 - * - * For practical purposes, these a/b values can be used instead - * - * a = 1.099, b = 0.018 for 10-bit display systems - * a = 1.0993, b = 0.0181 for 12-bit display systems - */ BT2020 = 10, - /** - * BT2100_PQ and BT2100_HLG correspond with display settings that - * implement the ITU-R Recommendation BT.2100 / Rec. 2100 for HDR TV. - * - * Primaries: - * x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * For BT2100_PQ, the transfer function is Perceptual Quantizer (PQ). For - * BT2100_HLG, the transfer function is Hybrid Log-Gamma (HLG). - */ BT2100_PQ = 11, BT2100_HLG = 12, }; -/** - * RenderIntent defines the mapping from color mode colors to display colors. - * - * A render intent must not change how it maps colors when the color mode - * changes. That is to say that when a render intent maps color C to color C', - * the fact that color C can have different pixel values in different color - * modes should not affect the mapping. - * - * RenderIntent overrides the render intents defined for individual color - * modes. It is ignored when the color mode is ColorMode::NATIVE, because - * ColorMode::NATIVE colors are already display colors. - */ enum class RenderIntent : int32_t { - /** - * Colors in the display gamut are unchanged. Colors out of the display - * gamut are hard-clipped. - * - * This implies that the display must have been calibrated unless - * ColorMode::NATIVE is the only supported color mode. - */ COLORIMETRIC = 0, - /** - * Enhance colors that are in the display gamut. Colors out of the display - * gamut are hard-clipped. - * - * The enhancement typically picks the biggest standard color space (e.g. - * DCI-P3) that is narrower than the display gamut and stretches it to the - * display gamut. The stretching is recommended to preserve skin tones. - */ ENHANCE = 1, - /** - * Tone map high-dynamic-range colors to the display's dynamic range. The - * dynamic range of the colors are communicated separately. After tone - * mapping, the mapping to the display gamut is as defined in - * COLORIMETRIC. - */ TONE_MAP_COLORIMETRIC = 2, - /** - * Tone map high-dynamic-range colors to the display's dynamic range. The - * dynamic range of the colors are communicated separately. After tone - * mapping, the mapping to the display gamut is as defined in ENHANCE. - * - * The tone mapping step and the enhancing step must match - * TONE_MAP_COLORIMETRIC and ENHANCE respectively when they are also - * supported. - */ TONE_MAP_ENHANCE = 3, }; -// -// type declarations for package -// template static inline std::string toString(int32_t o); @@ -5050,9 +4422,6 @@ constexpr int32_t& operator&=(int32_t& v, return v; } -// -// type header definitions for package -// template <> inline std::string toString<::android::hardware::graphics::common::V1_1::PixelFormat>(int32_t o) { @@ -6546,15 +5915,12 @@ static inline std::string toString(::android::hardware::graphics::common::V1_1:: return os; } -} // namespace V1_1 -} // namespace common -} // namespace graphics -} // namespace hardware -} // namespace android +} +} +} +} +} -// -// global type declarations for package -// namespace android { namespace hardware { @@ -6590,9 +5956,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_1::PixelFormat, 2 ::android::hardware::graphics::common::V1_1::PixelFormat::STENCIL_8, ::android::hardware::graphics::common::V1_1::PixelFormat::YCBCR_P010, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -6626,9 +5992,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_1::BufferUsage, 2 ::android::hardware::graphics::common::V1_1::BufferUsage::GPU_CUBE_MAP, ::android::hardware::graphics::common::V1_1::BufferUsage::GPU_MIPMAP_COMPLETE, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -6698,9 +6064,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_1::Dataspace, 61> ::android::hardware::graphics::common::V1_1::Dataspace::BT2020_ITU_HLG, ::android::hardware::graphics::common::V1_1::Dataspace::BT2020_HLG, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -6722,9 +6088,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_1::ColorMode, 13> ::android::hardware::graphics::common::V1_1::ColorMode::BT2100_PQ, ::android::hardware::graphics::common::V1_1::ColorMode::BT2100_HLG, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -6737,8 +6103,8 @@ constexpr std::array<::android::hardware::graphics::common::V1_1::RenderIntent, ::android::hardware::graphics::common::V1_1::RenderIntent::TONE_MAP_COLORIMETRIC, ::android::hardware::graphics::common::V1_1::RenderIntent::TONE_MAP_ENHANCE, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} -#endif // HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_COMMON_V1_1_TYPES_H +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapper.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapper.h index ce126427d..f060f1c1e 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapper.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapper.h @@ -12,7 +12,6 @@ namespace graphics { namespace mapper { namespace V2_0 { -// Forward declaration for forward reference support: enum class Error : int32_t; struct YCbCrLayout; @@ -25,34 +24,8 @@ enum class Error : int32_t { UNSUPPORTED = 7, }; -/** - * A buffer descriptor is an implementation-defined opaque data returned by - * createDescriptor. It describes the properties of a buffer and is consumed - * by the allocator. - */ typedef ::android::hardware::hidl_vec BufferDescriptor; -/** - * Structure for describing YCbCr formats for consumption by applications. - * This is used with PixelFormat::YCBCR_*_888. - * - * Buffer chroma subsampling is defined in the format. - * e.g. PixelFormat::YCBCR_420_888 has subsampling 4:2:0. - * - * Buffers must have a 8 bit depth. - * - * y, cb, and cr point to the first byte of their respective planes. - * - * Stride describes the distance in bytes from the first value of one row of - * the image to the first value of the next row. It includes the width of the - * image plus padding. - * yStride is the stride of the luma plane. - * cStride is the stride of the chroma planes. - * - * chromaStep is the distance in bytes from one chroma pixel value to the - * next. This is 2 bytes for semiplanar (because chroma values are interleaved - * and each chroma value is one byte) and 1 for planar. - */ struct YCbCrLayout final { void* y; void* cb; @@ -62,9 +35,6 @@ struct YCbCrLayout final { uint32_t chromaStep; }; -// -// type declarations for package -// template static inline std::string toString(int32_t o); @@ -107,11 +77,7 @@ constexpr int32_t& operator&=(int32_t& v, static inline std::string toString( const ::android::hardware::graphics::mapper::V2_0::YCbCrLayout& o); -// operator== and operator!= are not generated for YCbCrLayout -// -// type header definitions for package -// template <> inline std::string toString<::android::hardware::graphics::mapper::V2_0::Error>(int32_t o) { @@ -218,17 +184,17 @@ static inline std::string toString( return os; } -// operator== and operator!= are not generated for YCbCrLayout -} // namespace V2_0 -} // namespace mapper -} // namespace graphics -} // namespace hardware -} // namespace android -// -// global type declarations for package -// +} +} +} +} +} + + + + namespace android { namespace hardware { @@ -243,8 +209,8 @@ constexpr std::array<::android::hardware::graphics::mapper::V2_0::Error, 6> ::android::hardware::graphics::mapper::V2_0::Error::NO_RESOURCES, ::android::hardware::graphics::mapper::V2_0::Error::UNSUPPORTED, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} -#endif // HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_MAPPER_V2_0_TYPES_H +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapperextensions.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapperextensions.h index 79fddd18e..6890973e0 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapperextensions.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapperextensions.h @@ -13,7 +13,6 @@ namespace display { namespace mapperextensions { namespace V1_0 { -// Forward declaration for forward reference support: enum class Error : int32_t; struct YCbCrLayout; enum class PlaneComponent : uint32_t; @@ -36,61 +35,22 @@ struct YCbCrLayout final { uint32_t chromaStep; }; -/** - * Plane Component type definitions. - */ enum class PlaneComponent : uint32_t { - PLANE_COMPONENT_Y = 1u /* (1 << 0) */, - PLANE_COMPONENT_Cb = 2u /* (1 << 1) */, - PLANE_COMPONENT_Cr = 4u /* (1 << 2) */, - PLANE_COMPONENT_R = 1024u /* (1 << 10) */, - PLANE_COMPONENT_G = 2048u /* (1 << 11) */, - PLANE_COMPONENT_B = 4096u /* (1 << 12) */, - PLANE_COMPONENT_A = 1048576u /* (1 << 20) */, - PLANE_COMPONENT_RAW = 1073741824u /* (1 << 30) */, - PLANE_COMPONENT_META = 2147483648u /* (1 << 31) */, + PLANE_COMPONENT_Y = 1u , + PLANE_COMPONENT_Cb = 2u , + PLANE_COMPONENT_Cr = 4u , + PLANE_COMPONENT_R = 1024u , + PLANE_COMPONENT_G = 2048u , + PLANE_COMPONENT_B = 4096u , + PLANE_COMPONENT_A = 1048576u , + PLANE_COMPONENT_RAW = 1073741824u , + PLANE_COMPONENT_META = 2147483648u , }; -/** - * Flags to send with getFormatLayout for extra information for e.g. interlaced, - * otherwise send 0. - */ enum class Flags : int32_t { - LAYOUT_INTERLACED_FLAG = 1 /* (1 << 0) */, + LAYOUT_INTERLACED_FLAG = 1 , }; -/** - * Structure for describing plane layout information for YUVA/RGBA/RAW formats - * YUV formats can be planar or semi-planar. - * - * This layout structure is used with any pixel format that can be - * represented by it, such as: - * - HAL_PIXEL_FORMAT_YCbCr_* - * - HAL_PIXEL_FORMAT_RGB*_* - * - even implementation defined formats that can be represented by - * the structures - * - * Each plane can be made up of one or more PlaneComponents depending on - * the format. - * - * Each plane can be subsampled either vertically or horizontally by - * a power-of-two factor. - * - * Pixel increment describes the distance in bytes from one pixel to the - * next pixel (to the right) on the same row for the component plane. - * This can be negative. - * - * Stride is provided in both pixels and bytes. - * Stride_bytes describes the distance in bytes from the first pixel of - * one row to the first pixel of the next row (below) for the component plane. - * This can be negative. - * - * Scanlines describes the height of the plane in terms of number of rows. - * - * Size of plane is stride_bytes * scanlines + any padding required for - * alignment as per format specification. - * - */ struct PlaneLayout final { ::android::hardware::hidl_bitfield< ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneComponent> @@ -137,9 +97,6 @@ static_assert(sizeof(::vendor::qti::hardware::display::mapperextensions::V1_0::P static_assert(__alignof(::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout) == 4, "wrong alignment"); -// -// type declarations for package -// template static inline std::string toString(int32_t o); @@ -185,7 +142,6 @@ constexpr int32_t& operator&=( static inline std::string toString( const ::vendor::qti::hardware::display::mapperextensions::V1_0::YCbCrLayout& o); -// operator== and operator!= are not generated for YCbCrLayout template static inline std::string toString(uint32_t o); @@ -284,9 +240,6 @@ static inline bool operator!=( const ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout& lhs, const ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout& rhs); -// -// type header definitions for package -// template <> inline std::string toString<::vendor::qti::hardware::display::mapperextensions::V1_0::Error>( @@ -379,7 +332,6 @@ static inline std::string toString( return os; } -// operator== and operator!= are not generated for YCbCrLayout template <> inline std::string @@ -642,16 +594,13 @@ static inline bool operator!=( return !(lhs == rhs); } -} // namespace V1_0 -} // namespace mapperextensions -} // namespace display -} // namespace hardware -} // namespace qti -} // namespace vendor +} +} +} +} +} +} -// -// global type declarations for package -// namespace android { namespace hardware { @@ -664,9 +613,9 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::E ::vendor::qti::hardware::display::mapperextensions::V1_0::Error::NO_RESOURCES, ::vendor::qti::hardware::display::mapperextensions::V1_0::Error::UNSUPPORTED, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -688,9 +637,9 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::P ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneComponent:: PLANE_COMPONENT_META, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -700,8 +649,8 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::F hidl_enum_values<::vendor::qti::hardware::display::mapperextensions::V1_0::Flags> = { ::vendor::qti::hardware::display::mapperextensions::V1_0::Flags::LAYOUT_INTERLACED_FLAG, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} -#endif // HIDL_GENERATED_VENDOR_QTI_HARDWARE_DISPLAY_MAPPEREXTENSIONS_V1_0_TYPES_H +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/shim.cpp b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/shim.cpp index 34dd68296..3c8a0134e 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/shim.cpp +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/shim.cpp @@ -46,14 +46,13 @@ using namespace android::hardware; using namespace android; -// These two functions are used by the blob and aren't present on Q so weakly stub them extern "C" uint64_t __attribute__((weak)) atrace_get_enabled_tags() { return 0; } namespace android::hardware::details { void __attribute__((weak)) return_status::onValueRetrieval() const {} -} // namespace android::hardware::details +} bool IBase::isRemote() const { return false; @@ -146,7 +145,7 @@ sp IMapper::getService(const std::string& serviceName, const bool getSt return nullptr; } -} // namespace V2_0 +} namespace V3_0 { sp IMapper::getService(const std::string& serviceName, const bool getStub) { @@ -154,7 +153,7 @@ sp IMapper::getService(const std::string& serviceName, const bool getSt return nullptr; } -} // namespace V3_0 +} namespace V4_0 { sp IMapper::getService(const std::string& serviceName, const bool getStub) { @@ -162,8 +161,8 @@ sp IMapper::getService(const std::string& serviceName, const bool getSt return nullptr; } -} // namespace V4_0 -} // namespace android::hardware::graphics::mapper +} +} namespace vendor::qti::hardware::display { namespace mapper { namespace V4_0 { @@ -174,7 +173,7 @@ Return> IQtiMapper::castFrom(const sp> IQtiMapper::castFrom(const sp& parent, @@ -183,7 +182,7 @@ Return> IQtiMapper::castFrom(const sp> IQtiMapper::castFrom(const sp& parent, @@ -200,8 +199,8 @@ Return IQtiMapper::getMapperExtensions(getMapperExtensions_cb _hidl_cb) { return Void(); } -} // namespace V2_0 -} // namespace mapper +} +} namespace mapperextensions { namespace V1_1 { @@ -467,6 +466,6 @@ Return IQtiMapperExtensions::getFormatLayout(int32_t format, uint64_t usag return Void(); } -} // namespace V1_1 -} // namespace mapperextensions -} // namespace vendor::qti::hardware::display +} +} +} diff --git a/app/src/main/cpp/deviceinfo/vkinfo.c b/app/src/main/cpp/deviceinfo/vkinfo.c index a8d769818..81ccc6b0d 100644 --- a/app/src/main/cpp/deviceinfo/vkinfo.c +++ b/app/src/main/cpp/deviceinfo/vkinfo.c @@ -1,6 +1,3 @@ -// What the phone's own Vulkan driver says about its GPU, for the app's device info window: one -// instance on the system loader, the first physical device's properties, a few limits and the -// extensions DXVK and VKD3D-Proton lean on. Returned as "key=value" lines; nothing is kept open. #include #include #include @@ -62,12 +59,10 @@ Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz LINE("device=%s\n", p.deviceName); LINE("type=%s\n", type_name(p.deviceType)); LINE("api=%u.%u.%u\n", VK_API_VERSION_MAJOR(p.apiVersion), VK_API_VERSION_MINOR(p.apiVersion), VK_API_VERSION_PATCH(p.apiVersion)); - // Qualcomm packs its driver version as 10.10.12 bits, the same layout as VK_MAKE_VERSION. LINE("driver_version=%u.%u.%u\n", p.driverVersion >> 22, (p.driverVersion >> 12) & 0x3ff, p.driverVersion & 0xfff); LINE("vendor_id=0x%04x\ndevice_id=0x%08x\n", p.vendorID, p.deviceID); LINE("max_image_2d=%u\n", p.limits.maxImageDimension2D); - // Driver name and build string (Vulkan 1.2 or VK_KHR_driver_properties). uint32_t extCount = 0; if (vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, NULL); static VkExtensionProperties exts[512]; diff --git a/app/src/main/cpp/fakeinput_steam.cpp b/app/src/main/cpp/fakeinput_steam.cpp index 984650c59..c70d460a9 100644 --- a/app/src/main/cpp/fakeinput_steam.cpp +++ b/app/src/main/cpp/fakeinput_steam.cpp @@ -1,12 +1,8 @@ /* - * The fake evdev layer. Taken from WinNative (maxjivi05, main 0cfc070b + feature/wayland-gamescope - * through f1f34cd7, 2026-09-14..19), GPL-3.0: the locking, the fork handlers, the shared device - * table, the Steam virtual-gamepad identity and the Z/RZ trigger placement are that author's. - * Added here: an opt-in Xbox 360 identity (FAKE_EVDEV_IDENTITY=xbox360), which is what gets SDL's - * built-in mapping applied with no configuration when the pad is read by the Steam client itself; - * and a stand-in for /dev/uinput (FAKE_EVDEV_UINPUT=1), so the virtual pads Steam Input makes for - * a game become more of these nodes instead of vanishing. + * Taken from WinNative (maxjivi05, main 0cfc070b + feature/wayland-gamescope + * through f1f34cd7, 2026-09-14..19), GPL-3.0. */ + #include #include #include @@ -57,8 +53,6 @@ #define EXPORT extern "C" __attribute__((visibility("default"))) -// glibc declares the request as unsigned long; bionic as int. The same source also serves the -// Linux runtime's preload (tools/linuxfs), where Steam and SDL run against glibc. #ifdef __GLIBC__ typedef unsigned long ioctl_request_t; #else @@ -69,13 +63,9 @@ static constexpr uint16_t GAMEPAD_VENDOR_ID_BASE = 0x1234; static constexpr uint16_t GAMEPAD_PRODUCT_ID_BASE = 0x5678; static constexpr uint16_t GAMEPAD_VERSION = 0x0110; static constexpr const char *GAMEPAD_NAME_TEMPLATE = "Generic HID Gamepad %d"; -// The gamepad Steam Input presents to a game, and the name SDL reads its slot from. static constexpr uint16_t STEAM_VIRTUAL_VENDOR_ID = 0x28de; static constexpr uint16_t STEAM_VIRTUAL_PRODUCT_ID = 0x11ff; static constexpr const char *STEAM_VIRTUAL_NAME_TEMPLATE = "Microsoft X-Box 360 pad %d"; -// Opt-in identity for the process that reads the pad directly (the Steam client): SDL keys its -// mapping database on bus+vendor+product, and 045E:028E is the most widely mapped id there is. -// Not offset per slot: a real pad reports the same ids on every port. static constexpr uint16_t X360_VENDOR_ID = 0x045E; static constexpr uint16_t X360_PRODUCT_ID = 0x028E; static constexpr const char *X360_NAME_TEMPLATE = "Xbox 360 Controller (%d)"; @@ -90,27 +80,25 @@ static constexpr uint32_t FAKE_INPUT_RING_CAPACITY = 4096; static constexpr unsigned int FAKE_INPUT_MAJOR = 13; static constexpr unsigned int FAKE_INPUT_EVENT_MINOR_BASE = 64; static constexpr unsigned int FAKE_INPUT_JS_MINOR_BASE = 0; -// Nodes from event16 up are the virtual pads programs make through the /dev/uinput stand-in; -// below that are the app's own rings, one per slot. static constexpr int UINPUT_EVENT_BASE = 16; static constexpr int UINPUT_EVENT_LIMIT = 64; static constexpr uint32_t UINPUT_DESCRIPTION_MAGIC = 0x46495544; struct FakeInputRingHeader { - uint32_t magic; // 0 - uint32_t version; // 4 - uint32_t event_size; // 8 - uint32_t capacity; // 12 - uint64_t write_seq; // 16 - uint64_t generation; // 24 - // Authoritative absolute-state snapshot, published by the writer under a - // seqlock (odd snapshot_seq = write in progress). The reader replays it as a - // full keyframe whenever the delta stream could have desynced (open, ring - // overflow) so dropped events can recover without periodic duplicate input. - uint64_t snapshot_seq; // 32 - uint32_t snapshot_buttons; // 40 bit i -> kSnapshotButtons[i] pressed - int16_t snapshot_axes[8]; // 44 values in kSnapshotAxisCodes order - uint32_t resync_seq; // 60 + uint32_t magic; + uint32_t version; + uint32_t event_size; + uint32_t capacity; + uint64_t write_seq; + uint64_t generation; + + + + + uint64_t snapshot_seq; + uint32_t snapshot_buttons; + int16_t snapshot_axes[8]; + uint32_t resync_seq; }; static_assert(sizeof(FakeInputRingHeader) == 64, @@ -122,8 +110,6 @@ static constexpr size_t FAKE_INPUT_RING_SIZE = FAKE_INPUT_RING_HEADER_SIZE + (FAKE_INPUT_RING_CAPACITY * FAKE_INPUT_EVENT_SIZE); -// What a program told the /dev/uinput stand-in about the device it made, stored after the ring -// in that device's file so every process that opens the node answers the same identity. struct UinputDescription { uint32_t magic; int32_t owner_pid; @@ -142,7 +128,6 @@ static constexpr size_t UINPUT_FILE_SIZE = FAKE_INPUT_RING_SIZE + sizeof(UinputD struct FakeController { char *event = nullptr; - // Set for a node made through the /dev/uinput stand-in; null for the app's own pads. const UinputDescription *uinput = nullptr; int slot = -1; FakeInputRingHeader *ring = nullptr; @@ -157,9 +142,6 @@ struct FakeController { if (ring) munmap(ring, mapping_size); free(event); } - // Pending keyframe (full absolute-state baseline) currently streaming to the - // guest. The axis/button values are captured from the snapshot when the - // keyframe starts so the frame stays consistent across multi-read delivery. size_t keyframe_remaining = 0; int32_t keyframe_axes[8] = {0, 0, 0, 0, 0, 0, 0, 0}; uint32_t keyframe_buttons = 0; @@ -170,11 +152,6 @@ struct NeutralEventSpec { uint16_t code; }; -// Event template for a full keyframe: every button, every axis/hat, then a -// SYN_REPORT, in this fixed order. The value carried by each event is filled -// from the authoritative snapshot (see keyframe_value); an all-zero snapshot -// yields the neutral baseline. Replayed on open and ring overflow so dropped -// events cannot leave a guest stuck. static const NeutralEventSpec kNeutralEvents[] = { {EV_KEY, BTN_A}, {EV_KEY, BTN_B}, {EV_KEY, BTN_X}, {EV_KEY, BTN_Y}, {EV_KEY, BTN_TL}, {EV_KEY, BTN_TR}, @@ -187,30 +164,15 @@ static const NeutralEventSpec kNeutralEvents[] = { static constexpr size_t kNeutralEventCount = sizeof(kNeutralEvents) / sizeof(kNeutralEvents[0]); -// Axis layout of FakeInputRingHeader::snapshot_axes (mirrors the Java writer). static const uint16_t kSnapshotAxisCodes[8] = { ABS_X, ABS_Y, ABS_RX, ABS_RY, ABS_GAS, ABS_BRAKE, ABS_HAT0X, ABS_HAT0Y}; -// Bit i of FakeInputRingHeader::snapshot_buttons maps to this button code. static const uint16_t kSnapshotButtons[11] = { BTN_A, BTN_B, BTN_X, BTN_Y, BTN_TL, BTN_TR, BTN_SELECT, BTN_START, BTN_THUMBL, BTN_THUMBR, - // Bit 10 is the Steam button: the client's in-game menu is bound to it, and SDL reports it - // as button 8 from this position in the key bits. BTN_MODE}; static constexpr size_t kSnapshotButtonCount = sizeof(kSnapshotButtons) / sizeof(kSnapshotButtons[0]); -// An LD_PRELOAD interposer is called outside its own lifetime. The loader initialises libraries -// in dependency order, and the hooks below belong to whichever library gets there first: on the -// Linux runtime, libpython's initialiser runs before ours and calls close() and read(), so a hook -// fires before this library has been initialised at all. A file-scope std::unordered_map or -// std::recursive_mutex has no object yet at that moment - touching one is undefined behaviour, -// and in practice the process dies before main(). The same window reopens at exit, after static -// destructors have run. (Proton is a Python script, so this killed every game launched from the -// native Steam client, and the compatibility-tool registrar with it.) -// -// So everything with a constructor lives behind an accessor that builds it on first use and -// never destroys it. The leak is deliberate and bounded: one map or mutex for the process. namespace Logger { void init(); } @@ -266,9 +228,6 @@ static std::unordered_map &ff_effects() { return *map; } -// Every fd the maps hold, readable without controller_mutex(): the hooks see each read, write, -// ioctl and poll a process makes, and almost none are on these. Set before an fd is handed out, -// cleared once no map holds it; an fd past the bitmap takes the lock once any such fd exists. static constexpr int kWatchedFds = 1 << 16; static constexpr int kWatchedBits = sizeof(unsigned long) * 8; static std::atomic watched_fds[kWatchedFds / kWatchedBits]; @@ -300,17 +259,6 @@ static bool fake_uinput_enabled() { return config().uinput_enabled; } static bool ring_paths_loaded = false; static int next_ff_id = 0; -// fork() carries over only the calling thread, so a lock another thread was holding at that -// instant stays held in the child by a thread that is not there to release it. Every hook below -// takes controller_mutex, and the Steam client forks while its other threads are inside them, so -// the child blocked on its first open() and never reached exec: the spawn never completed, the -// client's main loop stalled past its own watchdog and it tore the session down. -// -// Taking the lock before the fork is what makes the copy consistent - no thread is part way -// through the tables it guards. The parent then unlocks it. The child cannot: a recursive mutex -// records the owning thread, the child's one thread has a new id, and unlocking one it does not -// own is refused, which would leave the lock held for good. It gets a fresh mutex instead, which -// is sound precisely because the fork was taken with the lock held. static void controller_fork_prepare() { controller_mutex().lock(); } static void controller_fork_parent() { controller_mutex().unlock(); } @@ -325,11 +273,6 @@ static void controller_fork_child() { namespace Logger { int log_enabled; -// FAKE_EVDEV_LOG_FILE: the session's pad.log, shared by every process the library is loaded in -// (the client, each game), so a pad report has the guest side in one file of the session folder. -// Opened with the raw syscalls (open() and close() are this library's own hooks), and only once -// there is a line to write: most processes never log one, and each open is a stop in proot on -// shared storage. static std::atomic log_fd{-1}; static std::atomic log_unusable{false}; static char log_path[PATH_MAX]; @@ -371,7 +314,6 @@ void log(const char *message, ...) { std::cerr.flush(); return; } - // One write per line: O_APPEND keeps lines from different processes whole. char line[512]; struct timespec now; clock_gettime(CLOCK_REALTIME, &now); @@ -392,12 +334,9 @@ void log(const char *message, ...) { ssize_t ignored = write(fd, line, n); (void)ignored; } -} // namespace Logger +} __attribute__((constructor)) static void library_init() { - // Warming the lazily built state, not owning it: when the loader runs this before anything - // calls a hook - the ordinary case - the first hook finds everything already there. Nothing - // below depends on it having run, which is the whole point. config(); controller_mutex(); controller_map(); @@ -408,10 +347,6 @@ send_vibration(int strong, int weak, uint16_t duration_ms, uint16_t slot) { if (!fake_vibration_enabled()) return; - // Rumble is best effort: the app listens on the abstract socket below (RumbleComponent) and - // drives the phone's vibrator. A full listener backlog must never park - // winebus (or every input hook through controller_mutex()), and a closing - // listener must not terminate the guest with SIGPIPE. int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); if (sock < 0) return; @@ -599,8 +534,6 @@ struct SnapshotState { static long long monotonic_ms(); -// The writer protects the events, cursor and snapshot in one publication. -// A busy writer is retried later; inventing a neutral state would lose holds. __attribute__((visibility("hidden"))) static bool read_snapshot(const FakeInputRingHeader *ring, SnapshotState &out) { for (int attempt = 0; attempt < 8; attempt++) { @@ -620,8 +553,6 @@ read_snapshot(const FakeInputRingHeader *ring, SnapshotState &out) { return false; } -// This baseline supersedes all deltas through its cursor. Replaying older -// deltas after it can reassert a control that the snapshot already released. __attribute__((visibility("hidden"))) static void capture_keyframe(FakeController &fake, const SnapshotState &snap) { fake.keyframe_buttons = snap.buttons; @@ -632,25 +563,22 @@ capture_keyframe(FakeController &fake, const SnapshotState &snap) { fake.needs_keyframe = false; } -// Resolve the value a keyframe event should carry from the captured snapshot. __attribute__((visibility("hidden"))) static int32_t keyframe_value(const FakeController &fake, uint16_t type, uint16_t code) { if (type == EV_KEY) { for (size_t i = 0; i < kSnapshotButtonCount; i++) if (kSnapshotButtons[i] == code) return (fake.keyframe_buttons >> i) & 1u; - return 0; // a code the snapshot word does not carry + return 0; } if (type == EV_ABS) { for (int i = 0; i < 8; i++) if (kSnapshotAxisCodes[i] == code) return fake.keyframe_axes[i]; } - return 0; // SYN / unknown + return 0; } -// A virtual pad's ring and description live beside its node, hidden from anything that lists -// event* names: /dev/input/.uinput-event. __attribute__((visibility("hidden"))) static std::string uinput_ring_path(int node) { return std::string(fake_hook_dir()) + "/.uinput-event" + std::to_string(node); } @@ -691,7 +619,6 @@ open_fake_input_ring(const char *event, int flags) { virtual_pad ? reinterpret_cast( static_cast(mapping) + FAKE_INPUT_RING_SIZE) : nullptr; - // A virtual pad whose maker has gone (a crashed client leaves its files) is not there. if (!ring_header_is_valid(ring) || (description && (description->magic != UINPUT_DESCRIPTION_MAGIC || !process_alive(description->owner_pid)))) { @@ -709,8 +636,6 @@ open_fake_input_ring(const char *event, int flags) { controller->uinput = description; controller->mapping_size = mapping_size; controller->generation = ring_generation(ring); - // Establish the cursor at open so taps arriving before the first read stay - // queued. If publication is busy, read() will finish establishing it later. SnapshotState snap; if (read_snapshot(ring, snap) && snap.generation == controller->generation) capture_keyframe(*controller, snap); @@ -725,13 +650,6 @@ open_fake_input_ring(const char *event, int flags) { return fd; } -// Under the Steam client a pad it manages is hidden from the game: the client lists it in -// SDL_GAMECONTROLLER_IGNORE_DEVICES, its overlay refuses the game's open() of the node, and the -// game is meant to see Steam Input's virtual gamepad instead. That one is a uinput device, which -// does not exist here, so the game was left with no controller at all. With -// FAKE_EVDEV_STEAM_VIRTUAL set the pad carries the virtual gamepad's identity for every process -// but the client's own, which must keep seeing the pad as the physical one it reads. -// Whether this process is the Steam client itself. Asked once: a process never becomes another. __attribute__((visibility("hidden"))) static bool is_steam_client() { static int known; if (known == 0) { @@ -757,14 +675,6 @@ __attribute__((visibility("hidden"))) static bool presents_steam_virtual() { return known == 1; } -// The virtual gamepad is an X-Box 360 pad, whose triggers are ABS_Z and ABS_RZ. Wine and SDL place -// a gamepad's axes by their position among the ones it advertises, so the triggers have to sit -// where that pad has them: as ABS_GAS and ABS_BRAKE they follow the sticks, the right stick -// lands on the left trigger, and a released right trigger reads as the right stick held up. That -// is every reader but the Steam client, which maps the pad from what it finds (controller.txt) -// and has always been shown ABS_GAS/ABS_BRAKE: a game Steam Input is off for reads this pad -// itself (the Xbox 360 setting), and so do the desktop's programs. A pad made through -// /dev/uinput is that pad for real, whoever reads it. __attribute__((visibility("hidden"))) static bool presents_xbox_triggers(const FakeController &fake) { return fake.uinput || !is_steam_client(); @@ -778,7 +688,6 @@ presented_abs_code(const FakeController &fake, uint16_t code) { return code; } -// The snapshot keeps the triggers in the ABS_GAS/ABS_BRAKE places either way. __attribute__((visibility("hidden"))) static uint16_t snapshot_abs_code(uint16_t code) { if (code == ABS_Z) return ABS_BRAKE; if (code == ABS_RZ) return ABS_GAS; @@ -807,8 +716,6 @@ __attribute__((visibility("hidden"))) static bool is_fake_input_fd(int fd) { static bool fake_pad_withdrawn(const FakeController &fake); -// A node whose pad is gone: the app republished its ring, or the pad was switched to a type that -// is not this node (it became the Deck or the Steam Controller again, fake_pad_withdrawn). static bool controller_gone(const FakeController &fake) { return ring_generation(fake.ring) != fake.generation || fake_pad_withdrawn(fake); } @@ -820,12 +727,9 @@ __attribute__((visibility("hidden"))) static bool fake_fd_is_stale(int fd) { return controller != controller_map().end() && controller_gone(*controller->second); } -// Caller holds controller_mutex(). static bool fake_has_unread_data(const FakeController &fake) { if (controller_gone(fake)) return false; - // Readiness never consumes resync requests or changes the read cursor. - // Do not spin a nonblocking guest on a producer's unfinished publication. if (fake.keyframe_remaining > 0) return true; uint64_t sequence = __atomic_load_n(&fake.ring->snapshot_seq, __ATOMIC_ACQUIRE); if (sequence & 1ULL) return false; @@ -874,20 +778,6 @@ __attribute__((visibility("hidden"))) static long long monotonic_ms() { return static_cast(now.tv_sec) * 1000LL + now.tv_nsec / 1000000LL; } -// ---- /dev/uinput, stood in for ---- -// -// Under Steam, a game is meant to read Steam Input's output, not the pad: the client hides the -// physical pad from the game (SDL_GAMECONTROLLER_IGNORE_DEVICES, and its overlay refuses the -// open) and makes it a virtual X-Box 360 pad through /dev/uinput, which it then drives with the -// layout the player chose. The sandbox has no uinput the client may use, so the virtual pad never -// appeared and every game was left reading the physical pad as-is. -// -// So /dev/uinput is answered here. A gamepad the client makes becomes another of this library's -// nodes - /dev/input/event16 and up - backed by a ring file of the same layout the app writes, -// and the client's writes are that ring's events. Any process reading the node sees the identity -// the client gave it and gets exactly what the client wrote. A device that is not a gamepad (the -// client's virtual keyboard and mouse) is accepted and its events dropped, which is what happened -// to them before. struct UinputWriter { UinputDescription description{}; bool created = false; @@ -918,8 +808,6 @@ __attribute__((visibility("hidden"))) static bool test_bit(const uint8_t *bits, return bit >= 0 && static_cast(bit) / 8 < size && (bits[bit / 8] >> (bit % 8)) & 1u; } -// A real uinput fd reads EV_UINPUT requests and polls as writable. An eventfd that is never -// signalled does both: nothing to read, always writable, blocking or not as the caller asked. __attribute__((visibility("hidden"))) static int open_uinput(int flags) { int fd = eventfd(0, ((flags & O_NONBLOCK) ? EFD_NONBLOCK : 0) | ((flags & O_CLOEXEC) ? EFD_CLOEXEC : 0)); if (fd < 0) return -1; @@ -932,8 +820,6 @@ __attribute__((visibility("hidden"))) static int open_uinput(int flags) { return fd; } -// The layout the client asks for only matters to readers; what makes it a pad is a south button -// and a left stick, which is also what every pad the ring format carries has. __attribute__((visibility("hidden"))) static bool is_gamepad(const UinputDescription &d) { return test_bit(d.evbits, sizeof(d.evbits), EV_KEY) && test_bit(d.keybits, sizeof(d.keybits), BTN_SOUTH) && test_bit(d.evbits, sizeof(d.evbits), EV_ABS) && test_bit(d.absbits, sizeof(d.absbits), ABS_X); @@ -941,8 +827,6 @@ __attribute__((visibility("hidden"))) static bool is_gamepad(const UinputDescrip __attribute__((visibility("hidden"))) static void unpublish_uinput(UinputWriter &w) { if (!w.published) return; - // Readers holding the mapping see the generation move and report the device gone; the node's - // removal is what tells SDL and Wine through inotify. __atomic_add_fetch(&w.ring->generation, 1, __ATOMIC_RELEASE); std::string node = std::string(fake_hook_dir()) + "/event" + std::to_string(w.node); unlink(node.c_str()); @@ -955,8 +839,6 @@ __attribute__((visibility("hidden"))) static void unpublish_uinput(UinputWriter w.published = false; } -// A node number is claimed by creating its ring file exclusively, so two processes making pads -// at once cannot take the same one. A file left by a process that has gone is reclaimed. __attribute__((visibility("hidden"))) static int claim_uinput_node(int *fd_out) { static auto my_open = reinterpret_cast(dlsym(RTLD_NEXT, "open")); for (int node = UINPUT_EVENT_BASE; node < UINPUT_EVENT_LIMIT; node++) { @@ -977,7 +859,7 @@ __attribute__((visibility("hidden"))) static int claim_uinput_node(int *fd_out) unlink((std::string(fake_hook_dir()) + "/event" + std::to_string(node)).c_str()); unlink((path + ".uevent").c_str()); unlink(path.c_str()); - node--; // try the same number again + node--; } } errno = ENOSPC; @@ -1010,8 +892,6 @@ __attribute__((visibility("hidden"))) static int publish_uinput(UinputWriter &w) shared->owner_pid = w.owner; __atomic_thread_fence(__ATOMIC_RELEASE); - // The identity Wine's HID bus reads through libudev (inputudev.c in libblsession), written - // before the node so the first lookup finds it. char uevent[256]; int length = snprintf(uevent, sizeof(uevent), "PRODUCT=%x/%x/%x/%x\nNAME=\"%s\"\nPHYS=\"%s\"\n", shared->id.bustype, shared->id.vendor, shared->id.product, shared->id.version, @@ -1039,8 +919,6 @@ __attribute__((visibility("hidden"))) static int publish_uinput(UinputWriter &w) return 0; } -// Same publication as the app's writer (FakeInputWriter.flushBufferToRing): the events, then the -// absolute state under its seqlock, then the cursor that makes both visible. __attribute__((visibility("hidden"))) static void flush_uinput(UinputWriter &w) { if (!w.published || w.pending.empty()) { w.pending.clear(); @@ -1081,7 +959,6 @@ __attribute__((visibility("hidden"))) static void flush_uinput(UinputWriter &w) __attribute__((visibility("hidden"))) static ssize_t write_uinput(UinputWriter &w, const void *buf, size_t count) { if (!w.created) { - // The legacy description: written once, before UI_DEV_CREATE. if (count != sizeof(struct uinput_user_dev)) { errno = EINVAL; return -1; @@ -1106,7 +983,6 @@ write_uinput(UinputWriter &w, const void *buf, size_t count) { const auto *events = static_cast(buf); for (size_t i = 0; i < count / sizeof(struct input_event); i++) { w.pending.push_back(events[i]); - // A frame ends at SYN_REPORT; a writer that never sends one still gets its events out. if ((events[i].type == EV_SYN && events[i].code == SYN_REPORT) || w.pending.size() >= 64) flush_uinput(w); } @@ -1141,7 +1017,7 @@ ioctl_uinput(UinputWriter &w, ioctl_request_t op, void *argp) { } if (w.created && number != _IOC_NR(UI_BEGIN_FF_UPLOAD) && number != _IOC_NR(UI_END_FF_UPLOAD) && number != _IOC_NR(UI_BEGIN_FF_ERASE) && number != _IOC_NR(UI_END_FF_ERASE)) { - errno = EINVAL; // the kernel refuses configuration once the device exists + errno = EINVAL; return -1; } switch (number) { @@ -1190,8 +1066,6 @@ ioctl_uinput(UinputWriter &w, ioctl_request_t op, void *argp) { return -1; } return 0; - // Force feedback reaches the client only as EV_UINPUT requests on this fd, and none are ever - // sent: a game's rumble on a virtual pad goes straight to the app (check_ff_event). case _IOC_NR(UI_BEGIN_FF_UPLOAD): case _IOC_NR(UI_END_FF_UPLOAD): case _IOC_NR(UI_BEGIN_FF_ERASE): @@ -1205,8 +1079,6 @@ ioctl_uinput(UinputWriter &w, ioctl_request_t op, void *argp) { } } -// Only the process that made the device takes it down: a forked child closing its inherited -// copy of the fd (as a spawn does before exec) must not remove the parent's pad. __attribute__((visibility("hidden"))) static void close_uinput(int fd) { std::lock_guard guard(controller_mutex()); auto it = uinput_map().find(fd); @@ -1215,21 +1087,6 @@ __attribute__((visibility("hidden"))) static void close_uinput(int fd) { uinput_map().erase(it); } -// ---- The pad as a Steam Deck controller ---- -// -// On a Deck, and on the handhelds InputPlumber serves, the Steam client reads the built-in -// controller itself as a Steam Deck controller: a vendor HID device it talks to over hidraw, which -// is what gives it the Quick Access button, the back grips, gyro and trackpads, and Steam Input's -// full treatment of the pad. With FAKE_EVDEV_DECK set the app's pad in ring slot 0 is presented to -// the client that way: /dev/hidraw16, found through the sysfs tree the app binds into the session -// (SteamDeckPad.kt), streams the Deck's 64-byte state report every 4 ms, as the real one does, built -// from the ring's absolute snapshot; its feature reports are answered as InputPlumber answers them -// for a virtual Deck. The evdev nodes of the app's pads are then withdrawn - the client would -// otherwise count the pad twice - and games get Steam Input's virtual pad (the /dev/uinput stand-in). -// -// A read returns one report and never blocks past the next one, so the fd is one end of a -// SOCK_SEQPACKET socket pair with a thread writing reports into the other end: poll, select and -// epoll then work on it with no help from the hooks here. static constexpr const char *DECK_HIDRAW_PATH = "/dev/hidraw16"; static constexpr unsigned int DECK_HIDRAW_MAJOR = 240; static constexpr unsigned int DECK_HIDRAW_MINOR = 16; @@ -1238,20 +1095,16 @@ static constexpr int DECK_REPORT_INTERVAL_US = 4000; static constexpr const char *DECK_NAME = "Valve Software Steam Deck Controller"; static constexpr const char *DECK_SERIAL = "DROIDDECK0001"; -// InputPlumber, src/drivers/steam_deck/report_descriptor.rs (CONTROLLER_DESCRIPTOR): one 64-byte -// vendor input report and one 64-byte feature report, no report ids. static const uint8_t kDeckReportDescriptor[] = { 0x06, 0xff, 0xff, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x02, 0x09, 0x03, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x06, 0x09, 0x07, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0xb1, 0x02, 0xc0}; -// Feature report ids (SDL, src/joystick/hidapi/steam/controller_constants.h). static constexpr uint8_t DECK_GET_ATTRIBUTES_VALUES = 0x83; static constexpr uint8_t DECK_GET_STRING_ATTRIBUTE = 0xAE; static constexpr uint8_t DECK_GET_CHIP_ID = 0xBA; static constexpr uint8_t DECK_TRIGGER_RUMBLE_CMD = 0xEB; -// Button bits of the state report (SDL, SDL_hidapi_steamdeck.c): the low word, then the high. static constexpr uint32_t DECK_L_R2 = 0x00000001, DECK_L_L2 = 0x00000002, DECK_L_R1 = 0x00000004, DECK_L_L1 = 0x00000008, DECK_L_Y = 0x00000010, DECK_L_B = 0x00000020, DECK_L_X = 0x00000040, DECK_L_A = 0x00000080, DECK_L_UP = 0x00000100, @@ -1264,8 +1117,6 @@ static constexpr uint32_t DECK_L_L5 = 0x00008000, DECK_L_R5 = 0x00010000, DECK_L_LEFT_TOUCH = 0x00080000, DECK_L_RIGHT_TOUCH = 0x00100000; static constexpr uint32_t DECK_H_L4 = 0x00000200, DECK_H_R4 = 0x00000400; static constexpr uint32_t DECK_H_QAM = 0x00040000; -// Bit 11 of the ring snapshot's button word: the Quick Access button, which an Xbox pad does not -// have and the evdev nodes therefore never report (FakeInputWriter.SNAPSHOT_IDX_QAM). static constexpr uint32_t SNAPSHOT_QAM_BIT = 1u << 11; struct DeckHidraw { @@ -1273,29 +1124,24 @@ struct DeckHidraw { FakeInputRingHeader *ring = nullptr; size_t mapping_size = 0; uint8_t pending_feature = 0; - bool touch = false; // Steam's touch controller (below), not the Deck - bool triton = false; // The paired 2025 Steam Controller, with native BLE report IDs - bool emulated = false; // That Steam Controller made from the app's own pad (FAKE_EVDEV_TRITON_PAD) - uint8_t sony = 0; // The app's own pad as a DualShock 4 (4) or DualSense (5), else 0 + bool touch = false; + bool triton = false; + bool emulated = false; + uint8_t sony = 0; }; -// What the Deck has and an Xbox pad does not, after the ring's events: motion (PadMotion) and the -// back grips and trackpads (the second screen's Deck controls, DeckControls), already in the -// Deck's axes and units, under a seqlock (FakeInputWriter.writeMotion / writeDeckControls). -// Absent from a ring file written without one. struct DeckImu { - uint32_t magic; // IMU1 once written - uint32_t imu_mode; // written by this library: the client's SETTING_IMU_MODE | 0x80000000 - uint64_t seq; // odd while being written + uint32_t magic; + uint32_t imu_mode; + uint64_t seq; int16_t accel[3]; int16_t gyro[3]; - int16_t pads[4]; // left X, Y, right X, Y: -32768..32767, Y up - uint16_t pressure[2]; // left, right: 0..32767 - uint32_t controls; // DECK_EXTRA_* bits - uint32_t pad_type; // PAD_TYPE_* | 0x80000000, written by the app; 0 until it does + int16_t pads[4]; + uint16_t pressure[2]; + uint32_t controls; + uint32_t pad_type; }; static_assert(offsetof(DeckImu, pad_type) == 44, "DeckImu::pad_type matches FakeInputWriter"); -// DeckImu::controls (DeckControls.kt). static constexpr uint32_t DECK_EXTRA_L4 = 1, DECK_EXTRA_R4 = 2, DECK_EXTRA_L5 = 4, DECK_EXTRA_R5 = 8, DECK_EXTRA_LPAD_TOUCH = 16, DECK_EXTRA_RPAD_TOUCH = 32, DECK_EXTRA_LPAD_CLICK = 64, @@ -1312,13 +1158,9 @@ static constexpr uint32_t DECK_IMU_MAGIC = 0x31554D49; static constexpr size_t DECK_IMU_BLOCK_SIZE = 64; static_assert(sizeof(DeckImu) <= DECK_IMU_BLOCK_SIZE, "the IMU block is 64 bytes in the ring file"); -// Shared native Steam Controller reports. Field offsets match FakeInputWriter.java. Android writes -// the original BLE input packet; this process writes Steam's HID output report for Android to send. -// A feature read is a round trip: this process bumps get_request, Android reads the controller's -// report characteristic and answers with get_reply set to the same number. struct TritonSharedData { uint32_t magic; - uint32_t connected; // written by the app: the paired controller is connected and streaming + uint32_t connected; uint64_t input_seq; uint32_t report_id; uint32_t input_length; @@ -1353,13 +1195,11 @@ static_assert(sizeof(TritonSharedData) <= TRITON_SHARED_BLOCK_SIZE, "Triton ring static constexpr const char *TRITON_HIDRAW_PATH = "/dev/hidraw18"; static constexpr unsigned int TRITON_HIDRAW_MAJOR = 240; static constexpr unsigned int TRITON_HIDRAW_MINOR = 18; -// The app's own pad as the wired Steam Controller: a node of its own, so a paired one keeps 18. static constexpr const char *TRITON_PAD_HIDRAW_PATH = "/dev/hidraw19"; static constexpr unsigned int TRITON_PAD_HIDRAW_MINOR = 19; static constexpr int TRITON_REPORT_ID_BYTES = 1; static constexpr int TRITON_STATE_BYTES = 45; static constexpr int TRITON_REPORT_INTERVAL_US = 4000; -// A BLE read takes a few connection intervals; past this the controller is taken to be gone. static constexpr int TRITON_FEATURE_TIMEOUT_MS = 500; static constexpr const char *TRITON_NAME = "Steam Controller"; static constexpr const char *TRITON_SERIAL = "DROIDDECKSC01"; @@ -1407,12 +1247,6 @@ static bool fake_deck_enabled() { return enabled == 1; } -// What the app's own pad is to the client, switchable while the session runs (the drawer's -// Controller page): the Deck (/dev/hidraw16), the wired Steam Controller (/dev/hidraw19, built from -// ring slot 0 as the Deck's reports are), or an Xbox 360 pad (its own evdev node, no HID device). -// The app writes the choice into the slot's Deck block (DeckImu::pad_type); until it has, -// FAKE_EVDEV_PAD_TYPE says what the session started with. Only the chosen device can be opened; -// the others end their streams, and the client is told through udev (pad_type_thread). static constexpr uint32_t PAD_TYPE_DECK = 0, PAD_TYPE_STEAM = 1, PAD_TYPE_XBOX = 2, PAD_TYPE_DS4 = 3, PAD_TYPE_DUALSENSE = 4, PAD_TYPE_LAST = PAD_TYPE_DUALSENSE; static constexpr uint32_t PAD_TYPE_SET = 0x80000000u; @@ -1429,7 +1263,6 @@ __attribute__((visibility("hidden"))) static uint32_t session_pad_type() { return static_cast(type); } -// Slot 0's Deck and Triton blocks, mapped once, read-only; null until the ring file has them. __attribute__((visibility("hidden"))) static const uint8_t *ring_blocks() { static std::atomic block{nullptr}; static std::atomic tries{0}; @@ -1455,8 +1288,6 @@ __attribute__((visibility("hidden"))) static const uint8_t *ring_blocks() { return mapped; } -// The paired Bluetooth controller is offered only while the app has it connected: Steam asks a -// device it is offered for its info, and one that never answers holds up every other controller. __attribute__((visibility("hidden"))) static bool triton_connected() { const uint8_t *blocks = ring_blocks(); if (!blocks) return false; @@ -1479,8 +1310,6 @@ static bool fake_triton_enabled() { return enabled == 1; } -// The Deck is the client's alone: a game that found it would have the pad twice, once through it -// and once through Steam Input's virtual pad. __attribute__((visibility("hidden"))) static bool process_is_steam_client() { static int known; if (known == 0) { @@ -1497,7 +1326,6 @@ __attribute__((visibility("hidden"))) static bool process_is_steam_client() { return known == 1; } -// What the client starts with, so a pad log says whether it was offered a Deck at all. __attribute__((constructor)) static void log_client_start() { config(); if (process_is_steam_client()) @@ -1509,8 +1337,6 @@ __attribute__((visibility("hidden"))) static bool is_deck_hidraw_path(const char return pathname && fake_deck_enabled() && !strcmp(pathname, DECK_HIDRAW_PATH) && live_pad_type() == PAD_TYPE_DECK; } -// The app's own pad as a PlayStation controller: /dev/hidraw20 (DualShock 4) or /dev/hidraw21 -// (DualSense), USB devices of their own in SteamDeckPad's sysfs. Returns 4 or 5, or 0. static constexpr const char *DS4_HIDRAW_PATH = "/dev/hidraw20"; static constexpr const char *DUALSENSE_HIDRAW_PATH = "/dev/hidraw21"; static constexpr unsigned int DS4_HIDRAW_MINOR = 20, DUALSENSE_HIDRAW_MINOR = 21; @@ -1531,11 +1357,8 @@ __attribute__((visibility("hidden"))) static bool is_triton_hidraw_path(const ch return pathname && fake_triton_enabled() && !strcmp(pathname, TRITON_HIDRAW_PATH) && triton_connected(); } -// The app's first pad is the Deck now: its evdev node is withdrawn from everyone. Further players' -// pads are left to the client alone, which hands them to games through Steam Input. __attribute__((visibility("hidden"))) static bool is_withdrawn_pad_path(const char *pathname) { if (!fake_deck_enabled() || !pathname || strncmp(pathname, "/dev/input/", 11)) return false; - // As an Xbox 360 pad it is its own evdev node again, for the client and games alike. if (live_pad_type() == PAD_TYPE_XBOX) return false; const char *event = strrchr(pathname, '/') + 1; if (strncmp(event, "event", 5) && strncmp(event, "js", 2)) return false; @@ -1560,15 +1383,12 @@ static inline void put32(uint8_t *at, uint32_t value) { for (int i = 0; i < 4; i++) at[i] = static_cast(value >> (8 * i)); } -// The Deck's state report (SDL, controller_structs.h: ValveInReportHeader_t then -// SteamDeckStatePacket_t) from the ring's snapshot: kSnapshotButtons order, kSnapshotAxisCodes -// order, sticks as evdev has them (Y down), triggers 0..255. __attribute__((visibility("hidden"))) static void build_deck_report(uint8_t *report, uint32_t packet, uint32_t buttons, const int32_t *axes, const DeckExtras &extras) { memset(report, 0, DECK_REPORT_BYTES); - report[0] = 0x01; // report version - report[2] = 0x09; // ID_CONTROLLER_DECK_STATE + report[0] = 0x01; + report[2] = 0x09; report[3] = DECK_REPORT_BYTES; put32(report + 4, packet); static const uint32_t kButtonBits[11] = {DECK_L_A, DECK_L_B, DECK_L_X, DECK_L_Y, @@ -1583,7 +1403,7 @@ build_deck_report(uint8_t *report, uint32_t packet, uint32_t buttons, const int3 if (hat_y > 0) low |= DECK_L_DOWN; if (hat_x < 0) low |= DECK_L_LEFT; if (hat_x > 0) low |= DECK_L_RIGHT; - int32_t left_trigger = axes[5], right_trigger = axes[4]; // ABS_BRAKE, ABS_GAS + int32_t left_trigger = axes[5], right_trigger = axes[4]; if (left_trigger > 127) low |= DECK_L_L2; if (right_trigger > 127) low |= DECK_L_R2; uint32_t controls = extras.controls; @@ -1597,13 +1417,13 @@ build_deck_report(uint8_t *report, uint32_t packet, uint32_t buttons, const int3 if (controls & DECK_EXTRA_RPAD_CLICK) low |= DECK_L_RIGHT_PAD; put32(report + 8, low); put32(report + 12, high); - for (int i = 0; i < 4; i++) put16(report + 16 + i * 2, extras.pads[i]); // s{Left,Right}PadX/Y + for (int i = 0; i < 4; i++) put16(report + 16 + i * 2, extras.pads[i]); for (int i = 0; i < 3; i++) { - put16(report + 24 + i * 2, extras.accel[i]); // sAccelX/Y/Z - put16(report + 30 + i * 2, extras.gyro[i]); // sGyroX/Y/Z + put16(report + 24 + i * 2, extras.accel[i]); + put16(report + 30 + i * 2, extras.gyro[i]); } - put16(report + 56, extras.pressure[0]); // sPressurePadLeft - put16(report + 58, extras.pressure[1]); // sPressurePadRight + put16(report + 56, extras.pressure[0]); + put16(report + 58, extras.pressure[1]); put16(report + 44, std::min(32767, left_trigger * 32767 / 255)); put16(report + 46, std::min(32767, right_trigger * 32767 / 255)); auto stick = [](int32_t value) { return std::max(-32767, std::min(32767, value)); }; @@ -1622,7 +1442,6 @@ __attribute__((visibility("hidden"))) static void *deck_report_thread(void *arg) uint32_t buttons = 0; int32_t axes[8] = {}; DeckExtras extras; - // Switched to another type, the stream ends: the client's read fails and it lets the Deck go. while (live_pad_type() == PAD_TYPE_DECK) { if (read_snapshot(self->ring, snap) && snap.generation == ring_generation(self->ring)) { buttons = snap.buttons; @@ -1631,8 +1450,6 @@ __attribute__((visibility("hidden"))) static void *deck_report_thread(void *arg) read_deck_extras(*self, extras); uint8_t report[DECK_REPORT_BYTES]; build_deck_report(report, ++packet, buttons, axes, extras); - // A reader that fell behind loses reports rather than getting stale ones later; a reader that - // has closed ends the stream. if (send(self->peer, report, sizeof(report), MSG_DONTWAIT | MSG_NOSIGNAL) < 0 && errno != EAGAIN && errno != EWOULDBLOCK) break; @@ -1644,15 +1461,6 @@ __attribute__((visibility("hidden"))) static void *deck_report_thread(void *arg) return nullptr; } -// ---- The pad as a PlayStation controller ---- -// -// The DualShock 4 (054c:09cc) and DualSense (054c:0ce6) as they are on USB, read by the client's -// own PlayStation drivers (SDL's hidapi ps4/ps5 are the reference): input report 1 every 4 ms, -// the feature reports a driver reads at start answered here (calibration, serial, firmware), and -// output reports' rumble on the handheld's motor. Built from the same ring as the Deck's report: -// sticks, triggers, buttons, motion (in the units the calibration below declares), and the -// touchpad from the Deck controls' two trackpads, the left one on its left half and the right one -// on its right half, either's click as the touchpad's. static constexpr int SONY_REPORT_BYTES = 64; static constexpr int SONY_TOUCH_WIDTH = 1920; static constexpr int SONY_TOUCH_HEIGHT_DS4 = 942, SONY_TOUCH_HEIGHT_DUALSENSE = 1080; @@ -1664,8 +1472,6 @@ struct SonyTouch { uint8_t next_id = 0; }; -// One finger per trackpad: Deck pads are -32768..32767 with Y up; the touchpad counts from its top -// left, 12 bits per axis. __attribute__((visibility("hidden"))) static void put_sony_touch(uint8_t *at, SonyTouch &touch, int side, bool down, int16_t x, int16_t y, int height) { if (down && !touch.down[side]) touch.id[side] = touch.next_id++ & 0x7f; @@ -1687,13 +1493,12 @@ build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint32_t butt uint8_t *p = report + 1; auto stick = [](int32_t value) { return static_cast(std::max(0, std::min(255, (value + 32768) >> 8))); }; p[0] = stick(axes[0]); - p[1] = stick(axes[1]); // evdev and the PlayStation pads both count Y downwards + p[1] = stick(axes[1]); p[2] = stick(axes[2]); p[3] = stick(axes[3]); int32_t hat_x = axes[6], hat_y = axes[7]; - static const uint8_t kHat[3][3] = {{7, 0, 1}, {6, 8, 2}, {5, 4, 3}}; // [y+1][x+1] + static const uint8_t kHat[3][3] = {{7, 0, 1}, {6, 8, 2}, {5, 4, 3}}; uint8_t hat = kHat[(hat_y > 0) - (hat_y < 0) + 1][(hat_x > 0) - (hat_x < 0) + 1]; - // Snapshot order (kButtonBits): A B X Y L1 R1 View Menu L3 R3 Steam. uint8_t face = (buttons & 4 ? 0x10 : 0) | (buttons & 1 ? 0x20 : 0) | (buttons & 2 ? 0x40 : 0) | (buttons & 8 ? 0x80 : 0); uint8_t shoulders = (buttons & 16 ? 0x01 : 0) | (buttons & 32 ? 0x02 : 0) | (axes[5] > 0 ? 0x04 : 0) | (axes[4] > 0 ? 0x08 : 0) | (buttons & 64 ? 0x10 : 0) | (buttons & 128 ? 0x20 : 0) | @@ -1702,9 +1507,6 @@ build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint32_t butt uint8_t system = (buttons & 1024 ? 0x01 : 0) | (click ? 0x02 : 0); uint8_t left_trigger = static_cast(std::max(0, std::min(255, axes[5]))); uint8_t right_trigger = static_cast(std::max(0, std::min(255, axes[4]))); - // Motion: the Deck block's gyro is 2000 dps and its accel 2 g over 16 bits; the calibration - // reports declare 16 units per degree/s and 8192 per g, and the axes are the Deck's reordered as - // SDL reads both (Deck X, Z, -Y). int16_t gyro[3] = {extras.gyro[0], extras.gyro[2], static_cast(-extras.gyro[1])}; int16_t accel[3] = {static_cast(extras.accel[0] / 2), static_cast(extras.accel[2] / 2), static_cast(-extras.accel[1] / 2)}; @@ -1715,13 +1517,13 @@ build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint32_t butt p[6] = static_cast(system | ((packet & 0x3f) << 2)); p[7] = left_trigger; p[8] = right_trigger; - put16(p + 9, static_cast(packet * 750)); // 5.33 us ticks: 4 ms + put16(p + 9, static_cast(packet * 750)); for (int i = 0; i < 3; i++) { put16(p + 12 + i * 2, gyro[i]); put16(p + 18 + i * 2, accel[i]); } - p[29] = 0x1b; // on the cable, full - p[32] = 1; // one touch packet + p[29] = 0x1b; + p[32] = 1; p[33] = static_cast(packet); put_sony_touch(p + 34, touch, 0, left, extras.pads[0], extras.pads[1], SONY_TOUCH_HEIGHT_DS4); put_sony_touch(p + 38, touch, 1, right, extras.pads[2], extras.pads[3], SONY_TOUCH_HEIGHT_DS4); @@ -1737,11 +1539,11 @@ build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint32_t butt put16(p + 15 + i * 2, gyro[i]); put16(p + 21 + i * 2, accel[i]); } - put32(p + 27, packet * 12000); // 0.33 us ticks: 4 ms + put32(p + 27, packet * 12000); put_sony_touch(p + 32, touch, 0, left, extras.pads[0], extras.pads[1], SONY_TOUCH_HEIGHT_DUALSENSE); put_sony_touch(p + 36, touch, 1, right, extras.pads[2], extras.pads[3], SONY_TOUCH_HEIGHT_DUALSENSE); - p[52] = 0x2a; // charging, full - p[53] = 0x08; // USB + p[52] = 0x2a; + p[53] = 0x08; } } @@ -1756,7 +1558,6 @@ __attribute__((visibility("hidden"))) static void *sony_report_thread(void *arg) int32_t axes[8] = {}; DeckExtras extras; SonyTouch touch; - // Switched to another type, the stream ends, as unplugging the controller would. while (live_pad_type() == type) { if (read_snapshot(self->ring, snap) && snap.generation == ring_generation(self->ring)) { buttons = snap.buttons; @@ -1780,7 +1581,6 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags, uin const char *path = sony == 4 ? DS4_HIDRAW_PATH : sony == 5 ? DUALSENSE_HIDRAW_PATH : DECK_HIDRAW_PATH; static auto my_open = reinterpret_cast(dlsym(RTLD_NEXT, "open")); if (!process_is_steam_client()) { - // Only the client reads the Deck; a game sees Steam Input's virtual pad instead. Logger::log("deck: %s refused: not the Steam client\n", path); errno = ENOENT; return -1; @@ -1793,7 +1593,6 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags, uin errno = ENODEV; return -1; } - // The IMU block is mapped only where the file has one: past its end a mapping faults. struct stat ring_stat; size_t mapping_size = FAKE_INPUT_RING_SIZE; if (fstat(ring_fd, &ring_stat) == 0 && @@ -1814,7 +1613,6 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags, uin munmap(mapping, mapping_size); return -1; } - // Room for a few reports only, so what the client reads is never more than a few ms old. int buffer = DECK_REPORT_BYTES * 4; setsockopt(pair[1], SOL_SOCKET, SO_SNDBUF, &buffer, sizeof(buffer)); if (!(flags & O_CLOEXEC)) fcntl(pair[0], F_SETFD, 0); @@ -1879,7 +1677,6 @@ __attribute__((visibility("hidden"))) static void *triton_report_thread(void *ar uint8_t report[TRITON_REPORT_ID_BYTES + 64] = {}; uint8_t report_id = 0x45; size_t length = TRITON_STATE_BYTES; - // A disconnect ends the stream, as unplugging the controller would. while (triton_connected()) { if (read_triton_state(*self, report_id, report + 1, length)) { report[0] = report_id; @@ -1895,8 +1692,6 @@ __attribute__((visibility("hidden"))) static void *triton_report_thread(void *ar return nullptr; } -// Button bits of the Triton state report (SDL, SDL_hidapi_steam_triton.c). SDL reads MENU as Back -// and VIEW as Start, the reverse of the Deck's names. static constexpr uint32_t TRITON_A = 0x00000001, TRITON_B = 0x00000002, TRITON_X = 0x00000004, TRITON_Y = 0x00000008, TRITON_QAM = 0x00000010, TRITON_R3 = 0x00000020, TRITON_VIEW = 0x00000040, TRITON_R4 = 0x00000080, TRITON_R5 = 0x00000100, @@ -1909,17 +1704,10 @@ static constexpr uint32_t TRITON_A = 0x00000001, TRITON_B = 0x00000002, TRITON_X TRITON_LSTICK_TOUCH = 0x01000000, TRITON_LPAD_TOUCH = 0x02000000, TRITON_LPAD_CLICK = 0x04000000, TRITON_LTRIGGER_CLICK = 0x08000000, TRITON_RGRIP_TOUCH = 0x10000000, TRITON_LGRIP_TOUCH = 0x20000000; -// The pad is built in, so it is the wired controller: 28de:1302 on USB, state report 0x42 (the -// same layout as the Bluetooth 0x45), where the paired one is 28de:1303 on Bluetooth. static constexpr uint8_t TRITON_STATE_WIRED = 0x42; static constexpr int16_t TRITON_WIRED_PRODUCT = 0x1302; -// A stick counts as touched once it leaves its centre: the pad has no capacitive caps to ask. static constexpr int32_t TRITON_STICK_TOUCH = 2000; -// The Triton BLE state report (SDL, controller_structs.h: TritonMTUNoQuat_t) from the ring's -// snapshot and the Deck block, which already hold the trackpads, back buttons and motion in the -// units both controllers share. A handheld is held whenever it is played, so both grips read as -// touched: Steam's grip-sense activators (gyro while held, say) then simply stay on. __attribute__((visibility("hidden"))) static void build_triton_report(uint8_t *report, uint32_t packet, uint32_t buttons, const int32_t *axes, const DeckExtras &extras) { @@ -1936,7 +1724,7 @@ build_triton_report(uint8_t *report, uint32_t packet, uint32_t buttons, const in if (axes[7] > 0) bits |= TRITON_DOWN; if (axes[6] < 0) bits |= TRITON_LEFT; if (axes[6] > 0) bits |= TRITON_RIGHT; - int32_t left_trigger = axes[5], right_trigger = axes[4]; // ABS_BRAKE, ABS_GAS: 0..255 + int32_t left_trigger = axes[5], right_trigger = axes[4]; if (left_trigger >= 250) bits |= TRITON_LTRIGGER_CLICK; if (right_trigger >= 250) bits |= TRITON_RTRIGGER_CLICK; if (abs(axes[0]) > TRITON_STICK_TOUCH || abs(axes[1]) > TRITON_STICK_TOUCH) bits |= TRITON_LSTICK_TOUCH; @@ -1964,7 +1752,7 @@ build_triton_report(uint8_t *report, uint32_t packet, uint32_t buttons, const in put16(report + 23, extras.pads[2]); put16(report + 25, extras.pads[3]); put16(report + 27, extras.pressure[1]); - put32(report + 29, packet * TRITON_REPORT_INTERVAL_US); // IMU timestamp, microseconds + put32(report + 29, packet * TRITON_REPORT_INTERVAL_US); for (int i = 0; i < 3; i++) { put16(report + 33 + i * 2, extras.accel[i]); put16(report + 39 + i * 2, extras.gyro[i]); @@ -2069,24 +1857,6 @@ __attribute__((visibility("hidden"))) static int open_triton_hidraw(int flags, b } -// ---- Steam's touch controller ---- -// -// docs/development/steam-touch-controller.md. Steam builds its touch controller (Steam Input's -// Mobile Touch type, the one Steam Link's on-screen controls are) for any HID device with the ids -// 0000:11fb. With FAKE_TOUCHCTL_RING set, the client is offered one as /dev/hidraw17 beside the -// Deck, its sysfs written by the app (SteamDeckPad.kt). The app draws the controls; this side only -// carries the device's traffic through the file FAKE_TOUCHCTL_RING names, which both map: -// -// - the app writes the 40-byte input report (under a seqlock), whether the device is plugged in, -// and the battery level; -// - this side writes what the client tells the device: the app and action set it is on (output -// report 4), the action set layers added and removed (5, 6), rumble (1, also passed to the app's -// vibration like any pad's) and the setting report (3), and how many readers have it open. -// -// Plugged in and out by the app (while its controls are up and no physical pad is in use), as -// Steam Link withdraws its touch controls: an unplugged device ends its open stream, refuses new -// opens, and a udev event (libblsession's stand-in monitor, udevmon.c) tells the client's HID -// discovery either way. Only the client is offered it; a game reads Steam Input's virtual pad. static constexpr const char *TOUCH_HIDRAW_PATH = "/dev/hidraw17"; static constexpr unsigned int TOUCH_HIDRAW_MINOR = 17; static constexpr int TOUCH_REPORT_BYTES = 40; @@ -2094,25 +1864,21 @@ static constexpr int TOUCH_REPORT_INTERVAL_US = 8000; static constexpr const char *TOUCH_NAME = "Mobile Touch Control"; static constexpr const char *TOUCH_SERIAL = "DROIDDECK0001"; static constexpr const char *TOUCH_DEVPATH = "/devices/droiddeck/usb2/2-1:1.0/0003:0000:11FB.0002/hidraw/hidraw17"; -// Generic Desktop / Game Pad: for a vendor id that is not Valve's the client opens only a device -// with a gamepad usage. One 40-byte input report, one 64-byte output and one 64-byte feature -// report, no report ids. Must match SteamDeckPad.kt. static const uint8_t kTouchReportDescriptor[] = { 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x09, 0x02, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x28, 0x81, 0x02, 0x09, 0x03, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0x91, 0x02, 0x09, 0x04, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0xb1, 0x02, 0xc0}; -// The shared file. Must match SteamTouchDevice.kt. struct TouchRingFile { - uint32_t magic; // TCH1 + uint32_t magic; uint32_t version; - uint64_t report_seq; // odd while the app writes report + uint64_t report_seq; uint8_t report[TOUCH_REPORT_BYTES]; uint32_t plugged; - uint32_t battery; // percent - // Written here. - uint64_t state_seq; // odd while being written + uint32_t battery; + + uint64_t state_seq; uint32_t opened; uint32_t appid; uint32_t action_set; @@ -2168,7 +1934,6 @@ __attribute__((visibility("hidden"))) static bool is_touch_hidraw_path(const cha return pathname && !strcmp(pathname, TOUCH_HIDRAW_PATH) && process_is_steam_client() && touch_ring(); } -// The state the client gave the device, written under the ring's seqlock. template __attribute__((visibility("hidden"))) static void touch_state(F &&write) { TouchRingFile *ring = touch_ring(); @@ -2185,8 +1950,6 @@ static inline uint32_t get32(const uint8_t *at) { static_cast(at[3]) << 24; } -// An output report from the client (Steam Link's CVirtualController::HandleFeatureReports): the -// type, then its fields. __attribute__((visibility("hidden"))) static void touch_output(const uint8_t *data, size_t size) { if (size < 9) return; static std::atomic seen[8]; @@ -2195,7 +1958,7 @@ __attribute__((visibility("hidden"))) static void touch_output(const uint8_t *da if (n && (n & (n - 1)) == 0) Logger::log("touch: output report %u (x%u): app %u, %u\n", type, n, get32(data + 1), get32(data + 5)); switch (type) { - case 1: { // rumble: low, high, duration + case 1: { uint16_t low = data[1] | data[2] << 8, high = data[3] | data[4] << 8; uint32_t ms = get32(data + 5); touch_state([&](TouchRingFile &r) { @@ -2207,10 +1970,10 @@ __attribute__((visibility("hidden"))) static void touch_output(const uint8_t *da send_vibration(low, high, static_cast(std::min(ms, 0xffff)), 0); break; } - case 3: // a setting: 0x30 and a value + case 3: if (data[1] == 0x30) touch_state([&](TouchRingFile &r) { r.setting30 = data[2] | data[3] << 8; }); break; - case 4: // the app and its action set + case 4: touch_state([&](TouchRingFile &r) { r.appid = get32(data + 1); r.action_set = get32(data + 5); @@ -2218,8 +1981,8 @@ __attribute__((visibility("hidden"))) static void touch_output(const uint8_t *da r.action_seq++; }); break; - case 5: // a layer added to the app's action set - case 6: // and removed + case 5: + case 6: touch_state([&](TouchRingFile &r) { uint32_t layer = get32(data + 5); uint32_t count = std::min(r.layer_count, 8); @@ -2259,7 +2022,6 @@ __attribute__((visibility("hidden"))) static void *touch_report_thread(void *arg struct timespec interval = {0, TOUCH_REPORT_INTERVAL_US * 1000L}; nanosleep(&interval, nullptr); } - // Ending the stream is the unplug: the client's read fails and it lets the controller go. syscall(SYS_close, self->peer); touch_state([](TouchRingFile &r) { if (r.opened) r.opened--; }); return nullptr; @@ -2312,17 +2074,17 @@ __attribute__((visibility("hidden"))) static int ioctl_touch(DeckHidraw &touch, } size_t size = _IOC_SIZE(op); switch (_IOC_NR(op)) { - case 0x01: // HIDIOCGRDESCSIZE + case 0x01: *static_cast(argp) = sizeof(kTouchReportDescriptor); return 0; - case 0x02: { // HIDIOCGRDESC + case 0x02: { auto *descriptor = static_cast(argp); uint32_t want; memcpy(&want, descriptor, sizeof(want)); memcpy(descriptor + 4, kTouchReportDescriptor, std::min(want, sizeof(kTouchReportDescriptor))); return 0; } - case 0x03: { // HIDIOCGRAWINFO + case 0x03: { struct { uint32_t bustype; int16_t vendor; @@ -2331,13 +2093,13 @@ __attribute__((visibility("hidden"))) static int ioctl_touch(DeckHidraw &touch, memcpy(argp, &info, sizeof(info)); return 0; } - case 0x04: return copy_ioctl_string(op, argp, TOUCH_NAME); // HIDIOCGRAWNAME - case 0x05: return copy_ioctl_string(op, argp, "usb-droiddeck-2/input0"); // HIDIOCGRAWPHYS - case 0x08: return copy_ioctl_string(op, argp, TOUCH_SERIAL); // HIDIOCGRAWUNIQ - case 0x06: // HIDIOCSFEATURE: Steam Link's controller ignores these + case 0x04: return copy_ioctl_string(op, argp, TOUCH_NAME); + case 0x05: return copy_ioctl_string(op, argp, "usb-droiddeck-2/input0"); + case 0x08: return copy_ioctl_string(op, argp, TOUCH_SERIAL); + case 0x06: touch.pending_feature = size > 0 ? static_cast(argp)[0] : 0; return static_cast(size); - case 0x07: { // HIDIOCGFEATURE: report 2 is the battery + case 0x07: { auto *buf = static_cast(argp); uint8_t id = size ? buf[0] : 0; memset(buf, 0, size); @@ -2355,8 +2117,6 @@ __attribute__((visibility("hidden"))) static int ioctl_touch(DeckHidraw &touch, } } -// Plugging in and out: the app flips `plugged`; this tells the client's HID discovery, which -// otherwise never looks again (the sandbox gives it no kernel uevents). __attribute__((visibility("hidden"))) static int inject_uevent(const char *action, const char *devpath, const char *subsystem, const char *devname, unsigned major, unsigned minor) { @@ -2370,7 +2130,6 @@ __attribute__((visibility("hidden"))) static void touch_uevent(const char *actio Logger::log("touch: %s (udev event to %d monitor%s)\n", action, sent, sent == 1 ? "" : "s"); } -// The devices each pad type is, as SteamDeckPad.kt describes them and inputudev.c the evdev node. static constexpr const char *DECK_DEVPATH = "/devices/droiddeck/usb1/1-1:1.2/0003:28DE:1205.0001/hidraw/hidraw16"; static constexpr const char *TRITON_PAD_DEVPATH = "/devices/droiddeck/usb3/3-1:1.0/0003:28DE:1302.0004/hidraw/hidraw19"; static constexpr const char *PAD_EVDEV_DEVPATH = "/devices/virtual/input/droiddeck0/event0"; @@ -2393,10 +2152,6 @@ __attribute__((visibility("hidden"))) static int pad_type_uevent(uint32_t type, return inject_uevent(action, PAD_EVDEV_DEVPATH, "input", "/dev/input/event0", major(rdev), minor(rdev)); } -// SDL's HIDAPI in the client (the PlayStation pads' driver) finds hidraw devices by watching /dev -// with inotify, with udev off, and looks again on any entry named hidraw. The guest cannot -// create one in /dev, so the client's watch of /dev also covers this directory (inotify_add_watch -// below), where a switch makes and removes one. __attribute__((visibility("hidden"))) static std::string hidraw_event_dir() { return std::string(fake_hook_dir()) + "/.hidraw-events"; } @@ -2414,7 +2169,6 @@ __attribute__((visibility("hidden"))) static unsigned pad_type_minor(uint32_t ty : type == PAD_TYPE_STEAM ? TRITON_PAD_HIDRAW_MINOR : DECK_HIDRAW_MINOR; } -// A switch, as the client sees one: the old device unplugged, then the new one plugged in. __attribute__((visibility("hidden"))) static void *pad_type_thread(void *) { static const char *kNames[] = {"Steam Deck controller", "Steam Controller", "Xbox 360 controller", "DualShock 4", "DualSense"}; @@ -2435,15 +2189,11 @@ __attribute__((visibility("hidden"))) static void *pad_type_thread(void *) { uint32_t now = live_pad_type(); if (now == was || now > PAD_TYPE_LAST) continue; int removed = pad_type_uevent(was, "remove"); - // Time for the client to let the old device go before the new one appears. struct timespec settle = {0, 300 * 1000 * 1000}; nanosleep(&settle, nullptr); int added = pad_type_uevent(now, "add"); if (was != PAD_TYPE_XBOX) poke_hidraw_watchers(pad_type_minor(was)); if (now != PAD_TYPE_XBOX) poke_hidraw_watchers(pad_type_minor(now)); - // The pad's evdev node comes or goes with the Xbox 360 type. Its hotplug is watched with - // inotify on /dev/input (SDL's, with udev off), so it is moved out and back in: everyone - // watching sees it removed and added, and opens it again only if it is the pad now. if (was == PAD_TYPE_XBOX || now == PAD_TYPE_XBOX) { std::string node = std::string(fake_hook_dir()) + "/event0"; std::string aside = std::string(fake_hook_dir()) + "/.event0-switching"; @@ -2489,13 +2239,8 @@ __attribute__((constructor)) static void start_touch_controller() { if (pthread_create(&thread, nullptr, touch_plug_thread, nullptr) == 0) pthread_detach(thread); } -// libudev (systemd's sd-device) accepts a device only where its directory is on sysfs, which it -// checks with fstatfs on the directory it resolved. The Deck's sysfs is the app's own files bound -// into /sys, so those paths are said to be sysfs; nothing else is touched. static constexpr long SYSFS_FS_MAGIC = 0x62656572; -// FAKE_DECK_SYSFS_LISTING: stand-ins for the listings of /sys, /sys/class and /sys/bus -// (SteamDeckPad.listingDir), for a client that is not allowed to list the real ones. __attribute__((visibility("hidden"))) static const char *deck_listing_dir() { static const char *dir = getenv("FAKE_DECK_SYSFS_LISTING"); return dir && *dir ? dir : nullptr; @@ -2517,9 +2262,6 @@ __attribute__((visibility("hidden"))) static bool is_deck_sysfs_fd(int fd) { return is_deck_sysfs_path(path); } -// Once per process: proof the client's enumeration reached the Deck's sysfs at all. No line here -// and no open of /dev/hidraw16 means the client never looked; a line here and no open means it -// looked and turned the device down. __attribute__((visibility("hidden"))) static void note_deck_sysfs(const char *how, int result) { static std::atomic noted{false}; if (!noted.exchange(true)) @@ -2563,20 +2305,12 @@ __attribute__((visibility("hidden"))) static int copy_ioctl_string(ioctl_request return static_cast(std::min(size, strlen(value) + 1)); } -// Feature reports carry the report id (0) first, then the Valve message: type, length, payload. -// The client repeats some feature reports (settings, rumble) for as long as it has the pad; each -// is logged the first time and then at 2, 4, 8... so the log shows what it asked and how often -// without a line per report. __attribute__((visibility("hidden"))) static void log_deck_feature(const char *what, uint8_t feature) { static std::atomic counts[2][256]; unsigned n = ++counts[what[0] == 's'][feature]; if ((n & (n - 1)) == 0) Logger::log("deck: feature 0x%02x %s (x%u)\n", feature, what, n); } -// A settings report (ID_SET_SETTINGS_VALUES) is a list of (setting id, 16-bit value) triples after -// the length byte. Returns SETTING_IMU_MODE (48, a SETTING_GYRO_MODE_* bitmask from SDL's -// controller_constants.h) when the report changes it, logged, else -1: the client repeats its -// settings for as long as it has the pad. static constexpr uint8_t DECK_SET_SETTINGS_VALUES = 0x87; static constexpr uint8_t DECK_SETTING_IMU_MODE = 48; @@ -2593,9 +2327,6 @@ __attribute__((visibility("hidden"))) static int deck_imu_mode_change(const uint return -1; } -// Tells the app whether the client wants motion (DeckImu::imu_mode, FakeInputWriter.readImuMode): -// PadMotion reads the phone's sensors only while it does. The mapping is read-only, and the -// client changes the mode a few times a minute at most, so the file is written directly. __attribute__((visibility("hidden"))) static void publish_imu_mode(const DeckHidraw &deck, int mode) { static auto my_open = reinterpret_cast(dlsym(RTLD_NEXT, "open")); if (deck.mapping_size < FAKE_INPUT_RING_SIZE + DECK_IMU_BLOCK_SIZE) return; @@ -2631,7 +2362,6 @@ deck_feature(DeckHidraw &deck, ioctl_request_t op, uint8_t *buf, bool set) { uint8_t reply[DECK_REPORT_BYTES + 1] = {}; switch (deck.pending_feature) { case DECK_GET_ATTRIBUTES_VALUES: { - // InputPlumber's capture from a real Deck (steam_deck_uhid.rs, handle_get_report). static const uint8_t kAttributes[] = {0x00, 0x83, 0x2d, 0x01, 0x05, 0x12, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x2b, 0x12, 0xa9, 0x62, 0x04, 0xad, 0xf1, 0xe4, 0x65, 0x09, 0x2e, 0x00, 0x00, @@ -2661,8 +2391,6 @@ deck_feature(DeckHidraw &deck, ioctl_request_t op, uint8_t *buf, bool set) { return static_cast(length); } -// The PlayStation pads' report descriptor: a gamepad with input report 1, the output report the -// model takes (5 on a DualShock 4, 2 on a DualSense) and the feature reports a driver reads. static const uint8_t kDs4ReportDescriptor[] = { 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x06, 0x00, 0xff, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x85, 0x01, 0x09, 0x20, 0x95, 0x3f, 0x81, 0x02, 0x85, 0x05, 0x09, 0x21, 0x95, 0x1f, 0x91, 0x02, @@ -2675,8 +2403,6 @@ static const uint8_t kDualSenseReportDescriptor[] = { 0x85, 0x05, 0x09, 0x22, 0x95, 0x28, 0xb1, 0x02, 0x85, 0x09, 0x09, 0x23, 0x95, 0x13, 0xb1, 0x02, 0x85, 0x20, 0x09, 0x24, 0x95, 0x3f, 0xb1, 0x02, 0xc0}; -// Calibration both models report the same way (SDL: LoadCalibrationData): no bias, and the -// ranges that make a unit 1/16 degree/s and 1/8192 g, the scale build_sony_report writes in. __attribute__((visibility("hidden"))) static void put_sony_calibration(uint8_t *reply) { for (int axis = 0; axis < 3; axis++) { put16(reply + 7 + axis * 4, 8640); @@ -2700,9 +2426,9 @@ __attribute__((visibility("hidden"))) static int sony_feature(const DeckHidraw & if ((pad.sony == 4 && id == 0x02) || (pad.sony == 5 && id == 0x05)) { put_sony_calibration(reply); } else if ((pad.sony == 4 && (id == 0x12 || id == 0x81)) || (pad.sony == 5 && id == 0x09)) { - for (int i = 0; i < 6; i++) reply[1 + i] = SONY_MAC[5 - i]; // the address, last byte first + for (int i = 0; i < 6; i++) reply[1 + i] = SONY_MAC[5 - i]; } else if (pad.sony == 5 && id == 0x20) { - reply[44] = 0x30; // firmware 0x0430: past the versions SDL treats as old + reply[44] = 0x30; reply[45] = 0x04; } log_deck_feature("read", id); @@ -2711,8 +2437,6 @@ __attribute__((visibility("hidden"))) static int sony_feature(const DeckHidraw & return static_cast(length); } -// Rumble from the output report, when its flags say it carries rumble: a DualShock 4's report 5 -// (right/weak, then left/strong at 4 and 5), a DualSense's report 2 (at 3 and 4). __attribute__((visibility("hidden"))) static void sony_output(const DeckHidraw &pad, const uint8_t *data, size_t size) { if (!data || size < 6) return; int weak, strong; @@ -2727,7 +2451,6 @@ __attribute__((visibility("hidden"))) static void sony_output(const DeckHidraw & } else { return; } - // The drivers send a new report when the rumble changes and a zero one to stop it. send_vibration(strong * 257, weak * 257, strong || weak ? 2000 : 0, 0); } @@ -2763,7 +2486,7 @@ __attribute__((visibility("hidden"))) static int ioctl_sony(DeckHidraw &pad, ioc : "Sony Interactive Entertainment DualSense Wireless Controller"); case 0x05: return copy_ioctl_string(op, argp, pad.sony == 4 ? "usb-droiddeck-4/input3" : "usb-droiddeck-5/input3"); case 0x08: return copy_ioctl_string(op, argp, "5e:43:4b:44:44:02"); - case 0x06: // HIDIOCSFEATURE: nothing the pads need is set this way + case 0x06: return static_cast(_IOC_SIZE(op)); case 0x07: return sony_feature(pad, static_cast(argp), _IOC_SIZE(op)); default: @@ -2779,17 +2502,17 @@ ioctl_deck(DeckHidraw &deck, ioctl_request_t op, void *argp) { return -1; } switch (_IOC_NR(op)) { - case 0x01: // HIDIOCGRDESCSIZE + case 0x01: *static_cast(argp) = sizeof(kDeckReportDescriptor); return 0; - case 0x02: { // HIDIOCGRDESC: struct hidraw_report_descriptor { __u32 size; __u8 value[4096]; } + case 0x02: { auto *descriptor = static_cast(argp); uint32_t size; memcpy(&size, descriptor, sizeof(size)); memcpy(descriptor + 4, kDeckReportDescriptor, std::min(size, sizeof(kDeckReportDescriptor))); return 0; } - case 0x03: { // HIDIOCGRAWINFO: struct hidraw_devinfo { __u32 bustype; __s16 vendor, product; } + case 0x03: { struct { uint32_t bustype; int16_t vendor; @@ -2798,11 +2521,11 @@ ioctl_deck(DeckHidraw &deck, ioctl_request_t op, void *argp) { memcpy(argp, &info, sizeof(info)); return 0; } - case 0x04: return copy_ioctl_string(op, argp, DECK_NAME); // HIDIOCGRAWNAME - case 0x05: return copy_ioctl_string(op, argp, "usb-droiddeck-1/input2"); // HIDIOCGRAWPHYS - case 0x08: return copy_ioctl_string(op, argp, DECK_SERIAL); // HIDIOCGRAWUNIQ - case 0x06: return deck_feature(deck, op, static_cast(argp), true); // HIDIOCSFEATURE - case 0x07: return deck_feature(deck, op, static_cast(argp), false); // HIDIOCGFEATURE + case 0x04: return copy_ioctl_string(op, argp, DECK_NAME); + case 0x05: return copy_ioctl_string(op, argp, "usb-droiddeck-1/input2"); + case 0x08: return copy_ioctl_string(op, argp, DECK_SERIAL); + case 0x06: return deck_feature(deck, op, static_cast(argp), true); + case 0x07: return deck_feature(deck, op, static_cast(argp), false); default: Logger::log("deck: unhandled hidraw ioctl 0x%02x\n", _IOC_NR(op)); errno = EINVAL; @@ -2831,10 +2554,6 @@ __attribute__((visibility("hidden"))) static void triton_queue_output(DeckHidraw Logger::log("triton: %s report 0x%02x (x%u, %zu bytes)\n", feature ? "feature" : "output", report_id, n, size); } -// HIDIOCGFEATURE: the client reads the controller's info (report 1) this way, and a device that -// answers with nothing is dropped as a zombie. The read goes to the controller itself, as SDL's -// BLE backend does it (hid.m, get_feature_report): its reply is the report, with the report ID -// put in front when the controller left it out. Called without controller_mutex: it waits. __attribute__((visibility("hidden"))) static int triton_get_feature(DeckHidraw &triton, uint8_t *report, size_t size) { if (!report || size < 2 || triton.mapping_size < TRITON_SHARED_OFFSET + TRITON_SHARED_BLOCK_SIZE) { errno = EINVAL; @@ -2877,9 +2596,6 @@ __attribute__((visibility("hidden"))) static int triton_get_feature(DeckHidraw & return -1; } -// The emulated controller's feature reports: report ID 1, then the Valve message (type, length, -// payload), answered here as the Deck's are. The identity is the one a 2025 Steam Controller gave -// on device (HWID 72, its firmware timestamp), with this library's serial. static constexpr uint32_t TRITON_HWID = 72; static constexpr uint32_t TRITON_FW_TIMESTAMP = 0x6AC686B3; @@ -2902,7 +2618,6 @@ __attribute__((visibility("hidden"))) static int triton_pad_feature(DeckHidraw & reply[1] = triton.pending_feature; switch (triton.pending_feature) { case DECK_GET_ATTRIBUTES_VALUES: { - // (attribute, 32-bit value) pairs: product id, capabilities, firmware timestamp, hardware id. static const uint8_t kIds[] = {0x01, 0x02, 0x04, 0x09, 0x0a, 0x0b}; const uint32_t values[] = {static_cast(TRITON_WIRED_PRODUCT), 0, TRITON_FW_TIMESTAMP, TRITON_HWID, 0, 4000}; reply[2] = sizeof(kIds) * 5; @@ -2914,7 +2629,7 @@ __attribute__((visibility("hidden"))) static int triton_pad_feature(DeckHidraw & } case DECK_GET_STRING_ATTRIBUTE: reply[2] = 0x14; - reply[3] = 0x01; // the unit serial + reply[3] = 0x01; memcpy(reply + 4, TRITON_SERIAL, strlen(TRITON_SERIAL)); break; case DECK_GET_CHIP_ID: @@ -2930,25 +2645,22 @@ __attribute__((visibility("hidden"))) static int triton_pad_feature(DeckHidraw & return static_cast(length); } -// The emulated controller's haptic output reports (SDL, controller_structs.h) on the handheld's -// own motor: rumble as asked, the trackpad pulses and effects as short ticks. __attribute__((visibility("hidden"))) static void triton_pad_output(const uint8_t *data, size_t size) { if (!data || size < 2) return; switch (data[0]) { - case 0x80: // ID_OUT_REPORT_HAPTIC_RUMBLE: type, intensity, left {speed, gain}, right {speed, gain} + case 0x80: if (size >= 10) { int left = data[4] | data[5] << 8, right = data[7] | data[8] << 8; - // SDL repeats a held rumble every 40 ms, so each report lasts a little longer than that. send_vibration(left, right, left || right ? 100 : 0, 0); } break; - case 0x81: // ID_OUT_REPORT_HAPTIC_PULSE: side, on_us, off_us, repeat count + case 0x81: if (size >= 8) { int on_ms = (data[2] | data[3] << 8) / 1000, repeat = data[6] | data[7] << 8; send_vibration(0, 40000, std::max(8, std::min(200, on_ms * std::max(1, repeat))), 0); } break; - case 0x82: case 0x83: case 0x84: case 0x85: // command, tone, sweep, script + case 0x82: case 0x83: case 0x84: case 0x85: send_vibration(0, 30000, 15, 0); break; default: @@ -2970,7 +2682,6 @@ __attribute__((visibility("hidden"))) static int ioctl_triton(DeckHidraw &triton uint32_t size; memcpy(&size, descriptor, sizeof(size)); memcpy(descriptor + 4, kTritonReportDescriptor, std::min(size, sizeof(kTritonReportDescriptor))); - // The wired controller's state report (SteamDeckPad's TRITON_WIRED_REPORT_DESCRIPTOR). if (triton.emulated && size > 15) descriptor[4 + 15] = TRITON_STATE_WIRED; return 0; } @@ -2988,8 +2699,6 @@ __attribute__((visibility("hidden"))) static int ioctl_triton(DeckHidraw &triton case 0x05: return copy_ioctl_string(op, argp, triton.emulated ? "usb-droiddeck-3/input0" : "bluetooth-droiddeck-1"); case 0x08: return copy_ioctl_string(op, argp, TRITON_SERIAL); case 0x06: { - // The client's feature reports are the report ID and 64 bytes; the last is padding the BLE - // characteristic does not take (SteamControllerBle.sendOutputReport drops it), so 64 are kept. size_t size = _IOC_SIZE(op); if (!argp || size < 2 || size > 65) { errno = EINVAL; @@ -3008,10 +2717,6 @@ __attribute__((visibility("hidden"))) static int ioctl_triton(DeckHidraw &triton } } -// A device scan that fails on a phone and not on the Thor: libudev gives up its whole scan on any -// sysfs directory it cannot read for a reason other than its absence, and a phone's SELinux policy -// denies directories a permissive device lets through. The client's /sys failures that are not -// ENOENT are logged, each path once, so pad.log names the directory that stopped the scan. __attribute__((visibility("hidden"))) static std::string note_sys_failure(int dirfd, const char *pathname) { int saved_errno = errno; std::string path; @@ -3052,11 +2757,6 @@ __attribute__((visibility("hidden"))) static std::string note_sys_failure(int di return path; } -// An enforcing SELinux policy (every retail phone; the Thor only with it forced on) refuses an app -// a listing of /sys, /sys/class and /sys/bus. libudev lists /sys/class to find the subsystems to -// descend into and gives up its whole scan when it cannot, so the client never reached hidraw and -// found no Deck. The client is listed the stand-ins instead - hidraw under class - and opens -// /sys/class/hidraw itself, which is ours and readable. Only on that refusal, only for the client. __attribute__((visibility("hidden"))) static std::string deck_listing_for(const char *path) { const char *listing = deck_listing_dir(); if (!path || !listing || errno != EACCES || !fake_deck_enabled() || !process_is_steam_client()) return {}; @@ -3072,9 +2772,6 @@ __attribute__((visibility("hidden"))) static std::string deck_listing_for(const } #ifdef __GLIBC__ -// sd-device's path chase now requires a mount ID, which statx only supplies from Linux 5.8. -// Older handheld kernels still publish the real ID in fdinfo. Scope this compatibility path to -// libudev/libsystemd in the Deck-reading Steam client, and never invent a unique mount ID. __attribute__((visibility("hidden"))) static bool deck_device_library(void *caller) { Dl_info info; if (!dladdr(caller, &info) || !info.dli_fname) return false; @@ -3099,7 +2796,6 @@ __attribute__((visibility("hidden"))) static bool deck_mount_id_from_fdinfo( } if (target < 0) return false; struct stat st; - // Pin the inode and check it still matches the successful statx before adding information. bool same = real_fstat(target, &st) == 0 && (buf->stx_mask & STATX_INO) && st.st_ino == buf->stx_ino && major(st.st_dev) == buf->stx_dev_major && minor(st.st_dev) == buf->stx_dev_minor; @@ -3129,7 +2825,7 @@ EXPORT int statx(int dirfd, const char *path, int flags, unsigned mask, struct s static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "statx")); int result = real(dirfd, path, flags, mask, buf); int saved_errno = errno; - constexpr unsigned unique_mount_id = 0x4000; // STATX_MNT_ID_UNIQUE, newer than build headers. + constexpr unsigned unique_mount_id = 0x4000; if (result == 0 && (mask & STATX_MNT_ID) && !(buf->stx_mask & (STATX_MNT_ID | unique_mount_id)) && fake_deck_enabled() && process_is_steam_client() && deck_device_library(__builtin_return_address(0))) { @@ -3553,7 +3249,6 @@ EXPORT int inotify_add_watch(int fd, const char *pathname, uint32_t mask) { } else if (!strcmp(pathname, "/dev/input")) { pathname = fake_hook_dir(); } else if (!strcmp(pathname, "/dev") && fake_deck_enabled() && process_is_steam_client()) { - // Beside /dev itself, not instead: the same descriptor gets both directories' events. std::string events = hidraw_event_dir(); mkdir(events.c_str(), 0700); my_inotify_add_watch(fd, events.c_str(), mask); @@ -3586,8 +3281,6 @@ static bool wait_snapshot(std::shared_ptr fake, return false; } if (read_snapshot(fake->ring, snap)) return true; - // Enumeration queries must not fail merely because Android is publishing - // an input frame. Other hooks (including Binder) can proceed while we wait. guard.unlock(); struct timespec wait = {0, 1000000}; int result = nanosleep(&wait, nullptr); @@ -3602,7 +3295,6 @@ static bool wait_snapshot(std::shared_ptr fake, static int forward_ioctl(int fd, ioctl_request_t op, void *argp) { using ioctl_fn = int (*)(int, ioctl_request_t, ...); - // Preserve later preload hooks, including GPU allocation adapters. static const auto next = reinterpret_cast(dlsym(RTLD_NEXT, "ioctl")); return next ? next(fd, op, argp) : static_cast(syscall(SYS_ioctl, fd, op, argp)); } @@ -3615,9 +3307,6 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) { argp = va_arg(va, void *); va_end(va); - // The lock must not be held across the passthrough: binder's transport is a - // blocking ioctl(BINDER_WRITE_READ), so a parked binder pool thread would own - // controller_mutex() for as long as it waits and deadlock every other ioctl. if (!watched(fd)) return forward_ioctl(fd, op, argp); std::unique_lock guard(controller_mutex()); auto maker = uinput_map().find(fd); @@ -3643,7 +3332,6 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) { int number = (op >> 0 & 0xFF); const char *event = controller->second->event ? controller->second->event : "event0"; int event_number = controller->second->slot; - // A virtual pad answers with what its maker described, not with the app's pad identity. const UinputDescription *made = controller->second->uinput; if (type == 0x45 && number == 0x1) { @@ -3866,8 +3554,6 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) { guard.unlock(); return syscall(SYS_read, fd, buf, count); } - // Keep the mapping alive across waits, even if close() removes the fd and - // another open reuses its number. Every state access still holds the lock. auto handle = controller->second; FakeController &fake = *handle; if (count < FAKE_INPUT_EVENT_SIZE) { @@ -3913,8 +3599,6 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) { memcpy(static_cast(buf) + i * FAKE_INPUT_EVENT_SIZE, &ev, FAKE_INPUT_EVENT_SIZE); } - // A producer can lap the reader while it copies. Discard that copy - // instead of delivering torn/overwritten events or losing a release. __atomic_thread_fence(__ATOMIC_ACQUIRE); if (snap.sequence != __atomic_load_n(&fake.ring->snapshot_seq, __ATOMIC_RELAXED)) continue; @@ -3923,8 +3607,6 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) { } } - // Finish a captured frame before handling a newer resync. Its counter is - // acknowledged only when that newer snapshot is actually captured. size_t events = std::min(requested, fake.keyframe_remaining); struct timeval now = {}; gettimeofday(&now, nullptr); @@ -4006,7 +3688,6 @@ EXPORT ssize_t writev(int fd, const struct iovec *iov, int iovcnt) { } auto made = uinput_map().find(fd); if (made != uinput_map().end()) { - // One description or whole events per vector, which is how uinput writers write. ssize_t total = 0; for (int i = 0; i < iovcnt; i++) { ssize_t written = write_uinput(*made->second, iov[i].iov_base, iov[i].iov_len); @@ -4154,8 +3835,6 @@ EXPORT int ppoll(struct pollfd *fds, nfds_t nfds, if (!is_fake_input_fd(fds[i].fd)) continue; if (!sigmask) return poll_fake(fds, nfds, static_cast(timespec_to_ms(timeout)), nullptr); - // Keep signals pending between polling slices. Each kernel ppoll applies - // the requested mask atomically with its wait, just like a single ppoll. sigset_t all_signals, original_mask; sigfillset(&all_signals); int error = pthread_sigmask(SIG_SETMASK, &all_signals, &original_mask); @@ -4315,8 +3994,6 @@ EXPORT int select(int nfds, fd_set *readfds, fd_set *writefds, } #ifdef __GLIBC__ -// Steam's binaries were linked against an older glibc and reach the calls above through these -// names instead. static mode_t open_mode(int flags, va_list va) { return (flags & (O_CREAT | O_TMPFILE)) ? va_arg(va, mode_t) : 0; } diff --git a/app/src/main/cpp/framegen/VulkanRendererContext.h b/app/src/main/cpp/framegen/VulkanRendererContext.h index cec4a4119..efbc00c68 100644 --- a/app/src/main/cpp/framegen/VulkanRendererContext.h +++ b/app/src/main/cpp/framegen/VulkanRendererContext.h @@ -84,12 +84,11 @@ struct VkTable { PFN_vkCmdSetScissor CmdSetScissor; PFN_vkCmdPipelineBarrier CmdPipelineBarrier; PFN_vkCmdCopyImage CmdCopyImage; - PFN_vkCmdBlitImage CmdBlitImage; // frame-gen capture resolution: composite -> swapchain upscale - // Compute: the native LSFG chain is 25 compute dispatches. + PFN_vkCmdBlitImage CmdBlitImage; + PFN_vkCmdDispatch CmdDispatch; PFN_vkCreateComputePipelines CreateComputePipelines; PFN_vkUnmapMemory UnmapMemory; - // win-fg native: clears its flow scratch, resets its scratch pool on resize. PFN_vkCmdClearColorImage CmdClearColorImage; PFN_vkResetDescriptorPool ResetDescriptorPool; PFN_vkCmdCopyBufferToImage CmdCopyBufferToImage; @@ -125,11 +124,8 @@ struct VkTable { #include #include -// Renderer-neutral direct-scanout impl (owns the SurfaceControl/AHB state). -// Included after SCANOUT_LOG is defined above; its own definition is guarded. #include "../scanout/ScanoutContext.h" -// Native (compositor-side) LSFG frame generation - capability gate. #include "lsfg/lsfg_probe.h" #include #include @@ -139,89 +135,83 @@ namespace winfg { class Engine; } static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2; -// Push-constant layouts for the spatial-upscaler post passes. The leading vec4 -// `ndc` matches upscale.vert; the remaining members are read only by the -// fragment stage. std430 offsets line up with these C structs. -struct SgsrPushConstants { // 36 bytes + + + +struct SgsrPushConstants { float ndc[4]; - float viewportInfo[4]; // xy = 1/inputSize, zw = inputSize px - float edgeSharpness; // SGSR EdgeSharpness, from the slider + float viewportInfo[4]; + float edgeSharpness; }; -struct NisPushConstants { // 36 bytes +struct NisPushConstants { float ndc[4]; - float viewportInfo[4]; // xy = 1/inputSize, zw = inputSize px - float sharpness; // NIS sharpness [0..1], from the slider + float viewportInfo[4]; + float sharpness; }; -struct EasuPushConstants { // 88 bytes +struct EasuPushConstants { float ndc[4]; uint32_t con0[4], con1[4], con2[4], con3[4]; float outW, outH; }; -struct RcasPushConstants { // 40 bytes +struct RcasPushConstants { float ndc[4]; - uint32_t con[4]; // con.x = bit-packed sharpness + uint32_t con[4]; float outW, outH; }; -// Composable post effects (AMD CAS sharpen + fake-HDR). Both lead with vec4 ndc -// (offset 0) like the upscaler PCs and stay well under the 88-byte PC range. -struct CasPushConstants { // 32 bytes + + +struct CasPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px - float sharpness; // CAS SHARPNESS term [0..1] + float resolution[2]; + float sharpness; float _pad; }; -struct HdrPushConstants { // 24 bytes +struct HdrPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px + float resolution[2]; }; -// Phase 2 screen effects ported from the GL EffectComposer. Each leads with vec4 -// ndc (offset 0) like the upscaler PCs and stays well under the 88-byte PC range. -struct FxaaPushConstants { // 24 bytes + + +struct FxaaPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px + float resolution[2]; }; -struct ToonPushConstants { // 24 bytes +struct ToonPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px + float resolution[2]; }; -struct ColorPushConstants { // 32 bytes +struct ColorPushConstants { float ndc[4]; - float brightness; // additive [-1,1] - float contrast; // [0,2] - float gamma; // [0.1,5] - float saturation; // [0,2]; 1 = unchanged, 0 = greyscale. - // APPENDED last so the three offsets above - // stay where color.frag already had them. + float brightness; + float contrast; + float gamma; + float saturation; + + }; -struct NtscPushConstants { // 28 bytes +struct NtscPushConstants { float ndc[4]; - float resolution[2]; // screen size in px (TextureSize/resolution) - float frameCount; // animated chroma-phase counter + float resolution[2]; + float frameCount; }; -struct CrtPushConstants { // 24 bytes +struct CrtPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px (unused by shader) + float resolution[2]; }; -struct DebandPushConstants { // 28 bytes +struct DebandPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px (layout parity) - float strength; // dither amplitude in LSBs (1.0 = +/-1/255) + float resolution[2]; + float strength; }; class VulkanRendererContext { public: - // `lsfgVk11Compat` (experimental) lets the LSFG capability probe accept a - // Vulkan 1.1/1.2 device that offers the extensions the chain needs; it has - // to be known before vkCreateDevice, hence a constructor argument. VulkanRendererContext(ANativeWindow* window, int cWidth, int cHeight, void* adrenotoolsHandle = nullptr, bool lsfgVk11Compat = false); ~VulkanRendererContext(); void onSurfaceResized(int width, int height); void setTransform(float ox, float oy, float sx, float sy); - // #413: game-region clip rect (surface px). Stored and applied as the swapchain scissor in - // recordCmdBuf so a FILL/STRETCH game confined to a half (TOP/BOTTOM alignment) can't bleed into - // the on-screen-controls half. A full-surface (or w<=0) rect means "no clip" (CENTER). void setClipRegion(int x, int y, int w, int h); void updatePointerPosition(short x, short y); void updateWindowContent(int64_t id, void* pixels, short w, short h, short stride, int x, int y); @@ -241,13 +231,7 @@ public: void scanoutSetBuffer(AHardwareBuffer* ahb, int x, int y, int w, int h, int fenceFd = -1); void scanoutSetCursorImage(void* pixels, short w, short h, short stride); void scanoutSetCursorPos(short x, short y, short hotX, short hotY); - // The renderer-neutral scanout implementation. The scanout methods above are - // thin forwarders to this (see VulkanRendererScanout.cpp); the cursor render- - // thread signalling stays in the forwarders, not in ScanoutContext. ScanoutContext scanout; - // Bound to scanout's atomics so the existing JNI ABI (r->scanoutActive / - // r->gameFrameDelivered direct member access) keeps compiling against a - // single source of truth. Must be declared after `scanout`. std::atomic& scanoutActive = scanout.scanoutActive; std::atomic& gameFrameDelivered = scanout.gameFrameDelivered; std::atomic surfaceDetached{false}; @@ -286,47 +270,16 @@ public: void setPresentMode(VkPresentModeKHR mode); std::vector getSupportedPresentModes() const; - // --- Native LSFG frame generation ------------------------------------- - // Capability verdict for the compositor-side LSFG engine. Filled at device - // creation and completed once the swapchain format is known; read by the - // UI (through JNI) to grey the engine out with a reason. const lsfg::Caps& lsfgCaps() const { return lsfgCaps_; } - // Arm/disarm native LSFG frame generation for this session. Changing the - // armed state recreates the swapchain, because the composite path needs - // TRANSFER_DST usage and a deeper image queue that a normal session does - // not pay for. void setFrameGenArmed(bool armed, int multiplier); bool frameGenArmed() const { return fgArmed_.load(std::memory_order_relaxed); } - // Live frame-gen telemetry for the in-game readout: - // [0] generations the governor currently trusts - // [1] generations actually planned for the last source frame - // [2] measured source (real) frames per second - // [3] measured presented frames per second - // [4] thermal status, -1 when the device gives no signal - // [5] GPU milliseconds the chain spends per generated frame, -1 unknown void frameGenStats(float out[6]) const; - // Why the selected native frame-gen engine cannot run here, for the UI: - // -1 not known yet (caps not probed), 0 fine, 1 this driver lacks what the - // engine needs (lsfgCaps().reason says which gate), 2 the engine failed to start. int frameGenProblem() const; - // Path to the SPIR-V cache built from the user's Lossless.dll. Setting it - // drops any existing engine so the next armed frame rebuilds from it. void setLsfgCachePath(const char* path); - // Flow scale (0.25-1.0) and the panel's real refresh rate. The pacer never - // generates above the refresh rate. - // Experimental: `captureHeight` sizes the composite ring the chain runs on, - // the width following the swapchain's aspect, so a phone GPU generates at - // game-like resolution and the result is blitted up. 0 = panel; - // kFgCaptureGame = the X screen's height (containerHeight), which already - // carries the per-game override and render scale; otherwise a pixel height. static constexpr int32_t kFgCaptureGame = -1; void setFrameGenTuning(float flowScale, float refreshHz, int32_t captureHeight = 0); - // Which native engine generates: 0 = LSFG (needs the cache built from the - // user's Lossless.dll), 1 = win-fg (our own chain, embedded, needs nothing). - // Switching drops the other engine so only one ever holds GPU resources. void setFrameGenEngine(int kind); - // win-fg only: interpolation model (3/4) and performance preset (0..2). void setWinFgTuning(int model, int perfPreset); private: @@ -359,11 +312,9 @@ private: ANativeWindow* window; int surfaceWidth, surfaceHeight, containerWidth, containerHeight; - // #413 compositor clip rect (surface px). clipRegionW<=0 => disabled (whole swapchain). Written from - // the renderer thread (setClipRegion), read from the render thread (recordCmdBuf) -> atomic. std::atomic clipRegionX{0}, clipRegionY{0}, clipRegionW{0}, clipRegionH{0}; void* adrenotoolsHandle = nullptr; - bool lsfgVk11Compat_ = false; // see the constructor + bool lsfgVk11Compat_ = false; int filterMode = 0; bool swapRB = false; float maxAnisotropy = 1.0f; @@ -386,9 +337,6 @@ private: std::vector framePreUpload; std::vector framePostUpload; - // [P0] All SurfaceControl / AHardwareBuffer scanout state moved to - // ScanoutContext (member `scanout` above). Dead members (lastDst*, - // ScanoutPending/scanoutPending*, fnSTSetBackPressure) were dropped. std::atomic pointerX{0}, pointerY{0}; float sceneOffsetX=0.f, sceneOffsetY=0.f, sceneScaleX=1.f, sceneScaleY=1.f; @@ -428,138 +376,69 @@ private: VkPipeline pipeline = VK_NULL_HANDLE; - // ---- Spatial upscaler (SGSR / FSR1) ---- - // upscalerMode enum (mirrored by JNI nativeSetUpscaler): - // 0 = none (passthrough; current sampler governs scaling) - // 1 = linear (rebuild base sampler LINEAR; no shader pass) - // 2 = nearest (rebuild base sampler NEAREST; no shader pass) - // 3 = sgsr (Snapdragon GSR 1.0; single pass; aspect-fit / letterbox) - // 4 = fsr (AMD FSR1 EASU+RCAS; two passes; fill / stretch) - // 5 = fsr_fit (AMD FSR1 EASU+RCAS; two passes; aspect-fit / letterbox) - // 6 = sharpen (RCAS-only; any resolution; aspect-fit / letterbox) - // 7 = nis (NVIDIA Image Scaling NVScaler; single pass; aspect-fit) - // 8 = sgsr_quality (SGSR 1 edge-direction variant; single pass; aspect-fit) - // Shader upscaling only engages for modes 3-8 AND when the game render - // resolution (container) is smaller than the swapchain. Otherwise the - // existing direct-to-swapchain path is used unchanged. int upscalerMode = 0; - // Independent of upscalerMode: high-quality Lanczos downscale for the - // supersampling case (game render res > display res). When enabled and the - // render res exceeds the swapchain, replaces the bilinear minify with a - // proper downscale pass. Supersampling and the upscale modes are mutually - // exclusive in practice (one is render>display, the other render CAS SHARPNESS + int casSharpness = 60; bool hdrEnabled = false; - float upscaleSharpness01 = 0.75f; // slider/100; RCAS lobe scale, SGSR edge derived - // Debanding: terminal TPDF/IGN dither pass before the 8-bit swapchain quantize. + float upscaleSharpness01 = 0.75f; + bool debandEnabled = false; - int debandStrength = 100; // slider 0..200 -> strength/100 LSBs + int debandStrength = 100; - // Phase 2 screen effects (GL EffectComposer parity). FXAA/Toon/CRT/NTSC are - // binary; Color is the brightness/contrast/gamma grade (always-applied via the - // sliders, no toggle) and is "enabled" only when not at neutral (0,0,1). bool fxaaEnabled = false; bool toonEnabled = false; bool crtEnabled = false; bool ntscEnabled = false; - bool colorEnabled = false; // derived: !neutral grade - float colorBrightness = 0.0f; // [-1,1] (slider/100, clamped) - float colorContrast = 0.0f; // [0,2] (slider/100, clamped) - float colorGamma = 1.0f; // [0.1,5] - float colorSaturation = 1.0f; // [0,2] (slider/100, clamped) - uint32_t ntscFrameCounter = 0; // animates NTSC chroma phase + bool colorEnabled = false; + float colorBrightness = 0.0f; + float colorContrast = 0.0f; + float colorGamma = 1.0f; + float colorSaturation = 1.0f; + uint32_t ntscFrameCounter = 0; - VkSampler upscaleSampler = VK_NULL_HANDLE; // linear clamp; offscreen/mid input + VkSampler upscaleSampler = VK_NULL_HANDLE; - // === Native LSFG: composite target ring =================================== - // Frame generation cannot composite straight into a swapchain image: the - // finished frame has to be READABLE (it becomes the next frame's LSFG - // input) and generated frames have to be STORAGE-WRITABLE by a compute - // dispatch. Android swapchain images are COLOR_ATTACHMENT only, and - // storage support on a swapchain format is not something a driver owes us. - // - // So when frame gen is armed the whole existing recording is redirected at - // a composite image we own - format-identical to the swapchain, so every - // existing pipeline stays render-pass compatible - and a copy moves it into - // the acquired swapchain image at the end. With frame gen off, not one of - // these objects is created and the direct-to-swapchain path is untouched. struct CompositeTarget { VkImage img = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; - VkImageView view = VK_NULL_HANDLE; // colour attachment + sampled - VkImageView storageView = VK_NULL_HANDLE; // compute writes (generate) + VkImageView view = VK_NULL_HANDLE; + VkImageView storageView = VK_NULL_HANDLE; VkFramebuffer fb = VK_NULL_HANDLE; - VkDescriptorSet ds = VK_NULL_HANDLE; // sampled, for later passes + VkDescriptorSet ds = VK_NULL_HANDLE; }; - // Hard ceiling: (max generations + 1) presentable frames per source frame, - // times a queue depth of 2, capped so the footprint stays bounded. static constexpr uint32_t kMaxCompositeTargets = 7; std::vector compositeTargets; - VkRenderPass compositeRenderPass = VK_NULL_HANDLE; // CLEAR -> GENERAL + VkRenderPass compositeRenderPass = VK_NULL_HANDLE; uint32_t compositeW = 0, compositeH = 0; - uint32_t compositeIndex = 0; // rotates per composite; gives history for free - bool compositeArmed = false; // targets exist AND this frame uses them - bool swapchainTransferDst = false; // swapchain was created with TRANSFER_DST + uint32_t compositeIndex = 0; + bool compositeArmed = false; + bool swapchainTransferDst = false; - // Set from the app when the native LSFG engine is selected for this - // session. Read on the render thread; false keeps every path as it was. std::atomic fgArmed_{false}; std::atomic fgMultiplier_{0}; - // Tuning is written by the UI thread and consumed by the render thread, so - // it is published through atomics and applied to the engine at the top of - // the frame-gen block. The UI thread never touches the engine itself. std::atomic fgFlowScale_{1.0f}; std::atomic fgRefreshHz_{0.0f}; std::atomic fgConfigDirty_{true}; - // Experimental (see setFrameGenTuning): capture height of the composite - // ring. 0 = panel resolution, kFgCaptureGame = the X screen's height. std::atomic fgCaptureHeight_{0}; - // Extent the composite ring should have for the current swapchain and - // capture height (width follows the swapchain aspect, both even). void compositeExtentFor(uint32_t& w, uint32_t& h) const; - // The extent the scene is rendered at this frame: the composite ring while - // frame gen runs on a ring smaller than the swapchain (experimental capture - // resolution), otherwise the swapchain. Every full-target pass (the effect - // chain, the spatial upscalers, the render-scale downscale) sizes itself by - // this, so those keep working below panel resolution and their - // intermediates shrink with the ring. VkExtent2D renderExtent() const { if (compositeActive() && (compositeW != swapchainExt.width || compositeH != swapchainExt.height)) return VkExtent2D{compositeW, compositeH}; return swapchainExt; } - // Copy (same extent) or blit (composite smaller than the swapchain) a - // composite-ring image, already in TRANSFER_SRC, into a swapchain image - // already in TRANSFER_DST. void recordCompositeToSwapchainTransfer(VkCommandBuffer cb, VkImage src, uint32_t imgIdx); bool createCompositeRenderPass(); bool ensureCompositeTargets(uint32_t w, uint32_t h, uint32_t count); void destroyCompositeTargets(); - // True when this frame should composite off-swapchain. Call on the render - // thread; every gate must hold or we fall back to the untouched path. bool compositeActive() const; - // Render pass / framebuffer this frame draws its FINAL pass into. VkRenderPass targetRenderPass() const; VkFramebuffer targetFramebuffer(uint32_t imgIdx) const; - // Copy the finished composite into the acquired swapchain image and leave - // it in PRESENT_SRC. No-op when the composite path is not active. void copyCompositeToSwapchain(VkCommandBuffer cb, uint32_t imgIdx); - // === Native LSFG: the software cursor ==================================== - // LSFG interpolates whatever it is given, so a cursor composited into the - // frame gets warped along the flow field and smears. It is therefore - // excluded from the composite while frame gen is armed and drawn once into - // EVERY presented image instead - real and generated alike - through a - // load-op render pass that leaves the image ready to present. VkRenderPass cursorOverlayRenderPass = VK_NULL_HANDLE; struct CursorOverlay { bool draw = false; @@ -569,19 +448,16 @@ private: CursorOverlay cursorOverlay_{}; bool createCursorOverlayRenderPass(); - // Draw the cursor into an already-copied swapchain image and leave it in - // PRESENT_SRC. Takes over the final transition from the copy helpers. void recordCursorOverlay(VkCommandBuffer cb, uint32_t imgIdx); - // True when the cursor is being drawn per present rather than composited. bool cursorDrawnPerPresent() const; - // === Native LSFG: the per-source-frame present plan ======================= - // Interpolation produces frames that belong BETWEEN N-1 and N, so the - // generated frames are presented FIRST and real frame N is held back one - // slot. All presents for one source frame are queued together: with FIFO - // the driver then shows them on consecutive vblanks, so the pacing falls - // out of the present mode for free - no sleeps, no render-mode change. - static constexpr uint32_t kMaxPresentsPerFrame = 4; // 1 real + up to 3 generated + + + + + + + static constexpr uint32_t kMaxPresentsPerFrame = 4; struct FrameGenPlan { uint32_t generations = 0; uint32_t presents = 1; @@ -594,30 +470,17 @@ private: bool lsfgEngineTried_ = false; std::unique_ptr winfgEngine_; bool winfgEngineTried_ = false; - std::atomic fgEngineKind_{0}; // 0 = lsfg, 1 = win-fg + std::atomic fgEngineKind_{0}; std::atomic fgModel_{4}; std::atomic fgPerfPreset_{1}; bool ensureWinFgEngine(); - // Capability gate for the SELECTED engine. win-fg's shaders need only a - // storage-capable swapchain format; the LSFG chain also needs the fp16 / - // memory-model feature set, which some otherwise capable GPUs lack. bool fgCapsOk() const; - // Sync objects are indexed per PRESENT, not per composite: each pending - // present needs its own image-available and render-finished semaphore. uint32_t syncSlot(uint32_t k) const { return currentFrame * kMaxPresentsPerFrame + k; } - // Destroy and rebuild every acquire/present semaphore. Called with the - // swapchain, so no stale pending signal can survive into the new one. void recreateSyncObjects(); uint32_t fgAcquireFailLog_ = 0; - // Measured PRESENTS per second - the rate that actually reaches the panel, - // counting generated frames. The pacer's own "loop rate" cannot serve this - // purpose: it is sampled once per SOURCE frame, so it always equals the - // guest rate and contains no evidence that generation happened at all. float fgPresentedRate_ = 0.0f; - // Measured SOURCE frames per second over the same window, for engines that - // do not measure it themselves (win-fg). float fgSourceRate_ = 0.0f; uint32_t fgSourceAccum_ = 0; uint32_t fgPresentAccum_ = 0; @@ -626,22 +489,10 @@ private: void trackPresentedRate(uint32_t presents); bool ensureLsfgEngine(); - // One command buffer per pending present: slot 0 carries the composite, - // the chain's shared passes and generated frame 0; each later generated - // frame and the real frame are recorded and submitted on their own, so a - // finished frame reaches the presentation engine without waiting for the - // rest of the chain. uint32_t cmdSlot(uint32_t k) const { return currentFrame * kMaxPresentsPerFrame + k; } - // Shared chain passes (the 24 shaders every generated frame depends on). void recordFrameGenProcess(VkCommandBuffer cb); - // Generated frame g: synthesise into a spare composite target and copy it - // into the swapchain image reserved for it. void recordFrameGenGeneration(VkCommandBuffer cb, uint32_t g); - // Chain cost: a timestamp pair per frame slot, start before the shared - // passes, end after the last generation's compute (before its copy, so a - // vblank wait on the swapchain image is not counted). Read back after the - // slot's fence wait, one frame later. VkQueryPool fgQueryPool_ = VK_NULL_HANDLE; bool fgTimestampsOk_ = false; float fgTimestampPeriodNs_ = 0.0f; @@ -654,22 +505,21 @@ private: void readFgQueryResult(); VkFormat offscreenFmt = VK_FORMAT_R8G8B8A8_UNORM; - VkRenderPass offscreenRenderPass = VK_NULL_HANDLE; // CLEAR -> SHADER_READ_ONLY + VkRenderPass offscreenRenderPass = VK_NULL_HANDLE; VkPipelineLayout postPipeLayout = VK_NULL_HANDLE; VkPipeline sgsrPipeline = VK_NULL_HANDLE; - VkPipeline nisPipeline = VK_NULL_HANDLE; // NVIDIA Image Scaling (mode 7) - VkPipeline sgsrQualityPipeline = VK_NULL_HANDLE; // SGSR 1 edge-direction (mode 8) + VkPipeline nisPipeline = VK_NULL_HANDLE; + VkPipeline sgsrQualityPipeline = VK_NULL_HANDLE; VkPipeline easuPipeline = VK_NULL_HANDLE; VkPipeline rcasPipeline = VK_NULL_HANDLE; VkPipeline downscalePipeline = VK_NULL_HANDLE; - // CAS/HDR can be either a non-final pass (writes an fx target via - // offscreenRenderPass) or the final pass (writes the swapchain via renderPass); - // a pipeline is render-pass-specific, so keep one variant of each. - VkPipeline casPipelineOff = VK_NULL_HANDLE; // -> fx target (offscreenRenderPass) - VkPipeline casPipelineSwap = VK_NULL_HANDLE; // -> swapchain (renderPass) + + + + VkPipeline casPipelineOff = VK_NULL_HANDLE; + VkPipeline casPipelineSwap = VK_NULL_HANDLE; VkPipeline hdrPipelineOff = VK_NULL_HANDLE; VkPipeline hdrPipelineSwap = VK_NULL_HANDLE; - // Phase 2 effects: same Off (-> fx target) / Swap (-> swapchain) variant pair. VkPipeline fxaaPipelineOff = VK_NULL_HANDLE; VkPipeline fxaaPipelineSwap = VK_NULL_HANDLE; VkPipeline toonPipelineOff = VK_NULL_HANDLE; @@ -680,10 +530,8 @@ private: VkPipeline ntscPipelineSwap = VK_NULL_HANDLE; VkPipeline crtPipelineOff = VK_NULL_HANDLE; VkPipeline crtPipelineSwap = VK_NULL_HANDLE; - // Debanding is always the LAST effect (terminal) -> only a swapchain variant. VkPipeline debandPipelineSwap = VK_NULL_HANDLE; - // offscreen composite target @ game/container resolution VkImage offscreenImg = VK_NULL_HANDLE; VkDeviceMemory offscreenMem = VK_NULL_HANDLE; VkImageView offscreenView = VK_NULL_HANDLE; @@ -691,7 +539,6 @@ private: VkDescriptorSet offscreenDS = VK_NULL_HANDLE; int offscreenW = 0, offscreenH = 0; - // FSR intermediate (EASU output) @ upscale output resolution VkImage midImg = VK_NULL_HANDLE; VkDeviceMemory midMem = VK_NULL_HANDLE; VkImageView midView = VK_NULL_HANDLE; @@ -699,8 +546,6 @@ private: VkDescriptorSet midDS = VK_NULL_HANDLE; int midW = 0, midH = 0; - // Effect-chain intermediates @ swapchain resolution. fx1 holds the scaled (or - // composited) image fed into CAS/HDR; fx2 ping-pongs when both effects run. VkImage fx1Img = VK_NULL_HANDLE; VkDeviceMemory fx1Mem = VK_NULL_HANDLE; VkImageView fx1View = VK_NULL_HANDLE; @@ -715,21 +560,18 @@ private: VkDescriptorSet fx2DS = VK_NULL_HANDLE; int fx2W = 0, fx2H = 0; - // Per-frame upscale plan, computed in renderFrame, consumed by recordCmdBuf. - // upFrame.mode reuses the upscalerMode enum (3=sgsr,4=fsr,5=fsr_fit,6=sharpen,7=nis,8=sgsr_quality) - // plus an internal sentinel (UPMODE_DOWNSCALE) for the supersampling path. struct UpscaleFrame { bool active = false; int mode = 0; int outX = 0, outY = 0, outW = 0, outH = 0; - bool cas = false; // run CAS this frame (snapshot of casEnabled) - bool hdr = false; // run HDR this frame (snapshot of hdrEnabled) - bool fxaa = false; // snapshot of fxaaEnabled - bool toon = false; // snapshot of toonEnabled - bool color = false; // snapshot of colorEnabled (non-neutral grade) - bool ntsc = false; // snapshot of ntscEnabled - bool crt = false; // snapshot of crtEnabled - bool deband = false;// snapshot of debandEnabled (terminal dither) + bool cas = false; + bool hdr = false; + bool fxaa = false; + bool toon = false; + bool color = false; + bool ntsc = false; + bool crt = false; + bool deband = false; SgsrPushConstants sgsrPC{}; NisPushConstants nisPC{}; EasuPushConstants easuPC{}; diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_alpha.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_alpha.cpp index 04a37b720..52b5c76c6 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_alpha.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_alpha.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_alpha.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_alpha.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_alpha.hpp index 9a4e15ddf..55db1c754 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_alpha.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_alpha.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_beta.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_beta.cpp index d9413e647..cc9746e11 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_beta.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_beta.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_beta.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_beta.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_beta.hpp index 7d259f9e2..a09efcb3b 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_beta.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_beta.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_chain.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_chain.cpp index 54acb1a0f..121b16a68 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_chain.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_chain.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_chain.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_chain.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_chain.hpp index 4bab527fd..f0611dac0 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_chain.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_chain.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_common.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_common.cpp index c93275486..5b61acd4c 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_common.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_common.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_common.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_common.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_common.hpp index c5ab3e0ad..3abe5cfb9 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_common.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_common.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_delta.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_delta.cpp index cf9b31bfa..d09f31d20 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_delta.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_delta.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_delta.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_delta.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_delta.hpp index e40ffe7bd..45e3b4adc 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_delta.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_delta.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp index 0af479be8..3a37f032a 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp @@ -1,9 +1,5 @@ -// See lsfg_dll.h. PE resource walk + on-device SPIR-V cache for the Lossless -// Scaling frame-generation chain. -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck -// by @maxjivi05. GPL-3.0-or-later. +// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). +// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later. #include "lsfg_dll.h" #include "lsfg_dxbc.h" @@ -25,9 +21,9 @@ namespace lsfg { namespace { -// ---- PE constants --------------------------------------------------------- -constexpr uint16_t kDosMagic = 0x5A4Du; // "MZ" -constexpr uint32_t kPeSignature = 0x00004550u; // "PE\0\0" + +constexpr uint16_t kDosMagic = 0x5A4Du; +constexpr uint32_t kPeSignature = 0x00004550u; constexpr uint16_t kPe32Magic = 0x010Bu; constexpr uint16_t kPe32PlusMagic = 0x020Bu; @@ -47,7 +43,6 @@ constexpr size_t kResourceEntrySize = 8u; constexpr uint32_t kResourceSubdirectoryBit = 0x80000000u; constexpr uint32_t kResourceTypeRcData = 10u; -// ---- SPIR-V constants ----------------------------------------------------- constexpr uint32_t kSpirvMagic = 0x07230203u; constexpr size_t kSpirvHeaderWords = 5u; constexpr uint32_t kSpirvWordCountShift = 16u; @@ -64,7 +59,7 @@ constexpr uint32_t kVariantFp32Offset = 98u; constexpr uint32_t kMaxResourceId = 512u; constexpr size_t kMaxSpirvWords = 16u * 1024u * 1024u; -constexpr uint32_t kCacheMagic = 0x4C534642u; // "BFSL" little-endian +constexpr uint32_t kCacheMagic = 0x4C534642u; constexpr uint32_t kCacheVersion = 1u; struct CacheHeader { @@ -83,7 +78,6 @@ struct PeSection { uint32_t rawSize = 0; }; -// Read-only mapping of the DLL. Closes itself; the DLL is never executed. class PeImage { public: ~PeImage() { close(); } @@ -168,7 +162,6 @@ private: std::vector sections_; }; -// RCDATA id -> blob, for the ids we care about. struct ResourceTable { const uint8_t* data[kMaxResourceId] = {}; uint32_t size[kMaxResourceId] = {}; @@ -217,7 +210,6 @@ bool resourceReadLeaf(const PeImage& img, size_t leafOffset, return true; } -// Walk type -> name -> language and keep the first language of every RCDATA id. bool collectRcData(const PeImage& img, size_t resourceBase, ResourceTable& out) { size_t typeTotal = 0; if (!resourceEntryCount(img, resourceBase, typeTotal)) return false; @@ -256,7 +248,7 @@ bool collectRcData(const PeImage& img, size_t resourceBase, ResourceTable& out) if (!resourceReadLeaf(img, resourceBase + langOffset, blob, blobSize)) continue; out.data[nameId] = blob; out.size[nameId] = blobSize; - break; // first language wins + break; } } } @@ -289,7 +281,6 @@ DllStatus parseResources(PeImage& img, ResourceTable& table) { return DllStatus::Ok; } -// ---- SPIR-V adoption (precompiled-variant path) --------------------------- bool isSpirvModule(const uint8_t* blob, uint32_t size) { if (!blob || size < kSpirvHeaderWords * sizeof(uint32_t)) return false; @@ -315,9 +306,6 @@ uint32_t lookupDescriptorSet(const std::vector& w, uint32_t targetId) return 0; } -// Precompiled blobs are renumbered into set/binding order - that is the -// convention they were built with. The DXBC path uses encounter order instead -// (see lsfg_dxbc), which is what DXVK's output pairs with. Do not merge them. bool renumberBindingsSetOrder(std::vector& w) { struct Slot { uint32_t set; uint32_t binding; size_t literalOffset; }; std::vector slots; @@ -399,8 +387,6 @@ bool writeCache(const std::string& cachePath, const CacheHeader& header, const M if (ok) ok = fsync(fileno(f)) == 0; fclose(f); - // Temp file + rename, so a failed or interrupted build can never leave a - // half-written cache that would later be loaded as if it were complete. if (!ok || rename(tempPath.c_str(), cachePath.c_str()) != 0) { unlink(tempPath.c_str()); return false; @@ -416,9 +402,8 @@ bool readCacheHeader(const std::string& cachePath, CacheHeader& out) { return ok && out.magic == kCacheMagic && out.version == kCacheVersion; } -} // namespace +} -// ---- public API ----------------------------------------------------------- const std::vector& shaderIds() { static const std::vector ids = [] { @@ -584,7 +569,6 @@ DllStatus loadModules(const std::string& cachePath, ModuleSet& outSet) { return status; } -// ---- SPIR-V version lowering (Vulkan 1.1/1.2 compat) ---------------------- namespace { @@ -593,14 +577,14 @@ constexpr uint32_t kSpirvOpExtInstImport= 11u; constexpr uint32_t kSpirvOpMemoryModel = 14u; constexpr uint32_t kSpirvOpCapability = 17u; -// Capabilities that are core in the version the translator emits but not in -// the versions we lower to. -constexpr uint32_t kCapVulkanMemoryModel = 5345u; // core from 1.5 -constexpr uint32_t kCapVulkanMemoryModelDeviceScope = 5346u; // core from 1.5 -constexpr uint32_t kCapDemoteToHelperInvocation = 5379u; // core from 1.6 + + +constexpr uint32_t kCapVulkanMemoryModel = 5345u; +constexpr uint32_t kCapVulkanMemoryModelDeviceScope = 5346u; +constexpr uint32_t kCapDemoteToHelperInvocation = 5379u; std::vector encodeOpExtension(const char* name) { - const size_t len = strlen(name) + 1; // incl. NUL + const size_t len = strlen(name) + 1; const size_t strWords = (len + 3) / 4; std::vector ins(1 + strWords, 0u); ins[0] = ((uint32_t)(1 + strWords) << kSpirvWordCountShift) | kSpirvOpExtension; @@ -624,17 +608,13 @@ bool hasOpExtension(const std::vector& w, const char* name) { return false; } -} // namespace +} bool downgradeSpirv(std::vector& w, uint32_t targetVersion) { if (w.size() < kSpirvHeaderWords || w[0] != kSpirvMagic) return false; if (targetVersion < kSpirv14) return false; - if (w[1] <= targetVersion) return true; // already low enough + if (w[1] <= targetVersion) return true; - // Walk the preamble first and only touch the module once it is known to - // be lowerable: note which extension-gated capabilities are declared and - // where the capability block ends (OpExtension must follow every - // OpCapability and precede OpExtInstImport / OpMemoryModel). bool needMemoryModel = false; size_t insertAt = kSpirvHeaderWords; size_t off = kSpirvHeaderWords; @@ -645,9 +625,6 @@ bool downgradeSpirv(std::vector& w, uint32_t targetVersion) { if (opcode == kSpirvOpCapability && length >= 2) { const uint32_t cap = w[off + 1]; if (cap == kCapVulkanMemoryModel || cap == kCapVulkanMemoryModelDeviceScope) needMemoryModel = true; - // Core only in 1.6, and VK_EXT_shader_demote_to_helper_invocation - // is never enabled on the compat path: refuse rather than hand the - // driver a module that is not legal at the lowered version. if (cap == kCapDemoteToHelperInvocation) return false; insertAt = off + length; } else if (opcode == kSpirvOpExtension) { @@ -659,7 +636,6 @@ bool downgradeSpirv(std::vector& w, uint32_t targetVersion) { off += length; } - // 1.5 has the memory model as core; only 1.4 needs it declared. if (targetVersion < kSpirv15 && needMemoryModel && !hasOpExtension(w, "SPV_KHR_vulkan_memory_model")) { const auto e = encodeOpExtension("SPV_KHR_vulkan_memory_model"); @@ -669,4 +645,4 @@ bool downgradeSpirv(std::vector& w, uint32_t targetVersion) { return true; } -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_dll.h b/app/src/main/cpp/framegen/lsfg/lsfg_dll.h index 74d6ca83d..a20a6c631 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_dll.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_dll.h @@ -1,35 +1,7 @@ +// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). +// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later. + #pragma once -// ============================================================================ -// lsfg_dll - extract the Lossless Scaling frame-generation shader chain from -// the user's own Lossless.dll, and cache it on device as SPIR-V. -// -// The 25 compute shaders that make up LSFG 3.1 are NOT redistributable. They -// ship as RCDATA resources inside Lossless.dll from the user's own paid copy -// of Lossless Scaling (Steam app 993090). Every implementation - upstream -// lsfg-vk, LSFG-Android, eden, WinNative - requires the user to supply their -// own DLL and extracts the resources on device. DroidDeck does the same, and -// bundles nothing. -// -// The DLL is mmap'd READ-ONLY and parsed as data. It is never loaded as a -// library and never executed. -// -// Resource layout, measured against real installs: -// * The base chain lives at RCDATA ids 255, 256 and 280-302 (25 modules), -// and is DXBC. It has always been DXBC. -// * Lossless Scaling MAY additionally carry precompiled SPIR-V copies at -// base+49 (native fp16) and base+98 (native fp32). No build currently -// downloadable from Steam carries them - public buildId 19655272 was -// measured with zero SPIR-V anywhere in the 311 MB install - so the DXBC -// path is the normal path and the translator is not optional. The SPIR-V -// path is kept because it costs nothing and a future build may ship them. -// -// Both producers fill the same shader-id -> SPIR-V words map, so everything -// downstream of this file is source-agnostic. -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck -// by @maxjivi05. GPL-3.0-or-later. -// ============================================================================ #include #include @@ -39,44 +11,37 @@ namespace lsfg { enum class DllStatus { Ok = 0, - NotInstalled, // no such file / cannot open - UnreadableFile, // opens but cannot be mapped or is empty + NotInstalled, + UnreadableFile, NotPortableExecutable, - MissingShaders, // parses, but the base chain is not all there - TranslationFailed, // a shader failed DXBC->SPIR-V or SPIR-V adoption - CacheUnusable // cache could not be written or read back + MissingShaders, + TranslationFailed, + CacheUnusable }; -// SPIR-V version words. The translator emits 1.6; a Vulkan 1.1 device takes -// 1.4 (via VK_KHR_spirv_1_4) and a 1.2 device 1.5. One definition for the -// probe, the engine and the shader loader. constexpr uint32_t kSpirv14 = 0x00010400u; constexpr uint32_t kSpirv15 = 0x00010500u; constexpr uint32_t kSpirv16 = 0x00010600u; enum class Variant { None = 0, - SpirvFp16, // precompiled fp16 blobs at base+49 - SpirvFp32, // precompiled fp32 blobs at base+98 - DxbcTranslated // base chain, translated on device (the normal case) + SpirvFp16, + SpirvFp32, + DxbcTranslated }; -// The 25 shader ids that make up the chain. constexpr uint32_t kShaderMipmaps = 255u; constexpr uint32_t kShaderGenerate = 256u; constexpr uint32_t kShaderChainFirst= 280u; constexpr uint32_t kShaderChainLast = 302u; constexpr uint32_t kShaderCount = 25u; -// Upstream spelling for the two ids the ported chain files refer to by name. -// Kept so those files stay byte-close to their upstream form; these alias the -// constants above rather than being a second source of truth. constexpr uint32_t LSFG_SHADER_MIPMAPS = kShaderMipmaps; constexpr uint32_t LSFG_SHADER_GENERATE = kShaderGenerate; struct Module { uint32_t id = 0; - std::vector words; // SPIR-V + std::vector words; }; struct ModuleSet { @@ -87,41 +52,19 @@ struct ModuleSet { bool complete() const; }; -// The canonical shader-id list, in dispatch-independent order. const std::vector& shaderIds(); const char* statusName(DllStatus s); const char* variantName(Variant v); -// Does this file look like a usable Lossless.dll? Checks the base chain only, -// so a DLL without the SPIR-V variants (i.e. every build on Steam today) is -// reported as valid - the translator handles it. DllStatus validateDll(const std::string& dllPath); -// Parse the DLL, produce all 25 SPIR-V modules, and write them to cachePath -// (via temp file + rename, so a failed build cannot leave a half-written -// cache behind). Records which producer ran in the cache header. DllStatus buildCache(const std::string& dllPath, const std::string& cachePath, bool preferFp16); -// Load a previously built cache. DllStatus loadModules(const std::string& cachePath, ModuleSet& outSet); -// Which producer built an existing cache, without loading the modules. DllStatus cacheVariant(const std::string& cachePath, Variant& outVariant); -// Experimental Vulkan 1.1 / 1.2 compat: lower a DXBC-translated module's -// declared SPIR-V version to `targetVersion` (kSpirv14 or kSpirv15). Below 1.5 -// the Vulkan memory model is not core, so SPV_KHR_vulkan_memory_model is -// declared for it (the device side enables VK_KHR_vulkan_memory_model). The -// cache is built without a device in hand, always at the translator's native -// 1.6, so this runs at load time against whatever the device can take. A -// module already at or below the target is left untouched. Returns false, -// leaving the module untouched, for a malformed module, a target below 1.4, or -// a module declaring DemoteToHelperInvocation: that capability is core only in -// 1.6 and its extension is never enabled on the compat path, so such a module -// cannot be made legal here (DXVK emits it for pixel-shader discard only, so -// the 25 compute shaders never carry it). Whether the device then accepts the -// result is vkCreateShaderModule's / vkCreateComputePipelines' verdict. bool downgradeSpirv(std::vector& words, uint32_t targetVersion); -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.cpp index c8084119f..01f904e9d 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.cpp @@ -1,10 +1,5 @@ -// See lsfg_dxbc.h. DXBC -> SPIR-V via DXVK's vendored translator. -// -// Follows upstream lsfg-vk's src/extract/trans.cpp, including its -// encounter-order binding renumber. Ported to WinNative (lsfg_dxbc.cpp) and -// DroidDeck by @maxjivi05 from the work of Camille LaVey / the Eden Emulator -// Project (GPL-3.0-or-later); the translator itself is DXVK's, zlib licensed, -// vendored under cpp/thirdparty/dxbc. +// Ported to WinNative and DroidDeck by @maxjivi05 from Camille LaVey / the Eden Emulator Project (GPL-3.0-or-later). +// DXVK's translator is zlib licensed; vendored under cpp/thirdparty/dxbc. #include "lsfg_dxbc.h" @@ -21,12 +16,12 @@ namespace { struct BindingSlot { uint32_t bindingIndex = 0; - uint32_t bindingOffset = 0; // word offset of the Binding literal + uint32_t bindingOffset = 0; uint32_t setIndex = 0; uint32_t setOffset = 0; }; -} // namespace +} bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector& outWords) { outWords.clear(); @@ -38,9 +33,6 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector const dxvk::DxbcModuleInfo info{}; auto code = module.compile(info, "CS"); - // Collect the descriptor decorations in the order the Binding - // decorations appear. Everything before the first OpFunction is the - // decoration block, so the scan stops there. std::vector slotsByVarId; std::vector varIdsInOrder; for (auto ins : code) { @@ -60,7 +52,6 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector if (ins.opCode() == spv::OpFunction) break; } - // Renumber to a dense 0..n range, in encounter order. uint32_t next = 0; for (const uint32_t varId : varIdsInOrder) { const BindingSlot& slot = slotsByVarId[varId]; @@ -75,11 +66,9 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector std::memcpy(outWords.data(), code.data(), byteSize); return true; } catch (...) { - // DXVK's compiler signals malformed bytecode by throwing. A bad - // resource must degrade to "this DLL is unusable", never to a crash. outWords.clear(); return false; } } -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h b/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h index 3fa643728..17ba1876d 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h @@ -1,39 +1,11 @@ #pragma once -// ============================================================================ -// lsfg_dxbc - DXBC -> SPIR-V for the Lossless Scaling shader chain. -// -// Why this exists at all: the base chain inside Lossless.dll is DXBC, and has -// always been DXBC. Lossless Scaling 3.2.2's release note ("Added shaders -// intended for use by the lsfg-vk project") describes precompiled SPIR-V -// copies at RCDATA base+49 / base+98 - but no build downloadable from Steam -// carries them. Measured against the current public build (buildId 19655272, -// 2025-08-19; linux_testing byte-identical on depot 993091): the whole 311 MB -// install contains zero occurrences of the SPIR-V magic word, and the DLL's -// 202 RCDATA entries are all DXBC. -// -// So a translator is required, not optional. Upstream lsfg-vk reaches the same -// conclusion and links DXVK's `dxbc` in src/extract/trans.cpp; we vendor the -// same subset (zlib licence) under cpp/thirdparty/dxbc and follow its -// trans.cpp step for step. -// -// The binding renumber here is ENCOUNTER ORDER - the order in which Binding -// decorations appear in DXVK's output - which is what DXVK's own layout pairs -// with. The precompiled-SPIR-V path in lsfg_dll.cpp uses a set/binding sort -// instead, because those blobs were built with that convention. The two must -// not be merged. -// ============================================================================ +// DXVK's dxbc subset is zlib licensed; vendored under cpp/thirdparty/dxbc. #include #include namespace lsfg { -// Translate one DXBC compute shader to SPIR-V, renumbering descriptor bindings -// into a dense 0..n range in encounter order. Returns false (leaving outWords -// empty) on any malformed input - DXVK's compiler throws, and everything is -// caught here. -// Emits DXVK's native SPIR-V 1.6; lowering for a Vulkan 1.1/1.2 device happens -// at load time (lsfg_dll.h downgradeSpirv). bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector& outWords); -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_engine.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_engine.cpp index ba9e8a021..fe0ea3ebe 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_engine.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_engine.cpp @@ -1,8 +1,3 @@ -// See lsfg_engine.h. -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by -// @maxjivi05, where it runs on gamescope's output in the compositor. #include "lsfg_engine.h" @@ -21,9 +16,6 @@ namespace lsfg { namespace { -// The chain needs two real frames in its history before anything it produces -// is meaningful, and we require the plan to stay warm for a couple of frames -// so a single settled sample cannot start generation from garbage. constexpr uint64_t kRequiredFrames = 2; constexpr uint32_t kRecurrenceFrames = 2; constexpr uint64_t kTelemetryInterval = 120; @@ -50,8 +42,6 @@ VkImageMemoryBarrier makeTransition(VkImage image, VkAccessFlags srcAccess, return b; } -// Copy the just-composited frame into the chain's input ring. `source` is the -// composite target, which the composite render pass leaves in GENERAL. void copyPresentedFrame(VkCommandBuffer cmd, VkImage source, LsfgImage& destination, VkExtent2D extent) { const VkImageMemoryBarrier before[] = { @@ -88,7 +78,7 @@ void copyPresentedFrame(VkCommandBuffer cmd, VkImage source, LsfgImage& destinat destination.SetLayout(VK_IMAGE_LAYOUT_GENERAL); } -} // namespace +} Engine::Engine() = default; @@ -142,9 +132,6 @@ void Engine::setGuestExtent(uint32_t width, uint32_t height) { peakGuestExtent_.height = std::max(peakGuestExtent_.height, height); } -// The flow pyramid is the expensive part, so its resolution is scaled relative -// to the largest guest surface seen: a game running well below panel -// resolution does not need a panel-resolution flow field. float Engine::effectiveFlowScale(uint32_t width) const { if (width == 0 || peakGuestExtent_.width == 0) return flowScale_; const float ratio = (float)peakGuestExtent_.width / (float)width; @@ -171,8 +158,6 @@ bool Engine::prepare(uint32_t width, uint32_t height, VkFormat format) { chain_ = std::make_unique(device_, *shaders_, VkExtent2D{width, height}, format, scale); if (!chain_->Valid()) { - // A device that cannot build the chain reports unsupported once and - // stops trying, rather than thrashing allocations every frame. LSFG_LOGW("chain build failed at %ux%u; frame generation unavailable", width, height); chain_.reset(); unavailable_ = true; @@ -204,17 +189,9 @@ uint32_t Engine::plan(uint32_t capacity, uint64_t sourceFrames) { plan_ = pacer_.Plan(std::min(capacity, kMaxGenerations), sourceFrames); - // The pacer says how many frames FIT in the panel's budget. The governor - // says how many this device can actually afford right now - on a handheld - // the chain competes with the game for one GPU, so an extra generation has - // to prove it improves total output without collapsing the real frame rate. if (governorEnabled_) { const LsfgPacerStats s = pacer_.Stats(); governor_.configure((uint32_t)std::min(pacer_.MaxGenerations(), kMaxGenerations)); - // presentedRate_, NOT the pacer's loop rate. The pacer samples its loop - // once per SOURCE frame, so its "loop rate" is the guest rate by - // construction and can never show that generation added anything - the - // governor could not accept a probe on any hardware. plan_.generations = governor_.cap((uint32_t)plan_.generations, s.source_rate, presentedRate_); } @@ -223,12 +200,6 @@ uint32_t Engine::plan(uint32_t capacity, uint64_t sourceFrames) { warmStreak_ = warm_ ? warmStreak_ + 1 : 0; generating_ = warm_ && warmStreak_ >= kRecurrenceFrames && plan_.generations > 0; - // The ring is only seeded while generating, so the first frame after any - // gap - arming, or a pacer discontinuity after a hitch - would interpolate - // between this frame and whatever the other slot held when generation last - // stopped. That is a visible warp, and it happens every time the engine - // starts. Spend this frame filling the ring instead and generate from the - // next one, when both slots are genuinely consecutive. primeHistory_ = false; if (generating_ && !historyFresh()) { primeHistory_ = true; @@ -265,11 +236,6 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32 lastCount_ = count; lastGenerations_ = generations; - // Seeding the input ring is a full-resolution image copy every frame, so it - // is NOT free at 100 fps - it was costing real frame rate while producing - // nothing. Copy only when generating, on the one priming frame that - // precedes generation, or while the governor is warming up towards a - // probe - so the ring is populated by the time it is needed. const bool needHistory = generations > 0 || primeHistory_ || followEveryFrame() || (governorEnabled_ && governor_.wantsHistory()); if (needHistory) { @@ -278,13 +244,6 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32 haveCopied_ = true; } - // The SHARED chain is 24 of the 25 shaders - the whole flow pyramid - and - // only `generate` runs per generated frame. Running it while producing - // nothing spends almost the entire cost of frame generation for no frames - // at all, every frame. That is what pinned the GPU at 100% on device - // regardless of the game's own settings, and it fed straight back into the - // governor: the chain made the source rate collapse, the governor saw the - // collapse and refused to generate, and refusing did not stop the chain. if (warm_ && (generations > 0 || followEveryFrame())) chain_->DispatchShared(cmd, count); } @@ -318,4 +277,4 @@ void Engine::reset() { plan_ = {}; } -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_engine.h b/app/src/main/cpp/framegen/lsfg/lsfg_engine.h index c8a5028d0..b04726425 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_engine.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_engine.h @@ -1,25 +1,4 @@ #pragma once -// ============================================================================ -// lsfg_engine - the compositor's handle on native LSFG frame generation. -// -// Owns the shader modules, the interpolation chain and the pacer, and exposes -// the small contract the render loop needs: -// -// prepare(w, h, format) build/rebuild the chain for this size -// plan(capacity, sourceFrames) how many frames to generate this time -// process(cmd, source, w, h, gens) take frame N in, run the shared chain -// generateInto(cmd, g, i, img, view) synthesise generated frame g -// -// Ordering matters and is not obvious: interpolation produces frames that -// belong BETWEEN N-1 and N, so the generated frames are presented FIRST and -// the real frame N is held back one slot. That one output interval is the -// unavoidable cost of interpolating rather than extrapolating; no placement of -// this code changes it. -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by -// @maxjivi05, where it runs on gamescope's output in the compositor. -// ============================================================================ #include #include @@ -28,7 +7,7 @@ #include #include "lsfg_common.hpp" -#include "lsfg_dll.h" // kSpirv16 +#include "lsfg_dll.h" #include "lsfg_governor.h" #include "lsfg_pacer.hpp" @@ -37,27 +16,16 @@ namespace lsfg { class LsfgShaders; class LsfgChain; -// Hard ceiling on generated frames per source frame (2x..4x -> 1..3). constexpr uint32_t kMaxGenerations = 3; class Engine { public: - // Both the constructor and destructor are out-of-line ON PURPOSE. The - // shader set and the chain are held by unique_ptr to forward-declared - // types, and an inline constructor would make the compiler emit the - // exception-cleanup path for those members at every construction site - - // which needs the complete types there. Keeping both here confines that to - // lsfg_engine.cpp, where the types are complete. Engine(); ~Engine(); Engine(const Engine&) = delete; Engine& operator=(const Engine&) = delete; - // Build the shader modules from a cache produced by lsfg_dll::buildCache. - // Returns false if the cache does not yield all 25 modules. `spirvTarget` - // is the highest SPIR-V version the device accepts (lsfg_probe.h); the - // cached 1.6 modules are lowered to it when it is below that. bool init(VkDevice device, VkPhysicalDevice physicalDevice, const std::string& cachePath, uint32_t spirvTarget = kSpirv16); @@ -71,28 +39,18 @@ public: bool needsRebuild(uint32_t width, uint32_t height, VkFormat format) const; bool prepare(uint32_t width, uint32_t height, VkFormat format); - // How many frames to generate for this source frame. Returns 0 until the - // history slots are valid and the rates have settled, so the first frames - // after a start or a resize are never interpolated from garbage. uint32_t plan(uint32_t capacity, uint64_t sourceFrames); - // Take the just-composited frame as LSFG input and run the shared part of - // the chain (everything except `generate`). `source` must be in GENERAL. void process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32_t height, uint32_t generations); - // Synthesise generated frame `generation` into the given storage image. void generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t targetIndex, VkImage targetImage, VkImageView targetView, uint32_t width, uint32_t height); - // What the probe governor currently trusts, and the latest thermal - // reading (-1 = no signal). Surfaced for the HUD and diagnostics. uint32_t acceptedGenerations() const { return governor_.accepted(); } int thermalStatus() const { return governor_.thermalStatus(); } float sourceRate() const; - // The rate that actually reaches the panel, generated frames included. - // Supplied by the renderer, which is the only thing that counts presents. float loopRate() const { return presentedRate_; } void setPresentedRate(float fps) { presentedRate_ = fps; } @@ -102,16 +60,8 @@ public: private: float effectiveFlowScale(uint32_t width) const; - // A generated frame is a blend of the last two REAL frames, so the input - // ring - which is two images deep - only holds usable history when the - // other slot was filled by the frame immediately before this one. It is - // not, every time generation resumes after a pause: the ring is seeded - // only while generating, so the slot still carries whatever was there when - // generation last stopped. bool historyFresh() const { return haveCopied_ && lastCopiedCount_ + 1 == frameCount_; } - // With a target rate the pacer plans 0..3 generations frame by frame, so the ring and the - // shared chain have to follow every frame it keeps warm, not just the ones that generate. bool followEveryFrame() const { return warm_ && pacer_.Config().target_rate != 0; } Device device_{}; @@ -120,12 +70,6 @@ private: std::unique_ptr chain_; LsfgPacer pacer_; ProbeGovernor governor_; - // The governor is OFF by default: the device's own thermal management is - // the authority, and a probe loop that can veto the user's setting is not - // wanted here. The multiplier is then what was asked for, still clamped by - // what the panel can show. Kept behind a flag rather than deleted because - // the probe/backoff logic is the right answer for a device WITHOUT decent - // native throttling, and this is the only thing that would have to change. bool governorEnabled_ = false; LsfgPlan plan_{}; @@ -151,4 +95,4 @@ private: bool unavailable_{}; }; -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_gamma.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_gamma.cpp index 574382ed7..83d117cb3 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_gamma.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_gamma.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_gamma.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_gamma.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_gamma.hpp index d1cd38486..c3dcb3848 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_gamma.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_gamma.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_generate.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_generate.cpp index 7be96561c..176208376 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_generate.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_generate.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_generate.hpp" #include "lsfg_dll.h" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_generate.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_generate.hpp index 0612b7422..716650da4 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_generate.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_generate.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_governor.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_governor.cpp index 828b5be6b..11de0eb8a 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_governor.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_governor.cpp @@ -1,4 +1,3 @@ -// See lsfg_governor.h. #include "lsfg_governor.h" @@ -12,13 +11,10 @@ namespace lsfg { namespace { -// A probe has to run long enough to survive a single unlucky frame, and the -// baseline long enough to be a fair comparison. constexpr float kBaselineSeconds = 2.0f; constexpr float kProbeSeconds = 2.0f; constexpr uint32_t kMinSamples = 30; -// Keep the extra generation only if it actually paid off. constexpr float kOutputGainRequired = 1.15f; constexpr float kSourceFloor = 0.70f; @@ -28,20 +24,20 @@ constexpr uint32_t kBackoffSteps = 4; constexpr float kThermalPollSeconds = 2.0f; constexpr float kThermalDecaySeconds = 1.0f; -// ADEVICE_THERMAL_STATUS_* from , which is API 30. Resolved -// by dlsym so the build keeps working below that and on devices that do not -// ship the service; -1 simply means "no thermal signal", never "hot". -// Android reports a STATUS, not a temperature - NONE 0, LIGHT 1, MODERATE 2, -// SEVERE 3, CRITICAL 4, EMERGENCY 5, SHUTDOWN 6 - so there is no degree value -// here to raise. The equivalent of "let it run hotter" is to act on a higher -// status, which is what these now do. -// -// A handheld under a real 3D load sits at SEVERE routinely; blocking there -// made the governor refuse on a device that was working exactly as intended. -// It now only stops GROWING at SEVERE and only gives frames back at CRITICAL, -// leaving the device's own thermal management as the authority. -constexpr int kThermalBlock = 4; // CRITICAL: give generations back -constexpr int kThermalNoGrowth = 3; // SEVERE: keep what we have, stop probing + + + + + + + + + + + + +constexpr int kThermalBlock = 4; +constexpr int kThermalNoGrowth = 3; using PFN_AThermal_acquireManager = void* (*)(); using PFN_AThermal_getCurrentThermalStatus = int (*)(void*); @@ -72,17 +68,12 @@ float phaseSeconds(std::chrono::steady_clock::time_point a, return std::chrono::duration(b - a).count(); } -} // namespace +} void ProbeGovernor::configure(uint32_t maxGenerations) { if (maxGenerations_ == maxGenerations) return; - // Raising the ceiling must NOT throw away what we already know works - - // going 2x -> 4x used to reset the accepted level to zero and restart the - // whole baseline/probe cycle, so a user raising the multiplier got FEWER - // frames. Only clamp down to a ceiling that has actually been lowered. maxGenerations_ = maxGenerations; if (accepted_ > maxGenerations_) accepted_ = maxGenerations_; - // Re-measure against the new ceiling, but keep the accepted level. phase_ = Phase::Baseline; havePhaseStart_ = false; sourceAccum_ = loopAccum_ = 0.0f; @@ -132,19 +123,8 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate if (!havePhaseStart_) enterBaseline(now); pollThermal(now); - // Thermal override: at SEVERE and above we stop asking for more and give - // generations back, but SLOWLY. - // - // This runs once per source frame - about forty times a second - so the - // previous version's unconditional accepted_-- was not "give one back", it - // was "slam to zero in three frames", and its unconditional enterBackoff() - // reset the backoff clock every single call, so the backoff could never - // expire and the governor could never measure or recover. One decay per - // second, and no churn at all once there is nothing left to give back. thermalBlocked_ = thermal_ >= kThermalBlock; if (thermal_ >= kThermalBlock) { - // Logged, sparingly. This is a REFUSAL TO EVEN MEASURE, and it was - // silent: on device it looked identical to "measured and declined". if ((refusalLog_++ % 600u) == 0u) GOV_LOGI("thermal status %d - not probing (accepted=%u)", thermal_, accepted_); const bool decayDue = !haveThermalDecay_ @@ -183,12 +163,10 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate sourceAccum_ = loopAccum_ = 0.0f; samples_ = 0; } else { - // Also previously silent. "Baseline completed but we did not - // probe" is a decision worth seeing in a log. if ((refusalLog_++ % 600u) == 0u) GOV_LOGI("not probing: accepted=%u max=%u requested=%u thermal=%d", accepted_, maxGenerations_, requested, thermal_); - enterBaseline(now); // nothing to try; measure again later + enterBaseline(now); } } break; @@ -226,12 +204,10 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate break; } - // While probing we allow one more than is currently accepted; otherwise the - // accepted level is the ceiling, and the pacer's own answer still applies. const uint32_t allowed = phase_ == Phase::Probing ? std::min(accepted_ + 1, maxGenerations_) : accepted_; return std::min(requested, allowed); } -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_governor.h b/app/src/main/cpp/framegen/lsfg/lsfg_governor.h index 0ae9b696d..3d04f4605 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_governor.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_governor.h @@ -1,29 +1,4 @@ #pragma once -// ============================================================================ -// lsfg_governor - decides whether an extra generated frame is actually WORTH -// it on this device, right now. -// -// The ported pacer answers "how many frames fit in the panel's budget". That -// is necessary but not sufficient on a handheld: the LSFG chain competes with -// the game for the same GPU, so a naive fixed 3x on a GPU-bound title lowers -// the real frame rate by more than the generated frames add. The user sees a -// higher HUD number and a worse-looking game. -// -// So the multiplier is treated as a ceiling to earn, not a setting to obey. -// The governor starts at 0 generations, periodically PROBES one more, and -// keeps it only if the probe measurably paid off: -// -// * total output rate must improve by at least 1.15x, and -// * the real (source) frame rate must not collapse below 0.70x baseline. -// -// A failed probe reverts immediately and backs off 5s, 15s, 30s, then 60s, so -// a device that cannot afford another generation stops being asked. -// -// Thermals are the second input a phone needs and a desktop does not. -// Sustained frame generation is a thermal decision as much as a performance -// one, so the governor gives back generations as the device heats and refuses -// to probe while it is throttling. -// ============================================================================ #include #include @@ -34,22 +9,15 @@ class ProbeGovernor { public: void configure(uint32_t maxGenerations); - // Cap the pacer's answer. Called once per source frame with the measured - // rates; returns the number of generations actually permitted. uint32_t cap(uint32_t requested, float sourceRate, float loopRate); void reset(); uint32_t accepted() const { return accepted_; } bool probing() const { return phase_ == Phase::Probing; } - // Whether the chain's input ring needs to be kept fed. Seeding it is a - // full-resolution copy every frame, so it is skipped during the long idle - // stretches - a backoff, or a thermal block - and resumed during Baseline, - // which gives it a couple of seconds to fill before a probe can start. bool wantsHistory() const { return !thermalBlocked_ && phase_ != Phase::Backoff; } - // Latest thermal reading, 0 (none) .. 6 (shutdown); -1 when unavailable. int thermalStatus() const { return thermal_; } private: @@ -68,11 +36,9 @@ private: Clock::time_point phaseStart_{}; bool havePhaseStart_ = false; - // Rolling means over the current phase window. float sourceAccum_ = 0.0f, loopAccum_ = 0.0f; uint32_t samples_ = 0; - // What the accepted level achieves, measured during Baseline. float baselineSource_ = 0.0f, baselineLoop_ = 0.0f; bool haveBaseline_ = false; @@ -87,4 +53,4 @@ private: bool haveThermalPoll_ = false; }; -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_jni.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_jni.cpp index ab13096d6..32dfd552d 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_jni.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_jni.cpp @@ -1,8 +1,3 @@ -// JNI surface for native LSFG shader-cache management. -// -// Deliberately free of any renderer handle: extracting the shader chain from -// Lossless.dll and caching it is a slow, once-per-DLL operation that the app -// runs off the render thread, long before a game launches. #include "lsfg_dll.h" @@ -20,7 +15,7 @@ std::string toStdString(JNIEnv* env, jstring s) { return out; } -} // namespace +} extern "C" JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_gpu_LsfgNative_nativeValidateDll(JNIEnv* env, jclass, jstring dllPath) { diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.cpp index ef1e11318..2e42facd6 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_mipmaps.hpp" #include "lsfg_dll.h" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.hpp index d301c26ef..e863b7491 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_pacer.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_pacer.cpp index 8ddf453c2..57307ee05 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_pacer.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_pacer.cpp @@ -1,9 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_pacer.hpp" @@ -21,7 +17,6 @@ constexpr float SOURCE_SMOOTHING = 0.15f; constexpr float SOURCE_STALE_SECONDS = 0.5f; constexpr float DISCONTINUITY_SECONDS = 0.25f; constexpr float HEADROOM_EPSILON = 0.02f; -// A target is aimed just under, so rounding never puts output past the panel's refresh rate. constexpr float TARGET_MARGIN = 0.1f; constexpr float CREDIT_EPSILON = 1.0e-4f; constexpr float SOURCE_ACCUM_FLOOR = 0.01f; @@ -121,17 +116,6 @@ LsfgPlan LsfgPacer::Plan(size_t capacity, uint64_t source_frames) { if (target_rate > 0.0f) target_rate -= TARGET_MARGIN; if (target_rate == 0.0f) { - // No explicit target rate: the multiplier is the multiplier, full stop. - // The panel-headroom clamp was here briefly and was removed at the - // user's decision; this is also upstream's behaviour on this branch. - // - // What that means above the panel's refresh rate, for the record: the - // display cannot show more frames than it refreshes, so under FIFO the - // surplus presents queue and the render thread waits on acquire. Nothing - // is displayed faster, and the compositor falls behind on guest - // deliveries instead. Below the refresh rate - every source at or under - // panel/4 - this branch behaves identically with or without the clamp. - // To restore it: limit = std::min(ceiling, HeadroomLimit()). output_credit = 0.0f; limit = ceiling; return LsfgPlan{limit, true}; diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_pacer.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_pacer.hpp index fe4a43ee5..5762ee47c 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_pacer.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_pacer.hpp @@ -1,9 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_probe.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_probe.cpp index 823283419..36ed36ced 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_probe.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_probe.cpp @@ -1,4 +1,3 @@ -// See lsfg_probe.h. Capability gate for native compositor-side LSFG. #include "lsfg_probe.h" #include "../VulkanRendererContext.h" @@ -16,7 +15,7 @@ bool hasExtension(const std::vector& exts, const char* na return false; } -} // namespace +} FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd, const std::vector& deviceExtensions, @@ -33,33 +32,21 @@ FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd, const bool below12 = props.apiVersion < VK_API_VERSION_1_2; if (below12) { - // Below 1.2 there is no VkPhysicalDeviceVulkan12Features to chain, and - // the SPIR-V 1.6 modules will not load anyway. Unless the experimental - // compat path is on AND the driver offers the extensions that make the - // chain loadable as SPIR-V 1.4, don't touch the device further: - // device creation stays exactly as it is today. if (!allowVk11) return fs; fs.hasSpirv14 = hasExtension(deviceExtensions, VK_KHR_SPIRV_1_4_EXTENSION_NAME); fs.hasFloatControls = hasExtension(deviceExtensions, VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); fs.hasMemoryModelExt = hasExtension(deviceExtensions, VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME); if (!fs.hasSpirv14 || !fs.hasFloatControls || !fs.hasMemoryModelExt) { - // Report what was looked at so explain() can name the gap. fs.queried = true; return fs; } fs.extensionPath = true; fs.spirvTarget = kSpirv14; } else if (!fs.apiAtLeast13 && allowVk11) { - // 1.2: SPIR-V 1.5 is core, VulkanMemoryModel is core. Nothing to - // enable beyond the features; the modules only need their header - // lowered from 1.6 to 1.5. Same experimental gate as the 1.1 path so - // a production build keeps rejecting 1.2 exactly as before. fs.extensionPath = true; fs.spirvTarget = kSpirv15; } - // On 1.2+ the Vulkan12Features struct carries the memory model; below - // that the KHR struct does (same sType value, same layout). VkPhysicalDeviceVulkan12Features v12{}; v12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; VkPhysicalDeviceVulkanMemoryModelFeaturesKHR mm{}; @@ -87,7 +74,7 @@ FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd, std::vector extensionNames(const FeatureSupport& f) { std::vector out; if (!f.extensionPath || f.deviceApiVersion >= VK_API_VERSION_1_2) return out; - out.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); // dependency of spirv_1_4 + out.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); out.push_back(VK_KHR_SPIRV_1_4_EXTENSION_NAME); out.push_back(VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME); return out; @@ -108,10 +95,6 @@ bool probeStorageFormat(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt) { VkFormatProperties fp{}; vk.GetPhysicalDeviceFormatProperties(pd, fmt, &fp); - // The composite target is COLOR_ATTACHMENT (effect chain writes it), - // SAMPLED (next frame's LSFG input reads it), STORAGE (generate writes it) - // and both blit ends (it is copied - or, below panel resolution, blitted - - // into the swapchain image). const VkFormatFeatureFlags need = VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT @@ -163,4 +146,4 @@ void explain(Caps& caps) { } } -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_probe.h b/app/src/main/cpp/framegen/lsfg/lsfg_probe.h index 38d1d1a4f..3042b1040 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_probe.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_probe.h @@ -1,45 +1,17 @@ #pragma once -// ============================================================================ -// lsfg_probe - capability gate for native (compositor-side) LSFG frame -// generation. -// -// The Lossless Scaling chain is 25 compute shaders that DXVK's DXBC translator -// emits as SPIR-V 1.6 with OpCapability VulkanMemoryModel and -// StorageImageWriteWithoutFormat. Three consequences, all checked here: -// -// * SPIR-V 1.6 will not load on a Vulkan 1.1 device, so the DEVICE (not just -// the instance) must report 1.3+ ... OR, on the experimental Vulkan 1.1 -// compat path, offer VK_KHR_spirv_1_4 + VK_KHR_vulkan_memory_model so the -// same modules can be handed over as SPIR-V 1.4 (see downgradeSpirv in -// lsfg_dll.h). The stock Qualcomm driver on Adreno 7xx phones reports -// 1.1.128 yet carries every one of those extensions (measured on an -// Adreno 710 / HyperOS device with `cmd gpu vkjson`). -// * vulkanMemoryModel, shaderStorageImageWriteWithoutFormat and -// shaderStorageImageExtendedFormats must be ENABLED at device creation. -// Today the renderer enables no features at all, so all three are off. -// * `generate` writes into a storage image, and Android swapchain formats -// are frequently not storage-capable - so the format is probed separately -// once the swapchain has picked one. -// -// A device failing any gate reports unsupported UP FRONT, with a reason, so -// the UI can grey the engine out instead of failing later inside -// vkCreateShaderModule or vkCreateComputePipelines. -// ============================================================================ #include #include #include -#include "lsfg_dll.h" // kSpirv14/15/16 +#include "lsfg_dll.h" struct VkTable; namespace lsfg { -// Which of the three required features the physical device OFFERS. Queried -// before vkCreateDevice; what we actually enable is recorded in Caps below. struct FeatureSupport { - bool queried = false; // vkGetPhysicalDeviceFeatures2 resolved + bool queried = false; bool apiAtLeast13 = false; bool vulkanMemoryModel = false; bool vulkanMemoryModelDeviceScope= false; @@ -47,65 +19,41 @@ struct FeatureSupport { bool storageImageExtendedFormats = false; uint32_t deviceApiVersion = 0; - // Experimental Vulkan 1.1 compat: the device is below 1.3 but offers the - // extensions that make the chain loadable. When set, the renderer must - // enable those extensions at device creation (see extensionNames) and the - // modules are downgraded to `spirvTarget` at load. bool extensionPath = false; - bool hasSpirv14 = false; // VK_KHR_spirv_1_4 - bool hasFloatControls = false; // VK_KHR_shader_float_controls (spirv_1_4 depends on it) - bool hasMemoryModelExt = false; // VK_KHR_vulkan_memory_model + bool hasSpirv14 = false; + bool hasFloatControls = false; + bool hasMemoryModelExt = false; uint32_t spirvTarget = kSpirv16; - // Every hard device-level gate passes (format is checked separately). bool deviceGatesPass() const { return queried && (apiAtLeast13 || extensionPath) && vulkanMemoryModel && storageImageWriteWithoutFormat && storageImageExtendedFormats; } }; -// What was actually enabled + the running verdict. Owned by the renderer. struct Caps { FeatureSupport features; - bool featuresEnabled = false; // the chain was passed to vkCreateDevice + bool featuresEnabled = false; bool storageOnSwapchainFormat = false; - // The swapchain format blits with VK_FILTER_LINEAR (needs - // SAMPLED_IMAGE_FILTER_LINEAR on the source). Not a gate: without it the - // capture-resolution upscale falls back to NEAREST. bool linearBlitOnSwapchainFormat = false; VkFormat probedFormat = VK_FORMAT_UNDEFINED; char reason[160] = "not probed"; - // The single question the UI and the render path ask. bool supported() const { return featuresEnabled && features.deviceGatesPass() && storageOnSwapchainFormat; } }; -// Ask the physical device which of the required features it offers. -// Safe on any driver: if vkGetPhysicalDeviceFeatures2 cannot be resolved, or -// the device reports below Vulkan 1.2 and `allowVk11` is false, nothing is -// chained and `queried` is left false - the caller then creates the device -// exactly as it always has. With `allowVk11`, a 1.1 device is accepted when -// `deviceExtensions` lists VK_KHR_spirv_1_4, VK_KHR_shader_float_controls and -// VK_KHR_vulkan_memory_model, and the memory-model features are queried -// through the KHR struct instead of VkPhysicalDeviceVulkan12Features. FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd, const std::vector& deviceExtensions, bool allowVk11); -// The device extensions the extension path needs enabled, in the order they -// should be pushed onto VkDeviceCreateInfo. Empty unless extensionPath. std::vector extensionNames(const FeatureSupport& f); -// Probe VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT on the live swapchain format. -// `generate` writes into an image of this format via a compute dispatch. bool probeStorageFormat(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt); -// Whether the format may be the source of a VK_FILTER_LINEAR blit. bool probeLinearBlit(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt); -// Fill caps.reason with the FIRST gate that failed (or "supported"). void explain(Caps& caps); -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_shaders.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_shaders.cpp index d3266d173..05562d390 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_shaders.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_shaders.cpp @@ -1,9 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_shaders.hpp" #include "lsfg_common.hpp" @@ -27,12 +23,6 @@ LsfgShaders::LsfgShaders(const Device& device_, const std::string& cache_path, return; } - // Vulkan 1.1/1.2 compat: the cache is always built at the translator's - // native SPIR-V 1.6 (no device exists at import time), so lower each module - // to what THIS device accepts. A no-op on 1.3+ devices. Only the - // DXBC-translated chain is lowered: downgradeSpirv knows the capabilities - // DXVK emits, not whatever a precompiled vendor SPIR-V variant may carry, - // and relabelling one of those could hand the driver invalid SPIR-V. uint32_t lowered = 0; for (Module& module : set.modules) { if (module.words.size() > 1 && module.words[1] > spirv_target) { diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_shaders.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_shaders.hpp index d91b00c18..0647564a9 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_shaders.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_shaders.hpp @@ -1,9 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once @@ -20,8 +16,6 @@ class Device; class LsfgShaders { public: LsfgShaders() = default; - // `spirv_target` is the highest SPIR-V version the device accepts (see - // lsfg_probe.h); cached modules above it are lowered before creation. LsfgShaders(const Device& device, const std::string& cache_path, uint32_t spirv_target = kSpirv16); ~LsfgShaders(); diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_vkd.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_vkd.cpp index 1e7877e92..b89807077 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_vkd.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_vkd.cpp @@ -1,4 +1,3 @@ -// See lsfg_vkd.h. Fills the chain's dispatch from the renderer's own table. #include "lsfg_vkd.h" #include "../VulkanRendererContext.h" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_vkd.h b/app/src/main/cpp/framegen/lsfg/lsfg_vkd.h index c86d2224f..3e0ce0c0e 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_vkd.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_vkd.h @@ -1,24 +1,4 @@ #pragma once -// ============================================================================ -// lsfg_vkd - the Vulkan entry points the LSFG chain calls, as a single global. -// -// Two dispatch styles meet here. DroidDeck's compositor resolves Vulkan -// through a per-context VkTable (`vk_`), because an adrenotools-loaded driver -// lives in an isolated linker namespace and shares no global symbols with the -// system loader. The LSFG chain, ported from Eden by way of WinNative, calls -// through a file-scope `vkd`. Rather than rewrite ~2,300 lines of proven chain -// code to thread a table through every call, this shim exposes exactly the 34 -// entry points the chain uses and is filled from the renderer's own table when -// the chain is created. -// -// That keeps the ported files close to their upstream form, which matters: -// they are the part of this feature that is hardest to review by eye and -// easiest to break with a well-meaning edit. -// -// Single-renderer assumption: there is one Vulkan compositor per process, so -// one global table is correct. lsfgVkdInit() is idempotent and simply -// overwrites; lsfgVkdReady() reports whether every pointer resolved. -// ============================================================================ #include @@ -63,8 +43,5 @@ struct LsfgVkDispatch { extern LsfgVkDispatch vkd; -// Fill `vkd` from the renderer's table. Returns false (and leaves the chain -// unusable) if any entry point failed to resolve - better to report the engine -// unsupported than to call through a null pointer mid-frame. bool lsfgVkdInit(const VkTable& table); bool lsfgVkdReady(); diff --git a/app/src/main/cpp/framegen/winfg/config.hpp b/app/src/main/cpp/framegen/winfg/config.hpp index d8786697c..a3bf09a5e 100644 --- a/app/src/main/cpp/framegen/winfg/config.hpp +++ b/app/src/main/cpp/framegen/winfg/config.hpp @@ -1,6 +1,3 @@ -// win-fg - runtime config: enable gate, model, multiplier, and the synthesis -// tuning parameters. Sourced from environment variables and an optional -// conf.toml (a config file, when present, wins over the env defaults). #pragma once #include #include @@ -12,143 +9,110 @@ namespace winfg { struct Config { - bool enabled = false; // gate; loader also honours the manifest enable var - // GRANULAR PRESENT-PATH DEBUG TRACE. Default OFF. When ON, winfg_QueuePresentKHR - // emits a per-frame, step-by-step trail (logcat tag "win-fg") so a freeze/ - // crash-on-enable leaves an obvious LAST line at the exact stage that hung or - // faulted (acquire-spare, compute-submit, present-generated/real, fence wait, - // fallback). Zero behaviour change and effectively zero cost when off - one - // predicted-not-taken bool test per step, no logcat writes. - // WIN_FG_DEBUG=1|0 / conf.toml debug=on|off. + bool enabled = false; + + + + + + + bool debug = false; - // ── EVEN-CADENCE FRAME PACING ───────────────────────────────────────────── - // Default ON. The 2x insert path presents the GENERATED frame then the REAL - // frame back-to-back in one vkQueuePresentKHR hook call, so on Mailbox/ - // Immediate both land in ~one instant followed by a full frame-interval gap - // until the next pair - uneven in time → judder ("slows down to catch up"). - // With pacing on, the layer measures the real-frame interval (EMA of a - // monotonic clock) and holds the REAL present to its scheduled beat so the - // generated frame lands near the temporal MIDPOINT between consecutive real - // frames (even gen,real,gen,real cadence). Pure bounded CPU sleep - no GPU - // wait added; if a frame is already behind schedule the wait is SKIPPED - // (present immediately) so worst case is one un-paced pair, never a hitch. - // FIFO self-paces so the waits collapse to ~0 there. Off ⇒ byte-identical - // back-to-back behaviour. WIN_FG_PACING=on|off / conf.toml pacing=on|off. - // ISOLATION BUILD: default flipped to OFF to A/B the AYANEO-750 post-setting-change - // flicker regression against the pacing feature (everything else byte-identical). bool pacing = false; - int model = 4; // 3 = symmetric flow, 4 = bidir + occlusion gate - int multiplier = 2; // generated presents per real present + 1 - // EXTRA SWAPCHAIN IMAGE HEADROOM (device-freeze fix). On top of the +1 spare the - // insert path needs, request this many ADDITIONAL images at swapchain creation: - // requested minImageCount = app_min + 1 (spare) + extra_images, - // clamped to surfaceCaps.maxImageCount (0 = unlimited -> no clamp), never below - // app_min+1. The extra headroom stops the guest's NEXT vkAcquireNextImageKHR from - // starving while win-fg holds a spare + a real frame in flight to the AHB-backed - // host compositor. Some SoCs (Adreno 840 / Turnip) recycle displayed AHBs too - // slowly to tolerate the extra present with only the +1 spare and freeze on the - // first inserted frame; the AYANEO (Adreno 750) recycles fast enough to tolerate - // it. Default 2 (a few MB VRAM), applied on all devices. On a device that already - // works this only grows the pool - no FPS/quality change; the pool-headroom guard - // in QueuePresentKHR stays a no-op when headroom is adequate. - // WIN_FG_EXTRA_IMAGES / conf.toml extra_images. + int model = 4; + int multiplier = 2; + + + + + + + + + + + + + int extraImages = 2; - // ── FLOW-RESOLUTION PERFORMANCE PRESET ──────────────────────────────────── - // Trades base-FPS-drop against optical-flow quality by selecting the FINEST - // pyramid level the dense flow is solved at (FrameGen::kFlowFinest): - // 0 = Quality -> kFlowFinest = 1 (~1/2-res flow; best motion, biggest FG cost) - // 1 = Balanced -> kFlowFinest = 2 (~1/4-res; TODAY's behaviour) [DEFAULT] - // 2 = Performance -> kFlowFinest = 3 (~1/8-res; cheapest -> smallest base-FPS drop) - // Out-of-range clamps to Balanced (1), so an unset/older conf.toml is byte- - // identical to today. Changing this LIVE rebuilds the affected per-size flow - // images (like a resize) inside the layer and resets the flow predictor, so a - // preset change is fully hot - no FG toggle needed. See FrameGen::configure / - // FrameGen::flowFinestForPreset. WIN_FG_PERF_PRESET / conf.toml perf_preset. - int perfPreset = 1; // 0 quality, 1 balanced (default), 2 performance - float flowScale = 1.0f; // scales solved flow magnitude - // C1 GLOBAL-MOTION PRE-WARP. Estimate the per-frame camera affine (LK) and - // remove it before the dense SAD flow search so the search only sees coherent - // object-only residual motion (kills fast-pan "melt"). 0 = auto (engage only - // when the LK solve is stable), 1 = on (force-engage once a solve exists), - // 2 = off (never pre-warp; identity affine → pipeline == pre-C1 exactly). - // WIN_FG_GM=auto|on|off / conf.toml global_motion=auto|on|off. - int gmMode = 0; // 0 auto, 1 on, 2 off - // C2 FLOW REGULARIZATION (TV-L1 smoothness prior). After C1 removes the camera - // motion, the dense flow (flowLvl_ at kFlowFinest, 1/4-res) is the OBJECT-only - // residual; it still carries spurious per-block vectors. C2 runs N semi-implicit - // TV-L1 denoising iterations on that field - an L1 data term + edge-aware Total- - // Variation regularizer - to kill incoherent vectors while KEEPING true motion - // discontinuities (object edges) sharp. Solved in-place; expand's median + C1 - // add-back consume the cleaned field unchanged. 0 iterations ⇒ byte-identical to - // pre-C2. See shaders/of3_flowreg.comp. - // WIN_FG_FLOWREG=auto|on|off / conf.toml flow_reg=auto|on|off. - int frMode = 0; // 0 auto (engaged), 1 on (engaged), 2 off - int frIters = 4; // TV-L1 iterations at kFlowFinest (0 ⇒ off) - float frLambda = 2.0f; // L1 data-fidelity weight (higher ⇒ trust SAD flow) - float frDt = 0.25f; // semi-implicit smoothing step (higher ⇒ smoother) - float frEdge = 8.0f; // luma-gradient edge sensitivity for the TV weight g - float frEps = 0.05f; // Charbonnier epsilon (px) for the TV diffusivity - // synthesis (wfg_synth) tuning - float beta = 8.0f; // softmax sharpness on importance Z - float lambda = 0.6f; // FB-consistency vs photometric weight - // Tightened 2026-08-17 to prioritise "no ghost" over max sharpness - the - // remaining single-frame ghosts came from pixels that the previous defaults - // still trusted with warp when they should have cross-faded. Raising both - // pushes borderline pixels toward the safe cross-fade path (softer, but no - // smear). Prewarp will lift the ceiling further; this is the "safety net" - // knob change until then. - // Walked back from (0.10, 9.0) tighten after user reported softening. The - // real fix for the residual ghosts is the 3a-fallback path change in - // layer.cpp (present real curr on fallback instead of blitting synth over); - // the gate can stay closer to the original values now. - float epsilon = 0.07f; // disocclusion floor (0.05 -> 0.10 -> 0.07) - float photoScale = 7.5f; // photometric residual scale into Z (6.0 -> 9.0 -> 7.5) - // HUD exclusion rect in pixel coords (x0,y0,x1,y1). Fragments inside are - // passed through as the real current frame (no warp, no synth) so text / - // overlays don't ghost. Disabled when x0 >= x1 or y0 >= y1 (the default). - // Sourced from WIN_FG_HUD_RECT="x0,y0,x1,y1" env var or conf.toml - // (hudRect="x0,y0,x1,y1"). Pattern is Isygold's Vegas DXVK framegen - - // host knows where the HUD is, tell the shader to skip it. + + + + + + + + + + + + int perfPreset = 1; + float flowScale = 1.0f; + + + + + + + int gmMode = 0; + + + + + + + + + + int frMode = 0; + int frIters = 4; + float frLambda = 2.0f; + float frDt = 0.25f; + float frEdge = 8.0f; + float frEps = 0.05f; + + float beta = 8.0f; + float lambda = 0.6f; + + + + + + + + + + + float epsilon = 0.07f; + float photoScale = 7.5f; + + + + + + float hudX0 = 0.0f; float hudY0 = 0.0f; float hudX1 = 0.0f; float hudY1 = 0.0f; - // ── TRAINING-DATA CAPTURE MODE (Route-B VFI dataset collection) ─────────── - // A dev-only gate. When OFF (default) the present path is byte-identical to a - // build without this feature - a single bool test and nothing else. When ON, - // every real present is downscaled on the GPU, read back async, and written to - // disk as lossless QOI (motion-rich 256² triplet patches by default, or full - // downscaled frames) plus a JSONL manifest, for offline (i-1,i+1)->i training. - // NEVER captures win-fg's generated frames - capture reads the real swapchain - // image at the TOP of QueuePresentKHR, before any interpolation. - // WIN_FG_CAPTURE=on|off / conf.toml capture=on|off. bool capture = false; - // Downscale target box (aspect-preserving, never upscales). WIN_FG_CAPTURE_W/H, - // conf capture_width/capture_height. int captureW = 1280; int captureH = 720; - // 0 = patch (self-contained aligned triplets, DEFAULT), 1 = frame (full - // downscaled frames in order). WIN_FG_CAPTURE_MODE=patch|frame. int capMode = 0; - int capPatches = 3; // patches per triplet in patch mode (WIN_FG_CAPTURE_PATCHES) - int capPatchSize = 256; // patch edge in downscaled px (WIN_FG_CAPTURE_PATCH) - // Skip a unit whose inter-frame motion (mean per-pixel luma abs-diff, 0..255) - // is below this. WIN_FG_CAPTURE_MOTION / conf capture_motion. + int capPatches = 3; + int capPatchSize = 256; + + float capMotion = 2.0f; - // Output dir; empty => $HOME/.cache/winfg-capture. WIN_FG_CAPTURE_DIR / - // conf capture_dir. A per-run session subdir is created under it. std::string capDir; - // Rolling-shard size cap (MiB). Output is a small number of large packed - // .wfgcap containers, not thousands of loose files; a new shard opens when the - // current one passes this. WIN_FG_CAPTURE_SHARD_MB / conf capture_shard_mb. int capShardMB = 1024; void sanitize() { if (model < 3) model = 3; if (model > 4) model = 4; if (multiplier < 2) multiplier = 2; if (multiplier > 4) multiplier = 4; if (extraImages < 0) extraImages = 0; if (extraImages > 8) extraImages = 8; - if (perfPreset < 0 || perfPreset > 2) perfPreset = 1; // clamp out-of-range -> Balanced + if (perfPreset < 0 || perfPreset > 2) perfPreset = 1; if (gmMode < 0) gmMode = 0; if (gmMode > 2) gmMode = 2; if (frMode < 0) frMode = 0; if (frMode > 2) frMode = 2; if (frIters < 0) frIters = 0; if (frIters > 16) frIters = 16; @@ -158,7 +122,6 @@ struct Config { if (frEps < 1e-3f) frEps = 1e-3f; if (flowScale < 0.05f) flowScale = 0.05f; if (flowScale > 4.0f) flowScale = 4.0f; if (beta < 0.0f) beta = 0.0f; if (lambda < 0.0f) lambda = 0.0f; - // capture knobs if (captureW < 16) captureW = 16; if (captureW > 7680) captureW = 7680; if (captureH < 16) captureH = 16; if (captureH > 4320) captureH = 4320; if (capMode < 0) capMode = 0; if (capMode > 1) capMode = 1; @@ -176,7 +139,6 @@ static inline int envi(const char* k, int d) { const char* v = std::getenv(k); return v ? std::atoi(v) : d; } -// "auto"/"on"/"off" (also true/false/on/off numerics) -> {0 auto, 1 on, 2 off}. static inline int parse_tristate(const std::string& v, int dflt) { if (v.empty()) return dflt; if (v == "auto") return 0; @@ -185,19 +147,16 @@ static inline int parse_tristate(const std::string& v, int dflt) { return dflt; } -// "on"/"off"/"1"/"true"/... -> bool. static inline bool parse_bool(const std::string& v, bool dflt) { int t = parse_tristate(v, dflt ? 1 : 2); return t == 1; } -// "patch"|"frame" -> {0 patch, 1 frame}. static inline int parse_capmode(const std::string& v, int dflt) { if (v == "patch" || v == "patches" || v == "0") return 0; if (v == "frame" || v == "frames" || v == "1") return 1; return dflt; } -// key=value TOML-lite reader (flat keys, '#' comments) - enough for our knobs. static inline void apply_toml(Config& c, const std::string& path) { std::ifstream f(path); if (!f) return; @@ -229,7 +188,6 @@ static inline void apply_toml(Config& c, const std::string& path) { else if (k == "lambda") c.lambda = std::strtof(v.c_str(), nullptr); else if (k == "epsilon") c.epsilon = std::strtof(v.c_str(), nullptr); else if (k == "photoScale") c.photoScale = std::strtof(v.c_str(), nullptr); - // ── capture-mode knobs ──────────────────────────────────────────────── else if (k == "capture") c.capture = parse_bool(v, c.capture); else if (k == "capture_dir") c.capDir = v; else if (k == "capture_width") c.captureW = std::atoi(v.c_str()); @@ -240,7 +198,6 @@ static inline void apply_toml(Config& c, const std::string& path) { else if (k == "capture_motion") c.capMotion = std::strtof(v.c_str(), nullptr); else if (k == "capture_shard_mb") c.capShardMB = std::atoi(v.c_str()); else if (k == "hudRect") { - // "x0,y0,x1,y1" in pixel coords float r[4] = {0,0,0,0}; int n = 0; size_t start = 0; std::string s = v; for (; n < 4; ++n) { @@ -255,22 +212,18 @@ static inline void apply_toml(Config& c, const std::string& path) { } } -// Path of the conf.toml the app writes (used for hot-reload mtime checks). static inline std::string conf_path() { if (const char* p = std::getenv("WIN_FG_CONF")) return p; const char* home = std::getenv("HOME"); return home ? std::string(home) + "/.config/win-fg/conf.toml" : std::string(); } -// Resolve the capture output root: explicit override, else $HOME/.cache/winfg-capture, -// else a /tmp fallback so a HOME-less prefix still writes somewhere pullable. static inline std::string capture_root(const Config& c) { if (!c.capDir.empty()) return c.capDir; if (const char* home = std::getenv("HOME")) return std::string(home) + "/.cache/winfg-capture"; return std::string("/tmp/winfg-capture"); } -// env defaults first, then conf.toml overrides (file wins when present). static inline Config load_config() { Config c; c.enabled = envi("WIN_FG_ENABLE", 0) != 0; @@ -292,7 +245,6 @@ static inline Config load_config() { c.lambda = envf("WIN_FG_LAMBDA", c.lambda); c.epsilon = envf("WIN_FG_EPSILON", c.epsilon); c.photoScale = envf("WIN_FG_PHOTOSCALE", c.photoScale); - // ── capture-mode env (conf.toml still wins below) ───────────────────────── if (const char* cc = std::getenv("WIN_FG_CAPTURE")) c.capture = parse_bool(cc, c.capture); c.captureW = envi("WIN_FG_CAPTURE_W", c.captureW); c.captureH = envi("WIN_FG_CAPTURE_H", c.captureH); @@ -303,7 +255,6 @@ static inline Config load_config() { c.capShardMB = envi("WIN_FG_CAPTURE_SHARD_MB", c.capShardMB); if (const char* cd = std::getenv("WIN_FG_CAPTURE_DIR")) c.capDir = cd; if (const char* h = std::getenv("WIN_FG_HUD_RECT")) { - // "x0,y0,x1,y1" - pixel coords; disabled when x0>=x1 or y0>=y1 float r[4] = {0,0,0,0}; int n = 0; std::string s = h; size_t start = 0; for (; n < 4; ++n) { @@ -321,37 +272,24 @@ static inline Config load_config() { return c; } -// UBO layout - MUST match wfg_synth.comp binding 0. -// The vec4 hudRect is 16-byte aligned in std140, sits at offset 32 (after the -// 8 leading floats which naturally occupy 32B). Disabled when hudX0>=hudX1. struct SynthUBO { float flowScale, alpha, beta, lambda, epsilon, photoScale, pad0, pad1; float hudX0, hudY0, hudX1, hudY1; }; -// UBO layout - MUST match of3_flow / of3_expand_m4 binding 0 (Model3UBO). struct FlowUBO { float flowScale; uint32_t level; float occlLo; float occlHi; }; -// C1 global-motion UBO - MUST match binding 1 in of3_gm_reduce / of3_gm_prewarp / -// of3_expand(_m4). std140: four vec4 = 64 bytes. The affine is in centered-UV -// space: P = [[p1,p3],[p2,p4]], translation (tx,ty); warp uvSrc = uv + P*(uv-0.5) -// + t maps a prev(template) coord to the curr(image) coord it samples (prev→curr). -// lin[] = running LK estimate (linearization point fed to of3_gm_reduce) -// app[] = APPLIED affine (of3_gm_prewarp + expand add-back); identity when -// disengaged so the pipeline is byte-identical to pre-C1. struct GMUBO { - float lin[4]; // p1,p2,p3,p4 - float linT[4]; // tx,ty, engaged, pad - float app[4]; // p1,p2,p3,p4 - float appT[4]; // tx,ty, pad, pad + float lin[4]; + float linT[4]; + float app[4]; + float appT[4]; }; -// C2 flow-regularization UBO - MUST match binding 0 (vec4 params) in -// of3_flowreg.comp. std140: one vec4 = 16 bytes. See Config::fr* fields. struct FlowRegUBO { - float dt; // params.x - semi-implicit smoothing step - float lambda; // params.y - L1 data-fidelity weight (θ = dt·lambda) - float edgeAlpha; // params.z - luma-gradient edge sensitivity for g - float epsTV; // params.w - Charbonnier epsilon (px) for the TV diffusivity + float dt; + float lambda; + float edgeAlpha; + float epsTV; }; -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/embedded_shaders.hpp b/app/src/main/cpp/framegen/winfg/embedded_shaders.hpp index 934d07a61..20dd4477c 100644 --- a/app/src/main/cpp/framegen/winfg/embedded_shaders.hpp +++ b/app/src/main/cpp/framegen/winfg/embedded_shaders.hpp @@ -1,5 +1,3 @@ -// GENERATED by tools/embed_spv.py - do not edit by hand. -// win-fg embedded SPIR-V: our optical-flow front end + synthesis only. #pragma once #include #include @@ -41,4 +39,4 @@ static inline Blob blob(Shader s) { default: return { nullptr, 0 }; } } -} } // namespace winfg::embedded +} } diff --git a/app/src/main/cpp/framegen/winfg/framegen.cpp b/app/src/main/cpp/framegen/winfg/framegen.cpp index 2848937d5..4ddd12021 100644 --- a/app/src/main/cpp/framegen/winfg/framegen.cpp +++ b/app/src/main/cpp/framegen/winfg/framegen.cpp @@ -1,4 +1,3 @@ -// win-fg - compute frame-generation engine implementation. #include "framegen.hpp" #include "embedded_shaders.hpp" #include "log.hpp" @@ -49,10 +48,6 @@ void FrameGen::destroyImage(Img& i) { i = Img{}; } -// C1: allocate the per-slot host-visible SSBOs the LK reduce writes partials into. -// Host-visible + coherent so the CPU can read them back after the owning frame's -// fence signals (no staging copy needed; the buffers are tiny - kGmThreads*kGmStride -// floats each, ~64 KB). Persistently mapped. bool FrameGen::makeGmBuffers() { const VkDeviceSize sz = (VkDeviceSize)kGmThreads * kGmStride * sizeof(float); const VkMemoryPropertyFlags want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; @@ -75,8 +70,6 @@ bool FrameGen::makeGmBuffers() { bool FrameGen::makePipe(Pipe& p, int shaderId, const std::vector&) { - // binding layout is derived per shader below in init(); here we only build - // the module + a generic layout supplied by caller via setLayout already set. embedded::Blob b = embedded::blob((embedded::Shader)shaderId); VkShaderModuleCreateInfo mi{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; mi.codeSize = b.words * 4; mi.pCode = b.code; @@ -101,7 +94,6 @@ void FrameGen::destroyPipe(Pipe& p) { p = Pipe{}; } -// Build a descriptor-set layout from an explicit (binding, type) list. static bool buildSetLayout(const DeviceDispatch* dd, VkDevice dev, const std::vector>& binds, VkDescriptorSetLayout& out) { @@ -123,12 +115,11 @@ bool FrameGen::init(const DeviceDispatch* dd, const InstanceDispatch* id, si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; VKOK(dd_->CreateSampler(device_, &si, nullptr, &linear_)); - // descriptor pool sized generously for our per-frame sets. VkDescriptorPoolSize sizes[] = { {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 256}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 256}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 64}, - {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 16}, // C1 LK-reduce SSBOs + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 16}, }; VkDescriptorPoolCreateInfo pci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; pci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; @@ -141,19 +132,15 @@ bool FrameGen::init(const DeviceDispatch* dd, const InstanceDispatch* id, const auto U = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; const auto SB = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - // per-shader binding sets (match the GLSL layout(binding=...) declarations) if (!buildSetLayout(dd_, dev, {B{32,S},B{48,W}}, pLuma_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{32,S},B{48,W}}, pDown_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{48,W}}, pFlow_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{48,W}}, pFlowM4_.setLayout)) return false; - // C1: expand sets gain a second UBO (binding 1 = GMUBO) for the global-motion add-back. if (!buildSetLayout(dd_, dev, {B{0,U},B{1,U},B{32,S},B{48,W},B{49,W}}, pExpand_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{0,U},B{1,U},B{32,S},B{48,W},B{49,W}}, pExpandM4_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{35,S},B{48,W}}, pSynth_.setLayout)) return false; - // C1: LK-reduce (UBO, SSBO, prev, curr) and curr-luma stabilization prewarp (UBO, src, dst). if (!buildSetLayout(dd_, dev, {B{1,U},B{16,SB},B{32,S},B{33,S}}, pGmReduce_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{1,U},B{32,S},B{48,W}}, pGmPrewarp_.setLayout)) return false; - // C2: flow-reg (UBO, u_in, f0_obs, luma, out). if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{48,W}}, pFlowReg_.setLayout)) return false; std::vector unused; @@ -177,23 +164,19 @@ static VkExtent2D levelExtent(VkExtent2D e, int l) { return { e.width >> l ? e.width >> l : 1u, e.height >> l ? e.height >> l : 1u }; } -bool FrameGen::onResize(VkExtent2D extent, VkFormat /*colorFormat*/, bool force) { - // force skips the same-extent early-out so a perf_preset change (which alters - // flowFinest_ and therefore the per-size flow-image resolutions) rebuilds them. +bool FrameGen::onResize(VkExtent2D extent, VkFormat , bool force) { + + if (!force && ready_ && extent.width == extent_.width && extent.height == extent_.height) return true; - // tear down previous size for (auto& i : pyrA_) destroyImage(i); pyrA_.clear(); for (auto& i : pyrB_) destroyImage(i); pyrB_.clear(); for (auto& i : pyrAs_) destroyImage(i); pyrAs_.clear(); for (auto& i : flowLvl_) destroyImage(i); flowLvl_.clear(); destroyImage(flowExpA_); destroyImage(flowExpB_); - destroyImage(flowRegA_); destroyImage(flowRegB_); // C2 + destroyImage(flowRegA_); destroyImage(flowRegB_); extent_ = extent; ready_ = false; const VkImageUsageFlags lumaUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT; - // flowLvl_ is cleared every frame (per-frame reset) and C2 copies the cleaned - // finest level back into it - both need TRANSFER_DST. The C2 ping-pong images - // are the copy SOURCE (TRANSFER_SRC). const VkImageUsageFlags flowUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; const VkImageUsageFlags regUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; pyrA_.resize(kLevels); pyrB_.resize(kLevels); flowLvl_.resize(kLevels); @@ -203,13 +186,11 @@ bool FrameGen::onResize(VkExtent2D extent, VkFormat /*colorFormat*/, bool force) if (!makeImage(pyrB_[l], e, VK_FORMAT_R32_SFLOAT, lumaUsage)) return false; if (!makeImage(flowLvl_[l], e, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false; } - // C1: stabilized curr luma - only the levels the dense flow reads (finest..coarsest). pyrAs_.resize(kLevels); for (int l = flowFinest_; l < kLevels; ++l) if (!makeImage(pyrAs_[l], levelExtent(extent, l), VK_FORMAT_R32_SFLOAT, lumaUsage)) return false; if (!makeImage(flowExpA_, extent, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false; if (!makeImage(flowExpB_, extent, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false; - // C2: TV-L1 ping-pong scratch at the finest solved flow level (perf_preset-driven res). if (!makeImage(flowRegA_, levelExtent(extent, flowFinest_), VK_FORMAT_R16G16B16A16_SFLOAT, regUsage)) return false; if (!makeImage(flowRegB_, levelExtent(extent, flowFinest_), VK_FORMAT_R16G16B16A16_SFLOAT, regUsage)) return false; ready_ = true; @@ -229,13 +210,6 @@ void FrameGen::barrier(VkCommandBuffer cmd, VkImage img, VkImageLayout from, VkI VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b); } -// NOTE: descriptor-set allocation/update per record() and the exact per-level -// flow wiring are implemented in record_impl.inc for readability; the graph is: -// luma(curr)->pyrA[0], luma(prev)->pyrB[0] -// downsample pyrA/pyrB to kLevels -// coarse->fine flow (per level) into flowLvl -// expand flowLvl[0] -> flowExpA/flowExpB -// synth(prev,curr,flowExpA,flowExpB, alpha) -> out void FrameGen::destroy() { if (!device_) return; @@ -245,11 +219,11 @@ void FrameGen::destroy() { for (auto& i : pyrAs_) destroyImage(i); pyrAs_.clear(); for (auto& i : flowLvl_) destroyImage(i); flowLvl_.clear(); destroyImage(flowExpA_); destroyImage(flowExpB_); - destroyImage(flowRegA_); destroyImage(flowRegB_); // C2 + destroyImage(flowRegA_); destroyImage(flowRegB_); destroyPipe(pLuma_); destroyPipe(pDown_); destroyPipe(pFlow_); destroyPipe(pFlowM4_); destroyPipe(pExpand_); destroyPipe(pExpandM4_); destroyPipe(pSynth_); destroyPipe(pGmReduce_); destroyPipe(pGmPrewarp_); - destroyPipe(pFlowReg_); // C2 + destroyPipe(pFlowReg_); for (int s = 0; s < kGmSlots; ++s) { if (gmSsboMem_[s]) dd_->UnmapMemory(device_, gmSsboMem_[s]); if (gmSsbo_[s]) dd_->DestroyBuffer(device_, gmSsbo_[s], nullptr); @@ -262,8 +236,6 @@ void FrameGen::destroy() { descPool_ = VK_NULL_HANDLE; linear_ = VK_NULL_HANDLE; device_ = VK_NULL_HANDLE; ready_ = false; } -} // namespace winfg +} -// record() + its scratch helpers reopen namespace winfg themselves, so include -// this OUTSIDE the namespace block above (keeps FrameGen:: members attached). #include "record_impl.inc" diff --git a/app/src/main/cpp/framegen/winfg/framegen.hpp b/app/src/main/cpp/framegen/winfg/framegen.hpp index 1980f1104..8b32fd0c0 100644 --- a/app/src/main/cpp/framegen/winfg/framegen.hpp +++ b/app/src/main/cpp/framegen/winfg/framegen.hpp @@ -1,8 +1,3 @@ -// win-fg - compute frame-generation engine. -// Owns the compute pipelines (our of3_* optical flow + wfg_synth) and the -// per-resolution intermediate images, and records the flow+synth graph that -// turns (prev, curr) into an interpolated frame. Written for win-fg; no code -// from bionic-fg / lsfg / GameScope. #pragma once #include "vk_dispatch.hpp" #include "config.hpp" @@ -14,70 +9,53 @@ class FrameGen { public: bool init(const DeviceDispatch* dd, const InstanceDispatch* id, VkPhysicalDevice phys, VkDevice dev, uint32_t queueFamily, VkQueue queue); - // Applies a new config. Recomputes the active finest flow level from - // cfg_.perfPreset; if it changed while resources are live, safely rebuilds the - // affected per-size flow images (like a resize) and resets the flow predictor, - // so a perf_preset change is FULLY LIVE - no FG toggle needed. Defined in - // record_impl.inc (needs the scratch-state teardown). See flowFinestForPreset. void configure(const Config& c); - // (Re)build per-resolution resources for a swapchain of the given size/format. - // force=true rebuilds even when the extent is unchanged (used by configure() - // when a perf_preset change alters the finest solved flow level). bool onResize(VkExtent2D extent, VkFormat colorFormat, bool force = false); - // Record the whole flow+synth graph into cmd. prevView/currView are SAMPLED - // views of the two real frames; outView is a STORAGE view of the target - // image the generated frame is written into. alpha in (0,1). - // gmSlot selects which C1 global-motion SSBO to reduce into / read back; the - // caller MUST pass the ring-slot index of the FrameCtx it just fence-waited - // (that guarantees this slot's PREVIOUS reduce has completed and is safe to - // read on the host - see the decoupled-LK note in record_impl.inc). void record(VkCommandBuffer cmd, VkImageView prevView, VkImageView currView, VkImageView outView, float alpha, uint32_t gmSlot = 0); void destroy(); bool valid() const { return device_ != VK_NULL_HANDLE && ready_; } const Config& config() const { return cfg_; } - // public so the record_impl.inc free helpers can reference them struct Img { VkImage image = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; VkExtent2D extent{}; VkFormat fmt{}; }; struct Pipe { VkPipeline pipeline = VK_NULL_HANDLE; VkPipelineLayout layout = VK_NULL_HANDLE; VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; VkShaderModule module = VK_NULL_HANDLE; }; - // ── C1 GLOBAL-MOTION PRE-WARP constants (public so record_impl.inc's free - // helpers can size/index the SSBO pool). See of3_gm_reduce/of3_gm_prewarp. ── - static constexpr int kGmSlots = 3; // >= present ring size; one SSBO per in-flight ctx - static constexpr int kGmLevel = 3; // 1/8-res luma pyramid level for the LK estimate - static constexpr int kGmThreads = 512; // fixed reduce threads (8 groups * local_size_x 64) - static constexpr int kGmStride = 32; // floats/thread: 21 H + 6 b + coverage + resid + pad - // Finest pyramid level the dense flow is solved at (0 = full res). This is now - // RUNTIME-SELECTABLE via conf.toml perf_preset (0 Quality=1 / 1 Balanced=2 / - // 2 Performance=3). kFlowFinestDefault is the Balanced value (today's behaviour); - // the ACTIVE level lives in flowFinest_ and is read through flowFinest(). onResize - // sizes the stabilized-curr pyramid + C2 scratch from it, and record_impl.inc - // wires the flow/expand inputs to it - both must agree, so they all read - // flowFinest()/flowFinest_ (never a hard-coded 2). (Was a static constexpr = 2.) - static constexpr int kFlowFinestDefault = 2; // Balanced (perf_preset=1) - // Map perf_preset -> finest solved level. Out-of-range -> Balanced. A higher - // level = coarser (cheaper) flow = smaller base-FPS drop. configure() clamps the - // result against kLevels so at least two solved levels always remain. + + + static constexpr int kGmSlots = 3; + static constexpr int kGmLevel = 3; + static constexpr int kGmThreads = 512; + static constexpr int kGmStride = 32; + + + + + + + + static constexpr int kFlowFinestDefault = 2; + + + static int flowFinestForPreset(int preset) { switch (preset) { - case 0: return 1; // Quality ~1/2-res - case 2: return 3; // Performance ~1/8-res - case 1: default: return kFlowFinestDefault; // Balanced ~1/4-res (today) + case 0: return 1; + case 2: return 3; + case 1: default: return kFlowFinestDefault; } } - // The ACTIVE finest solved flow level (set by configure() from perf_preset). int flowFinest() const { return flowFinest_; } private: uint32_t findMemType(uint32_t bits, VkMemoryPropertyFlags props) const; bool makeImage(Img& out, VkExtent2D ext, VkFormat fmt, VkImageUsageFlags usage); void destroyImage(Img& i); - bool makeGmBuffers(); // allocate the kGmSlots host-visible LK-reduce SSBOs - // Read back one reduce SSBO, solve the 6x6 inverse-compositional LK step, and - // update the running/applied global affine. Returns true if it engaged this - // frame. Implemented in record_impl.inc (has the ScratchState/UBO context). + bool makeGmBuffers(); + + + bool updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent); bool makePipe(Pipe& p, int shaderId, const std::vector& bindings); @@ -97,55 +75,36 @@ private: Config cfg_; VkExtent2D extent_{}; bool ready_ = false; - // ACTIVE finest solved flow level (perf_preset). Default = Balanced (today's 2) - // so an unconfigured engine and an unset conf.toml are byte-identical to before. int flowFinest_ = kFlowFinestDefault; - // pipelines Pipe pLuma_, pDown_, pFlow_, pFlowM4_, pExpand_, pExpandM4_, pSynth_; - // C1: LK normal-equation reduce + curr-luma stabilization pre-warp Pipe pGmReduce_, pGmPrewarp_; - // C2: TV-L1 flow-regularization iteration (one dispatch per iteration) Pipe pFlowReg_; - // per-resolution resources - // 7 levels (was 5): the coarsest levels set the max motion the solver can track. - // At 5 levels the reach was only ~±84px/frame at 720p, so fast camera motion - // (150-300px/frame at the periphery) saturated the search -> incoherent vectors - // -> oil-paint melt. Two more (tiny) coarse levels push the reach to ~±300px so - // large global motion is captured coherently instead of smeared. Near-zero cost: - // the extra levels are 40x22 and 20x11 images. static const int kLevels = 7; - std::vector pyrA_, pyrB_; // luma pyramids for curr / prev - std::vector pyrAs_; // C1: STABILIZED curr luma (flow levels only) - std::vector flowLvl_; // per-level flow scratch - Img flowExpA_, flowExpB_; // fwd / bwd expanded flow (+conf) - // C2: TV-L1 ping-pong scratch at kFlowFinest res. flowLvl_[kFlowFinest] holds - // the fixed observation f0 (never written during the iterations); u ping-pongs - // between these two; the final iterate is copied back into flowLvl_[kFlowFinest] - // so of3_expand consumes the cleaned field with no binding change (⇒ skipping - // C2 leaves flowLvl_ byte-identical to pre-C2). + std::vector pyrA_, pyrB_; + std::vector pyrAs_; + std::vector flowLvl_; + Img flowExpA_, flowExpB_; + + + + + Img flowRegA_, flowRegB_; - // ── C1 global-motion state ──────────────────────────────────────────────── - // Host-visible SSBOs the reduce shader writes per-thread partials into; the - // CPU sums + solves them. One per ring slot so a readback never races a live - // submission (the caller fence-waited its slot before calling record()). VkBuffer gmSsbo_[kGmSlots] {}; VkDeviceMemory gmSsboMem_[kGmSlots]{}; void* gmSsboPtr_[kGmSlots]{}; bool gmBuffersReady_ = false; - // running affine params p1..p6 in centered-UV space (see GMUBO in config.hpp); - // identity = all zero. gmLinAt_ records the linearization params in effect when - // each slot's reduce was dispatched (the correct IC-update base for that slot). double gmRun_[6] {}; double gmLinAt_[kGmSlots][6] {}; bool gmSlotValid_[kGmSlots]{}; - bool gmHaveEst_ = false; // at least one good solve so far - int gmStableCount_ = 0; // consecutive good solves (auto-engage threshold) - bool gmEngaged_ = false; // is the affine being APPLIED this frame - float gmLastTransPx_ = 0.0f; // estimated global translation magnitude (full-res px), for logging + bool gmHaveEst_ = false; + int gmStableCount_ = 0; + bool gmEngaged_ = false; + float gmLastTransPx_ = 0.0f; unsigned long long gmFrames_ = 0; }; -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/log.hpp b/app/src/main/cpp/framegen/winfg/log.hpp index 77c7b8270..056f136f4 100644 --- a/app/src/main/cpp/framegen/winfg/log.hpp +++ b/app/src/main/cpp/framegen/winfg/log.hpp @@ -1,5 +1,3 @@ -// win-fg - logging. On Android goes to logcat (tag "win-fg") so device bring-up -// can trace the layer; elsewhere falls back to stderr. #pragma once #if defined(__ANDROID__) #include @@ -11,10 +9,4 @@ #define WFG_LOGE(...) do { std::fprintf(stderr, "[win-fg][E] " __VA_ARGS__); std::fprintf(stderr, "\n"); } while(0) #endif -// Runtime-gated granular debug trace. The gate is a RUNTIME bool (Config::debug), -// not a compile-time switch - a shipping .so carries the full trail but pays only -// a predicted-not-taken bool test per step when debug is off (no logcat write, no -// arg evaluation of the message). Same "win-fg" tag so the app captures it under -// its own UID. Used to step the present path so a freeze leaves an obvious last -// line at the stage that hung. Usage: WFG_LOGD(dbg, "P#%llu step ...", n); #define WFG_LOGD(cond, ...) do { if (cond) WFG_LOGI(__VA_ARGS__); } while(0) diff --git a/app/src/main/cpp/framegen/winfg/record_impl.inc b/app/src/main/cpp/framegen/winfg/record_impl.inc index dcf0696ce..2e8fba009 100644 --- a/app/src/main/cpp/framegen/winfg/record_impl.inc +++ b/app/src/main/cpp/framegen/winfg/record_impl.inc @@ -1,57 +1,28 @@ -// win-fg - FrameGen::record() and its lazy per-size descriptor/UBO setup. -// Included from framegen.cpp (has access to FrameGen privates). Written for -// win-fg; standard Vulkan compute wiring, no third-party code. -// -// ── C1 GLOBAL-MOTION PRE-WARP (this file wires it end to end) ───────────────── -// The dense SAD flow search melts on fast camera pans because the global motion -// saturates its window. C1 estimates the per-frame camera affine with a cheap -// inverse-compositional Lucas-Kanade solve (of3_gm_reduce accumulates the 6x6 -// normal equations at 1/8 res → CPU 6x6 Cholesky here), then of3_gm_prewarp -// removes it from the curr luma pyramid BEFORE the search, so the search only -// sees small, coherent, object-only residual motion. of3_expand(_m4) composes -// the global affine back so wfg_synth is unchanged and the final warp is correct. -// -// DECOUPLED (stall-free) SCHEDULING: a true in-frame CPU solve would need a -// mid-frame GPU submit+readback stall - unacceptable on a GPU-bound title. So we -// read back the reduce SSBO the caller ALREADY fence-waited (its ring slot from a -// couple of presents ago) and run ONE Gauss-Newton step per frame, warm-started -// from the running estimate. Camera motion is temporally smooth, so a ~1-2 frame -// old affine is an excellent predictor; and because the SAME affine is removed -// (prewarp) and added back (expand), the result is EXACT for any affine we pick - -// an approximate/stale one only changes how much residual the search must absorb, -// never correctness. Identity affine ⇒ byte-identical to the pre-C1 pipeline, so -// the bail-to-identity safety path can never be worse than "no pre-warp". namespace { struct ScratchState { - // host-visible UBO buffers VkBuffer synthUbo = VK_NULL_HANDLE; VkDeviceMemory synthUboMem = VK_NULL_HANDLE; void* synthUboPtr = nullptr; std::vector flowUbo; std::vector flowUboMem; std::vector flowUboPtr; VkBuffer expandUbo = VK_NULL_HANDLE; VkDeviceMemory expandUboMem = VK_NULL_HANDLE; void* expandUboPtr = nullptr; - // C2: flow-regularization UBO (shared by all iterations; refreshed per frame). VkBuffer flowRegUbo = VK_NULL_HANDLE; VkDeviceMemory flowRegUboMem = VK_NULL_HANDLE; void* flowRegUboPtr = nullptr; - // C1: per-slot global-motion UBO (double-buffered by ring slot so a live GPU - // read is never overwritten by the next frame - see the header note). VkBuffer gmUbo[winfg::FrameGen::kGmSlots]{}; VkDeviceMemory gmUboMem[winfg::FrameGen::kGmSlots]{}; void* gmUboPtr[winfg::FrameGen::kGmSlots]{}; - // persistent descriptor sets VkDescriptorSet sLumaCurr = VK_NULL_HANDLE, sLumaPrev = VK_NULL_HANDLE; std::vector sDownA, sDownB, sFlow; - VkDescriptorSet sExpand[winfg::FrameGen::kGmSlots]{}; // per-slot (binds gmUbo[slot]) - // C2: three flow-reg sets covering every (u_in → u_out) pairing of the - // ping-pong. sFlowReg0: in=flowLvl_[F] (=f0), out=A. sFlowRegAB: in=A, out=B. - // sFlowRegBA: in=B, out=A. f0 (binding 33) = flowLvl_[F] in all three. + VkDescriptorSet sExpand[winfg::FrameGen::kGmSlots]{}; + + + VkDescriptorSet sFlowReg0 = VK_NULL_HANDLE, sFlowRegAB = VK_NULL_HANDLE, sFlowRegBA = VK_NULL_HANDLE; VkDescriptorSet sSynth = VK_NULL_HANDLE; - // C1: per-slot reduce set + per-slot per-level prewarp sets VkDescriptorSet sGmReduce[winfg::FrameGen::kGmSlots]{}; - std::vector sGmPrewarp[winfg::FrameGen::kGmSlots]; // sized kLevels; [flowFinest()..) used + std::vector sGmPrewarp[winfg::FrameGen::kGmSlots]; VkExtent2D builtFor{0,0}; bool built = false; - bool flowInited = false; // flow images laid out once, then persist (temporal predictor) + bool flowInited = false; }; -ScratchState g_scratch; // one swapchain in flight in this first-draft engine +ScratchState g_scratch; } static bool makeUbo(const winfg::DeviceDispatch* dd, VkDevice dev, @@ -77,7 +48,6 @@ static VkDescriptorSet allocSet(const winfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet s = VK_NULL_HANDLE; dd->AllocateDescriptorSets(dev, &ai, &s); return s; } -// write helpers static void wImg(const winfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet set, uint32_t binding, VkDescriptorType type, VkImageView view, VkSampler samp, VkImageLayout layout) { VkDescriptorImageInfo ii{}; ii.imageView = view; ii.sampler = samp; ii.imageLayout = layout; @@ -102,9 +72,6 @@ static void wSsbo(const winfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet dd->UpdateDescriptorSets(dev, 1, &w, 0, nullptr); } -// ── C1 CPU-side linear algebra (clean-room, textbook) ──────────────────────── -// 6x6 symmetric positive-definite solve via Cholesky. Returns false if not PD -// (a bad/ill-conditioned frame → caller bails to identity). Damped by the caller. static bool cholSolve6(double A[6][6], const double b[6], double x[6]) { double L[6][6] = {{0}}; for (int i = 0; i < 6; ++i) { @@ -112,7 +79,7 @@ static bool cholSolve6(double A[6][6], const double b[6], double x[6]) { double s = A[i][j]; for (int k = 0; k < j; ++k) s -= L[i][k] * L[j][k]; if (i == j) { - if (s <= 1e-20) return false; // not positive-definite + if (s <= 1e-20) return false; L[i][j] = std::sqrt(s); } else { L[i][j] = s / L[j][j]; @@ -124,7 +91,6 @@ static bool cholSolve6(double A[6][6], const double b[6], double x[6]) { for (int i = 5; i >= 0; --i) { double s = y[i]; for (int k = i+1; k < 6; ++k) s -= L[k][i]*x[k]; x[i] = s / L[i][i]; } return true; } -// affine <-> params. params p=(p1,p2,p3,p4,p5,p6); M = I + [[p1,p3,p5],[p2,p4,p6]]. static void affFromParams(const double p[6], double M[3][3]) { M[0][0]=1.0+p[0]; M[0][1]=p[2]; M[0][2]=p[4]; M[1][0]=p[1]; M[1][1]=1.0+p[3]; M[1][2]=p[5]; @@ -153,8 +119,6 @@ static void mulAff(const double A[3][3], const double B[3][3], double out[3][3]) namespace winfg { -// Lazily allocate per-size UBOs + descriptor sets and wire the fixed (internal- -// resource) bindings once. External views (prev/curr/out) are wired per frame. static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, const VkPhysicalDeviceMemoryProperties& mp, VkDescriptorPool pool, VkSampler samp, VkExtent2D ext, @@ -168,11 +132,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, FrameGen::Img& flowRegA, FrameGen::Img& flowRegB, VkBuffer* gmSsbo, int kLevels, const Config& cfg); -// Tear down the lazily-built scratch (all UBO buffers + every descriptor set) so a -// live perf_preset rebuild neither leaks them nor exhausts the descriptor pool. -// The CALLER must have made the device idle first - no in-flight command buffer may -// still reference these. Resetting the pool frees every set FrameGen allocated (all -// sets come from this pool); the mapped UBO memory is unmapped + freed explicitly. static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorPool pool) { auto freeUbo = [&](VkBuffer& b, VkDeviceMemory& m, void*& p) { if (m) dd->UnmapMemory(dev, m); @@ -186,29 +145,20 @@ static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorP freeUbo(S.flowRegUbo, S.flowRegUboMem, S.flowRegUboPtr); for (size_t i = 0; i < S.flowUbo.size(); ++i) freeUbo(S.flowUbo[i], S.flowUboMem[i], S.flowUboPtr[i]); for (int s = 0; s < FrameGen::kGmSlots; ++s) freeUbo(S.gmUbo[s], S.gmUboMem[s], S.gmUboPtr[s]); - if (pool && dd->ResetDescriptorPool) dd->ResetDescriptorPool(dev, pool, 0); // reclaim all sets at once - S = ScratchState{}; // built=false -> next record() rebuilds via ensureScratch + if (pool && dd->ResetDescriptorPool) dd->ResetDescriptorPool(dev, pool, 0); + S = ScratchState{}; } -// ── perf_preset apply / live rebuild ───────────────────────────────────────── -// Sets the config, resolves the perf_preset -> finest-solved-level, and - when -// that level changed while resources are already live - safely rebuilds the -// affected per-size flow images (a resize) + scratch descriptor sets and resets -// the flow predictor. This makes a perf_preset change FULLY LIVE per-present (the -// conf.toml hot-reload path calls this), so no FG toggle / bg-fg cycle is needed. void FrameGen::configure(const Config& c) { cfg_ = c; cfg_.sanitize(); - // Map preset -> finest level, clamped so at least TWO solved levels always - // remain (coarse->fine needs a range). With kLevels=7 the presets (1/2/3) never - // hit the clamp; the guard is defensive for future level/preset tuning. int want = flowFinestForPreset(cfg_.perfPreset); - const int maxFinest = kLevels - 2; // keep >= 2 solved levels + const int maxFinest = kLevels - 2; if (want > maxFinest) { WFG_LOGE("perf-preset=%d wants kFlowFinest=%d but only %d pyramid levels exist " "(need >=2 solved) - clamping to %d", cfg_.perfPreset, want, kLevels, maxFinest); want = maxFinest; } - if (want < 1) want = 1; // never solve full-res (level 0) + if (want < 1) want = 1; const int prevFinest = flowFinest_; flowFinest_ = want; @@ -216,35 +166,27 @@ void FrameGen::configure(const Config& c) { WFG_LOGI("perf-preset=%d -> kFlowFinest=%d (was %d) live-rebuild=%d ready=%d", cfg_.perfPreset, flowFinest_, prevFinest, live ? 1 : 0, ready_ ? 1 : 0); - if (!live) return; // unchanged, or not yet built (initial configure before onResize) + if (!live) return; - // The per-size flow images (pyrAs_/flowReg*/flowLvl_ wiring) and the scratch - // descriptor sets were built for the OLD finest level. Rebuild them so record() - // never reads/writes mismatched-size images. - dd_->DeviceWaitIdle(device_); // no in-flight cmd buf may touch these - destroyScratch(dd_, device_, descPool_); // free old scratch (UBOs + all sets) - if (!onResize(extent_, VK_FORMAT_UNDEFINED, /*force=*/true)) + + + + dd_->DeviceWaitIdle(device_); + destroyScratch(dd_, device_, descPool_); + if (!onResize(extent_, VK_FORMAT_UNDEFINED, true)) WFG_LOGE("perf-preset live rebuild: onResize FAILED at %ux%u - FG disabled until next resize", extent_.width, extent_.height); - // Reset the persistent flow predictor so stale, differently-scaled state never - // feeds the new resolution (avoids a melt/garbage frame on switch). The per-frame - // flow field (flowLvl_) is already cleared every frame; the cross-frame state is - // the C1 global-motion estimate + its per-slot reduce partials (dispatched against - // the old pyramid) - drop both back to identity. for (int i = 0; i < 6; ++i) gmRun_[i] = 0.0; for (int s = 0; s < kGmSlots; ++s) { gmSlotValid_[s] = false; for (int i = 0; i < 6; ++i) gmLinAt_[s][i] = 0.0; } gmHaveEst_ = false; gmEngaged_ = false; gmStableCount_ = 0; gmLastTransPx_ = 0.0f; WFG_LOGI("perf-preset live rebuild DONE (kFlowFinest=%d, flow predictor reset)", flowFinest_); } -// ── C1: read back one reduce SSBO, run one inverse-compositional LK step ────── -// Updates the running/applied global affine. Returns whether GM engages this frame. bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) { ++gmFrames_; - if (!gmBuffersReady_ || cfg_.gmMode == 2 /*off*/) { - // off: keep identity applied; keep estimating cheaply is pointless when off. + if (!gmBuffersReady_ || cfg_.gmMode == 2 ) { + gmEngaged_ = false; gmStableCount_ = 0; - // still refresh the linearization record so a live switch to auto starts clean for (int i = 0; i < 6; ++i) gmRun_[i] = 0.0; for (int i = 0; i < 6; ++i) gmLinAt_[gmSlot][i] = gmRun_[i]; gmLastTransPx_ = 0.0f; @@ -260,31 +202,25 @@ bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) { for (int k = 0; k < kGmStride; ++k) sums[k] += (double)row[k]; } double coverage = sums[27]; - // require a few percent of the level's pixels as inliers double lvlpx = double((fullExtent.width >> kGmLevel) * (fullExtent.height >> kGmLevel)); double minCov = 0.03 * (lvlpx > 0 ? lvlpx : 1.0); if (coverage > minCov && coverage > 64.0) { double H[6][6]; int idx = 0; for (int r = 0; r < 6; ++r) for (int c = r; c < 6; ++c) { H[r][c] = H[c][r] = sums[idx]; ++idx; } double b[6]; for (int i = 0; i < 6; ++i) b[i] = sums[21 + i]; - // Levenberg damping for conditioning (relative + tiny absolute). for (int i = 0; i < 6; ++i) H[i][i] += 1e-3 * H[i][i] + 1e-9; double dp[6]; if (cholSolve6(H, b, dp)) { - // reject wild steps (something non-affine / a scene cut) bool bounded = std::fabs(dp[0]) < 0.5 && std::fabs(dp[1]) < 0.5 && std::fabs(dp[2]) < 0.5 && std::fabs(dp[3]) < 0.5 && std::fabs(dp[4]) < 0.25 && std::fabs(dp[5]) < 0.25; if (bounded) { - // IC update: M_new = M_lin ∘ W(dp)^-1 (compose onto the SLOT's - // linearization base, which is what this reduce linearized around). double Mlin[3][3], Md[3][3], Mdi[3][3], Mnew[3][3]; affFromParams(gmLinAt_[gmSlot], Mlin); affFromParams(dp, Md); if (invAff(Md, Mdi)) { mulAff(Mlin, Mdi, Mnew); double pnew[6]; paramsFromAff(Mnew, pnew); - // final sanity clamp on the composed estimate bool sane = std::fabs(pnew[0]) < 0.6 && std::fabs(pnew[1]) < 0.6 && std::fabs(pnew[2]) < 0.6 && std::fabs(pnew[3]) < 0.6 && std::fabs(pnew[4]) < 0.5 && std::fabs(pnew[5]) < 0.5; @@ -297,17 +233,14 @@ bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) { if (good) ++gmStableCount_; else gmStableCount_ = 0; - // engage policy switch (cfg_.gmMode) { - case 1: gmEngaged_ = gmHaveEst_; break; // on - case 2: gmEngaged_ = false; break; // off (unreachable; handled above) - default: gmEngaged_ = (gmStableCount_ >= 3); break; // auto: 3 consecutive good solves + case 1: gmEngaged_ = gmHaveEst_; break; + case 2: gmEngaged_ = false; break; + default: gmEngaged_ = (gmStableCount_ >= 3); break; } - // record the linearization base for the reduce we are about to dispatch into - // this slot this frame (i.e. the current running estimate). for (int i = 0; i < 6; ++i) gmLinAt_[gmSlot][i] = gmRun_[i]; - gmSlotValid_[gmSlot] = true; // this frame's reduce will (re)write it + gmSlotValid_[gmSlot] = true; gmLastTransPx_ = (float)std::sqrt( (gmRun_[4]*fullExtent.width) * (gmRun_[4]*fullExtent.width) + @@ -330,9 +263,7 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur pFlowReg_, flowRegA_, flowRegB_, gmSsbo_, kLevels, cfg_); } - // ── C1: solve the global-motion affine (reads the SSBO the caller fence-waited) ── bool gmEngaged = updateGlobalMotion(gmSlot, extent_); - // Build this slot's GMUBO from the running / applied affine. if (g_scratch.gmUboPtr[gmSlot]) { GMUBO g{}; g.lin[0]=(float)gmRun_[0]; g.lin[1]=(float)gmRun_[1]; g.lin[2]=(float)gmRun_[2]; g.lin[3]=(float)gmRun_[3]; @@ -340,10 +271,9 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur if (gmEngaged) { g.app[0]=(float)gmRun_[0]; g.app[1]=(float)gmRun_[1]; g.app[2]=(float)gmRun_[2]; g.app[3]=(float)gmRun_[3]; g.appT[0]=(float)gmRun_[4]; g.appT[1]=(float)gmRun_[5]; - } // else app/appT stay 0 -> identity -> prewarp/expand are pre-C1 no-ops + } std::memcpy(g_scratch.gmUboPtr[gmSlot], &g, sizeof(g)); } - // Log engage state + estimated global translation periodically (tag win-fg). { static int lastEng = -1; if (gmEngaged != (lastEng == 1) || (gmFrames_ % 300ull) == 0) { @@ -353,12 +283,10 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur } } - // update UBOs for this frame SynthUBO su{cfg_.flowScale, alpha, cfg_.beta, cfg_.lambda, cfg_.epsilon, cfg_.photoScale, 0, 0, cfg_.hudX0, cfg_.hudY0, cfg_.hudX1, cfg_.hudY1}; if (g_scratch.synthUboPtr) std::memcpy(g_scratch.synthUboPtr, &su, sizeof(su)); - // wire per-frame external views wImg(dd_, device_, g_scratch.sLumaCurr, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, currView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); wImg(dd_, device_, g_scratch.sLumaPrev, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, prevView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); wImg(dd_, device_, g_scratch.sSynth, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, prevView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); @@ -372,23 +300,13 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur }; auto bar = [&](FrameGen::Img& i){ barrier(cmd, i.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL); }; - // Internal images are created UNDEFINED and fully recomputed every frame, so - // discard-transition them all to GENERAL up front (all descriptors use GENERAL - // for these). This also fixes the first-frame layout (storage writes require - // GENERAL, not UNDEFINED). for (auto& i : pyrA_) barrier(cmd, i.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); for (auto& i : pyrB_) barrier(cmd, i.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); for (int l = flowFinest_; l < kLevels; ++l) barrier(cmd, pyrAs_[l].image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); barrier(cmd, flowExpA_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); barrier(cmd, flowExpB_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); - // C2: ping-pong scratch to GENERAL (storage writes require it; contents discarded). barrier(cmd, flowRegA_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); barrier(cmd, flowRegB_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); - // Reset the flow field to zero every frame. The coarsest level uses it as its - // search predictor; carrying it across frames lets a spurious vector feed - // forward until it diverges and melts the image (the bg/fg pause was resetting - // it - that's why artifacts vanished on resume). Fresh zero every frame = - // stable, standard pyramid flow, no feedback loop. { VkClearColorValue zero{}; VkImageSubresourceRange rng{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; @@ -406,20 +324,14 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur } } - // 1) luma of curr/prev at full res disp(pLuma_, g_scratch.sLumaCurr, levelExtent(extent_, 0)); disp(pLuma_, g_scratch.sLumaPrev, levelExtent(extent_, 0)); bar(pyrA_[0]); bar(pyrB_[0]); - // 2) downsample pyramids for (int l = 1; l < kLevels; ++l) { disp(pDown_, g_scratch.sDownA[l - 1], levelExtent(extent_, l)); disp(pDown_, g_scratch.sDownB[l - 1], levelExtent(extent_, l)); bar(pyrA_[l]); bar(pyrB_[l]); } - // 2b) C1: LK normal-equation reduce (fixed grid, 1/8-res) → this slot's SSBO. - // Reads raw prev/curr luma at kGmLevel; writes per-thread partials. A - // compute→host barrier makes the write visible to the CPU readback that - // happens when this slot comes round again (~1-2 frames later). if (gmBuffersReady_ && g_scratch.sGmReduce[gmSlot]) { dd_->CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pGmReduce_.pipeline); dd_->CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pGmReduce_.layout, 0, 1, &g_scratch.sGmReduce[gmSlot], 0, nullptr); @@ -429,28 +341,15 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &mb, 0, nullptr, 0, nullptr); } - // 2c) C1: pre-warp curr luma pyramid by the applied affine → stabilized pyramid - // (identity affine ⇒ exact copy, so the flow below is unchanged when - // disengaged). The dense flow reads pyrAs from here on. for (int l = flowFinest_; l < kLevels; ++l) { disp(pGmPrewarp_, g_scratch.sGmPrewarp[gmSlot][l], levelExtent(extent_, l)); bar(pyrAs_[l]); } - // 3) coarse->fine flow (only down to flowFinest_ - reduced res for perf; the - // finest level is perf_preset-selectable). Curr input is now the STABILIZED - // pyramid (pyrAs), wired in ensureScratch. for (int l = kLevels - 1; l >= flowFinest_; --l) { disp(flowP, g_scratch.sFlow[l], levelExtent(extent_, l)); bar(flowLvl_[l]); } - // 3b) ── C2: TV-L1 flow regularization on the finest residual field ────────── - // Run frIters semi-implicit TV-L1 denoising iterations on flowLvl_[flowFinest_] - // (the object-only residual left by C1). flowLvl_[F] is the fixed data term f0 - // (read, never written here); u ping-pongs A<->B; the final iterate is copied - // back into flowLvl_[F] so of3_expand consumes the cleaned field unchanged. - // Disengaged (frMode==off or frIters==0) ⇒ flowLvl_[F] untouched ⇒ byte-identical - // to pre-C2. Identity/zero iterations is therefore never worse than "no reg". const bool frEngaged = (cfg_.frMode != 2) && (cfg_.frIters > 0); if (frEngaged) { const int F = flowFinest_; @@ -471,9 +370,6 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur bar(*outImg); last = outImg; } - // Copy the cleaned finest flow back into flowLvl_[F] (GENERAL->GENERAL). - // Barrier: C2's f0 reads (SHADER_READ of flowLvl_[F]) and the last iterate - // write must complete before the transfer overwrites/reads. VkMemoryBarrier mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; mb.srcAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; mb.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; @@ -491,7 +387,7 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &mb2, 0, nullptr, 0, nullptr); } - { // periodic C2 status (tag win-fg), mirrors the C1 log cadence + { static int lastFr = -1; int fe = frEngaged ? 1 : 0; if (fe != lastFr || (gmFrames_ % 300ull) == 0) { @@ -501,14 +397,11 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur } } - // 4) expand finest solved flow -> fwd/bwd (+conf), + C1 global-motion add-back. disp(expandP, g_scratch.sExpand[gmSlot], levelExtent(extent_, 0)); bar(flowExpA_); bar(flowExpB_); - // 5) synthesise interpolated frame disp(pSynth_, g_scratch.sSynth, levelExtent(extent_, 0)); } -// ---- lazy scratch builder ---- static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, const VkPhysicalDeviceMemoryProperties& mp, VkDescriptorPool pool, VkSampler samp, VkExtent2D ext, @@ -528,7 +421,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, makeUbo(dd, dev, mp, sizeof(SynthUBO), S.synthUbo, S.synthUboMem, S.synthUboPtr); makeUbo(dd, dev, mp, sizeof(FlowUBO), S.expandUbo, S.expandUboMem, S.expandUboPtr); if (S.expandUboPtr) { FlowUBO u{cfg.flowScale, 0, 0.6f, 1.6f}; std::memcpy(S.expandUboPtr, &u, sizeof(u)); } - // C2: flow-reg UBO (params refreshed each frame in record() for hot-reload). makeUbo(dd, dev, mp, sizeof(FlowRegUBO), S.flowRegUbo, S.flowRegUboMem, S.flowRegUboPtr); if (S.flowRegUboPtr) { FlowRegUBO fu{cfg.frDt, cfg.frLambda, cfg.frEdge, cfg.frEps}; std::memcpy(S.flowRegUboPtr, &fu, sizeof(fu)); } S.flowUbo.resize(kLevels); S.flowUboMem.resize(kLevels); S.flowUboPtr.resize(kLevels); @@ -536,19 +428,16 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, makeUbo(dd, dev, mp, sizeof(FlowUBO), S.flowUbo[l], S.flowUboMem[l], S.flowUboPtr[l]); if (S.flowUboPtr[l]) { FlowUBO u{cfg.flowScale, (uint32_t)l, 0.6f, 1.6f}; std::memcpy(S.flowUboPtr[l], &u, sizeof(u)); } } - // C1: per-slot GMUBO (initialised to identity) for (int s = 0; s < FrameGen::kGmSlots; ++s) { makeUbo(dd, dev, mp, sizeof(GMUBO), S.gmUbo[s], S.gmUboMem[s], S.gmUboPtr[s]); if (S.gmUboPtr[s]) { GMUBO g{}; std::memcpy(S.gmUboPtr[s], &g, sizeof(g)); } } - // luma sets (external src wired per frame; dst is pyramid[0]) S.sLumaCurr = allocSet(dd, dev, pool, pLuma.setLayout); S.sLumaPrev = allocSet(dd, dev, pool, pLuma.setLayout); wImg(dd, dev, S.sLumaCurr, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, pyrA[0].view, VK_NULL_HANDLE, GEN); wImg(dd, dev, S.sLumaPrev, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, pyrB[0].view, VK_NULL_HANDLE, GEN); - // downsample sets (fixed) S.sDownA.resize(kLevels - 1); S.sDownB.resize(kLevels - 1); for (int l = 1; l < kLevels; ++l) { S.sDownA[l-1] = allocSet(dd, dev, pool, pDown.setLayout); @@ -559,10 +448,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, wImg(dd, dev, S.sDownB[l-1], 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, pyrB[l].view, VK_NULL_HANDLE, GEN); } - // flow sets (fixed): predictor = coarser flow level, or self at coarsest. - // C1: the CURR input (binding 33) is the STABILIZED pyramid pyrAs for the - // solved flow levels; prev stays raw. Below kFlowFinest the level is never - // dispatched, so leaving those at raw curr is harmless. S.sFlow.resize(kLevels); for (int l = 0; l < kLevels; ++l) { S.sFlow[l] = allocSet(dd, dev, pool, pFlow.setLayout); @@ -575,19 +460,17 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, wImg(dd, dev, S.sFlow[l], 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowLvl[l].view, VK_NULL_HANDLE, GEN); } - // C1: reduce sets (one per slot) - prev/curr raw luma at kGmLevel + SSBO + GMUBO. if (gmSsbo) { for (int s = 0; s < FrameGen::kGmSlots; ++s) { if (!gmSsbo[s]) continue; S.sGmReduce[s] = allocSet(dd, dev, pool, pGmReduce.setLayout); wUbo (dd, dev, S.sGmReduce[s], 1, S.gmUbo[s], sizeof(GMUBO)); wSsbo(dd, dev, S.sGmReduce[s], 16, gmSsbo[s], VK_WHOLE_SIZE); - wImg (dd, dev, S.sGmReduce[s], 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[FrameGen::kGmLevel].view, samp, GEN); // prev = template - wImg (dd, dev, S.sGmReduce[s], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrA[FrameGen::kGmLevel].view, samp, GEN); // curr = image + wImg (dd, dev, S.sGmReduce[s], 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[FrameGen::kGmLevel].view, samp, GEN); + wImg (dd, dev, S.sGmReduce[s], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrA[FrameGen::kGmLevel].view, samp, GEN); } } - // C1: prewarp sets (one per slot, per solved flow level) - raw curr in, stabilized out. for (int s = 0; s < FrameGen::kGmSlots; ++s) { S.sGmPrewarp[s].assign(kLevels, VK_NULL_HANDLE); for (int l = fg->flowFinest(); l < kLevels; ++l) { @@ -598,7 +481,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, } } - // expand sets (one per slot): flow inputs fixed; binding 1 = that slot's GMUBO. for (int s = 0; s < FrameGen::kGmSlots; ++s) { S.sExpand[s] = allocSet(dd, dev, pool, pExpand.setLayout); wUbo(dd, dev, S.sExpand[s], 0, S.expandUbo, sizeof(FlowUBO)); @@ -608,9 +490,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, wImg(dd, dev, S.sExpand[s], 49, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, expB.view, VK_NULL_HANDLE, GEN); } - // C2: flow-reg sets. F = kFlowFinest. f0 (binding 33) = flowLvl_[F] in all; luma - // (binding 34) = prev luma at F (pyrB) for the edge-aware weight. u_in (32) and - // u_out (48) alternate to realize the ping-pong (see ScratchState comment). { const int F = fg->flowFinest(); S.sFlowReg0 = allocSet(dd, dev, pool, pFlowReg.setLayout); @@ -619,26 +498,19 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, VkDescriptorSet all3[3] = {S.sFlowReg0, S.sFlowRegAB, S.sFlowRegBA}; for (VkDescriptorSet s : all3) { wUbo(dd, dev, s, 0, S.flowRegUbo, sizeof(FlowRegUBO)); - wImg(dd, dev, s, 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[F].view, samp, GEN); // f0 - wImg(dd, dev, s, 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[F].view, samp, GEN); // luma + wImg(dd, dev, s, 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[F].view, samp, GEN); + wImg(dd, dev, s, 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[F].view, samp, GEN); } - // iter0: in=flowLvl_[F] (=f0), out=A wImg(dd, dev, S.sFlowReg0, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[F].view, samp, GEN); wImg(dd, dev, S.sFlowReg0, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowRegA.view, VK_NULL_HANDLE, GEN); - // odd iters: in=A, out=B wImg(dd, dev, S.sFlowRegAB, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowRegA.view, samp, GEN); wImg(dd, dev, S.sFlowRegAB, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowRegB.view, VK_NULL_HANDLE, GEN); - // even iters (>0): in=B, out=A wImg(dd, dev, S.sFlowRegBA, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowRegB.view, samp, GEN); wImg(dd, dev, S.sFlowRegBA, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowRegA.view, VK_NULL_HANDLE, GEN); } - // synth set: external prev/curr/out wired per frame; flow inputs fixed S.sSynth = allocSet(dd, dev, pool, pSynth.setLayout); wUbo(dd, dev, S.sSynth, 0, S.synthUbo, sizeof(SynthUBO)); - // GENERAL (not SR): these internal flow images stay in GENERAL across the whole - // record() (written as storage by expand, sampled here) - see the discard - // transition at the top of record(). wImg(dd, dev, S.sSynth, 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expA.view, samp, GEN); wImg(dd, dev, S.sSynth, 35, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expB.view, samp, GEN); @@ -646,4 +518,4 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, return true; } -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/vk_dispatch.hpp b/app/src/main/cpp/framegen/winfg/vk_dispatch.hpp index d1a6366ad..ef259250b 100644 --- a/app/src/main/cpp/framegen/winfg/vk_dispatch.hpp +++ b/app/src/main/cpp/framegen/winfg/vk_dispatch.hpp @@ -1,7 +1,3 @@ -// win-fg - minimal Vulkan layer dispatch tables. -// Structure follows the public Khronos loader/layer interface and the -// renderdoc "Vulkan layer guide" skeleton (learning references, Apache-2.0 / -// CC-BY). No code from bionic-fg / lsfg / GameScope. #pragma once #include #include @@ -9,53 +5,35 @@ namespace winfg { -// The loader sets the first pointer-sized word of a dispatchable handle to the -// dispatch table pointer; layers key their own maps on that value so that all -// handles sharing a chain resolve to the same table. static inline void* dispatch_key(void* handle) { return *reinterpret_cast(handle); } -// Instance-level functions we call from the layer. struct InstanceDispatch { PFN_vkGetInstanceProcAddr GetInstanceProcAddr = nullptr; PFN_vkDestroyInstance DestroyInstance = nullptr; PFN_vkGetPhysicalDeviceMemoryProperties GetPhysicalDeviceMemoryProperties = nullptr; PFN_vkGetPhysicalDeviceQueueFamilyProperties GetPhysicalDeviceQueueFamilyProperties = nullptr; - // Device-context logging (affected-device fingerprint). Properties2 is core 1.1; - // may be null on a 1.0 instance - always null-check before calling. PFN_vkGetPhysicalDeviceProperties GetPhysicalDeviceProperties = nullptr; PFN_vkGetPhysicalDeviceProperties2 GetPhysicalDeviceProperties2 = nullptr; - // Surface caps - used to clamp the extra-image-headroom request to the surface's - // maxImageCount so the bump never exceeds what the driver can grant. PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR GetPhysicalDeviceSurfaceCapabilitiesKHR = nullptr; }; -// Device-level functions we call. Populated by walking vkGetDeviceProcAddr on -// the next layer down at device-create time. struct DeviceDispatch { PFN_vkGetDeviceProcAddr GetDeviceProcAddr = nullptr; PFN_vkDestroyDevice DestroyDevice = nullptr; PFN_vkGetDeviceQueue GetDeviceQueue = nullptr; PFN_vkQueueSubmit QueueSubmit = nullptr; - // Synchronization2 submit path (core 1.3 + KHR alias). DXVK may use either the - // legacy QueueSubmit or QueueSubmit2/2KHR; both are hooked for the guest-submit - // diagnostic. May be null if the device did not enable them - always null-check. PFN_vkQueueSubmit2 QueueSubmit2 = nullptr; PFN_vkQueueSubmit2KHR QueueSubmit2KHR = nullptr; PFN_vkQueueWaitIdle QueueWaitIdle = nullptr; PFN_vkDeviceWaitIdle DeviceWaitIdle = nullptr; - // swapchain / present PFN_vkCreateSwapchainKHR CreateSwapchainKHR = nullptr; PFN_vkDestroySwapchainKHR DestroySwapchainKHR = nullptr; PFN_vkGetSwapchainImagesKHR GetSwapchainImagesKHR = nullptr; PFN_vkAcquireNextImageKHR AcquireNextImageKHR = nullptr; - // Alternate acquire entry (VK_KHR_swapchain 1.1+ / device-group). DXVK may use - // either AcquireNextImageKHR or AcquireNextImage2KHR - both hooked for the - // guest-acquire diagnostic. May be null - always null-check. PFN_vkAcquireNextImage2KHR AcquireNextImage2KHR = nullptr; PFN_vkQueuePresentKHR QueuePresentKHR = nullptr; - // resources PFN_vkCreateImage CreateImage = nullptr; PFN_vkDestroyImage DestroyImage = nullptr; PFN_vkCreateImageView CreateImageView = nullptr; @@ -73,7 +51,6 @@ struct DeviceDispatch { PFN_vkCreateSampler CreateSampler = nullptr; PFN_vkDestroySampler DestroySampler = nullptr; - // pipeline / descriptors PFN_vkCreateShaderModule CreateShaderModule = nullptr; PFN_vkDestroyShaderModule DestroyShaderModule = nullptr; PFN_vkCreateDescriptorSetLayout CreateDescriptorSetLayout = nullptr; @@ -84,15 +61,10 @@ struct DeviceDispatch { PFN_vkDestroyPipeline DestroyPipeline = nullptr; PFN_vkCreateDescriptorPool CreateDescriptorPool = nullptr; PFN_vkDestroyDescriptorPool DestroyDescriptorPool = nullptr; - // Frees every set in the pool at once - used by FrameGen::configure() to reclaim - // the old scratch descriptor sets when a perf_preset change forces a live rebuild - // (otherwise repeated preset switches would exhaust the pool). May be null in - // theory; the rebuild path null-checks before calling. PFN_vkResetDescriptorPool ResetDescriptorPool = nullptr; PFN_vkAllocateDescriptorSets AllocateDescriptorSets = nullptr; PFN_vkUpdateDescriptorSets UpdateDescriptorSets = nullptr; - // command recording PFN_vkCreateCommandPool CreateCommandPool = nullptr; PFN_vkDestroyCommandPool DestroyCommandPool = nullptr; PFN_vkAllocateCommandBuffers AllocateCommandBuffers = nullptr; @@ -104,7 +76,7 @@ struct DeviceDispatch { PFN_vkCmdDispatch CmdDispatch = nullptr; PFN_vkCmdPipelineBarrier CmdPipelineBarrier = nullptr; PFN_vkCmdCopyImage CmdCopyImage = nullptr; - PFN_vkCmdCopyImageToBuffer CmdCopyImageToBuffer = nullptr; // capture readback + PFN_vkCmdCopyImageToBuffer CmdCopyImageToBuffer = nullptr; PFN_vkCmdBlitImage CmdBlitImage = nullptr; PFN_vkCmdClearColorImage CmdClearColorImage = nullptr; PFN_vkCreateFence CreateFence = nullptr; @@ -120,4 +92,4 @@ static inline void load(PFN& slot, PFN_vkGetDeviceProcAddr gdpa, VkDevice dev, c slot = reinterpret_cast(gdpa(dev, name)); } -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/winfg_engine.cpp b/app/src/main/cpp/framegen/winfg/winfg_engine.cpp index c2737e261..965e15a15 100644 --- a/app/src/main/cpp/framegen/winfg/winfg_engine.cpp +++ b/app/src/main/cpp/framegen/winfg/winfg_engine.cpp @@ -1,4 +1,3 @@ -// See winfg_engine.h. #include "winfg_engine.h" #include "winfg_vkd.h" #include "log.hpp" @@ -8,11 +7,6 @@ namespace winfg { namespace { -// 2x: the guest layer synthesises at 0.35, not 0.5 - biased towards prev to -// cut the double-exposure ghost on HUD/text ("iterative bring-up knob" in -// layer.cpp). Kept for parity, since that is the tuning that was proven. -// 3x/4x: evenly spaced; never exercised by the guest layer, so this is the -// new ground native opens up and the first thing to judge by eye. float alphaFor(uint32_t k, uint32_t n) { if (n <= 1) return 0.35f; return (float)(k + 1) / (float)(n + 1); @@ -30,7 +24,7 @@ VkImageMemoryBarrier bar(VkImage img, VkImageLayout from, VkImageLayout to, return b; } -} // namespace +} Engine::Engine() = default; Engine::~Engine() { destroyRing(); fg_.destroy(); } @@ -65,8 +59,8 @@ bool Engine::prepare(uint32_t w, uint32_t h, VkFormat fmt) { if (unavailable_ || w == 0 || h == 0 || fmt == VK_FORMAT_UNDEFINED) return false; if (builtW_ == w && builtH_ == h && builtFmt_ == fmt && fg_.valid()) return true; - // onResize rebuilds the pyramids/scratch; our own input ring follows it. - if (!fg_.onResize(VkExtent2D{w, h}, fmt, /*force=*/false)) { + + if (!fg_.onResize(VkExtent2D{w, h}, fmt, false)) { WFG_LOGE("native: onResize %ux%u failed; frame generation unavailable", w, h); unavailable_ = true; return false; @@ -83,21 +77,17 @@ bool Engine::prepare(uint32_t w, uint32_t h, VkFormat fmt) { } uint32_t Engine::plan(uint32_t capacity) const { - if (!valid() || history_ < 2) return 0; // need prev AND curr + if (!valid() || history_ < 2) return 0; const uint32_t want = (uint32_t)std::max(cfg_.multiplier - 1, 0); return std::min({want, capacity, kMaxGenerations}); } void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t w, uint32_t h, - uint32_t /*generations*/) { + uint32_t ) { if (!valid()) return; - // Rotate: the slot we are about to overwrite is the OLD prev; after the - // copy it is the new curr and the other slot is prev. curr_ = 1 - curr_; Slot& dst = ring_[curr_]; - // source is GENERAL (composite render pass leaves it there); the slot may - // be in any layout from last use - UNDEFINED is legal as "discard". VkImageMemoryBarrier pre[2] = { bar(source, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT), @@ -115,8 +105,6 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t w, uint32_t h dd_.CmdCopyImage(cmd, source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - // source back to GENERAL for the real-frame copy that follows; the new - // curr becomes SHADER_READ_ONLY, which is what the chain samples it as. VkImageMemoryBarrier post[2] = { bar(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT), @@ -138,17 +126,11 @@ void Engine::generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t cou const Slot& curr = ring_[curr_]; if (!prev.valid || !curr.valid) return; - // The chain writes the output through a STORAGE descriptor in GENERAL. - // The composite ring already keeps its targets in GENERAL; make the - // prior contents' writes visible and take ownership for the compute write. VkImageMemoryBarrier toWrite = bar(targetImage, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_WRITE_BIT); dd_.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toWrite); - // gmSlot indexes the C1 global-motion SSBO read back on the CPU next - // frame; kGmSlots (3) >= our frames in flight (2), so rotating per source - // frame keeps a slot from being read while its reduce is still in flight. fg_.record(cmd, prev.view, curr.view, targetView, alphaFor(generation, count), gmSlot_); if (generation + 1 == count) gmSlot_ = (gmSlot_ + 1) % (uint32_t)FrameGen::kGmSlots; @@ -157,7 +139,6 @@ void Engine::generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t cou void Engine::reset() { history_ = 0; frames_ = 0; } -// ---- input ring -------------------------------------------------------------- uint32_t Engine::memType(uint32_t bits, VkMemoryPropertyFlags props) const { for (uint32_t i = 0; i < memProps_.memoryTypeCount; i++) @@ -201,4 +182,4 @@ void Engine::destroyRing() { history_ = 0; } -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/winfg_engine.h b/app/src/main/cpp/framegen/winfg/winfg_engine.h index 13675da99..9a607f531 100644 --- a/app/src/main/cpp/framegen/winfg/winfg_engine.h +++ b/app/src/main/cpp/framegen/winfg/winfg_engine.h @@ -1,28 +1,4 @@ #pragma once -// ============================================================================ -// winfg_engine - win-fg (Bannerlator's own frame generation) running inside -// the compositor, behind the same contract the LSFG engine uses: -// -// prepare(w, h, format) build/rebuild the chain for this size -// plan(capacity) how many frames to generate this time -// process(cmd, source, w, h, gens) take frame N in (becomes "curr") -// generateInto(cmd, k, n, ...) synthesise generated frame k of n -// -// Why this exists: as a guest-side Vulkan layer, win-fg's generated frames -// had to cross Wine -> DRI3 -> our X server -> the compositor, where the -// per-window latest-wins overwrite dropped them before they were ever -// presented - the HUD counted 2x, the panel got 1x. Here the chain runs in the -// compositor's own command stream and its frames are presented directly, on -// the same multi-present path that is device-proven for LSFG Native. -// -// Unlike LSFG this needs nothing from the user: the ten shaders are ours -// (MIT, see LICENSE.win-fg / NOTICE_FIDELITYFX_OPTICALFLOW.md) and embedded. -// -// The chain (framegen.cpp) is used unmodified. Its inputs are two sampled -// rgba8 views (prev, curr) and its output a GENERAL storage view; the guest -// layer transitions curr to SHADER_READ_ONLY before record() and the output -// UNDEFINED -> GENERAL, so we do exactly the same around the same call. -// ============================================================================ #include #include #include "framegen.hpp" @@ -32,8 +8,6 @@ struct VkTable; namespace winfg { -// Hard ceiling on generated frames per source frame (2x..4x -> 1..3), matching -// the LSFG engine and the drawer's buttons. constexpr uint32_t kMaxGenerations = 3; class Engine { @@ -48,22 +22,15 @@ public: bool valid() const { return fg_.valid() && !unavailable_; } bool unavailable() const { return unavailable_; } - // model 3/4, perf preset 0..2, flow scale - the same knobs the drawer - // already exposes for win-fg; they map straight onto winfg::Config. void configure(uint32_t multiplier, int model, int perfPreset, float flowScale); bool prepare(uint32_t width, uint32_t height, VkFormat format); - // 0 until there is a previous frame to interpolate from. uint32_t plan(uint32_t capacity) const; - // Copy the composited frame into the input ring as "curr" (the previous - // one becomes "prev"). `source` must be in GENERAL. void process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32_t height, uint32_t generations); - // Synthesise generated frame `generation` of `count` into the storage view. - // The target must be in GENERAL (the composite ring keeps it there). void generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t count, VkImage targetImage, VkImageView targetView, uint32_t width, uint32_t height); @@ -91,12 +58,12 @@ private: bool unavailable_ = false; Slot ring_[2]; - int curr_ = 0; // index of "curr" in ring_; prev is 1 - curr_ - uint32_t history_ = 0; // frames copied so far (need 2) - uint32_t gmSlot_ = 0; // rotates through FrameGen::kGmSlots + int curr_ = 0; + uint32_t history_ = 0; + uint32_t gmSlot_ = 0; uint32_t builtW_ = 0, builtH_ = 0; VkFormat builtFmt_ = VK_FORMAT_UNDEFINED; uint64_t frames_ = 0; }; -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/winfg_vkd.cpp b/app/src/main/cpp/framegen/winfg/winfg_vkd.cpp index d76f357c1..a0c2dd441 100644 --- a/app/src/main/cpp/framegen/winfg/winfg_vkd.cpp +++ b/app/src/main/cpp/framegen/winfg/winfg_vkd.cpp @@ -1,4 +1,3 @@ -// See winfg_vkd.h. #include "winfg_vkd.h" #include "../VulkanRendererContext.h" #include diff --git a/app/src/main/cpp/framegen/winfg/winfg_vkd.h b/app/src/main/cpp/framegen/winfg/winfg_vkd.h index 59d27bf2e..8faffc826 100644 --- a/app/src/main/cpp/framegen/winfg/winfg_vkd.h +++ b/app/src/main/cpp/framegen/winfg/winfg_vkd.h @@ -1,19 +1,4 @@ #pragma once -// ============================================================================ -// winfg_vkd - fill win-fg's own dispatch tables from the renderer's VkTable. -// -// win-fg's chain (framegen.cpp / record_impl.inc) was written as a Vulkan -// LAYER and calls through winfg::DeviceDispatch / InstanceDispatch. Inside the -// compositor there is no layer chain to resolve those from, so they are filled -// from the renderer's per-context table instead - the same bridge lsfg_vkd -// provides for the LSFG chain, for the same reason: an adrenotools driver -// shares no global symbols with the system loader. -// -// Only the entry points the chain actually calls are filled (36 device-level -// plus GetPhysicalDeviceMemoryProperties); everything else in the structs -// stays null and is never reached. winfgVkdInit reports a false if any of -// those failed to resolve, so a null is never called mid-frame. -// ============================================================================ #include "vk_dispatch.hpp" struct VkTable; bool winfgVkdInit(const VkTable& table, winfg::DeviceDispatch& dd, winfg::InstanceDispatch& id); diff --git a/app/src/main/cpp/scanout/ScanoutContext.h b/app/src/main/cpp/scanout/ScanoutContext.h index e14a82a9b..098136640 100644 --- a/app/src/main/cpp/scanout/ScanoutContext.h +++ b/app/src/main/cpp/scanout/ScanoutContext.h @@ -1,17 +1,5 @@ #pragma once -// Renderer-neutral direct-scanout implementation (Android SurfaceControl / -// SurfaceFlinger). Pure NDK: dlsym'd ASurfaceControl_* / ASurfaceTransaction_* -// plus AHardwareBuffer_*. It contains ZERO Vulkan/GL/EGL calls and ZERO -// render-thread coupling: the owning renderer drives it through the public -// surface below and decides WHEN the deferred cursor transaction is applied -// (the Vulkan path defers to its render loop; a GL path would apply inline). -// -// Extracted verbatim from VulkanRendererContext / VulkanRendererScanout.cpp -// (P0 of the GL direct-scanout plan). The only intended semantic change vs the -// old code is that the cursor render-thread signalling (needsRender / dirtyCV) -// stays in the owner: setCursorImage()/setCursorPos() return whether an apply -// became pending so the owner can signal its loop exactly as before. #include #include @@ -27,50 +15,34 @@ class ScanoutContext { public: - // --- API load / lifecycle --- bool loadApi(); - // Fallback path: creates the child SurfaceControls under the owner's own - // ANativeWindow. Passing a window updates the stored fallback window. void initFromWindow(ANativeWindow* win = nullptr); - // Primary path: wraps the two sibling game/cursor windows (handed in by the - // owner) as ASurfaceControls. Takes ownership and releases both windows. void initFromWindows(ANativeWindow* gameWin, ANativeWindow* cursorWin); void destroy(); - // --- per-frame game buffer (applies its transaction INLINE) --- void setBuffer(AHardwareBuffer* ahb, int x, int y, int w, int h, int fenceFd = -1); - // --- cursor: pure state-setters. They mutate state only and DO NOT signal - // any render thread. Each returns true if a deferred apply is now - // pending (i.e. the owner should arrange for applyPendingCursor()). bool setCursorImage(void* pixels, short w, short h, short stride); bool setCursorPos(short x, short y, short hotX, short hotY); - // Applies the pending cursor image/position transaction. Safe to call from - // any thread (a SurfaceControl transaction apply is thread-safe). The Vulkan - // owner calls this from its render loop; an inline owner may call it directly. void applyPendingCursor(); - // --- geometry / sizing fed by the owner --- void setDst(int x, int y, int w, int h); void setContainerSize(int w, int h) { containerWidth = w; containerHeight = h; } void setSurfaceSize(int w, int h) { surfaceWidth = w; surfaceHeight = h; } void setFallbackWindow(ANativeWindow* win) { window = win; } void setVerboseLog(bool v) { verboseLog = v; } - // --- queries --- bool isActive() const { return scanoutActive.load(); } bool isGameFrameDelivered() const { return gameFrameDelivered.load(std::memory_order_acquire); } void* debugGameSC() const { return scanoutGameSC; } - // Public so the owner can expose them through its existing ABI without a - // second source of truth (Vulkan binds references to these). std::atomic scanoutActive{false}; std::atomic gameFrameDelivered{false}; private: - ANativeWindow* window = nullptr; // owner's own window, for the fallback path + ANativeWindow* window = nullptr; bool verboseLog = true; int surfaceWidth = 0, surfaceHeight = 0, containerWidth = 0, containerHeight = 0; diff --git a/app/src/main/cpp/sdnative/sdnative.c b/app/src/main/cpp/sdnative/sdnative.c index aa928b856..c7ec580ab 100644 --- a/app/src/main/cpp/sdnative/sdnative.c +++ b/app/src/main/cpp/sdnative/sdnative.c @@ -1,11 +1,3 @@ -/* - * One function: the release fence the fake-input ring writer needs. - * - * FakeInputWriter publishes events into a shared-memory ring that libfakeinput.so reads from - * another process. Java's memory model says nothing about what a foreign reader sees, so the - * writer calls this between filling the events and advancing the sequence counter: everything - * written before it is visible to the reader before the counter moves. - */ #include #include diff --git a/app/src/main/cpp/shaders/cas_frag.h b/app/src/main/cpp/shaders/cas_frag.h index 21347e38d..103c2cce4 100644 --- a/app/src/main/cpp/shaders/cas_frag.h +++ b/app/src/main/cpp/shaders/cas_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t cas_code[] = { 0x07230203,0x00010000,0x0008000b,0x000000f3,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/color_frag.h b/app/src/main/cpp/shaders/color_frag.h index bde543ab4..7026c7c88 100644 --- a/app/src/main/cpp/shaders/color_frag.h +++ b/app/src/main/cpp/shaders/color_frag.h @@ -1,4 +1,3 @@ - // 1116.5.0 #pragma once const uint32_t color_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000059,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/crt_frag.h b/app/src/main/cpp/shaders/crt_frag.h index 2239f2274..e814ea7f7 100644 --- a/app/src/main/cpp/shaders/crt_frag.h +++ b/app/src/main/cpp/shaders/crt_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t crt_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000063,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/deband_frag.h b/app/src/main/cpp/shaders/deband_frag.h index 4a59f2135..dab0cba93 100644 --- a/app/src/main/cpp/shaders/deband_frag.h +++ b/app/src/main/cpp/shaders/deband_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t deband_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000053,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/fsr_easu_frag.h b/app/src/main/cpp/shaders/fsr_easu_frag.h index eb6d36af4..b2d125298 100644 --- a/app/src/main/cpp/shaders/fsr_easu_frag.h +++ b/app/src/main/cpp/shaders/fsr_easu_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t fsr_easu_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000046e,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/fsr_rcas_frag.h b/app/src/main/cpp/shaders/fsr_rcas_frag.h index 066d125dd..f241d7590 100644 --- a/app/src/main/cpp/shaders/fsr_rcas_frag.h +++ b/app/src/main/cpp/shaders/fsr_rcas_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t fsr_rcas_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000025e,0x00000000,0x00020011,0x00000001,0x00020011, diff --git a/app/src/main/cpp/shaders/fxaa_frag.h b/app/src/main/cpp/shaders/fxaa_frag.h index 6b7be374b..8f22155f7 100644 --- a/app/src/main/cpp/shaders/fxaa_frag.h +++ b/app/src/main/cpp/shaders/fxaa_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t fxaa_code[] = { 0x07230203,0x00010000,0x0008000b,0x000000e4,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/hdr_frag.h b/app/src/main/cpp/shaders/hdr_frag.h index 37e5783aa..4bcdfad3a 100644 --- a/app/src/main/cpp/shaders/hdr_frag.h +++ b/app/src/main/cpp/shaders/hdr_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t hdr_code[] = { 0x07230203,0x00010000,0x0008000b,0x000000f4,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/nis_frag.h b/app/src/main/cpp/shaders/nis_frag.h index 468283d7f..b20294eae 100644 --- a/app/src/main/cpp/shaders/nis_frag.h +++ b/app/src/main/cpp/shaders/nis_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t nis_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000644,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/ntsc_frag.h b/app/src/main/cpp/shaders/ntsc_frag.h index 9eb75f218..409c9d90f 100644 --- a/app/src/main/cpp/shaders/ntsc_frag.h +++ b/app/src/main/cpp/shaders/ntsc_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t ntsc_code[] = { 0x07230203,0x00010000,0x0008000b,0x000000d5,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/sgsr_frag.h b/app/src/main/cpp/shaders/sgsr_frag.h index 2452fb68f..11f19152e 100644 --- a/app/src/main/cpp/shaders/sgsr_frag.h +++ b/app/src/main/cpp/shaders/sgsr_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t sgsr_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000216,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/sgsr_quality_frag.h b/app/src/main/cpp/shaders/sgsr_quality_frag.h index ca44e6a6a..34b629f2d 100644 --- a/app/src/main/cpp/shaders/sgsr_quality_frag.h +++ b/app/src/main/cpp/shaders/sgsr_quality_frag.h @@ -1,4 +1,3 @@ - // 1116.5.0 #pragma once const uint32_t sgsr_quality_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000269,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/toon_frag.h b/app/src/main/cpp/shaders/toon_frag.h index aa54788ff..615ebdaee 100644 --- a/app/src/main/cpp/shaders/toon_frag.h +++ b/app/src/main/cpp/shaders/toon_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t toon_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000006d,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/upscale_vert.h b/app/src/main/cpp/shaders/upscale_vert.h index c64f91729..61f678288 100644 --- a/app/src/main/cpp/shaders/upscale_vert.h +++ b/app/src/main/cpp/shaders/upscale_vert.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t upscale_vert_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000004b,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/termux/termux.c b/app/src/main/cpp/termux/termux.c index 7d4f86e31..5365e95bc 100644 --- a/app/src/main/cpp/termux/termux.c +++ b/app/src/main/cpp/termux/termux.c @@ -1,4 +1,3 @@ -// Upstream: termux/termux-app v0.118.0, terminal-emulator/src/main/jni/termux.c. // Licensed under the repository's GPL-3.0 license (see LICENSE). #include #include @@ -51,14 +50,12 @@ static int create_subprocess(JNIEnv* env, return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx"); } - // Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display. struct termios tios; tcgetattr(ptm, &tios); tios.c_iflag |= IUTF8; tios.c_iflag &= ~(IXON | IXOFF); tcsetattr(ptm, TCSANOW, &tios); - /** Set initial winsize. */ struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns }; ioctl(ptm, TIOCSWINSZ, &sz); @@ -69,7 +66,6 @@ static int create_subprocess(JNIEnv* env, *pProcessId = (int) pid; return ptm; } else { - // Clear signals which the Android java process may have blocked: sigset_t signals_to_unblock; sigfillset(&signals_to_unblock); sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0); @@ -100,13 +96,11 @@ static int create_subprocess(JNIEnv* env, if (chdir(cwd) != 0) { char* error_message; - // No need to free asprintf()-allocated memory since doing execvp() or exit() below. if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()"; perror(error_message); fflush(stderr); } execvp(cmd, argv); - // Show terminal output about failing exec() call: char* error_message; if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()"; perror(error_message); @@ -207,7 +201,6 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSE } else if (WIFSIGNALED(status)) { return -WTERMSIG(status); } else { - // Should never happen - waitpid(2) says "One of the first three macros will evaluate to a non-zero (true) value". return 0; } } diff --git a/app/src/main/cpp/thirdparty/dxbc/.gitignore b/app/src/main/cpp/thirdparty/dxbc/.gitignore index 4522fe54d..bc9544853 100644 --- a/app/src/main/cpp/thirdparty/dxbc/.gitignore +++ b/app/src/main/cpp/thirdparty/dxbc/.gitignore @@ -1,3 +1,2 @@ -# cmake files .cache/ build/ diff --git a/app/src/main/cpp/thirdparty/dxbc/CMakeLists.txt b/app/src/main/cpp/thirdparty/dxbc/CMakeLists.txt index 2651d7b8d..43422465e 100644 --- a/app/src/main/cpp/thirdparty/dxbc/CMakeLists.txt +++ b/app/src/main/cpp/thirdparty/dxbc/CMakeLists.txt @@ -13,7 +13,6 @@ file(GLOB SOURCES add_library(dxbc STATIC ${SOURCES}) -# target set_target_properties(dxbc PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON) diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_analysis.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_analysis.h index 6c8b0325b..87113c1dc 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_analysis.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_analysis.h @@ -9,13 +9,6 @@ namespace dxvk { - /** - * \brief Info about unordered access views - * - * Stores whether an UAV is accessed with typed - * read or atomic instructions. This information - * will be used to generate image types. - */ struct DxbcUavInfo { bool accessTypedLoad = false; bool accessAtomicOp = false; @@ -25,27 +18,15 @@ namespace dxvk { VkAccessFlags accessFlags = 0; }; - /** - * \brief Info about shader resource views - * - * Stores whether an SRV is accessed with - * sparse feedback. Useful for buffers. - */ struct DxbcSrvInfo { bool sparseFeedback = false; }; - /** - * \brief Counts cull and clip distances - */ struct DxbcClipCullInfo { uint32_t numClipPlanes = 0; uint32_t numCullPlanes = 0; }; - /** - * \brief Shader analysis info - */ struct DxbcAnalysisInfo { std::array uavInfos; std::array srvInfos; @@ -60,13 +41,6 @@ namespace dxvk { bool usesKill = false; }; - /** - * \brief DXBC shader analysis pass - * - * Collects information about the shader itself - * and the resources used by the shader, which - * will later be used by the actual compiler. - */ class DxbcAnalyzer { public: @@ -81,10 +55,6 @@ namespace dxvk { ~DxbcAnalyzer(); - /** - * \brief Processes a single instruction - * \param [in] ins The instruction - */ void processInstruction( const DxbcShaderInstruction& ins); diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_isgn.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_isgn.h index 9b4b79186..da3c97d68 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_isgn.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_isgn.h @@ -9,12 +9,6 @@ namespace dxvk { - /** - * \brief Signature entry - * - * Stores the semantic name of an input or - * output and the corresponding register. - */ struct DxbcSgnEntry { std::string semanticName; uint32_t semanticIndex; @@ -26,12 +20,6 @@ namespace dxvk { uint32_t streamId; }; - /** - * \brief Input/Output signature chunk - * - * Stores information about the input and - * output registers used by the shader stage. - */ class DxbcIsgn : public RcObject { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_shex.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_shex.h index 8deecc867..02b3cd914 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_shex.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_shex.h @@ -6,13 +6,6 @@ namespace dxvk { - /** - * \brief Shader code chunk - * - * Stores the DXBC shader code itself, as well - * as some meta info about the shader, i.e. what - * type of shader this is. - */ class DxbcShex : public RcObject { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_common.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_common.h index d9d420767..4a382c8c4 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_common.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_common.h @@ -4,12 +4,6 @@ namespace dxvk { - /** - * \brief DXBC Program type - * - * Defines the shader stage that a DXBC - * module has been compiled form. - */ enum class DxbcProgramType : uint16_t { PixelShader = 0, VertexShader = 1, @@ -24,11 +18,6 @@ namespace dxvk { using DxbcProgramTypeFlags = Flags; - /** - * \brief DXBC shader info - * - * Stores the shader program type. - */ class DxbcProgramInfo { public: @@ -37,30 +26,12 @@ namespace dxvk { DxbcProgramInfo(DxbcProgramType type) : m_type(type) { } - /** - * \brief Program type - * \returns Program type - */ DxbcProgramType type() const { return m_type; } - /** - * \brief Vulkan shader stage - * - * The \c VkShaderStageFlagBits constant - * that corresponds to the program type. - * \returns Vulkan shaer stage - */ VkShaderStageFlagBits shaderStage() const; - /** - * \brief SPIR-V execution model - * - * The execution model that corresponds - * to the Vulkan shader stage. - * \returns SPIR-V execution model - */ spv::ExecutionModel executionModel() const; private: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_compiler.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_compiler.h index f63d1e11a..d5c699885 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_compiler.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_compiler.h @@ -16,27 +16,12 @@ namespace dxvk { - /** - * \brief Vector type - * - * Convenience struct that stores a scalar - * type and a component count. The compiler - * can use this to generate SPIR-V types. - */ struct DxbcVectorType { DxbcScalarType ctype; uint32_t ccount; }; - /** - * \brief Array type - * - * Convenience struct that stores a scalar type, a - * component count and an array size. An array of - * length 0 will be evaluated to a vector type. The - * compiler can use this to generate SPIR-V types. - */ struct DxbcArrayType { DxbcScalarType ctype; uint32_t ccount; @@ -44,39 +29,18 @@ namespace dxvk { }; - /** - * \brief Register info - * - * Stores the array type of a register and - * its storage class. The compiler can use - * this to generate SPIR-V pointer types. - */ struct DxbcRegisterInfo { DxbcArrayType type; spv::StorageClass sclass; }; - /** - * \brief Register value - * - * Stores a vector type and a SPIR-V ID that - * represents an intermediate value. This is - * used to track the type of such values. - */ struct DxbcRegisterValue { DxbcVectorType type; uint32_t id; }; - /** - * \brief Register pointer - * - * Stores a vector type and a SPIR-V ID that - * represents a pointer to such a vector. This - * can be used to load registers conveniently. - */ struct DxbcRegisterPointer { DxbcVectorType type; uint32_t id; @@ -98,12 +62,6 @@ namespace dxvk { }; - /** - * \brief Specialization constant properties - * - * Stores the name, data type and initial - * value of a specialization constant. - */ struct DxbcSpecConstant { DxbcScalarType ctype; uint32_t ccount; @@ -112,13 +70,6 @@ namespace dxvk { }; - /** - * \brief Helper struct for conditional execution - * - * Stores a set of labels required to implement either - * an if-then block or an if-then-else block. This is - * not used to implement control flow instructions. - */ struct DxbcConditional { uint32_t labelIf = 0; uint32_t labelElse = 0; @@ -144,9 +95,6 @@ namespace dxvk { }; - /** - * \brief Vertex shader-specific structure - */ struct DxbcCompilerVsPart { uint32_t functionId = 0; @@ -157,9 +105,6 @@ namespace dxvk { }; - /** - * \brief Geometry shader-specific structure - */ struct DxbcCompilerGsPart { DxbcPrimitive inputPrimitive = DxbcPrimitive::Undefined; DxbcPrimitiveTopology outputTopology = DxbcPrimitiveTopology::Undefined; @@ -175,9 +120,6 @@ namespace dxvk { }; - /** - * \brief Pixel shader-specific structure - */ struct DxbcCompilerPsPart { uint32_t functionId = 0; @@ -196,9 +138,6 @@ namespace dxvk { }; - /** - * \brief Compute shader-specific structure - */ struct DxbcCompilerCsPart { uint32_t functionId = 0; @@ -213,12 +152,6 @@ namespace dxvk { }; - /** - * \brief Hull shader fork/join phase - * - * Defines a function and built-in variables - * for a single fork or join phase sub-program. - */ struct DxbcCompilerHsForkJoinPhase { uint32_t functionId = 0; uint32_t instanceCount = 1; @@ -228,35 +161,20 @@ namespace dxvk { }; - /** - * \brief Hull shader control point phase - * - * Defines the function for the control - * point phase program of a hull shader. - */ struct DxbcCompilerHsControlPointPhase { uint32_t functionId = 0; }; - /** - * \brief Hull shader phase - * - * Used to identify the current - * phase and function ID. - */ enum class DxbcCompilerHsPhase : uint32_t { - None, ///< No active phase - Decl, ///< \c hs_decls - ControlPoint, ///< \c hs_control_point_phase - Fork, ///< \c hs_fork_phase - Join, ///< \c hs_join_phase + None, + Decl, + ControlPoint, + Fork, + Join, }; - /** - * \brief Hull shader-specific structure - */ struct DxbcCompilerHsPart { DxbcCompilerHsPhase currPhaseType = DxbcCompilerHsPhase::None; size_t currPhaseId = 0; @@ -284,9 +202,6 @@ namespace dxvk { }; - /** - * \brief Domain shader-specific structure - */ struct DxbcCompilerDsPart { uint32_t functionId = 0; @@ -362,13 +277,6 @@ namespace dxvk { }; - /** - * \brief DXBC to SPIR-V shader compiler - * - * Processes instructions from a DXBC shader and creates - * a DXVK shader object, which contains the SPIR-V module - * and information about the shader resource bindings. - */ class DxbcCompiler { public: @@ -383,35 +291,13 @@ namespace dxvk { const DxbcAnalysisInfo& analysis); ~DxbcCompiler(); - /** - * \brief Processes a single instruction - * \param [in] ins The instruction - */ void processInstruction( const DxbcShaderInstruction& ins); - /** - * \brief Emits transform feedback passthrough - * - * Writes all captured input variables to the - * corresponding xfb outputs, and sets up the - * geometry shader for point-to-point mode. - */ void processXfbPassthrough(); - /** - * \brief Finalizes the shader - * \returns The final shader object - */ SpirvCodeBuffer finalize(); - /** - * \brief Extracts immediate constant buffer data - * - * Only defined if the ICB needs to be backed by a - * uniform buffer. - * \returns Immediate constant buffer data - */ std::vector getIcbData() const { return std::move(m_icbData); } @@ -428,49 +314,27 @@ namespace dxvk { const DxbcAnalysisInfo* m_analysis; - /////////////////////////////////////////////////////// - // Resource slot description for the shader. This will - // be used to map D3D11 bindings to DXVK bindings. std::vector m_bindings; - //////////////////////////////////////////////// - // Temporary r# vector registers with immediate - // indexing, and x# vector array registers. std::vector m_rRegs; std::vector m_xRegs; - ///////////////////////////////////////////// - // Thread group shared memory (g#) registers std::vector m_gRegs; - /////////////////////////////////////////////////////////// - // v# registers as defined by the shader. The type of each - // of these inputs is either float4 or an array of float4. std::array< DxbcRegisterPointer, DxbcMaxInterfaceRegs> m_vRegs; std::vector m_vMappings; - ////////////////////////////////////////////////////////// - // o# registers as defined by the shader. In the fragment - // shader stage, these registers are typed by the signature, - // in all other stages, they are float4 registers or arrays. std::array< DxbcRegisterPointer, DxbcMaxInterfaceRegs> m_oRegs; std::vector m_oMappings; - ///////////////////////////////////////////// - // xfb output registers for geometry shaders std::vector m_xfbVars; - ///////////////////////////////////////////// - // Dynamically indexed input and output regs std::vector m_indexRanges = { }; - ////////////////////////////////////////////////////// - // Shader resource variables. These provide access to - // constant buffers, samplers, textures, and UAVs. std::array m_constantBuffers; std::array m_samplers; std::array m_textures; @@ -479,9 +343,6 @@ namespace dxvk { bool m_hasGloballyCoherentUav = false; bool m_hasRasterizerOrderedUav = false; - /////////////////////////////////////////////// - // Control flow information. Stores labels for - // currently active if-else blocks and loops. std::vector m_controlFlowBlocks; bool m_topLevelIsUniform = true; @@ -489,22 +350,13 @@ namespace dxvk { uint64_t m_uavRdMask = 0u; uint64_t m_uavWrMask = 0u; - ////////////////////////////////////////////// - // Function state tracking. Required in order - // to properly end functions in some cases. bool m_insideFunction = false; - /////////////////////////////////////////////////////////// - // Array of input values. Since v# registers are indexable - // in DXBC, we need to copy them into an array first. uint32_t m_vArrayLength = 0; uint32_t m_vArrayLengthId = 0; uint32_t m_vArray = 0; - //////////////////////////////////////////////////// - // Per-vertex input and output blocks. Depending on - // the shader stage, these may be declared as arrays. uint32_t m_positionIn = 0; uint32_t m_positionOut = 0; @@ -514,43 +366,25 @@ namespace dxvk { uint32_t m_primitiveIdIn = 0; uint32_t m_primitiveIdOut = 0; - ////////////////////////////////////////////////// - // Immediate constant buffer. If defined, this is - // an array of four-component uint32 vectors. uint32_t m_icbArray = 0; std::vector m_icbData; uint32_t m_icbComponents = 0u; uint32_t m_icbSize = 0u; - /////////////////////////////////////////////////// - // Sample pos array. If defined, this iis an array - // of 32 four-component float vectors. uint32_t m_samplePositions = 0; - //////////////////////////////////////////// - // Struct type used for UAV counter buffers uint32_t m_uavCtrStructType = 0; uint32_t m_uavCtrPointerType = 0; - //////////////////////////////// - // Function IDs for subroutines std::unordered_map m_subroutines; - /////////////////////////////////////////////////// - // Entry point description - we'll need to declare - // the function ID and all input/output variables. uint32_t m_entryPointId = 0; bool m_hasRawAccessChains = false; - //////////////////////////////////////////// - // Inter-stage shader interface slots. Also - // covers vertex input and fragment output. uint32_t m_inputMask = 0u; uint32_t m_outputMask = 0u; - /////////////////////////////////// - // Shader-specific data structures DxbcCompilerVsPart m_vs; DxbcCompilerHsPart m_hs; DxbcCompilerDsPart m_ds; @@ -558,8 +392,6 @@ namespace dxvk { DxbcCompilerPsPart m_ps; DxbcCompilerCsPart m_cs; - ////////////////////// - // Global state stuff bool m_precise = true; DxbcOpcode m_lastOp = DxbcOpcode::Nop; @@ -568,8 +400,6 @@ namespace dxvk { VkPrimitiveTopology m_inputTopology = VK_PRIMITIVE_TOPOLOGY_MAX_ENUM; VkPrimitiveTopology m_outputTopology = VK_PRIMITIVE_TOPOLOGY_MAX_ENUM; - ///////////////////////////////////////////////////// - // Shader interface and metadata declaration methods void emitDcl( const DxbcShaderInstruction& ins); @@ -662,8 +492,6 @@ namespace dxvk { uint32_t emitDclUavCounter( uint32_t regId); - //////////////////////// - // Custom data handlers void emitDclImmediateConstantBuffer( const DxbcShaderInstruction& ins); @@ -680,8 +508,6 @@ namespace dxvk { void emitCustomData( const DxbcShaderInstruction& ins); - ////////////////////////////// - // Instruction class handlers void emitVectorAlu( const DxbcShaderInstruction& ins); @@ -787,8 +613,6 @@ namespace dxvk { void emitTypedUavStore( const DxbcShaderInstruction& ins); - ///////////////////////////////////// - // Control flow instruction handlers void emitControlFlowIf( const DxbcShaderInstruction& ins); @@ -843,10 +667,6 @@ namespace dxvk { void emitControlFlow( const DxbcShaderInstruction& ins); - //////////////////////////////////////////////// - // Constant building methods. These are used to - // generate constant vectors that store the same - // value in each component. DxbcRegisterValue emitBuildConstVecf32( float x, float y, @@ -880,8 +700,6 @@ namespace dxvk { DxbcRegisterValue emitBuildZeroVector( DxbcVectorType type); - ///////////////////////////////////////// - // Generic register manipulation methods DxbcRegisterValue emitRegisterBitcast( DxbcRegisterValue srcValue, DxbcScalarType dstType); @@ -930,8 +748,6 @@ namespace dxvk { DxbcRegisterValue value, DxbcOpModifiers modifiers); - /////////////////////////// - // Sparse feedback methods uint32_t emitExtractSparseTexel( uint32_t texelTypeId, uint32_t resultId); @@ -940,22 +756,16 @@ namespace dxvk { const DxbcRegister& feedbackRegister, uint32_t resultId); - //////////////////////////////// - // Pointer manipulation methods DxbcRegisterPointer emitArrayAccess( DxbcRegisterPointer pointer, spv::StorageClass sclass, uint32_t index); - /////////////////////////////////////// - // Image register manipulation methods uint32_t emitLoadSampledImage( const DxbcShaderResource& textureResource, const DxbcSampler& samplerResource, bool isDepthCompare); - //////////////////////// - // Address load methods DxbcRegisterPointer emitGetTempPtr( const DxbcRegister& operand); @@ -981,8 +791,6 @@ namespace dxvk { const DxbcRegister& operand, const DxbcRegister& address); - ////////////////////////// - // Resource query methods DxbcRegisterValue emitQueryBufferSize( const DxbcRegister& resource); @@ -999,8 +807,6 @@ namespace dxvk { const DxbcRegister& resource, DxbcRegisterValue lod); - //////////////////////////////////// - // Buffer index calculation methods DxbcRegisterValue emitCalcBufferIndexStructured( DxbcRegisterValue structId, DxbcRegisterValue structOffset, @@ -1017,8 +823,6 @@ namespace dxvk { const DxbcRegister& coordReg, const DxbcImageInfo& imageInfo); - ////////////////////////////// - // Operand load/store methods DxbcRegisterValue emitIndexLoad( DxbcRegIndex index); @@ -1045,8 +849,6 @@ namespace dxvk { const DxbcRegister& reg, DxbcRegisterValue value); - //////////////////////////// - // Input/output preparation void emitInputSetup(); void emitInputSetup(uint32_t vertexCount); @@ -1055,8 +857,6 @@ namespace dxvk { void emitInitWorkgroupMemory(); - ////////////////////////////////////////// - // System value load methods (per shader) DxbcRegisterValue emitVsSystemValueLoad( DxbcSystemValue sv, DxbcRegMask mask); @@ -1070,8 +870,6 @@ namespace dxvk { DxbcSystemValue sv, DxbcRegMask mask); - /////////////////////////////////////////// - // System value store methods (per shader) void emitVsSystemValueStore( DxbcSystemValue sv, DxbcRegMask mask, @@ -1097,8 +895,6 @@ namespace dxvk { DxbcRegMask mask, const DxbcRegisterValue& value); - /////////////////////////////// - // Special system value stores void emitClipCullStore( DxbcSystemValue sv, uint32_t dstArray); @@ -1109,8 +905,6 @@ namespace dxvk { void emitPointSizeStore(); - ////////////////////////////////////// - // Common function definition methods void emitInit(); void emitFunctionBegin( @@ -1124,8 +918,6 @@ namespace dxvk { void emitMainFunctionBegin(); - ///////////////////////////////// - // Shader initialization methods void emitVsInit(); void emitHsInit(); void emitDsInit(); @@ -1133,8 +925,6 @@ namespace dxvk { void emitPsInit(); void emitCsInit(); - /////////////////////////////// - // Shader finalization methods void emitVsFinalize(); void emitHsFinalize(); void emitDsFinalize(); @@ -1142,16 +932,12 @@ namespace dxvk { void emitPsFinalize(); void emitCsFinalize(); - /////////////////////// - // Xfb related methods void emitXfbOutputDeclarations(); void emitXfbOutputSetup( uint32_t streamId, bool passthrough); - /////////////////////////////// - // Hull shader phase methods void emitHsControlPointPhase( const DxbcCompilerHsControlPointPhase& phase); @@ -1174,8 +960,6 @@ namespace dxvk { spv::StorageClass storageClass, uint32_t vertexCount); - ////////////// - // Misc stuff void emitDclInputArray( uint32_t vertexCount); @@ -1194,8 +978,6 @@ namespace dxvk { void emitFloatControl(); - /////////////////////////////// - // Variable definition methods uint32_t emitNewVariable( const DxbcRegisterInfo& info); @@ -1212,8 +994,6 @@ namespace dxvk { uint32_t emitPushConstants(); - //////////////// - // Misc methods DxbcCfgBlock* cfgFindBlock( const std::initializer_list& types); @@ -1265,8 +1045,6 @@ namespace dxvk { uint64_t readMask, uint64_t writeMask); - /////////////////////////// - // Type definition methods uint32_t getScalarTypeId( DxbcScalarType type); diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_decoder.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_decoder.h index 771837152..4ad3f82c8 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_decoder.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_decoder.h @@ -14,12 +14,6 @@ namespace dxvk { struct DxbcRegister; - /** - * \brief Source operand modifiers - * - * These are applied after loading - * an operand register. - */ enum class DxbcRegModifier : uint32_t { Neg = 0, Abs = 1, @@ -28,32 +22,17 @@ namespace dxvk { using DxbcRegModifiers = Flags; - /** - * \brief Constant buffer binding - * - * Stores information required to - * access a constant buffer. - */ struct DxbcConstantBuffer { uint32_t varId = 0; uint32_t size = 0; }; - /** - * \brief Sampler binding - * - * Stores a sampler variable that can be - * used together with a texture resource. - */ struct DxbcSampler { uint32_t varId = 0; uint32_t typeId = 0; }; - /** - * \brief Image type information - */ struct DxbcImageInfo { spv::Dim dim = spv::Dim1D; uint32_t array = 0; @@ -63,12 +42,6 @@ namespace dxvk { }; - /** - * \brief Shader resource binding - * - * Stores a resource variable - * and associated type IDs. - */ struct DxbcShaderResource { DxbcResourceType type = DxbcResourceType::Typed; DxbcImageInfo imageInfo; @@ -84,12 +57,6 @@ namespace dxvk { }; - /** - * \brief Unordered access binding - * - * Stores a resource variable that is provided - * by a UAV, as well as associated type IDs. - */ struct DxbcUav { DxbcResourceType type = DxbcResourceType::Typed; DxbcImageInfo imageInfo; @@ -105,12 +72,6 @@ namespace dxvk { }; - /** - * \brief Component swizzle - * - * Maps vector components to - * other vector components. - */ class DxbcRegSwizzle { public: @@ -133,12 +94,6 @@ namespace dxvk { }; - /** - * \brief Component mask - * - * Enables access to certain - * subset of vector components. - */ class DxbcRegMask { public: @@ -211,12 +166,6 @@ namespace dxvk { }; - /** - * \brief System value mapping - * - * Maps a system value to a given set of - * components of an input or output register. - */ struct DxbcSvMapping { uint32_t regId; DxbcRegMask regMask; @@ -230,9 +179,6 @@ namespace dxvk { }; - /** - * \brief Instruction operand - */ struct DxbcRegister { DxbcOperandType type; DxbcScalarType dataType; @@ -252,24 +198,12 @@ namespace dxvk { }; - /** - * \brief Instruction result modifiers - * - * Modifiers that are applied - * to all destination operands. - */ struct DxbcOpModifiers { bool saturate; bool precise; }; - /** - * \brief Opcode controls - * - * Instruction-specific controls. Usually, - * only one of the members will be valid. - */ class DxbcShaderOpcodeControls { public: @@ -355,24 +289,11 @@ namespace dxvk { }; - /** - * \brief Sample controls - * - * Constant texel offset with - * values raning from -8 to 7. - */ struct DxbcShaderSampleControls { int u, v, w; }; - /** - * \brief Immediate value - * - * Immediate argument represented either - * as a 32-bit or 64-bit unsigned integer, - * or a 32-bit or 32-bit floating point number. - */ union DxbcImmediate { float f32; double f64; @@ -381,17 +302,6 @@ namespace dxvk { }; - /** - * \brief Shader instruction - * - * Note that this structure may store pointer to - * external structures, such as the original code - * buffer. This is safe to use if and only if: - * - The \ref DxbcDecodeContext that created it - * still exists and was not moved - * - The code buffer that was being decoded - * still exists and was not moved. - */ struct DxbcShaderInstruction { DxbcOpcode op; DxbcInstClass opClass; @@ -413,12 +323,6 @@ namespace dxvk { }; - /** - * \brief DXBC code slice - * - * Convenient pointer pair that allows - * reading the code word stream safely. - */ class DxbcCodeSlice { public: @@ -448,34 +352,14 @@ namespace dxvk { }; - /** - * \brief Decode context - * - * Stores data that is required to decode a single - * instruction. This data is not persistent, so it - * should be forwarded to the compiler right away. - */ class DxbcDecodeContext { public: - /** - * \brief Retrieves current instruction - * - * This is only valid after a call to \ref decode. - * \returns Reference to last decoded instruction - */ const DxbcShaderInstruction& getInstruction() const { return m_instruction; } - /** - * \brief Decodes an instruction - * - * This also advances the given code slice by the - * number of dwords consumed by the instruction. - * \param [in] code Code slice - */ void decodeInstruction(DxbcCodeSlice& code); private: @@ -487,8 +371,6 @@ namespace dxvk { std::array m_immOperands; std::array m_indices; - // Index into the indices array. Used when decoding - // instruction operands with relative indexing. uint32_t m_indexId = 0; void decodeCustomData(DxbcCodeSlice code); diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_defs.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_defs.h index 8782481af..a0792aaa0 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_defs.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_defs.h @@ -7,98 +7,66 @@ namespace dxvk { constexpr size_t DxbcMaxInterfaceRegs = 32; constexpr size_t DxbcMaxOperandCount = 8; - /** - * \brief Operand kind - * - * In the instruction format definition, this specified - * whether an operand uses an actual operand token, or - * whether it is stored as an immediate value. - */ enum class DxbcOperandKind { - DstReg, ///< Destination register - SrcReg, ///< Source register - Imm32, ///< Constant number + DstReg, + SrcReg, + Imm32, }; - /** - * \brief Instruction class - * - * Instructions with a similar format are grouped into - * instruction classes in order to make implementing - * new instructions easier. - */ enum class DxbcInstClass { - Declaration, ///< Interface or resource declaration - CustomData, ///< Immediate constant buffer - ControlFlow, ///< Control flow instructions - GeometryEmit, ///< Special geometry shader instructions - Atomic, ///< Atomic operations - AtomicCounter, ///< Atomic counter operations - Barrier, ///< Execution or memory barrier - BitExtract, ///< Bit field extract operations - BitInsert, ///< Bit field insert operations - BitScan, ///< Bit scan operations - BufferQuery, ///< Buffer query instruction - BufferLoad, ///< Structured or raw buffer load - BufferStore, ///< Structured or raw buffer store - ConvertFloat16, ///< 16-bit float packing/unpacking - ConvertFloat64, ///< 64-bit float conversion - HullShaderPhase, ///< Hull shader phase declaration - HullShaderInstCnt, ///< Hull shader phase instance count - Interpolate, ///< Input attribute interpolation - NoOperation, ///< The most useful instruction class - SparseCheckAccess, ///< Verifies sparse resource access - TextureQuery, ///< Texture query instruction - TextureQueryLod, ///< Texture LOD query instruction - TextureQueryMs, ///< Multisample texture query - TextureQueryMsPos, ///< Sample position query - TextureFetch, ///< Texture fetch instruction - TextureGather, ///< Texture gather instruction - TextureSample, ///< Texture sampling instruction - TypedUavLoad, ///< Typed UAV load - TypedUavStore, ///< Typed UAV store - VectorAlu, ///< Component-wise vector instructions - VectorCmov, ///< Component-wise conditional move - VectorCmp, ///< Component-wise vector comparison - VectorDeriv, ///< Vector derivatives - VectorDot, ///< Dot product instruction - VectorIdiv, ///< Component-wise integer division - VectorImul, ///< Component-wise integer multiplication - VectorMsad, ///< Component-wise sum of absolute difference - VectorShift, ///< Bit shift operations on vectors - VectorSinCos, ///< Sine and Cosine instruction - Undefined, ///< Instruction code not defined + Declaration, + CustomData, + ControlFlow, + GeometryEmit, + Atomic, + AtomicCounter, + Barrier, + BitExtract, + BitInsert, + BitScan, + BufferQuery, + BufferLoad, + BufferStore, + ConvertFloat16, + ConvertFloat64, + HullShaderPhase, + HullShaderInstCnt, + Interpolate, + NoOperation, + SparseCheckAccess, + TextureQuery, + TextureQueryLod, + TextureQueryMs, + TextureQueryMsPos, + TextureFetch, + TextureGather, + TextureSample, + TypedUavLoad, + TypedUavStore, + VectorAlu, + VectorCmov, + VectorCmp, + VectorDeriv, + VectorDot, + VectorIdiv, + VectorImul, + VectorMsad, + VectorShift, + VectorSinCos, + Undefined, }; - /** - * \brief Instruction operand format - * - * Stores the kind and the expected data type - * of an operand. Used when parsing instructions. - */ struct DxbcInstOperandFormat { DxbcOperandKind kind; DxbcScalarType type; }; - /** - * \brief Instruction format - * - * Defines the instruction class as well as - * the format of the insttruction operands. - */ struct DxbcInstFormat { uint32_t operandCount = 0; DxbcInstClass instructionClass = DxbcInstClass::Undefined; DxbcInstOperandFormat operands[DxbcMaxOperandCount]; }; - /** - * \brief Retrieves instruction format info - * - * \param [in] opcode The opcode to retrieve - * \returns Instruction format info - */ DxbcInstFormat dxbcInstructionFormat(DxbcOpcode opcode); } \ No newline at end of file diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_enums.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_enums.h index f6d29a5b8..531a7afd2 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_enums.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_enums.h @@ -4,9 +4,6 @@ namespace dxvk { - /** - * \brief Instruction code listing - */ enum class DxbcOpcode : uint32_t { Add = 0, And = 1, @@ -246,9 +243,6 @@ namespace dxvk { }; - /** - * \brief Extended opcode - */ enum class DxbcExtOpcode : uint32_t { Empty = 0, SampleControls = 1, @@ -257,12 +251,6 @@ namespace dxvk { }; - /** - * \brief Operand type - * - * Selects the 'register file' from which - * to retrieve an operand's value. - */ enum class DxbcOperandType : uint32_t { Temp = 0, Input = 1, @@ -310,12 +298,6 @@ namespace dxvk { }; - /** - * \brief Number of components - * - * Used by operands to determine whether the - * operand has one, four or zero components. - */ enum class DxbcComponentCount : uint32_t { Component0 = 0, Component1 = 1, @@ -323,13 +305,6 @@ namespace dxvk { }; - /** - * \brief Component selection mode - * - * When an operand has four components, the - * component selection mode deterines which - * components are used for the operation. - */ enum class DxbcRegMode : uint32_t { Mask = 0, Swizzle = 1, @@ -337,12 +312,6 @@ namespace dxvk { }; - /** - * \brief Index representation - * - * Determines how an operand - * register index is stored. - */ enum class DxbcOperandIndexRepresentation : uint32_t { Imm32 = 0, Imm64 = 1, @@ -352,18 +321,11 @@ namespace dxvk { }; - /** - * \brief Extended operand type - */ enum class DxbcOperandExt : uint32_t { OperandModifier = 1, }; - /** - * \brief Resource dimension - * The type of a resource. - */ enum class DxbcResourceDim : uint32_t { Unknown = 0, Buffer = 1, @@ -381,27 +343,19 @@ namespace dxvk { }; - /** - * \brief Resource return type - * Data type for resource read ops. - */ enum class DxbcResourceReturnType : uint32_t { Unorm = 1, Snorm = 2, Sint = 3, Uint = 4, Float = 5, - Mixed = 6, /// ? + Mixed = 6, Double = 7, - Continued = 8, /// ? - Unused = 9, /// ? + Continued = 8, + Unused = 9, }; - /** - * \brief Register component type - * Data type of a register component. - */ enum class DxbcRegisterComponentType : uint32_t { Unknown = 0, Uint32 = 1, @@ -410,9 +364,6 @@ namespace dxvk { }; - /** - * \brief Instruction return type - */ enum class DxbcInstructionReturnType : uint32_t { Float = 0, Uint = 1, @@ -493,9 +444,6 @@ namespace dxvk { using DxbcSyncFlags = Flags; - /** - * \brief Geometry shader input primitive - */ enum class DxbcPrimitive : uint32_t { Undefined = 0, Point = 1, @@ -538,9 +486,6 @@ namespace dxvk { }; - /** - * \brief Geometry shader output topology - */ enum class DxbcPrimitiveTopology : uint32_t { Undefined = 0, PointList = 1, @@ -555,9 +500,6 @@ namespace dxvk { }; - /** - * \brief Sampler operation mode - */ enum class DxbcSamplerMode : uint32_t { Default = 0, Comparison = 1, @@ -565,13 +507,6 @@ namespace dxvk { }; - /** - * \brief Scalar value type - * - * Enumerates possible register component - * types. Scalar types are represented as - * a one-component vector type. - */ enum class DxbcScalarType : uint32_t { Uint32 = 0, Uint64 = 1, @@ -583,9 +518,6 @@ namespace dxvk { }; - /** - * \brief Tessellator domain - */ enum class DxbcTessDomain : uint32_t { Undefined = 0, Isolines = 1, @@ -593,9 +525,6 @@ namespace dxvk { Quads = 3, }; - /** - * \brief Tessellator partitioning - */ enum class DxbcTessPartitioning : uint32_t { Undefined = 0, Integer = 1, @@ -604,9 +533,6 @@ namespace dxvk { FractEven = 4, }; - /** - * \brief UAV definition flags - */ enum class DxbcUavFlag : uint32_t { GloballyCoherent = 0, RasterizerOrdered = 1, @@ -614,9 +540,6 @@ namespace dxvk { using DxbcUavFlags = Flags; - /** - * \brief Tessellator output primitive - */ enum class DxbcTessOutputPrimitive : uint32_t { Undefined = 0, Point = 1, @@ -625,12 +548,6 @@ namespace dxvk { TriangleCcw = 4, }; - /** - * \brief Custom data class - * - * Stores which type of custom data is - * referenced by the instruction. - */ enum class DxbcCustomDataClass : uint32_t { Comment = 0, DebugInfo = 1, diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_header.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_header.h index 8eca0d499..f9b33aa1f 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_header.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_header.h @@ -6,12 +6,6 @@ namespace dxvk { - /** - * \brief DXBC header - * - * Stores information about the shader file itself - * and the data chunks stored inside the file. - */ class DxbcHeader { public: @@ -19,22 +13,10 @@ namespace dxvk { DxbcHeader(DxbcReader& reader); ~DxbcHeader(); - /** - * \brief Number of chunks - * \returns Chunk count - */ uint32_t numChunks() const { return m_chunkOffsets.size(); } - /** - * \brief Chunk offset - * - * Retrieves the offset of a chunk, in - * bytes, from the start of the file. - * \param [in] chunkId Chunk index - * \returns Byte offset of that chunk - */ uint32_t chunkOffset(uint32_t chunkId) const { return m_chunkOffsets.at(chunkId); } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_modinfo.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_modinfo.h index 13e733dde..d9ff25522 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_modinfo.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_modinfo.h @@ -4,22 +4,10 @@ namespace dxvk { - /** - * \brief Tessellation info - * - * Stores the maximum tessellation factor - * to export from tessellation shaders. - */ struct DxbcTessInfo { float maxTessFactor; }; - /** - * \brief Xfb capture entry - * - * Stores an output variable to capture, - * as well as the buffer to write it to. - */ struct DxbcXfbEntry { const char* semanticName; uint32_t semanticIndex; @@ -30,13 +18,6 @@ namespace dxvk { uint32_t offset; }; - /** - * \brief Xfb info - * - * Stores capture entries and output buffer - * strides. This structure must only be - * defined if \c entryCount is non-zero. - */ struct DxbcXfbInfo { uint32_t entryCount; DxbcXfbEntry entries[128]; @@ -44,12 +25,6 @@ namespace dxvk { int32_t rasterizedStream; }; - /** - * \brief Shader module info - * - * Stores information which may affect shader compilation. - * This data can be supplied by the client API implementation. - */ struct DxbcModuleInfo { DxbcOptions options; DxbcTessInfo* tess; diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_module.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_module.h index 3ef75cfe1..ef7617b95 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_module.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_module.h @@ -11,31 +11,18 @@ #include -// References used for figuring out DXBC: -// - https://github.com/tgjones/slimshader-cpp -// - Wine namespace dxvk { class DxbcAnalyzer; class DxbcCompiler; - /** - * \brief Immediate constant buffer properties - */ struct DxbcIcbInfo { size_t size = 0u; const void* data = nullptr; }; - /** - * \brief DXBC shader module - * - * Reads the DXBC byte code and extracts information - * about the resource bindings and the instruction - * stream. A module can then be compiled to SPIR-V. - */ class DxbcModule { public: @@ -43,10 +30,6 @@ namespace dxvk { DxbcModule(DxbcReader& reader); ~DxbcModule(); - /** - * \brief Shader type - * \returns Shader type - */ std::optional programInfo() const { if (m_shexChunk == nullptr) return std::nullopt; @@ -54,21 +37,10 @@ namespace dxvk { return m_shexChunk->programInfo(); } - /** - * \brief Queries shader binding mask - * - * Only valid after successfully compiling the shader. - */ std::optional bindings() const { return m_bindings; } - /** - * \brief Retrieves immediate constant buffer info - * - * Only valid after successfully compiling the shader. - * \returns Immediate constant buffer data - */ DxbcIcbInfo icbInfo() const { DxbcIcbInfo result = { }; result.size = m_icb.size() * sizeof(uint32_t); @@ -76,37 +48,13 @@ namespace dxvk { return result; } - /** - * \brief Input and output signature chunks - * - * Parts of the D3D11 API need access to the - * input or output signature of the shader. - */ Rc isgn() const { return m_isgnChunk; } Rc osgn() const { return m_osgnChunk; } - /** - * \brief Compiles DXBC shader to SPIR-V module - * - * \param [in] moduleInfo DXBC module info - * \param [in] fileName File name, will be added to - * the compiled SPIR-V for debugging purposes. - * \returns The compiled shader object - */ SpirvCodeBuffer compile( const DxbcModuleInfo& moduleInfo, const std::string& fileName); - /** - * \brief Compiles a pass-through geometry shader - * - * Applications can pass a vertex shader to create - * a geometry shader with stream output. In this - * case, we have to create a passthrough geometry - * shader, which operates in point to point mode. - * \param [in] moduleInfo DXBC module info - * \param [in] fileName SPIR-V shader name - */ SpirvCodeBuffer compilePassthroughShader( const DxbcModuleInfo& moduleInfo, const std::string& fileName) const; diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_names.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_names.h index 52235ae59..67e337491 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_names.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_names.h @@ -23,4 +23,4 @@ namespace dxvk { std::ostream& operator << (std::ostream& os, DxbcCustomDataClass e); std::ostream& operator << (std::ostream& os, DxbcScalarType e); -} // namespace dxvk +} diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_options.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_options.h index 0fe46a767..c8856026e 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_options.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_options.h @@ -20,54 +20,34 @@ namespace dxvk { struct DxbcOptions { DxbcOptions() {} - // Clamp oDepth in fragment shaders if the depth - // clip device feature is not supported bool useDepthClipWorkaround = false; - /// Determines whether format qualifiers - /// on typed UAV loads are required bool supportsTypedUavLoadR32 = false; - /// Determines whether raw access chains are supported bool supportsRawAccessChains = false; - /// Clear thread-group shared memory to zero bool zeroInitWorkgroupMemory = false; - /// Declare vertex positions as invariant bool invariantPosition = false; - /// Insert memory barriers after TGSM stoes bool forceVolatileTgsmAccess = false; - /// Try to detect hazards in UAV access and insert - /// barriers when we know control flow is uniform. bool forceComputeUavBarriers = false; - /// Replace ld_ms with ld bool disableMsaa = false; - /// Force sample rate shading by using sample - /// interpolation for fragment shader inputs bool forceSampleRateShading = false; - // Enable per-sample interlock if supported bool enableSampleShadingInterlock = false; - /// Use tightly packed arrays for immediate - /// constant buffers if possible bool supportsTightIcbPacking = false; - /// Whether exporting point size is required bool needsPointSizeExport = true; - /// Whether to enable sincos emulation bool sincosEmulation = false; - /// Float control flags DxbcFloatControlFlags floatControl; - /// Minimum storage buffer alignment VkDeviceSize minSsboAlignment = 0; }; diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_reader.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_reader.h index a16000179..36748327f 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_reader.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_reader.h @@ -7,12 +7,6 @@ namespace dxvk { - /** - * \brief DXBC bytecode reader - * - * Holds references to the shader byte code and - * provides methods to read - */ class DxbcReader { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_tag.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_tag.h index 2ba17509c..bd9a6ce3b 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_tag.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_tag.h @@ -4,12 +4,6 @@ namespace dxvk { - /** - * \brief Four-character tag - * - * Used to identify chunks in the - * compiled DXBC file by name. - */ class DxbcTag { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_util.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_util.h index 7d98af6ee..686905aaa 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_util.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_util.h @@ -5,17 +5,11 @@ namespace dxvk { - /** - * \brief Push constant struct - */ struct DxbcPushConstants { uint32_t rasterizerSampleCount; }; - /** - * \brief Binding numbers and properties - */ enum DxbcBindingProperties : uint32_t { DxbcConstBufBindingIndex = 0, DxbcConstBufBindingCount = 16, @@ -33,12 +27,6 @@ namespace dxvk { }; - /** - * \brief Shader binding mask - * - * Stores a bit masks of resource bindings - * that are accessed by any given shader. - */ struct DxbcBindingMask { uint32_t cbvMask = 0u; uint32_t samplerMask = 0u; @@ -70,94 +58,41 @@ namespace dxvk { }; - /** - * \brief Computes first binding index for a given stage - * - * \param [in] stage The shader stage - * \returns Index of first binding - */ inline uint32_t computeStageBindingOffset(DxbcProgramType stage) { return DxbcStageBindingCount * uint32_t(stage); } - /** - * \brief Computes first UAV binding index offset for a given stage - * - * \param [in] stage The shader stage - * \returns Index of first UAV binding - */ inline uint32_t computeStageUavBindingOffset(DxbcProgramType stage) { return DxbcUavBindingIndex + DxbcUavBindingCount * (stage == DxbcProgramType::ComputeShader ? 2 : 0); } - /** - * \brief Computes constant buffer binding index - * - * \param [in] stage Shader stage - * \param [in] index Constant buffer index - * \returns Binding index - */ inline uint32_t computeConstantBufferBinding(DxbcProgramType stage, uint32_t index) { return computeStageBindingOffset(stage) + DxbcConstBufBindingIndex + index; } - /** - * \brief Computes sampler binding index - * - * \param [in] stage Shader stage - * \param [in] index Sampler index - * \returns Binding index - */ inline uint32_t computeSamplerBinding(DxbcProgramType stage, uint32_t index) { return computeStageBindingOffset(stage) + DxbcSamplerBindingIndex + index; } - /** - * \brief Computes resource binding index - * - * \param [in] stage Shader stage - * \param [in] index Resource index - * \returns Binding index - */ inline uint32_t computeSrvBinding(DxbcProgramType stage, uint32_t index) { return computeStageBindingOffset(stage) + DxbcResourceBindingIndex + index; } - /** - * \brief Computes UAV binding offset - * - * \param [in] stage Shader stage - * \param [in] index UAV index - * \returns Binding index - */ inline uint32_t computeUavBinding(DxbcProgramType stage, uint32_t index) { return computeStageUavBindingOffset(stage) + index; } - /** - * \brief Computes UAV counter binding offset - * - * \param [in] stage Shader stage - * \param [in] index UAV index - * \returns Binding index - */ inline uint32_t computeUavCounterBinding(DxbcProgramType stage, uint32_t index) { return computeStageUavBindingOffset(stage) + DxbcUavBindingCount + index; } - /** - * \brief Primitive vertex count - * - * Calculates the number of vertices - * for a given primitive type. - */ uint32_t primitiveVertexCount( DxbcPrimitive primitive); diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxvk/dxvk_pipelayout.h b/app/src/main/cpp/thirdparty/dxbc/include/dxvk/dxvk_pipelayout.h index 9873719c7..190724bb5 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxvk/dxvk_pipelayout.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxvk/dxvk_pipelayout.h @@ -15,12 +15,6 @@ namespace dxvk { class DxvkDevice; class DxvkPipelineManager; - /** - * \brief Order-invariant atomic access operation - * - * Information used to optimize barriers when a resource - * is accessed exlusively via order-invariant stores. - */ struct DxvkAccessOp { enum OpType : uint16_t { None = 0x0u, @@ -56,50 +50,22 @@ namespace dxvk { static_assert(sizeof(DxvkAccessOp) == sizeof(uint16_t)); - /** - * \brief Binding info - * - * Stores metadata for a single binding in - * a given shader, or for the whole pipeline. - */ struct DxvkBindingInfo { - VkDescriptorType descriptorType = VK_DESCRIPTOR_TYPE_MAX_ENUM; ///< Vulkan descriptor type - uint32_t resourceBinding = 0u; ///< API binding slot for the resource - VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; ///< Image view type - VkShaderStageFlagBits stage = VK_SHADER_STAGE_FLAG_BITS_MAX_ENUM; ///< Shader stage - VkAccessFlags access = 0u; ///< Access mask for the resource - DxvkAccessOp accessOp = DxvkAccessOp::None; ///< Order-invariant store type, if any - bool uboSet = false; ///< Whether to include this in the UBO set - bool isMultisampled = false; ///< Multisampled binding + VkDescriptorType descriptorType = VK_DESCRIPTOR_TYPE_MAX_ENUM; + uint32_t resourceBinding = 0u; + VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; + VkShaderStageFlagBits stage = VK_SHADER_STAGE_FLAG_BITS_MAX_ENUM; + VkAccessFlags access = 0u; + DxvkAccessOp accessOp = DxvkAccessOp::None; + bool uboSet = false; + bool isMultisampled = false; - /** - * \brief Computes descriptor set index for the given binding - * - * This is determines based on the shader stages that use the binding. - * \returns Descriptor set index - */ uint32_t computeSetIndex() const; - /** - * \brief Numeric value of the binding - * - * Used when sorting bindings. - * \returns Numeric value - */ uint32_t value() const; - /** - * \brief Checks for equality - * - * \param [in] other Binding to compare to - * \returns \c true if both bindings are equal - */ bool eq(const DxvkBindingInfo& other) const; - /** - * \brief Hashes binding info - * \returns Binding hash - */ size_t hash() const; }; diff --git a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_code_buffer.h b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_code_buffer.h index 4e33d6dcb..ed8a2c36e 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_code_buffer.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_code_buffer.h @@ -8,13 +8,6 @@ namespace dxvk { - /** - * \brief SPIR-V code buffer - * - * Helper class for generating SPIR-V shaders. - * Stores arbitrary SPIR-V instructions in a - * format that can be read by Vulkan drivers. - */ class SpirvCodeBuffer { public: @@ -35,193 +28,62 @@ namespace dxvk { SpirvCodeBuffer &operator=(const SpirvCodeBuffer &) = default; SpirvCodeBuffer &operator=(SpirvCodeBuffer &&) = default; - /** - * \brief Code data - * \returns Code data - */ const uint32_t* data() const { return m_code.data(); } uint32_t* data() { return m_code.data(); } - /** - * \brief Code size, in dwords - * \returns Code size, in dwords - */ uint32_t dwords() const { return m_code.size(); } - /** - * \brief Code size, in bytes - * \returns Code size, in bytes - */ size_t size() const { return m_code.size() * sizeof(uint32_t); } - /** - * \brief Begin instruction iterator - * - * Points to the first instruction in the instruction - * block. The header, if any, will be skipped over. - * \returns Instruction iterator - */ SpirvInstructionIterator begin() { return SpirvInstructionIterator( m_code.data(), 0, m_code.size()); } - /** - * \brief End instruction iterator - * - * Points to the end of the instruction block. - * \returns Instruction iterator - */ SpirvInstructionIterator end() { return SpirvInstructionIterator(nullptr, 0, 0); } - /** - * \brief Allocates a new ID - * - * Returns a new valid ID and increments the - * maximum ID count stored in the header. - * \returns The new SPIR-V ID - */ uint32_t allocId(); - /** - * \brief Appends an instruction - * - * Slightly faster than individually adding words. - * \param [in] ins Instruction - */ void append(const SpirvInstruction& ins); - /** - * \brief Merges two code buffers - * - * This is useful to generate declarations or - * the SPIR-V header at the same time as the - * code when doing so in advance is impossible. - * \param [in] other Code buffer to append - */ void append(const SpirvCodeBuffer& other); - /** - * \brief Appends an 32-bit word to the buffer - * \param [in] word The word to append - */ void putWord(uint32_t word); - /** - * \brief Appends an instruction word to the buffer - * - * Adds a single word containing both the word count - * and the op code number for a single instruction. - * \param [in] opCode Operand code - * \param [in] wordCount Number of words - */ void putIns(spv::Op opCode, uint16_t wordCount); - /** - * \brief Appends a 32-bit integer to the buffer - * \param [in] value The number to add - */ void putInt32(uint32_t word); - /** - * \brief Appends a 64-bit integer to the buffer - * - * A 64-bit integer will take up two 32-bit words. - * \param [in] value 64-bit value to add - */ void putInt64(uint64_t value); - /** - * \brief Appends a 32-bit float to the buffer - * \param [in] value The number to add - */ void putFloat32(float value); - /** - * \brief Appends a 64-bit float to the buffer - * \param [in] value The number to add - */ void putFloat64(double value); - /** - * \brief Appends a literal string to the buffer - * \param [in] str String to append to the buffer - */ void putStr(const char* str); - /** - * \brief Adds the header to the buffer - * - * \param [in] version SPIR-V version - * \param [in] boundIds Number of bound IDs - */ void putHeader(uint32_t version, uint32_t boundIds); - /** - * \brief Erases given number of dwords - * - * Removes data from the code buffer, starting - * at the current insertion offset. - * \param [in] size Number of words to remove - */ void erase(size_t size); - /** - * \brief Computes length of a literal string - * - * \param [in] str The string to check - * \returns Number of words consumed by a string - */ uint32_t strLen(const char* str); - /** - * \brief Stores the SPIR-V module to a stream - * - * The ability to save modules to a file - * exists mostly for debugging purposes. - * \param [in] stream Output stream - */ void store(std::ostream& stream) const; - /** - * \brief Retrieves current insertion pointer - * - * Sometimes it may be necessay to insert code into the - * middle of the stream rather than appending it. This - * retrieves the current function pointer. Note that the - * pointer will become invalid if any code is inserted - * before the current pointer location. - * \returns Current instruction pointr - */ size_t getInsertionPtr() const { return m_ptr; } - /** - * \brief Sets insertion pointer to a specific value - * - * Sets the insertion pointer to a value that was - * previously retrieved by \ref getInsertionPtr. - * \returns Current instruction pointr - */ void beginInsertion(size_t ptr) { m_ptr = ptr; } - /** - * \brief Sets insertion pointer to the end - * - * After this call, new instructions will be - * appended to the stream. In other words, - * this will restore default behaviour. - * \returns Previous instruction pointer - */ size_t endInsertion() { return std::exchange(m_ptr, m_code.size()); } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_instruction.h b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_instruction.h index 1f9314157..cfa932ea6 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_instruction.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_instruction.h @@ -4,13 +4,6 @@ namespace dxvk { - /** - * \brief SPIR-V instruction - * - * Helps parsing a single instruction, providing - * access to the op code, instruction length and - * instruction arguments. - */ class SpirvInstruction { public: @@ -19,64 +12,29 @@ namespace dxvk { SpirvInstruction(uint32_t* code, uint32_t offset, uint32_t length) : m_code(code), m_offset(offset), m_length(length) { } - /** - * \brief SPIR-V Op code - * \returns The op code - */ spv::Op opCode() const { return static_cast( this->arg(0) & spv::OpCodeMask); } - /** - * \brief Instruction length - * \returns Number of DWORDs - */ uint32_t length() const { return this->arg(0) >> spv::WordCountShift; } - /** - * \brief Instruction offset - * \returns Offset in DWORDs - */ uint32_t offset() const { return m_offset; } - /** - * \brief Argument value - * - * Retrieves an argument DWORD. Note that some instructions - * take 64-bit arguments which require more than one DWORD. - * Arguments start at index 1. Calling this method with an - * argument ID of 0 will return the opcode token. - * \param [in] idx Argument index, starting at 1 - * \returns The argument value - */ uint32_t arg(uint32_t idx) const { const uint32_t index = m_offset + idx; return index < m_length ? m_code[index] : 0; } - /** - * \brief Argument string - * - * Retrieves a pointer to a UTF-8-encoded string. - * \param [in] idx Argument index, starting at 1 - * \returns Pointer to the literal string - */ const char* chr(uint32_t idx) const { const uint32_t index = m_offset + idx; return index < m_length ? reinterpret_cast(&m_code[index]) : nullptr; } - /** - * \brief Changes the value of an argument - * - * \param [in] idx Argument index, starting at 1 - * \param [in] word New argument word - */ void setArg(uint32_t idx, uint32_t word) const { if (m_offset + idx < m_length) m_code[m_offset + idx] = word; @@ -91,12 +49,6 @@ namespace dxvk { }; - /** - * \brief SPIR-V instruction iterator - * - * Convenient iterator that can be used - * to process raw SPIR-V shader code. - */ class SpirvInstructionIterator { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_module.h b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_module.h index 02b7b7cf7..6827d1b8b 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_module.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_module.h @@ -45,14 +45,6 @@ namespace dxvk { return (major << 16) | (minor << 8); } - /** - * \brief SPIR-V module - * - * This class generates a code buffer containing a full - * SPIR-V shader module. Ensures that the module layout - * is valid, as defined in the SPIR-V 1.0 specification, - * section 2.4 "Logical Layout of a Module". - */ class SpirvModule { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/GLSL.std.450.h b/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/GLSL.std.450.h index 0594f907a..6b862f16e 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/GLSL.std.450.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/GLSL.std.450.h @@ -13,7 +13,7 @@ ** ** MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS KHRONOS ** STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS SPECIFICATIONS AND -** HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/ +** HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/ ** ** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS ** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, @@ -31,7 +31,7 @@ static const int GLSLstd450Version = 100; static const int GLSLstd450Revision = 3; enum GLSLstd450 { - GLSLstd450Bad = 0, // Don't use + GLSLstd450Bad = 0, GLSLstd450Round = 1, GLSLstd450RoundEven = 2, @@ -71,8 +71,8 @@ enum GLSLstd450 { GLSLstd450Determinant = 33, GLSLstd450MatrixInverse = 34, - GLSLstd450Modf = 35, // second operand needs an OpVariable to write to - GLSLstd450ModfStruct = 36, // no OpVariable operand + GLSLstd450Modf = 35, + GLSLstd450ModfStruct = 36, GLSLstd450FMin = 37, GLSLstd450UMin = 38, GLSLstd450SMin = 39, @@ -83,13 +83,13 @@ enum GLSLstd450 { GLSLstd450UClamp = 44, GLSLstd450SClamp = 45, GLSLstd450FMix = 46, - GLSLstd450IMix = 47, // Reserved + GLSLstd450IMix = 47, GLSLstd450Step = 48, GLSLstd450SmoothStep = 49, GLSLstd450Fma = 50, - GLSLstd450Frexp = 51, // second operand needs an OpVariable to write to - GLSLstd450FrexpStruct = 52, // no OpVariable operand + GLSLstd450Frexp = 51, + GLSLstd450FrexpStruct = 52, GLSLstd450Ldexp = 53, GLSLstd450PackSnorm4x8 = 54, @@ -128,4 +128,4 @@ enum GLSLstd450 { GLSLstd450Count }; -#endif // #ifndef GLSLstd450_H +#endif diff --git a/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/spirv.hpp b/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/spirv.hpp index f439e472f..945e75a07 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/spirv.hpp +++ b/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/spirv.hpp @@ -22,27 +22,7 @@ // FROM,OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS // IN THE MATERIALS. -// This header is automatically generated by the same tool that creates -// the Binary Section of the SPIR-V specification. -// Enumeration tokens for SPIR-V, in various styles: -// C, C++, C++11, JSON, Lua, Python, C#, D, Beef -// -// - C will have tokens with a "Spv" prefix, e.g.: SpvSourceLanguageGLSL -// - C++ will have tokens in the "spv" name space, e.g.: spv::SourceLanguageGLSL -// - C++11 will use enum classes in the spv namespace, e.g.: spv::SourceLanguage::GLSL -// - Lua will use tables, e.g.: spv.SourceLanguage.GLSL -// - Python will use dictionaries, e.g.: spv['SourceLanguage']['GLSL'] -// - C# will use enum classes in the Specification class located in the "Spv" namespace, -// e.g.: Spv.Specification.SourceLanguage.GLSL -// - D will have tokens under the "spv" module, e.g: spv.SourceLanguage.GLSL -// - Beef will use enum classes in the Specification class located in the "Spv" namespace, -// e.g.: Spv.Specification.SourceLanguage.GLSL -// -// Some tokens act like mask values, which can be OR'd together, -// while others are mutually exclusive. The mask-like ones have -// "Mask" in their name, and a parallel enum that has the shift -// amount (1 << x) for each corresponding enumerant. #ifndef spirv_HPP #define spirv_HPP @@ -2398,7 +2378,7 @@ enum Op { inline void HasResultAndType(Op opcode, bool *hasResult, bool *hasResultType) { *hasResult = *hasResultType = false; switch (opcode) { - default: /* unknown opcode */ break; + default: break; case OpNop: *hasResult = false; *hasResultType = false; break; case OpUndef: *hasResult = true; *hasResultType = true; break; case OpSourceContinued: *hasResult = false; *hasResultType = false; break; @@ -5144,9 +5124,8 @@ inline const char* OpToString(Op value) { } } -#endif /* SPV_ENABLE_UTILITY_CODE */ +#endif -// Overload bitwise operators for mask bit combining inline ImageOperandsMask operator|(ImageOperandsMask a, ImageOperandsMask b) { return ImageOperandsMask(unsigned(a) | unsigned(b)); } inline ImageOperandsMask operator&(ImageOperandsMask a, ImageOperandsMask b) { return ImageOperandsMask(unsigned(a) & unsigned(b)); } @@ -5209,6 +5188,6 @@ inline RawAccessChainOperandsMask operator&(RawAccessChainOperandsMask a, RawAcc inline RawAccessChainOperandsMask operator^(RawAccessChainOperandsMask a, RawAccessChainOperandsMask b) { return RawAccessChainOperandsMask(unsigned(a) ^ unsigned(b)); } inline RawAccessChainOperandsMask operator~(RawAccessChainOperandsMask a) { return RawAccessChainOperandsMask(~unsigned(a)); } -} // end namespace spv +} -#endif // #ifndef spirv_HPP +#endif diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/log/log.h b/app/src/main/cpp/thirdparty/dxbc/include/util/log/log.h index b12428c08..58075723b 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/log/log.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/log/log.h @@ -14,12 +14,6 @@ namespace dxvk { None = 5, }; - /** - * \brief Logger - * - * Logger for one DLL. Creates a text file and - * writes all log messages to that file. - */ class Logger { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc.h b/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc.h index b92cf00b4..b84654c4f 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc.h @@ -6,25 +6,14 @@ namespace dxvk { - /** - * \brief Reference-counted object - */ class RcObject { public: - /** - * \brief Increments reference count - * \returns New reference count - */ force_inline uint32_t incRef() { return ++m_refCount; } - /** - * \brief Decrements reference count - * \returns New reference count - */ force_inline uint32_t decRef() { return --m_refCount; } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc_ptr.h b/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc_ptr.h index ac465758c..c0d1e0778 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc_ptr.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc_ptr.h @@ -8,13 +8,6 @@ namespace dxvk { - /** - * \brief Pointer for reference-counted objects - * - * This only requires the given type to implement \c incRef - * and \c decRef methods that adjust the reference count. - * \tparam T Object type - */ template class Rc { template @@ -108,36 +101,15 @@ namespace dxvk { return m_object != nullptr; } - /** - * \brief Sets pointer without acquiring a reference - * - * Must only be use when a reference has been taken via - * other means. - * \param [in] object Object pointer - */ void unsafeInsert(T* object) { this->decRef(); m_object = object; } - /** - * \brief Extracts raw pointer - * - * Sets the smart pointer to null without decrementing the - * reference count. Must only be used when the reference - * count is decremented in some other way. - * \returns Pointer to owned object - */ T* unsafeExtract() { return std::exchange(m_object, nullptr); } - /** - * \brief Creates smart pointer without taking reference - * - * Must only be used when a refernece has been obtained via other means. - * \param [in] object Pointer to object to take ownership of - */ static Rc unsafeCreate(T* object) { return Rc(object, false); } @@ -159,7 +131,6 @@ namespace dxvk { if constexpr (std::is_void_vdecRef())>) { m_object->decRef(); } else { - // Deprecated, objects should manage themselves now. if (!m_object->decRef()) delete m_object; } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/util_bit.h b/app/src/main/cpp/thirdparty/dxbc/include/util/util_bit.h index 2a5302581..7df34d081 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/util_bit.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/util_bit.h @@ -72,11 +72,6 @@ namespace dxvk::bit { #elif defined(__BMI__) return __tzcnt_u32(n); #elif defined(DXVK_ARCH_X86) && (defined(__GNUC__) || defined(__clang__)) - // tzcnt is encoded as rep bsf, so we can use it on all - // processors, but the behaviour of zero inputs differs: - // - bsf: zf = 1, cf = ?, result = ? - // - tzcnt: zf = 0, cf = 1, result = 32 - // We'll have to handle this case manually. uint32_t res; uint32_t tmp; asm ( @@ -223,14 +218,6 @@ namespace dxvk::bit { } - /** - * \brief Clears cache lines of memory - * - * Uses non-temporal stores. The memory region offset - * and size are assumed to be aligned to 64 bytes. - * \param [in] mem Memory region to clear - * \param [in] size Number of bytes to clear - */ inline void bclear(void* mem, size_t size) { #if defined(DXVK_ARCH_X86) && (defined(__GNUC__) || defined(__clang__) || defined(_MSC_VER)) auto zero = _mm_setzero_si128(); @@ -253,13 +240,6 @@ namespace dxvk::bit { } - /** - * \brief Compares two aligned structs bit by bit - * - * \param [in] a First struct - * \param [in] b Second struct - * \returns \c true if the structs are equal - */ template bool bcmpeq(const T* a, const T* b) { static_assert(alignof(T) >= 16); @@ -319,7 +299,6 @@ namespace dxvk::bit { uint32_t dword = 0; uint32_t bit = idx; - // Compiler doesn't remove this otherwise. if constexpr (Dwords > 1) { dword = idx / 32; bit = idx % 32; @@ -332,7 +311,6 @@ namespace dxvk::bit { uint32_t dword = 0; uint32_t bit = idx; - // Compiler doesn't remove this otherwise. if constexpr (Dwords > 1) { dword = idx / 32; bit = idx % 32; @@ -354,7 +332,6 @@ namespace dxvk::bit { uint32_t dword = 0; uint32_t bit = idx; - // Compiler doesn't remove this otherwise. if constexpr (Dwords > 1) { dword = idx / 32; bit = idx % 32; @@ -594,17 +571,6 @@ namespace dxvk::bit { }; - /** - * \brief Encodes float as fixed point - * - * Rounds away from zero. If this is not suitable for - * certain use cases, implement round to nearest even. - * \tparam T Integer type, may be signed - * \tparam I Integer bits - * \tparam F Fractional bits - * \param n Float to encode - * \returns Encoded fixed-point value - */ template T encodeFixed(float n) { if (n != n) @@ -631,18 +597,8 @@ namespace dxvk::bit { } - /** - * \brief Decodes fixed-point integer to float - * - * \tparam T Integer type, may be signed - * \tparam I Integer bits - * \tparam F Fractional bits - * \param n Number to decode - * \returns Decoded number - */ template float decodeFixed(T n) { - // Sign-extend as necessary if constexpr (std::is_signed_v) n -= (n & (T(1u) << (I + F - 1u))) << 1u; @@ -650,9 +606,6 @@ namespace dxvk::bit { } - /** - * \brief Inserts one null bit after each bit - */ inline uint32_t split2(uint32_t c) { c = (c ^ (c << 8u)) & 0x00ff00ffu; c = (c ^ (c << 4u)) & 0x0f0f0f0fu; @@ -662,9 +615,6 @@ namespace dxvk::bit { } - /** - * \brief Inserts two null bits after each bit - */ inline uint64_t split3(uint64_t c) { c = (c | c << 32u) & 0x001f00000000ffffull; c = (c | c << 16u) & 0x001f0000ff0000ffull; @@ -675,32 +625,16 @@ namespace dxvk::bit { } - /** - * \brief Interleaves bits from two integers - * - * Both numbers must fit into 16 bits. - * \param [in] x X coordinate - * \param [in] y Y coordinate - * \returns Morton code of x and y - */ inline uint32_t interleave(uint16_t x, uint16_t y) { return split2(x) | (split2(y) << 1u); } - /** - * \brief Interleaves bits from three integers - * - * All three numbers must fit into 16 bits. - */ inline uint64_t interleave(uint16_t x, uint16_t y, uint16_t z) { return split3(x) | (split3(y) << 1u) | (split3(z) << 2u); } - /** - * \brief 48-bit integer storage type - */ struct uint48_t { explicit uint48_t(uint64_t n) : a(uint16_t(n)), b(uint16_t(n >> 16)), c(uint16_t(n >> 32)) { } @@ -710,7 +644,6 @@ namespace dxvk::bit { uint16_t c; explicit operator uint64_t () const { - // GCC generates worse code if we promote to uint64 directly uint32_t lo = uint32_t(a) | (uint32_t(b) << 16); return uint64_t(lo) | (uint64_t(c) << 32); } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/util_error.h b/app/src/main/cpp/thirdparty/dxbc/include/util/util_error.h index 2cfd45ffd..5790952f6 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/util_error.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/util_error.h @@ -4,12 +4,6 @@ namespace dxvk { - /** - * \brief DXVK error - * - * A generic exception class that stores a - * message. Exceptions should be logged. - */ class DxvkError { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/util_math.h b/app/src/main/cpp/thirdparty/dxbc/include/util/util_math.h index 9276d9337..c26bf7c87 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/util_math.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/util_math.h @@ -24,9 +24,6 @@ namespace dxvk { return (what / to) * to; } - // Equivalent of std::clamp for use with floating point numbers - // Handles (-){INFINITY,NAN} cases. - // Will return min in cases of NAN, etc. inline float fclamp(float value, float min, float max) { return std::fmin( std::fmax(value, min), max); diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/util_small_vector.h b/app/src/main/cpp/thirdparty/dxbc/include/util/util_small_vector.h index 861c9e0e0..b9091efd8 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/util_small_vector.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/util_small_vector.h @@ -193,7 +193,6 @@ namespace dxvk { } u; size_t pick_capacity(size_t n) { - // Pick next largest power of two for the new capacity return size_t(1u) << ((sizeof(n) * 8u) - bit::lzcnt(n - 1)); } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/util_string.h b/app/src/main/cpp/thirdparty/dxbc/include/util/util_string.h index 5fe00fbf5..e8ca7f2d0 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/util_string.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/util_string.h @@ -48,15 +48,6 @@ namespace dxvk::str { uint32_t* end, uint32_t ch); - /** - * \brief Decodes a single character - * - * Note that \c begin and \c end must not be equal. - * \param [in] begin Pointer to current position within the input string - * \param [in] end Pointer to the end of the input string - * \param [out] ch Pointer to the decoded character code - * \returns Pointer to next character in the input string - */ template const T* decodeChar( const T* begin, @@ -72,18 +63,6 @@ namespace dxvk::str { return reinterpret_cast(result); } - /** - * \brief Encodes a character - * - * Note that \c begin and \c end may be both be \c nullptr or equal, in - * which case only the length of the encoded character will be returned. - * \param [in] begin Pointer to current position within the output string - * \param [in] end Pointer to the end of the output string - * \param [in] ch Character to encode - * \returns If begin is \c nullptr , the number of units required to encode - * the character. Otherwise, the number of units written to the output. - * This may return \c 0 for characters that cannot be written or encoded. - */ template size_t encodeChar( T* begin, @@ -97,13 +76,6 @@ namespace dxvk::str { ch); } - /** - * \brief Computes length of a null-terminated string - * - * \param [in] begin Start of input string - * \returns Number of characters in input string, - * excluding the terminating null character - */ template size_t length(const S* string) { size_t result = 0; @@ -114,23 +86,6 @@ namespace dxvk::str { return result; } - /** - * \brief Converts string from one encoding to another - * - * The output string arguments may be \c nullptr. In that case, the - * total length of the transcoded string will be returned, in units - * of the output character type. The output string will only be - * null-terminated if the input string is also null-terminated. - * \tparam D Output character type - * \tparam S Input character type - * \param [in] dstBegin Start of output string - * \param [in] dstLength Length of output string - * \param [in] srcBegin Start of input string - * \param [in] srcLength Length of input string - * \returns If \c dstBegin is \c nullptr , the total number of output - * characters required to store the output string. Otherwise, the - * total number of characters written to the output string. - */ template size_t transcodeString( D* dstBegin, @@ -159,20 +114,8 @@ namespace dxvk::str { return totalLength; } - /** - * \brief Creates string object from wide char array - * - * \param [in] ws Null-terminated wide string - * \returns Regular string object - */ std::string fromws(const wchar_t* ws); - /** - * \brief Creates wide string object from char array - * - * \param [in] mbs Null-terminated string - * \returns Wide string object - */ std::wstring tows(const char* mbs); #ifdef _WIN32 @@ -211,25 +154,15 @@ namespace dxvk::str { } } - /** - * \brief Split string at one or more delimiters characters - * - * \param [in] string String to split - * \param [in] delims Delimiter characters - * \returns Vector of substring views - */ inline std::vector split(std::string_view string, std::string_view delims = " ") { std::vector tokens; for (size_t start = 0; start < string.size(); ) { - // Find first delimiter const auto end = string.find_first_of(delims, start); - // Add non-empty tokens if (start != end) tokens.emplace_back(string.substr(start, end-start)); - // Break at the end of string if (end == std::string_view::npos) break; diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_analysis.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_analysis.cpp index 6064e9ee5..138939e83 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_analysis.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_analysis.cpp @@ -13,9 +13,6 @@ namespace dxvk { m_osgn (osgn), m_psgn (psgn), m_analysis(&analysis) { - // Get number of clipping and culling planes from the - // input and output signatures. We will need this to - // declare the shader input and output interfaces. m_analysis->clipCullIn = getClipCullInfo(m_isgn); m_analysis->clipCullOut = getClipCullInfo(m_osgn); } @@ -36,7 +33,6 @@ namespace dxvk { m_analysis->uavInfos[registerId].accessAtomicOp = true; m_analysis->uavInfos[registerId].accessFlags |= VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; - // Check whether the atomic operation is order-invariant DxvkAccessOp op = DxvkAccessOp::None; switch (ins.op) { @@ -104,8 +100,6 @@ namespace dxvk { const uint32_t registerId = ins.dst[0].idx[0].offset; m_analysis->uavInfos[registerId].accessFlags |= VK_ACCESS_SHADER_WRITE_BIT; - // The UAV format may change between dispatches, so be conservative here - // and only allow this optimization when the app is writing zeroes. DxvkAccessOp storeOp = getStoreAccessOp(DxbcRegMask(0xf), ins.src[1u]); if (storeOp != DxvkAccessOp(DxvkAccessOp::OpType::StoreUi, 0u)) @@ -190,7 +184,6 @@ namespace dxvk { if (m_analysis->uavInfos[uav].accessOp == DxvkAccessOp::None) m_analysis->uavInfos[uav].accessOp = op; - // Maintain ordering if the UAV is accessed via other operations as well if (op == DxvkAccessOp::None || m_analysis->uavInfos[uav].accessOp != op) m_analysis->uavInfos[uav].nonInvariantAccess = true; } @@ -200,31 +193,26 @@ namespace dxvk { if (src.type != DxbcOperandType::Imm32) return DxvkAccessOp::None; - // Trivial case, same value is written to all components if (src.componentCount == DxbcComponentCount::Component1) return getConstantStoreOp(src.imm.u32_1); if (src.componentCount != DxbcComponentCount::Component4) return DxvkAccessOp::None; - // Otherwise, make sure that all written components are equal DxvkAccessOp op = DxvkAccessOp::None; for (uint32_t i = 0u; i < 4u; i++) { if (!writeMask[i]) continue; - // If the written value can't be represented, skip DxvkAccessOp scalarOp = getConstantStoreOp(src.imm.u32_4[i]); if (scalarOp == DxvkAccessOp::None) return DxvkAccessOp::None; - // First component written if (op == DxvkAccessOp::None) op = scalarOp; - // Conflicting store ops if (op != scalarOp) return DxvkAccessOp::None; } diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_isgn.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_isgn.cpp index 80e540f23..6291ac901 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_isgn.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_isgn.cpp @@ -11,7 +11,6 @@ namespace dxvk { DxbcScalarType::Sint32, DxbcScalarType::Float32, }; - // https://github.com/DarkStarSword/3d-fixes/blob/master/dx11shaderanalyse.py#L101 bool hasStream = (tag == "ISG1") || (tag == "OSG1") || (tag == "PSG1") || (tag == "OSG5"); bool hasPrecision = (tag == "ISG1") || (tag == "OSG1") || (tag == "PSG1"); diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_shex.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_shex.cpp index 552329b88..3e5d7d36f 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_shex.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_shex.cpp @@ -3,14 +3,10 @@ namespace dxvk { DxbcShex::DxbcShex(DxbcReader reader) { - // The shader version and type are stored in a 32-bit unit, - // where the first byte contains the major and minor version - // numbers, and the high word contains the program type. reader.skip(2); auto pType = reader.readEnum(); m_programInfo = DxbcProgramInfo(pType); - // Read the actual shader code as an array of DWORDs. auto codeLength = reader.readu32() - 2; m_code.resize(codeLength); reader.read(m_code.data(), codeLength * sizeof(uint32_t)); diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_compiler.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_compiler.cpp index 0b6286fe5..388d34868 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_compiler.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_compiler.cpp @@ -20,17 +20,13 @@ namespace dxvk { m_osgn (osgn), m_psgn (psgn), m_analysis (&analysis) { - // Declare an entry point ID. We'll need it during the - // initialization phase where the execution mode is set. m_entryPointId = m_module.allocateId(); - // Set the shader name so that we recognize it in renderdoc m_module.setDebugSource( spv::SourceLanguageUnknown, 0, m_module.addDebugString(fileName.c_str()), nullptr); - // Set the memory model. This is the same for all shaders. m_module.enableCapability( spv::CapabilityVulkanMemoryModel); @@ -38,7 +34,6 @@ namespace dxvk { spv::AddressingModelLogical, spv::MemoryModelVulkan); - // Make sure our interface registers are clear for (uint32_t i = 0; i < DxbcMaxInterfaceRegs; i++) { m_vRegs.at(i) = DxbcRegisterPointer { }; m_oRegs.at(i) = DxbcRegisterPointer { }; @@ -194,7 +189,6 @@ namespace dxvk { DxbcInterpolationMode::Undefined); } - // Figure out which streams to enable uint32_t streamMask = 0; for (size_t i = 0; i < m_xfbVars.size(); i++) @@ -205,18 +199,13 @@ namespace dxvk { m_module.opEmitVertex(m_module.constu32(streamId)); } - // End the main function emitFunctionEnd(); - // For pass-through we always assume points m_inputTopology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; } SpirvCodeBuffer DxbcCompiler::finalize() { - // Depending on the shader type, this will prepare - // input registers, call various shader functions - // and write back the output registers. switch (m_programInfo.type()) { case DxbcProgramType::VertexShader: this->emitVsFinalize(); break; case DxbcProgramType::HullShader: this->emitHsFinalize(); break; @@ -227,11 +216,8 @@ namespace dxvk { default: throw DxvkError("Invalid shader stage"); } - // Emit float control mode if the extension is supported this->emitFloatControl(); - // Declare the entry point, we now have all the - // information we need, including the interfaces m_module.addEntryPoint(m_entryPointId, m_programInfo.executionModel(), "main"); m_module.setDebugName(m_entryPointId, "main"); @@ -341,9 +327,6 @@ namespace dxvk { void DxbcCompiler::emitDclIndexRange(const DxbcShaderInstruction& ins) { - // dcl_index_range has one operand: - // (0) Range start, either an input or output register - // (1) Range end uint32_t index = ins.dst[0].idxDim - 1u; DxbcIndexRange range = { }; @@ -356,19 +339,11 @@ namespace dxvk { void DxbcCompiler::emitDclTemps(const DxbcShaderInstruction& ins) { - // dcl_temps has one operand: - // (imm0) Number of temp registers - // Ignore this and declare temps on demand. } void DxbcCompiler::emitDclIndexableTemp(const DxbcShaderInstruction& ins) { - // dcl_indexable_temps has three operands: - // (imm0) Array register index (x#) - // (imm1) Number of vectors stored in the array - // (imm2) Component count of each individual vector. This is - // always 4 in fxc-generated binaries and therefore useless. const uint32_t regId = ins.imm[0].u32; DxbcRegisterInfo info; @@ -398,21 +373,9 @@ namespace dxvk { case DxbcOperandType::Input: case DxbcOperandType::Output: { - // dcl_input and dcl_output instructions - // have the following operands: - // (dst0) The register to declare - // (imm0) The system value (optional) uint32_t regDim = 0; uint32_t regIdx = 0; - // In the vertex and fragment shader stage, the - // operand indices will have the following format: - // (0) Register index - // - // In other stages, the input and output registers - // may be declared as arrays of a fixed size: - // (0) Array length - // (1) Register index if (ins.dst[0].idxDim == 2) { regDim = ins.dst[0].idx[0].offset; regIdx = ins.dst[0].idx[1].offset; @@ -425,9 +388,6 @@ namespace dxvk { return; } - // This declaration may map an output register to a system - // value. If that is the case, the system value type will - // be stored in the second operand. const bool hasSv = ins.op == DxbcOpcode::DclInputSgv || ins.op == DxbcOpcode::DclInputSiv @@ -441,8 +401,6 @@ namespace dxvk { if (hasSv) sv = static_cast(ins.imm[0].u32); - // In the pixel shader, inputs are declared with an - // interpolation mode that is part of the op token. const bool hasInterpolationMode = ins.op == DxbcOpcode::DclInputPs || ins.op == DxbcOpcode::DclInputPsSiv; @@ -452,7 +410,6 @@ namespace dxvk { if (hasInterpolationMode) im = ins.controls.interpolation(); - // Declare the actual input/output variable switch (ins.op) { case DxbcOpcode::DclInput: case DxbcOpcode::DclInputSgv: @@ -599,14 +556,10 @@ namespace dxvk { } break; case DxbcOperandType::OutputControlPointId: { - // This system value map to the invocation - // ID, which has been declared already. } break; case DxbcOperandType::InputPatchConstant: case DxbcOperandType::OutputControlPoint: { - // These have been declared as global input and - // output arrays, so there's nothing left to do. } break; case DxbcOperandType::InputGsInstanceId: { @@ -621,8 +574,6 @@ namespace dxvk { m_module.enableExtension("SPV_EXT_fragment_fully_covered"); m_module.enableCapability(spv::CapabilityFragmentFullyCoveredEXT); - // This is bool in SPIR-V but uint32 in DXBC. A bool value of - // false must be 0, and bit 1 must be set to represent true. uint32_t builtinId = emitNewBuiltinVariable({ { DxbcScalarType::Bool, 1, 0 }, spv::StorageClassInput }, @@ -660,9 +611,6 @@ namespace dxvk { DxbcRegMask regMask, DxbcSystemValue sv, DxbcInterpolationMode im) { - // Avoid declaring the same variable multiple times. - // This may happen when multiple system values are - // mapped to different parts of the same register. if (m_vRegs.at(regIdx).id == 0 && sv == DxbcSystemValue::None) { const DxbcVectorType regType = getInputRegType(regIdx); @@ -679,7 +627,6 @@ namespace dxvk { m_vRegs.at(regIdx) = { regType, varId }; - // Interpolation mode, used in pixel shaders if (im == DxbcInterpolationMode::Constant) m_module.decorate(varId, spv::DecorationFlat); @@ -706,11 +653,9 @@ namespace dxvk { } } - // Declare the input slot as defined m_inputMask |= 1u << regIdx; m_vArrayLength = std::max(m_vArrayLength, regIdx + 1); } else if (sv != DxbcSystemValue::None) { - // Add a new system value mapping if needed bool skipSv = sv == DxbcSystemValue::ClipDistance || sv == DxbcSystemValue::CullDistance; @@ -726,21 +671,15 @@ namespace dxvk { DxbcRegMask regMask, DxbcSystemValue sv, DxbcInterpolationMode im) { - // Add a new system value mapping if needed. Clip - // and cull distances are handled separately. if (sv != DxbcSystemValue::None && sv != DxbcSystemValue::ClipDistance && sv != DxbcSystemValue::CullDistance) m_oMappings.push_back({ regIdx, regMask, sv }); if (m_programInfo.type() == DxbcProgramType::HullShader) { - // Hull shaders don't use standard outputs if (getCurrentHsForkJoinPhase() != nullptr) m_hs.outputPerPatchMask |= 1 << regIdx; } else if (m_oRegs.at(regIdx).id == 0) { - // Avoid declaring the same variable multiple times. - // This may happen when multiple system values are - // mapped to different parts of the same register. const DxbcVectorType regType = getOutputRegType(regIdx); DxbcRegisterInfo info; @@ -749,14 +688,9 @@ namespace dxvk { info.type.alength = regDim; info.sclass = spv::StorageClassOutput; - // In xfb mode, we set up the actual - // output vars when emitting a vertex if (m_moduleInfo.xfb != nullptr) info.sclass = spv::StorageClassPrivate; - // In geometry shaders, don't duplicate system value outputs - // to stay within device limits. The pixel shader will read - // all GS system value outputs as system value inputs. if (m_programInfo.type() == DxbcProgramType::GeometryShader && sv != DxbcSystemValue::None) info.sclass = spv::StorageClassPrivate; @@ -766,33 +700,24 @@ namespace dxvk { if (info.sclass == spv::StorageClassOutput) { m_module.decorateLocation(varId, regIdx); - // Add index decoration for potential dual-source blending if (m_programInfo.type() == DxbcProgramType::PixelShader) m_module.decorateIndex(varId, 0); - // Declare vertex positions in all stages as invariant, even if - // this is not the last stage, to help with potential Z fighting. if (sv == DxbcSystemValue::Position && m_moduleInfo.options.invariantPosition) m_module.decorate(varId, spv::DecorationInvariant); } m_oRegs.at(regIdx) = { regType, varId }; - // Declare the output slot as defined m_outputMask |= 1u << regIdx; } } void DxbcCompiler::emitDclConstantBuffer(const DxbcShaderInstruction& ins) { - // dcl_constant_buffer has one operand with two indices: - // (0) Constant buffer register ID (cb#) - // (1) Number of constants in the buffer uint32_t bufferId = ins.dst[0].idx[0].offset; uint32_t elementCount = ins.dst[0].idx[1].offset; - // With dynamic indexing, games will often index constant buffers - // out of bounds. Declare an upper bound to stay within spec. if (ins.controls.accessType() == DxbcConstantBufferAccessType::DynamicallyIndexed) elementCount = 4096; @@ -806,15 +731,11 @@ namespace dxvk { uint32_t numConstants, uint32_t numComponents, const char* name) { - // Uniform buffer data is stored as a fixed-size array - // of 4x32-bit vectors. SPIR-V requires explicit strides. const uint32_t arrayType = m_module.defArrayTypeUnique( getVectorTypeId({ DxbcScalarType::Float32, numComponents }), m_module.constu32(numConstants)); m_module.decorateArrayStride(arrayType, sizeof(uint32_t) * numComponents); - // SPIR-V requires us to put that array into a - // struct and decorate that struct as a block. const uint32_t structType = m_module.defStructTypeUnique(1, &arrayType); m_module.decorate(structType, spv::DecorationBlock); @@ -823,15 +744,12 @@ namespace dxvk { m_module.setDebugName (structType, str::format(name, "_t").c_str()); m_module.setDebugMemberName (structType, 0, "m"); - // Variable that we'll use to access the buffer const uint32_t varId = m_module.newVar( m_module.defPointerType(structType, spv::StorageClassUniform), spv::StorageClassUniform); m_module.setDebugName(varId, name); - // Compute the DXVK binding slot index for the buffer. - // D3D11 needs to bind the actual buffers to this slot. uint32_t bindingId = computeConstantBufferBinding( m_programInfo.type(), regIdx); @@ -843,7 +761,6 @@ namespace dxvk { buf.size = numConstants; m_constantBuffers.at(regIdx) = buf; - // Store descriptor info for the shader interface DxvkBindingInfo binding = { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER }; binding.viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; binding.access = VK_ACCESS_UNIFORM_READ_BIT; @@ -854,17 +771,12 @@ namespace dxvk { void DxbcCompiler::emitDclSampler(const DxbcShaderInstruction& ins) { - // dclSampler takes one operand: - // (dst0) The sampler register to declare const uint32_t samplerId = ins.dst[0].idx[0].offset; - // The sampler type is opaque, but we still have to - // define a pointer and a variable in oder to use it const uint32_t samplerType = m_module.defSamplerType(); const uint32_t samplerPtrType = m_module.defPointerType( samplerType, spv::StorageClassUniformConstant); - // Define the sampler variable const uint32_t varId = m_module.newVar(samplerPtrType, spv::StorageClassUniformConstant); m_module.setDebugName(varId, @@ -873,14 +785,12 @@ namespace dxvk { m_samplers.at(samplerId).varId = varId; m_samplers.at(samplerId).typeId = samplerType; - // Compute binding slot index for the sampler uint32_t bindingId = computeSamplerBinding( m_programInfo.type(), samplerId); m_module.decorateDescriptorSet(varId, 0); m_module.decorateBinding(varId, bindingId); - // Store descriptor info for the shader interface DxvkBindingInfo binding = { VK_DESCRIPTOR_TYPE_SAMPLER }; binding.viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; binding.resourceBinding = bindingId; @@ -895,12 +805,8 @@ namespace dxvk { void DxbcCompiler::emitDclResourceTyped(const DxbcShaderInstruction& ins) { - // dclResource takes two operands: - // (dst0) The resource register ID - // (imm0) The resource return type const uint32_t registerId = ins.dst[0].idx[0].offset; - // We also handle unordered access views here const bool isUav = ins.op == DxbcOpcode::DclUavTyped; if (isUav) { @@ -909,12 +815,8 @@ namespace dxvk { m_module.enableCapability(spv::CapabilityStorageImageWriteWithoutFormat); } - // Defines the type of the resource (texture2D, ...) const DxbcResourceDim resourceType = ins.controls.resourceDim(); - // Defines the type of a read operation. DXBC has the ability - // to define four different types whereas SPIR-V only allows - // one, but in practice this should not be much of a problem. auto xType = static_cast( bit::extract(ins.imm[0].u32, 0, 3)); auto yType = static_cast( @@ -927,12 +829,9 @@ namespace dxvk { if ((xType != yType) || (xType != zType) || (xType != wType)) Logger::warn("DxbcCompiler: dcl_resource: Ignoring resource return types"); - // Declare the actual sampled type const DxbcScalarType sampledType = [xType] { switch (xType) { - // FIXME is this correct? There's no documentation about it case DxbcResourceReturnType::Mixed: return DxbcScalarType::Uint32; - // FIXME do we have to manually clamp writes to SNORM/UNORM resources? case DxbcResourceReturnType::Snorm: return DxbcScalarType::Float32; case DxbcResourceReturnType::Unorm: return DxbcScalarType::Float32; case DxbcResourceReturnType::Float: return DxbcScalarType::Float32; @@ -942,11 +841,9 @@ namespace dxvk { } }(); - // Declare the resource type const uint32_t sampledTypeId = getScalarTypeId(sampledType); const DxbcImageInfo typeInfo = getResourceType(resourceType, isUav); - // Declare additional capabilities if necessary switch (resourceType) { case DxbcResourceDim::Buffer: m_module.enableCapability(isUav @@ -967,13 +864,9 @@ namespace dxvk { break; default: - // No additional capabilities required break; } - // If the read-without-format capability is not set and this - // image is access via a typed load, or if atomic operations - // are used,, we must define the image format explicitly. spv::ImageFormat imageFormat = spv::ImageFormatUnknown; if (isUav) { @@ -983,15 +876,10 @@ namespace dxvk { imageFormat = getScalarImageFormat(sampledType); } - // We do not know whether the image is going to be used as - // a color image or a depth image yet, but we can pick the - // correct type when creating a sampled image object. const uint32_t imageTypeId = m_module.defImageType(sampledTypeId, typeInfo.dim, 0, typeInfo.array, typeInfo.ms, typeInfo.sampled, imageFormat); - // We'll declare the texture variable with the color type - // and decide which one to use when the texture is sampled. const uint32_t resourcePtrType = m_module.defPointerType( imageTypeId, spv::StorageClassUniformConstant); @@ -1001,8 +889,6 @@ namespace dxvk { m_module.setDebugName(varId, str::format(isUav ? "u" : "t", registerId).c_str()); - // Compute the DXVK binding slot index for the resource. - // D3D11 needs to bind the actual resource to this slot. uint32_t bindingId = isUav ? computeUavBinding(m_programInfo.type(), registerId) : computeSrvBinding(m_programInfo.type(), registerId); @@ -1010,8 +896,6 @@ namespace dxvk { m_module.decorateDescriptorSet(varId, 0); m_module.decorateBinding(varId, bindingId); - // Declare a specialization constant which will - // store whether or not the resource is bound. if (isUav) { DxbcUav uav; uav.type = DxbcResourceType::Typed; @@ -1053,7 +937,6 @@ namespace dxvk { m_textures.at(registerId) = res; } - // Store descriptor info for the shader interface DxvkBindingInfo binding = { }; binding.viewType = typeInfo.vtype; binding.resourceBinding = bindingId; @@ -1084,11 +967,6 @@ namespace dxvk { void DxbcCompiler::emitDclResourceRawStructured(const DxbcShaderInstruction& ins) { - // dcl_resource_raw and dcl_uav_raw take one argument: - // (dst0) The resource register ID - // dcl_resource_structured and dcl_uav_structured take two arguments: - // (dst0) The resource register ID - // (imm0) Structure stride, in bytes const uint32_t registerId = ins.dst[0].idx[0].offset; const bool isUav = ins.op == DxbcOpcode::DclUavRaw @@ -1102,11 +980,9 @@ namespace dxvk { const DxbcImageInfo typeInfo = { spv::DimBuffer, 0, 0, isUav ? 2u : 1u, VK_IMAGE_VIEW_TYPE_MAX_ENUM }; - // Declare the resource type uint32_t resTypeId = 0; uint32_t varId = 0; - // Write back resource info DxbcResourceType resType = isStructured ? DxbcResourceType::Structured : DxbcResourceType::Raw; @@ -1119,12 +995,10 @@ namespace dxvk { ? (resStride & -resStride) : 16; - // Compute the DXVK binding slot index for the resource. uint32_t bindingId = isUav ? computeUavBinding(m_programInfo.type(), registerId) : computeSrvBinding(m_programInfo.type(), registerId); - // Test whether we should use a raw SSBO for this resource bool hasSparseFeedback = isUav ? m_analysis->uavInfos[registerId].sparseFeedback : m_analysis->srvInfos[registerId].sparseFeedback; @@ -1148,8 +1022,6 @@ namespace dxvk { str::format(isUav ? "u" : "t", registerId, "_t").c_str()); m_module.setDebugMemberName(structType, 0, "m"); } else { - // Structured and raw buffers are represented as - // texel buffers consisting of 32-bit integers. m_module.enableCapability(isUav ? spv::CapabilityImageBuffer : spv::CapabilitySampledBuffer); @@ -1197,7 +1069,6 @@ namespace dxvk { m_textures.at(registerId) = res; } - // Store descriptor info for the shader interface DxvkBindingInfo binding = { }; binding.descriptorType = useRawSsbo ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER @@ -1222,7 +1093,6 @@ namespace dxvk { m_bindings.push_back(binding); - // If supported, we'll be using raw access chains to access this if (!m_hasRawAccessChains && m_moduleInfo.options.supportsRawAccessChains) { m_module.enableExtension("SPV_NV_raw_access_chains"); m_module.enableCapability(spv::CapabilityRawAccessChainsNV); @@ -1233,13 +1103,6 @@ namespace dxvk { void DxbcCompiler::emitDclThreadGroupSharedMemory(const DxbcShaderInstruction& ins) { - // dcl_tgsm_raw takes two arguments: - // (dst0) The resource register ID - // (imm0) Block size, in bytes - // dcl_tgsm_structured takes three arguments: - // (dst0) The resource register ID - // (imm0) Structure stride, in bytes - // (imm1) Structure count const bool isStructured = ins.op == DxbcOpcode::DclThreadGroupSharedMemoryStructured; const uint32_t regId = ins.dst[0].idx[0].offset; @@ -1271,9 +1134,6 @@ namespace dxvk { void DxbcCompiler::emitDclGsInputPrimitive(const DxbcShaderInstruction& ins) { - // The input primitive type is stored within in the - // control bits of the opcode token. In SPIR-V, we - // have to define an execution mode. const auto mode = [&] { switch (ins.controls.primitive()) { case DxbcPrimitive::Point: return std::make_pair(VK_PRIMITIVE_TOPOLOGY_POINT_LIST, spv::ExecutionModeInputPoints); @@ -1294,9 +1154,6 @@ namespace dxvk { void DxbcCompiler::emitDclGsOutputTopology(const DxbcShaderInstruction& ins) { - // The input primitive topology is stored within in the - // control bits of the opcode token. In SPIR-V, we have - // to define an execution mode. auto mode = [&] { switch (ins.controls.primitiveTopology()) { case DxbcPrimitiveTopology::PointList: return std::make_pair(VK_PRIMITIVE_TOPOLOGY_POINT_LIST, spv::ExecutionModeOutputPoints); @@ -1312,8 +1169,6 @@ namespace dxvk { void DxbcCompiler::emitDclMaxOutputVertexCount(const DxbcShaderInstruction& ins) { - // dcl_max_output_vertex_count has one operand: - // (imm0) The maximum number of vertices m_gs.outputVertexCount = ins.imm[0].u32; m_module.setOutputVertices(m_entryPointId, m_gs.outputVertexCount); @@ -1321,8 +1176,6 @@ namespace dxvk { void DxbcCompiler::emitDclInputControlPointCount(const DxbcShaderInstruction& ins) { - // dcl_input_control_points has the control point - // count embedded within the opcode token. if (m_programInfo.type() == DxbcProgramType::HullShader) { m_hs.vertexCountIn = ins.controls.controlPointCount(); @@ -1337,8 +1190,6 @@ namespace dxvk { void DxbcCompiler::emitDclOutputControlPointCount(const DxbcShaderInstruction& ins) { - // dcl_output_control_points has the control point - // count embedded within the opcode token. m_hs.vertexCountOut = ins.controls.controlPointCount(); m_hs.outputPerPatch = emitTessInterfacePerPatch(spv::StorageClassPrivate); @@ -1407,10 +1258,6 @@ namespace dxvk { void DxbcCompiler::emitDclThreadGroup(const DxbcShaderInstruction& ins) { - // dcl_thread_group has three operands: - // (imm0) Number of threads in X dimension - // (imm1) Number of threads in Y dimension - // (imm2) Number of threads in Z dimension m_cs.workgroupSizeX = ins.imm[0].u32; m_cs.workgroupSizeY = ins.imm[1].u32; m_cs.workgroupSizeZ = ins.imm[2].u32; @@ -1421,15 +1268,12 @@ namespace dxvk { void DxbcCompiler::emitDclGsInstanceCount(const DxbcShaderInstruction& ins) { - // dcl_gs_instance_count has one operand: - // (imm0) Number of geometry shader invocations m_module.setInvocations(m_entryPointId, ins.imm[0].u32); m_gs.invocationCount = ins.imm[0].u32; } uint32_t DxbcCompiler::emitDclUavCounter(uint32_t regId) { - // Declare a structure type which holds the UAV counter if (m_uavCtrStructType == 0) { const uint32_t t_u32 = m_module.defIntType(32, 0); const uint32_t t_struct = m_module.defStructTypeUnique(1, &t_u32); @@ -1445,7 +1289,6 @@ namespace dxvk { t_struct, spv::StorageClassStorageBuffer); } - // Declare the buffer variable const uint32_t varId = m_module.newVar( m_uavCtrPointerType, spv::StorageClassStorageBuffer); @@ -1458,7 +1301,6 @@ namespace dxvk { m_module.decorateDescriptorSet(varId, 0); m_module.decorateBinding(varId, bindingId); - // Declare the storage buffer binding DxvkBindingInfo binding = { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER }; binding.resourceBinding = bindingId; binding.viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; @@ -1476,8 +1318,6 @@ namespace dxvk { if ((ins.customDataSize & 0x3) != 0) throw DxvkError("DxbcCompiler: Immediate constant buffer size not a multiple of four DWORDs"); - // A lot of the time we'll be dealing with a scalar or vec2 - // array here, there's no reason to emit all those zeroes. uint32_t componentCount = 1u; for (uint32_t i = 0; i < ins.customDataSize; i += 4u) { @@ -1507,7 +1347,6 @@ namespace dxvk { uint32_t dwordCount, const uint32_t* dwordArray, uint32_t componentCount) { - // Declare individual vector constants as 4x32-bit vectors small_vector vectorIds; DxbcVectorType vecType; @@ -1530,11 +1369,8 @@ namespace dxvk { vectorIds.push_back(id); } - // Pad array with one entry of zeroes so that we can - // handle out-of-bounds accesses more conveniently. vectorIds.push_back(emitBuildZeroVector(vecType).id); - // Declare the array that contains all the vectors DxbcArrayType arrInfo; arrInfo.ctype = DxbcScalarType::Uint32; arrInfo.ccount = componentCount; @@ -1544,8 +1380,6 @@ namespace dxvk { uint32_t arrayId = m_module.constComposite( arrayTypeId, vectorIds.size(), vectorIds.data()); - // Declare the variable that will hold the constant - // data and initialize it with the constant array. uint32_t pointerTypeId = m_module.defPointerType( arrayTypeId, spv::StorageClassPrivate); @@ -1567,15 +1401,11 @@ namespace dxvk { uint32_t componentCount) { uint32_t vectorCount = dwordCount / 4u; - // Tightly pack vec2 or scalar arrays if possible. Don't bother with - // vec3 since we'd rather have properly vectorized loads in that case. if (m_moduleInfo.options.supportsTightIcbPacking && componentCount <= 2u) m_icbComponents = componentCount; else m_icbComponents = 4u; - // Immediate constant buffer can be read out of bounds, declare - // it with the maximum possible size and rely on robustness. this->emitDclConstantBufferVar(Icb_BindingSlotId, 4096u, m_icbComponents, "icb"); m_icbData.reserve(vectorCount * componentCount); @@ -1618,15 +1448,11 @@ namespace dxvk { const uint32_t typeId = getVectorTypeId(dst.type); switch (ins.op) { - ///////////////////// - // Move instructions case DxbcOpcode::Mov: case DxbcOpcode::DMov: dst.id = src.at(0).id; break; - ///////////////////////////////////// - // ALU operations on float32 numbers case DxbcOpcode::Add: case DxbcOpcode::DAdd: dst.id = m_module.opFAdd(typeId, @@ -1657,8 +1483,6 @@ namespace dxvk { case DxbcOpcode::Mad: case DxbcOpcode::DFma: if (ins.controls.precise()) { - // FXC only emits precise mad if the shader explicitly uses - // the HLSL mad()/fma() intrinsics, let's preserve that. dst.id = m_module.opFFma(typeId, src.at(0).id, src.at(1).id, src.at(2).id); } else { @@ -1730,8 +1554,6 @@ namespace dxvk { typeId, src.at(0).id); break; - ///////////////////////////////////// - // ALU operations on signed integers case DxbcOpcode::IAdd: dst.id = m_module.opIAdd(typeId, src.at(0).id, src.at(1).id); @@ -1760,8 +1582,6 @@ namespace dxvk { typeId, src.at(0).id); break; - /////////////////////////////////////// - // ALU operations on unsigned integers case DxbcOpcode::UMax: dst.id = m_module.opUMax(typeId, src.at(0).id, src.at(1).id); @@ -1772,8 +1592,6 @@ namespace dxvk { src.at(0).id, src.at(1).id); break; - /////////////////////////////////////// - // Bit operations on unsigned integers case DxbcOpcode::And: dst.id = m_module.opBitwiseAnd(typeId, src.at(0).id, src.at(1).id); @@ -1804,8 +1622,6 @@ namespace dxvk { typeId, src.at(0).id); break; - /////////////////////////// - // Conversion instructions case DxbcOpcode::ItoF: dst.id = m_module.opConvertStoF( typeId, src.at(0).id); @@ -1836,19 +1652,12 @@ namespace dxvk { if (ins.controls.precise() || m_precise) m_module.decorate(dst.id, spv::DecorationNoContraction); - // Store computed value dst = emitDstOperandModifiers(dst, ins.modifiers); emitRegisterStore(ins.dst[0], dst); } void DxbcCompiler::emitVectorCmov(const DxbcShaderInstruction& ins) { - // movc and swapc have the following operands: - // (dst0) The first destination register - // (dst1) The second destination register (swapc only) - // (src0) The condition vector - // (src1) Vector to select from if the condition is not 0 - // (src2) Vector to select from if the condition is 0 DxbcRegMask condMask = ins.dst[0].mask; if (ins.dst[0].dataType == DxbcScalarType::Float64) { @@ -1864,8 +1673,6 @@ namespace dxvk { uint32_t componentCount = condMask.popCount(); - // We'll compare against a vector of zeroes to generate a - // boolean vector, which in turn will be used by OpSelect uint32_t zeroType = m_module.defIntType(32, 0); uint32_t boolType = m_module.defBoolType(); @@ -1879,8 +1686,6 @@ namespace dxvk { zero = m_module.constComposite(zeroType, componentCount, zeroVec.data()); } - // In case of swapc, the second destination operand receives - // the output that a cmov instruction would normally get const uint32_t trueIndex = ins.op == DxbcOpcode::Swapc ? 1 : 0; for (uint32_t i = 0; i < ins.dstCount; i++) { @@ -1899,14 +1704,8 @@ namespace dxvk { } void DxbcCompiler::emitVectorCmp(const DxbcShaderInstruction& ins) { - // Compare instructions have three operands: - // (dst0) The destination register - // (src0) The first vector to compare - // (src1) The second vector to compare uint32_t componentCount = ins.dst[0].mask.popCount(); - // For 64-bit operations, we'll return a 32-bit - // vector, so we have to adjust the read mask DxbcRegMask srcMask = ins.dst[0].mask; if (isDoubleType(ins.src[0].dataType)) { @@ -1920,8 +1719,6 @@ namespace dxvk { emitRegisterLoad(ins.src[1], srcMask), }; - // Condition, which is a boolean vector used - // to select between the ~0u and 0u vectors. uint32_t condition = 0; uint32_t conditionType = m_module.defBoolType(); @@ -1991,7 +1788,6 @@ namespace dxvk { return; } - // Generate constant vectors for selection uint32_t sFalse = m_module.constu32( 0u); uint32_t sTrue = m_module.constu32(~0u); @@ -2012,8 +1808,6 @@ namespace dxvk { if (invert) std::swap(sFalse, sTrue); - // Perform component-wise mask selection - // based on the condition evaluated above. result.id = m_module.opSelect( typeId, condition, sTrue, sFalse); @@ -2022,9 +1816,6 @@ namespace dxvk { void DxbcCompiler::emitVectorDeriv(const DxbcShaderInstruction& ins) { - // Derivative instructions have two operands: - // (dst0) Destination register for the derivative - // (src0) The operand to compute the derivative of DxbcRegisterValue value = emitRegisterLoad(ins.src[0], ins.dst[0].mask); const uint32_t typeId = getVectorTypeId(value.type); @@ -2096,9 +1887,6 @@ namespace dxvk { m_module.opCompositeExtract(componentType, src.at(1).id, 1, &i)); } - // Unconditionally mark as precise since the exact order of operation - // matters for some games, even if the instruction itself is not marked - // as precise. m_module.decorate(dst.id, spv::DecorationNoContraction); } @@ -2108,16 +1896,10 @@ namespace dxvk { void DxbcCompiler::emitVectorIdiv(const DxbcShaderInstruction& ins) { - // udiv has four operands: - // (dst0) Quotient destination register - // (dst1) Remainder destination register - // (src0) The first vector to compare - // (src1) The second vector to compare if (ins.dst[0].type == DxbcOperandType::Null && ins.dst[1].type == DxbcOperandType::Null) return; - // FIXME support this if applications require it if (ins.dst[0].type != DxbcOperandType::Null && ins.dst[1].type != DxbcOperandType::Null && ins.dst[0].mask != ins.dst[1].mask) { @@ -2125,8 +1907,6 @@ namespace dxvk { return; } - // Load source operands as integers with the - // mask of one non-NULL destination operand const DxbcRegMask srcMask = ins.dst[0].type != DxbcOperandType::Null ? ins.dst[0].mask @@ -2137,7 +1917,6 @@ namespace dxvk { emitRegisterLoad(ins.src[1], srcMask), }; - // Division by zero will return 0xffffffff for both results auto bvecId = getVectorTypeId({ DxbcScalarType::Bool, srcMask.popCount() }); DxbcRegisterValue const0 = emitBuildConstVecu32( 0u, 0u, 0u, 0u, srcMask); @@ -2145,8 +1924,6 @@ namespace dxvk { uint32_t cmpValue = m_module.opINotEqual(bvecId, src.at(1).id, const0.id); - // Compute results only if the destination - // operands are not NULL. if (ins.dst[0].type != DxbcOperandType::Null) { DxbcRegisterValue quotient; quotient.type.ctype = ins.dst[0].dataType; @@ -2184,17 +1961,10 @@ namespace dxvk { void DxbcCompiler::emitVectorImul(const DxbcShaderInstruction& ins) { - // imul and umul have four operands: - // (dst0) High destination register - // (dst1) Low destination register - // (src0) The first vector to compare - // (src1) The second vector to compare if (ins.dst[0].type == DxbcOperandType::Null) { if (ins.dst[1].type == DxbcOperandType::Null) return; - // If dst0 is NULL, this instruction behaves just - // like any other three-operand ALU instruction const std::array src = { emitRegisterLoad(ins.src[0], ins.dst[1].mask), emitRegisterLoad(ins.src[1], ins.dst[1].mask), @@ -2210,18 +1980,12 @@ namespace dxvk { result = emitDstOperandModifiers(result, ins.modifiers); emitRegisterStore(ins.dst[1], result); } else { - // TODO implement this Logger::warn("DxbcCompiler: Extended Imul not yet supported"); } } void DxbcCompiler::emitVectorMsad(const DxbcShaderInstruction& ins) { - // msad has four operands: - // (dst0) Destination - // (src0) Reference (packed uint8) - // (src1) Source (packed uint8) - // (src2) Accumulator DxbcRegisterValue refReg = emitRegisterLoad(ins.src[0], ins.dst[0].mask); DxbcRegisterValue srcReg = emitRegisterLoad(ins.src[1], ins.dst[0].mask); DxbcRegisterValue result = emitRegisterLoad(ins.src[2], ins.dst[0].mask); @@ -2249,10 +2013,6 @@ namespace dxvk { void DxbcCompiler::emitVectorShift(const DxbcShaderInstruction& ins) { - // Shift operations have three operands: - // (dst0) The destination register - // (src0) The register to shift - // (src1) The shift amount (scalar) DxbcRegisterValue shiftReg = emitRegisterLoad(ins.src[0], ins.dst[0].mask); DxbcRegisterValue countReg = emitRegisterLoad(ins.src[1], ins.dst[0].mask); @@ -2298,12 +2058,7 @@ namespace dxvk { void DxbcCompiler::emitVectorSinCos(const DxbcShaderInstruction& ins) { - // sincos has three operands: - // (dst0) Destination register for sin(x) - // (dst1) Destination register for cos(x) - // (src0) Source operand x - // Load source operand as 32-bit float vector. const DxbcRegisterValue srcValue = emitRegisterLoad( ins.src[0], DxbcRegMask(true, true, true, true)); @@ -2315,7 +2070,6 @@ namespace dxvk { DxbcRegisterValue cosVector = { }; cosVector.type.ctype = DxbcScalarType::Float32; - // Only compute sincos for enabled components std::array sinIds = { }; std::array cosIds = { }; @@ -2353,8 +2107,6 @@ namespace dxvk { void DxbcCompiler::emitGeometryEmit(const DxbcShaderInstruction& ins) { - // In xfb mode we might have multiple streams, so - // we have to figure out which stream to write to uint32_t streamId = 0; uint32_t streamVar = 0; @@ -2363,7 +2115,6 @@ namespace dxvk { streamVar = m_module.constu32(streamId); } - // Checking the negation is easier for EmitThenCut/EmitThenCutStream bool doEmit = ins.op != DxbcOpcode::Cut && ins.op != DxbcOpcode::CutStream; bool doCut = ins.op != DxbcOpcode::Emit && ins.op != DxbcOpcode::EmitStream; @@ -2382,26 +2133,15 @@ namespace dxvk { void DxbcCompiler::emitAtomic(const DxbcShaderInstruction& ins) { - // atomic_* operations have the following operands: - // (dst0) Destination u# or g# register - // (src0) Index into the texture or buffer - // (src1) The source value for the operation - // (src2) Second source operand (optional) - // imm_atomic_* operations have the following operands: - // (dst0) Register that receives the result - // (dst1) Destination u# or g# register - // (srcX) As above const DxbcBufferInfo bufferInfo = getBufferInfo(ins.dst[ins.dstCount - 1]); bool isImm = ins.dstCount == 2; bool isUav = ins.dst[ins.dstCount - 1].type == DxbcOperandType::UnorderedAccessView; bool isSsbo = bufferInfo.isSsbo; - // Retrieve destination pointer for the atomic operation> const DxbcRegisterPointer pointer = emitGetAtomicPointer( ins.dst[ins.dstCount - 1], ins.src[0]); - // Load source values std::array src; for (uint32_t i = 1; i < ins.srcCount; i++) { @@ -2410,7 +2150,6 @@ namespace dxvk { pointer.type.ctype); } - // Define memory scope and semantics based on the operands uint32_t scope = 0; uint32_t semantics = 0; @@ -2430,12 +2169,10 @@ namespace dxvk { const uint32_t scopeId = m_module.constu32(scope); const uint32_t semanticsId = m_module.constu32(semantics); - // Perform the atomic operation on the given pointer DxbcRegisterValue value; value.type = pointer.type; value.id = 0; - // The result type, which is a scalar integer const uint32_t typeId = getVectorTypeId(value.type); switch (ins.op) { @@ -2516,23 +2253,17 @@ namespace dxvk { return; } - // Write back the result to the destination - // register if this is an imm_atomic_* opcode. if (isImm) emitRegisterStore(ins.dst[0], value); } void DxbcCompiler::emitAtomicCounter(const DxbcShaderInstruction& ins) { - // imm_atomic_alloc and imm_atomic_consume have the following operands: - // (dst0) The register that will hold the old counter value - // (dst1) The UAV whose counter is going to be modified const uint32_t registerId = ins.dst[1].idx[0].offset; if (m_uavs.at(registerId).ctrId == 0) m_uavs.at(registerId).ctrId = emitDclUavCounter(registerId); - // Get a pointer to the atomic counter in question DxbcRegisterInfo ptrType; ptrType.type.ctype = DxbcScalarType::Uint32; ptrType.type.ccount = 1; @@ -2545,7 +2276,6 @@ namespace dxvk { m_uavs.at(registerId).ctrId, 1, &zeroId); - // Define memory scope and semantics based on the operands uint32_t scope = spv::ScopeQueueFamily; uint32_t semantics = spv::MemorySemanticsUniformMemoryMask | spv::MemorySemanticsAcquireReleaseMask; @@ -2553,7 +2283,6 @@ namespace dxvk { uint32_t scopeId = m_module.constu32(scope); uint32_t semanticsId = m_module.constu32(semantics); - // Compute the result value DxbcRegisterValue value; value.type.ctype = DxbcScalarType::Uint32; value.type.ccount = 1; @@ -2580,14 +2309,11 @@ namespace dxvk { return; } - // Store the result emitRegisterStore(ins.dst[0], value); } void DxbcCompiler::emitBarrier(const DxbcShaderInstruction& ins) { - // sync takes no operands. Instead, the synchronization - // scope is defined by the operand control bits. const DxbcSyncFlags flags = ins.controls.syncFlags(); uint32_t executionScope = spv::ScopeInvocation; @@ -2643,11 +2369,6 @@ namespace dxvk { void DxbcCompiler::emitBitExtract(const DxbcShaderInstruction& ins) { - // ibfe and ubfe take the following arguments: - // (dst0) The destination register - // (src0) Number of bits to extact - // (src1) Offset of the bits to extract - // (src2) Register to extract bits from const bool isSigned = ins.op == DxbcOpcode::IBfe; DxbcRegisterValue bitCnt = emitRegisterLoad(ins.src[0], ins.dst[0].mask); @@ -2688,12 +2409,6 @@ namespace dxvk { void DxbcCompiler::emitBitInsert(const DxbcShaderInstruction& ins) { - // ibfe and ubfe take the following arguments: - // (dst0) The destination register - // (src0) Number of bits to extact - // (src1) Offset of the bits to extract - // (src2) Register to take bits from - // (src3) Register to replace bits in DxbcRegisterValue bitCnt = emitRegisterLoad(ins.src[0], ins.dst[0].mask); DxbcRegisterValue bitOfs = emitRegisterLoad(ins.src[1], ins.dst[0].mask); @@ -2733,16 +2448,12 @@ namespace dxvk { void DxbcCompiler::emitBitScan(const DxbcShaderInstruction& ins) { - // firstbit(lo|hi|shi) have two operands: - // (dst0) The destination operant - // (src0) Source operand to scan DxbcRegisterValue src = emitRegisterLoad(ins.src[0], ins.dst[0].mask); DxbcRegisterValue dst; dst.type.ctype = ins.dst[0].dataType; dst.type.ccount = ins.dst[0].mask.popCount(); - // Result type, should be an unsigned integer const uint32_t typeId = getVectorTypeId(dst.type); switch (ins.op) { @@ -2752,8 +2463,6 @@ namespace dxvk { default: Logger::warn(str::format("DxbcCompiler: Unhandled instruction: ", ins.op)); return; } - // The 'Hi' variants are counted from the MSB in DXBC - // rather than the LSB, so we have to invert the number if (ins.op == DxbcOpcode::FirstBitHi || ins.op == DxbcOpcode::FirstBitShi) { uint32_t boolTypeId = m_module.defBoolType(); @@ -2769,28 +2478,20 @@ namespace dxvk { constff.id); } - // No modifiers are supported emitRegisterStore(ins.dst[0], dst); } void DxbcCompiler::emitBufferQuery(const DxbcShaderInstruction& ins) { - // bufinfo takes two arguments - // (dst0) The destination register - // (src0) The buffer register to query const DxbcBufferInfo bufferInfo = getBufferInfo(ins.src[0]); bool isSsbo = bufferInfo.isSsbo; - // We'll store this as a scalar unsigned integer DxbcRegisterValue result = isSsbo ? emitQueryBufferSize(ins.src[0]) : emitQueryTexelBufferSize(ins.src[0]); uint32_t typeId = getVectorTypeId(result.type); - // Adjust returned size if this is a raw or structured - // buffer, as emitQueryTexelBufferSize only returns the - // number of typed elements in the buffer. if (bufferInfo.type == DxbcResourceType::Raw) { result.id = m_module.opIMul(typeId, result.id, m_module.constu32(4)); @@ -2799,56 +2500,34 @@ namespace dxvk { m_module.constu32(bufferInfo.stride / 4)); } - // Store the result. The scalar will be extended to a - // vector if the write mask consists of more than one - // component, which is the desired behaviour. emitRegisterStore(ins.dst[0], result); } void DxbcCompiler::emitBufferLoad(const DxbcShaderInstruction& ins) { - // ld_raw takes three arguments: - // (dst0) Destination register - // (src0) Byte offset - // (src1) Source register - // ld_structured takes four arguments: - // (dst0) Destination register - // (src0) Structure index - // (src1) Byte offset - // (src2) Source register const bool isStructured = ins.op == DxbcOpcode::LdStructured || ins.op == DxbcOpcode::LdStructuredS; - // Source register. The exact way we access - // the data depends on the register type. const DxbcRegister& dstReg = ins.dst[0]; const DxbcRegister& srcReg = isStructured ? ins.src[2] : ins.src[1]; if (dstReg.type == DxbcOperandType::UnorderedAccessView) emitUavBarrier(uint64_t(1u) << srcReg.idx[0].offset, 0u); - // Retrieve common info about the buffer const DxbcBufferInfo bufferInfo = getBufferInfo(srcReg); - // Shared memory is the only type of buffer that - // is not accessed through a texel buffer view bool isTgsm = srcReg.type == DxbcOperandType::ThreadGroupSharedMemory; bool isSsbo = bufferInfo.isSsbo; - // Common types and IDs used while loading the data uint32_t bufferId = isTgsm || isSsbo ? 0 : m_module.opLoad(bufferInfo.typeId, bufferInfo.varId); uint32_t vectorTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 4 }); uint32_t scalarTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 1 }); - // Since all data is represented as a sequence of 32-bit - // integers, we have to load each component individually. std::array ccomps = { 0, 0, 0, 0 }; std::array scomps = { 0, 0, 0, 0 }; uint32_t scount = 0; - // The sparse feedback ID will be non-zero for sparse - // instructions on input. We need to reset it to 0. SpirvMemoryOperands memoryOperands; SpirvImageOperands imageOperands; imageOperands.sparse = ins.dstCount == 2; @@ -2987,8 +2666,6 @@ namespace dxvk { throw DxvkError("DxbcCompiler: Invalid operand type for strucured/raw load"); } - // Only read sparse feedback once. This may be somewhat inaccurate - // for reads that straddle pages, but we can't easily emulate this. if (imageOperands.sparse) { imageOperands.sparse = false; sparseFeedbackId = resultId; @@ -3029,19 +2706,8 @@ namespace dxvk { void DxbcCompiler::emitBufferStore(const DxbcShaderInstruction& ins) { - // store_raw takes three arguments: - // (dst0) Destination register - // (src0) Byte offset - // (src1) Source register - // store_structured takes four arguments: - // (dst0) Destination register - // (src0) Structure index - // (src1) Byte offset - // (src2) Source register const bool isStructured = ins.op == DxbcOpcode::StoreStructured; - // Source register. The exact way we access - // the data depends on the register type. const DxbcRegister& dstReg = ins.dst[0]; const DxbcRegister& srcReg = isStructured ? ins.src[2] : ins.src[1]; @@ -3051,10 +2717,8 @@ namespace dxvk { DxbcRegisterValue value = emitRegisterLoad(srcReg, dstReg.mask); value = emitRegisterBitcast(value, DxbcScalarType::Uint32); - // Retrieve common info about the buffer const DxbcBufferInfo bufferInfo = getBufferInfo(dstReg); - // Thread Group Shared Memory is not accessed through a texel buffer view bool isTgsm = dstReg.type == DxbcOperandType::ThreadGroupSharedMemory; bool isSsbo = bufferInfo.isSsbo; @@ -3063,7 +2727,6 @@ namespace dxvk { uint32_t scalarTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 1 }); uint32_t vectorTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 4 }); - // Set memory operands according to resource properties SpirvMemoryOperands memoryOperands; SpirvImageOperands imageOperands; @@ -3087,7 +2750,6 @@ namespace dxvk { } } - // Compute flat element index as necessary bool useRawAccessChains = isSsbo && m_hasRawAccessChains; DxbcRegisterValue index = emitRegisterLoad(ins.src[0], DxbcRegMask(true, false, false, false)); @@ -3206,42 +2868,31 @@ namespace dxvk { void DxbcCompiler::emitConvertFloat16(const DxbcShaderInstruction& ins) { - // f32tof16 takes two operands: - // (dst0) Destination register as a uint32 vector - // (src0) Source register as a float32 vector - // f16tof32 takes two operands: - // (dst0) Destination register as a float32 vector - // (src0) Source register as a uint32 vector const DxbcRegisterValue src = emitRegisterLoad(ins.src[0], ins.dst[0].mask); - // We handle both packing and unpacking here const bool isPack = ins.op == DxbcOpcode::F32toF16; - // The conversion instructions do not map very well to the - // SPIR-V pack instructions, which operate on 2D vectors. std::array scalarIds = {{ 0, 0, 0, 0 }}; const uint32_t componentCount = src.type.ccount; - // These types are used in both pack and unpack operations const uint32_t t_u32 = getVectorTypeId({ DxbcScalarType::Uint32, 1 }); const uint32_t t_f32 = getVectorTypeId({ DxbcScalarType::Float32, 1 }); const uint32_t t_f32v2 = getVectorTypeId({ DxbcScalarType::Float32, 2 }); - // Constant zero-bit pattern, used for packing const uint32_t zerof32 = isPack ? m_module.constf32(0.0f) : 0; for (uint32_t i = 0; i < componentCount; i++) { const DxbcRegisterValue componentValue = emitRegisterExtract(src, DxbcRegMask::select(i)); - if (isPack) { // f32tof16 + if (isPack) { const std::array packIds = {{ componentValue.id, zerof32 }}; scalarIds[i] = m_module.opPackHalf2x16(t_u32, m_module.opCompositeConstruct(t_f32v2, packIds.size(), packIds.data())); - } else { // f16tof32 + } else { const uint32_t zeroIndex = 0; scalarIds[i] = m_module.opCompositeExtract(t_f32, @@ -3261,10 +2912,6 @@ namespace dxvk { : scalarIds[0]; if (isPack) { - // Some drivers return infinity if the input value is above a certain - // threshold, but D3D wants us to return infinity only if the input is - // actually infinite. Fix this up to return the maximum representable - // 16-bit floating point number instead, but preserve input infinity. uint32_t t_bvec = getVectorTypeId({ DxbcScalarType::Bool, componentCount }); uint32_t f16Infinity = m_module.constuReplicant(0x7C00, componentCount); uint32_t f16Unsigned = m_module.constuReplicant(0x7FFF, componentCount); @@ -3279,22 +2926,17 @@ namespace dxvk { result.id); } - // Store result in the destination register emitRegisterStore(ins.dst[0], result); } void DxbcCompiler::emitConvertFloat64(const DxbcShaderInstruction& ins) { - // ftod and dtof take the following operands: - // (dst0) Destination operand - // (src0) Number to convert uint32_t dstBits = ins.dst[0].mask.popCount(); DxbcRegMask srcMask = isDoubleType(ins.dst[0].dataType) ? DxbcRegMask(dstBits >= 2, dstBits >= 4, false, false) : DxbcRegMask(dstBits >= 1, dstBits >= 1, dstBits >= 2, dstBits >= 2); - // Perform actual conversion, destination modifiers are not applied DxbcRegisterValue val = emitRegisterLoad(ins.src[0], srcMask); DxbcRegisterValue result; @@ -3393,8 +3035,6 @@ namespace dxvk { void DxbcCompiler::emitInterpolate(const DxbcShaderInstruction& ins) { m_module.enableCapability(spv::CapabilityInterpolationFunction); - // The SPIR-V instructions operate on input variable pointers, - // which are all declared as four-component float vectors. uint32_t registerId = ins.src[0].idx[0].offset; DxbcRegisterValue result; @@ -3418,7 +3058,6 @@ namespace dxvk { } break; case DxbcOpcode::EvalSnapped: { - // The offset is encoded as a 4-bit fixed point value DxbcRegisterValue offset = emitRegisterLoad( ins.src[1], DxbcRegMask(true, true, false, false)); offset.id = m_module.opBitFieldSExtract( @@ -3454,9 +3093,6 @@ namespace dxvk { void DxbcCompiler::emitSparseCheckAccess( const DxbcShaderInstruction& ins) { - // check_access_mapped has two operands: - // (dst0) The destination register - // (src0) The residency code m_module.enableCapability(spv::CapabilitySparseResidency); DxbcRegisterValue srcValue = emitRegisterLoad(ins.src[0], ins.dst[0].mask); @@ -3474,34 +3110,22 @@ namespace dxvk { void DxbcCompiler::emitTextureQuery(const DxbcShaderInstruction& ins) { - // resinfo has three operands: - // (dst0) The destination register - // (src0) Resource LOD to query - // (src1) Resource to query const DxbcBufferInfo resourceInfo = getBufferInfo(ins.src[1]); const DxbcResinfoType resinfoType = ins.controls.resinfoType(); - // Read the exact LOD for the image query const DxbcRegisterValue mipLod = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); const DxbcScalarType returnType = resinfoType == DxbcResinfoType::Uint ? DxbcScalarType::Uint32 : DxbcScalarType::Float32; - // Query the size of the selected mip level, as well as the - // total number of mip levels. We will have to combine the - // result into a four-component vector later. DxbcRegisterValue imageSize = emitQueryTextureSize(ins.src[1], mipLod); DxbcRegisterValue imageLevels = emitQueryTextureLods(ins.src[1]); - // If the mip level is out of bounds, D3D requires us to return - // zero before applying modifiers, whereas SPIR-V is undefined, - // so we need to fix it up manually here. imageSize.id = m_module.opSelect(getVectorTypeId(imageSize.type), m_module.opULessThan(m_module.defBoolType(), mipLod.id, imageLevels.id), imageSize.id, emitBuildZeroVector(imageSize.type).id); - // Convert intermediates to the requested type if (returnType == DxbcScalarType::Float32) { imageSize.type.ctype = DxbcScalarType::Float32; imageSize.id = m_module.opConvertUtoF( @@ -3514,9 +3138,6 @@ namespace dxvk { imageLevels.id); } - // If the selected return type is rcpFloat, we need - // to compute the reciprocal of the image dimensions, - // but not the array size, so we need to separate it. const uint32_t imageCoordDim = imageSize.type.ccount; DxbcRegisterValue imageLayers; @@ -3536,8 +3157,6 @@ namespace dxvk { imageSize.id); } - // Concatenate result vectors and scalars to form a - // 4D vector. Unused components will be set to zero. std::array vectorIds = { imageSize.id, 0, 0, 0 }; uint32_t numVectorIds = 1; @@ -3555,7 +3174,6 @@ namespace dxvk { vectorIds[numVectorIds++] = imageLevels.id; - // Create the actual result vector DxbcRegisterValue result; result.type.ctype = returnType; result.type.ccount = 4; @@ -3563,8 +3181,6 @@ namespace dxvk { getVectorTypeId(result.type), numVectorIds, vectorIds.data()); - // Swizzle components using the resource swizzle - // and the destination operand's write mask result = emitRegisterSwizzle(result, ins.src[1].swizzle, ins.dst[0].mask); emitRegisterStore(ins.dst[0], result); @@ -3572,32 +3188,21 @@ namespace dxvk { void DxbcCompiler::emitTextureQueryLod(const DxbcShaderInstruction& ins) { - // All sample instructions have at least these operands: - // (dst0) The destination register - // (src0) Texture coordinates - // (src1) The texture itself - // (src2) The sampler object const DxbcRegister& texCoordReg = ins.src[0]; const DxbcRegister& textureReg = ins.src[1]; const DxbcRegister& samplerReg = ins.src[2]; - // Texture and sampler register IDs const auto& texture = m_textures.at(textureReg.idx[0].offset); const auto& sampler = m_samplers.at(samplerReg.idx[0].offset); - // Load texture coordinates const DxbcRegisterValue coord = emitRegisterLoad(texCoordReg, DxbcRegMask::firstN(getTexLayerDim(texture.imageInfo))); - // Query the LOD. The result is a two-dimensional float32 - // vector containing the mip level and virtual LOD numbers. const uint32_t sampledImageId = emitLoadSampledImage(texture, sampler, false); const uint32_t queriedLodId = m_module.opImageQueryLod( getVectorTypeId({ DxbcScalarType::Float32, 2 }), sampledImageId, coord.id); - // Build the result array vector by filling up - // the remaining two components with zeroes. const uint32_t zero = m_module.constf32(0.0f); const std::array resultIds = {{ queriedLodId, zero, zero }}; @@ -3614,9 +3219,6 @@ namespace dxvk { void DxbcCompiler::emitTextureQueryMs(const DxbcShaderInstruction& ins) { - // sampleinfo has two operands: - // (dst0) The destination register - // (src0) Resource to query DxbcRegisterValue sampleCount = emitQueryTextureSamples(ins.src[0]); if (ins.controls.returnType() != DxbcInstructionReturnType::Uint) { @@ -3631,15 +3233,9 @@ namespace dxvk { void DxbcCompiler::emitTextureQueryMsPos(const DxbcShaderInstruction& ins) { - // samplepos has three operands: - // (dst0) The destination register - // (src0) Resource to query - // (src1) Sample index if (m_samplePositions == 0) m_samplePositions = emitSamplePosArray(); - // The lookup index is qual to the sample count plus the - // sample index, or 0 if the resource cannot be queried. DxbcRegisterValue sampleCount = emitQueryTextureSamples(ins.src[0]); DxbcRegisterValue sampleIndex = emitRegisterLoad( ins.src[1], DxbcRegMask(true, false, false, false)); @@ -3648,7 +3244,6 @@ namespace dxvk { getVectorTypeId(sampleCount.type), sampleCount.id, sampleIndex.id); - // Validate the parameters uint32_t sampleCountValid = m_module.opULessThanEqual( m_module.defBoolType(), sampleCount.id, @@ -3659,8 +3254,6 @@ namespace dxvk { sampleIndex.id, sampleCount.id); - // If the lookup cannot be performed, set the lookup - // index to zero, which will return a zero vector. lookupIndex = m_module.opSelect( getVectorTypeId(sampleCount.type), m_module.opLogicalAnd( @@ -3670,8 +3263,6 @@ namespace dxvk { lookupIndex, m_module.constu32(0)); - // Load sample pos vector and write the masked - // components to the destination register. DxbcRegisterPointer samplePos; samplePos.type.ctype = DxbcScalarType::Float32; samplePos.type.ccount = 2; @@ -3681,7 +3272,6 @@ namespace dxvk { spv::StorageClassPrivate), m_samplePositions, 1, &lookupIndex); - // Expand to vec4 by appending zeroes DxbcRegisterValue result = emitValueLoad(samplePos); DxbcRegisterValue zero; @@ -3699,28 +3289,15 @@ namespace dxvk { void DxbcCompiler::emitTextureFetch(const DxbcShaderInstruction& ins) { - // ld has three operands: - // (dst0) The destination register - // (src0) Source address - // (src1) Source texture - // ld2dms has four operands: - // (dst0) The destination register - // (src0) Source address - // (src1) Source texture - // (src2) Sample number const auto& texture = m_textures.at(ins.src[1].idx[0].offset); const uint32_t imageLayerDim = getTexLayerDim(texture.imageInfo); bool isMultisampled = ins.op == DxbcOpcode::LdMs || ins.op == DxbcOpcode::LdMsS; - // Load the texture coordinates. The last component - // contains the LOD if the resource is an image. const DxbcRegisterValue address = emitRegisterLoad( ins.src[0], DxbcRegMask(true, true, true, true)); - // Additional image operands. This will store - // the LOD and the address offset if present. SpirvImageOperands imageOperands; imageOperands.sparse = ins.dstCount == 2; @@ -3741,8 +3318,6 @@ namespace dxvk { } } - // The LOD is not present when reading from - // a buffer or from a multisample texture. if (texture.imageInfo.dim != spv::DimBuffer && texture.imageInfo.ms == 0) { DxbcRegisterValue imageLod; @@ -3750,7 +3325,6 @@ namespace dxvk { imageLod = emitRegisterExtract( address, DxbcRegMask(false, false, false, true)); } else { - // If we force-disabled MSAA, fetch from LOD 0 imageLod.type = { DxbcScalarType::Uint32, 1 }; imageLod.id = m_module.constu32(0); } @@ -3759,8 +3333,6 @@ namespace dxvk { imageOperands.sLod = imageLod.id; } - // The ld2dms instruction has a sample index, but we - // are only allowed to set it for multisample views if (isMultisampled && texture.imageInfo.ms == 1) { DxbcRegisterValue sampleId = emitRegisterLoad( ins.src[2], DxbcRegMask(true, false, false, false)); @@ -3769,11 +3341,8 @@ namespace dxvk { imageOperands.sSampleId = sampleId.id; } - // Extract coordinates from address const DxbcRegisterValue coord = emitCalcTexCoord(address, texture.imageInfo); - // Reading a typed image or buffer view - // always returns a four-component vector. const uint32_t imageId = m_module.opLoad(texture.imageTypeId, texture.varId); DxbcVectorType texelType; @@ -3796,8 +3365,6 @@ namespace dxvk { ? emitExtractSparseTexel(texelTypeId, resultId) : resultId; - // Swizzle components using the texture swizzle - // and the destination operand's write mask result = emitRegisterSwizzle(result, ins.src[1].swizzle, ins.dst[0].mask); @@ -3809,18 +3376,6 @@ namespace dxvk { void DxbcCompiler::emitTextureGather(const DxbcShaderInstruction& ins) { - // Gather4 takes the following operands: - // (dst0) The destination register - // (dst1) The residency code for sparse ops - // (src0) Texture coordinates - // (src1) The texture itself - // (src2) The sampler, with a component selector - // Gather4C takes the following additional operand: - // (src3) The depth reference value - // The Gather4Po variants take an additional operand - // which defines an extended constant offset. - // TODO reduce code duplication by moving some common code - // in both sample() and gather() into separate methods const bool isExtendedGather = ins.op == DxbcOpcode::Gather4Po || ins.op == DxbcOpcode::Gather4PoC || ins.op == DxbcOpcode::Gather4PoS @@ -3830,18 +3385,13 @@ namespace dxvk { const DxbcRegister& textureReg = ins.src[1 + isExtendedGather]; const DxbcRegister& samplerReg = ins.src[2 + isExtendedGather]; - // Texture and sampler register IDs const auto& texture = m_textures.at(textureReg.idx[0].offset); const auto& sampler = m_samplers.at(samplerReg.idx[0].offset); - // Image type, which stores the image dimensions etc. const uint32_t imageLayerDim = getTexLayerDim(texture.imageInfo); - // Load the texture coordinates. SPIR-V allows these - // to be float4 even if not all components are used. DxbcRegisterValue coord = emitLoadTexCoord(texCoordReg, texture.imageInfo); - // Load reference value for depth-compare operations const bool isDepthCompare = ins.op == DxbcOpcode::Gather4C || ins.op == DxbcOpcode::Gather4PoC || ins.op == DxbcOpcode::Gather4CS @@ -3852,7 +3402,6 @@ namespace dxvk { DxbcRegMask(true, false, false, false)) : DxbcRegisterValue(); - // Accumulate additional image operands. SpirvImageOperands imageOperands; imageOperands.sparse = ins.dstCount == 2; @@ -3881,8 +3430,6 @@ namespace dxvk { } } - // Gathering texels always returns a four-component - // vector, even for the depth-compare variants. uint32_t sampledImageId = emitLoadSampledImage(texture, sampler, isDepthCompare); DxbcVectorType texelType; @@ -3898,7 +3445,6 @@ namespace dxvk { if (sampledImageId) { switch (ins.op) { - // Simple image gather operation case DxbcOpcode::Gather4: case DxbcOpcode::Gather4S: case DxbcOpcode::Gather4Po: @@ -3909,7 +3455,6 @@ namespace dxvk { imageOperands); } break; - // Depth-compare operation case DxbcOpcode::Gather4C: case DxbcOpcode::Gather4CS: case DxbcOpcode::Gather4PoC: @@ -3930,15 +3475,12 @@ namespace dxvk { resultId = m_module.constNull(resultTypeId); } - // If necessary, deal with the sparse result DxbcRegisterValue result; result.type = texelType; result.id = imageOperands.sparse ? emitExtractSparseTexel(texelTypeId, resultId) : resultId; - // Swizzle components using the texture swizzle - // and the destination operand's write mask result = emitRegisterSwizzle(result, textureReg.swizzle, ins.dst[0].mask); @@ -3950,25 +3492,16 @@ namespace dxvk { void DxbcCompiler::emitTextureSample(const DxbcShaderInstruction& ins) { - // All sample instructions have at least these operands: - // (dst0) The destination register - // (src0) Texture coordinates - // (src1) The texture itself - // (src2) The sampler object const DxbcRegister& texCoordReg = ins.src[0]; const DxbcRegister& textureReg = ins.src[1]; const DxbcRegister& samplerReg = ins.src[2]; - // Texture and sampler register IDs const auto& texture = m_textures.at(textureReg.idx[0].offset); const auto& sampler = m_samplers.at(samplerReg.idx[0].offset); const uint32_t imageLayerDim = getTexLayerDim(texture.imageInfo); - // Load the texture coordinates. SPIR-V allows these - // to be float4 even if not all components are used. DxbcRegisterValue coord = emitLoadTexCoord(texCoordReg, texture.imageInfo); - // Load reference value for depth-compare operations const bool isDepthCompare = ins.op == DxbcOpcode::SampleC || ins.op == DxbcOpcode::SampleClz || ins.op == DxbcOpcode::SampleCClampS @@ -3978,7 +3511,6 @@ namespace dxvk { ? emitRegisterLoad(ins.src[3], DxbcRegMask(true, false, false, false)) : DxbcRegisterValue(); - // Load explicit gradients for sample operations that require them const bool hasExplicitGradients = ins.op == DxbcOpcode::SampleD || ins.op == DxbcOpcode::SampleDClampS; @@ -3990,7 +3522,6 @@ namespace dxvk { ? emitRegisterLoad(ins.src[4], DxbcRegMask::firstN(imageLayerDim)) : DxbcRegisterValue(); - // LOD for certain sample operations const bool hasLod = ins.op == DxbcOpcode::SampleL || ins.op == DxbcOpcode::SampleLS || ins.op == DxbcOpcode::SampleB @@ -4000,7 +3531,6 @@ namespace dxvk { ? emitRegisterLoad(ins.src[3], DxbcRegMask(true, false, false, false)) : DxbcRegisterValue(); - // Min LOD for certain sparse operations const bool hasMinLod = ins.op == DxbcOpcode::SampleClampS || ins.op == DxbcOpcode::SampleBClampS || ins.op == DxbcOpcode::SampleDClampS @@ -4010,8 +3540,6 @@ namespace dxvk { ? emitRegisterLoad(ins.src[ins.srcCount - 1], DxbcRegMask(true, false, false, false)) : DxbcRegisterValue(); - // Accumulate additional image operands. These are - // not part of the actual operand token in SPIR-V. SpirvImageOperands imageOperands; imageOperands.sparse = ins.dstCount == 2; @@ -4039,11 +3567,8 @@ namespace dxvk { imageOperands.sMinLod = minLod.id; } - // Combine the texture and the sampler into a sampled image uint32_t sampledImageId = emitLoadSampledImage(texture, sampler, isDepthCompare); - // Sampling an image always returns a four-component - // vector, whereas depth-compare ops return a scalar. DxbcVectorType texelType; texelType.ctype = texture.sampledType; texelType.ccount = isDepthCompare ? 1 : 4; @@ -4057,7 +3582,6 @@ namespace dxvk { if (sampledImageId) { switch (ins.op) { - // Simple image sample operation case DxbcOpcode::Sample: case DxbcOpcode::SampleClampS: { resultId = m_module.opImageSampleImplicitLod( @@ -4065,7 +3589,6 @@ namespace dxvk { imageOperands); } break; - // Depth-compare operation case DxbcOpcode::SampleC: case DxbcOpcode::SampleCClampS: { resultId = m_module.opImageSampleDrefImplicitLod( @@ -4073,7 +3596,6 @@ namespace dxvk { referenceValue.id, imageOperands); } break; - // Depth-compare operation on mip level zero case DxbcOpcode::SampleClz: case DxbcOpcode::SampleClzS: { imageOperands.flags |= spv::ImageOperandsLodMask; @@ -4084,7 +3606,6 @@ namespace dxvk { referenceValue.id, imageOperands); } break; - // Sample operation with explicit gradients case DxbcOpcode::SampleD: case DxbcOpcode::SampleDClampS: { imageOperands.flags |= spv::ImageOperandsGradMask; @@ -4096,7 +3617,6 @@ namespace dxvk { imageOperands); } break; - // Sample operation with explicit LOD case DxbcOpcode::SampleL: case DxbcOpcode::SampleLS: { imageOperands.flags |= spv::ImageOperandsLodMask; @@ -4107,7 +3627,6 @@ namespace dxvk { imageOperands); } break; - // Sample operation with LOD bias case DxbcOpcode::SampleB: case DxbcOpcode::SampleBClampS: { imageOperands.flags |= spv::ImageOperandsBiasMask; @@ -4135,8 +3654,6 @@ namespace dxvk { ? emitExtractSparseTexel(texelTypeId, resultId) : resultId; - // Swizzle components using the texture swizzle - // and the destination operand's write mask if (result.type.ccount != 1) { result = emitRegisterSwizzle(result, textureReg.swizzle, ins.dst[0].mask); @@ -4150,16 +3667,11 @@ namespace dxvk { void DxbcCompiler::emitTypedUavLoad(const DxbcShaderInstruction& ins) { - // load_uav_typed has three operands: - // (dst0) The destination register - // (src0) The texture or buffer coordinates - // (src1) The UAV to load from const uint32_t registerId = ins.src[1].idx[0].offset; const DxbcUav uavInfo = m_uavs.at(registerId); emitUavBarrier(uint64_t(1u) << registerId, 0u); - // Load texture coordinates DxbcRegisterValue texCoord = emitLoadTexCoord( ins.src[0], uavInfo.imageInfo); @@ -4183,12 +3695,10 @@ namespace dxvk { if (imageOperands.sparse) resultTypeId = getSparseResultTypeId(texelTypeId); - // Load source value from the UAV resultId = m_module.opImageRead(resultTypeId, m_module.opLoad(uavInfo.imageTypeId, uavInfo.varId), texCoord.id, imageOperands); - // Apply component swizzle and mask DxbcRegisterValue uavValue; uavValue.type = texelType; uavValue.id = imageOperands.sparse @@ -4206,14 +3716,9 @@ namespace dxvk { void DxbcCompiler::emitTypedUavStore(const DxbcShaderInstruction& ins) { - // store_uav_typed has three operands: - // (dst0) The destination UAV - // (src0) The texture or buffer coordinates - // (src1) The value to store const DxbcBufferInfo uavInfo = getBufferInfo(ins.dst[0]); emitUavBarrier(0u, uint64_t(1u) << ins.dst[0].idx[0].offset); - // Set image operands for coherent access if necessary SpirvImageOperands imageOperands; if (uavInfo.coherence) { @@ -4222,16 +3727,12 @@ namespace dxvk { imageOperands.makeAvailable = m_module.constu32(uavInfo.coherence); } - // Load texture coordinates DxbcRegisterValue texCoord = emitLoadTexCoord(ins.src[0], uavInfo.image); - // Load the value that will be written to the image. We'll - // have to cast it to the component type of the image. const DxbcRegisterValue texValue = emitRegisterBitcast( emitRegisterLoad(ins.src[1], DxbcRegMask(true, true, true, true)), uavInfo.stype); - // Write the given value to the image m_module.opImageWrite( m_module.opLoad(uavInfo.typeId, uavInfo.varId), texCoord.id, texValue.id, imageOperands); @@ -4239,14 +3740,9 @@ namespace dxvk { void DxbcCompiler::emitControlFlowIf(const DxbcShaderInstruction& ins) { - // Load the first component of the condition - // operand and perform a zero test on it. const DxbcRegisterValue condition = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); - // Declare the 'if' block. We do not know if there - // will be an 'else' block or not, so we'll assume - // that there is one and leave it empty otherwise. DxbcCfgBlock block; block.type = DxbcCfgBlockType::If; block.b_if.ztestId = emitRegisterZeroTest(condition, ins.controls.zeroTest()).id; @@ -4256,9 +3752,6 @@ namespace dxvk { block.b_if.headerPtr = m_module.getInsertionPtr(); m_controlFlowBlocks.push_back(block); - // We'll insert the branch instruction when closing - // the block, since we don't know whether or not an - // else block is needed right now. m_module.opLabel(block.b_if.labelIf); } @@ -4269,13 +3762,9 @@ namespace dxvk { || m_controlFlowBlocks.back().b_if.labelElse != 0) throw DxvkError("DxbcCompiler: 'Else' without 'If' found"); - // Set the 'Else' flag so that we do - // not insert a dummy block on 'EndIf' DxbcCfgBlock& block = m_controlFlowBlocks.back(); block.b_if.labelElse = m_module.allocateId(); - // Close the 'If' block by branching to - // the merge block we declared earlier m_module.opBranch(block.b_if.labelEnd); m_module.opLabel (block.b_if.labelElse); } @@ -4286,11 +3775,9 @@ namespace dxvk { || m_controlFlowBlocks.back().type != DxbcCfgBlockType::If) throw DxvkError("DxbcCompiler: 'EndIf' without 'If' found"); - // Remove the block from the stack, it's closed DxbcCfgBlock block = m_controlFlowBlocks.back(); m_controlFlowBlocks.pop_back(); - // Write out the 'if' header m_module.beginInsertion(block.b_if.headerPtr); m_module.opSelectionMerge( @@ -4306,20 +3793,15 @@ namespace dxvk { m_module.endInsertion(); - // End the active 'if' or 'else' block m_module.opBranch(block.b_if.labelEnd); m_module.opLabel (block.b_if.labelEnd); } void DxbcCompiler::emitControlFlowSwitch(const DxbcShaderInstruction& ins) { - // Load the selector as a scalar unsigned integer const DxbcRegisterValue selector = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); - // Declare switch block. We cannot insert the switch - // instruction itself yet because the number of case - // statements and blocks is unknown at this point. DxbcCfgBlock block; block.type = DxbcCfgBlockType::Switch; block.b_switch.insertPtr = m_module.getInsertionPtr(); @@ -4330,7 +3812,6 @@ namespace dxvk { block.b_switch.labelCases = nullptr; m_controlFlowBlocks.push_back(block); - // Define the first 'case' label m_module.opLabel(block.b_switch.labelCase); } @@ -4340,11 +3821,9 @@ namespace dxvk { || m_controlFlowBlocks.back().type != DxbcCfgBlockType::Switch) throw DxvkError("DxbcCompiler: 'Case' without 'Switch' found"); - // The source operand must be a 32-bit immediate. if (ins.src[0].type != DxbcOperandType::Imm32) throw DxvkError("DxbcCompiler: Invalid operand type for 'Case'"); - // Use the last label allocated for 'case'. DxbcCfgBlockSwitch* block = &m_controlFlowBlocks.back().b_switch; if (caseBlockIsFallthrough()) { @@ -4376,7 +3855,6 @@ namespace dxvk { m_module.opLabel (block->labelCase); } - // Set the last label allocated for 'case' as the default label. block->labelDefault = block->labelCase; } @@ -4386,7 +3864,6 @@ namespace dxvk { || m_controlFlowBlocks.back().type != DxbcCfgBlockType::Switch) throw DxvkError("DxbcCompiler: 'EndSwitch' without 'Switch' found"); - // Remove the block from the stack, it's closed DxbcCfgBlock block = m_controlFlowBlocks.back(); m_controlFlowBlocks.pop_back(); @@ -4396,17 +3873,13 @@ namespace dxvk { : block.b_switch.labelCase; } - // Close the current 'case' block m_module.opBranch(block.b_switch.labelBreak); - // Insert the 'switch' statement. For that, we need to - // gather all the literal-label pairs for the construct. m_module.beginInsertion(block.b_switch.insertPtr); m_module.opSelectionMerge( block.b_switch.labelBreak, spv::SelectionControlMaskNone); - // We'll restore the original order of the case labels here std::vector jumpTargets; for (auto i = block.b_switch.labelCases; i != nullptr; i = i->next) jumpTargets.insert(jumpTargets.begin(), i->desc); @@ -4418,20 +3891,16 @@ namespace dxvk { jumpTargets.data()); m_module.endInsertion(); - // Destroy the list of case labels - // FIXME we're leaking memory if compilation fails. DxbcSwitchLabel* caseLabel = block.b_switch.labelCases; while (caseLabel != nullptr) delete std::exchange(caseLabel, caseLabel->next); - // Begin new block after switch blocks m_module.opLabel(block.b_switch.labelBreak); } void DxbcCompiler::emitControlFlowLoop(const DxbcShaderInstruction& ins) { - // Declare the 'loop' block DxbcCfgBlock block; block.type = DxbcCfgBlockType::Loop; block.b_loop.labelHeader = m_module.allocateId(); @@ -4458,15 +3927,12 @@ namespace dxvk { || m_controlFlowBlocks.back().type != DxbcCfgBlockType::Loop) throw DxvkError("DxbcCompiler: 'EndLoop' without 'Loop' found"); - // Remove the block from the stack, it's closed const DxbcCfgBlock block = m_controlFlowBlocks.back(); m_controlFlowBlocks.pop_back(); - // Declare the continue block m_module.opBranch(block.b_loop.labelContinue); m_module.opLabel (block.b_loop.labelContinue); - // Declare the merge block m_module.opBranch(block.b_loop.labelHeader); m_module.opLabel (block.b_loop.labelBreak); } @@ -4486,17 +3952,13 @@ namespace dxvk { m_module.opBranch(isBreak ? cfgBlock->b_loop.labelBreak : cfgBlock->b_loop.labelContinue); - } else /* if (cfgBlock->type == DxbcCfgBlockType::Switch) */ { + } else { m_module.opBranch(cfgBlock->b_switch.labelBreak); } - // Subsequent instructions assume that there is an open block const uint32_t labelId = m_module.allocateId(); m_module.opLabel(labelId); - // If this is on the same level as a switch-case construct, - // rather than being nested inside an 'if' statement, close - // the current 'case' block. if (m_controlFlowBlocks.back().type == DxbcCfgBlockType::Switch) cfgBlock->b_switch.labelCase = labelId; } @@ -4512,14 +3974,12 @@ namespace dxvk { if (cfgBlock == nullptr) throw DxvkError("DxbcCompiler: 'Breakc' or 'Continuec' outside 'Loop' or 'Switch' found"); - // Perform zero test on the first component of the condition const DxbcRegisterValue condition = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); const DxbcRegisterValue zeroTest = emitRegisterZeroTest( condition, ins.controls.zeroTest()); - // We basically have to wrap this into an 'if' block const uint32_t breakBlock = m_module.allocateId(); const uint32_t mergeBlock = m_module.allocateId(); @@ -4535,7 +3995,7 @@ namespace dxvk { m_module.opBranch(isBreak ? cfgBlock->b_loop.labelBreak : cfgBlock->b_loop.labelContinue); - } else /* if (cfgBlock->type == DxbcCfgBlockType::Switch) */ { + } else { m_module.opBranch(cfgBlock->b_switch.labelBreak); } @@ -4550,27 +4010,23 @@ namespace dxvk { m_module.opReturn(); m_module.opLabel(labelId); - // return can be used in place of break to terminate a case block if (m_controlFlowBlocks.back().type == DxbcCfgBlockType::Switch) m_controlFlowBlocks.back().b_switch.labelCase = labelId; m_topLevelIsUniform = false; } else { - // Last instruction in the current function this->emitFunctionEnd(); } } void DxbcCompiler::emitControlFlowRetc(const DxbcShaderInstruction& ins) { - // Perform zero test on the first component of the condition const DxbcRegisterValue condition = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); const DxbcRegisterValue zeroTest = emitRegisterZeroTest( condition, ins.controls.zeroTest()); - // We basically have to wrap this into an 'if' block const uint32_t returnLabel = m_module.allocateId(); const uint32_t continueLabel = m_module.allocateId(); @@ -4585,14 +4041,11 @@ namespace dxvk { m_module.opLabel(continueLabel); - // The return condition may be non-uniform m_topLevelIsUniform = false; } void DxbcCompiler::emitControlFlowDiscard(const DxbcShaderInstruction& ins) { - // Discard actually has an operand that determines - // whether or not the fragment should be discarded const DxbcRegisterValue condition = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); @@ -4614,7 +4067,6 @@ namespace dxvk { m_module.enableCapability(spv::CapabilityDemoteToHelperInvocation); - // Discard is just retc in a trenchcoat m_topLevelIsUniform = false; } @@ -4634,8 +4086,6 @@ namespace dxvk { m_insideFunction = true; - // We have to assume that this function gets - // called from non-uniform control flow m_topLevelIsUniform = false; } @@ -4654,14 +4104,12 @@ namespace dxvk { uint32_t functionNr = ins.src[1].idx[0].offset; uint32_t functionId = getFunctionId(functionNr); - // Perform zero test on the first component of the condition const DxbcRegisterValue condition = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); const DxbcRegisterValue zeroTest = emitRegisterZeroTest( condition, ins.controls.zeroTest()); - // We basically have to wrap this into an 'if' block const uint32_t callLabel = m_module.allocateId(); const uint32_t skipLabel = m_module.allocateId(); @@ -4778,7 +4226,6 @@ namespace dxvk { float z, float w, const DxbcRegMask& writeMask) { - // TODO refactor these functions into one single template std::array ids = { 0, 0, 0, 0 }; uint32_t componentIndex = 0; @@ -4947,8 +4394,6 @@ namespace dxvk { indices[dstIndex++] = swizzle[i]; } - // If the swizzle combined with the mask can be reduced - // to a no-op, we don't need to insert any instructions. bool isIdentitySwizzle = dstIndex == value.type.ccount; for (uint32_t i = 0; i < dstIndex && isIdentitySwizzle; i++) @@ -4957,8 +4402,6 @@ namespace dxvk { if (isIdentitySwizzle) return value; - // Use OpCompositeExtract if the resulting vector contains - // only one component, and OpVectorShuffle if it is a vector. DxbcRegisterValue result; result.type.ctype = value.type.ctype; result.type.ccount = dstIndex; @@ -4996,24 +4439,15 @@ namespace dxvk { const uint32_t typeId = getVectorTypeId(result.type); if (srcMask.popCount() == 0) { - // Nothing to do if the insertion mask is empty result.id = dstValue.id; } else if (dstValue.type.ccount == 1) { - // Both values are scalar, so the first component - // of the write mask decides which one to take. result.id = srcMask[0] ? srcValue.id : dstValue.id; } else if (srcValue.type.ccount == 1) { - // The source value is scalar. Since OpVectorShuffle - // requires both arguments to be vectors, we have to - // use OpCompositeInsert to modify the vector instead. const uint32_t componentId = srcMask.firstSet(); result.id = m_module.opCompositeInsert(typeId, srcValue.id, dstValue.id, 1, &componentId); } else { - // Both arguments are vectors. We can determine which - // components to take from which vector and use the - // OpVectorShuffle instruction. std::array components; uint32_t srcComponentId = dstValue.type.ccount; @@ -5232,8 +4666,6 @@ namespace dxvk { DxbcRegisterPointer DxbcCompiler::emitGetTempPtr( const DxbcRegister& operand) { - // r# regs are indexed as follows: - // (0) register index (immediate) uint32_t regIdx = operand.idx[0].offset; if (regIdx >= m_rRegs.size()) @@ -5269,14 +4701,6 @@ namespace dxvk { DxbcRegisterPointer DxbcCompiler::emitGetInputPtr( const DxbcRegister& operand) { - // In the vertex and pixel stages, - // v# regs are indexed as follows: - // (0) register index (relative) - // - // In the tessellation and geometry - // stages, the index has two dimensions: - // (0) vertex index (relative) - // (1) register index (relative) DxbcRegisterPointer result; result.type.ctype = DxbcScalarType::Float32; result.type.ccount = 4; @@ -5286,8 +4710,6 @@ namespace dxvk { for (uint32_t i = 0; i < operand.idxDim; i++) indices.at(i) = emitIndexLoad(operand.idx[i]).id; - // Pick the input array depending on - // the program type and operand type struct InputArray { uint32_t id; spv::StorageClass sclass; @@ -5327,9 +4749,6 @@ namespace dxvk { DxbcRegisterPointer DxbcCompiler::emitGetOutputPtr( const DxbcRegister& operand) { if (m_programInfo.type() == DxbcProgramType::HullShader) { - // Hull shaders are special in that they have two sets of - // output registers, one for per-patch values and one for - // per-vertex values. DxbcRegisterPointer result; result.type.ctype = DxbcScalarType::Float32; result.type.ccount = 4; @@ -5361,8 +4780,6 @@ namespace dxvk { return result; } else { - // Regular shaders have their output - // registers set up at declaration time return m_oRegs.at(operand.idx[0].offset); } } @@ -5373,8 +4790,6 @@ namespace dxvk { DxbcRegisterValue constId = emitIndexLoad(operand.idx[0]); if (m_icbArray) { - // We pad the icb array with an extra zero vector, so we can - // clamp the index and get correct robustness behaviour. constId.id = m_module.opUMin(getVectorTypeId(constId.type), constId.id, m_module.constu32(m_icbSize)); @@ -5542,16 +4957,12 @@ namespace dxvk { DxbcRegisterPointer DxbcCompiler::emitGetAtomicPointer( const DxbcRegister& operand, const DxbcRegister& address) { - // Query information about the resource itself const uint32_t registerId = operand.idx[0].offset; const DxbcBufferInfo resourceInfo = getBufferInfo(operand); - // For UAVs and shared memory, different methods - // of obtaining the final pointer are used. bool isTgsm = operand.type == DxbcOperandType::ThreadGroupSharedMemory; bool isSsbo = resourceInfo.isSsbo; - // Compute the actual address into the resource const DxbcRegisterValue addressValue = [&] { switch (resourceInfo.type) { case DxbcResourceType::Raw: @@ -5581,7 +4992,6 @@ namespace dxvk { } }(); - // Compute the actual pointer DxbcRegisterPointer result; result.type.ctype = resourceInfo.stype; result.type.ccount = 1; @@ -5620,14 +5030,11 @@ namespace dxvk { DxbcRegisterValue DxbcCompiler::emitQueryTexelBufferSize( const DxbcRegister& resource) { - // Load the texel buffer object. This cannot be used with - // constant buffers or any other type of resource. const DxbcBufferInfo bufferInfo = getBufferInfo(resource); const uint32_t bufferId = m_module.opLoad( bufferInfo.typeId, bufferInfo.varId); - // We'll store this as a scalar unsigned integer DxbcRegisterValue result; result.type.ctype = DxbcScalarType::Uint32; result.type.ccount = 1; @@ -5651,7 +5058,6 @@ namespace dxvk { getVectorTypeId(result.type), m_module.opLoad(info.typeId, info.varId)); } else { - // Report one LOD in case of UAVs or multisampled images result.id = m_module.constu32(1); } @@ -5662,8 +5068,6 @@ namespace dxvk { DxbcRegisterValue DxbcCompiler::emitQueryTextureSamples( const DxbcRegister& resource) { if (resource.type == DxbcOperandType::Rasterizer) { - // SPIR-V has no gl_NumSamples equivalent, so we - // have to work around it using a push constant if (!m_ps.pushConstantId) m_ps.pushConstantId = emitPushConstants(); @@ -5689,7 +5093,6 @@ namespace dxvk { getVectorTypeId(result.type), m_module.opLoad(info.typeId, info.varId)); } else { - // OpImageQuerySamples requires MSAA images result.id = m_module.constu32(1); } @@ -5812,22 +5215,15 @@ namespace dxvk { DxbcRegisterPointer ptr, DxbcRegisterValue value, DxbcRegMask writeMask) { - // If the component types are not compatible, - // we need to bit-cast the source variable. if (value.type.ctype != ptr.type.ctype) value = emitRegisterBitcast(value, ptr.type.ctype); - // If the source value consists of only one component, - // it is stored in all components of the destination. if (value.type.ccount == 1) value = emitRegisterExtend(value, writeMask.popCount()); if (ptr.type.ccount == writeMask.popCount()) { - // Simple case: We write to the entire register m_module.opStore(ptr.id, value.id); } else { - // We only write to part of the destination - // register, so we need to load and modify it DxbcRegisterValue tmp = emitValueLoad(ptr); tmp = emitRegisterInsert(tmp, value, writeMask); @@ -5838,7 +5234,6 @@ namespace dxvk { DxbcRegisterValue DxbcCompiler::emitRegisterLoadRaw( const DxbcRegister& reg) { - // Try to find index range for the given register const DxbcIndexRange* indexRange = nullptr; if (reg.idxDim && reg.idx[reg.idxDim - 1u].relReg) { @@ -5870,8 +5265,6 @@ namespace dxvk { m_module.constu32(indexRange->length)); } - // Kind of ugly to have an empty else block here but there's no - // way for us to know the current block ID for the phi below DxbcConditional cond; cond.labelIf = m_module.allocateId(); cond.labelElse = m_module.allocateId(); @@ -5906,7 +5299,6 @@ namespace dxvk { DxbcRegisterValue value = emitValueLoad(emitGetOperandPtr(reg)); - // Pad icb values to a vec4 since the app may access components that are always 0 if (reg.type == DxbcOperandType::ImmediateConstantBuffer && value.type.ccount < 4u) { DxbcVectorType zeroType; zeroType.ctype = value.type.ctype; @@ -5928,9 +5320,6 @@ namespace dxvk { DxbcRegisterValue DxbcCompiler::emitConstantBufferLoad( const DxbcRegister& reg, DxbcRegMask writeMask) { - // Constant buffers take a two-dimensional index: - // (0) register index (immediate) - // (1) constant offset (relative) DxbcRegisterInfo info; info.type.ctype = DxbcScalarType::Float32; info.type.ccount = 4; @@ -5952,7 +5341,6 @@ namespace dxvk { m_constantBuffers.at(regId).varId, indices.size(), indices.data()); - // Load individual components from buffer std::array ccomps = { 0, 0, 0, 0 }; std::array scomps = { 0, 0, 0, 0 }; uint32_t scount = 0; @@ -5993,7 +5381,6 @@ namespace dxvk { scount, scomps.data()); } - // Apply any post-processing that might be necessary result = emitRegisterBitcast(result, reg.dataType); result = emitSrcOperandModifiers(result, reg.modifiers); return result; @@ -6008,14 +5395,12 @@ namespace dxvk { DxbcRegisterValue result; if (reg.componentCount == DxbcComponentCount::Component1) { - // Create one single u32 constant result.type.ctype = DxbcScalarType::Uint32; result.type.ccount = 1; result.id = m_module.constu32(reg.imm.u32_1); result = emitRegisterExtend(result, writeMask.popCount()); } else if (reg.componentCount == DxbcComponentCount::Component4) { - // Create a u32 vector with as many components as needed std::array indices = { }; uint32_t indexId = 0; @@ -6037,26 +5422,19 @@ namespace dxvk { } } else { - // Something went horribly wrong in the decoder or the shader is broken throw DxvkError("DxbcCompiler: Invalid component count for immediate operand"); } - // Cast constants to the requested type return emitRegisterBitcast(result, reg.dataType); } else if (reg.type == DxbcOperandType::ConstantBuffer) { return emitConstantBufferLoad(reg, writeMask); } else { - // Load operand from the operand pointer DxbcRegisterValue result = emitRegisterLoadRaw(reg); - // Apply operand swizzle to the operand value result = emitRegisterSwizzle(result, reg.swizzle, writeMask); - // Cast it to the requested type. We need to do - // this after the swizzling for 64-bit types. result = emitRegisterBitcast(result, reg.dataType); - // Apply operand modifiers result = emitSrcOperandModifiers(result, reg.modifiers); return result; } @@ -6100,7 +5478,6 @@ namespace dxvk { void DxbcCompiler::emitInputSetup() { m_module.setLateConst(m_vArrayLengthId, &m_vArrayLength); - // Copy all defined v# registers into the input array const uint32_t vecTypeId = m_module.defVectorType(m_module.defFloatType(32), 4); const uint32_t ptrTypeId = m_module.defPointerType(vecTypeId, spv::StorageClassPrivate); @@ -6121,8 +5498,6 @@ namespace dxvk { } } - // Copy all system value registers into the array, - // preserving any previously written contents. for (const DxbcSvMapping& map : m_vMappings) { const uint32_t registerId = m_module.consti32(map.regId); @@ -6147,8 +5522,6 @@ namespace dxvk { void DxbcCompiler::emitInputSetup(uint32_t vertexCount) { m_module.setLateConst(m_vArrayLengthId, &m_vArrayLength); - // Copy all defined v# registers into the input array. Note - // that the outer index of the array is the vertex index. const uint32_t vecTypeId = m_module.defVectorType(m_module.defFloatType(32), 4); const uint32_t dstPtrTypeId = m_module.defPointerType(vecTypeId, spv::StorageClassPrivate); @@ -6179,8 +5552,6 @@ namespace dxvk { } } - // Copy all system value registers into the array, - // preserving any previously written contents. for (const DxbcSvMapping& map : m_vMappings) { const uint32_t registerId = m_module.consti32(map.regId); @@ -6240,9 +5611,6 @@ namespace dxvk { void DxbcCompiler::emitOutputDepthClamp() { - // HACK: Some drivers do not clamp FragDepth to [minDepth..maxDepth] - // before writing to the depth attachment, but we do not have acccess - // to those. Clamp to [0..1] instead. if (m_ps.builtinDepth) { DxbcRegisterPointer ptr; ptr.type = { DxbcScalarType::Float32, 1 }; @@ -6466,9 +5834,6 @@ namespace dxvk { ptrIn.type = { DxbcScalarType::Float32, 4 }; ptrIn.id = m_ps.builtinFragCoord; - // The X, Y and Z components of the SV_POSITION semantic - // are identical to Vulkan's FragCoord builtin, but we - // need to compute the reciprocal of the W component. DxbcRegisterValue fragCoord = emitValueLoad(ptrIn); uint32_t componentIndex = 3; @@ -6675,18 +6040,18 @@ namespace dxvk { }; static const std::array s_tessFactors = {{ - { m_hs.builtinTessLevelOuter, 0 }, // FinalQuadUeq0EdgeTessFactor - { m_hs.builtinTessLevelOuter, 1 }, // FinalQuadVeq0EdgeTessFactor - { m_hs.builtinTessLevelOuter, 2 }, // FinalQuadUeq1EdgeTessFactor - { m_hs.builtinTessLevelOuter, 3 }, // FinalQuadVeq1EdgeTessFactor - { m_hs.builtinTessLevelInner, 0 }, // FinalQuadUInsideTessFactor - { m_hs.builtinTessLevelInner, 1 }, // FinalQuadVInsideTessFactor - { m_hs.builtinTessLevelOuter, 0 }, // FinalTriUeq0EdgeTessFactor - { m_hs.builtinTessLevelOuter, 1 }, // FinalTriVeq0EdgeTessFactor - { m_hs.builtinTessLevelOuter, 2 }, // FinalTriWeq0EdgeTessFactor - { m_hs.builtinTessLevelInner, 0 }, // FinalTriInsideTessFactor - { m_hs.builtinTessLevelOuter, 0 }, // FinalLineDensityTessFactor - { m_hs.builtinTessLevelOuter, 1 }, // FinalLineDetailTessFactor + { m_hs.builtinTessLevelOuter, 0 }, + { m_hs.builtinTessLevelOuter, 1 }, + { m_hs.builtinTessLevelOuter, 2 }, + { m_hs.builtinTessLevelOuter, 3 }, + { m_hs.builtinTessLevelInner, 0 }, + { m_hs.builtinTessLevelInner, 1 }, + { m_hs.builtinTessLevelOuter, 0 }, + { m_hs.builtinTessLevelOuter, 1 }, + { m_hs.builtinTessLevelOuter, 2 }, + { m_hs.builtinTessLevelInner, 0 }, + { m_hs.builtinTessLevelOuter, 0 }, + { m_hs.builtinTessLevelOuter, 1 }, }}; const TessFactor tessFactor = s_tessFactors.at(uint32_t(sv) @@ -6695,7 +6060,6 @@ namespace dxvk { const uint32_t tessFactorArrayIndex = m_module.constu32(tessFactor.index); - // Apply global tess factor limit float maxTessFactor = m_hs.maxTessFactor; if (m_moduleInfo.tess != nullptr) { @@ -6841,7 +6205,6 @@ namespace dxvk { for (auto e = m_isgn->begin(); e != m_isgn->end(); e++) { if (e->systemValue == sv) { - // Load individual components from the source array uint32_t componentIndex = 0; std::array componentIds = {{ 0, 0, 0, 0 }}; @@ -6862,7 +6225,6 @@ namespace dxvk { } } - // Put everything into one vector DxbcRegisterValue dstValue; dstValue.type = { DxbcScalarType::Float32, componentIndex }; dstValue.id = componentIds[0]; @@ -6873,7 +6235,6 @@ namespace dxvk { componentIndex, componentIds.data()); } - // Store vector to the input array uint32_t registerId = m_module.consti32(e->registerId); DxbcRegisterPointer dstInput; @@ -6904,12 +6265,9 @@ namespace dxvk { void DxbcCompiler::emitInit() { - // Set up common capabilities for all shaders m_module.enableCapability(spv::CapabilityShader); m_module.enableCapability(spv::CapabilityImageQuery); - // Initialize the shader module with capabilities - // etc. Each shader type has its own peculiarities. switch (m_programInfo.type()) { case DxbcProgramType::VertexShader: emitVsInit(); break; case DxbcProgramType::HullShader: emitHsInit(); break; @@ -6966,10 +6324,8 @@ namespace dxvk { m_module.enableCapability(spv::CapabilityCullDistance); m_module.enableCapability(spv::CapabilityDrawParameters); - // Standard input array emitDclInputArray(0); - // Cull/clip distances as outputs m_clipDistances = emitDclClipCullDistanceArray( m_analysis->clipCullOut.numClipPlanes, spv::BuiltInClipDistance, @@ -6980,7 +6336,6 @@ namespace dxvk { spv::BuiltInCullDistance, spv::StorageClassOutput); - // Main function of the vertex shader m_vs.functionId = m_module.allocateId(); m_module.setDebugName(m_vs.functionId, "vs_main"); @@ -7018,7 +6373,6 @@ namespace dxvk { m_ds.builtinTessLevelOuter = emitBuiltinTessLevelOuter(spv::StorageClassInput); m_ds.builtinTessLevelInner = emitBuiltinTessLevelInner(spv::StorageClassInput); - // Cull/clip distances as outputs m_clipDistances = emitDclClipCullDistanceArray( m_analysis->clipCullOut.numClipPlanes, spv::BuiltInClipDistance, @@ -7029,7 +6383,6 @@ namespace dxvk { spv::BuiltInCullDistance, spv::StorageClassOutput); - // Main function of the domain shader m_ds.functionId = m_module.allocateId(); m_module.setDebugName(m_ds.functionId, "ds_main"); @@ -7047,7 +6400,6 @@ namespace dxvk { m_module.enableCapability(spv::CapabilityClipDistance); m_module.enableCapability(spv::CapabilityCullDistance); - // Enable capabilities for xfb mode if necessary if (m_moduleInfo.xfb) { m_module.enableCapability(spv::CapabilityGeometryStreams); m_module.enableCapability(spv::CapabilityTransformFeedback); @@ -7055,11 +6407,9 @@ namespace dxvk { m_module.setExecutionMode(m_entryPointId, spv::ExecutionModeXfb); } - // We only need outputs if rasterization is enabled m_gs.needsOutputSetup = !m_moduleInfo.xfb || m_moduleInfo.xfb->rasterizedStream >= 0; - // Cull/clip distances as outputs m_clipDistances = emitDclClipCullDistanceArray( m_analysis->clipCullOut.numClipPlanes, spv::BuiltInClipDistance, @@ -7070,11 +6420,9 @@ namespace dxvk { spv::BuiltInCullDistance, spv::StorageClassOutput); - // Emit Xfb variables if necessary if (m_moduleInfo.xfb) emitXfbOutputDeclarations(); - // Main function of the vertex shader m_gs.functionId = m_module.allocateId(); m_module.setDebugName(m_gs.functionId, "gs_main"); @@ -7093,10 +6441,8 @@ namespace dxvk { m_module.setExecutionMode(m_entryPointId, spv::ExecutionModeOriginUpperLeft); - // Standard input array emitDclInputArray(0); - // Cull/clip distances as inputs m_clipDistances = emitDclClipCullDistanceArray( m_analysis->clipCullIn.numClipPlanes, spv::BuiltInClipDistance, @@ -7107,7 +6453,6 @@ namespace dxvk { spv::BuiltInCullDistance, spv::StorageClassInput); - // Main function of the pixel shader m_ps.functionId = m_module.allocateId(); m_module.setDebugName(m_ps.functionId, "ps_main"); @@ -7121,7 +6466,6 @@ namespace dxvk { void DxbcCompiler::emitCsInit() { - // Main function of the compute shader m_cs.functionId = m_module.allocateId(); m_module.setDebugName(m_cs.functionId, "cs_main"); @@ -7152,13 +6496,11 @@ namespace dxvk { if (m_hs.cpPhase.functionId == 0) m_hs.cpPhase = this->emitNewHullShaderPassthroughPhase(); - // Control point phase this->emitMainFunctionBegin(); this->emitInputSetup(m_hs.vertexCountIn); this->emitHsControlPointPhase(m_hs.cpPhase); this->emitHsPhaseBarrier(); - // Fork-join phases and output setup this->emitHsInvocationBlockBegin(1); for (const auto& phase : m_hs.forkPhases) @@ -7196,8 +6538,6 @@ namespace dxvk { m_module.opFunctionCall( m_module.defVoidType(), m_gs.functionId, 0, nullptr); - // No output setup at this point as that was - // already done during the EmitVertex step this->emitFunctionEnd(); } @@ -7209,8 +6549,6 @@ namespace dxvk { this->emitClipCullLoad(DxbcSystemValue::CullDistance, m_cullDistances); if (m_hasRasterizerOrderedUav) { - // For simplicity, just lock the entire fragment shader - // if there are any rasterizer ordered views. m_module.enableExtension("SPV_EXT_fragment_shader_interlock"); if (m_module.hasCapability(spv::CapabilitySampleRateShading) @@ -7293,7 +6631,6 @@ namespace dxvk { m_moduleInfo.xfb->strides[xfbEntry->bufferId]); } - // TODO Compact location/component assignment for (uint32_t i = 0; i < m_xfbVars.size(); i++) { m_xfbVars[i].location = i; m_xfbVars[i].component = 0; @@ -7362,9 +6699,6 @@ namespace dxvk { info.ctype = DxbcScalarType::Float32; info.ccount = 4; - // Define the array type. This will be two-dimensional - // in some shaders, with the outer index representing - // the vertex ID within an invocation. m_vArrayLength = m_isgn != nullptr ? std::max(1u, m_isgn->maxRegisterCount()) : 1; m_vArrayLengthId = m_module.lateConst32(getScalarTypeId(DxbcScalarType::Uint32)); @@ -7376,9 +6710,6 @@ namespace dxvk { arrayTypeId, m_module.constu32(vertexCount)); } - // Define the actual variable. Note that this is private - // because we will copy input registers and some system - // variables to the array during the setup phase. const uint32_t ptrTypeId = m_module.defPointerType( arrayTypeId, spv::StorageClassPrivate); @@ -7433,7 +6764,6 @@ namespace dxvk { uint32_t funTypeId = m_module.defFunctionType( m_module.defVoidType(), 0, nullptr); - // Begin passthrough function uint32_t funId = m_module.allocateId(); m_module.setDebugName(funId, "hs_passthrough"); @@ -7442,8 +6772,6 @@ namespace dxvk { funTypeId); this->emitFunctionLabel(); - // We'll basically copy each input variable to the corresponding - // output, using the shader's invocation ID as the array index. const uint32_t invocationId = m_module.opLoad( getScalarTypeId(DxbcScalarType::Uint32), m_hs.builtinInvocationId); @@ -7455,7 +6783,6 @@ namespace dxvk { DxbcSystemValue::None, DxbcInterpolationMode::Undefined); - // Vector type index const std::array dstIndices = {{ invocationId, m_module.constu32(i->registerId) }}; @@ -7477,7 +6804,6 @@ namespace dxvk { emitValueStore(dstPtr, srcValue, DxbcRegMask::firstN(srcValue.type.ccount)); } - // End function this->emitFunctionEnd(); DxbcCompilerHsControlPointPhase result; @@ -7636,19 +6962,14 @@ namespace dxvk { uint32_t DxbcCompiler::emitSamplePosArray() { const std::array samplePosVectors = {{ - // Invalid sample count / unbound resource m_module.constvec2f32( 0.0f, 0.0f), - // VK_SAMPLE_COUNT_1_BIT m_module.constvec2f32( 0.0f, 0.0f), - // VK_SAMPLE_COUNT_2_BIT m_module.constvec2f32( 0.25f, 0.25f), m_module.constvec2f32(-0.25f,-0.25f), - // VK_SAMPLE_COUNT_4_BIT m_module.constvec2f32(-0.125f,-0.375f), m_module.constvec2f32( 0.375f,-0.125f), m_module.constvec2f32(-0.375f, 0.125f), m_module.constvec2f32( 0.125f, 0.375f), - // VK_SAMPLE_COUNT_8_BIT m_module.constvec2f32( 0.0625f,-0.1875f), m_module.constvec2f32(-0.0625f, 0.1875f), m_module.constvec2f32( 0.3125f, 0.0625f), @@ -7657,7 +6978,6 @@ namespace dxvk { m_module.constvec2f32(-0.4375f,-0.0625f), m_module.constvec2f32( 0.1875f, 0.4375f), m_module.constvec2f32( 0.4375f,-0.4375f), - // VK_SAMPLE_COUNT_16_BIT m_module.constvec2f32( 0.0625f, 0.0625f), m_module.constvec2f32(-0.0625f,-0.1875f), m_module.constvec2f32(-0.1875f, 0.1250f), @@ -7920,10 +7240,6 @@ namespace dxvk { || m_programInfo.type() != DxbcProgramType::ComputeShader) return; - // If both masks are 0, emit a barrier in case at least one read-write UAV - // has a pending unsynchronized access. Only consider read-after-write and - // write-after-read hazards, assume that back-to-back stores are safe and - // do not overlap in memory. Atomics are also completely ignored here. uint64_t rdMask = m_uavRdMask; uint64_t wrMask = m_uavWrMask; @@ -7939,8 +7255,6 @@ namespace dxvk { insertBarrier |= (m_analysis->uavInfos[uav].accessFlags & rwAccess) == rwAccess; } - // Need to be in uniform top-level control flow, or otherwise - // it is not safe to insert control barriers. if (insertBarrier && m_controlFlowBlocks.empty() && m_topLevelIsUniform) { m_module.opControlBarrier( m_module.constu32(spv::ScopeWorkgroup), @@ -7956,7 +7270,6 @@ namespace dxvk { m_uavRdMask = 0u; } - // Mark pending accesses m_uavWrMask |= writeMask; m_uavRdMask |= readMask; } @@ -8093,9 +7406,6 @@ namespace dxvk { DxbcRegisterPointer DxbcCompiler::getIndexableTempPtr( const DxbcRegister& operand, DxbcRegisterValue vectorId) { - // x# regs are indexed as follows: - // (0) register index (immediate) - // (1) element index (relative) const uint32_t regId = operand.idx[0].offset; DxbcRegisterInfo info; @@ -8124,8 +7434,6 @@ namespace dxvk { uint32_t DxbcCompiler::getUavCoherence(uint32_t registerId, DxbcUavFlags flags) { - // For any ROV with write access, we must ensure that - // availability operations happen within the locked scope. if (flags.test(DxbcUavFlag::RasterizerOrdered) && (m_analysis->uavInfos[registerId].accessFlags & VK_ACCESS_SHADER_WRITE_BIT)) { m_hasGloballyCoherentUav = true; @@ -8133,22 +7441,14 @@ namespace dxvk { return spv::ScopeQueueFamily; } - // Ignore any resources that can't both be read and written in - // the current shader, explicit availability/visibility operands - // are not useful in that case. if (m_analysis->uavInfos[registerId].accessFlags != (VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT)) return 0; - // If the globally coherent flag is set, the resource must be - // coherent across multiple workgroups of the same dispatch if (flags.test(DxbcUavFlag::GloballyCoherent)) { m_hasGloballyCoherentUav = true; return spv::ScopeQueueFamily; } - // In compute shaders, UAVs are implicitly workgroup coherent, - // but we can rely on memory barrier instructions to make any - // access available and visible to the entire workgroup. if (m_programInfo.type() == DxbcProgramType::ComputeShader) return spv::ScopeInvocation; diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_decoder.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_decoder.cpp index 0de7ddad1..d1f0632c9 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_decoder.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_decoder.cpp @@ -41,8 +41,6 @@ namespace dxvk { void DxbcDecodeContext::decodeInstruction(DxbcCodeSlice& code) { const uint32_t token0 = code.at(0); - // Initialize the instruction structure. Some of these values - // may not get written otherwise while decoding the instruction. m_instruction.op = static_cast(bit::extract(token0, 0, 10)); m_instruction.opClass = DxbcInstClass::Undefined; m_instruction.sampleControls = { 0, 0, 0 }; @@ -56,12 +54,8 @@ namespace dxvk { m_instruction.customDataSize = 0; m_instruction.customData = nullptr; - // Reset the index pointer, which may still contain - // a non-zero value from the previous iteration m_indexId = 0; - // Instruction length, in DWORDs. This includes the token - // itself and any other prefix that an instruction may have. uint32_t length = 0; if (m_instruction.op == DxbcOpcode::CustomData) { @@ -72,8 +66,6 @@ namespace dxvk { this->decodeOperation(code.take(length)); } - // Advance the caller's slice to the next token so that - // they can make consecutive calls to decodeInstruction() code = code.skip(length); } @@ -86,11 +78,9 @@ namespace dxvk { return; } - // Custom data blocks have their own instruction class m_instruction.op = DxbcOpcode::CustomData; m_instruction.opClass = DxbcInstClass::CustomData; - // We'll point into the code buffer rather than making a copy m_instruction.customDataType = static_cast( bit::extract(code.at(0), 11, 31)); m_instruction.customDataSize = blockLength - 2; @@ -101,15 +91,11 @@ namespace dxvk { void DxbcDecodeContext::decodeOperation(DxbcCodeSlice code) { uint32_t token = code.read(); - // Result modifiers, which are applied to common ALU ops m_instruction.modifiers.saturate = !!bit::extract(token, 13, 13); m_instruction.modifiers.precise = !!bit::extract(token, 19, 22); - // Opcode controls. It will depend on the - // opcode itself which ones are valid. m_instruction.controls = DxbcShaderOpcodeControls(token); - // Process extended opcode tokens while (bit::extract(token, 31, 31)) { token = code.read(); @@ -128,7 +114,6 @@ namespace dxvk { aoffimmi.v = bit::extract(token, 13, 16); aoffimmi.w = bit::extract(token, 17, 20); - // Four-bit signed numbers, sign-extend them m_instruction.sampleControls.u = aoffimmi.u; m_instruction.sampleControls.v = aoffimmi.v; m_instruction.sampleControls.w = aoffimmi.w; @@ -136,7 +121,7 @@ namespace dxvk { case DxbcExtOpcode::ResourceDim: case DxbcExtOpcode::ResourceReturnType: - break; // part of resource description + break; default: Logger::warn(str::format( @@ -145,9 +130,6 @@ namespace dxvk { } } - // Retrieve the instruction format in order to parse the - // operands. Doing this mostly automatically means that - // the compiler can rely on the operands being valid. const DxbcInstFormat format = dxbcInstructionFormat(m_instruction.op); m_instruction.opClass = format.instructionClass; @@ -157,39 +139,29 @@ namespace dxvk { void DxbcDecodeContext::decodeComponentSelection(DxbcRegister& reg, uint32_t token) { - // Pick the correct component selection mode based on the - // component count. We'll simplify this here so that the - // compiler can assume that everything is a 4D vector. reg.componentCount = static_cast(bit::extract(token, 0, 1)); switch (reg.componentCount) { - // No components - used for samplers etc. case DxbcComponentCount::Component0: reg.mask = DxbcRegMask(false, false, false, false); reg.swizzle = DxbcRegSwizzle(0, 0, 0, 0); break; - // One component - used for immediates - // and a few built-in registers. case DxbcComponentCount::Component1: reg.mask = DxbcRegMask(true, false, false, false); reg.swizzle = DxbcRegSwizzle(0, 0, 0, 0); break; - // Four components - everything else. This requires us - // to actually parse the component selection mode. case DxbcComponentCount::Component4: { const DxbcRegMode componentMode = static_cast(bit::extract(token, 2, 3)); switch (componentMode) { - // Write mask for destination operands case DxbcRegMode::Mask: reg.mask = bit::extract(token, 4, 7); reg.swizzle = DxbcRegSwizzle(0, 1, 2, 3); break; - // Swizzle for source operands (including resources) case DxbcRegMode::Swizzle: reg.mask = DxbcRegMask(true, true, true, true); reg.swizzle = DxbcRegSwizzle( @@ -199,9 +171,6 @@ namespace dxvk { bit::extract(token, 10, 11)); break; - // Selection of one component. We can generate both a - // mask and a swizzle for this so that the compiler - // won't have to deal with this case specifically. case DxbcRegMode::Select1: { const uint32_t n = bit::extract(token, 4, 5); reg.mask = DxbcRegMask(n == 0, n == 1, n == 2, n == 3); @@ -223,13 +192,10 @@ namespace dxvk { while (bit::extract(token, 31, 31)) { token = code.read(); - // Type of the extended operand token const DxbcOperandExt extTokenType = static_cast(bit::extract(token, 0, 5)); switch (extTokenType) { - // Operand modifiers, which are used to manipulate the - // value of a source operand during the load operation case DxbcOperandExt::OperandModifier: reg.modifiers = bit::extract(token, 6, 13); break; @@ -247,13 +213,10 @@ namespace dxvk { if (reg.type == DxbcOperandType::Imm32 || reg.type == DxbcOperandType::Imm64) { switch (reg.componentCount) { - // This is commonly used if only one vector - // component is involved in an operation case DxbcComponentCount::Component1: { reg.imm.u32_1 = code.read(); } break; - // Typical four-component vector case DxbcComponentCount::Component4: { reg.imm.u32_4[0] = code.read(); reg.imm.u32_4[1] = code.read(); @@ -272,7 +235,6 @@ namespace dxvk { reg.idxDim = bit::extract(token, 20, 21); for (uint32_t i = 0; i < reg.idxDim; i++) { - // An index can be encoded in various different ways const DxbcOperandIndexRepresentation repr = static_cast( bit::extract(token, 22 + 3 * i, 24 + 3 * i)); diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_defs.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_defs.cpp index 0d193ada9..34effc2d3 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_defs.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_defs.cpp @@ -3,344 +3,273 @@ namespace dxvk { const std::array g_instructionFormats = {{ - /* Add */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* And */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Break */ { 0, DxbcInstClass::ControlFlow }, - /* Breakc */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Call */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Callc */ { 2, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Case */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Continue */ { 0, DxbcInstClass::ControlFlow }, - /* Continuec */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Cut */ { 0, DxbcInstClass::GeometryEmit }, - /* Default */ { 0, DxbcInstClass::ControlFlow }, - /* DerivRtx */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DerivRty */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Discard */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Div */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Dp2 */ { 3, DxbcInstClass::VectorDot, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Dp3 */ { 3, DxbcInstClass::VectorDot, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Dp4 */ { 3, DxbcInstClass::VectorDot, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Else */ { 0, DxbcInstClass::ControlFlow }, - /* Emit */ { 0, DxbcInstClass::GeometryEmit }, - /* EmitThenCut */ { 0, DxbcInstClass::GeometryEmit }, - /* EndIf */ { 0, DxbcInstClass::ControlFlow }, - /* EndLoop */ { 0, DxbcInstClass::ControlFlow }, - /* EndSwitch */ { 0, DxbcInstClass::ControlFlow }, - /* Eq */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Exp */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Frc */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* FtoI */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* FtoU */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Ge */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* IAdd */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* If */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* IEq */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IGe */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* ILt */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IMad */ { 4, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IMax */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IMin */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IMul */ { 4, DxbcInstClass::VectorImul, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* INe */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* INeg */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IShl */ { 3, DxbcInstClass::VectorShift, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* IShr */ { 3, DxbcInstClass::VectorShift, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ItoF */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* Label */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* Ld */ { 3, DxbcInstClass::TextureFetch, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* LdMs */ { 4, DxbcInstClass::TextureFetch, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* Log */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Loop */ { 0, DxbcInstClass::ControlFlow }, - /* Lt */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Mad */ { 4, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Min */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Max */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* CustomData */ { 0, DxbcInstClass::CustomData }, - /* Mov */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Movc */ { 4, DxbcInstClass::VectorCmov, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Mul */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Ne */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Nop */ { 0, DxbcInstClass::NoOperation }, - /* Not */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Or */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ResInfo */ { 3, DxbcInstClass::TextureQuery, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Ret */ { 0, DxbcInstClass::ControlFlow }, - /* Retc */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* RoundNe */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* RoundNi */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* RoundPi */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* RoundZ */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Rsq */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Sample */ { 4, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleC */ { 5, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -348,7 +277,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleClz */ { 5, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -356,7 +284,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleL */ { 5, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -364,7 +291,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleD */ { 6, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -373,7 +299,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleB */ { 5, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -381,240 +306,188 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Sqrt */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Switch */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* SinCos */ { 3, DxbcInstClass::VectorSinCos, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* UDiv */ { 4, DxbcInstClass::VectorIdiv, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ULt */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UGe */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UMul */ { 4, DxbcInstClass::VectorImul, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UMad */ { 4, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UMax */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UMin */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UShr */ { 3, DxbcInstClass::VectorShift, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UtoF */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Xor */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* DclResource */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclConstantBuffer */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclSampler */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclIndexRange */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclGsOutputPrimitiveTopology */ { 0, DxbcInstClass::Declaration }, - /* DclGsInputPrimitive */ { 0, DxbcInstClass::Declaration }, - /* DclMaxOutputVertexCount */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclInput */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclInputSgv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclInputSiv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclInputPs */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclInputPsSgv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclInputPsSiv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclOutput */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclOutputSgv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclOutputSiv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclTemps */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclIndexableTemp */ { 3, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclGlobalFlags */ { 0, DxbcInstClass::Declaration }, - /* Reserved0 */ { 0, DxbcInstClass::Undefined }, - /* Lod */ { 4, DxbcInstClass::TextureQueryLod, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4 */ { 4, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SamplePos */ { 3, DxbcInstClass::TextureQueryMsPos, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* SampleInfo */ { 2, DxbcInstClass::TextureQueryMs, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Reserved1 */ { }, - /* HsDecls */ { 0, DxbcInstClass::HullShaderPhase }, - /* HsControlPointPhase */ { 0, DxbcInstClass::HullShaderPhase }, - /* HsForkPhase */ { 0, DxbcInstClass::HullShaderPhase }, - /* HsJoinPhase */ { 0, DxbcInstClass::HullShaderPhase }, - /* EmitStream */ { 1, DxbcInstClass::GeometryEmit, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* CutStream */ { 1, DxbcInstClass::GeometryEmit, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* EmitThenCutStream */ { 1, DxbcInstClass::GeometryEmit, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* InterfaceCall */ { }, - /* BufInfo */ { 2, DxbcInstClass::BufferQuery, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* DerivRtxCoarse */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DerivRtxFine */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DerivRtyCoarse */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DerivRtyFine */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4C */ { 5, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -622,7 +495,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4Po */ { 5, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -630,7 +502,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4PoC */ { 6, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -639,60 +510,48 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Rcp */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* F32toF16 */ { 2, DxbcInstClass::ConvertFloat16, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* F16toF32 */ { 2, DxbcInstClass::ConvertFloat16, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UAddc */ { }, - /* USubb */ { }, - /* CountBits */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* FirstBitHi */ { 2, DxbcInstClass::BitScan, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* FirstBitLo */ { 2, DxbcInstClass::BitScan, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* FirstBitShi */ { 2, DxbcInstClass::BitScan, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UBfe */ { 4, DxbcInstClass::BitExtract, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* IBfe */ { 4, DxbcInstClass::BitExtract, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* Bfi */ { 5, DxbcInstClass::BitInsert, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, @@ -700,12 +559,10 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* BfRev */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Swapc */ { 5, DxbcInstClass::VectorCmov, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, @@ -713,217 +570,174 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DclStream */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* DclFunctionBody */ { }, - /* DclFunctionTable */ { }, - /* DclInterface */ { }, - /* DclInputControlPointCount */ { 0, DxbcInstClass::Declaration }, - /* DclOutputControlPointCount */ { 0, DxbcInstClass::Declaration }, - /* DclTessDomain */ { 0, DxbcInstClass::Declaration }, - /* DclTessPartitioning */ { 0, DxbcInstClass::Declaration }, - /* DclTessOutputPrimitive */ { 0, DxbcInstClass::Declaration }, - /* DclHsMaxTessFactor */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Float32 }, } }, - /* DclHsForkPhaseInstanceCount */ { 1, DxbcInstClass::HullShaderInstCnt, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclHsJoinPhaseInstanceCount */ { 1, DxbcInstClass::HullShaderInstCnt, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclThreadGroup */ { 3, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclUavTyped */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclUavRaw */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclUavStructured */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclThreadGroupSharedMemoryRaw */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclThreadGroupSharedMemoryStructured */ { 3, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclResourceRaw */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclResourceStructured */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* LdUavTyped */ { 3, DxbcInstClass::TypedUavLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* StoreUavTyped */ { 3, DxbcInstClass::TypedUavStore, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* LdRaw */ { 3, DxbcInstClass::BufferLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* StoreRaw */ { 3, DxbcInstClass::BufferStore, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* LdStructured */ { 4, DxbcInstClass::BufferLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* StoreStructured */ { 4, DxbcInstClass::BufferStore, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicAnd */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicOr */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicXor */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicCmpStore */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicIAdd */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicIMax */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* AtomicIMin */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* AtomicUMax */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicUMin */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicAlloc */ { 2, DxbcInstClass::AtomicCounter, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicConsume */ { 2, DxbcInstClass::AtomicCounter, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicIAdd */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicAnd */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicOr */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicXor */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicExch */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicCmpExch */ { 5, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -931,181 +745,147 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicIMax */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* ImmAtomicIMin */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* ImmAtomicUMax */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicUMin */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Sync */ { 0, DxbcInstClass::Barrier }, - /* DAdd */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DMax */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DMin */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DMul */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DEq */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DGe */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DLt */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DNe */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DMov */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DMovc */ { 4, DxbcInstClass::VectorCmov, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DtoF */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* FtoD */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* EvalSnapped */ { 3, DxbcInstClass::Interpolate, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* EvalSampleIndex */ { 3, DxbcInstClass::Interpolate, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* EvalCentroid */ { 2, DxbcInstClass::Interpolate, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DclGsInstanceCount */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* Abort */ { }, - /* DebugBreak */ { }, - /* ReservedBegin11_1 */ { }, - /* DDiv */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DFma */ { 4, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DRcp */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* Msad */ { 4, DxbcInstClass::VectorMsad, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* DtoI */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DtoU */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* ItoD */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* UtoD */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ReservedBegin11_2 */ { }, - /* Gather4S */ { 5, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1113,7 +893,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4CS */ { 6, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1122,7 +901,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4PoS */ { 6, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1131,7 +909,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4PoCS */ { 7, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1141,14 +918,12 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* LdS */ { 4, DxbcInstClass::TextureFetch, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* LdMsS */ { 5, DxbcInstClass::TextureFetch, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1156,21 +931,18 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* LdUavTypedS */ { 4, DxbcInstClass::TypedUavLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* LdRawS */ { 4, DxbcInstClass::BufferLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* LdStructuredS */ { 5, DxbcInstClass::BufferLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1178,7 +950,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* SampleLS */ { 6, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1187,7 +958,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleClzS */ { 6, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1196,7 +966,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleClampS */ { 6, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1205,7 +974,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleBClampS */ { 7, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1215,7 +983,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleDClampS */ { 8, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1226,7 +993,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleCClampS */ { 7, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1236,7 +1002,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* CheckAccessFullyMapped */ { 2, DxbcInstClass::SparseCheckAccess, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_header.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_header.cpp index 9b5f69895..c73eef378 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_header.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_header.cpp @@ -3,21 +3,18 @@ namespace dxvk { DxbcHeader::DxbcHeader(DxbcReader& reader) { - // FourCC at the start of the file, must be 'DXBC' DxbcTag fourcc = reader.readTag(); if (fourcc != "DXBC") throw DxvkError("DxbcHeader::DxbcHeader: Invalid fourcc, expected 'DXBC'"); - // Stuff we don't actually need to store - reader.skip(4 * sizeof(uint32_t)); // Check sum - reader.skip(1 * sizeof(uint32_t)); // Constant 1 - reader.skip(1 * sizeof(uint32_t)); // Bytecode length + + reader.skip(4 * sizeof(uint32_t)); + reader.skip(1 * sizeof(uint32_t)); + reader.skip(1 * sizeof(uint32_t)); - // Number of chunks in the file uint32_t chunkCount = reader.readu32(); - // Chunk offsets are stored immediately after for (uint32_t i = 0; i < chunkCount; i++) m_chunkOffsets.push_back(reader.readu32()); } diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_module.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_module.cpp index 573d503b0..c2ff65673 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_module.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_module.cpp @@ -8,13 +8,9 @@ namespace dxvk { : m_header(reader) { for (uint32_t i = 0; i < m_header.numChunks(); i++) { - // The chunk tag is stored at the beginning of each chunk auto chunkReader = reader.clone(m_header.chunkOffset(i)); auto tag = chunkReader.readTag(); - // The chunk size follows right after the four-character - // code. This does not include the eight bytes that are - // consumed by the FourCC and chunk length entry. auto chunkLength = chunkReader.readu32(); chunkReader = chunkReader.clone(8); diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_names.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_names.cpp index c6a00eccc..ce9dfbd63 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_names.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_names.cpp @@ -442,4 +442,4 @@ namespace dxvk { } -} //namespace dxvk +} diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_util.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_util.cpp index 3d885fc9c..e1c1c4418 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_util.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_util.cpp @@ -4,14 +4,14 @@ namespace dxvk { uint32_t primitiveVertexCount(DxbcPrimitive primitive) { static const std::array s_vertexCounts = { - 0, // Undefined - 1, // Point - 2, // Line - 3, // Triangle - 0, // Undefined - 0, // Undefined - 4, // Line with adjacency - 6, // Triangle with adjacency + 0, + 1, + 2, + 3, + 0, + 0, + 4, + 6, }; if (primitive >= DxbcPrimitive::Patch1) { diff --git a/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_code_buffer.cpp b/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_code_buffer.cpp index f8166df51..456c46d9e 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_code_buffer.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_code_buffer.cpp @@ -130,7 +130,6 @@ namespace dxvk { } } - // Commit current word this->putWord(word); } @@ -138,9 +137,9 @@ namespace dxvk { void SpirvCodeBuffer::putHeader(uint32_t version, uint32_t boundIds) { this->putWord(spv::MagicNumber); this->putWord(version); - this->putWord(0); // Generator + this->putWord(0); this->putWord(boundIds); - this->putWord(0); // Schema + this->putWord(0); } @@ -152,7 +151,6 @@ namespace dxvk { uint32_t SpirvCodeBuffer::strLen(const char* str) { - // Null-termination plus padding return (std::strlen(str) + 4) / 4; } diff --git a/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_module.cpp b/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_module.cpp index aad1b39ec..f37d93b24 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_module.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_module.cpp @@ -29,9 +29,6 @@ namespace dxvk { result.append(m_typeConstDefs); result.append(m_variables); - // Perform some crude dead code elimination. In some cases, our compilers - // may emit invalid code, such as an unreachable block branching to a loop's - // continue block, but those cases cannot be reasonably detected up-front. std::unordered_set reachableBlocks; std::unordered_set mergeBlocks; @@ -78,8 +75,6 @@ namespace dxvk { void SpirvModule::enableCapability( spv::Capability capability) { - // Scan the generated instructions to check - // whether we already enabled the capability. if (!hasCapability(capability)) { m_capabilities.putIns (spv::OpCapability, 2); m_capabilities.putWord(capability); @@ -416,7 +411,6 @@ namespace dxvk { std::array args = { value, value, value, value }; - // Can't make a scalar composite. if (count == 1) return args[0]; @@ -434,7 +428,6 @@ namespace dxvk { std::array args = { value, value, value, value }; - // Can't make a scalar composite. if (count == 1) return args[0]; @@ -452,7 +445,6 @@ namespace dxvk { std::array args = { value, value, value, value }; - // Can't make a scalar composite. if (count == 1) return args[0]; @@ -470,7 +462,6 @@ namespace dxvk { std::array args = { value, value, value, value }; - // Can't make a scalar composite. if (count == 1) return args[0]; @@ -3765,7 +3756,6 @@ namespace dxvk { uint32_t SpirvModule::opSinCos( uint32_t x, bool useBuiltIn) { - // We only operate on 32-bit floats here uint32_t floatType = defFloatType(32); uint32_t resultType = defVectorType(floatType, 2u); @@ -3777,7 +3767,6 @@ namespace dxvk { uint32_t sintType = defIntType(32, true); uint32_t boolType = defBoolType(); - // Normalize input to multiple of pi/4 uint32_t xNorm = opFMul(floatType, opFAbs(floatType, x), constf32(4.0 / pi)); uint32_t xTrunc = opTrunc(floatType, xNorm); @@ -3785,11 +3774,9 @@ namespace dxvk { uint32_t xInt = opConvertFtoU(uintType, xTrunc); - // Mirror input along x axis as necessary uint32_t mirror = opINotEqual(boolType, opBitwiseAnd(uintType, xInt, constu32(1u)), constu32(0u)); xFract = opSelect(floatType, mirror, opFSub(floatType, constf32(1.0f), xFract), xFract); - // Compute taylor series for fractional part uint32_t xFract_2 = opFMul(floatType, xFract, xFract); uint32_t xFract_4 = opFMul(floatType, xFract_2, xFract_2); uint32_t xFract_6 = opFMul(floatType, xFract_4, xFract_2); @@ -3809,23 +3796,18 @@ namespace dxvk { taylor = opFMul(floatType, taylor, xFract); decorate(taylor, spv::DecorationNoContraction); - // Compute co-function based on sin^2 + cos^2 = 1 uint32_t coFunc = opSqrt(floatType, opFSub(floatType, constf32(1.0f), opFMul(floatType, taylor, taylor))); - // Determine whether the taylor series was used for sine or cosine and assign the correct result uint32_t funcIsSin = opIEqual(boolType, opBitwiseAnd(uintType, opIAdd(uintType, xInt, constu32(1u)), constu32(2u)), constu32(0u)); uint32_t sin = opSelect(floatType, funcIsSin, taylor, coFunc); uint32_t cos = opSelect(floatType, funcIsSin, coFunc, taylor); - // Determine whether sine is negative. Interpret the input as a - // signed integer in order to propagate signed zeroes properly. uint32_t inputNeg = opSLessThan(boolType, opBitcast(sintType, x), consti32(0)); uint32_t sinNeg = opINotEqual(boolType, opBitwiseAnd(uintType, xInt, constu32(4u)), constu32(0u)); sinNeg = opLogicalNotEqual(boolType, sinNeg, inputNeg); - // Determine whether cosine is negative uint32_t cosNeg = opINotEqual(boolType, opBitwiseAnd(uintType, opIAdd(uintType, xInt, constu32(2u)), constu32(4u)), constu32(0u)); sin = opSelect(floatType, sinNeg, opFNegate(floatType, sin), sin); @@ -3841,9 +3823,6 @@ namespace dxvk { spv::Op op, uint32_t argCount, const uint32_t* argIds) { - // Since the type info is stored in the code buffer, - // we can use the code buffer to look up type IDs as - // well. Result IDs are always stored as argument 1. for (auto ins : m_typeConstDefs) { bool match = ins.opCode() == op && ins.length() == 2 + argCount; @@ -3855,7 +3834,6 @@ namespace dxvk { return ins.arg(1); } - // Type not yet declared, create a new one. uint32_t resultId = this->allocateId(); m_typeConstDefs.putIns (op, 2 + argCount); m_typeConstDefs.putWord(resultId); @@ -3871,7 +3849,6 @@ namespace dxvk { uint32_t typeId, uint32_t argCount, const uint32_t* argIds) { - // Avoid declaring constants multiple times for (auto ins : m_typeConstDefs) { bool match = ins.opCode() == op && ins.length() == 3 + argCount @@ -3889,7 +3866,6 @@ namespace dxvk { return id; } - // Constant not yet declared, make a new one uint32_t resultId = this->allocateId(); m_typeConstDefs.putIns (op, 3 + argCount); m_typeConstDefs.putWord(typeId); @@ -3940,7 +3916,6 @@ namespace dxvk { uint32_t SpirvModule::getImageOperandWordCount(const SpirvImageOperands& op) const { - // Each flag may add one or more operands const uint32_t result = ((op.flags & spv::ImageOperandsBiasMask) ? 1 : 0) + ((op.flags & spv::ImageOperandsLodMask) ? 1 : 0) @@ -3953,7 +3928,6 @@ namespace dxvk { + ((op.flags & spv::ImageOperandsMakeTexelAvailableMask) ? 1 : 0) + ((op.flags & spv::ImageOperandsMakeTexelVisibleMask) ? 1 : 0); - // Add a DWORD for the operand mask if it is non-zero return op.flags ? result + 1 : 0; } @@ -4003,7 +3977,6 @@ namespace dxvk { return sclass == spv::StorageClassInput || sclass == spv::StorageClassOutput; } else { - // All global variables need to be declared return sclass != spv::StorageClassFunction; } } @@ -4055,10 +4028,6 @@ namespace dxvk { case spv::OpLoopMerge: { mergeBlocks.insert(ins.arg(1)); - // It is possible for the continue block to be unreachable in - // practice, but we still need to emit it if we are not going - // to eliminate this loop. Since the current block dominates - // the loop, use it to keep the continue block intact. branches.insert({ blockId, ins.arg(2) }); } break; diff --git a/app/src/main/cpp/thirdparty/dxbc/src/util/util_string.cpp b/app/src/main/cpp/thirdparty/dxbc/src/util/util_string.cpp index 9bb4b6114..83403e536 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/util/util_string.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/util/util_string.cpp @@ -9,20 +9,15 @@ namespace dxvk::str { uint32_t first = begin[0]; if (likely(first < 0x80)) { - // Basic ASCII character ch = uint32_t(first); return begin + 1; } else if (unlikely(first < 0xC0)) { - // Character starts with a continuation byte, - // just skip until we find the next valid prefix while ((begin < end) && (((*begin) & 0xC0) == 0x80)) begin += 1; ch = uint32_t('?'); return begin; } else { - // The number of leading 1 bits in the first byte - // determines the length of this character size_t length = bit::lzcnt((~first) << 24); if (unlikely(begin + length > end)) { @@ -43,7 +38,6 @@ namespace dxvk::str { | ((uint32_t(begin[2]) & 0x3F) << 6) | ((uint32_t(begin[3]) & 0x3F)); } else { - // Invalid prefix ch = uint32_t('?'); } @@ -71,7 +65,6 @@ namespace dxvk::str { + ((uint32_t(begin[1]) & 0x3FF)); return begin + 2; } else if (unlikely(first < 0xE000)) { - // Stray low surrogate ch = uint32_t('?'); return begin + 1; } else { @@ -137,7 +130,6 @@ namespace dxvk::str { return 4; } else { - // Invalid code point for UTF-8 return 0; } } @@ -157,8 +149,6 @@ namespace dxvk::str { return 1; } else if (ch < 0xE000) { - // Private use code points, - // we can't encode these return 0; } else if (ch < 0x10000) { if (begin) { @@ -181,7 +171,6 @@ namespace dxvk::str { return 2; } else { - // Invalid code point return 0; } } diff --git a/app/src/main/cpp/waylandcomp/CMakeLists.txt b/app/src/main/cpp/waylandcomp/CMakeLists.txt index 7cd4255dd..bd7c4a6ba 100644 --- a/app/src/main/cpp/waylandcomp/CMakeLists.txt +++ b/app/src/main/cpp/waylandcomp/CMakeLists.txt @@ -1,9 +1,3 @@ -# ── Embedded Wayland compositor (experimental parallel runtime) ────────────── -# Runs a libwayland-server compositor in-process so games launched via Wine's -# winewayland.drv can present into it. Protocol glue is pre-generated in -# generated/ (no host wayland-scanner needed at app build time). Built by the app -# (add_subdirectory from ../CMakeLists.txt) and on its own by standalone/ for -# build-waylandcomp.yml. add_library(droiddeckwayland SHARED src/compositor.c src/vk_present.c @@ -38,25 +32,15 @@ add_library(droiddeckwayland SHARED src/framegen_bridge.c src/framegen_jni.c ) -# Warnings on for the compositor's own C. They do not fail the build; they are there to be read. target_compile_options(droiddeckwayland PRIVATE $<$:-Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers>) target_include_directories(droiddeckwayland PRIVATE generated prebuilt/include src - # The X11 Vulkan renderer's committed SPIR-V headers (*_frag.h / upscale_vert.h, made by - # Bannerlator's gen_shaders.sh): the screen-effect chain (src/effects_chain.c) runs the same bytes. ${CMAKE_CURRENT_SOURCE_DIR}/../shaders ) -# ── Frame generation (feat/wayland-framegen) ───────────────────────────────── -# The two native engines the X11 renderer runs (LSFG Native: framegen/lsfg, Win-FG Native: -# framegen/winfg) are compiled into this library unmodified and driven by -# src/framegen_engine.cpp through the compositor's own Turnip entry points. They need the -# framegen tree and the vendored DXBC translator (thirdparty/dxbc, for the LSFG cache format), -# which only the app build has: the standalone build (standalone/) gets the stub, which reports -# every engine unavailable and generates nothing. set(DROIDDECK_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/lsfg) set(DROIDDECK_FRAMEGEN_WINFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/winfg) if (TARGET dxbc AND EXISTS ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp @@ -80,15 +64,14 @@ if (TARGET dxbc AND EXISTS ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_pacer.cpp ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_governor.cpp - # The Lossless.dll import (validate / extract / cache) for the app's LsfgNative. ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_jni.cpp ${DROIDDECK_FRAMEGEN_WINFG_DIR}/framegen.cpp ${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_vkd.cpp ${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp ) target_include_directories(droiddeckwayland PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/.. # as the frame-generation engines see it - ${CMAKE_CURRENT_SOURCE_DIR}/../framegen # VulkanRendererContext.h (VkTable), lsfg/, winfg/ + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_SOURCE_DIR}/../framegen ) target_compile_options(droiddeckwayland PRIVATE $<$:-std=c++17 -fvisibility=hidden>) @@ -104,8 +87,6 @@ endif() add_library(wayland-server SHARED IMPORTED) set_target_properties(wayland-server PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/prebuilt/lib/libwayland-server.so) -# Vulkan is loaded at runtime via adrenotools (Turnip), not linked directly - so the -# compositor uses Turnip's driver, which supports dmabuf import (drm_format_modifier). target_link_libraries(droiddeckwayland wayland-server adrenotools diff --git a/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-protocol.c index 08e5d76e0..17b0c0dfd 100644 --- a/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright 2026 The412Banner * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-server-protocol.h index a06ea0706..788982c10 100644 --- a/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef BANNER_AHB_V1_SERVER_PROTOCOL_H #define BANNER_AHB_V1_SERVER_PROTOCOL_H @@ -14,12 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_banner_ahb_v1 The banner_ahb_v1 protocol - * @section page_ifaces_banner_ahb_v1 Interfaces - * - @subpage page_iface_banner_ahb_v1 - Android gralloc buffers behind dma-buf wl_buffers +/* * @section page_copyright_banner_ahb_v1 Copyright - *
  *
  * Copyright 2026 The412Banner
  *
@@ -41,132 +36,23 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + struct banner_ahb_v1; struct wl_buffer; #ifndef BANNER_AHB_V1_INTERFACE #define BANNER_AHB_V1_INTERFACE -/** - * @page page_iface_banner_ahb_v1 banner_ahb_v1 - * @section page_iface_banner_ahb_v1_desc Description - * - * Zero-copy window layers on Android. A Vulkan swapchain image that the - * client allocated as an Android AHardwareBuffer (gralloc) is still shared - * as a dma-buf through zwp_linux_dmabuf_v1, so the compositor can import - * and draw it the usual way. This interface additionally hands the - * compositor the AHardwareBuffer itself, once per wl_buffer, so the - * compositor can put the buffer straight on an Android SurfaceControl - * layer (SurfaceFlinger / the display hardware composite it) without - * copying it. - * - * The AHardwareBuffer travels over a Unix socket: the client creates a - * socketpair, writes the buffer's handle into one end with - * AHardwareBuffer_sendHandleToUnixSocket, then sends the other end in the - * attach request. The compositor reads it with - * AHardwareBuffer_recvHandleFromUnixSocket and closes the socket. Its - * reference to the AHardwareBuffer lives as long as the wl_buffer (plus - * whatever the display still holds). - * - * The compositor advertises this global whenever it can put buffers on a - * display layer at all; whether it WANTS gralloc buffers right now is the - * mode event (version 2), sent on bind and every time the user switches - * zero-copy on or off during the session. A client decides per swapchain, - * when it creates the swapchain, from the last mode it received, and a - * client whose live swapchain was built for the other mode retires it - * (VK_ERROR_OUT_OF_DATE_KHR on its next acquire or present) so the program - * rebuilds it for the new mode. Buffers of the old swapchain stay fully - * presentable until then: an attached AHardwareBuffer keeps its association - * for the wl_buffer's lifetime whatever the mode is now. A version 1 - * client (no mode event) keeps deciding from its own environment. A - * client that does not see the global (or does not want it) allocates - * swapchain images the standard way; nothing else changes. - * - * Synchronisation stays implicit: the dma-buf carries the client's render - * fence (DMA_BUF_IOCTL_IMPORT_SYNC_FILE by the client's driver), the - * compositor exports it as the layer's acquire fence, and the compositor - * imports the display's release fence into the dma-buf before it sends - * wl_buffer.release, so the client's next acquire waits on it. - * @section page_iface_banner_ahb_v1_api API - * See @ref iface_banner_ahb_v1. - */ -/** - * @defgroup iface_banner_ahb_v1 The banner_ahb_v1 interface - * - * Zero-copy window layers on Android. A Vulkan swapchain image that the - * client allocated as an Android AHardwareBuffer (gralloc) is still shared - * as a dma-buf through zwp_linux_dmabuf_v1, so the compositor can import - * and draw it the usual way. This interface additionally hands the - * compositor the AHardwareBuffer itself, once per wl_buffer, so the - * compositor can put the buffer straight on an Android SurfaceControl - * layer (SurfaceFlinger / the display hardware composite it) without - * copying it. - * - * The AHardwareBuffer travels over a Unix socket: the client creates a - * socketpair, writes the buffer's handle into one end with - * AHardwareBuffer_sendHandleToUnixSocket, then sends the other end in the - * attach request. The compositor reads it with - * AHardwareBuffer_recvHandleFromUnixSocket and closes the socket. Its - * reference to the AHardwareBuffer lives as long as the wl_buffer (plus - * whatever the display still holds). - * - * The compositor advertises this global whenever it can put buffers on a - * display layer at all; whether it WANTS gralloc buffers right now is the - * mode event (version 2), sent on bind and every time the user switches - * zero-copy on or off during the session. A client decides per swapchain, - * when it creates the swapchain, from the last mode it received, and a - * client whose live swapchain was built for the other mode retires it - * (VK_ERROR_OUT_OF_DATE_KHR on its next acquire or present) so the program - * rebuilds it for the new mode. Buffers of the old swapchain stay fully - * presentable until then: an attached AHardwareBuffer keeps its association - * for the wl_buffer's lifetime whatever the mode is now. A version 1 - * client (no mode event) keeps deciding from its own environment. A - * client that does not see the global (or does not want it) allocates - * swapchain images the standard way; nothing else changes. - * - * Synchronisation stays implicit: the dma-buf carries the client's render - * fence (DMA_BUF_IOCTL_IMPORT_SYNC_FILE by the client's driver), the - * compositor exports it as the layer's acquire fence, and the compositor - * imports the display's release fence into the dma-buf before it sends - * wl_buffer.release, so the client's next acquire waits on it. - */ extern const struct wl_interface banner_ahb_v1_interface; #endif -/** - * @ingroup iface_banner_ahb_v1 - * @struct banner_ahb_v1_interface - */ struct banner_ahb_v1_interface { - /** - * destroy the object - * - * Buffers already attached keep their AHardwareBuffer - * association. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * hand the compositor a buffer's AHardwareBuffer - * - * Associates the AHardwareBuffer readable from socket with the - * (already created) dma-buf wl_buffer. width, height and stride - * (pixels) describe the gralloc allocation; modifier_hi/lo is the - * DRM format modifier the dma-buf was shared with (the same value - * passed to zwp_linux_buffer_params_v1.add); image_count is the - * number of images in the swapchain this buffer belongs to (for - * the compositor's log). Sent once per wl_buffer, before the - * buffer is first attached to a surface. - * @param buffer the dma-buf wl_buffer - * @param socket socket the AHardwareBuffer handle was written to - * @param width buffer width in pixels - * @param height buffer height in pixels - * @param stride gralloc row stride in pixels - * @param modifier_hi high 32 bits of the DRM format modifier - * @param modifier_lo low 32 bits of the DRM format modifier - * @param image_count images in the client's swapchain - */ void (*attach)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *buffer, @@ -181,26 +67,11 @@ struct banner_ahb_v1_interface { #define BANNER_AHB_V1_MODE 0 -/** - * @ingroup iface_banner_ahb_v1 - */ #define BANNER_AHB_V1_MODE_SINCE_VERSION 2 -/** - * @ingroup iface_banner_ahb_v1 - */ #define BANNER_AHB_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_banner_ahb_v1 - */ #define BANNER_AHB_V1_ATTACH_SINCE_VERSION 1 -/** - * @ingroup iface_banner_ahb_v1 - * Sends an mode event to the client owning the resource. - * @param resource_ The client's resource - * @param enabled 1 = gralloc swapchain images wanted, 0 = not - */ static inline void banner_ahb_v1_send_mode(struct wl_resource *resource_, uint32_t enabled) { diff --git a/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-protocol.c index 9ed8996dd..ae0a87daa 100644 --- a/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright 2026 The412Banner * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-server-protocol.h index f2608a46e..78f65e7fb 100644 --- a/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef BANNER_DESKTOP_V1_SERVER_PROTOCOL_H #define BANNER_DESKTOP_V1_SERVER_PROTOCOL_H @@ -14,12 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_banner_desktop_v1 The banner_desktop_v1 protocol - * @section page_ifaces_banner_desktop_v1 Interfaces - * - @subpage page_iface_banner_desktop_v1 - Windows virtual desktop layout +/* * @section page_copyright_banner_desktop_v1 Copyright - *
  *
  * Copyright 2026 The412Banner
  *
@@ -41,98 +36,41 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + struct banner_desktop_v1; struct wl_surface; #ifndef BANNER_DESKTOP_V1_INTERFACE #define BANNER_DESKTOP_V1_INTERFACE -/** - * @page page_iface_banner_desktop_v1 banner_desktop_v1 - * @section page_iface_banner_desktop_v1_desc Description - * - * Each Wine process is a separate Wayland client, so a Windows virtual - * desktop can't be built from subsurfaces. Instead the process that owns - * the desktop window marks one surface as the desktop, and every process - * reports where its top-level windows sit on that desktop and the global - * window stacking order. The compositor draws the desktop at the bottom, - * the windows at the reported positions, and sends pointer and keyboard - * input to the desktop surface; Wine routes it to the right window. - * @section page_iface_banner_desktop_v1_api API - * See @ref iface_banner_desktop_v1. - */ -/** - * @defgroup iface_banner_desktop_v1 The banner_desktop_v1 interface - * - * Each Wine process is a separate Wayland client, so a Windows virtual - * desktop can't be built from subsurfaces. Instead the process that owns - * the desktop window marks one surface as the desktop, and every process - * reports where its top-level windows sit on that desktop and the global - * window stacking order. The compositor draws the desktop at the bottom, - * the windows at the reported positions, and sends pointer and keyboard - * input to the desktop surface; Wine routes it to the right window. - */ extern const struct wl_interface banner_desktop_v1_interface; #endif -/** - * @ingroup iface_banner_desktop_v1 - * @struct banner_desktop_v1_interface - */ struct banner_desktop_v1_interface { - /** - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * mark a surface as the virtual desktop - * - * The surface is drawn at the origin, below every window, and - * sets the size of the desktop. - */ void (*set_desktop)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface); - /** - * place a top-level window on the desktop - * - * Position of the top-left corner of the surface in desktop - * coordinates, and the window handle used by set_zorder. - */ void (*set_window)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface, uint32_t hwnd, int32_t x, int32_t y); - /** - * report the desktop's window stacking order - * - * Visible top-level window handles as 32-bit values, topmost - * first. - */ void (*set_zorder)(struct wl_client *client, struct wl_resource *resource, struct wl_array *hwnds); }; -/** - * @ingroup iface_banner_desktop_v1 - */ #define BANNER_DESKTOP_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_banner_desktop_v1 - */ #define BANNER_DESKTOP_V1_SET_DESKTOP_SINCE_VERSION 1 -/** - * @ingroup iface_banner_desktop_v1 - */ #define BANNER_DESKTOP_V1_SET_WINDOW_SINCE_VERSION 1 -/** - * @ingroup iface_banner_desktop_v1 - */ #define BANNER_DESKTOP_V1_SET_ZORDER_SINCE_VERSION 1 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/generated/color-management-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/color-management-v1-protocol.c index ff31f8a41..e4fe936c2 100644 --- a/app/src/main/cpp/waylandcomp/generated/color-management-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/color-management-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright 2019 Sebastian Wick * Copyright 2019 Erwin Burema @@ -34,7 +32,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/color-management-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/color-management-v1-server-protocol.h index 08008e8f8..edd25e45b 100644 --- a/app/src/main/cpp/waylandcomp/generated/color-management-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/color-management-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef COLOR_MANAGEMENT_V1_SERVER_PROTOCOL_H #define COLOR_MANAGEMENT_V1_SERVER_PROTOCOL_H @@ -14,64 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_color_management_v1 The color_management_v1 protocol - * color management protocol - * - * @section page_desc_color_management_v1 Description - * - * The aim of the color management extension is to allow clients to know - * the color properties of outputs, and to tell the compositor about the color - * properties of their content on surfaces. Doing this enables a compositor - * to perform automatic color management of content for different outputs - * according to how content is intended to look like. - * - * The color properties are represented as an image description object which - * is immutable after it has been created. A wl_output always has an - * associated image description that clients can observe. A wl_surface - * always has an associated preferred image description as a hint chosen by - * the compositor that clients can also observe. Clients can set an image - * description on a wl_surface to denote the color characteristics of the - * surface contents. - * - * An image description includes SDR and HDR colorimetry and encoding, HDR - * metadata, and viewing environment parameters. An image description does - * not include the properties set through color-representation extension. - * It is expected that the color-representation extension is used in - * conjunction with the color management extension when necessary, - * particularly with the YUV family of pixel formats. - * - * Recommendation ITU-T H.273 - * "Coding-independent code points for video signal type identification" - * shall be referred to as simply H.273 here. - * - * The color-and-hdr repository - * (https://gitlab.freedesktop.org/pq/color-and-hdr) contains - * background information on the protocol design and legacy color management. - * It also contains a glossary, learning resources for digital color, tools, - * samples and more. - * - * The terminology used in this protocol is based on common color science and - * color encoding terminology where possible. The glossary in the color-and-hdr - * repository shall be the authority on the definition of terms in this - * protocol. - * - * Warning! The protocol described in this file is currently in the testing - * phase. Backward compatible changes may be added together with the - * corresponding interface version bump. Backward incompatible changes can - * only be done by creating a new major version of the extension. - * - * @section page_ifaces_color_management_v1 Interfaces - * - @subpage page_iface_wp_color_manager_v1 - color manager singleton - * - @subpage page_iface_wp_color_management_output_v1 - output color properties - * - @subpage page_iface_wp_color_management_surface_v1 - color management extension to a surface - * - @subpage page_iface_wp_color_management_surface_feedback_v1 - color management extension to a surface - * - @subpage page_iface_wp_image_description_creator_icc_v1 - holder of image description ICC information - * - @subpage page_iface_wp_image_description_creator_params_v1 - holder of image description parameters - * - @subpage page_iface_wp_image_description_v1 - Colorimetric image description - * - @subpage page_iface_wp_image_description_info_v1 - Colorimetric image description information +/* * @section page_copyright_color_management_v1 Copyright - *
  *
  * Copyright 2019 Sebastian Wick
  * Copyright 2019 Erwin Burema
@@ -98,8 +41,64 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_output; struct wl_surface; struct wp_color_management_output_v1; @@ -113,360 +112,47 @@ struct wp_image_description_v1; #ifndef WP_COLOR_MANAGER_V1_INTERFACE #define WP_COLOR_MANAGER_V1_INTERFACE -/** - * @page page_iface_wp_color_manager_v1 wp_color_manager_v1 - * @section page_iface_wp_color_manager_v1_desc Description - * - * A singleton global interface used for getting color management extensions - * for wl_surface and wl_output objects, and for creating client defined - * image description objects. The extension interfaces allow - * getting the image description of outputs and setting the image - * description of surfaces. - * - * Compositors should never remove this global. - * @section page_iface_wp_color_manager_v1_api API - * See @ref iface_wp_color_manager_v1. - */ -/** - * @defgroup iface_wp_color_manager_v1 The wp_color_manager_v1 interface - * - * A singleton global interface used for getting color management extensions - * for wl_surface and wl_output objects, and for creating client defined - * image description objects. The extension interfaces allow - * getting the image description of outputs and setting the image - * description of surfaces. - * - * Compositors should never remove this global. - */ extern const struct wl_interface wp_color_manager_v1_interface; #endif #ifndef WP_COLOR_MANAGEMENT_OUTPUT_V1_INTERFACE #define WP_COLOR_MANAGEMENT_OUTPUT_V1_INTERFACE -/** - * @page page_iface_wp_color_management_output_v1 wp_color_management_output_v1 - * @section page_iface_wp_color_management_output_v1_desc Description - * - * A wp_color_management_output_v1 describes the color properties of an - * output. - * - * The wp_color_management_output_v1 is associated with the wl_output global - * underlying the wl_output object. Therefore the client destroying the - * wl_output object has no impact, but the compositor removing the output - * global makes the wp_color_management_output_v1 object inert. - * @section page_iface_wp_color_management_output_v1_api API - * See @ref iface_wp_color_management_output_v1. - */ -/** - * @defgroup iface_wp_color_management_output_v1 The wp_color_management_output_v1 interface - * - * A wp_color_management_output_v1 describes the color properties of an - * output. - * - * The wp_color_management_output_v1 is associated with the wl_output global - * underlying the wl_output object. Therefore the client destroying the - * wl_output object has no impact, but the compositor removing the output - * global makes the wp_color_management_output_v1 object inert. - */ extern const struct wl_interface wp_color_management_output_v1_interface; #endif #ifndef WP_COLOR_MANAGEMENT_SURFACE_V1_INTERFACE #define WP_COLOR_MANAGEMENT_SURFACE_V1_INTERFACE -/** - * @page page_iface_wp_color_management_surface_v1 wp_color_management_surface_v1 - * @section page_iface_wp_color_management_surface_v1_desc Description - * - * A wp_color_management_surface_v1 allows the client to set the color - * space and HDR properties of a surface. - * - * If the wl_surface associated with the wp_color_management_surface_v1 is - * destroyed, the wp_color_management_surface_v1 object becomes inert. - * @section page_iface_wp_color_management_surface_v1_api API - * See @ref iface_wp_color_management_surface_v1. - */ -/** - * @defgroup iface_wp_color_management_surface_v1 The wp_color_management_surface_v1 interface - * - * A wp_color_management_surface_v1 allows the client to set the color - * space and HDR properties of a surface. - * - * If the wl_surface associated with the wp_color_management_surface_v1 is - * destroyed, the wp_color_management_surface_v1 object becomes inert. - */ extern const struct wl_interface wp_color_management_surface_v1_interface; #endif #ifndef WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_INTERFACE #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_INTERFACE -/** - * @page page_iface_wp_color_management_surface_feedback_v1 wp_color_management_surface_feedback_v1 - * @section page_iface_wp_color_management_surface_feedback_v1_desc Description - * - * A wp_color_management_surface_feedback_v1 allows the client to get the - * preferred image description of a surface. - * - * If the wl_surface associated with this object is destroyed, the - * wp_color_management_surface_feedback_v1 object becomes inert. - * @section page_iface_wp_color_management_surface_feedback_v1_api API - * See @ref iface_wp_color_management_surface_feedback_v1. - */ -/** - * @defgroup iface_wp_color_management_surface_feedback_v1 The wp_color_management_surface_feedback_v1 interface - * - * A wp_color_management_surface_feedback_v1 allows the client to get the - * preferred image description of a surface. - * - * If the wl_surface associated with this object is destroyed, the - * wp_color_management_surface_feedback_v1 object becomes inert. - */ extern const struct wl_interface wp_color_management_surface_feedback_v1_interface; #endif #ifndef WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_INTERFACE #define WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_INTERFACE -/** - * @page page_iface_wp_image_description_creator_icc_v1 wp_image_description_creator_icc_v1 - * @section page_iface_wp_image_description_creator_icc_v1_desc Description - * - * This type of object is used for collecting all the information required - * to create a wp_image_description_v1 object from an ICC file. A complete - * set of required parameters consists of these properties: - * - ICC file - * - * Each required property must be set exactly once if the client is to create - * an image description. The set requests verify that a property was not - * already set. The create request verifies that all required properties are - * set. There may be several alternative requests for setting each property, - * and in that case the client must choose one of them. - * - * Once all properties have been set, the create request must be used to - * create the image description object, destroying the creator in the - * process. - * @section page_iface_wp_image_description_creator_icc_v1_api API - * See @ref iface_wp_image_description_creator_icc_v1. - */ -/** - * @defgroup iface_wp_image_description_creator_icc_v1 The wp_image_description_creator_icc_v1 interface - * - * This type of object is used for collecting all the information required - * to create a wp_image_description_v1 object from an ICC file. A complete - * set of required parameters consists of these properties: - * - ICC file - * - * Each required property must be set exactly once if the client is to create - * an image description. The set requests verify that a property was not - * already set. The create request verifies that all required properties are - * set. There may be several alternative requests for setting each property, - * and in that case the client must choose one of them. - * - * Once all properties have been set, the create request must be used to - * create the image description object, destroying the creator in the - * process. - */ extern const struct wl_interface wp_image_description_creator_icc_v1_interface; #endif #ifndef WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_INTERFACE #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_INTERFACE -/** - * @page page_iface_wp_image_description_creator_params_v1 wp_image_description_creator_params_v1 - * @section page_iface_wp_image_description_creator_params_v1_desc Description - * - * This type of object is used for collecting all the parameters required - * to create a wp_image_description_v1 object. A complete set of required - * parameters consists of these properties: - * - transfer characteristic function (tf) - * - chromaticities of primaries and white point (primary color volume) - * - * The following properties are optional and have a well-defined default - * if not explicitly set: - * - primary color volume luminance range - * - reference white luminance level - * - mastering display primaries and white point (target color volume) - * - mastering luminance range - * - * The following properties are optional and will be ignored - * if not explicitly set: - * - maximum content light level - * - maximum frame-average light level - * - * Each required property must be set exactly once if the client is to create - * an image description. The set requests verify that a property was not - * already set. The create request verifies that all required properties are - * set. There may be several alternative requests for setting each property, - * and in that case the client must choose one of them. - * - * Once all properties have been set, the create request must be used to - * create the image description object, destroying the creator in the - * process. - * @section page_iface_wp_image_description_creator_params_v1_api API - * See @ref iface_wp_image_description_creator_params_v1. - */ -/** - * @defgroup iface_wp_image_description_creator_params_v1 The wp_image_description_creator_params_v1 interface - * - * This type of object is used for collecting all the parameters required - * to create a wp_image_description_v1 object. A complete set of required - * parameters consists of these properties: - * - transfer characteristic function (tf) - * - chromaticities of primaries and white point (primary color volume) - * - * The following properties are optional and have a well-defined default - * if not explicitly set: - * - primary color volume luminance range - * - reference white luminance level - * - mastering display primaries and white point (target color volume) - * - mastering luminance range - * - * The following properties are optional and will be ignored - * if not explicitly set: - * - maximum content light level - * - maximum frame-average light level - * - * Each required property must be set exactly once if the client is to create - * an image description. The set requests verify that a property was not - * already set. The create request verifies that all required properties are - * set. There may be several alternative requests for setting each property, - * and in that case the client must choose one of them. - * - * Once all properties have been set, the create request must be used to - * create the image description object, destroying the creator in the - * process. - */ extern const struct wl_interface wp_image_description_creator_params_v1_interface; #endif #ifndef WP_IMAGE_DESCRIPTION_V1_INTERFACE #define WP_IMAGE_DESCRIPTION_V1_INTERFACE -/** - * @page page_iface_wp_image_description_v1 wp_image_description_v1 - * @section page_iface_wp_image_description_v1_desc Description - * - * An image description carries information about the color encoding used on - * a surface when attached to a wl_surface via - * wp_color_management_surface_v1.set_image_description. A compositor can use - * this information to decode pixel values into colorimetrically meaningful - * quantities. - * - * Note, that the wp_image_description_v1 object is not ready to be used - * immediately after creation. The object eventually delivers either the - * 'ready' or the 'failed' event, specified in all requests creating it. The - * object is deemed "ready" after receiving the 'ready' event. - * - * An object which is not ready is illegal to use, it can only be destroyed. - * Any other request in this interface shall result in the 'not_ready' - * protocol error. Attempts to use an object which is not ready through other - * interfaces shall raise protocol errors defined there. - * - * Once created and regardless of how it was created, a - * wp_image_description_v1 object always refers to one fixed image - * description. It cannot change after creation. - * @section page_iface_wp_image_description_v1_api API - * See @ref iface_wp_image_description_v1. - */ -/** - * @defgroup iface_wp_image_description_v1 The wp_image_description_v1 interface - * - * An image description carries information about the color encoding used on - * a surface when attached to a wl_surface via - * wp_color_management_surface_v1.set_image_description. A compositor can use - * this information to decode pixel values into colorimetrically meaningful - * quantities. - * - * Note, that the wp_image_description_v1 object is not ready to be used - * immediately after creation. The object eventually delivers either the - * 'ready' or the 'failed' event, specified in all requests creating it. The - * object is deemed "ready" after receiving the 'ready' event. - * - * An object which is not ready is illegal to use, it can only be destroyed. - * Any other request in this interface shall result in the 'not_ready' - * protocol error. Attempts to use an object which is not ready through other - * interfaces shall raise protocol errors defined there. - * - * Once created and regardless of how it was created, a - * wp_image_description_v1 object always refers to one fixed image - * description. It cannot change after creation. - */ extern const struct wl_interface wp_image_description_v1_interface; #endif #ifndef WP_IMAGE_DESCRIPTION_INFO_V1_INTERFACE #define WP_IMAGE_DESCRIPTION_INFO_V1_INTERFACE -/** - * @page page_iface_wp_image_description_info_v1 wp_image_description_info_v1 - * @section page_iface_wp_image_description_info_v1_desc Description - * - * Sends all matching events describing an image description object exactly - * once and finally sends the 'done' event. - * - * This means - * - if the image description is parametric, it must send - * - primaries - * - named_primaries, if applicable - * - at least one of tf_power and tf_named, as applicable - * - luminances - * - target_primaries - * - target_luminance - * - if the image description is parametric, it may send, if applicable, - * - target_max_cll - * - target_max_fall - * - if the image description contains an ICC profile, it must send the - * icc_file event - * - * Once a wp_image_description_info_v1 object has delivered a 'done' event it - * is automatically destroyed. - * - * Every wp_image_description_info_v1 created from the same - * wp_image_description_v1 shall always return the exact same data. - * @section page_iface_wp_image_description_info_v1_api API - * See @ref iface_wp_image_description_info_v1. - */ -/** - * @defgroup iface_wp_image_description_info_v1 The wp_image_description_info_v1 interface - * - * Sends all matching events describing an image description object exactly - * once and finally sends the 'done' event. - * - * This means - * - if the image description is parametric, it must send - * - primaries - * - named_primaries, if applicable - * - at least one of tf_power and tf_named, as applicable - * - luminances - * - target_primaries - * - target_luminance - * - if the image description is parametric, it may send, if applicable, - * - target_max_cll - * - target_max_fall - * - if the image description contains an ICC profile, it must send the - * icc_file event - * - * Once a wp_image_description_info_v1 object has delivered a 'done' event it - * is automatically destroyed. - * - * Every wp_image_description_info_v1 created from the same - * wp_image_description_v1 shall always return the exact same data. - */ extern const struct wl_interface wp_image_description_info_v1_interface; #endif #ifndef WP_COLOR_MANAGER_V1_ERROR_ENUM #define WP_COLOR_MANAGER_V1_ERROR_ENUM enum wp_color_manager_v1_error { - /** - * request not supported - */ WP_COLOR_MANAGER_V1_ERROR_UNSUPPORTED_FEATURE = 0, - /** - * color management surface exists already - */ WP_COLOR_MANAGER_V1_ERROR_SURFACE_EXISTS = 1, }; -#endif /* WP_COLOR_MANAGER_V1_ERROR_ENUM */ +#endif #ifndef WP_COLOR_MANAGER_V1_ERROR_ENUM_IS_VALID #define WP_COLOR_MANAGER_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_manager_v1 - * Validate a wp_color_manager_v1 error value. - * - * @return true on success, false on error. - * @ref wp_color_manager_v1_error - */ static inline bool wp_color_manager_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -478,56 +164,21 @@ wp_color_manager_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_COLOR_MANAGER_V1_ERROR_ENUM_IS_VALID */ +#endif #ifndef WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM #define WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM -/** - * @ingroup iface_wp_color_manager_v1 - * rendering intents - * - * See the ICC.1:2022 specification from the International Color Consortium - * for more details about rendering intents. - * - * The principles of ICC defined rendering intents apply with all types of - * image descriptions, not only those with ICC file profiles. - * - * Compositors must support the perceptual rendering intent. Other - * rendering intents are optional. - */ enum wp_color_manager_v1_render_intent { - /** - * perceptual - */ WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL = 0, - /** - * media-relative colorimetric - */ WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE = 1, - /** - * saturation - */ WP_COLOR_MANAGER_V1_RENDER_INTENT_SATURATION = 2, - /** - * ICC-absolute colorimetric - */ WP_COLOR_MANAGER_V1_RENDER_INTENT_ABSOLUTE = 3, - /** - * media-relative colorimetric + black point compensation - */ WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE_BPC = 4, }; -#endif /* WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM */ +#endif #ifndef WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM_IS_VALID #define WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_manager_v1 - * Validate a wp_color_manager_v1 render_intent value. - * - * @return true on success, false on error. - * @ref wp_color_manager_v1_render_intent - */ static inline bool wp_color_manager_v1_render_intent_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -545,69 +196,24 @@ wp_color_manager_v1_render_intent_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM_IS_VALID */ +#endif #ifndef WP_COLOR_MANAGER_V1_FEATURE_ENUM #define WP_COLOR_MANAGER_V1_FEATURE_ENUM -/** - * @ingroup iface_wp_color_manager_v1 - * compositor supported features - */ enum wp_color_manager_v1_feature { - /** - * create_icc_creator request - */ WP_COLOR_MANAGER_V1_FEATURE_ICC_V2_V4 = 0, - /** - * create_parametric_creator request - */ WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC = 1, - /** - * parametric set_primaries request - */ WP_COLOR_MANAGER_V1_FEATURE_SET_PRIMARIES = 2, - /** - * parametric set_tf_power request - */ WP_COLOR_MANAGER_V1_FEATURE_SET_TF_POWER = 3, - /** - * parametric set_luminances request - */ WP_COLOR_MANAGER_V1_FEATURE_SET_LUMINANCES = 4, - /** - * parametric set_mastering_display_primaries request - * - * The compositor supports set_mastering_display_primaries - * request with a target color volume fully contained inside the - * primary color volume. - */ WP_COLOR_MANAGER_V1_FEATURE_SET_MASTERING_DISPLAY_PRIMARIES = 5, - /** - * parametric target exceeds primary color volume - * - * The compositor additionally supports target color volumes that - * extend outside of the primary color volume. - * - * This can only be advertised if feature - * set_mastering_display_primaries is supported as well. - */ WP_COLOR_MANAGER_V1_FEATURE_EXTENDED_TARGET_VOLUME = 6, - /** - * get_windows_scrgb request - */ WP_COLOR_MANAGER_V1_FEATURE_WINDOWS_SCRGB = 7, }; -#endif /* WP_COLOR_MANAGER_V1_FEATURE_ENUM */ +#endif #ifndef WP_COLOR_MANAGER_V1_FEATURE_ENUM_IS_VALID #define WP_COLOR_MANAGER_V1_FEATURE_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_manager_v1 - * Validate a wp_color_manager_v1 feature value. - * - * @return true on success, false on error. - * @ref wp_color_manager_v1_feature - */ static inline bool wp_color_manager_v1_feature_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -631,121 +237,26 @@ wp_color_manager_v1_feature_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_COLOR_MANAGER_V1_FEATURE_ENUM_IS_VALID */ +#endif #ifndef WP_COLOR_MANAGER_V1_PRIMARIES_ENUM #define WP_COLOR_MANAGER_V1_PRIMARIES_ENUM -/** - * @ingroup iface_wp_color_manager_v1 - * named color primaries - * - * Named color primaries used to encode well-known sets of primaries. H.273 - * is the authority, when it comes to the exact values of primaries and - * authoritative specifications, where an equivalent code point exists. - * - * A value of 0 is invalid and will never be present in the list of enums. - * - * Descriptions do list the specifications for convenience. - */ enum wp_color_manager_v1_primaries { - /** - * Color primaries for the sRGB color space as defined by the BT.709 standard - * - * Color primaries as defined by - Rec. ITU-R BT.709-6 - Rec. - * ITU-R BT.1361-0 conventional colour gamut system and extended - * colour gamut system (historical) - IEC 61966-2-1 sRGB or sYCC - - * IEC 61966-2-4 - Society of Motion Picture and Television - * Engineers (SMPTE) RP 177 (1993) Annex B Equivalent to H.273 - * ColourPrimaries code point 1. - */ WP_COLOR_MANAGER_V1_PRIMARIES_SRGB = 1, - /** - * Color primaries for PAL-M as defined by the BT.470 standard - * - * Color primaries as defined by - Rec. ITU-R BT.470-6 System M - * (historical) - United States National Television System - * Committee 1953 Recommendation for transmission standards for - * color television - United States Federal Communications - * Commission (2003) Title 47 Code of Federal Regulations 73.682 - * (a)(20) Equivalent to H.273 ColourPrimaries code point 4. - */ WP_COLOR_MANAGER_V1_PRIMARIES_PAL_M = 2, - /** - * Color primaries for PAL as defined by the BT.601 standard - * - * Color primaries as defined by - Rec. ITU-R BT.470-6 System B, - * G (historical) - Rec. ITU-R BT.601-7 625 - Rec. ITU-R BT.1358-0 - * 625 (historical) - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM - * Equivalent to H.273 ColourPrimaries code point 5. - */ WP_COLOR_MANAGER_V1_PRIMARIES_PAL = 3, - /** - * Color primaries for NTSC as defined by the BT.601 standard - * - * Color primaries as defined by - Rec. ITU-R BT.601-7 525 - Rec. - * ITU-R BT.1358-1 525 or 625 (historical) - Rec. ITU-R BT.1700-0 - * NTSC - SMPTE 170M (2004) - SMPTE 240M (1999) (historical) - * Equivalent to H.273 ColourPrimaries code point 6 and 7. - */ WP_COLOR_MANAGER_V1_PRIMARIES_NTSC = 4, - /** - * Generic film with colour filters using Illuminant C - * - * Color primaries as defined by H.273 for generic film. - * Equivalent to H.273 ColourPrimaries code point 8. - */ WP_COLOR_MANAGER_V1_PRIMARIES_GENERIC_FILM = 5, - /** - * Color primaries as defined by the BT.2020 and BT.2100 standard - * - * Color primaries as defined by - Rec. ITU-R BT.2020-2 - Rec. - * ITU-R BT.2100-0 Equivalent to H.273 ColourPrimaries code point - * 9. - */ WP_COLOR_MANAGER_V1_PRIMARIES_BT2020 = 6, - /** - * Color primaries of the full CIE 1931 XYZ color space - * - * Color primaries as defined as the maximum of the CIE 1931 XYZ - * color space by - SMPTE ST 428-1 - (CIE 1931 XYZ as in ISO - * 11664-1) Equivalent to H.273 ColourPrimaries code point 10. - */ WP_COLOR_MANAGER_V1_PRIMARIES_CIE1931_XYZ = 7, - /** - * Color primaries of the DCI P3 color space as defined by the SMPTE RP 431 standard - * - * Color primaries as defined by Digital Cinema System and - * published in SMPTE RP 431-2 (2011). Equivalent to H.273 - * ColourPrimaries code point 11. - */ WP_COLOR_MANAGER_V1_PRIMARIES_DCI_P3 = 8, - /** - * Color primaries of Display P3 variant of the DCI-P3 color space as defined by the SMPTE EG 432 standard - * - * Color primaries as defined by Digital Cinema System and - * published in SMPTE EG 432-1 (2010). Equivalent to H.273 - * ColourPrimaries code point 12. - */ WP_COLOR_MANAGER_V1_PRIMARIES_DISPLAY_P3 = 9, - /** - * Color primaries of the Adobe RGB color space as defined by the ISO 12640 standard - * - * Color primaries as defined by Adobe as "Adobe RGB" and later - * published by ISO 12640-4 (2011). - */ WP_COLOR_MANAGER_V1_PRIMARIES_ADOBE_RGB = 10, }; -#endif /* WP_COLOR_MANAGER_V1_PRIMARIES_ENUM */ +#endif #ifndef WP_COLOR_MANAGER_V1_PRIMARIES_ENUM_IS_VALID #define WP_COLOR_MANAGER_V1_PRIMARIES_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_manager_v1 - * Validate a wp_color_manager_v1 primaries value. - * - * @return true on success, false on error. - * @ref wp_color_manager_v1_primaries - */ static inline bool wp_color_manager_v1_primaries_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -773,173 +284,29 @@ wp_color_manager_v1_primaries_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_COLOR_MANAGER_V1_PRIMARIES_ENUM_IS_VALID */ +#endif #ifndef WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM #define WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM -/** - * @ingroup iface_wp_color_manager_v1 - * named transfer functions - * - * Named transfer functions used to represent well-known transfer - * characteristics. H.273 is the authority, when it comes to the exact - * formulas and authoritative specifications, where an equivalent code - * point exists. - * - * A value of 0 is invalid and will never be present in the list of enums. - * - * Descriptions do list the specifications for convenience. - */ enum wp_color_manager_v1_transfer_function { - /** - * BT.1886 display transfer characteristic - * - * Rec. ITU-R BT.1886 is the display transfer characteristic - * assumed by - Rec. ITU-R BT.601-7 525 and 625 - Rec. ITU-R - * BT.709-6 - Rec. ITU-R BT.2020-2 These recommendations are - * referred to by H.273 TransferCharacteristics code points 1, 6, - * 14, and 15, which are all equivalent. - * - * This TF implies these default luminances from Rec. ITU-R - * BT.2035: - primary color volume minimum: 0.01 cd/m² - primary - * color volume maximum: 100 cd/m² - reference white: 100 cd/m² - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_BT1886 = 1, - /** - * Assumed display gamma 2.2 transfer function - * - * Transfer characteristics as defined by - Rec. ITU-R BT.470-6 - * System M (historical) - United States National Television System - * Committee 1953 Recommendation for transmission standards for - * color television - United States Federal Communications - * Commission (2003) Title 47 Code of Federal Regulations 73.682 - * (a) (20) - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM Equivalent - * to H.273 TransferCharacteristics code point 4. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22 = 2, - /** - * Assumed display gamma 2.8 transfer function - * - * Transfer characteristics as defined by - Rec. ITU-R BT.470-6 - * System B, G (historical) Equivalent to H.273 - * TransferCharacteristics code point 5. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA28 = 3, - /** - * SMPTE ST 240 transfer function - * - * Transfer characteristics as defined by - SMPTE ST 240 (1999) - * Equivalent to H.273 TransferCharacteristics code point 7. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST240 = 4, - /** - * extended linear transfer function - * - * Linear transfer function defined over all real numbers. - * Normalised electrical values are equal the normalised optical - * values. - * - * The differences to H.273 TransferCharacteristics code point 8 - * are the definition over all real numbers. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR = 5, - /** - * logarithmic 100:1 transfer function - * - * Logarithmic transfer characteristic (100:1 range). Equivalent - * to H.273 TransferCharacteristics code point 9. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_100 = 6, - /** - * logarithmic (100*Sqrt(10) : 1) transfer function - * - * Logarithmic transfer characteristic (100 * Sqrt(10) : 1 - * range). Equivalent to H.273 TransferCharacteristics code point - * 10. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_316 = 7, - /** - * IEC 61966-2-4 transfer function - * - * Transfer characteristics as defined by - IEC 61966-2-4 - * Equivalent to H.273 TransferCharacteristics code point 11. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_XVYCC = 8, - /** - * sRGB piece-wise transfer function - * - * Transfer characteristics as defined by - IEC 61966-2-1 sRGB - * Equivalent to H.273 TransferCharacteristics code point 13 with - * MatrixCoefficients set to 0. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB = 9, - /** - * Extended sRGB piece-wise transfer function - * - * Transfer characteristics as defined by - IEC 61966-2-1 sYCC - * Equivalent to H.273 TransferCharacteristics code point 13 with - * MatrixCoefficients set to anything but 0. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB = 10, - /** - * perceptual quantizer transfer function - * - * Transfer characteristics as defined by - SMPTE ST 2084 (2014) - * for 10-, 12-, 14- and 16-bit systems - Rec. ITU-R BT.2100-2 - * perceptual quantization (PQ) system Equivalent to H.273 - * TransferCharacteristics code point 16. - * - * This TF implies these default luminances - primary color volume - * minimum: 0.005 cd/m² - primary color volume maximum: 10000 - * cd/m² - reference white: 203 cd/m² - * - * The difference between the primary color volume minimum and - * maximum must be approximately 10000 cd/m² as that is the swing - * of the EOTF defined by ST 2084 and BT.2100. The default value - * for the reference white is a protocol addition: it is suggested - * by Report ITU-R BT.2408-7 and is not part of ST 2084 or BT.2100. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ = 11, - /** - * SMPTE ST 428 transfer function - * - * Transfer characteristics as defined by - SMPTE ST 428-1 (2019) - * Equivalent to H.273 TransferCharacteristics code point 17. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST428 = 12, - /** - * hybrid log-gamma transfer function - * - * Transfer characteristics as defined by - ARIB STD-B67 (2015) - - * Rec. ITU-R BT.2100-2 hybrid log-gamma (HLG) system Equivalent to - * H.273 TransferCharacteristics code point 18. - * - * This TF implies these default luminances - primary color volume - * minimum: 0.005 cd/m² - primary color volume maximum: 1000 - * cd/m² - reference white: 203 cd/m² - * - * HLG is a relative display-referred signal with a specified - * non-linear mapping to the display peak luminance (the HLG OOTF). - * All absolute luminance values used here for HLG assume a 1000 - * cd/m² peak display. - * - * The default value for the reference white is a protocol - * addition: it is suggested by Report ITU-R BT.2408-7 and is not - * part of ARIB STD-B67 or BT.2100. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG = 13, }; -#endif /* WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM */ +#endif #ifndef WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM_IS_VALID #define WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_manager_v1 - * Validate a wp_color_manager_v1 transfer_function value. - * - * @return true on success, false on error. - * @ref wp_color_manager_v1_transfer_function - */ static inline bool wp_color_manager_v1_transfer_function_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -973,149 +340,29 @@ wp_color_manager_v1_transfer_function_is_valid(uint32_t value, uint32_t version) return false; } } -#endif /* WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_color_manager_v1 - * @struct wp_color_manager_v1_interface - */ struct wp_color_manager_v1_interface { - /** - * destroy the color manager - * - * Destroy the wp_color_manager_v1 object. This does not affect - * any other objects in any way. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a color management interface for a wl_output - * - * This creates a new wp_color_management_output_v1 object for - * the given wl_output. - * - * See the wp_color_management_output_v1 interface for more - * details. - */ void (*get_output)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *output); - /** - * create a color management interface for a wl_surface - * - * If a wp_color_management_surface_v1 object already exists for - * the given wl_surface, the protocol error surface_exists is - * raised. - * - * This creates a new color wp_color_management_surface_v1 object - * for the given wl_surface. - * - * See the wp_color_management_surface_v1 interface for more - * details. - */ void (*get_surface)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *surface); - /** - * create a color management feedback interface - * - * This creates a new color - * wp_color_management_surface_feedback_v1 object for the given - * wl_surface. - * - * See the wp_color_management_surface_feedback_v1 interface for - * more details. - */ void (*get_surface_feedback)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *surface); - /** - * make a new ICC-based image description creator object - * - * Makes a new ICC-based image description creator object with - * all properties initially unset. The client can then use the - * object's interface to define all the required properties for an - * image description and finally create a wp_image_description_v1 - * object. - * - * This request can be used when the compositor advertises - * wp_color_manager_v1.feature.icc_v2_v4. Otherwise this request - * raises the protocol error unsupported_feature. - * @param obj the new creator object - */ void (*create_icc_creator)(struct wl_client *client, struct wl_resource *resource, uint32_t obj); - /** - * make a new parametric image description creator object - * - * Makes a new parametric image description creator object with - * all properties initially unset. The client can then use the - * object's interface to define all the required properties for an - * image description and finally create a wp_image_description_v1 - * object. - * - * This request can be used when the compositor advertises - * wp_color_manager_v1.feature.parametric. Otherwise this request - * raises the protocol error unsupported_feature. - * @param obj the new creator object - */ void (*create_parametric_creator)(struct wl_client *client, struct wl_resource *resource, uint32_t obj); - /** - * create Windows-scRGB image description object - * - * This creates a pre-defined image description for the so-called - * Windows-scRGB stimulus encoding. This comes from the Windows 10 - * handling of its own definition of an scRGB color space for an - * HDR screen driven in BT.2100/PQ signalling mode. - * - * Windows-scRGB uses sRGB (BT.709) color primaries and white - * point. The transfer characteristic is extended linear. - * - * The nominal color channel value range is extended, meaning it - * includes negative and greater than 1.0 values. Negative values - * are used to escape the sRGB color gamut boundaries. To make use - * of the extended range, the client needs to use a pixel format - * that can represent those values, e.g. floating-point 16 bits per - * channel. - * - * Nominal color value R=G=B=0.0 corresponds to BT.2100/PQ system 0 - * cd/m², and R=G=B=1.0 corresponds to BT.2100/PQ system 80 - * cd/m². The maximum is R=G=B=125.0 corresponding to 10k cd/m². - * - * Windows-scRGB is displayed by Windows 10 by converting it to - * BT.2100/PQ, maintaining the CIE 1931 chromaticity and mapping - * the luminance as above. No adjustment is made to the signal to - * account for the viewing conditions. - * - * The reference white level of Windows-scRGB is unknown. If a - * reference white level must be assumed for compositor processing, - * it should be R=G=B=2.5375 corresponding to 203 cd/m² of Report - * ITU-R BT.2408-7. - * - * The target color volume of Windows-scRGB is unknown. The color - * gamut may be anything between sRGB and BT.2100. - * - * Note: EGL_EXT_gl_colorspace_scrgb_linear definition differs from - * Windows-scRGB by using R=G=B=1.0 as the reference white level, - * while Windows-scRGB reference white level is unknown or varies. - * However, it seems probable that Windows implements both - * EGL_EXT_gl_colorspace_scrgb_linear and Vulkan - * VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT as Windows-scRGB. - * - * This request can be used when the compositor advertises - * wp_color_manager_v1.feature.windows_scrgb. Otherwise this - * request raises the protocol error unsupported_feature. - * - * The resulting image description object does not allow - * get_information request. The wp_image_description_v1.ready event - * shall be sent. - */ void (*create_windows_scrgb)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); @@ -1127,165 +374,53 @@ struct wp_color_manager_v1_interface { #define WP_COLOR_MANAGER_V1_SUPPORTED_PRIMARIES_NAMED 3 #define WP_COLOR_MANAGER_V1_DONE 4 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_SUPPORTED_INTENT_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_SUPPORTED_FEATURE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_SUPPORTED_TF_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_SUPPORTED_PRIMARIES_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_GET_OUTPUT_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_GET_SURFACE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_GET_SURFACE_FEEDBACK_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_CREATE_ICC_CREATOR_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_CREATE_PARAMETRIC_CREATOR_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_CREATE_WINDOWS_SCRGB_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - * Sends an supported_intent event to the client owning the resource. - * @param resource_ The client's resource - * @param render_intent rendering intent - */ static inline void wp_color_manager_v1_send_supported_intent(struct wl_resource *resource_, uint32_t render_intent) { wl_resource_post_event(resource_, WP_COLOR_MANAGER_V1_SUPPORTED_INTENT, render_intent); } -/** - * @ingroup iface_wp_color_manager_v1 - * Sends an supported_feature event to the client owning the resource. - * @param resource_ The client's resource - * @param feature supported feature - */ static inline void wp_color_manager_v1_send_supported_feature(struct wl_resource *resource_, uint32_t feature) { wl_resource_post_event(resource_, WP_COLOR_MANAGER_V1_SUPPORTED_FEATURE, feature); } -/** - * @ingroup iface_wp_color_manager_v1 - * Sends an supported_tf_named event to the client owning the resource. - * @param resource_ The client's resource - * @param tf Named transfer function - */ static inline void wp_color_manager_v1_send_supported_tf_named(struct wl_resource *resource_, uint32_t tf) { wl_resource_post_event(resource_, WP_COLOR_MANAGER_V1_SUPPORTED_TF_NAMED, tf); } -/** - * @ingroup iface_wp_color_manager_v1 - * Sends an supported_primaries_named event to the client owning the resource. - * @param resource_ The client's resource - * @param primaries Named color primaries - */ static inline void wp_color_manager_v1_send_supported_primaries_named(struct wl_resource *resource_, uint32_t primaries) { wl_resource_post_event(resource_, WP_COLOR_MANAGER_V1_SUPPORTED_PRIMARIES_NAMED, primaries); } -/** - * @ingroup iface_wp_color_manager_v1 - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wp_color_manager_v1_send_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, WP_COLOR_MANAGER_V1_DONE); } -/** - * @ingroup iface_wp_color_management_output_v1 - * @struct wp_color_management_output_v1_interface - */ struct wp_color_management_output_v1_interface { - /** - * destroy the color management output - * - * Destroy the color wp_color_management_output_v1 object. This - * does not affect any remaining protocol objects. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * get the image description of the output - * - * This creates a new wp_image_description_v1 object for the - * current image description of the output. There always is exactly - * one image description active for an output so the client should - * destroy the image description created by earlier invocations of - * this request. This request is usually sent as a reaction to the - * image_description_changed event or when creating a - * wp_color_management_output_v1 object. - * - * The image description of an output represents the color encoding - * the output expects. There might be performance and power - * advantages, as well as improved color reproduction, if a content - * update matches the image description of the output it is being - * shown on. If a content update is shown on any other output than - * the one it matches the image description of, then the color - * reproduction on those outputs might be considerably worse. - * - * The created wp_image_description_v1 object preserves the image - * description of the output from the time the object was created. - * - * The resulting image description object allows get_information - * request. - * - * If this protocol object is inert, the resulting image - * description object shall immediately deliver the - * wp_image_description_v1.failed event with the no_output cause. - * - * If the interface version is inadequate for the output's image - * description, meaning that the client does not support all the - * events needed to deliver the crucial information, the resulting - * image description object shall immediately deliver the - * wp_image_description_v1.failed event with the low_version cause. - * - * Otherwise the object shall immediately deliver the ready event. - */ void (*get_image_description)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); @@ -1293,25 +428,11 @@ struct wp_color_management_output_v1_interface { #define WP_COLOR_MANAGEMENT_OUTPUT_V1_IMAGE_DESCRIPTION_CHANGED 0 -/** - * @ingroup iface_wp_color_management_output_v1 - */ #define WP_COLOR_MANAGEMENT_OUTPUT_V1_IMAGE_DESCRIPTION_CHANGED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_output_v1 - */ #define WP_COLOR_MANAGEMENT_OUTPUT_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_output_v1 - */ #define WP_COLOR_MANAGEMENT_OUTPUT_V1_GET_IMAGE_DESCRIPTION_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_output_v1 - * Sends an image_description_changed event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wp_color_management_output_v1_send_image_description_changed(struct wl_resource *resource_) { @@ -1320,35 +441,15 @@ wp_color_management_output_v1_send_image_description_changed(struct wl_resource #ifndef WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM #define WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM -/** - * @ingroup iface_wp_color_management_surface_v1 - * protocol errors - */ enum wp_color_management_surface_v1_error { - /** - * unsupported rendering intent - */ WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_RENDER_INTENT = 0, - /** - * invalid image description - */ WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_IMAGE_DESCRIPTION = 1, - /** - * forbidden request on inert object - */ WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_INERT = 2, }; -#endif /* WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM */ +#endif #ifndef WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM_IS_VALID #define WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_management_surface_v1 - * Validate a wp_color_management_surface_v1 error value. - * - * @return true on success, false on error. - * @ref wp_color_management_surface_v1_error - */ static inline bool wp_color_management_surface_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1362,128 +463,34 @@ wp_color_management_surface_v1_error_is_valid(uint32_t value, uint32_t version) return false; } } -#endif /* WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_color_management_surface_v1 - * @struct wp_color_management_surface_v1_interface - */ struct wp_color_management_surface_v1_interface { - /** - * destroy the color management interface for a surface - * - * Destroy the wp_color_management_surface_v1 object and do the - * same as unset_image_description. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the surface image description - * - * If this protocol object is inert, the protocol error inert is - * raised. - * - * Set the image description of the underlying surface. The image - * description and rendering intent are double-buffered state, see - * wl_surface.commit. - * - * It is the client's responsibility to understand the image - * description it sets on a surface, and to provide content that - * matches that image description. Compositors might convert images - * to match their own or any other image descriptions. - * - * Image descriptions which are not ready (see - * wp_image_description_v1) are forbidden in this request, and in - * such case the protocol error image_description is raised. - * - * All image descriptions which are ready (see - * wp_image_description_v1) are allowed and must always be accepted - * by the compositor. - * - * A rendering intent provides the client's preference on how - * content colors should be mapped to each output. The - * render_intent value must be one advertised by the compositor - * with wp_color_manager_v1.render_intent event, otherwise the - * protocol error render_intent is raised. - * - * When an image description is set on a surface, the Transfer - * Characteristics of the image description defines the valid range - * of the nominal (real-valued) color channel values. The - * processing of out-of-range color channel values is undefined, - * but compositors are recommended to clamp the values to the valid - * range when possible. - * - * By default, a surface does not have an associated image - * description nor a rendering intent. The handling of color on - * such surfaces is compositor implementation defined. Compositors - * should handle such surfaces as sRGB, but may handle them - * differently if they have specific requirements. - * - * Setting the image description has copy semantics; after this - * request, the image description can be immediately destroyed - * without affecting the pending state of the surface. - * @param render_intent rendering intent - */ void (*set_image_description)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *image_description, uint32_t render_intent); - /** - * remove the surface image description - * - * If this protocol object is inert, the protocol error inert is - * raised. - * - * This request removes any image description from the surface. See - * set_image_description for how a compositor handles a surface - * without an image description. This is double-buffered state, see - * wl_surface.commit. - */ void (*unset_image_description)(struct wl_client *client, struct wl_resource *resource); }; -/** - * @ingroup iface_wp_color_management_surface_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_V1_SET_IMAGE_DESCRIPTION_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_V1_UNSET_IMAGE_DESCRIPTION_SINCE_VERSION 1 #ifndef WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - * protocol errors - */ enum wp_color_management_surface_feedback_v1_error { - /** - * forbidden request on inert object - */ WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_INERT = 0, - /** - * attempted to use an unsupported feature - */ WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_UNSUPPORTED_FEATURE = 1, }; -#endif /* WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM */ +#endif #ifndef WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM_IS_VALID #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - * Validate a wp_color_management_surface_feedback_v1 error value. - * - * @return true on success, false on error. - * @ref wp_color_management_surface_feedback_v1_error - */ static inline bool wp_color_management_surface_feedback_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1495,76 +502,14 @@ wp_color_management_surface_feedback_v1_error_is_valid(uint32_t value, uint32_t return false; } } -#endif /* WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - * @struct wp_color_management_surface_feedback_v1_interface - */ struct wp_color_management_surface_feedback_v1_interface { - /** - * destroy the color management interface for a surface - * - * Destroy the wp_color_management_surface_feedback_v1 object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * get the preferred image description - * - * If this protocol object is inert, the protocol error inert is - * raised. - * - * The preferred image description represents the compositor's - * preferred color encoding for this wl_surface at the current - * time. There might be performance and power advantages, as well - * as improved color reproduction, if the image description of a - * content update matches the preferred image description. - * - * This creates a new wp_image_description_v1 object for the - * currently preferred image description for the wl_surface. The - * client should stop using and destroy the image descriptions - * created by earlier invocations of this request for the - * associated wl_surface. This request is usually sent as a - * reaction to the preferred_changed event or when creating a - * wp_color_management_surface_feedback_v1 object if the client is - * capable of adapting to image descriptions. - * - * The created wp_image_description_v1 object preserves the - * preferred image description of the wl_surface from the time the - * object was created. - * - * The resulting image description object allows get_information - * request. - * - * If the image description is parametric, the client should set it - * on its wl_surface only if the image description is an exact - * match with the client content. Particularly if everything else - * matches, but the target color volume is greater than what the - * client needs, the client should create its own parameric image - * description with its exact parameters. - * - * If the interface version is inadequate for the preferred image - * description, meaning that the client does not support all the - * events needed to deliver the crucial information, the resulting - * image description object shall immediately deliver the - * wp_image_description_v1.failed event with the low_version cause, - * otherwise the object shall immediately deliver the ready event. - */ void (*get_preferred)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); - /** - * get the preferred image description - * - * The same description as for get_preferred applies, except the - * returned image description is guaranteed to be parametric. This - * is meant for clients that can only deal with parametric image - * descriptions. - * - * If the compositor doesn't support parametric image descriptions, - * the unsupported_feature error is emitted. - */ void (*get_preferred_parametric)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); @@ -1572,30 +517,12 @@ struct wp_color_management_surface_feedback_v1_interface { #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_PREFERRED_CHANGED 0 -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_PREFERRED_CHANGED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_GET_PREFERRED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_GET_PREFERRED_PARAMETRIC_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - * Sends an preferred_changed event to the client owning the resource. - * @param resource_ The client's resource - * @param identity image description id number - */ static inline void wp_color_management_surface_feedback_v1_send_preferred_changed(struct wl_resource *resource_, uint32_t identity) { @@ -1604,43 +531,17 @@ wp_color_management_surface_feedback_v1_send_preferred_changed(struct wl_resourc #ifndef WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM #define WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM -/** - * @ingroup iface_wp_image_description_creator_icc_v1 - * protocol errors - */ enum wp_image_description_creator_icc_v1_error { - /** - * incomplete parameter set - */ WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_INCOMPLETE_SET = 0, - /** - * property already set - */ WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ALREADY_SET = 1, - /** - * fd not seekable and readable - */ WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_BAD_FD = 2, - /** - * no or too much data - */ WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_BAD_SIZE = 3, - /** - * offset + length exceeds file size - */ WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_OUT_OF_FILE = 4, }; -#endif /* WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM */ +#endif #ifndef WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM_IS_VALID #define WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_image_description_creator_icc_v1 - * Validate a wp_image_description_creator_icc_v1 error value. - * - * @return true on success, false on error. - * @ref wp_image_description_creator_icc_v1_error - */ static inline bool wp_image_description_creator_icc_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1658,91 +559,12 @@ wp_image_description_creator_icc_v1_error_is_valid(uint32_t value, uint32_t vers return false; } } -#endif /* WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_image_description_creator_icc_v1 - * @struct wp_image_description_creator_icc_v1_interface - */ struct wp_image_description_creator_icc_v1_interface { - /** - * Create the image description object from ICC data - * - * Create an image description object based on the ICC - * information previously set on this object. A compositor must - * parse the ICC data in some undefined but finite amount of time. - * - * The completeness of the parameter set is verified. If the set is - * not complete, the protocol error incomplete_set is raised. For - * the definition of a complete set, see the description of this - * interface. - * - * If the particular combination of the information is not - * supported by the compositor, the resulting image description - * object shall immediately deliver the - * wp_image_description_v1.failed event with the 'unsupported' - * cause. If a valid image description was created from the - * information, the wp_image_description_v1.ready event will - * eventually be sent instead. - * - * This request destroys the wp_image_description_creator_icc_v1 - * object. - * - * The resulting image description object does not allow - * get_information request. - */ void (*create)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); - /** - * set the ICC profile file - * - * Sets the ICC profile file to be used as the basis of the image - * description. - * - * The data shall be found through the given fd at the given - * offset, having the given length. The fd must be seekable and - * readable. Violating these requirements raises the bad_fd - * protocol error. - * - * If reading the data fails due to an error independent of the - * client, the compositor shall send the - * wp_image_description_v1.failed event on the created - * wp_image_description_v1 with the 'operating_system' cause. - * - * The maximum size of the ICC profile is 32 MB. If length is - * greater than that or zero, the protocol error bad_size is - * raised. If offset + length exceeds the file size, the protocol - * error out_of_file is raised. - * - * A compositor may read the file at any time starting from this - * request and only until whichever happens first: - If create - * request was issued, the wp_image_description_v1 object delivers - * either failed or ready event; or - if create request was not - * issued, this wp_image_description_creator_icc_v1 object is - * destroyed. - * - * A compositor shall not modify the contents of the file, and the - * fd may be sealed for writes and size changes. The client must - * ensure to its best ability that the data does not change while - * the compositor is reading it. - * - * The data must represent a valid ICC profile. The ICC profile - * version must be 2 or 4, it must be a 3 channel profile and the - * class must be Display or ColorSpace. Violating these - * requirements will not result in a protocol error, but will - * eventually send the wp_image_description_v1.failed event on the - * created wp_image_description_v1 with the 'unsupported' cause. - * - * See the International Color Consortium specification ICC.1:2022 - * for more details about ICC profiles. - * - * If ICC file has already been set on this object, the protocol - * error already_set is raised. - * @param icc_profile ICC profile - * @param offset byte offset in fd to start of ICC data - * @param length length of ICC data in bytes - */ void (*set_icc_file)(struct wl_client *client, struct wl_resource *resource, int32_t icc_profile, @@ -1751,58 +573,23 @@ struct wp_image_description_creator_icc_v1_interface { }; -/** - * @ingroup iface_wp_image_description_creator_icc_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_CREATE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_icc_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_SET_ICC_FILE_SINCE_VERSION 1 #ifndef WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM -/** - * @ingroup iface_wp_image_description_creator_params_v1 - * protocol errors - */ enum wp_image_description_creator_params_v1_error { - /** - * incomplete parameter set - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_INCOMPLETE_SET = 0, - /** - * property already set - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ALREADY_SET = 1, - /** - * request not supported - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_UNSUPPORTED_FEATURE = 2, - /** - * invalid transfer characteristic - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_INVALID_TF = 3, - /** - * invalid primaries named - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_INVALID_PRIMARIES_NAMED = 4, - /** - * invalid luminance value or range - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_INVALID_LUMINANCE = 5, }; -#endif /* WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM */ +#endif #ifndef WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM_IS_VALID #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_image_description_creator_params_v1 - * Validate a wp_image_description_creator_params_v1 error value. - * - * @return true on success, false on error. - * @ref wp_image_description_creator_params_v1_error - */ static inline bool wp_image_description_creator_params_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1822,144 +609,21 @@ wp_image_description_creator_params_v1_error_is_valid(uint32_t value, uint32_t v return false; } } -#endif /* WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_image_description_creator_params_v1 - * @struct wp_image_description_creator_params_v1_interface - */ struct wp_image_description_creator_params_v1_interface { - /** - * Create the image description object using params - * - * Create an image description object based on the parameters - * previously set on this object. - * - * The completeness of the parameter set is verified. If the set is - * not complete, the protocol error incomplete_set is raised. For - * the definition of a complete set, see the description of this - * interface. - * - * The protocol error invalid_luminance is raised if any of the - * following requirements is not met: - When max_cll is set, it - * must be greater than min L and less or equal to max L of the - * mastering luminance range. - When max_fall is set, it must be - * greater than min L and less or equal to max L of the mastering - * luminance range. - When both max_cll and max_fall are set, - * max_fall must be less or equal to max_cll. - * - * If the particular combination of the parameter set is not - * supported by the compositor, the resulting image description - * object shall immediately deliver the - * wp_image_description_v1.failed event with the 'unsupported' - * cause. If a valid image description was created from the - * parameter set, the wp_image_description_v1.ready event will - * eventually be sent instead. - * - * This request destroys the wp_image_description_creator_params_v1 - * object. - * - * The resulting image description object does not allow - * get_information request. - */ void (*create)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); - /** - * named transfer characteristic - * - * Sets the transfer characteristic using explicitly enumerated - * named functions. - * - * When the resulting image description is attached to an image, - * the content should be encoded and decoded according to the - * industry standard practices for the transfer characteristic. - * - * Only names advertised with wp_color_manager_v1 event - * supported_tf_named are allowed. Other values shall raise the - * protocol error invalid_tf. - * - * If transfer characteristic has already been set on this object, - * the protocol error already_set is raised. - * @param tf named transfer function - */ void (*set_tf_named)(struct wl_client *client, struct wl_resource *resource, uint32_t tf); - /** - * transfer characteristic as a power curve - * - * Sets the color component transfer characteristic to a power - * curve with the given exponent. Negative values are handled by - * mirroring the positive half of the curve through the origin. The - * valid domain and range of the curve are all finite real numbers. - * This curve represents the conversion from electrical to optical - * color channel values. - * - * When the resulting image description is attached to an image, - * the content should be encoded with the inverse of the power - * curve. - * - * The curve exponent shall be multiplied by 10000 to get the - * argument eexp value to carry the precision of 4 decimals. - * - * The curve exponent must be at least 1.0 and at most 10.0. - * Otherwise the protocol error invalid_tf is raised. - * - * If transfer characteristic has already been set on this object, - * the protocol error already_set is raised. - * - * This request can be used when the compositor advertises - * wp_color_manager_v1.feature.set_tf_power. Otherwise this request - * raises the protocol error unsupported_feature. - * @param eexp the exponent * 10000 - */ void (*set_tf_power)(struct wl_client *client, struct wl_resource *resource, uint32_t eexp); - /** - * named primaries - * - * Sets the color primaries and white point using explicitly - * named sets. This describes the primary color volume which is the - * basis for color value encoding. - * - * Only names advertised with wp_color_manager_v1 event - * supported_primaries_named are allowed. Other values shall raise - * the protocol error invalid_primaries_named. - * - * If primaries have already been set on this object, the protocol - * error already_set is raised. - * @param primaries named primaries - */ void (*set_primaries_named)(struct wl_client *client, struct wl_resource *resource, uint32_t primaries); - /** - * primaries as chromaticity coordinates - * - * Sets the color primaries and white point using CIE 1931 xy - * chromaticity coordinates. This describes the primary color - * volume which is the basis for color value encoding. - * - * Each coordinate value is multiplied by 1 million to get the - * argument value to carry precision of 6 decimals. - * - * If primaries have already been set on this object, the protocol - * error already_set is raised. - * - * This request can be used if the compositor advertises - * wp_color_manager_v1.feature.set_primaries. Otherwise this - * request raises the protocol error unsupported_feature. - * @param r_x Red x * 1M - * @param r_y Red y * 1M - * @param g_x Green x * 1M - * @param g_y Green y * 1M - * @param b_x Blue x * 1M - * @param b_y Blue y * 1M - * @param w_x White x * 1M - * @param w_y White y * 1M - */ void (*set_primaries)(struct wl_client *client, struct wl_resource *resource, int32_t r_x, @@ -1970,132 +634,11 @@ struct wp_image_description_creator_params_v1_interface { int32_t b_y, int32_t w_x, int32_t w_y); - /** - * primary color volume luminance range and reference white - * - * Sets the primary color volume luminance range and the - * reference white luminance level. These values include the - * minimum display emission and ambient flare luminances, assumed - * to be optically additive and have the chromaticity of the - * primary color volume white point. - * - * The default luminances from - * https://www.color.org/chardata/rgb/srgb.xalter are - primary - * color volume minimum: 0.2 cd/m² - primary color volume maximum: - * 80 cd/m² - reference white: 80 cd/m² - * - * Setting a named transfer characteristic can imply other default - * luminances. - * - * The default luminances get overwritten when this request is - * used. With transfer_function.st2084_pq the given 'max_lum' value - * is ignored, and 'max_lum' is taken as 'min_lum' + 10000 cd/m². - * - * 'min_lum' and 'max_lum' specify the minimum and maximum - * luminances of the primary color volume as reproduced by the - * targeted display. - * - * 'reference_lum' specifies the luminance of the reference white - * as reproduced by the targeted display, and reflects the targeted - * viewing environment. - * - * Compositors should make sure that all content is anchored, - * meaning that an input signal level of 'reference_lum' on one - * image description and another input signal level of - * 'reference_lum' on another image description should produce the - * same output level, even though the 'reference_lum' on both image - * representations can be different. - * - * 'reference_lum' may be higher than 'max_lum'. In that case - * reaching the reference white output level in image content - * requires the 'extended_target_volume' feature support. - * - * If 'max_lum' or 'reference_lum' are less than or equal to - * 'min_lum', the protocol error invalid_luminance is raised. - * - * The minimum luminance is multiplied by 10000 to get the argument - * 'min_lum' value and carries precision of 4 decimals. The maximum - * luminance and reference white luminance values are unscaled. - * - * If the primary color volume luminance range and the reference - * white luminance level have already been set on this object, the - * protocol error already_set is raised. - * - * This request can be used if the compositor advertises - * wp_color_manager_v1.feature.set_luminances. Otherwise this - * request raises the protocol error unsupported_feature. - * @param min_lum minimum luminance (cd/m²) * 10000 - * @param max_lum maximum luminance (cd/m²) - * @param reference_lum reference white luminance (cd/m²) - */ void (*set_luminances)(struct wl_client *client, struct wl_resource *resource, uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum); - /** - * mastering display primaries as chromaticity coordinates - * - * Provides the color primaries and white point of the mastering - * display using CIE 1931 xy chromaticity coordinates. This is - * compatible with the SMPTE ST 2086 definition of HDR static - * metadata. - * - * The mastering display primaries and mastering display luminances - * define the target color volume. - * - * If mastering display primaries are not explicitly set, the - * target color volume is assumed to have the same primaries as the - * primary color volume. - * - * The target color volume is defined by all tristimulus values - * between 0.0 and 1.0 (inclusive) of the color space defined by - * the given mastering display primaries and white point. The - * colorimetry is identical between the container color space and - * the mastering display color space, including that no chromatic - * adaptation is applied even if the white points differ. - * - * The target color volume can exceed the primary color volume to - * allow for a greater color volume with an existing color space - * definition (for example scRGB). It can be smaller than the - * primary color volume to minimize gamut and tone mapping - * distances for big color spaces (HDR metadata). - * - * To make use of the entire target color volume a suitable pixel - * format has to be chosen (e.g. floating point to exceed the - * primary color volume, or abusing limited quantization range as - * with xvYCC). - * - * Each coordinate value is multiplied by 1 million to get the - * argument value to carry precision of 6 decimals. - * - * If mastering display primaries have already been set on this - * object, the protocol error already_set is raised. - * - * This request can be used if the compositor advertises - * wp_color_manager_v1.feature.set_mastering_display_primaries. - * Otherwise this request raises the protocol error - * unsupported_feature. The advertisement implies support only for - * target color volumes fully contained within the primary color - * volume. - * - * If a compositor additionally supports target color volume - * exceeding the primary color volume, it must advertise - * wp_color_manager_v1.feature.extended_target_volume. If a client - * uses target color volume exceeding the primary color volume and - * the compositor does not support it, the result is implementation - * defined. Compositors are recommended to detect this case and - * fail the image description gracefully, but it may as well result - * in color artifacts. - * @param r_x Red x * 1M - * @param r_y Red y * 1M - * @param g_x Green x * 1M - * @param g_y Green y * 1M - * @param b_x Blue x * 1M - * @param b_y Blue y * 1M - * @param w_x White x * 1M - * @param w_y White y * 1M - */ void (*set_mastering_display_primaries)(struct wl_client *client, struct wl_resource *resource, int32_t r_x, @@ -2106,148 +649,40 @@ struct wp_image_description_creator_params_v1_interface { int32_t b_y, int32_t w_x, int32_t w_y); - /** - * display mastering luminance range - * - * Sets the luminance range that was used during the content - * mastering process as the minimum and maximum absolute luminance - * L. These values include the minimum display emission and ambient - * flare luminances, assumed to be optically additive and have the - * chromaticity of the primary color volume white point. This - * should be compatible with the SMPTE ST 2086 definition of HDR - * static metadata. - * - * The mastering display primaries and mastering display luminances - * define the target color volume. - * - * If mastering luminances are not explicitly set, the target color - * volume is assumed to have the same min and max luminances as the - * primary color volume. - * - * If max L is less than or equal to min L, the protocol error - * invalid_luminance is raised. - * - * Min L value is multiplied by 10000 to get the argument min_lum - * value and carry precision of 4 decimals. Max L value is unscaled - * for max_lum. - * - * This request can be used if the compositor advertises - * wp_color_manager_v1.feature.set_mastering_display_primaries. - * Otherwise this request raises the protocol error - * unsupported_feature. The advertisement implies support only for - * target color volumes fully contained within the primary color - * volume. - * - * If a compositor additionally supports target color volume - * exceeding the primary color volume, it must advertise - * wp_color_manager_v1.feature.extended_target_volume. If a client - * uses target color volume exceeding the primary color volume and - * the compositor does not support it, the result is implementation - * defined. Compositors are recommended to detect this case and - * fail the image description gracefully, but it may as well result - * in color artifacts. - * @param min_lum min L (cd/m²) * 10000 - * @param max_lum max L (cd/m²) - */ void (*set_mastering_luminance)(struct wl_client *client, struct wl_resource *resource, uint32_t min_lum, uint32_t max_lum); - /** - * maximum content light level - * - * Sets the maximum content light level (max_cll) as defined by - * CTA-861-H. - * - * max_cll is undefined by default. - * @param max_cll Maximum content light level (cd/m²) - */ void (*set_max_cll)(struct wl_client *client, struct wl_resource *resource, uint32_t max_cll); - /** - * maximum frame-average light level - * - * Sets the maximum frame-average light level (max_fall) as - * defined by CTA-861-H. - * - * max_fall is undefined by default. - * @param max_fall Maximum frame-average light level (cd/m²) - */ void (*set_max_fall)(struct wl_client *client, struct wl_resource *resource, uint32_t max_fall); }; -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_CREATE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_TF_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_TF_POWER_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_PRIMARIES_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_PRIMARIES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_LUMINANCES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_MASTERING_DISPLAY_PRIMARIES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_MASTERING_LUMINANCE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_MAX_CLL_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_MAX_FALL_SINCE_VERSION 1 #ifndef WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM #define WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM -/** - * @ingroup iface_wp_image_description_v1 - * protocol errors - */ enum wp_image_description_v1_error { - /** - * attempted to use an object which is not ready - */ WP_IMAGE_DESCRIPTION_V1_ERROR_NOT_READY = 0, - /** - * get_information not allowed - */ WP_IMAGE_DESCRIPTION_V1_ERROR_NO_INFORMATION = 1, }; -#endif /* WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM */ +#endif #ifndef WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM_IS_VALID #define WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_image_description_v1 - * Validate a wp_image_description_v1 error value. - * - * @return true on success, false on error. - * @ref wp_image_description_v1_error - */ static inline bool wp_image_description_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2259,43 +694,20 @@ wp_image_description_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM_IS_VALID */ +#endif #ifndef WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM #define WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM -/** - * @ingroup iface_wp_image_description_v1 - * generic reason for failure - */ enum wp_image_description_v1_cause { - /** - * interface version too low - */ WP_IMAGE_DESCRIPTION_V1_CAUSE_LOW_VERSION = 0, - /** - * unsupported image description data - */ WP_IMAGE_DESCRIPTION_V1_CAUSE_UNSUPPORTED = 1, - /** - * error independent of the client - */ WP_IMAGE_DESCRIPTION_V1_CAUSE_OPERATING_SYSTEM = 2, - /** - * the relevant output no longer exists - */ WP_IMAGE_DESCRIPTION_V1_CAUSE_NO_OUTPUT = 3, }; -#endif /* WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM */ +#endif #ifndef WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM_IS_VALID #define WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM_IS_VALID -/** - * @ingroup iface_wp_image_description_v1 - * Validate a wp_image_description_v1 cause value. - * - * @return true on success, false on error. - * @ref wp_image_description_v1_cause - */ static inline bool wp_image_description_v1_cause_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2311,37 +723,11 @@ wp_image_description_v1_cause_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_image_description_v1 - * @struct wp_image_description_v1_interface - */ struct wp_image_description_v1_interface { - /** - * destroy the image description - * - * Destroy this object. It is safe to destroy an object which is - * not ready. - * - * Destroying a wp_image_description_v1 object has no side-effects, - * not even if a - * wp_color_management_surface_v1.set_image_description has not yet - * been followed by a wl_surface.commit. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * get information about the image description - * - * Creates a wp_image_description_info_v1 object which delivers - * the information that makes up the image description. - * - * Not all image description protocol objects allow get_information - * request. Whether it is allowed or not is defined by the request - * that created the object. If get_information is not allowed, the - * protocol error no_information is raised. - */ void (*get_information)(struct wl_client *client, struct wl_resource *resource, uint32_t information); @@ -2350,43 +736,18 @@ struct wp_image_description_v1_interface { #define WP_IMAGE_DESCRIPTION_V1_FAILED 0 #define WP_IMAGE_DESCRIPTION_V1_READY 1 -/** - * @ingroup iface_wp_image_description_v1 - */ #define WP_IMAGE_DESCRIPTION_V1_FAILED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_v1 - */ #define WP_IMAGE_DESCRIPTION_V1_READY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_v1 - */ #define WP_IMAGE_DESCRIPTION_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_v1 - */ #define WP_IMAGE_DESCRIPTION_V1_GET_INFORMATION_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_v1 - * Sends an failed event to the client owning the resource. - * @param resource_ The client's resource - * @param cause generic reason - * @param msg ad hoc human-readable explanation - */ static inline void wp_image_description_v1_send_failed(struct wl_resource *resource_, uint32_t cause, const char *msg) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_V1_FAILED, cause, msg); } -/** - * @ingroup iface_wp_image_description_v1 - * Sends an ready event to the client owning the resource. - * @param resource_ The client's resource - * @param identity image description id number - */ static inline void wp_image_description_v1_send_ready(struct wl_resource *resource_, uint32_t identity) { @@ -2405,195 +766,79 @@ wp_image_description_v1_send_ready(struct wl_resource *resource_, uint32_t ident #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_MAX_CLL 9 #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_MAX_FALL 10 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_ICC_FILE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_PRIMARIES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_PRIMARIES_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TF_POWER_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TF_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_LUMINANCES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_PRIMARIES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_LUMINANCE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_MAX_CLL_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_MAX_FALL_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wp_image_description_info_v1_send_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_DONE); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an icc_file event to the client owning the resource. - * @param resource_ The client's resource - * @param icc ICC profile file descriptor - * @param icc_size ICC profile size, in bytes - */ static inline void wp_image_description_info_v1_send_icc_file(struct wl_resource *resource_, int32_t icc, uint32_t icc_size) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_ICC_FILE, icc, icc_size); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an primaries event to the client owning the resource. - * @param resource_ The client's resource - * @param r_x Red x * 1M - * @param r_y Red y * 1M - * @param g_x Green x * 1M - * @param g_y Green y * 1M - * @param b_x Blue x * 1M - * @param b_y Blue y * 1M - * @param w_x White x * 1M - * @param w_y White y * 1M - */ static inline void wp_image_description_info_v1_send_primaries(struct wl_resource *resource_, int32_t r_x, int32_t r_y, int32_t g_x, int32_t g_y, int32_t b_x, int32_t b_y, int32_t w_x, int32_t w_y) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_PRIMARIES, r_x, r_y, g_x, g_y, b_x, b_y, w_x, w_y); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an primaries_named event to the client owning the resource. - * @param resource_ The client's resource - * @param primaries named primaries - */ static inline void wp_image_description_info_v1_send_primaries_named(struct wl_resource *resource_, uint32_t primaries) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_PRIMARIES_NAMED, primaries); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an tf_power event to the client owning the resource. - * @param resource_ The client's resource - * @param eexp the exponent * 10000 - */ static inline void wp_image_description_info_v1_send_tf_power(struct wl_resource *resource_, uint32_t eexp) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_TF_POWER, eexp); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an tf_named event to the client owning the resource. - * @param resource_ The client's resource - * @param tf named transfer function - */ static inline void wp_image_description_info_v1_send_tf_named(struct wl_resource *resource_, uint32_t tf) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_TF_NAMED, tf); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an luminances event to the client owning the resource. - * @param resource_ The client's resource - * @param min_lum minimum luminance (cd/m²) * 10000 - * @param max_lum maximum luminance (cd/m²) - * @param reference_lum reference white luminance (cd/m²) - */ static inline void wp_image_description_info_v1_send_luminances(struct wl_resource *resource_, uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_LUMINANCES, min_lum, max_lum, reference_lum); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an target_primaries event to the client owning the resource. - * @param resource_ The client's resource - * @param r_x Red x * 1M - * @param r_y Red y * 1M - * @param g_x Green x * 1M - * @param g_y Green y * 1M - * @param b_x Blue x * 1M - * @param b_y Blue y * 1M - * @param w_x White x * 1M - * @param w_y White y * 1M - */ static inline void wp_image_description_info_v1_send_target_primaries(struct wl_resource *resource_, int32_t r_x, int32_t r_y, int32_t g_x, int32_t g_y, int32_t b_x, int32_t b_y, int32_t w_x, int32_t w_y) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_PRIMARIES, r_x, r_y, g_x, g_y, b_x, b_y, w_x, w_y); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an target_luminance event to the client owning the resource. - * @param resource_ The client's resource - * @param min_lum min L (cd/m²) * 10000 - * @param max_lum max L (cd/m²) - */ static inline void wp_image_description_info_v1_send_target_luminance(struct wl_resource *resource_, uint32_t min_lum, uint32_t max_lum) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_LUMINANCE, min_lum, max_lum); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an target_max_cll event to the client owning the resource. - * @param resource_ The client's resource - * @param max_cll Maximum content light-level (cd/m²) - */ static inline void wp_image_description_info_v1_send_target_max_cll(struct wl_resource *resource_, uint32_t max_cll) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_MAX_CLL, max_cll); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an target_max_fall event to the client owning the resource. - * @param resource_ The client's resource - * @param max_fall Maximum frame-average light level (cd/m²) - */ static inline void wp_image_description_info_v1_send_target_max_fall(struct wl_resource *resource_, uint32_t max_fall) { diff --git a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-client-protocol.h b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-client-protocol.h index e7c4665f3..e0a20b854 100644 --- a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-client-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-client-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef LINUX_DMABUF_V1_CLIENT_PROTOCOL_H #define LINUX_DMABUF_V1_CLIENT_PROTOCOL_H @@ -11,14 +10,8 @@ extern "C" { #endif -/** - * @page page_linux_dmabuf_v1 The linux_dmabuf_v1 protocol - * @section page_ifaces_linux_dmabuf_v1 Interfaces - * - @subpage page_iface_zwp_linux_dmabuf_v1 - factory for creating dmabuf-based wl_buffers - * - @subpage page_iface_zwp_linux_buffer_params_v1 - parameters for creating a dmabuf-based wl_buffer - * - @subpage page_iface_zwp_linux_dmabuf_feedback_v1 - dmabuf feedback +/* * @section page_copyright_linux_dmabuf_v1 Copyright - *
  *
  * Copyright © 2014, 2015 Collabora, Ltd.
  *
@@ -40,8 +33,14 @@ extern "C" {
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + struct wl_buffer; struct wl_surface; struct zwp_linux_buffer_params_v1; @@ -50,331 +49,21 @@ struct zwp_linux_dmabuf_v1; #ifndef ZWP_LINUX_DMABUF_V1_INTERFACE #define ZWP_LINUX_DMABUF_V1_INTERFACE -/** - * @page page_iface_zwp_linux_dmabuf_v1 zwp_linux_dmabuf_v1 - * @section page_iface_zwp_linux_dmabuf_v1_desc Description - * - * This interface offers ways to create generic dmabuf-based wl_buffers. - * - * For more information about dmabuf, see: - * https://www.kernel.org/doc/html/next/userspace-api/dma-buf-alloc-exchange.html - * - * Clients can use the get_surface_feedback request to get dmabuf feedback - * for a particular surface. If the client wants to retrieve feedback not - * tied to a surface, they can use the get_default_feedback request. - * - * The following are required from clients: - * - * - Clients must ensure that either all data in the dma-buf is - * coherent for all subsequent read access or that coherency is - * correctly handled by the underlying kernel-side dma-buf - * implementation. - * - * - Don't make any more attachments after sending the buffer to the - * compositor. Making more attachments later increases the risk of - * the compositor not being able to use (re-import) an existing - * dmabuf-based wl_buffer. - * - * The underlying graphics stack must ensure the following: - * - * - The dmabuf file descriptors relayed to the server will stay valid - * for the whole lifetime of the wl_buffer. This means the server may - * at any time use those fds to import the dmabuf into any kernel - * sub-system that might accept it. - * - * However, when the underlying graphics stack fails to deliver the - * promise, because of e.g. a device hot-unplug which raises internal - * errors, after the wl_buffer has been successfully created the - * compositor must not raise protocol errors to the client when dmabuf - * import later fails. - * - * To create a wl_buffer from one or more dmabufs, a client creates a - * zwp_linux_buffer_params_v1 object with a zwp_linux_dmabuf_v1.create_params - * request. All planes required by the intended format are added with - * the 'add' request. Finally, a 'create' or 'create_immed' request is - * issued, which has the following outcome depending on the import success. - * - * The 'create' request, - * - on success, triggers a 'created' event which provides the final - * wl_buffer to the client. - * - on failure, triggers a 'failed' event to convey that the server - * cannot use the dmabufs received from the client. - * - * For the 'create_immed' request, - * - on success, the server immediately imports the added dmabufs to - * create a wl_buffer. No event is sent from the server in this case. - * - on failure, the server can choose to either: - * - terminate the client by raising a fatal error. - * - mark the wl_buffer as failed, and send a 'failed' event to the - * client. If the client uses a failed wl_buffer as an argument to any - * request, the behaviour is compositor implementation-defined. - * - * For all DRM formats and unless specified in another protocol extension, - * pre-multiplied alpha is used for pixel values. - * - * Unless specified otherwise in another protocol extension, implicit - * synchronization is used. In other words, compositors and clients must - * wait and signal fences implicitly passed via the DMA-BUF's reservation - * mechanism. - * @section page_iface_zwp_linux_dmabuf_v1_api API - * See @ref iface_zwp_linux_dmabuf_v1. - */ -/** - * @defgroup iface_zwp_linux_dmabuf_v1 The zwp_linux_dmabuf_v1 interface - * - * This interface offers ways to create generic dmabuf-based wl_buffers. - * - * For more information about dmabuf, see: - * https://www.kernel.org/doc/html/next/userspace-api/dma-buf-alloc-exchange.html - * - * Clients can use the get_surface_feedback request to get dmabuf feedback - * for a particular surface. If the client wants to retrieve feedback not - * tied to a surface, they can use the get_default_feedback request. - * - * The following are required from clients: - * - * - Clients must ensure that either all data in the dma-buf is - * coherent for all subsequent read access or that coherency is - * correctly handled by the underlying kernel-side dma-buf - * implementation. - * - * - Don't make any more attachments after sending the buffer to the - * compositor. Making more attachments later increases the risk of - * the compositor not being able to use (re-import) an existing - * dmabuf-based wl_buffer. - * - * The underlying graphics stack must ensure the following: - * - * - The dmabuf file descriptors relayed to the server will stay valid - * for the whole lifetime of the wl_buffer. This means the server may - * at any time use those fds to import the dmabuf into any kernel - * sub-system that might accept it. - * - * However, when the underlying graphics stack fails to deliver the - * promise, because of e.g. a device hot-unplug which raises internal - * errors, after the wl_buffer has been successfully created the - * compositor must not raise protocol errors to the client when dmabuf - * import later fails. - * - * To create a wl_buffer from one or more dmabufs, a client creates a - * zwp_linux_buffer_params_v1 object with a zwp_linux_dmabuf_v1.create_params - * request. All planes required by the intended format are added with - * the 'add' request. Finally, a 'create' or 'create_immed' request is - * issued, which has the following outcome depending on the import success. - * - * The 'create' request, - * - on success, triggers a 'created' event which provides the final - * wl_buffer to the client. - * - on failure, triggers a 'failed' event to convey that the server - * cannot use the dmabufs received from the client. - * - * For the 'create_immed' request, - * - on success, the server immediately imports the added dmabufs to - * create a wl_buffer. No event is sent from the server in this case. - * - on failure, the server can choose to either: - * - terminate the client by raising a fatal error. - * - mark the wl_buffer as failed, and send a 'failed' event to the - * client. If the client uses a failed wl_buffer as an argument to any - * request, the behaviour is compositor implementation-defined. - * - * For all DRM formats and unless specified in another protocol extension, - * pre-multiplied alpha is used for pixel values. - * - * Unless specified otherwise in another protocol extension, implicit - * synchronization is used. In other words, compositors and clients must - * wait and signal fences implicitly passed via the DMA-BUF's reservation - * mechanism. - */ extern const struct wl_interface zwp_linux_dmabuf_v1_interface; #endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_INTERFACE #define ZWP_LINUX_BUFFER_PARAMS_V1_INTERFACE -/** - * @page page_iface_zwp_linux_buffer_params_v1 zwp_linux_buffer_params_v1 - * @section page_iface_zwp_linux_buffer_params_v1_desc Description - * - * This temporary object is a collection of dmabufs and other - * parameters that together form a single logical buffer. The temporary - * object may eventually create one wl_buffer unless cancelled by - * destroying it before requesting 'create'. - * - * Single-planar formats only require one dmabuf, however - * multi-planar formats may require more than one dmabuf. For all - * formats, an 'add' request must be called once per plane (even if the - * underlying dmabuf fd is identical). - * - * You must use consecutive plane indices ('plane_idx' argument for 'add') - * from zero to the number of planes used by the drm_fourcc format code. - * All planes required by the format must be given exactly once, but can - * be given in any order. Each plane index can only be set once; subsequent - * calls with a plane index which has already been set will result in a - * plane_set error being generated. - * @section page_iface_zwp_linux_buffer_params_v1_api API - * See @ref iface_zwp_linux_buffer_params_v1. - */ -/** - * @defgroup iface_zwp_linux_buffer_params_v1 The zwp_linux_buffer_params_v1 interface - * - * This temporary object is a collection of dmabufs and other - * parameters that together form a single logical buffer. The temporary - * object may eventually create one wl_buffer unless cancelled by - * destroying it before requesting 'create'. - * - * Single-planar formats only require one dmabuf, however - * multi-planar formats may require more than one dmabuf. For all - * formats, an 'add' request must be called once per plane (even if the - * underlying dmabuf fd is identical). - * - * You must use consecutive plane indices ('plane_idx' argument for 'add') - * from zero to the number of planes used by the drm_fourcc format code. - * All planes required by the format must be given exactly once, but can - * be given in any order. Each plane index can only be set once; subsequent - * calls with a plane index which has already been set will result in a - * plane_set error being generated. - */ extern const struct wl_interface zwp_linux_buffer_params_v1_interface; #endif #ifndef ZWP_LINUX_DMABUF_FEEDBACK_V1_INTERFACE #define ZWP_LINUX_DMABUF_FEEDBACK_V1_INTERFACE -/** - * @page page_iface_zwp_linux_dmabuf_feedback_v1 zwp_linux_dmabuf_feedback_v1 - * @section page_iface_zwp_linux_dmabuf_feedback_v1_desc Description - * - * This object advertises dmabuf parameters feedback. This includes the - * preferred devices and the supported formats/modifiers. - * - * The parameters are sent once when this object is created and whenever they - * change. The done event is always sent once after all parameters have been - * sent. When a single parameter changes, all parameters are re-sent by the - * compositor. - * - * Compositors can re-send the parameters when the current client buffer - * allocations are sub-optimal. Compositors should not re-send the - * parameters if re-allocating the buffers would not result in a more optimal - * configuration. In particular, compositors should avoid sending the exact - * same parameters multiple times in a row. - * - * The tranche_target_device and tranche_formats events are grouped by - * tranches of preference. For each tranche, a tranche_target_device, one - * tranche_flags and one or more tranche_formats events are sent, followed - * by a tranche_done event finishing the list. The tranches are sent in - * descending order of preference. All formats and modifiers in the same - * tranche have the same preference. - * - * To send parameters, the compositor sends one main_device event (unless - * the client bound version 6 or above), tranches (each consisting of one - * tranche_target_device event, one tranche_flags event, tranche_formats - * events and then a tranche_done event), then one done event. - * - * With version 6 and above, the compositor must always advertise at least - * one tranche with the sampling flag set. - * - * The compositor sends a format_table event at least once for each dmabuf - * parameters feedback object, before any tranche events. Unless the format - * table contents change later, the compositor is free not to send another - * format_table event for the same dmabuf parameters feedback object. - * @section page_iface_zwp_linux_dmabuf_feedback_v1_api API - * See @ref iface_zwp_linux_dmabuf_feedback_v1. - */ -/** - * @defgroup iface_zwp_linux_dmabuf_feedback_v1 The zwp_linux_dmabuf_feedback_v1 interface - * - * This object advertises dmabuf parameters feedback. This includes the - * preferred devices and the supported formats/modifiers. - * - * The parameters are sent once when this object is created and whenever they - * change. The done event is always sent once after all parameters have been - * sent. When a single parameter changes, all parameters are re-sent by the - * compositor. - * - * Compositors can re-send the parameters when the current client buffer - * allocations are sub-optimal. Compositors should not re-send the - * parameters if re-allocating the buffers would not result in a more optimal - * configuration. In particular, compositors should avoid sending the exact - * same parameters multiple times in a row. - * - * The tranche_target_device and tranche_formats events are grouped by - * tranches of preference. For each tranche, a tranche_target_device, one - * tranche_flags and one or more tranche_formats events are sent, followed - * by a tranche_done event finishing the list. The tranches are sent in - * descending order of preference. All formats and modifiers in the same - * tranche have the same preference. - * - * To send parameters, the compositor sends one main_device event (unless - * the client bound version 6 or above), tranches (each consisting of one - * tranche_target_device event, one tranche_flags event, tranche_formats - * events and then a tranche_done event), then one done event. - * - * With version 6 and above, the compositor must always advertise at least - * one tranche with the sampling flag set. - * - * The compositor sends a format_table event at least once for each dmabuf - * parameters feedback object, before any tranche events. Unless the format - * table contents change later, the compositor is free not to send another - * format_table event for the same dmabuf parameters feedback object. - */ extern const struct wl_interface zwp_linux_dmabuf_feedback_v1_interface; #endif -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * @struct zwp_linux_dmabuf_v1_listener - */ struct zwp_linux_dmabuf_v1_listener { - /** - * supported buffer format - * - * This event advertises one buffer format that the server - * supports. All the supported formats are advertised once when the - * client binds to this interface. A roundtrip after binding - * guarantees that the client has received all supported formats. - * - * For the definition of the format codes, see the - * zwp_linux_buffer_params_v1::create request. - * - * Starting version 4, the format event is deprecated and must not - * be sent by compositors. Instead, use get_default_feedback or - * get_surface_feedback. - * @param format DRM_FORMAT code - * @deprecated Deprecated since version 4 - */ void (*format)(void *data, struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1, uint32_t format); - /** - * supported buffer format modifier - * - * This event advertises the formats that the server supports, - * along with the modifiers supported for each format. All the - * supported modifiers for all the supported formats are advertised - * once when the client binds to this interface. A roundtrip after - * binding guarantees that the client has received all supported - * format-modifier pairs. - * - * For legacy support, DRM_FORMAT_MOD_INVALID (that is, modifier_hi - * == 0x00ffffff and modifier_lo == 0xffffffff) is allowed in this - * event. It indicates that the server can support the format with - * an implicit modifier. When a plane has DRM_FORMAT_MOD_INVALID as - * its modifier, it is as if no explicit modifier is specified. The - * effective modifier will be derived from the dmabuf. - * - * A compositor that sends valid modifiers and - * DRM_FORMAT_MOD_INVALID for a given format supports both explicit - * modifiers and implicit modifiers. - * - * For the definition of the format and modifier codes, see the - * zwp_linux_buffer_params_v1::create and - * zwp_linux_buffer_params_v1::add requests. - * - * Starting version 4, the modifier event is deprecated and must - * not be sent by compositors. Instead, use get_default_feedback or - * get_surface_feedback. - * @param format DRM_FORMAT code - * @param modifier_hi high 32 bits of layout modifier - * @param modifier_lo low 32 bits of layout modifier - * @since 3 - * @deprecated Deprecated since version 4 - */ void (*modifier)(void *data, struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1, uint32_t format, @@ -382,9 +71,6 @@ struct zwp_linux_dmabuf_v1_listener { uint32_t modifier_lo); }; -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ static inline int zwp_linux_dmabuf_v1_add_listener(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1, const struct zwp_linux_dmabuf_v1_listener *listener, void *data) @@ -398,40 +84,20 @@ zwp_linux_dmabuf_v1_add_listener(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1 #define ZWP_LINUX_DMABUF_V1_GET_DEFAULT_FEEDBACK 2 #define ZWP_LINUX_DMABUF_V1_GET_SURFACE_FEEDBACK 3 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_FORMAT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_MODIFIER_SINCE_VERSION 3 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_CREATE_PARAMS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_GET_DEFAULT_FEEDBACK_SINCE_VERSION 4 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_GET_SURFACE_FEEDBACK_SINCE_VERSION 4 -/** @ingroup iface_zwp_linux_dmabuf_v1 */ static inline void zwp_linux_dmabuf_v1_set_user_data(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) zwp_linux_dmabuf_v1, user_data); } -/** @ingroup iface_zwp_linux_dmabuf_v1 */ static inline void * zwp_linux_dmabuf_v1_get_user_data(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) { @@ -444,12 +110,6 @@ zwp_linux_dmabuf_v1_get_version(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) return wl_proxy_get_version((struct wl_proxy *) zwp_linux_dmabuf_v1); } -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * - * Objects created through this interface, especially wl_buffers, will - * remain valid. - */ static inline void zwp_linux_dmabuf_v1_destroy(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) { @@ -457,14 +117,6 @@ zwp_linux_dmabuf_v1_destroy(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) ZWP_LINUX_DMABUF_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_linux_dmabuf_v1), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * - * This temporary object is used to collect multiple dmabuf handles into - * a single batch to create a wl_buffer. It can only be used once and - * should be destroyed after a 'created' or 'failed' event has been - * received. - */ static inline struct zwp_linux_buffer_params_v1 * zwp_linux_dmabuf_v1_create_params(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) { @@ -476,14 +128,6 @@ zwp_linux_dmabuf_v1_create_params(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v return (struct zwp_linux_buffer_params_v1 *) params_id; } -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * - * This request creates a new zwp_linux_dmabuf_feedback_v1 object not bound - * to a particular surface. This object will deliver feedback about dmabuf - * parameters to use if the client doesn't support per-surface feedback - * (see get_surface_feedback). - */ static inline struct zwp_linux_dmabuf_feedback_v1 * zwp_linux_dmabuf_v1_get_default_feedback(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) { @@ -495,16 +139,6 @@ zwp_linux_dmabuf_v1_get_default_feedback(struct zwp_linux_dmabuf_v1 *zwp_linux_d return (struct zwp_linux_dmabuf_feedback_v1 *) id; } -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * - * This request creates a new zwp_linux_dmabuf_feedback_v1 object for the - * specified wl_surface. This object will deliver feedback about dmabuf - * parameters to use for buffers attached to this surface. - * - * If the surface is destroyed before the zwp_linux_dmabuf_feedback_v1 object, - * the feedback object becomes inert. - */ static inline struct zwp_linux_dmabuf_feedback_v1 * zwp_linux_dmabuf_v1_get_surface_feedback(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1, struct wl_surface *surface) { @@ -519,99 +153,35 @@ zwp_linux_dmabuf_v1_get_surface_feedback(struct zwp_linux_dmabuf_v1 *zwp_linux_d #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM #define ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM enum zwp_linux_buffer_params_v1_error { - /** - * the zwp_linux_buffer_params_v1 object has already been used to create a wl_buffer - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ALREADY_USED = 0, - /** - * plane index out of bounds - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_IDX = 1, - /** - * the plane index was already set - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_SET = 2, - /** - * missing or too many planes to create a buffer - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INCOMPLETE = 3, - /** - * format not supported - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_FORMAT = 4, - /** - * invalid width or height - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_DIMENSIONS = 5, - /** - * offset + stride * height goes out of dmabuf bounds - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_OUT_OF_BOUNDS = 6, - /** - * invalid wl_buffer resulted from importing dmabufs via the create_immed request on given buffer_params - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_WL_BUFFER = 7, - /** - * an array with mismatching size for a dev_t was used - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_DEV_T_SIZE = 8, }; -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM */ +#endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM #define ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM enum zwp_linux_buffer_params_v1_flags { - /** - * contents are y-inverted - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_Y_INVERT = 1, - /** - * content is interlaced - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_INTERLACED = 2, - /** - * bottom field first - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_BOTTOM_FIRST = 4, }; -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM */ +#endif -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * @struct zwp_linux_buffer_params_v1_listener - */ struct zwp_linux_buffer_params_v1_listener { - /** - * buffer creation succeeded - * - * This event indicates that the attempted buffer creation was - * successful. It provides the new wl_buffer referencing the - * dmabuf(s). - * - * Upon receiving this event, the client should destroy the - * zwp_linux_buffer_params_v1 object. - * @param buffer id for the the newly created wl_buffer - */ void (*created)(void *data, struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, struct wl_buffer *buffer); - /** - * buffer creation failed - * - * This event indicates that the attempted buffer creation has - * failed. It usually means that one of the dmabuf constraints has - * not been fulfilled. - * - * Upon receiving this event, the client should destroy the - * zwp_linux_buffer_params_v1 object. - */ void (*failed)(void *data, struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1); }; -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ static inline int zwp_linux_buffer_params_v1_add_listener(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, const struct zwp_linux_buffer_params_v1_listener *listener, void *data) @@ -626,44 +196,21 @@ zwp_linux_buffer_params_v1_add_listener(struct zwp_linux_buffer_params_v1 *zwp_l #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED 3 #define ZWP_LINUX_BUFFER_PARAMS_V1_SET_SAMPLING_DEVICE 4 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_FAILED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_ADD_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_SET_SAMPLING_DEVICE_SINCE_VERSION 6 -/** @ingroup iface_zwp_linux_buffer_params_v1 */ static inline void zwp_linux_buffer_params_v1_set_user_data(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) zwp_linux_buffer_params_v1, user_data); } -/** @ingroup iface_zwp_linux_buffer_params_v1 */ static inline void * zwp_linux_buffer_params_v1_get_user_data(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1) { @@ -676,12 +223,6 @@ zwp_linux_buffer_params_v1_get_version(struct zwp_linux_buffer_params_v1 *zwp_li return wl_proxy_get_version((struct wl_proxy *) zwp_linux_buffer_params_v1); } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * - * Cleans up the temporary data sent to the server for dmabuf-based - * wl_buffer creation. - */ static inline void zwp_linux_buffer_params_v1_destroy(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1) { @@ -689,29 +230,6 @@ zwp_linux_buffer_params_v1_destroy(struct zwp_linux_buffer_params_v1 *zwp_linux_ ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_linux_buffer_params_v1), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * - * This request adds one dmabuf to the set in this - * zwp_linux_buffer_params_v1. - * - * The 64-bit unsigned value combined from modifier_hi and modifier_lo - * is the dmabuf layout modifier. DRM AddFB2 ioctl calls this the - * fb modifier, which is defined in drm_mode.h of Linux UAPI. - * This is an opaque token. Drivers use this token to express tiling, - * compression, etc. driver-specific modifications to the base format - * defined by the DRM fourcc code. - * - * Starting from version 4, the invalid_format protocol error is sent if - * the format + modifier pair was not advertised as supported. - * - * Starting from version 5, the invalid_format protocol error is sent if - * all planes don't use the same modifier. - * - * This request raises the PLANE_IDX error if plane_idx is too large. - * The error PLANE_SET is raised if attempting to set a plane that - * was already set. - */ static inline void zwp_linux_buffer_params_v1_add(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, int32_t fd, uint32_t plane_idx, uint32_t offset, uint32_t stride, uint32_t modifier_hi, uint32_t modifier_lo) { @@ -719,69 +237,6 @@ zwp_linux_buffer_params_v1_add(struct zwp_linux_buffer_params_v1 *zwp_linux_buff ZWP_LINUX_BUFFER_PARAMS_V1_ADD, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_linux_buffer_params_v1), 0, fd, plane_idx, offset, stride, modifier_hi, modifier_lo); } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * - * This asks for creation of a wl_buffer from the added dmabuf - * buffers. The wl_buffer is not created immediately but returned via - * the 'created' event if the dmabuf sharing succeeds. The sharing - * may fail at runtime for reasons a client cannot predict, in - * which case the 'failed' event is triggered. - * - * The 'format' argument is a DRM_FORMAT code, as defined by the - * libdrm's drm_fourcc.h. The Linux kernel's DRM sub-system is the - * authoritative source on how the format codes should work. - * - * The 'flags' is a bitfield of the flags defined in enum "flags". - * 'y_invert' means that the image needs to be y-flipped. - * - * Flag 'interlaced' means that the frame in the buffer is not - * progressive as usual, but interlaced. An interlaced buffer as - * supported here must always contain both top and bottom fields. - * The top field always begins on the first pixel row. The temporal - * ordering between the two fields is top field first, unless - * 'bottom_first' is specified. It is undefined whether 'bottom_first' - * is ignored if 'interlaced' is not set. - * - * This protocol does not convey any information about field rate, - * duration, or timing, other than the relative ordering between the - * two fields in one buffer. A compositor may have to estimate the - * intended field rate from the incoming buffer rate. It is undefined - * whether the time of receiving wl_surface.commit with a new buffer - * attached, applying the wl_surface state, wl_surface.frame callback - * trigger, presentation, or any other point in the compositor cycle - * is used to measure the frame or field times. There is no support - * for detecting missed or late frames/fields/buffers either, and - * there is no support whatsoever for cooperating with interlaced - * compositor output. - * - * The composited image quality resulting from the use of interlaced - * buffers is explicitly undefined. A compositor may use elaborate - * hardware features or software to deinterlace and create progressive - * output frames from a sequence of interlaced input buffers, or it - * may produce substandard image quality. However, compositors that - * cannot guarantee reasonable image quality in all cases are recommended - * to just reject all interlaced buffers. - * - * Any argument errors, including non-positive width or height, - * mismatch between the number of planes and the format, bad - * format, bad offset or stride, may be indicated by fatal protocol - * errors: INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS, - * OUT_OF_BOUNDS. - * - * Dmabuf import errors in the server that are not obvious client - * bugs are returned via the 'failed' event as non-fatal. This - * allows attempting dmabuf sharing and falling back in the client - * if it fails. - * - * This request can be sent only once in the object's lifetime, after - * which the only legal request is destroy. This object should be - * destroyed after issuing a 'create' request. Attempting to use this - * object after issuing 'create' raises the ALREADY_USED protocol error. - * - * It is not mandatory to issue 'create'. If a client wants to - * cancel the buffer creation, it can just destroy this object. - */ static inline void zwp_linux_buffer_params_v1_create(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, int32_t width, int32_t height, uint32_t format, uint32_t flags) { @@ -789,33 +244,6 @@ zwp_linux_buffer_params_v1_create(struct zwp_linux_buffer_params_v1 *zwp_linux_b ZWP_LINUX_BUFFER_PARAMS_V1_CREATE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_linux_buffer_params_v1), 0, width, height, format, flags); } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * - * This asks for immediate creation of a wl_buffer by importing the - * added dmabufs. - * - * In case of import success, no event is sent from the server, and the - * wl_buffer is ready to be used by the client. - * - * Upon import failure, either of the following may happen, as seen fit - * by the implementation: - * - the client is terminated with one of the following fatal protocol - * errors: - * - INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS, OUT_OF_BOUNDS, - * in case of argument errors such as mismatch between the number - * of planes and the format, bad format, non-positive width or - * height, or bad offset or stride. - * - INVALID_WL_BUFFER, in case the cause for failure is unknown or - * platform specific. - * - the server creates an invalid wl_buffer, marks it as failed and - * sends a 'failed' event to the client. The result of using this - * invalid wl_buffer as an argument in any request by the client is - * defined by the compositor implementation. - * - * This takes the same arguments as a 'create' request, and obeys the - * same restrictions. - */ static inline struct wl_buffer * zwp_linux_buffer_params_v1_create_immed(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, int32_t width, int32_t height, uint32_t format, uint32_t flags) { @@ -827,23 +255,6 @@ zwp_linux_buffer_params_v1_create_immed(struct zwp_linux_buffer_params_v1 *zwp_l return (struct wl_buffer *) buffer_id; } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * - * Set the device the compositor should import the dmabufs to for sampling - * in the next create or create_immed request. - * - * To avoid race conditions when the compositor removes a device from the - * tranches, it is not a protocol error if the device hasn't been advertised - * by the compositor in a tranche with the sampling flag, but the import is - * likely to fail in that case. - * - * If the client doesn't know a suitable target device, it shouldn't set one, - * and the compositor should attempt import on all devices it supports. - * - * If the array is too small to contain a dev_t or larger than required, the - * invalid_dev_t_size error will be emitted. - */ static inline void zwp_linux_buffer_params_v1_set_sampling_device(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, struct wl_array *device) { @@ -854,221 +265,36 @@ zwp_linux_buffer_params_v1_set_sampling_device(struct zwp_linux_buffer_params_v1 #ifndef ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM enum zwp_linux_dmabuf_feedback_v1_tranche_flags { - /** - * direct scan-out tranche - * - * The scanout flag is a hint that direct scan-out may be - * attempted by the compositor on the target device if the client - * appropriately allocates a buffer. How to allocate a buffer that - * can be scanned out on the target device is - * implementation-defined. - * @since 4 - */ ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SCANOUT = 1, - /** - * sampling tranche - * - * The sampling flag describes that the compositor is able to - * efficiently sample from buffers imported to the target device if - * the client appropriately allocates a buffer. How to allocate a - * buffer that can be efficiently sampled on the target device is - * implementation defined. - * @since 6 - */ ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SAMPLING = 2, }; -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SCANOUT_SINCE_VERSION 4 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SAMPLING_SINCE_VERSION 6 -#endif /* ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM */ +#endif -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * @struct zwp_linux_dmabuf_feedback_v1_listener - */ struct zwp_linux_dmabuf_feedback_v1_listener { - /** - * all feedback has been sent - * - * This event is sent after all parameters of a - * zwp_linux_dmabuf_feedback_v1 object have been sent. - * - * This allows changes to the zwp_linux_dmabuf_feedback_v1 - * parameters to be seen as atomic, even if they happen via - * multiple events. - */ void (*done)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1); - /** - * format and modifier table - * - * This event provides a file descriptor which can be - * memory-mapped to access the format and modifier table. - * - * The table contains a tightly packed array of consecutive format - * + modifier pairs. Each pair is 16 bytes wide. It contains a - * format as a 32-bit unsigned integer, followed by 4 bytes of - * unused padding, and a modifier as a 64-bit unsigned integer. The - * native endianness is used. - * - * The client must map the file descriptor in read-only private - * mode. - * - * Compositors are not allowed to mutate the table file contents - * once this event has been sent. Instead, compositors must create - * a new, separate table file and re-send feedback parameters. - * Compositors are allowed to store duplicate format + modifier - * pairs in the table. - * @param fd table file descriptor - * @param size table size, in bytes - */ void (*format_table)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, int32_t fd, uint32_t size); - /** - * preferred main device - * - * This event advertises the main device that the server prefers - * to use when direct scan-out to the target device isn't possible. - * The advertised main device may be different for each - * zwp_linux_dmabuf_feedback_v1 object, and may change over time. - * - * There is exactly one main device. The compositor must send at - * least one preference tranche with tranche_target_device equal to - * main_device. - * - * Clients need to create buffers that the main device can import - * and read from, otherwise creating the dmabuf wl_buffer will fail - * (see the zwp_linux_buffer_params_v1.create and create_immed - * requests for details). The main device will also likely be kept - * active by the compositor, so clients can use it instead of - * waking up another device for power savings. - * - * In general the device is a DRM node. The DRM node type (primary - * vs. render) is unspecified. Clients must not rely on the - * compositor sending a particular node type. Clients cannot check - * two devices for equality by comparing the dev_t value. - * - * If explicit modifiers are not supported and the client performs - * buffer allocations on a different device than the main device, - * then the client must force the buffer to have a linear layout. - * - * With version 6 and above, this event is no longer sent. Clients - * should use a device with the sampling flag in the tranches - * instead. - * @param device device dev_t value - * @deprecated Deprecated since version 6 - */ void (*main_device)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, struct wl_array *device); - /** - * a preference tranche has been sent - * - * This event splits tranche_target_device and tranche_formats - * events into preference tranches. It is sent after a set of - * tranche_target_device and tranche_formats events; it represents - * the end of a tranche. The next tranche will have a lower - * preference. - */ void (*tranche_done)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1); - /** - * target device - * - * This event advertises the target device that the server - * prefers to use for a buffer created given this tranche. The - * advertised target device may be different for each preference - * tranche, and may change over time. - * - * There is exactly one target device per tranche. - * - * The target device may be a scan-out device, for example if the - * compositor prefers to directly scan-out a buffer created given - * this tranche. The target device may be a rendering device, for - * example if the compositor prefers to texture from said buffer. - * - * The client can use this hint to allocate the buffer in a way - * that makes it accessible from the target device, ideally - * directly. The buffer must still be accessible from a device with - * the sampling flag, either through direct import or a potentially - * more expensive fallback path. If the buffer can't be directly - * imported for sampling, then clients must be prepared for the - * compositor changing the tranche priority or making wl_buffer - * creation fail (see the zwp_linux_buffer_params_v1.create and - * create_immed requests for details). - * - * If the device is a DRM node, the DRM node type (primary vs. - * render) is unspecified. Clients must not rely on the compositor - * sending a particular node type. Clients cannot check two devices - * for equality by comparing the dev_t value. - * - * This event is tied to a preference tranche, see the tranche_done - * event. - * @param device device dev_t value - */ void (*tranche_target_device)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, struct wl_array *device); - /** - * supported buffer format modifiers - * - * This event advertises the format + modifier combinations that - * the compositor supports. - * - * It carries an array of indices, each referring to a format + - * modifier pair in the last received format table (see the - * format_table event). Each index is a 16-bit unsigned integer in - * native endianness. - * - * For legacy support, DRM_FORMAT_MOD_INVALID is an allowed - * modifier. It indicates that the server can support the format - * with an implicit modifier. When a buffer has - * DRM_FORMAT_MOD_INVALID as its modifier, it is as if no explicit - * modifier is specified. The effective modifier will be derived - * from the dmabuf. - * - * A compositor that sends valid modifiers and - * DRM_FORMAT_MOD_INVALID for a given format supports both explicit - * modifiers and implicit modifiers. - * - * Compositors must not send duplicate format + modifier pairs - * within the same tranche or across two different tranches with - * the same target device and flags. - * - * This event is tied to a preference tranche, see the tranche_done - * event. - * - * For the definition of the format and modifier codes, see the - * zwp_linux_buffer_params_v1.create request. - * @param indices array of 16-bit indexes - */ void (*tranche_formats)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, struct wl_array *indices); - /** - * tranche flags - * - * This event sets tranche-specific flags. This event is tied to - * a preference tranche, see the tranche_done event. With version 6 - * and above, the compositor must set at least one flag in each - * tranche. - * @param flags tranche flags - */ void (*tranche_flags)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, uint32_t flags); }; -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ static inline int zwp_linux_dmabuf_feedback_v1_add_listener(struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, const struct zwp_linux_dmabuf_feedback_v1_listener *listener, void *data) @@ -1079,48 +305,22 @@ zwp_linux_dmabuf_feedback_v1_add_listener(struct zwp_linux_dmabuf_feedback_v1 *z #define ZWP_LINUX_DMABUF_FEEDBACK_V1_DESTROY 0 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_FORMAT_TABLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_MAIN_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_TARGET_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FORMATS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_DESTROY_SINCE_VERSION 1 -/** @ingroup iface_zwp_linux_dmabuf_feedback_v1 */ static inline void zwp_linux_dmabuf_feedback_v1_set_user_data(struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) zwp_linux_dmabuf_feedback_v1, user_data); } -/** @ingroup iface_zwp_linux_dmabuf_feedback_v1 */ static inline void * zwp_linux_dmabuf_feedback_v1_get_user_data(struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1) { @@ -1133,12 +333,6 @@ zwp_linux_dmabuf_feedback_v1_get_version(struct zwp_linux_dmabuf_feedback_v1 *zw return wl_proxy_get_version((struct wl_proxy *) zwp_linux_dmabuf_feedback_v1); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * - * Using this request a client can tell the server that it is not going to - * use the zwp_linux_dmabuf_feedback_v1 object anymore. - */ static inline void zwp_linux_dmabuf_feedback_v1_destroy(struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1) { diff --git a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-protocol.c index 7d2c38984..f1014b981 100644 --- a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2014, 2015 Collabora, Ltd. * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-server-protocol.h index 7d87d90ac..849cdbe1d 100644 --- a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef LINUX_DMABUF_V1_SERVER_PROTOCOL_H #define LINUX_DMABUF_V1_SERVER_PROTOCOL_H @@ -14,14 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_linux_dmabuf_v1 The linux_dmabuf_v1 protocol - * @section page_ifaces_linux_dmabuf_v1 Interfaces - * - @subpage page_iface_zwp_linux_dmabuf_v1 - factory for creating dmabuf-based wl_buffers - * - @subpage page_iface_zwp_linux_buffer_params_v1 - parameters for creating a dmabuf-based wl_buffer - * - @subpage page_iface_zwp_linux_dmabuf_feedback_v1 - dmabuf feedback +/* * @section page_copyright_linux_dmabuf_v1 Copyright - *
  *
  * Copyright © 2014, 2015 Collabora, Ltd.
  *
@@ -43,8 +36,14 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + struct wl_buffer; struct wl_surface; struct zwp_linux_buffer_params_v1; @@ -53,322 +52,26 @@ struct zwp_linux_dmabuf_v1; #ifndef ZWP_LINUX_DMABUF_V1_INTERFACE #define ZWP_LINUX_DMABUF_V1_INTERFACE -/** - * @page page_iface_zwp_linux_dmabuf_v1 zwp_linux_dmabuf_v1 - * @section page_iface_zwp_linux_dmabuf_v1_desc Description - * - * This interface offers ways to create generic dmabuf-based wl_buffers. - * - * For more information about dmabuf, see: - * https://www.kernel.org/doc/html/next/userspace-api/dma-buf-alloc-exchange.html - * - * Clients can use the get_surface_feedback request to get dmabuf feedback - * for a particular surface. If the client wants to retrieve feedback not - * tied to a surface, they can use the get_default_feedback request. - * - * The following are required from clients: - * - * - Clients must ensure that either all data in the dma-buf is - * coherent for all subsequent read access or that coherency is - * correctly handled by the underlying kernel-side dma-buf - * implementation. - * - * - Don't make any more attachments after sending the buffer to the - * compositor. Making more attachments later increases the risk of - * the compositor not being able to use (re-import) an existing - * dmabuf-based wl_buffer. - * - * The underlying graphics stack must ensure the following: - * - * - The dmabuf file descriptors relayed to the server will stay valid - * for the whole lifetime of the wl_buffer. This means the server may - * at any time use those fds to import the dmabuf into any kernel - * sub-system that might accept it. - * - * However, when the underlying graphics stack fails to deliver the - * promise, because of e.g. a device hot-unplug which raises internal - * errors, after the wl_buffer has been successfully created the - * compositor must not raise protocol errors to the client when dmabuf - * import later fails. - * - * To create a wl_buffer from one or more dmabufs, a client creates a - * zwp_linux_buffer_params_v1 object with a zwp_linux_dmabuf_v1.create_params - * request. All planes required by the intended format are added with - * the 'add' request. Finally, a 'create' or 'create_immed' request is - * issued, which has the following outcome depending on the import success. - * - * The 'create' request, - * - on success, triggers a 'created' event which provides the final - * wl_buffer to the client. - * - on failure, triggers a 'failed' event to convey that the server - * cannot use the dmabufs received from the client. - * - * For the 'create_immed' request, - * - on success, the server immediately imports the added dmabufs to - * create a wl_buffer. No event is sent from the server in this case. - * - on failure, the server can choose to either: - * - terminate the client by raising a fatal error. - * - mark the wl_buffer as failed, and send a 'failed' event to the - * client. If the client uses a failed wl_buffer as an argument to any - * request, the behaviour is compositor implementation-defined. - * - * For all DRM formats and unless specified in another protocol extension, - * pre-multiplied alpha is used for pixel values. - * - * Unless specified otherwise in another protocol extension, implicit - * synchronization is used. In other words, compositors and clients must - * wait and signal fences implicitly passed via the DMA-BUF's reservation - * mechanism. - * @section page_iface_zwp_linux_dmabuf_v1_api API - * See @ref iface_zwp_linux_dmabuf_v1. - */ -/** - * @defgroup iface_zwp_linux_dmabuf_v1 The zwp_linux_dmabuf_v1 interface - * - * This interface offers ways to create generic dmabuf-based wl_buffers. - * - * For more information about dmabuf, see: - * https://www.kernel.org/doc/html/next/userspace-api/dma-buf-alloc-exchange.html - * - * Clients can use the get_surface_feedback request to get dmabuf feedback - * for a particular surface. If the client wants to retrieve feedback not - * tied to a surface, they can use the get_default_feedback request. - * - * The following are required from clients: - * - * - Clients must ensure that either all data in the dma-buf is - * coherent for all subsequent read access or that coherency is - * correctly handled by the underlying kernel-side dma-buf - * implementation. - * - * - Don't make any more attachments after sending the buffer to the - * compositor. Making more attachments later increases the risk of - * the compositor not being able to use (re-import) an existing - * dmabuf-based wl_buffer. - * - * The underlying graphics stack must ensure the following: - * - * - The dmabuf file descriptors relayed to the server will stay valid - * for the whole lifetime of the wl_buffer. This means the server may - * at any time use those fds to import the dmabuf into any kernel - * sub-system that might accept it. - * - * However, when the underlying graphics stack fails to deliver the - * promise, because of e.g. a device hot-unplug which raises internal - * errors, after the wl_buffer has been successfully created the - * compositor must not raise protocol errors to the client when dmabuf - * import later fails. - * - * To create a wl_buffer from one or more dmabufs, a client creates a - * zwp_linux_buffer_params_v1 object with a zwp_linux_dmabuf_v1.create_params - * request. All planes required by the intended format are added with - * the 'add' request. Finally, a 'create' or 'create_immed' request is - * issued, which has the following outcome depending on the import success. - * - * The 'create' request, - * - on success, triggers a 'created' event which provides the final - * wl_buffer to the client. - * - on failure, triggers a 'failed' event to convey that the server - * cannot use the dmabufs received from the client. - * - * For the 'create_immed' request, - * - on success, the server immediately imports the added dmabufs to - * create a wl_buffer. No event is sent from the server in this case. - * - on failure, the server can choose to either: - * - terminate the client by raising a fatal error. - * - mark the wl_buffer as failed, and send a 'failed' event to the - * client. If the client uses a failed wl_buffer as an argument to any - * request, the behaviour is compositor implementation-defined. - * - * For all DRM formats and unless specified in another protocol extension, - * pre-multiplied alpha is used for pixel values. - * - * Unless specified otherwise in another protocol extension, implicit - * synchronization is used. In other words, compositors and clients must - * wait and signal fences implicitly passed via the DMA-BUF's reservation - * mechanism. - */ extern const struct wl_interface zwp_linux_dmabuf_v1_interface; #endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_INTERFACE #define ZWP_LINUX_BUFFER_PARAMS_V1_INTERFACE -/** - * @page page_iface_zwp_linux_buffer_params_v1 zwp_linux_buffer_params_v1 - * @section page_iface_zwp_linux_buffer_params_v1_desc Description - * - * This temporary object is a collection of dmabufs and other - * parameters that together form a single logical buffer. The temporary - * object may eventually create one wl_buffer unless cancelled by - * destroying it before requesting 'create'. - * - * Single-planar formats only require one dmabuf, however - * multi-planar formats may require more than one dmabuf. For all - * formats, an 'add' request must be called once per plane (even if the - * underlying dmabuf fd is identical). - * - * You must use consecutive plane indices ('plane_idx' argument for 'add') - * from zero to the number of planes used by the drm_fourcc format code. - * All planes required by the format must be given exactly once, but can - * be given in any order. Each plane index can only be set once; subsequent - * calls with a plane index which has already been set will result in a - * plane_set error being generated. - * @section page_iface_zwp_linux_buffer_params_v1_api API - * See @ref iface_zwp_linux_buffer_params_v1. - */ -/** - * @defgroup iface_zwp_linux_buffer_params_v1 The zwp_linux_buffer_params_v1 interface - * - * This temporary object is a collection of dmabufs and other - * parameters that together form a single logical buffer. The temporary - * object may eventually create one wl_buffer unless cancelled by - * destroying it before requesting 'create'. - * - * Single-planar formats only require one dmabuf, however - * multi-planar formats may require more than one dmabuf. For all - * formats, an 'add' request must be called once per plane (even if the - * underlying dmabuf fd is identical). - * - * You must use consecutive plane indices ('plane_idx' argument for 'add') - * from zero to the number of planes used by the drm_fourcc format code. - * All planes required by the format must be given exactly once, but can - * be given in any order. Each plane index can only be set once; subsequent - * calls with a plane index which has already been set will result in a - * plane_set error being generated. - */ extern const struct wl_interface zwp_linux_buffer_params_v1_interface; #endif #ifndef ZWP_LINUX_DMABUF_FEEDBACK_V1_INTERFACE #define ZWP_LINUX_DMABUF_FEEDBACK_V1_INTERFACE -/** - * @page page_iface_zwp_linux_dmabuf_feedback_v1 zwp_linux_dmabuf_feedback_v1 - * @section page_iface_zwp_linux_dmabuf_feedback_v1_desc Description - * - * This object advertises dmabuf parameters feedback. This includes the - * preferred devices and the supported formats/modifiers. - * - * The parameters are sent once when this object is created and whenever they - * change. The done event is always sent once after all parameters have been - * sent. When a single parameter changes, all parameters are re-sent by the - * compositor. - * - * Compositors can re-send the parameters when the current client buffer - * allocations are sub-optimal. Compositors should not re-send the - * parameters if re-allocating the buffers would not result in a more optimal - * configuration. In particular, compositors should avoid sending the exact - * same parameters multiple times in a row. - * - * The tranche_target_device and tranche_formats events are grouped by - * tranches of preference. For each tranche, a tranche_target_device, one - * tranche_flags and one or more tranche_formats events are sent, followed - * by a tranche_done event finishing the list. The tranches are sent in - * descending order of preference. All formats and modifiers in the same - * tranche have the same preference. - * - * To send parameters, the compositor sends one main_device event (unless - * the client bound version 6 or above), tranches (each consisting of one - * tranche_target_device event, one tranche_flags event, tranche_formats - * events and then a tranche_done event), then one done event. - * - * With version 6 and above, the compositor must always advertise at least - * one tranche with the sampling flag set. - * - * The compositor sends a format_table event at least once for each dmabuf - * parameters feedback object, before any tranche events. Unless the format - * table contents change later, the compositor is free not to send another - * format_table event for the same dmabuf parameters feedback object. - * @section page_iface_zwp_linux_dmabuf_feedback_v1_api API - * See @ref iface_zwp_linux_dmabuf_feedback_v1. - */ -/** - * @defgroup iface_zwp_linux_dmabuf_feedback_v1 The zwp_linux_dmabuf_feedback_v1 interface - * - * This object advertises dmabuf parameters feedback. This includes the - * preferred devices and the supported formats/modifiers. - * - * The parameters are sent once when this object is created and whenever they - * change. The done event is always sent once after all parameters have been - * sent. When a single parameter changes, all parameters are re-sent by the - * compositor. - * - * Compositors can re-send the parameters when the current client buffer - * allocations are sub-optimal. Compositors should not re-send the - * parameters if re-allocating the buffers would not result in a more optimal - * configuration. In particular, compositors should avoid sending the exact - * same parameters multiple times in a row. - * - * The tranche_target_device and tranche_formats events are grouped by - * tranches of preference. For each tranche, a tranche_target_device, one - * tranche_flags and one or more tranche_formats events are sent, followed - * by a tranche_done event finishing the list. The tranches are sent in - * descending order of preference. All formats and modifiers in the same - * tranche have the same preference. - * - * To send parameters, the compositor sends one main_device event (unless - * the client bound version 6 or above), tranches (each consisting of one - * tranche_target_device event, one tranche_flags event, tranche_formats - * events and then a tranche_done event), then one done event. - * - * With version 6 and above, the compositor must always advertise at least - * one tranche with the sampling flag set. - * - * The compositor sends a format_table event at least once for each dmabuf - * parameters feedback object, before any tranche events. Unless the format - * table contents change later, the compositor is free not to send another - * format_table event for the same dmabuf parameters feedback object. - */ extern const struct wl_interface zwp_linux_dmabuf_feedback_v1_interface; #endif -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * @struct zwp_linux_dmabuf_v1_interface - */ struct zwp_linux_dmabuf_v1_interface { - /** - * unbind the factory - * - * Objects created through this interface, especially wl_buffers, - * will remain valid. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a temporary object for buffer parameters - * - * This temporary object is used to collect multiple dmabuf - * handles into a single batch to create a wl_buffer. It can only - * be used once and should be destroyed after a 'created' or - * 'failed' event has been received. - * @param params_id id for the newly created zwp_linux_buffer_params_v1 - */ void (*create_params)(struct wl_client *client, struct wl_resource *resource, uint32_t params_id); - /** - * get default feedback - * - * This request creates a new zwp_linux_dmabuf_feedback_v1 object - * not bound to a particular surface. This object will deliver - * feedback about dmabuf parameters to use if the client doesn't - * support per-surface feedback (see get_surface_feedback). - * @since 4 - */ void (*get_default_feedback)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * get feedback for a surface - * - * This request creates a new zwp_linux_dmabuf_feedback_v1 object - * for the specified wl_surface. This object will deliver feedback - * about dmabuf parameters to use for buffers attached to this - * surface. - * - * If the surface is destroyed before the - * zwp_linux_dmabuf_feedback_v1 object, the feedback object becomes - * inert. - * @since 4 - */ void (*get_surface_feedback)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -378,52 +81,20 @@ struct zwp_linux_dmabuf_v1_interface { #define ZWP_LINUX_DMABUF_V1_FORMAT 0 #define ZWP_LINUX_DMABUF_V1_MODIFIER 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_FORMAT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_MODIFIER_SINCE_VERSION 3 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_CREATE_PARAMS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_GET_DEFAULT_FEEDBACK_SINCE_VERSION 4 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_GET_SURFACE_FEEDBACK_SINCE_VERSION 4 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * Sends an format event to the client owning the resource. - * @param resource_ The client's resource - * @param format DRM_FORMAT code - */ static inline void zwp_linux_dmabuf_v1_send_format(struct wl_resource *resource_, uint32_t format) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_V1_FORMAT, format); } -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * Sends an modifier event to the client owning the resource. - * @param resource_ The client's resource - * @param format DRM_FORMAT code - * @param modifier_hi high 32 bits of layout modifier - * @param modifier_lo low 32 bits of layout modifier - */ static inline void zwp_linux_dmabuf_v1_send_modifier(struct wl_resource *resource_, uint32_t format, uint32_t modifier_hi, uint32_t modifier_lo) { @@ -433,54 +104,20 @@ zwp_linux_dmabuf_v1_send_modifier(struct wl_resource *resource_, uint32_t format #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM #define ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM enum zwp_linux_buffer_params_v1_error { - /** - * the zwp_linux_buffer_params_v1 object has already been used to create a wl_buffer - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ALREADY_USED = 0, - /** - * plane index out of bounds - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_IDX = 1, - /** - * the plane index was already set - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_SET = 2, - /** - * missing or too many planes to create a buffer - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INCOMPLETE = 3, - /** - * format not supported - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_FORMAT = 4, - /** - * invalid width or height - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_DIMENSIONS = 5, - /** - * offset + stride * height goes out of dmabuf bounds - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_OUT_OF_BOUNDS = 6, - /** - * invalid wl_buffer resulted from importing dmabufs via the create_immed request on given buffer_params - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_WL_BUFFER = 7, - /** - * an array with mismatching size for a dev_t was used - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_DEV_T_SIZE = 8, }; -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM */ +#endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM_IS_VALID #define ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * Validate a zwp_linux_buffer_params_v1 error value. - * - * @return true on success, false on error. - * @ref zwp_linux_buffer_params_v1_error - */ static inline bool zwp_linux_buffer_params_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -506,35 +143,19 @@ zwp_linux_buffer_params_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM_IS_VALID */ +#endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM #define ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM enum zwp_linux_buffer_params_v1_flags { - /** - * contents are y-inverted - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_Y_INVERT = 1, - /** - * content is interlaced - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_INTERLACED = 2, - /** - * bottom field first - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_BOTTOM_FIRST = 4, }; -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM */ +#endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM_IS_VALID #define ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM_IS_VALID -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * Validate a zwp_linux_buffer_params_v1 flags value. - * - * @return true on success, false on error. - * @ref zwp_linux_buffer_params_v1_flags - */ static inline bool zwp_linux_buffer_params_v1_flags_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -546,51 +167,11 @@ zwp_linux_buffer_params_v1_flags_is_valid(uint32_t value, uint32_t version) { valid |= ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_BOTTOM_FIRST; return (value & ~valid) == 0; } -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * @struct zwp_linux_buffer_params_v1_interface - */ struct zwp_linux_buffer_params_v1_interface { - /** - * delete this object, used or not - * - * Cleans up the temporary data sent to the server for - * dmabuf-based wl_buffer creation. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * add a dmabuf to the temporary set - * - * This request adds one dmabuf to the set in this - * zwp_linux_buffer_params_v1. - * - * The 64-bit unsigned value combined from modifier_hi and - * modifier_lo is the dmabuf layout modifier. DRM AddFB2 ioctl - * calls this the fb modifier, which is defined in drm_mode.h of - * Linux UAPI. This is an opaque token. Drivers use this token to - * express tiling, compression, etc. driver-specific modifications - * to the base format defined by the DRM fourcc code. - * - * Starting from version 4, the invalid_format protocol error is - * sent if the format + modifier pair was not advertised as - * supported. - * - * Starting from version 5, the invalid_format protocol error is - * sent if all planes don't use the same modifier. - * - * This request raises the PLANE_IDX error if plane_idx is too - * large. The error PLANE_SET is raised if attempting to set a - * plane that was already set. - * @param fd dmabuf fd - * @param plane_idx plane index - * @param offset offset in bytes - * @param stride stride in bytes - * @param modifier_hi high 32 bits of layout modifier - * @param modifier_lo low 32 bits of layout modifier - */ void (*add)(struct wl_client *client, struct wl_resource *resource, int32_t fd, @@ -599,112 +180,12 @@ struct zwp_linux_buffer_params_v1_interface { uint32_t stride, uint32_t modifier_hi, uint32_t modifier_lo); - /** - * create a wl_buffer from the given dmabufs - * - * This asks for creation of a wl_buffer from the added dmabuf - * buffers. The wl_buffer is not created immediately but returned - * via the 'created' event if the dmabuf sharing succeeds. The - * sharing may fail at runtime for reasons a client cannot predict, - * in which case the 'failed' event is triggered. - * - * The 'format' argument is a DRM_FORMAT code, as defined by the - * libdrm's drm_fourcc.h. The Linux kernel's DRM sub-system is the - * authoritative source on how the format codes should work. - * - * The 'flags' is a bitfield of the flags defined in enum "flags". - * 'y_invert' means that the image needs to be y-flipped. - * - * Flag 'interlaced' means that the frame in the buffer is not - * progressive as usual, but interlaced. An interlaced buffer as - * supported here must always contain both top and bottom fields. - * The top field always begins on the first pixel row. The temporal - * ordering between the two fields is top field first, unless - * 'bottom_first' is specified. It is undefined whether - * 'bottom_first' is ignored if 'interlaced' is not set. - * - * This protocol does not convey any information about field rate, - * duration, or timing, other than the relative ordering between - * the two fields in one buffer. A compositor may have to estimate - * the intended field rate from the incoming buffer rate. It is - * undefined whether the time of receiving wl_surface.commit with a - * new buffer attached, applying the wl_surface state, - * wl_surface.frame callback trigger, presentation, or any other - * point in the compositor cycle is used to measure the frame or - * field times. There is no support for detecting missed or late - * frames/fields/buffers either, and there is no support whatsoever - * for cooperating with interlaced compositor output. - * - * The composited image quality resulting from the use of - * interlaced buffers is explicitly undefined. A compositor may use - * elaborate hardware features or software to deinterlace and - * create progressive output frames from a sequence of interlaced - * input buffers, or it may produce substandard image quality. - * However, compositors that cannot guarantee reasonable image - * quality in all cases are recommended to just reject all - * interlaced buffers. - * - * Any argument errors, including non-positive width or height, - * mismatch between the number of planes and the format, bad - * format, bad offset or stride, may be indicated by fatal protocol - * errors: INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS, - * OUT_OF_BOUNDS. - * - * Dmabuf import errors in the server that are not obvious client - * bugs are returned via the 'failed' event as non-fatal. This - * allows attempting dmabuf sharing and falling back in the client - * if it fails. - * - * This request can be sent only once in the object's lifetime, - * after which the only legal request is destroy. This object - * should be destroyed after issuing a 'create' request. Attempting - * to use this object after issuing 'create' raises the - * ALREADY_USED protocol error. - * - * It is not mandatory to issue 'create'. If a client wants to - * cancel the buffer creation, it can just destroy this object. - * @param width base plane width in pixels - * @param height base plane height in pixels - * @param format DRM_FORMAT code - * @param flags see enum flags - */ void (*create)(struct wl_client *client, struct wl_resource *resource, int32_t width, int32_t height, uint32_t format, uint32_t flags); - /** - * immediately create a wl_buffer from the given dmabufs - * - * This asks for immediate creation of a wl_buffer by importing - * the added dmabufs. - * - * In case of import success, no event is sent from the server, and - * the wl_buffer is ready to be used by the client. - * - * Upon import failure, either of the following may happen, as seen - * fit by the implementation: - the client is terminated with one - * of the following fatal protocol errors: - INCOMPLETE, - * INVALID_FORMAT, INVALID_DIMENSIONS, OUT_OF_BOUNDS, in case of - * argument errors such as mismatch between the number of planes - * and the format, bad format, non-positive width or height, or bad - * offset or stride. - INVALID_WL_BUFFER, in case the cause for - * failure is unknown or platform specific. - the server creates an - * invalid wl_buffer, marks it as failed and sends a 'failed' event - * to the client. The result of using this invalid wl_buffer as an - * argument in any request by the client is defined by the - * compositor implementation. - * - * This takes the same arguments as a 'create' request, and obeys - * the same restrictions. - * @param buffer_id id for the newly created wl_buffer - * @param width base plane width in pixels - * @param height base plane height in pixels - * @param format DRM_FORMAT code - * @param flags see enum flags - * @since 2 - */ void (*create_immed)(struct wl_client *client, struct wl_resource *resource, uint32_t buffer_id, @@ -712,26 +193,6 @@ struct zwp_linux_buffer_params_v1_interface { int32_t height, uint32_t format, uint32_t flags); - /** - * set the target device of the wl_buffer - * - * Set the device the compositor should import the dmabufs to for - * sampling in the next create or create_immed request. - * - * To avoid race conditions when the compositor removes a device - * from the tranches, it is not a protocol error if the device - * hasn't been advertised by the compositor in a tranche with the - * sampling flag, but the import is likely to fail in that case. - * - * If the client doesn't know a suitable target device, it - * shouldn't set one, and the compositor should attempt import on - * all devices it supports. - * - * If the array is too small to contain a dev_t or larger than - * required, the invalid_dev_t_size error will be emitted. - * @param device device dev_t value - * @since 6 - */ void (*set_sampling_device)(struct wl_client *client, struct wl_resource *resource, struct wl_array *device); @@ -740,53 +201,21 @@ struct zwp_linux_buffer_params_v1_interface { #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATED 0 #define ZWP_LINUX_BUFFER_PARAMS_V1_FAILED 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_FAILED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_ADD_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_SET_SAMPLING_DEVICE_SINCE_VERSION 6 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * Sends an created event to the client owning the resource. - * @param resource_ The client's resource - * @param buffer id for the the newly created wl_buffer - */ static inline void zwp_linux_buffer_params_v1_send_created(struct wl_resource *resource_, struct wl_resource *buffer) { wl_resource_post_event(resource_, ZWP_LINUX_BUFFER_PARAMS_V1_CREATED, buffer); } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * Sends an failed event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_linux_buffer_params_v1_send_failed(struct wl_resource *resource_) { @@ -796,48 +225,15 @@ zwp_linux_buffer_params_v1_send_failed(struct wl_resource *resource_) #ifndef ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM enum zwp_linux_dmabuf_feedback_v1_tranche_flags { - /** - * direct scan-out tranche - * - * The scanout flag is a hint that direct scan-out may be - * attempted by the compositor on the target device if the client - * appropriately allocates a buffer. How to allocate a buffer that - * can be scanned out on the target device is - * implementation-defined. - * @since 4 - */ ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SCANOUT = 1, - /** - * sampling tranche - * - * The sampling flag describes that the compositor is able to - * efficiently sample from buffers imported to the target device if - * the client appropriately allocates a buffer. How to allocate a - * buffer that can be efficiently sampled on the target device is - * implementation defined. - * @since 6 - */ ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SAMPLING = 2, }; -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SCANOUT_SINCE_VERSION 4 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SAMPLING_SINCE_VERSION 6 -#endif /* ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM */ +#endif #ifndef ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM_IS_VALID #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM_IS_VALID -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Validate a zwp_linux_dmabuf_feedback_v1 tranche_flags value. - * - * @return true on success, false on error. - * @ref zwp_linux_dmabuf_feedback_v1_tranche_flags - */ static inline bool zwp_linux_dmabuf_feedback_v1_tranche_flags_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -847,19 +243,9 @@ zwp_linux_dmabuf_feedback_v1_tranche_flags_is_valid(uint32_t value, uint32_t ver valid |= ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SAMPLING; return (value & ~valid) == 0; } -#endif /* ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * @struct zwp_linux_dmabuf_feedback_v1_interface - */ struct zwp_linux_dmabuf_feedback_v1_interface { - /** - * destroy the feedback object - * - * Using this request a client can tell the server that it is not - * going to use the zwp_linux_dmabuf_feedback_v1 object anymore. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; @@ -872,117 +258,52 @@ struct zwp_linux_dmabuf_feedback_v1_interface { #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FORMATS 5 #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS 6 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_FORMAT_TABLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_MAIN_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_TARGET_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FORMATS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_linux_dmabuf_feedback_v1_send_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_DONE); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an format_table event to the client owning the resource. - * @param resource_ The client's resource - * @param fd table file descriptor - * @param size table size, in bytes - */ static inline void zwp_linux_dmabuf_feedback_v1_send_format_table(struct wl_resource *resource_, int32_t fd, uint32_t size) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_FORMAT_TABLE, fd, size); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an main_device event to the client owning the resource. - * @param resource_ The client's resource - * @param device device dev_t value - */ static inline void zwp_linux_dmabuf_feedback_v1_send_main_device(struct wl_resource *resource_, struct wl_array *device) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_MAIN_DEVICE, device); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an tranche_done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_linux_dmabuf_feedback_v1_send_tranche_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_DONE); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an tranche_target_device event to the client owning the resource. - * @param resource_ The client's resource - * @param device device dev_t value - */ static inline void zwp_linux_dmabuf_feedback_v1_send_tranche_target_device(struct wl_resource *resource_, struct wl_array *device) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_TARGET_DEVICE, device); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an tranche_formats event to the client owning the resource. - * @param resource_ The client's resource - * @param indices array of 16-bit indexes - */ static inline void zwp_linux_dmabuf_feedback_v1_send_tranche_formats(struct wl_resource *resource_, struct wl_array *indices) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FORMATS, indices); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an tranche_flags event to the client owning the resource. - * @param resource_ The client's resource - * @param flags tranche flags - */ static inline void zwp_linux_dmabuf_feedback_v1_send_tranche_flags(struct wl_resource *resource_, uint32_t flags) { diff --git a/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-protocol.c index 89ea811ba..8a697736a 100644 --- a/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2014 Jonas Ådahl * Copyright © 2015 Red Hat Inc. @@ -30,7 +28,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-server-protocol.h index 8aeb92141..599797e8a 100644 --- a/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef POINTER_CONSTRAINTS_UNSTABLE_V1_SERVER_PROTOCOL_H #define POINTER_CONSTRAINTS_UNSTABLE_V1_SERVER_PROTOCOL_H @@ -14,37 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_pointer_constraints_unstable_v1 The pointer_constraints_unstable_v1 protocol - * protocol for constraining pointer motions - * - * @section page_desc_pointer_constraints_unstable_v1 Description - * - * This protocol specifies a set of interfaces used for adding constraints to - * the motion of a pointer. Possible constraints include confining pointer - * motions to a given region, or locking it to its current position. - * - * In order to constrain the pointer, a client must first bind the global - * interface "wp_pointer_constraints" which, if a compositor supports pointer - * constraints, is exposed by the registry. Using the bound global object, the - * client uses the request that corresponds to the type of constraint it wants - * to make. See wp_pointer_constraints for more details. - * - * Warning! The protocol described in this file is experimental and backward - * incompatible changes may be made. Backward compatible changes may be added - * together with the corresponding interface version bump. Backward - * incompatible changes are done by bumping the version number in the protocol - * and interface names and resetting the interface version. Once the protocol - * is to be declared stable, the 'z' prefix and the version number in the - * protocol and interface names are removed and the interface version number is - * reset. - * - * @section page_ifaces_pointer_constraints_unstable_v1 Interfaces - * - @subpage page_iface_zwp_pointer_constraints_v1 - constrain the movement of a pointer - * - @subpage page_iface_zwp_locked_pointer_v1 - receive relative pointer motion events - * - @subpage page_iface_zwp_confined_pointer_v1 - confined pointer object +/* * @section page_copyright_pointer_constraints_unstable_v1 Copyright - *
  *
  * Copyright © 2014      Jonas Ådahl
  * Copyright © 2015      Red Hat Inc.
@@ -67,8 +37,37 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_pointer; struct wl_region; struct wl_surface; @@ -78,180 +77,26 @@ struct zwp_pointer_constraints_v1; #ifndef ZWP_POINTER_CONSTRAINTS_V1_INTERFACE #define ZWP_POINTER_CONSTRAINTS_V1_INTERFACE -/** - * @page page_iface_zwp_pointer_constraints_v1 zwp_pointer_constraints_v1 - * @section page_iface_zwp_pointer_constraints_v1_desc Description - * - * The global interface exposing pointer constraining functionality. It - * exposes two requests: lock_pointer for locking the pointer to its - * position, and confine_pointer for locking the pointer to a region. - * - * The lock_pointer and confine_pointer requests create the objects - * wp_locked_pointer and wp_confined_pointer respectively, and the client can - * use these objects to interact with the lock. - * - * For any surface, only one lock or confinement may be active across all - * wl_pointer objects of the same seat. If a lock or confinement is requested - * when another lock or confinement is active or requested on the same surface - * and with any of the wl_pointer objects of the same seat, an - * 'already_constrained' error will be raised. - * @section page_iface_zwp_pointer_constraints_v1_api API - * See @ref iface_zwp_pointer_constraints_v1. - */ -/** - * @defgroup iface_zwp_pointer_constraints_v1 The zwp_pointer_constraints_v1 interface - * - * The global interface exposing pointer constraining functionality. It - * exposes two requests: lock_pointer for locking the pointer to its - * position, and confine_pointer for locking the pointer to a region. - * - * The lock_pointer and confine_pointer requests create the objects - * wp_locked_pointer and wp_confined_pointer respectively, and the client can - * use these objects to interact with the lock. - * - * For any surface, only one lock or confinement may be active across all - * wl_pointer objects of the same seat. If a lock or confinement is requested - * when another lock or confinement is active or requested on the same surface - * and with any of the wl_pointer objects of the same seat, an - * 'already_constrained' error will be raised. - */ extern const struct wl_interface zwp_pointer_constraints_v1_interface; #endif #ifndef ZWP_LOCKED_POINTER_V1_INTERFACE #define ZWP_LOCKED_POINTER_V1_INTERFACE -/** - * @page page_iface_zwp_locked_pointer_v1 zwp_locked_pointer_v1 - * @section page_iface_zwp_locked_pointer_v1_desc Description - * - * The wp_locked_pointer interface represents a locked pointer state. - * - * While the lock of this object is active, the wl_pointer objects of the - * associated seat will not emit any wl_pointer.motion events. - * - * This object will send the event 'locked' when the lock is activated. - * Whenever the lock is activated, it is guaranteed that the locked surface - * will already have received pointer focus and that the pointer will be - * within the region passed to the request creating this object. - * - * To unlock the pointer, send the destroy request. This will also destroy - * the wp_locked_pointer object. - * - * If the compositor decides to unlock the pointer the unlocked event is - * sent. See wp_locked_pointer.unlock for details. - * - * When unlocking, the compositor may warp the cursor position to the set - * cursor position hint. If it does, it will not result in any relative - * motion events emitted via wp_relative_pointer. - * - * If the surface the lock was requested on is destroyed and the lock is not - * yet activated, the wp_locked_pointer object is now defunct and must be - * destroyed. - * @section page_iface_zwp_locked_pointer_v1_api API - * See @ref iface_zwp_locked_pointer_v1. - */ -/** - * @defgroup iface_zwp_locked_pointer_v1 The zwp_locked_pointer_v1 interface - * - * The wp_locked_pointer interface represents a locked pointer state. - * - * While the lock of this object is active, the wl_pointer objects of the - * associated seat will not emit any wl_pointer.motion events. - * - * This object will send the event 'locked' when the lock is activated. - * Whenever the lock is activated, it is guaranteed that the locked surface - * will already have received pointer focus and that the pointer will be - * within the region passed to the request creating this object. - * - * To unlock the pointer, send the destroy request. This will also destroy - * the wp_locked_pointer object. - * - * If the compositor decides to unlock the pointer the unlocked event is - * sent. See wp_locked_pointer.unlock for details. - * - * When unlocking, the compositor may warp the cursor position to the set - * cursor position hint. If it does, it will not result in any relative - * motion events emitted via wp_relative_pointer. - * - * If the surface the lock was requested on is destroyed and the lock is not - * yet activated, the wp_locked_pointer object is now defunct and must be - * destroyed. - */ extern const struct wl_interface zwp_locked_pointer_v1_interface; #endif #ifndef ZWP_CONFINED_POINTER_V1_INTERFACE #define ZWP_CONFINED_POINTER_V1_INTERFACE -/** - * @page page_iface_zwp_confined_pointer_v1 zwp_confined_pointer_v1 - * @section page_iface_zwp_confined_pointer_v1_desc Description - * - * The wp_confined_pointer interface represents a confined pointer state. - * - * This object will send the event 'confined' when the confinement is - * activated. Whenever the confinement is activated, it is guaranteed that - * the surface the pointer is confined to will already have received pointer - * focus and that the pointer will be within the region passed to the request - * creating this object. It is up to the compositor to decide whether this - * requires some user interaction and if the pointer will warp to within the - * passed region if outside. - * - * To unconfine the pointer, send the destroy request. This will also destroy - * the wp_confined_pointer object. - * - * If the compositor decides to unconfine the pointer the unconfined event is - * sent. The wp_confined_pointer object is at this point defunct and should - * be destroyed. - * @section page_iface_zwp_confined_pointer_v1_api API - * See @ref iface_zwp_confined_pointer_v1. - */ -/** - * @defgroup iface_zwp_confined_pointer_v1 The zwp_confined_pointer_v1 interface - * - * The wp_confined_pointer interface represents a confined pointer state. - * - * This object will send the event 'confined' when the confinement is - * activated. Whenever the confinement is activated, it is guaranteed that - * the surface the pointer is confined to will already have received pointer - * focus and that the pointer will be within the region passed to the request - * creating this object. It is up to the compositor to decide whether this - * requires some user interaction and if the pointer will warp to within the - * passed region if outside. - * - * To unconfine the pointer, send the destroy request. This will also destroy - * the wp_confined_pointer object. - * - * If the compositor decides to unconfine the pointer the unconfined event is - * sent. The wp_confined_pointer object is at this point defunct and should - * be destroyed. - */ extern const struct wl_interface zwp_confined_pointer_v1_interface; #endif #ifndef ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM #define ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM -/** - * @ingroup iface_zwp_pointer_constraints_v1 - * wp_pointer_constraints error values - * - * These errors can be emitted in response to wp_pointer_constraints - * requests. - */ enum zwp_pointer_constraints_v1_error { - /** - * pointer constraint already requested on that surface - */ ZWP_POINTER_CONSTRAINTS_V1_ERROR_ALREADY_CONSTRAINED = 1, }; -#endif /* ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM */ +#endif #ifndef ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM_IS_VALID #define ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_zwp_pointer_constraints_v1 - * Validate a zwp_pointer_constraints_v1 error value. - * - * @return true on success, false on error. - * @ref zwp_pointer_constraints_v1_error - */ static inline bool zwp_pointer_constraints_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -261,49 +106,18 @@ zwp_pointer_constraints_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM_IS_VALID */ +#endif #ifndef ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM #define ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM -/** - * @ingroup iface_zwp_pointer_constraints_v1 - * constraint lifetime - * - * These values represent different lifetime semantics. They are passed - * as arguments to the factory requests to specify how the constraint - * lifetimes should be managed. - */ enum zwp_pointer_constraints_v1_lifetime { - /** - * the pointer constraint is defunct once deactivated - * - * A oneshot pointer constraint will never reactivate once it has - * been deactivated. See the corresponding deactivation event - * (wp_locked_pointer.unlocked and wp_confined_pointer.unconfined) - * for details. - */ ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT = 1, - /** - * the pointer constraint may reactivate - * - * A persistent pointer constraint may again reactivate once it - * has been deactivated. See the corresponding deactivation event - * (wp_locked_pointer.unlocked and wp_confined_pointer.unconfined) - * for details. - */ ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT = 2, }; -#endif /* ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM */ +#endif #ifndef ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM_IS_VALID #define ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM_IS_VALID -/** - * @ingroup iface_zwp_pointer_constraints_v1 - * Validate a zwp_pointer_constraints_v1 lifetime value. - * - * @return true on success, false on error. - * @ref zwp_pointer_constraints_v1_lifetime - */ static inline bool zwp_pointer_constraints_v1_lifetime_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -315,67 +129,11 @@ zwp_pointer_constraints_v1_lifetime_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwp_pointer_constraints_v1 - * @struct zwp_pointer_constraints_v1_interface - */ struct zwp_pointer_constraints_v1_interface { - /** - * destroy the pointer constraints manager object - * - * Used by the client to notify the server that it will no longer - * use this pointer constraints object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * lock pointer to a position - * - * The lock_pointer request lets the client request to disable - * movements of the virtual pointer (i.e. the cursor), effectively - * locking the pointer to a position. This request may not take - * effect immediately; in the future, when the compositor deems - * implementation-specific constraints are satisfied, the pointer - * lock will be activated and the compositor sends a locked event. - * - * The protocol provides no guarantee that the constraints are ever - * satisfied, and does not require the compositor to send an error - * if the constraints cannot ever be satisfied. It is thus possible - * to request a lock that will never activate. - * - * There may not be another pointer constraint of any kind - * requested or active on the surface for any of the wl_pointer - * objects of the seat of the passed pointer when requesting a - * lock. If there is, an error will be raised. See general pointer - * lock documentation for more details. - * - * The intersection of the region passed with this request and the - * input region of the surface is used to determine where the - * pointer must be in order for the lock to activate. It is up to - * the compositor whether to warp the pointer or require some kind - * of user interaction for the lock to activate. If the region is - * null the surface input region is used. - * - * A surface may receive pointer focus without the lock being - * activated. - * - * The request creates a new object wp_locked_pointer which is used - * to interact with the lock as well as receive updates about its - * state. See the the description of wp_locked_pointer for further - * information. - * - * Note that while a pointer is locked, the wl_pointer objects of - * the corresponding seat will not emit any wl_pointer.motion - * events, but relative motion events will still be emitted via - * wp_relative_pointer objects of the same seat. wl_pointer.axis - * and wl_pointer.button events are unaffected. - * @param surface surface to lock pointer to - * @param pointer the pointer that should be locked - * @param region region of surface - * @param lifetime lock lifetime - */ void (*lock_pointer)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -383,32 +141,6 @@ struct zwp_pointer_constraints_v1_interface { struct wl_resource *pointer, struct wl_resource *region, uint32_t lifetime); - /** - * confine pointer to a region - * - * The confine_pointer request lets the client request to confine - * the pointer cursor to a given region. This request may not take - * effect immediately; in the future, when the compositor deems - * implementation- specific constraints are satisfied, the pointer - * confinement will be activated and the compositor sends a - * confined event. - * - * The intersection of the region passed with this request and the - * input region of the surface is used to determine where the - * pointer must be in order for the confinement to activate. It is - * up to the compositor whether to warp the pointer or require some - * kind of user interaction for the confinement to activate. If the - * region is null the surface input region is used. - * - * The request will create a new object wp_confined_pointer which - * is used to interact with the confinement as well as receive - * updates about its state. See the the description of - * wp_confined_pointer for further information. - * @param surface surface to lock pointer to - * @param pointer the pointer that should be confined - * @param region region of surface - * @param lifetime confinement lifetime - */ void (*confine_pointer)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -419,65 +151,17 @@ struct zwp_pointer_constraints_v1_interface { }; -/** - * @ingroup iface_zwp_pointer_constraints_v1 - */ #define ZWP_POINTER_CONSTRAINTS_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_pointer_constraints_v1 - */ #define ZWP_POINTER_CONSTRAINTS_V1_LOCK_POINTER_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_pointer_constraints_v1 - */ #define ZWP_POINTER_CONSTRAINTS_V1_CONFINE_POINTER_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - * @struct zwp_locked_pointer_v1_interface - */ struct zwp_locked_pointer_v1_interface { - /** - * destroy the locked pointer object - * - * Destroy the locked pointer object. If applicable, the - * compositor will unlock the pointer. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the pointer cursor position hint - * - * Set the cursor position hint relative to the top left corner - * of the surface. - * - * If the client is drawing its own cursor, it should update the - * position hint to the position of its own cursor. A compositor - * may use this information to warp the pointer upon unlock in - * order to avoid pointer jumps. - * - * The cursor position hint is double buffered. The new hint will - * only take effect when the associated surface gets it pending - * state applied. See wl_surface.commit for details. - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ void (*set_cursor_position_hint)(struct wl_client *client, struct wl_resource *resource, wl_fixed_t surface_x, wl_fixed_t surface_y); - /** - * set a new lock region - * - * Set a new region used to lock the pointer. - * - * The new lock region is double-buffered. The new lock region will - * only take effect when the associated surface gets its pending - * state applied. See wl_surface.commit for details. - * - * For details about the lock region, see wp_locked_pointer. - * @param region region of surface - */ void (*set_region)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *region); @@ -486,84 +170,28 @@ struct zwp_locked_pointer_v1_interface { #define ZWP_LOCKED_POINTER_V1_LOCKED 0 #define ZWP_LOCKED_POINTER_V1_UNLOCKED 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - */ #define ZWP_LOCKED_POINTER_V1_LOCKED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - */ #define ZWP_LOCKED_POINTER_V1_UNLOCKED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - */ #define ZWP_LOCKED_POINTER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - */ #define ZWP_LOCKED_POINTER_V1_SET_CURSOR_POSITION_HINT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - */ #define ZWP_LOCKED_POINTER_V1_SET_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - * Sends an locked event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_locked_pointer_v1_send_locked(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWP_LOCKED_POINTER_V1_LOCKED); } -/** - * @ingroup iface_zwp_locked_pointer_v1 - * Sends an unlocked event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_locked_pointer_v1_send_unlocked(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWP_LOCKED_POINTER_V1_UNLOCKED); } -/** - * @ingroup iface_zwp_confined_pointer_v1 - * @struct zwp_confined_pointer_v1_interface - */ struct zwp_confined_pointer_v1_interface { - /** - * destroy the confined pointer object - * - * Destroy the confined pointer object. If applicable, the - * compositor will unconfine the pointer. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set a new confine region - * - * Set a new region used to confine the pointer. - * - * The new confine region is double-buffered. The new confine - * region will only take effect when the associated surface gets - * its pending state applied. See wl_surface.commit for details. - * - * If the confinement is active when the new confinement region is - * applied and the pointer ends up outside of newly applied region, - * the pointer may warped to a position within the new confinement - * region. If warped, a wl_pointer.motion event will be emitted, - * but no wp_relative_pointer.relative_motion event. - * - * The compositor may also, instead of using the new region, - * unconfine the pointer. - * - * For details about the confine region, see wp_confined_pointer. - * @param region region of surface - */ void (*set_region)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *region); @@ -572,40 +200,18 @@ struct zwp_confined_pointer_v1_interface { #define ZWP_CONFINED_POINTER_V1_CONFINED 0 #define ZWP_CONFINED_POINTER_V1_UNCONFINED 1 -/** - * @ingroup iface_zwp_confined_pointer_v1 - */ #define ZWP_CONFINED_POINTER_V1_CONFINED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_confined_pointer_v1 - */ #define ZWP_CONFINED_POINTER_V1_UNCONFINED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_confined_pointer_v1 - */ #define ZWP_CONFINED_POINTER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_confined_pointer_v1 - */ #define ZWP_CONFINED_POINTER_V1_SET_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_confined_pointer_v1 - * Sends an confined event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_confined_pointer_v1_send_confined(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWP_CONFINED_POINTER_V1_CONFINED); } -/** - * @ingroup iface_zwp_confined_pointer_v1 - * Sends an unconfined event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_confined_pointer_v1_send_unconfined(struct wl_resource *resource_) { diff --git a/app/src/main/cpp/waylandcomp/generated/presentation-time-protocol.c b/app/src/main/cpp/waylandcomp/generated/presentation-time-protocol.c index 63eea910d..8db001733 100644 --- a/app/src/main/cpp/waylandcomp/generated/presentation-time-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/presentation-time-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2013-2014 Collabora, Ltd. * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/presentation-time-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/presentation-time-server-protocol.h index 1e1a39c07..828051726 100644 --- a/app/src/main/cpp/waylandcomp/generated/presentation-time-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/presentation-time-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef PRESENTATION_TIME_SERVER_PROTOCOL_H #define PRESENTATION_TIME_SERVER_PROTOCOL_H @@ -14,13 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_presentation_time The presentation_time protocol - * @section page_ifaces_presentation_time Interfaces - * - @subpage page_iface_wp_presentation - timed presentation related wl_surface requests - * - @subpage page_iface_wp_presentation_feedback - presentation time feedback event +/* * @section page_copyright_presentation_time Copyright - *
  *
  * Copyright © 2013-2014 Collabora, Ltd.
  *
@@ -42,8 +36,13 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + struct wl_output; struct wl_surface; struct wp_presentation; @@ -51,131 +50,23 @@ struct wp_presentation_feedback; #ifndef WP_PRESENTATION_INTERFACE #define WP_PRESENTATION_INTERFACE -/** - * @page page_iface_wp_presentation wp_presentation - * @section page_iface_wp_presentation_desc Description - * - * - * - * - * The main feature of this interface is accurate presentation - * timing feedback to ensure smooth video playback while maintaining - * audio/video synchronization. Some features use the concept of a - * presentation clock, which is defined in the - * presentation.clock_id event. - * - * A content update for a wl_surface is submitted by a - * wl_surface.commit request. Request 'feedback' associates with - * the wl_surface.commit and provides feedback on the content - * update, particularly the final realized presentation time. - * - * - * - * When the final realized presentation time is available, e.g. - * after a framebuffer flip completes, the requested - * presentation_feedback.presented events are sent. The final - * presentation time can differ from the compositor's predicted - * display update time and the update's target time, especially - * when the compositor misses its target vertical blanking period. - * @section page_iface_wp_presentation_api API - * See @ref iface_wp_presentation. - */ -/** - * @defgroup iface_wp_presentation The wp_presentation interface - * - * - * - * - * The main feature of this interface is accurate presentation - * timing feedback to ensure smooth video playback while maintaining - * audio/video synchronization. Some features use the concept of a - * presentation clock, which is defined in the - * presentation.clock_id event. - * - * A content update for a wl_surface is submitted by a - * wl_surface.commit request. Request 'feedback' associates with - * the wl_surface.commit and provides feedback on the content - * update, particularly the final realized presentation time. - * - * - * - * When the final realized presentation time is available, e.g. - * after a framebuffer flip completes, the requested - * presentation_feedback.presented events are sent. The final - * presentation time can differ from the compositor's predicted - * display update time and the update's target time, especially - * when the compositor misses its target vertical blanking period. - */ extern const struct wl_interface wp_presentation_interface; #endif #ifndef WP_PRESENTATION_FEEDBACK_INTERFACE #define WP_PRESENTATION_FEEDBACK_INTERFACE -/** - * @page page_iface_wp_presentation_feedback wp_presentation_feedback - * @section page_iface_wp_presentation_feedback_desc Description - * - * A presentation_feedback object returns an indication that a - * wl_surface content update has become visible to the user. - * One object corresponds to one content update submission - * (wl_surface.commit). There are two possible outcomes: the - * content update is presented to the user, and a presentation - * timestamp delivered; or, the user did not see the content - * update because it was superseded or its surface destroyed, - * and the content update is discarded. - * - * Once a presentation_feedback object has delivered a 'presented' - * or 'discarded' event it is automatically destroyed. - * @section page_iface_wp_presentation_feedback_api API - * See @ref iface_wp_presentation_feedback. - */ -/** - * @defgroup iface_wp_presentation_feedback The wp_presentation_feedback interface - * - * A presentation_feedback object returns an indication that a - * wl_surface content update has become visible to the user. - * One object corresponds to one content update submission - * (wl_surface.commit). There are two possible outcomes: the - * content update is presented to the user, and a presentation - * timestamp delivered; or, the user did not see the content - * update because it was superseded or its surface destroyed, - * and the content update is discarded. - * - * Once a presentation_feedback object has delivered a 'presented' - * or 'discarded' event it is automatically destroyed. - */ extern const struct wl_interface wp_presentation_feedback_interface; #endif #ifndef WP_PRESENTATION_ERROR_ENUM #define WP_PRESENTATION_ERROR_ENUM -/** - * @ingroup iface_wp_presentation - * fatal presentation errors - * - * These fatal protocol errors may be emitted in response to - * illegal presentation requests. - */ enum wp_presentation_error { - /** - * invalid value in tv_nsec - */ WP_PRESENTATION_ERROR_INVALID_TIMESTAMP = 0, - /** - * invalid flag - */ WP_PRESENTATION_ERROR_INVALID_FLAG = 1, }; -#endif /* WP_PRESENTATION_ERROR_ENUM */ +#endif #ifndef WP_PRESENTATION_ERROR_ENUM_IS_VALID #define WP_PRESENTATION_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_presentation - * Validate a wp_presentation error value. - * - * @return true on success, false on error. - * @ref wp_presentation_error - */ static inline bool wp_presentation_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -187,37 +78,11 @@ wp_presentation_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_PRESENTATION_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_presentation - * @struct wp_presentation_interface - */ struct wp_presentation_interface { - /** - * unbind from the presentation interface - * - * Informs the server that the client will no longer be using - * this protocol object. Existing objects created by this object - * are not affected. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * request presentation feedback information - * - * Request presentation feedback for the current content - * submission on the given surface. This creates a new - * presentation_feedback object, which will deliver the feedback - * information once. If multiple presentation_feedback objects are - * created for the same submission, they will all deliver the same - * information. - * - * For details on what information is returned, see the - * presentation_feedback interface. - * @param surface target surface - * @param callback new feedback object - */ void (*feedback)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface, @@ -226,26 +91,11 @@ struct wp_presentation_interface { #define WP_PRESENTATION_CLOCK_ID 0 -/** - * @ingroup iface_wp_presentation - */ #define WP_PRESENTATION_CLOCK_ID_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation - */ #define WP_PRESENTATION_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation - */ #define WP_PRESENTATION_FEEDBACK_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation - * Sends an clock_id event to the client owning the resource. - * @param resource_ The client's resource - * @param clk_id platform clock identifier - */ static inline void wp_presentation_send_clock_id(struct wl_resource *resource_, uint32_t clk_id) { @@ -254,66 +104,16 @@ wp_presentation_send_clock_id(struct wl_resource *resource_, uint32_t clk_id) #ifndef WP_PRESENTATION_FEEDBACK_KIND_ENUM #define WP_PRESENTATION_FEEDBACK_KIND_ENUM -/** - * @ingroup iface_wp_presentation_feedback - * bitmask of flags in presented event - * - * These flags provide information about how the presentation of - * the related content update was done. The intent is to help - * clients assess the reliability of the feedback and the visual - * quality with respect to possible tearing and timings. - */ enum wp_presentation_feedback_kind { - /** - * presentation was vsync'd - * - * The presentation was synchronized to the "vertical retrace" by - * the display hardware such that tearing does not happen. Relying - * on software scheduling is not acceptable for this flag. If - * presentation is done by a copy to the active frontbuffer, then - * it must guarantee that tearing cannot happen. - */ WP_PRESENTATION_FEEDBACK_KIND_VSYNC = 0x1, - /** - * hardware provided the presentation timestamp - * - * The display hardware provided measurements that the hardware - * driver converted into a presentation timestamp. Sampling a clock - * in software is not acceptable for this flag. - */ WP_PRESENTATION_FEEDBACK_KIND_HW_CLOCK = 0x2, - /** - * hardware signalled the start of the presentation - * - * The display hardware signalled that it started using the new - * image content. The opposite of this is e.g. a timer being used - * to guess when the display hardware has switched to the new image - * content. - */ WP_PRESENTATION_FEEDBACK_KIND_HW_COMPLETION = 0x4, - /** - * presentation was done zero-copy - * - * The presentation of this update was done zero-copy. This means - * the buffer from the client was given to display hardware as is, - * without copying it. Compositing with OpenGL counts as copying, - * even if textured directly from the client buffer. Possible - * zero-copy cases include direct scanout of a fullscreen surface - * and a surface on a hardware overlay. - */ WP_PRESENTATION_FEEDBACK_KIND_ZERO_COPY = 0x8, }; -#endif /* WP_PRESENTATION_FEEDBACK_KIND_ENUM */ +#endif #ifndef WP_PRESENTATION_FEEDBACK_KIND_ENUM_IS_VALID #define WP_PRESENTATION_FEEDBACK_KIND_ENUM_IS_VALID -/** - * @ingroup iface_wp_presentation_feedback - * Validate a wp_presentation_feedback kind value. - * - * @return true on success, false on error. - * @ref wp_presentation_feedback_kind - */ static inline bool wp_presentation_feedback_kind_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -327,61 +127,29 @@ wp_presentation_feedback_kind_is_valid(uint32_t value, uint32_t version) { valid |= WP_PRESENTATION_FEEDBACK_KIND_ZERO_COPY; return (value & ~valid) == 0; } -#endif /* WP_PRESENTATION_FEEDBACK_KIND_ENUM_IS_VALID */ +#endif #define WP_PRESENTATION_FEEDBACK_SYNC_OUTPUT 0 #define WP_PRESENTATION_FEEDBACK_PRESENTED 1 #define WP_PRESENTATION_FEEDBACK_DISCARDED 2 -/** - * @ingroup iface_wp_presentation_feedback - */ #define WP_PRESENTATION_FEEDBACK_SYNC_OUTPUT_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation_feedback - */ #define WP_PRESENTATION_FEEDBACK_PRESENTED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation_feedback - */ #define WP_PRESENTATION_FEEDBACK_DISCARDED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation_feedback - * Sends an sync_output event to the client owning the resource. - * @param resource_ The client's resource - * @param output presentation output - */ static inline void wp_presentation_feedback_send_sync_output(struct wl_resource *resource_, struct wl_resource *output) { wl_resource_post_event(resource_, WP_PRESENTATION_FEEDBACK_SYNC_OUTPUT, output); } -/** - * @ingroup iface_wp_presentation_feedback - * Sends an presented event to the client owning the resource. - * @param resource_ The client's resource - * @param tv_sec_hi high 32 bits of the seconds part of the presentation timestamp - * @param tv_sec_lo low 32 bits of the seconds part of the presentation timestamp - * @param tv_nsec nanoseconds part of the presentation timestamp - * @param refresh nanoseconds till next refresh - * @param seq_hi high 32 bits of refresh counter - * @param seq_lo low 32 bits of refresh counter - * @param flags combination of 'kind' values - */ static inline void wp_presentation_feedback_send_presented(struct wl_resource *resource_, uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec, uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, uint32_t flags) { wl_resource_post_event(resource_, WP_PRESENTATION_FEEDBACK_PRESENTED, tv_sec_hi, tv_sec_lo, tv_nsec, refresh, seq_hi, seq_lo, flags); } -/** - * @ingroup iface_wp_presentation_feedback - * Sends an discarded event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wp_presentation_feedback_send_discarded(struct wl_resource *resource_) { diff --git a/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-protocol.c index 454f95556..eb9254f31 100644 --- a/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2014 Jonas Ådahl * Copyright © 2015 Red Hat Inc. @@ -30,7 +28,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-server-protocol.h index f40c97498..a25e2b18c 100644 --- a/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef RELATIVE_POINTER_UNSTABLE_V1_SERVER_PROTOCOL_H #define RELATIVE_POINTER_UNSTABLE_V1_SERVER_PROTOCOL_H @@ -14,40 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_relative_pointer_unstable_v1 The relative_pointer_unstable_v1 protocol - * protocol for relative pointer motion events - * - * @section page_desc_relative_pointer_unstable_v1 Description - * - * This protocol specifies a set of interfaces used for making clients able to - * receive relative pointer events not obstructed by barriers (such as the - * monitor edge or other pointer barriers). - * - * To start receiving relative pointer events, a client must first bind the - * global interface "wp_relative_pointer_manager" which, if a compositor - * supports relative pointer motion events, is exposed by the registry. After - * having created the relative pointer manager proxy object, the client uses - * it to create the actual relative pointer object using the - * "get_relative_pointer" request given a wl_pointer. The relative pointer - * motion events will then, when applicable, be transmitted via the proxy of - * the newly created relative pointer object. See the documentation of the - * relative pointer interface for more details. - * - * Warning! The protocol described in this file is experimental and backward - * incompatible changes may be made. Backward compatible changes may be added - * together with the corresponding interface version bump. Backward - * incompatible changes are done by bumping the version number in the protocol - * and interface names and resetting the interface version. Once the protocol - * is to be declared stable, the 'z' prefix and the version number in the - * protocol and interface names are removed and the interface version number is - * reset. - * - * @section page_ifaces_relative_pointer_unstable_v1 Interfaces - * - @subpage page_iface_zwp_relative_pointer_manager_v1 - get relative pointer objects - * - @subpage page_iface_zwp_relative_pointer_v1 - relative pointer object +/* * @section page_copyright_relative_pointer_unstable_v1 Copyright - *
  *
  * Copyright © 2014      Jonas Ådahl
  * Copyright © 2015      Red Hat Inc.
@@ -70,74 +37,56 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_pointer; struct zwp_relative_pointer_manager_v1; struct zwp_relative_pointer_v1; #ifndef ZWP_RELATIVE_POINTER_MANAGER_V1_INTERFACE #define ZWP_RELATIVE_POINTER_MANAGER_V1_INTERFACE -/** - * @page page_iface_zwp_relative_pointer_manager_v1 zwp_relative_pointer_manager_v1 - * @section page_iface_zwp_relative_pointer_manager_v1_desc Description - * - * A global interface used for getting the relative pointer object for a - * given pointer. - * @section page_iface_zwp_relative_pointer_manager_v1_api API - * See @ref iface_zwp_relative_pointer_manager_v1. - */ -/** - * @defgroup iface_zwp_relative_pointer_manager_v1 The zwp_relative_pointer_manager_v1 interface - * - * A global interface used for getting the relative pointer object for a - * given pointer. - */ extern const struct wl_interface zwp_relative_pointer_manager_v1_interface; #endif #ifndef ZWP_RELATIVE_POINTER_V1_INTERFACE #define ZWP_RELATIVE_POINTER_V1_INTERFACE -/** - * @page page_iface_zwp_relative_pointer_v1 zwp_relative_pointer_v1 - * @section page_iface_zwp_relative_pointer_v1_desc Description - * - * A wp_relative_pointer object is an extension to the wl_pointer interface - * used for emitting relative pointer events. It shares the same focus as - * wl_pointer objects of the same seat and will only emit events when it has - * focus. - * @section page_iface_zwp_relative_pointer_v1_api API - * See @ref iface_zwp_relative_pointer_v1. - */ -/** - * @defgroup iface_zwp_relative_pointer_v1 The zwp_relative_pointer_v1 interface - * - * A wp_relative_pointer object is an extension to the wl_pointer interface - * used for emitting relative pointer events. It shares the same focus as - * wl_pointer objects of the same seat and will only emit events when it has - * focus. - */ extern const struct wl_interface zwp_relative_pointer_v1_interface; #endif -/** - * @ingroup iface_zwp_relative_pointer_manager_v1 - * @struct zwp_relative_pointer_manager_v1_interface - */ struct zwp_relative_pointer_manager_v1_interface { - /** - * destroy the relative pointer manager object - * - * Used by the client to notify the server that it will no longer - * use this relative pointer manager object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * get a relative pointer object - * - * Create a relative pointer interface given a wl_pointer object. - * See the wp_relative_pointer interface for more details. - */ void (*get_relative_pointer)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -145,52 +94,20 @@ struct zwp_relative_pointer_manager_v1_interface { }; -/** - * @ingroup iface_zwp_relative_pointer_manager_v1 - */ #define ZWP_RELATIVE_POINTER_MANAGER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_relative_pointer_manager_v1 - */ #define ZWP_RELATIVE_POINTER_MANAGER_V1_GET_RELATIVE_POINTER_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_relative_pointer_v1 - * @struct zwp_relative_pointer_v1_interface - */ struct zwp_relative_pointer_v1_interface { - /** - * release the relative pointer object - * - * - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; #define ZWP_RELATIVE_POINTER_V1_RELATIVE_MOTION 0 -/** - * @ingroup iface_zwp_relative_pointer_v1 - */ #define ZWP_RELATIVE_POINTER_V1_RELATIVE_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_relative_pointer_v1 - */ #define ZWP_RELATIVE_POINTER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_relative_pointer_v1 - * Sends an relative_motion event to the client owning the resource. - * @param resource_ The client's resource - * @param utime_hi high 32 bits of a 64 bit timestamp with microsecond granularity - * @param utime_lo low 32 bits of a 64 bit timestamp with microsecond granularity - * @param dx the x component of the motion vector - * @param dy the y component of the motion vector - * @param dx_unaccel the x component of the unaccelerated motion vector - * @param dy_unaccel the y component of the unaccelerated motion vector - */ static inline void zwp_relative_pointer_v1_send_relative_motion(struct wl_resource *resource_, uint32_t utime_hi, uint32_t utime_lo, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t dx_unaccel, wl_fixed_t dy_unaccel) { diff --git a/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-protocol.c index 01434221c..2543c95cf 100644 --- a/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.26.0 */ - /* * Copyright © 2022 Simon Ser * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-server-protocol.h index a030d4775..a081bc417 100644 --- a/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.26.0 */ #ifndef SINGLE_PIXEL_BUFFER_V1_SERVER_PROTOCOL_H #define SINGLE_PIXEL_BUFFER_V1_SERVER_PROTOCOL_H @@ -14,27 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_single_pixel_buffer_v1 The single_pixel_buffer_v1 protocol - * single pixel buffer factory - * - * @section page_desc_single_pixel_buffer_v1 Description - * - * This protocol extension allows clients to create single-pixel buffers. - * - * Compositors supporting this protocol extension should also support the - * viewporter protocol extension. Clients may use viewporter to scale a - * single-pixel buffer to a desired size. - * - * Warning! The protocol described in this file is currently in the testing - * phase. Backward compatible changes may be added together with the - * corresponding interface version bump. Backward incompatible changes can - * only be done by creating a new major version of the extension. - * - * @section page_ifaces_single_pixel_buffer_v1 Interfaces - * - @subpage page_iface_wp_single_pixel_buffer_manager_v1 - global factory for single-pixel buffers +/* * @section page_copyright_single_pixel_buffer_v1 Copyright - *
  *
  * Copyright © 2022 Simon Ser
  *
@@ -56,66 +36,38 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + struct wl_buffer; struct wp_single_pixel_buffer_manager_v1; #ifndef WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_INTERFACE #define WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_INTERFACE -/** - * @page page_iface_wp_single_pixel_buffer_manager_v1 wp_single_pixel_buffer_manager_v1 - * @section page_iface_wp_single_pixel_buffer_manager_v1_desc Description - * - * The wp_single_pixel_buffer_manager_v1 interface is a factory for - * single-pixel buffers. - * @section page_iface_wp_single_pixel_buffer_manager_v1_api API - * See @ref iface_wp_single_pixel_buffer_manager_v1. - */ -/** - * @defgroup iface_wp_single_pixel_buffer_manager_v1 The wp_single_pixel_buffer_manager_v1 interface - * - * The wp_single_pixel_buffer_manager_v1 interface is a factory for - * single-pixel buffers. - */ extern const struct wl_interface wp_single_pixel_buffer_manager_v1_interface; #endif -/** - * @ingroup iface_wp_single_pixel_buffer_manager_v1 - * @struct wp_single_pixel_buffer_manager_v1_interface - */ struct wp_single_pixel_buffer_manager_v1_interface { - /** - * destroy the manager - * - * Destroy the wp_single_pixel_buffer_manager_v1 object. - * - * The child objects created via this interface are unaffected. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a 1×1 buffer from 32-bit RGBA values - * - * Create a single-pixel buffer from four 32-bit RGBA values. - * - * Unless specified in another protocol extension, the RGBA values - * use pre-multiplied alpha. - * - * The width and height of the buffer are 1. - * - * The r, g, b and a arguments valid range is from UINT32_MIN (0) - * to UINT32_MAX (0xffffffff). - * - * These arguments should be interpreted as a percentage, i.e. - - * UINT32_MIN = 0% of the given color component - UINT32_MAX = 100% - * of the given color component - * @param r value of the buffer's red channel - * @param g value of the buffer's green channel - * @param b value of the buffer's blue channel - * @param a value of the buffer's alpha channel - */ void (*create_u32_rgba_buffer)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -126,13 +78,7 @@ struct wp_single_pixel_buffer_manager_v1_interface { }; -/** - * @ingroup iface_wp_single_pixel_buffer_manager_v1 - */ #define WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_single_pixel_buffer_manager_v1 - */ #define WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_CREATE_U32_RGBA_BUFFER_SINCE_VERSION 1 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-protocol.c b/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-protocol.c index a6030f8df..557da50a4 100644 --- a/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2012, 2013 Intel Corporation * Copyright © 2015, 2016 Jan Arne Petersen @@ -34,7 +32,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-server-protocol.h index c8473dd6a..e7e142ce2 100644 --- a/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef TEXT_INPUT_UNSTABLE_V3_SERVER_PROTOCOL_H #define TEXT_INPUT_UNSTABLE_V3_SERVER_PROTOCOL_H @@ -14,33 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_text_input_unstable_v3 The text_input_unstable_v3 protocol - * Protocol for composing text - * - * @section page_desc_text_input_unstable_v3 Description - * - * This protocol allows compositors to act as input methods and to send text - * to applications. A text input object is used to manage state of what are - * typically text entry fields in the application. - * - * This document adheres to the RFC 2119 when using words like "must", - * "should", "may", etc. - * - * Warning! The protocol described in this file is experimental and - * backward incompatible changes may be made. Backward compatible changes - * may be added together with the corresponding interface version bump. - * Backward incompatible changes are done by bumping the version number in - * the protocol and interface names and resetting the interface version. - * Once the protocol is to be declared stable, the 'z' prefix and the - * version number in the protocol and interface names are removed and the - * interface version number is reset. - * - * @section page_ifaces_text_input_unstable_v3 Interfaces - * - @subpage page_iface_zwp_text_input_v3 - text input - * - @subpage page_iface_zwp_text_input_manager_v3 - text input manager +/* * @section page_copyright_text_input_unstable_v3 Copyright - *
  *
  * Copyright © 2012, 2013 Intel Corporation
  * Copyright © 2015, 2016 Jan Arne Petersen
@@ -67,8 +41,33 @@ struct wl_resource;
  * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
  * THIS SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_seat; struct wl_surface; struct zwp_text_input_manager_v3; @@ -76,118 +75,23 @@ struct zwp_text_input_v3; #ifndef ZWP_TEXT_INPUT_V3_INTERFACE #define ZWP_TEXT_INPUT_V3_INTERFACE -/** - * @page page_iface_zwp_text_input_v3 zwp_text_input_v3 - * @section page_iface_zwp_text_input_v3_desc Description - * - * The zwp_text_input_v3 interface represents text input and input methods - * associated with a seat. It provides enter/leave events to follow the - * text input focus for a seat. - * - * Requests are used to enable/disable the text-input object and set - * state information like surrounding and selected text or the content type. - * The information about the entered text is sent to the text-input object - * via the preedit_string and commit_string events. - * - * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices - * must not point to middle bytes inside a code point: they must either - * point to the first byte of a code point or to the end of the buffer. - * Lengths must be measured between two valid indices. - * - * Focus moving throughout surfaces will result in the emission of - * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused - * surface must commit zwp_text_input_v3.enable and - * zwp_text_input_v3.disable requests as the keyboard focus moves across - * editable and non-editable elements of the UI. Those two requests are not - * expected to be paired with each other, the compositor must be able to - * handle consecutive series of the same request. - * - * State is sent by the state requests (set_surrounding_text, - * set_content_type and set_cursor_rectangle) and a commit request. After an - * enter event or disable request all state information is invalidated and - * needs to be resent by the client. - * @section page_iface_zwp_text_input_v3_api API - * See @ref iface_zwp_text_input_v3. - */ -/** - * @defgroup iface_zwp_text_input_v3 The zwp_text_input_v3 interface - * - * The zwp_text_input_v3 interface represents text input and input methods - * associated with a seat. It provides enter/leave events to follow the - * text input focus for a seat. - * - * Requests are used to enable/disable the text-input object and set - * state information like surrounding and selected text or the content type. - * The information about the entered text is sent to the text-input object - * via the preedit_string and commit_string events. - * - * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices - * must not point to middle bytes inside a code point: they must either - * point to the first byte of a code point or to the end of the buffer. - * Lengths must be measured between two valid indices. - * - * Focus moving throughout surfaces will result in the emission of - * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused - * surface must commit zwp_text_input_v3.enable and - * zwp_text_input_v3.disable requests as the keyboard focus moves across - * editable and non-editable elements of the UI. Those two requests are not - * expected to be paired with each other, the compositor must be able to - * handle consecutive series of the same request. - * - * State is sent by the state requests (set_surrounding_text, - * set_content_type and set_cursor_rectangle) and a commit request. After an - * enter event or disable request all state information is invalidated and - * needs to be resent by the client. - */ extern const struct wl_interface zwp_text_input_v3_interface; #endif #ifndef ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE #define ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE -/** - * @page page_iface_zwp_text_input_manager_v3 zwp_text_input_manager_v3 - * @section page_iface_zwp_text_input_manager_v3_desc Description - * - * A factory for text-input objects. This object is a global singleton. - * @section page_iface_zwp_text_input_manager_v3_api API - * See @ref iface_zwp_text_input_manager_v3. - */ -/** - * @defgroup iface_zwp_text_input_manager_v3 The zwp_text_input_manager_v3 interface - * - * A factory for text-input objects. This object is a global singleton. - */ extern const struct wl_interface zwp_text_input_manager_v3_interface; #endif #ifndef ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM #define ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * text change reason - * - * Reason for the change of surrounding text or cursor posision. - */ enum zwp_text_input_v3_change_cause { - /** - * input method caused the change - */ ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD = 0, - /** - * something else than the input method caused the change - */ ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER = 1, }; -#endif /* ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 change_cause value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_change_cause - */ static inline bool zwp_text_input_v3_change_cause_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -199,101 +103,33 @@ zwp_text_input_v3_change_cause_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM_IS_VALID */ +#endif #ifndef ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM #define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * content hint - * - * Content hint is a bitmask to allow to modify the behavior of the text - * input. - */ enum zwp_text_input_v3_content_hint { - /** - * no special behavior - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE = 0x0, - /** - * suggest word completions - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION = 0x1, - /** - * suggest word corrections - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK = 0x2, - /** - * switch to uppercase letters at the start of a sentence - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION = 0x4, - /** - * prefer lowercase letters - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE = 0x8, - /** - * prefer uppercase letters - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE = 0x10, - /** - * prefer casing for titles and headings (can be language dependent) - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE = 0x20, - /** - * characters should be hidden - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT = 0x40, - /** - * typed text should not be stored - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA = 0x80, - /** - * just Latin characters should be entered - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN = 0x100, - /** - * the text input is multiline - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE = 0x200, - /** - * an on-screen way to fill in the input is already provided by the client - * @since 2 - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_ON_SCREEN_INPUT_PROVIDED = 0x400, - /** - * prefer not offering emoji support - * @since 2 - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_NO_EMOJI = 0x800, - /** - * the text input will display preedit text in place - * @since 2 - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_PREEDIT_SHOWN = 0x1000, }; -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ON_SCREEN_INPUT_PROVIDED_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_CONTENT_HINT_NO_EMOJI_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_CONTENT_HINT_PREEDIT_SHOWN_SINCE_VERSION 2 -#endif /* ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 content_hint value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_content_hint - */ static inline bool zwp_text_input_v3_content_hint_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -327,89 +163,30 @@ zwp_text_input_v3_content_hint_is_valid(uint32_t value, uint32_t version) { valid |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_PREEDIT_SHOWN; return (value & ~valid) == 0; } -#endif /* ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM_IS_VALID */ +#endif #ifndef ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM #define ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * content purpose - * - * The content purpose allows to specify the primary purpose of a text - * input. - * - * This allows an input method to show special purpose input panels with - * extra characters or to disallow some characters. - */ enum zwp_text_input_v3_content_purpose { - /** - * default input, allowing all characters - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL = 0, - /** - * allow only alphabetic characters - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA = 1, - /** - * allow only digits - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS = 2, - /** - * input a number (including decimal separator and sign) - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER = 3, - /** - * input a phone number - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE = 4, - /** - * input an URL - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL = 5, - /** - * input an email address - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL = 6, - /** - * input a name of a person - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME = 7, - /** - * input a password (combine with sensitive_data hint) - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD = 8, - /** - * input is a numeric password (combine with sensitive_data hint) - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN = 9, - /** - * input a date - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE = 10, - /** - * input a time - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME = 11, - /** - * input a date and time - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME = 12, - /** - * input for a terminal - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL = 13, }; -#endif /* ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 content_purpose value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_content_purpose - */ static inline bool zwp_text_input_v3_content_purpose_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -445,27 +222,17 @@ zwp_text_input_v3_content_purpose_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM_IS_VALID */ +#endif #ifndef ZWP_TEXT_INPUT_V3_ERROR_ENUM #define ZWP_TEXT_INPUT_V3_ERROR_ENUM enum zwp_text_input_v3_error { - /** - * an invalid or duplicate action was specified - */ ZWP_TEXT_INPUT_V3_ERROR_INVALID_ACTION = 0, }; -#endif /* ZWP_TEXT_INPUT_V3_ERROR_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_ERROR_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 error value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_error - */ static inline bool zwp_text_input_v3_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -475,40 +242,18 @@ zwp_text_input_v3_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_TEXT_INPUT_V3_ERROR_ENUM_IS_VALID */ +#endif #ifndef ZWP_TEXT_INPUT_V3_ACTION_ENUM #define ZWP_TEXT_INPUT_V3_ACTION_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * action - * - * A possible action to perform on a text input. - * - * The submit action is intended for input entries that expect some sort of - * activation after user interaction, e.g. the URL entry in a browser. - */ enum zwp_text_input_v3_action { - /** - * no action - */ ZWP_TEXT_INPUT_V3_ACTION_NONE = 0, - /** - * the action is submitted - */ ZWP_TEXT_INPUT_V3_ACTION_SUBMIT = 1, }; -#endif /* ZWP_TEXT_INPUT_V3_ACTION_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_ACTION_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_ACTION_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 action value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_action - */ static inline bool zwp_text_input_v3_action_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -520,57 +265,23 @@ zwp_text_input_v3_action_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_TEXT_INPUT_V3_ACTION_ENUM_IS_VALID */ +#endif #ifndef ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM #define ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * preedit style hint - * - * Style hints for the preedit string. - */ enum zwp_text_input_v3_preedit_hint { - /** - * simple pre-edit text style, typically underlined - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_WHOLE = 1, - /** - * hint for a selected piece of text, e.g. per-character navigation and composition - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SELECTION = 2, - /** - * predicted text, not typed by the user - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_PREDICTION = 3, - /** - * prefixed text not being currently edited, e.g. prior to a 'selection' section - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_PREFIX = 4, - /** - * suffixed text not being currently edited, e.g. after a 'selection' section - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SUFFIX = 5, - /** - * spelling error - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SPELLING_ERROR = 6, - /** - * wrong composition, e.g. user input that can not be transliterated - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_COMPOSE_ERROR = 7, }; -#endif /* ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 preedit_hint value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_preedit_hint - */ static inline bool zwp_text_input_v3_preedit_hint_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -592,269 +303,40 @@ zwp_text_input_v3_preedit_hint_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwp_text_input_v3 - * @struct zwp_text_input_v3_interface - */ struct zwp_text_input_v3_interface { - /** - * Destroy the wp_text_input - * - * Destroy the wp_text_input object. Also disables all surfaces - * enabled through this wp_text_input object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * Request text input to be enabled - * - * Requests text input on the surface previously obtained from - * the enter event. - * - * This request must be issued every time the focused text input - * changes to a new one, including within the current surface. Use - * zwp_text_input_v3.disable when there is no longer any input - * focus on the current surface. - * - * Clients must not enable more than one text input on the single - * seat and should disable the current text input before enabling - * the new one. Requests to enable a text input when another text - * input is enabled on the same seat must be ignored by compositor. - * - * This request resets all state associated with previous enable, - * disable, set_surrounding_text, set_text_change_cause, - * set_content_type, and set_cursor_rectangle requests, as well as - * the state associated with preedit_string, commit_string, and - * delete_surrounding_text events. - * - * The set_surrounding_text, set_content_type and - * set_cursor_rectangle requests must follow if the text input - * supports the necessary functionality. - * - * State set with this request is double-buffered. It will get - * applied on the next zwp_text_input_v3.commit request, and stay - * valid until the next committed enable or disable request. - * - * The changes must be applied by the compositor after issuing a - * zwp_text_input_v3.commit request. - */ void (*enable)(struct wl_client *client, struct wl_resource *resource); - /** - * Disable text input on a surface - * - * Explicitly disable text input on the current surface - * (typically when there is no focus on any text entry inside the - * surface). - * - * State set with this request is double-buffered. It will get - * applied on the next zwp_text_input_v3.commit request. - */ void (*disable)(struct wl_client *client, struct wl_resource *resource); - /** - * sets the surrounding text - * - * Sets the surrounding plain text around the input, excluding - * the preedit text. - * - * The client should notify the compositor of any changes in any of - * the values carried with this request, including changes caused - * by handling incoming text-input events as well as changes caused - * by other mechanisms like keyboard typing. - * - * If the client is unaware of the text around the cursor, it - * should not issue this request, to signify lack of support to the - * compositor. - * - * Text is UTF-8 encoded, and should include the cursor position, - * the complete selection and additional characters before and - * after them. There is a maximum length of wayland messages, so - * text can not be longer than 4000 bytes. - * - * Cursor is the byte offset of the cursor within text buffer. - * - * Anchor is the byte offset of the selection anchor within text - * buffer. If there is no selected text, anchor is the same as - * cursor. - * - * If any preedit text is present, it is replaced with a cursor for - * the purpose of this event. - * - * Values set with this request are double-buffered. They will get - * applied on the next zwp_text_input_v3.commit request, and stay - * valid until the next committed enable or disable request. - * - * The initial state for affected fields is empty, meaning that the - * text input does not support sending surrounding text. If the - * empty values get applied, subsequent attempts to change them may - * have no effect. - */ void (*set_surrounding_text)(struct wl_client *client, struct wl_resource *resource, const char *text, int32_t cursor, int32_t anchor); - /** - * indicates the cause of surrounding text change - * - * Tells the compositor why the text surrounding the cursor - * changed. - * - * Whenever the client detects an external change in text, cursor, - * or anchor posision, it must issue this request to the - * compositor. This request is intended to give the input method a - * chance to update the preedit text in an appropriate way, e.g. by - * removing it when the user starts typing with a keyboard. - * - * cause describes the source of the change. - * - * The value set with this request is double-buffered. It must be - * applied and reset to initial at the next - * zwp_text_input_v3.commit request. - * - * The initial value of cause is input_method. - */ void (*set_text_change_cause)(struct wl_client *client, struct wl_resource *resource, uint32_t cause); - /** - * set content purpose and hint - * - * Sets the content purpose and content hint. While the purpose - * is the basic purpose of an input field, the hint flags allow to - * modify some of the behavior. - * - * Values set with this request are double-buffered. They will get - * applied on the next zwp_text_input_v3.commit request. Subsequent - * attempts to update them may have no effect. The values remain - * valid until the next committed enable or disable request. - * - * The initial value for hint is none, and the initial value for - * purpose is normal. - */ void (*set_content_type)(struct wl_client *client, struct wl_resource *resource, uint32_t hint, uint32_t purpose); - /** - * set cursor position - * - * Marks an area around the cursor as a x, y, width, height - * rectangle in surface local coordinates. - * - * Allows the compositor to put a window with word suggestions near - * the cursor, without obstructing the text being input. - * - * If the client is unaware of the position of edited text, it - * should not issue this request, to signify lack of support to the - * compositor. - * - * Values set with this request are double-buffered. They will get - * applied on the next zwp_text_input_v3.commit request, and stay - * valid until the next committed enable or disable request. - * - * The initial values describing a cursor rectangle are empty. That - * means the text input does not support describing the cursor - * area. If the empty values get applied, subsequent attempts to - * change them may have no effect. - * - * As of version 2, the zwp_text_input_v3.commit request does not - * apply values sent with this request. Instead, it stores them in - * a separate "committed" area. The committed values, if still - * valid, get applied on the next wl_surface.commit request on the - * surface with text-input focus. Both committed and applied values - * get invalidated on: - * - * - the next committed enable or disable request, or - a change of - * the focused surface of the text-input (leave or enter events). - * - * This double stage application allows the compositor to position - * the input method popup in the same frame as the contents of the - * text on the surface are updated. - */ void (*set_cursor_rectangle)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * commit state - * - * Atomically applies state changes recently sent to the - * compositor. - * - * The commit request establishes and updates the state of the - * client, and must be issued after any changes to apply them. - * - * Text input state (enabled status, content purpose, content hint, - * surrounding text and change cause, cursor rectangle) is - * conceptually double-buffered within the context of a text input, - * i.e. between a committed enable request and the following - * committed enable or disable request. - * - * Protocol requests modify the pending state, as opposed to the - * current state in use by the input method. A commit request - * atomically applies all pending state, replacing the current - * state. After commit, the new pending state is as documented for - * each related request. - * - * Requests are applied in the order of arrival. - * - * Neither current nor pending state are modified unless noted - * otherwise. - * - * The compositor must count the number of commit requests coming - * from each zwp_text_input_v3 object and use the count as the - * serial in done events. - */ void (*commit)(struct wl_client *client, struct wl_resource *resource); - /** - * set the available actions - * - * Set the actions available for this text input. - * - * Values set with this request are double-buffered. They will get - * applied on the next zwp_text_input_v3.commit request. - * - * If the available_actions array contains the none action, or - * contains the same action multiple times, the compositor must - * raise the invalid_action protocol error. - * - * Initially, no actions are available. - * @param available_actions available actions - * @since 2 - */ void (*set_available_actions)(struct wl_client *client, struct wl_resource *resource, struct wl_array *available_actions); - /** - * show input panel - * - * Requests an input panel to be shown (e.g. a on-screen - * keyboard). - * - * This request only hints the desired interaction pattern from the - * client side, and its effect may be ignored by compositors given - * other environmental factors. Repeated calls will be ignored. - * @since 2 - */ void (*show_input_panel)(struct wl_client *client, struct wl_resource *resource); - /** - * hide input panel - * - * Requests an input panel to be hidden. - * - * This request only hints the desired interaction pattern from the - * client side, and its effect may be ignored by compositors given - * other environmental factors. Repeated calls will be ignored. - * @since 2 - */ void (*hide_input_panel)(struct wl_client *client, struct wl_resource *resource); }; @@ -869,212 +351,85 @@ struct zwp_text_input_v3_interface { #define ZWP_TEXT_INPUT_V3_LANGUAGE 7 #define ZWP_TEXT_INPUT_V3_PREEDIT_HINT 8 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_PREEDIT_STRING_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_COMMIT_STRING_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_DELETE_SURROUNDING_TEXT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_ACTION_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_LANGUAGE_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_ENABLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_DISABLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_COMMIT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SET_AVAILABLE_ACTIONS_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SHOW_INPUT_PANEL_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_HIDE_INPUT_PANEL_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an enter event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_text_input_v3_send_enter(struct wl_resource *resource_, struct wl_resource *surface) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_ENTER, surface); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an leave event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_text_input_v3_send_leave(struct wl_resource *resource_, struct wl_resource *surface) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_LEAVE, surface); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an preedit_string event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_text_input_v3_send_preedit_string(struct wl_resource *resource_, const char *text, int32_t cursor_begin, int32_t cursor_end) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_PREEDIT_STRING, text, cursor_begin, cursor_end); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an commit_string event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_text_input_v3_send_commit_string(struct wl_resource *resource_, const char *text) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_COMMIT_STRING, text); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an delete_surrounding_text event to the client owning the resource. - * @param resource_ The client's resource - * @param before_length length of text before current cursor position - * @param after_length length of text after current cursor position - */ static inline void zwp_text_input_v3_send_delete_surrounding_text(struct wl_resource *resource_, uint32_t before_length, uint32_t after_length) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_DELETE_SURROUNDING_TEXT, before_length, after_length); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_text_input_v3_send_done(struct wl_resource *resource_, uint32_t serial) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_DONE, serial); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an action event to the client owning the resource. - * @param resource_ The client's resource - * @param action action performed - * @param serial serial number of the action event - */ static inline void zwp_text_input_v3_send_action(struct wl_resource *resource_, uint32_t action, uint32_t serial) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_ACTION, action, serial); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an language event to the client owning the resource. - * @param resource_ The client's resource - * @param language new language set by IME - */ static inline void zwp_text_input_v3_send_language(struct wl_resource *resource_, const char *language) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_LANGUAGE, language); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an preedit_hint event to the client owning the resource. - * @param resource_ The client's resource - * @param start starting point of the affected substring - * @param end end point of the affected substring - * @param hint hint to apply - */ static inline void zwp_text_input_v3_send_preedit_hint(struct wl_resource *resource_, uint32_t start, uint32_t end, uint32_t hint) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_PREEDIT_HINT, start, end, hint); } -/** - * @ingroup iface_zwp_text_input_manager_v3 - * @struct zwp_text_input_manager_v3_interface - */ struct zwp_text_input_manager_v3_interface { - /** - * Destroy the wp_text_input_manager - * - * Destroy the wp_text_input_manager object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a new text input object - * - * Creates a new text-input object for a given seat. - */ void (*get_text_input)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -1082,13 +437,7 @@ struct zwp_text_input_manager_v3_interface { }; -/** - * @ingroup iface_zwp_text_input_manager_v3 - */ #define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_manager_v3 - */ #define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT_SINCE_VERSION 1 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/generated/viewporter-protocol.c b/app/src/main/cpp/waylandcomp/generated/viewporter-protocol.c index d6aaa71e7..8efb3ee3d 100644 --- a/app/src/main/cpp/waylandcomp/generated/viewporter-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/viewporter-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2013-2016 Collabora, Ltd. * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/viewporter-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/viewporter-server-protocol.h index 51957565c..9495136e6 100644 --- a/app/src/main/cpp/waylandcomp/generated/viewporter-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/viewporter-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef VIEWPORTER_SERVER_PROTOCOL_H #define VIEWPORTER_SERVER_PROTOCOL_H @@ -14,13 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_viewporter The viewporter protocol - * @section page_ifaces_viewporter Interfaces - * - @subpage page_iface_wp_viewporter - surface cropping and scaling - * - @subpage page_iface_wp_viewport - crop and scale interface to a wl_surface +/* * @section page_copyright_viewporter Copyright - *
  *
  * Copyright © 2013-2016 Collabora, Ltd.
  *
@@ -42,184 +36,35 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + struct wl_surface; struct wp_viewport; struct wp_viewporter; #ifndef WP_VIEWPORTER_INTERFACE #define WP_VIEWPORTER_INTERFACE -/** - * @page page_iface_wp_viewporter wp_viewporter - * @section page_iface_wp_viewporter_desc Description - * - * The global interface exposing surface cropping and scaling - * capabilities is used to instantiate an interface extension for a - * wl_surface object. This extended interface will then allow - * cropping and scaling the surface contents, effectively - * disconnecting the direct relationship between the buffer and the - * surface size. - * @section page_iface_wp_viewporter_api API - * See @ref iface_wp_viewporter. - */ -/** - * @defgroup iface_wp_viewporter The wp_viewporter interface - * - * The global interface exposing surface cropping and scaling - * capabilities is used to instantiate an interface extension for a - * wl_surface object. This extended interface will then allow - * cropping and scaling the surface contents, effectively - * disconnecting the direct relationship between the buffer and the - * surface size. - */ extern const struct wl_interface wp_viewporter_interface; #endif #ifndef WP_VIEWPORT_INTERFACE #define WP_VIEWPORT_INTERFACE -/** - * @page page_iface_wp_viewport wp_viewport - * @section page_iface_wp_viewport_desc Description - * - * An additional interface to a wl_surface object, which allows the - * client to specify the cropping and scaling of the surface - * contents. - * - * This interface works with two concepts: the source rectangle (src_x, - * src_y, src_width, src_height), and the destination size (dst_width, - * dst_height). The contents of the source rectangle are scaled to the - * destination size, and content outside the source rectangle is ignored. - * This state is double-buffered, and is applied on the next - * wl_surface.commit. - * - * The two parts of crop and scale state are independent: the source - * rectangle, and the destination size. Initially both are unset, that - * is, no scaling is applied. The whole of the current wl_buffer is - * used as the source, and the surface size is as defined in - * wl_surface.attach. - * - * If the destination size is set, it causes the surface size to become - * dst_width, dst_height. The source (rectangle) is scaled to exactly - * this size. This overrides whatever the attached wl_buffer size is, - * unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface - * has no content and therefore no size. Otherwise, the size is always - * at least 1x1 in surface local coordinates. - * - * If the source rectangle is set, it defines what area of the wl_buffer is - * taken as the source. If the source rectangle is set and the destination - * size is not set, then src_width and src_height must be integers, and the - * surface size becomes the source rectangle size. This results in cropping - * without scaling. If src_width or src_height are not integers and - * destination size is not set, the bad_size protocol error is raised when - * the surface state is applied. - * - * The coordinate transformations from buffer pixel coordinates up to - * the surface-local coordinates happen in the following order: - * 1. buffer_transform (wl_surface.set_buffer_transform) - * 2. buffer_scale (wl_surface.set_buffer_scale) - * 3. crop and scale (wp_viewport.set*) - * This means, that the source rectangle coordinates of crop and scale - * are given in the coordinates after the buffer transform and scale, - * i.e. in the coordinates that would be the surface-local coordinates - * if the crop and scale was not applied. - * - * If src_x or src_y are negative, the bad_value protocol error is raised. - * Otherwise, if the source rectangle is partially or completely outside of - * the non-NULL wl_buffer, then the out_of_buffer protocol error is raised - * when the surface state is applied. A NULL wl_buffer does not raise the - * out_of_buffer error. - * - * If the wl_surface associated with the wp_viewport is destroyed, - * all wp_viewport requests except 'destroy' raise the protocol error - * no_surface. - * - * If the wp_viewport object is destroyed, the crop and scale - * state is removed from the wl_surface. The change will be applied - * on the next wl_surface.commit. - * @section page_iface_wp_viewport_api API - * See @ref iface_wp_viewport. - */ -/** - * @defgroup iface_wp_viewport The wp_viewport interface - * - * An additional interface to a wl_surface object, which allows the - * client to specify the cropping and scaling of the surface - * contents. - * - * This interface works with two concepts: the source rectangle (src_x, - * src_y, src_width, src_height), and the destination size (dst_width, - * dst_height). The contents of the source rectangle are scaled to the - * destination size, and content outside the source rectangle is ignored. - * This state is double-buffered, and is applied on the next - * wl_surface.commit. - * - * The two parts of crop and scale state are independent: the source - * rectangle, and the destination size. Initially both are unset, that - * is, no scaling is applied. The whole of the current wl_buffer is - * used as the source, and the surface size is as defined in - * wl_surface.attach. - * - * If the destination size is set, it causes the surface size to become - * dst_width, dst_height. The source (rectangle) is scaled to exactly - * this size. This overrides whatever the attached wl_buffer size is, - * unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface - * has no content and therefore no size. Otherwise, the size is always - * at least 1x1 in surface local coordinates. - * - * If the source rectangle is set, it defines what area of the wl_buffer is - * taken as the source. If the source rectangle is set and the destination - * size is not set, then src_width and src_height must be integers, and the - * surface size becomes the source rectangle size. This results in cropping - * without scaling. If src_width or src_height are not integers and - * destination size is not set, the bad_size protocol error is raised when - * the surface state is applied. - * - * The coordinate transformations from buffer pixel coordinates up to - * the surface-local coordinates happen in the following order: - * 1. buffer_transform (wl_surface.set_buffer_transform) - * 2. buffer_scale (wl_surface.set_buffer_scale) - * 3. crop and scale (wp_viewport.set*) - * This means, that the source rectangle coordinates of crop and scale - * are given in the coordinates after the buffer transform and scale, - * i.e. in the coordinates that would be the surface-local coordinates - * if the crop and scale was not applied. - * - * If src_x or src_y are negative, the bad_value protocol error is raised. - * Otherwise, if the source rectangle is partially or completely outside of - * the non-NULL wl_buffer, then the out_of_buffer protocol error is raised - * when the surface state is applied. A NULL wl_buffer does not raise the - * out_of_buffer error. - * - * If the wl_surface associated with the wp_viewport is destroyed, - * all wp_viewport requests except 'destroy' raise the protocol error - * no_surface. - * - * If the wp_viewport object is destroyed, the crop and scale - * state is removed from the wl_surface. The change will be applied - * on the next wl_surface.commit. - */ extern const struct wl_interface wp_viewport_interface; #endif #ifndef WP_VIEWPORTER_ERROR_ENUM #define WP_VIEWPORTER_ERROR_ENUM enum wp_viewporter_error { - /** - * the surface already has a viewport object associated - */ WP_VIEWPORTER_ERROR_VIEWPORT_EXISTS = 0, }; -#endif /* WP_VIEWPORTER_ERROR_ENUM */ +#endif #ifndef WP_VIEWPORTER_ERROR_ENUM_IS_VALID #define WP_VIEWPORTER_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_viewporter - * Validate a wp_viewporter error value. - * - * @return true on success, false on error. - * @ref wp_viewporter_error - */ static inline bool wp_viewporter_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -229,32 +74,11 @@ wp_viewporter_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_VIEWPORTER_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_viewporter - * @struct wp_viewporter_interface - */ struct wp_viewporter_interface { - /** - * unbind from the cropping and scaling interface - * - * Informs the server that the client will not be using this - * protocol object anymore. This does not affect any other objects, - * wp_viewport objects included. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * extend surface interface for crop and scale - * - * Instantiate an interface extension for the given wl_surface to - * crop and scale its content. If the given wl_surface already has - * a wp_viewport object associated, the viewport_exists protocol - * error is raised. - * @param id the new viewport interface id - * @param surface the surface - */ void (*get_viewport)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -262,46 +86,21 @@ struct wp_viewporter_interface { }; -/** - * @ingroup iface_wp_viewporter - */ #define WP_VIEWPORTER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_viewporter - */ #define WP_VIEWPORTER_GET_VIEWPORT_SINCE_VERSION 1 #ifndef WP_VIEWPORT_ERROR_ENUM #define WP_VIEWPORT_ERROR_ENUM enum wp_viewport_error { - /** - * negative or zero values in width or height - */ WP_VIEWPORT_ERROR_BAD_VALUE = 0, - /** - * destination size is not integer - */ WP_VIEWPORT_ERROR_BAD_SIZE = 1, - /** - * source rectangle extends outside of the content area - */ WP_VIEWPORT_ERROR_OUT_OF_BUFFER = 2, - /** - * the wl_surface was destroyed - */ WP_VIEWPORT_ERROR_NO_SURFACE = 3, }; -#endif /* WP_VIEWPORT_ERROR_ENUM */ +#endif #ifndef WP_VIEWPORT_ERROR_ENUM_IS_VALID #define WP_VIEWPORT_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_viewport - * Validate a wp_viewport error value. - * - * @return true on success, false on error. - * @ref wp_viewport_error - */ static inline bool wp_viewport_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -317,63 +116,17 @@ wp_viewport_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_VIEWPORT_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_viewport - * @struct wp_viewport_interface - */ struct wp_viewport_interface { - /** - * remove scaling and cropping from the surface - * - * The associated wl_surface's crop and scale state is removed. - * The change is applied on the next wl_surface.commit. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the source rectangle for cropping - * - * Set the source rectangle of the associated wl_surface. See - * wp_viewport for the description, and relation to the wl_buffer - * size. - * - * If all of x, y, width and height are -1.0, the source rectangle - * is unset instead. Any other set of values where width or height - * are zero or negative, or x or y are negative, raise the - * bad_value protocol error. - * - * The crop and scale state is double-buffered state, and will be - * applied on the next wl_surface.commit. - * @param x source rectangle x - * @param y source rectangle y - * @param width source rectangle width - * @param height source rectangle height - */ void (*set_source)(struct wl_client *client, struct wl_resource *resource, wl_fixed_t x, wl_fixed_t y, wl_fixed_t width, wl_fixed_t height); - /** - * set the surface size for scaling - * - * Set the destination size of the associated wl_surface. See - * wp_viewport for the description, and relation to the wl_buffer - * size. - * - * If width is -1 and height is -1, the destination size is unset - * instead. Any other pair of values for width and height that - * contains zero or negative values raises the bad_value protocol - * error. - * - * The crop and scale state is double-buffered state, and will be - * applied on the next wl_surface.commit. - * @param width surface width - * @param height surface height - */ void (*set_destination)(struct wl_client *client, struct wl_resource *resource, int32_t width, @@ -381,17 +134,8 @@ struct wp_viewport_interface { }; -/** - * @ingroup iface_wp_viewport - */ #define WP_VIEWPORT_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_viewport - */ #define WP_VIEWPORT_SET_SOURCE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_viewport - */ #define WP_VIEWPORT_SET_DESTINATION_SINCE_VERSION 1 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-protocol.c index 4bb2945af..8e8cafaf3 100644 --- a/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2018 Simon Ser * Copyright © 2019 Ivan Molodetskikh @@ -32,7 +30,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-server-protocol.h index a48487584..f8d847ce6 100644 --- a/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef WLR_DATA_CONTROL_UNSTABLE_V1_SERVER_PROTOCOL_H #define WLR_DATA_CONTROL_UNSTABLE_V1_SERVER_PROTOCOL_H @@ -14,35 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_wlr_data_control_unstable_v1 The wlr_data_control_unstable_v1 protocol - * control data devices - * - * @section page_desc_wlr_data_control_unstable_v1 Description - * - * This protocol allows a privileged client to control data devices. In - * particular, the client will be able to manage the current selection and take - * the role of a clipboard manager. - * - * Warning! The protocol described in this file is experimental and - * backward incompatible changes may be made. Backward compatible changes - * may be added together with the corresponding interface version bump. - * Backward incompatible changes are done by bumping the version number in - * the protocol and interface names and resetting the interface version. - * Once the protocol is to be declared stable, the 'z' prefix and the - * version number in the protocol and interface names are removed and the - * interface version number is reset. - * - * Note! This protocol is deprecated and not intended for production use. - * For clipboard management, use the ext-data-control-v1 protocol. - * - * @section page_ifaces_wlr_data_control_unstable_v1 Interfaces - * - @subpage page_iface_zwlr_data_control_manager_v1 - manager to control data devices - * - @subpage page_iface_zwlr_data_control_device_v1 - manage a data device for a seat - * - @subpage page_iface_zwlr_data_control_source_v1 - offer to transfer data - * - @subpage page_iface_zwlr_data_control_offer_v1 - offer to transfer data +/* * @section page_copyright_wlr_data_control_unstable_v1 Copyright - *
  *
  * Copyright © 2018 Simon Ser
  * Copyright © 2019 Ivan Molodetskikh
@@ -67,8 +39,35 @@ struct wl_resource;
  * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
  * THIS SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_seat; struct zwlr_data_control_device_v1; struct zwlr_data_control_manager_v1; @@ -77,158 +76,47 @@ struct zwlr_data_control_source_v1; #ifndef ZWLR_DATA_CONTROL_MANAGER_V1_INTERFACE #define ZWLR_DATA_CONTROL_MANAGER_V1_INTERFACE -/** - * @page page_iface_zwlr_data_control_manager_v1 zwlr_data_control_manager_v1 - * @section page_iface_zwlr_data_control_manager_v1_desc Description - * - * This interface is a manager that allows creating per-seat data device - * controls. - * @section page_iface_zwlr_data_control_manager_v1_api API - * See @ref iface_zwlr_data_control_manager_v1. - */ -/** - * @defgroup iface_zwlr_data_control_manager_v1 The zwlr_data_control_manager_v1 interface - * - * This interface is a manager that allows creating per-seat data device - * controls. - */ extern const struct wl_interface zwlr_data_control_manager_v1_interface; #endif #ifndef ZWLR_DATA_CONTROL_DEVICE_V1_INTERFACE #define ZWLR_DATA_CONTROL_DEVICE_V1_INTERFACE -/** - * @page page_iface_zwlr_data_control_device_v1 zwlr_data_control_device_v1 - * @section page_iface_zwlr_data_control_device_v1_desc Description - * - * This interface allows a client to manage a seat's selection. - * - * When the seat is destroyed, this object becomes inert. - * @section page_iface_zwlr_data_control_device_v1_api API - * See @ref iface_zwlr_data_control_device_v1. - */ -/** - * @defgroup iface_zwlr_data_control_device_v1 The zwlr_data_control_device_v1 interface - * - * This interface allows a client to manage a seat's selection. - * - * When the seat is destroyed, this object becomes inert. - */ extern const struct wl_interface zwlr_data_control_device_v1_interface; #endif #ifndef ZWLR_DATA_CONTROL_SOURCE_V1_INTERFACE #define ZWLR_DATA_CONTROL_SOURCE_V1_INTERFACE -/** - * @page page_iface_zwlr_data_control_source_v1 zwlr_data_control_source_v1 - * @section page_iface_zwlr_data_control_source_v1_desc Description - * - * The wlr_data_control_source object is the source side of a - * wlr_data_control_offer. It is created by the source client in a data - * transfer and provides a way to describe the offered data and a way to - * respond to requests to transfer the data. - * @section page_iface_zwlr_data_control_source_v1_api API - * See @ref iface_zwlr_data_control_source_v1. - */ -/** - * @defgroup iface_zwlr_data_control_source_v1 The zwlr_data_control_source_v1 interface - * - * The wlr_data_control_source object is the source side of a - * wlr_data_control_offer. It is created by the source client in a data - * transfer and provides a way to describe the offered data and a way to - * respond to requests to transfer the data. - */ extern const struct wl_interface zwlr_data_control_source_v1_interface; #endif #ifndef ZWLR_DATA_CONTROL_OFFER_V1_INTERFACE #define ZWLR_DATA_CONTROL_OFFER_V1_INTERFACE -/** - * @page page_iface_zwlr_data_control_offer_v1 zwlr_data_control_offer_v1 - * @section page_iface_zwlr_data_control_offer_v1_desc Description - * - * A wlr_data_control_offer represents a piece of data offered for transfer - * by another client (the source client). The offer describes the different - * MIME types that the data can be converted to and provides the mechanism - * for transferring the data directly from the source client. - * @section page_iface_zwlr_data_control_offer_v1_api API - * See @ref iface_zwlr_data_control_offer_v1. - */ -/** - * @defgroup iface_zwlr_data_control_offer_v1 The zwlr_data_control_offer_v1 interface - * - * A wlr_data_control_offer represents a piece of data offered for transfer - * by another client (the source client). The offer describes the different - * MIME types that the data can be converted to and provides the mechanism - * for transferring the data directly from the source client. - */ extern const struct wl_interface zwlr_data_control_offer_v1_interface; #endif -/** - * @ingroup iface_zwlr_data_control_manager_v1 - * @struct zwlr_data_control_manager_v1_interface - */ struct zwlr_data_control_manager_v1_interface { - /** - * create a new data source - * - * Create a new data source. - * @param id data source to create - */ void (*create_data_source)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * get a data device for a seat - * - * Create a data device that can be used to manage a seat's - * selection. - */ void (*get_data_device)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *seat); - /** - * destroy the manager - * - * All objects created by the manager will still remain valid, - * until their appropriate destroy request has been called. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; -/** - * @ingroup iface_zwlr_data_control_manager_v1 - */ #define ZWLR_DATA_CONTROL_MANAGER_V1_CREATE_DATA_SOURCE_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_manager_v1 - */ #define ZWLR_DATA_CONTROL_MANAGER_V1_GET_DATA_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_manager_v1 - */ #define ZWLR_DATA_CONTROL_MANAGER_V1_DESTROY_SINCE_VERSION 1 #ifndef ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM #define ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM enum zwlr_data_control_device_v1_error { - /** - * source given to set_selection or set_primary_selection was already used before - */ ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_USED_SOURCE = 1, }; -#endif /* ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM */ +#endif #ifndef ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM_IS_VALID #define ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_zwlr_data_control_device_v1 - * Validate a zwlr_data_control_device_v1 error value. - * - * @return true on success, false on error. - * @ref zwlr_data_control_device_v1_error - */ static inline bool zwlr_data_control_device_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -238,51 +126,14 @@ zwlr_data_control_device_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwlr_data_control_device_v1 - * @struct zwlr_data_control_device_v1_interface - */ struct zwlr_data_control_device_v1_interface { - /** - * copy data to the selection - * - * This request asks the compositor to set the selection to the - * data from the source on behalf of the client. - * - * The given source may not be used in any further set_selection or - * set_primary_selection requests. Attempting to use a previously - * used source is a protocol error. - * - * To unset the selection, set the source to NULL. - */ void (*set_selection)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *source); - /** - * destroy this data device - * - * Destroys the data device object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * copy data to the primary selection - * - * This request asks the compositor to set the primary selection - * to the data from the source on behalf of the client. - * - * The given source may not be used in any further set_selection or - * set_primary_selection requests. Attempting to use a previously - * used source is a protocol error. - * - * To unset the primary selection, set the source to NULL. - * - * The compositor will ignore this request if it does not support - * primary selection. - * @since 2 - */ void (*set_primary_selection)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *source); @@ -293,74 +144,33 @@ struct zwlr_data_control_device_v1_interface { #define ZWLR_DATA_CONTROL_DEVICE_V1_FINISHED 2 #define ZWLR_DATA_CONTROL_DEVICE_V1_PRIMARY_SELECTION 3 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_DATA_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_FINISHED_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_PRIMARY_SELECTION_SINCE_VERSION 2 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_SET_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_SET_PRIMARY_SELECTION_SINCE_VERSION 2 -/** - * @ingroup iface_zwlr_data_control_device_v1 - * Sends an data_offer event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwlr_data_control_device_v1_send_data_offer(struct wl_resource *resource_, struct wl_resource *id) { wl_resource_post_event(resource_, ZWLR_DATA_CONTROL_DEVICE_V1_DATA_OFFER, id); } -/** - * @ingroup iface_zwlr_data_control_device_v1 - * Sends an selection event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwlr_data_control_device_v1_send_selection(struct wl_resource *resource_, struct wl_resource *id) { wl_resource_post_event(resource_, ZWLR_DATA_CONTROL_DEVICE_V1_SELECTION, id); } -/** - * @ingroup iface_zwlr_data_control_device_v1 - * Sends an finished event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwlr_data_control_device_v1_send_finished(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWLR_DATA_CONTROL_DEVICE_V1_FINISHED); } -/** - * @ingroup iface_zwlr_data_control_device_v1 - * Sends an primary_selection event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwlr_data_control_device_v1_send_primary_selection(struct wl_resource *resource_, struct wl_resource *id) { @@ -370,22 +180,12 @@ zwlr_data_control_device_v1_send_primary_selection(struct wl_resource *resource_ #ifndef ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM #define ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM enum zwlr_data_control_source_v1_error { - /** - * offer sent after wlr_data_control_device.set_selection - */ ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_INVALID_OFFER = 1, }; -#endif /* ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM */ +#endif #ifndef ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM_IS_VALID #define ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_zwlr_data_control_source_v1 - * Validate a zwlr_data_control_source_v1 error value. - * - * @return true on success, false on error. - * @ref zwlr_data_control_source_v1_error - */ static inline bool zwlr_data_control_source_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -395,32 +195,12 @@ zwlr_data_control_source_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwlr_data_control_source_v1 - * @struct zwlr_data_control_source_v1_interface - */ struct zwlr_data_control_source_v1_interface { - /** - * add an offered MIME type - * - * This request adds a MIME type to the set of MIME types - * advertised to targets. Can be called several times to offer - * multiple types. - * - * Calling this after wlr_data_control_device.set_selection is a - * protocol error. - * @param mime_type MIME type offered by the data source - */ void (*offer)(struct wl_client *client, struct wl_resource *resource, const char *mime_type); - /** - * destroy this source - * - * Destroys the data source object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; @@ -428,106 +208,40 @@ struct zwlr_data_control_source_v1_interface { #define ZWLR_DATA_CONTROL_SOURCE_V1_SEND 0 #define ZWLR_DATA_CONTROL_SOURCE_V1_CANCELLED 1 -/** - * @ingroup iface_zwlr_data_control_source_v1 - */ #define ZWLR_DATA_CONTROL_SOURCE_V1_SEND_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_source_v1 - */ #define ZWLR_DATA_CONTROL_SOURCE_V1_CANCELLED_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_source_v1 - */ #define ZWLR_DATA_CONTROL_SOURCE_V1_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_source_v1 - */ #define ZWLR_DATA_CONTROL_SOURCE_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_source_v1 - * Sends an send event to the client owning the resource. - * @param resource_ The client's resource - * @param mime_type MIME type for the data - * @param fd file descriptor for the data - */ static inline void zwlr_data_control_source_v1_send_send(struct wl_resource *resource_, const char *mime_type, int32_t fd) { wl_resource_post_event(resource_, ZWLR_DATA_CONTROL_SOURCE_V1_SEND, mime_type, fd); } -/** - * @ingroup iface_zwlr_data_control_source_v1 - * Sends an cancelled event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwlr_data_control_source_v1_send_cancelled(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWLR_DATA_CONTROL_SOURCE_V1_CANCELLED); } -/** - * @ingroup iface_zwlr_data_control_offer_v1 - * @struct zwlr_data_control_offer_v1_interface - */ struct zwlr_data_control_offer_v1_interface { - /** - * request that the data is transferred - * - * To transfer the offered data, the client issues this request - * and indicates the MIME type it wants to receive. The transfer - * happens through the passed file descriptor (typically created - * with the pipe system call). The source client writes the data in - * the MIME type representation requested and then closes the file - * descriptor. - * - * The receiving client reads from the read end of the pipe until - * EOF and then closes its end, at which point the transfer is - * complete. - * - * This request may happen multiple times for different MIME types. - * @param mime_type MIME type desired by receiver - * @param fd file descriptor for data transfer - */ void (*receive)(struct wl_client *client, struct wl_resource *resource, const char *mime_type, int32_t fd); - /** - * destroy this offer - * - * Destroys the data offer object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; #define ZWLR_DATA_CONTROL_OFFER_V1_OFFER 0 -/** - * @ingroup iface_zwlr_data_control_offer_v1 - */ #define ZWLR_DATA_CONTROL_OFFER_V1_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_offer_v1 - */ #define ZWLR_DATA_CONTROL_OFFER_V1_RECEIVE_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_offer_v1 - */ #define ZWLR_DATA_CONTROL_OFFER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_offer_v1 - * Sends an offer event to the client owning the resource. - * @param resource_ The client's resource - * @param mime_type offered MIME type - */ static inline void zwlr_data_control_offer_v1_send_offer(struct wl_resource *resource_, const char *mime_type) { diff --git a/app/src/main/cpp/waylandcomp/generated/xdg-shell-client-protocol.h b/app/src/main/cpp/waylandcomp/generated/xdg-shell-client-protocol.h index 5280ba38b..c73ba799a 100644 --- a/app/src/main/cpp/waylandcomp/generated/xdg-shell-client-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/xdg-shell-client-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef XDG_SHELL_CLIENT_PROTOCOL_H #define XDG_SHELL_CLIENT_PROTOCOL_H @@ -11,16 +10,8 @@ extern "C" { #endif -/** - * @page page_xdg_shell The xdg_shell protocol - * @section page_ifaces_xdg_shell Interfaces - * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces - * - @subpage page_iface_xdg_positioner - child surface positioner - * - @subpage page_iface_xdg_surface - desktop user interface surface base interface - * - @subpage page_iface_xdg_toplevel - toplevel surface - * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus +/* * @section page_copyright_xdg_shell Copyright - *
  *
  * Copyright © 2008-2013 Kristian Høgsberg
  * Copyright © 2013      Rafael Antognolli
@@ -47,8 +38,16 @@ extern "C" {
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + struct wl_output; struct wl_seat; struct wl_surface; @@ -60,366 +59,44 @@ struct xdg_wm_base; #ifndef XDG_WM_BASE_INTERFACE #define XDG_WM_BASE_INTERFACE -/** - * @page page_iface_xdg_wm_base xdg_wm_base - * @section page_iface_xdg_wm_base_desc Description - * - * The xdg_wm_base interface is exposed as a global object enabling clients - * to turn their wl_surfaces into windows in a desktop environment. It - * defines the basic functionality needed for clients and the compositor to - * create windows that can be dragged, resized, maximized, etc, as well as - * creating transient windows such as popup menus. - * @section page_iface_xdg_wm_base_api API - * See @ref iface_xdg_wm_base. - */ -/** - * @defgroup iface_xdg_wm_base The xdg_wm_base interface - * - * The xdg_wm_base interface is exposed as a global object enabling clients - * to turn their wl_surfaces into windows in a desktop environment. It - * defines the basic functionality needed for clients and the compositor to - * create windows that can be dragged, resized, maximized, etc, as well as - * creating transient windows such as popup menus. - */ extern const struct wl_interface xdg_wm_base_interface; #endif #ifndef XDG_POSITIONER_INTERFACE #define XDG_POSITIONER_INTERFACE -/** - * @page page_iface_xdg_positioner xdg_positioner - * @section page_iface_xdg_positioner_desc Description - * - * The xdg_positioner provides a collection of rules for the placement of a - * child surface relative to a parent surface. Rules can be defined to ensure - * the child surface remains within the visible area's borders, and to - * specify how the child surface changes its position, such as sliding along - * an axis, or flipping around a rectangle. These positioner-created rules are - * constrained by the requirement that a child surface must intersect with or - * be at least partially adjacent to its parent surface. - * - * See the various requests for details about possible rules. - * - * At the time of the request, the compositor makes a copy of the rules - * specified by the xdg_positioner. Thus, after the request is complete the - * xdg_positioner object can be destroyed or reused; further changes to the - * object will have no effect on previous usages. - * - * For an xdg_positioner object to be considered complete, it must have a - * non-zero size set by set_size, and a non-zero anchor rectangle set by - * set_anchor_rect. Passing an incomplete xdg_positioner object when - * positioning a surface raises an invalid_positioner error. - * @section page_iface_xdg_positioner_api API - * See @ref iface_xdg_positioner. - */ -/** - * @defgroup iface_xdg_positioner The xdg_positioner interface - * - * The xdg_positioner provides a collection of rules for the placement of a - * child surface relative to a parent surface. Rules can be defined to ensure - * the child surface remains within the visible area's borders, and to - * specify how the child surface changes its position, such as sliding along - * an axis, or flipping around a rectangle. These positioner-created rules are - * constrained by the requirement that a child surface must intersect with or - * be at least partially adjacent to its parent surface. - * - * See the various requests for details about possible rules. - * - * At the time of the request, the compositor makes a copy of the rules - * specified by the xdg_positioner. Thus, after the request is complete the - * xdg_positioner object can be destroyed or reused; further changes to the - * object will have no effect on previous usages. - * - * For an xdg_positioner object to be considered complete, it must have a - * non-zero size set by set_size, and a non-zero anchor rectangle set by - * set_anchor_rect. Passing an incomplete xdg_positioner object when - * positioning a surface raises an invalid_positioner error. - */ extern const struct wl_interface xdg_positioner_interface; #endif #ifndef XDG_SURFACE_INTERFACE #define XDG_SURFACE_INTERFACE -/** - * @page page_iface_xdg_surface xdg_surface - * @section page_iface_xdg_surface_desc Description - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides a base set of functionality required to construct user - * interface elements requiring management by the compositor, such as - * toplevel windows, menus, etc. The types of functionality are split into - * xdg_surface roles. - * - * Creating an xdg_surface does not set the role for a wl_surface. In order - * to map an xdg_surface, the client must create a role-specific object - * using, e.g., get_toplevel, get_popup. The wl_surface for any given - * xdg_surface can have at most one role, and may not be assigned any role - * not based on xdg_surface. - * - * A role must be assigned before any other requests are made to the - * xdg_surface object. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_surface state to take effect. - * - * Creating an xdg_surface from a wl_surface which has a buffer attached or - * committed is a client error, and any attempts by a client to attach or - * manipulate a buffer prior to the first xdg_surface.configure call must - * also be treated as errors. - * - * After creating a role-specific object and setting it up, the client must - * perform an initial commit without any buffer attached. The compositor - * will reply with an xdg_surface.configure event. The client must - * acknowledge it and is then allowed to attach a buffer to map the surface. - * - * Mapping an xdg_surface-based role surface is defined as making it - * possible for the surface to be shown by the compositor. Note that - * a mapped surface is not guaranteed to be visible once it is mapped. - * - * For an xdg_surface to be mapped by the compositor, the following - * conditions must be met: - * (1) the client has assigned an xdg_surface-based role to the surface - * (2) the client has set and committed the xdg_surface state and the - * role-dependent state to the surface - * (3) the client has committed a buffer to the surface - * - * A newly-unmapped surface is considered to have met condition (1) out - * of the 3 required conditions for mapping a surface if its role surface - * has not been destroyed, i.e. the client must perform the initial commit - * again before attaching a buffer. - * @section page_iface_xdg_surface_api API - * See @ref iface_xdg_surface. - */ -/** - * @defgroup iface_xdg_surface The xdg_surface interface - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides a base set of functionality required to construct user - * interface elements requiring management by the compositor, such as - * toplevel windows, menus, etc. The types of functionality are split into - * xdg_surface roles. - * - * Creating an xdg_surface does not set the role for a wl_surface. In order - * to map an xdg_surface, the client must create a role-specific object - * using, e.g., get_toplevel, get_popup. The wl_surface for any given - * xdg_surface can have at most one role, and may not be assigned any role - * not based on xdg_surface. - * - * A role must be assigned before any other requests are made to the - * xdg_surface object. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_surface state to take effect. - * - * Creating an xdg_surface from a wl_surface which has a buffer attached or - * committed is a client error, and any attempts by a client to attach or - * manipulate a buffer prior to the first xdg_surface.configure call must - * also be treated as errors. - * - * After creating a role-specific object and setting it up, the client must - * perform an initial commit without any buffer attached. The compositor - * will reply with an xdg_surface.configure event. The client must - * acknowledge it and is then allowed to attach a buffer to map the surface. - * - * Mapping an xdg_surface-based role surface is defined as making it - * possible for the surface to be shown by the compositor. Note that - * a mapped surface is not guaranteed to be visible once it is mapped. - * - * For an xdg_surface to be mapped by the compositor, the following - * conditions must be met: - * (1) the client has assigned an xdg_surface-based role to the surface - * (2) the client has set and committed the xdg_surface state and the - * role-dependent state to the surface - * (3) the client has committed a buffer to the surface - * - * A newly-unmapped surface is considered to have met condition (1) out - * of the 3 required conditions for mapping a surface if its role surface - * has not been destroyed, i.e. the client must perform the initial commit - * again before attaching a buffer. - */ extern const struct wl_interface xdg_surface_interface; #endif #ifndef XDG_TOPLEVEL_INTERFACE #define XDG_TOPLEVEL_INTERFACE -/** - * @page page_iface_xdg_toplevel xdg_toplevel - * @section page_iface_xdg_toplevel_desc Description - * - * This interface defines an xdg_surface role which allows a surface to, - * among other things, set window-like properties such as maximize, - * fullscreen, and minimize, set application-specific metadata like title and - * id, and well as trigger user interactive operations such as interactive - * resize and move. - * - * Unmapping an xdg_toplevel means that the surface cannot be shown - * by the compositor until it is explicitly mapped again. - * All active operations (e.g., move, resize) are canceled and all - * attributes (e.g. title, state, stacking, ...) are discarded for - * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to - * the state it had right after xdg_surface.get_toplevel. The client - * can re-map the toplevel by performing a commit without any buffer - * attached, waiting for a configure event and handling it as usual (see - * xdg_surface description). - * - * Attaching a null buffer to a toplevel unmaps the surface. - * @section page_iface_xdg_toplevel_api API - * See @ref iface_xdg_toplevel. - */ -/** - * @defgroup iface_xdg_toplevel The xdg_toplevel interface - * - * This interface defines an xdg_surface role which allows a surface to, - * among other things, set window-like properties such as maximize, - * fullscreen, and minimize, set application-specific metadata like title and - * id, and well as trigger user interactive operations such as interactive - * resize and move. - * - * Unmapping an xdg_toplevel means that the surface cannot be shown - * by the compositor until it is explicitly mapped again. - * All active operations (e.g., move, resize) are canceled and all - * attributes (e.g. title, state, stacking, ...) are discarded for - * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to - * the state it had right after xdg_surface.get_toplevel. The client - * can re-map the toplevel by performing a commit without any buffer - * attached, waiting for a configure event and handling it as usual (see - * xdg_surface description). - * - * Attaching a null buffer to a toplevel unmaps the surface. - */ extern const struct wl_interface xdg_toplevel_interface; #endif #ifndef XDG_POPUP_INTERFACE #define XDG_POPUP_INTERFACE -/** - * @page page_iface_xdg_popup xdg_popup - * @section page_iface_xdg_popup_desc Description - * - * A popup surface is a short-lived, temporary surface. It can be used to - * implement for example menus, popovers, tooltips and other similar user - * interface concepts. - * - * A popup can be made to take an explicit grab. See xdg_popup.grab for - * details. - * - * When the popup is dismissed, a popup_done event will be sent out, and at - * the same time the surface will be unmapped. See the xdg_popup.popup_done - * event for details. - * - * Explicitly destroying the xdg_popup object will also dismiss the popup and - * unmap the surface. Clients that want to dismiss the popup when another - * surface of their own is clicked should dismiss the popup using the destroy - * request. - * - * A newly created xdg_popup will be stacked on top of all previously created - * xdg_popup surfaces associated with the same xdg_toplevel. - * - * The parent of an xdg_popup must be mapped (see the xdg_surface - * description) before the xdg_popup itself. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_popup state to take effect. - * @section page_iface_xdg_popup_api API - * See @ref iface_xdg_popup. - */ -/** - * @defgroup iface_xdg_popup The xdg_popup interface - * - * A popup surface is a short-lived, temporary surface. It can be used to - * implement for example menus, popovers, tooltips and other similar user - * interface concepts. - * - * A popup can be made to take an explicit grab. See xdg_popup.grab for - * details. - * - * When the popup is dismissed, a popup_done event will be sent out, and at - * the same time the surface will be unmapped. See the xdg_popup.popup_done - * event for details. - * - * Explicitly destroying the xdg_popup object will also dismiss the popup and - * unmap the surface. Clients that want to dismiss the popup when another - * surface of their own is clicked should dismiss the popup using the destroy - * request. - * - * A newly created xdg_popup will be stacked on top of all previously created - * xdg_popup surfaces associated with the same xdg_toplevel. - * - * The parent of an xdg_popup must be mapped (see the xdg_surface - * description) before the xdg_popup itself. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_popup state to take effect. - */ extern const struct wl_interface xdg_popup_interface; #endif #ifndef XDG_WM_BASE_ERROR_ENUM #define XDG_WM_BASE_ERROR_ENUM enum xdg_wm_base_error { - /** - * given wl_surface has another role - */ XDG_WM_BASE_ERROR_ROLE = 0, - /** - * xdg_wm_base was destroyed before children - */ XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1, - /** - * the client tried to map or destroy a non-topmost popup - */ XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2, - /** - * the client specified an invalid popup parent surface - */ XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3, - /** - * the client provided an invalid surface state - */ XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4, - /** - * the client provided an invalid positioner - */ XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5, - /** - * the client didn’t respond to a ping event in time - */ XDG_WM_BASE_ERROR_UNRESPONSIVE = 6, }; -#endif /* XDG_WM_BASE_ERROR_ENUM */ +#endif -/** - * @ingroup iface_xdg_wm_base - * @struct xdg_wm_base_listener - */ struct xdg_wm_base_listener { - /** - * check if the client is alive - * - * The ping event asks the client if it's still alive. Pass the - * serial specified in the event back to the compositor by sending - * a "pong" request back with the specified serial. See - * xdg_wm_base.pong. - * - * Compositors can use this to determine if the client is still - * alive. It's unspecified what will happen if the client doesn't - * respond to the ping request, or in what timeframe. Clients - * should try to respond in a reasonable amount of time. The - * “unresponsive” error is provided for compositors that wish - * to disconnect unresponsive clients. - * - * A compositor is free to ping in any way it wants, but a client - * must always respond to any xdg_wm_base object it created. - * @param serial pass this to the pong request - */ void (*ping)(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial); }; -/** - * @ingroup iface_xdg_wm_base - */ static inline int xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base, const struct xdg_wm_base_listener *listener, void *data) @@ -433,36 +110,19 @@ xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base, #define XDG_WM_BASE_GET_XDG_SURFACE 2 #define XDG_WM_BASE_PONG 3 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_PING_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_PONG_SINCE_VERSION 1 -/** @ingroup iface_xdg_wm_base */ static inline void xdg_wm_base_set_user_data(struct xdg_wm_base *xdg_wm_base, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) xdg_wm_base, user_data); } -/** @ingroup iface_xdg_wm_base */ static inline void * xdg_wm_base_get_user_data(struct xdg_wm_base *xdg_wm_base) { @@ -475,15 +135,6 @@ xdg_wm_base_get_version(struct xdg_wm_base *xdg_wm_base) return wl_proxy_get_version((struct wl_proxy *) xdg_wm_base); } -/** - * @ingroup iface_xdg_wm_base - * - * Destroy this xdg_wm_base object. - * - * Destroying a bound xdg_wm_base object while there are surfaces - * still alive created by this xdg_wm_base object instance is illegal - * and will result in a defunct_surfaces error. - */ static inline void xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base) { @@ -491,13 +142,6 @@ xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base) XDG_WM_BASE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_xdg_wm_base - * - * Create a positioner object. A positioner object is used to position - * surfaces relative to some parent surface. See the interface description - * and xdg_surface.get_popup for details. - */ static inline struct xdg_positioner * xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base) { @@ -509,23 +153,6 @@ xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base) return (struct xdg_positioner *) id; } -/** - * @ingroup iface_xdg_wm_base - * - * This creates an xdg_surface for the given surface. While xdg_surface - * itself is not a role, the corresponding surface may only be assigned - * a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is - * illegal to create an xdg_surface for a wl_surface which already has an - * assigned role and this will result in a role error. - * - * This creates an xdg_surface for the given surface. An xdg_surface is - * used as basis to define a role to a given surface, such as xdg_toplevel - * or xdg_popup. It also manages functionality shared between xdg_surface - * based surface roles. - * - * See the documentation of xdg_surface for more details about what an - * xdg_surface is and how it is used. - */ static inline struct xdg_surface * xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface) { @@ -537,13 +164,6 @@ xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface * return (struct xdg_surface *) id; } -/** - * @ingroup iface_xdg_wm_base - * - * A client must respond to a ping event with a pong request or - * the client may be deemed unresponsive. See xdg_wm_base.ping - * and xdg_wm_base.error.unresponsive. - */ static inline void xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial) { @@ -554,12 +174,9 @@ xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial) #ifndef XDG_POSITIONER_ERROR_ENUM #define XDG_POSITIONER_ERROR_ENUM enum xdg_positioner_error { - /** - * invalid input provided - */ XDG_POSITIONER_ERROR_INVALID_INPUT = 0, }; -#endif /* XDG_POSITIONER_ERROR_ENUM */ +#endif #ifndef XDG_POSITIONER_ANCHOR_ENUM #define XDG_POSITIONER_ANCHOR_ENUM @@ -574,7 +191,7 @@ enum xdg_positioner_anchor { XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7, XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8, }; -#endif /* XDG_POSITIONER_ANCHOR_ENUM */ +#endif #ifndef XDG_POSITIONER_GRAVITY_ENUM #define XDG_POSITIONER_GRAVITY_ENUM @@ -589,115 +206,20 @@ enum xdg_positioner_gravity { XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8, }; -#endif /* XDG_POSITIONER_GRAVITY_ENUM */ +#endif #ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM #define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM -/** - * @ingroup iface_xdg_positioner - * constraint adjustments - * - * The constraint adjustment value define ways the compositor will adjust - * the position of the surface, if the unadjusted position would result - * in the surface being partly constrained. - * - * Whether a surface is considered 'constrained' is left to the compositor - * to determine. For example, the surface may be partly outside the - * compositor's defined 'work area', thus necessitating the child surface's - * position be adjusted until it is entirely inside the work area. - * - * The adjustments can be combined, according to a defined precedence: 1) - * Flip, 2) Slide, 3) Resize. - */ enum xdg_positioner_constraint_adjustment { - /** - * don't move the child surface when constrained - * - * Don't alter the surface position even if it is constrained on - * some axis, for example partially outside the edge of an output. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0, - /** - * move along the x axis until unconstrained - * - * Slide the surface along the x axis until it is no longer - * constrained. - * - * First try to slide towards the direction of the gravity on the x - * axis until either the edge in the opposite direction of the - * gravity is unconstrained or the edge in the direction of the - * gravity is constrained. - * - * Then try to slide towards the opposite direction of the gravity - * on the x axis until either the edge in the direction of the - * gravity is unconstrained or the edge in the opposite direction - * of the gravity is constrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1, - /** - * move along the y axis until unconstrained - * - * Slide the surface along the y axis until it is no longer - * constrained. - * - * First try to slide towards the direction of the gravity on the y - * axis until either the edge in the opposite direction of the - * gravity is unconstrained or the edge in the direction of the - * gravity is constrained. - * - * Then try to slide towards the opposite direction of the gravity - * on the y axis until either the edge in the direction of the - * gravity is unconstrained or the edge in the opposite direction - * of the gravity is constrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2, - /** - * invert the anchor and gravity on the x axis - * - * Invert the anchor and gravity on the x axis if the surface is - * constrained on the x axis. For example, if the left edge of the - * surface is constrained, the gravity is 'left' and the anchor is - * 'left', change the gravity to 'right' and the anchor to 'right'. - * - * If the adjusted position also ends up being constrained, the - * resulting position of the flip_x adjustment will be the one - * before the adjustment. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4, - /** - * invert the anchor and gravity on the y axis - * - * Invert the anchor and gravity on the y axis if the surface is - * constrained on the y axis. For example, if the bottom edge of - * the surface is constrained, the gravity is 'bottom' and the - * anchor is 'bottom', change the gravity to 'top' and the anchor - * to 'top'. - * - * The adjusted position is calculated given the original anchor - * rectangle and offset, but with the new flipped anchor and - * gravity values. - * - * If the adjusted position also ends up being constrained, the - * resulting position of the flip_y adjustment will be the one - * before the adjustment. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8, - /** - * horizontally resize the surface - * - * Resize the surface horizontally so that it is completely - * unconstrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16, - /** - * vertically resize the surface - * - * Resize the surface vertically so that it is completely - * unconstrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32, }; -#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */ +#endif #define XDG_POSITIONER_DESTROY 0 #define XDG_POSITIONER_SET_SIZE 1 @@ -711,55 +233,23 @@ enum xdg_positioner_constraint_adjustment { #define XDG_POSITIONER_SET_PARENT_CONFIGURE 9 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3 -/** @ingroup iface_xdg_positioner */ static inline void xdg_positioner_set_user_data(struct xdg_positioner *xdg_positioner, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) xdg_positioner, user_data); } -/** @ingroup iface_xdg_positioner */ static inline void * xdg_positioner_get_user_data(struct xdg_positioner *xdg_positioner) { @@ -772,11 +262,6 @@ xdg_positioner_get_version(struct xdg_positioner *xdg_positioner) return wl_proxy_get_version((struct wl_proxy *) xdg_positioner); } -/** - * @ingroup iface_xdg_positioner - * - * Notify the compositor that the xdg_positioner will no longer be used. - */ static inline void xdg_positioner_destroy(struct xdg_positioner *xdg_positioner) { @@ -784,15 +269,6 @@ xdg_positioner_destroy(struct xdg_positioner *xdg_positioner) XDG_POSITIONER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_xdg_positioner - * - * Set the size of the surface that is to be positioned with the positioner - * object. The size is in surface-local coordinates and corresponds to the - * window geometry. See xdg_surface.set_window_geometry. - * - * If a zero or negative size is set the invalid_input error is raised. - */ static inline void xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, int32_t height) { @@ -800,20 +276,6 @@ xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, in XDG_POSITIONER_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, width, height); } -/** - * @ingroup iface_xdg_positioner - * - * Specify the anchor rectangle within the parent surface that the child - * surface will be placed relative to. The rectangle is relative to the - * window geometry as defined by xdg_surface.set_window_geometry of the - * parent surface. - * - * When the xdg_positioner object is used to position a child surface, the - * anchor rectangle may not extend outside the window geometry of the - * positioned child's parent surface. - * - * If a negative size is set the invalid_input error is raised. - */ static inline void xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -821,16 +283,6 @@ xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, XDG_POSITIONER_SET_ANCHOR_RECT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y, width, height); } -/** - * @ingroup iface_xdg_positioner - * - * Defines the anchor point for the anchor rectangle. The specified anchor - * is used derive an anchor point that the child surface will be - * positioned relative to. If a corner anchor is set (e.g. 'top_left' or - * 'bottom_right'), the anchor point will be at the specified corner; - * otherwise, the derived anchor point will be centered on the specified - * edge, or in the center of the anchor rectangle if no edge is specified. - */ static inline void xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor) { @@ -838,17 +290,6 @@ xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor XDG_POSITIONER_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, anchor); } -/** - * @ingroup iface_xdg_positioner - * - * Defines in what direction a surface should be positioned, relative to - * the anchor point of the parent surface. If a corner gravity is - * specified (e.g. 'bottom_right' or 'top_left'), then the child surface - * will be placed towards the specified gravity; otherwise, the child - * surface will be centered over the anchor point on any axis that had no - * gravity specified. If the gravity is not in the ‘gravity’ enum, an - * invalid_input error is raised. - */ static inline void xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravity) { @@ -856,23 +297,6 @@ xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravi XDG_POSITIONER_SET_GRAVITY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, gravity); } -/** - * @ingroup iface_xdg_positioner - * - * Specify how the window should be positioned if the originally intended - * position caused the surface to be constrained, meaning at least - * partially outside positioning boundaries set by the compositor. The - * adjustment is set by constructing a bitmask describing the adjustment to - * be made when the surface is constrained on that axis. - * - * If no bit for one axis is set, the compositor will assume that the child - * surface should not change its position on that axis when constrained. - * - * If more than one bit for one axis is set, the order of how adjustments - * are applied is specified in the corresponding adjustment descriptions. - * - * The default adjustment is none. - */ static inline void xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment) { @@ -880,21 +304,6 @@ xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, constraint_adjustment); } -/** - * @ingroup iface_xdg_positioner - * - * Specify the surface position offset relative to the position of the - * anchor on the anchor rectangle and the anchor on the surface. For - * example if the anchor of the anchor rectangle is at (x, y), the surface - * has the gravity bottom|right, and the offset is (ox, oy), the calculated - * surface position will be (x + ox, y + oy). The offset position of the - * surface is the one used for constraint testing. See - * set_constraint_adjustment. - * - * An example use case is placing a popup menu on top of a user interface - * element, while aligning the user interface element of the parent surface - * with some user interface element placed somewhere in the popup surface. - */ static inline void xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y) { @@ -902,16 +311,6 @@ xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int3 XDG_POSITIONER_SET_OFFSET, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y); } -/** - * @ingroup iface_xdg_positioner - * - * When set reactive, the surface is reconstrained if the conditions used - * for constraining changed, e.g. the parent window moved. - * - * If the conditions changed and the popup was reconstrained, an - * xdg_popup.configure event is sent with updated geometry, followed by an - * xdg_surface.configure event. - */ static inline void xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner) { @@ -919,17 +318,6 @@ xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner) XDG_POSITIONER_SET_REACTIVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0); } -/** - * @ingroup iface_xdg_positioner - * - * Set the parent window geometry the compositor should use when - * positioning the popup. The compositor may use this information to - * determine the future state the popup should be constrained using. If - * this doesn't match the dimension of the parent the popup is eventually - * positioned against, the behavior is undefined. - * - * The arguments are given in the surface-local coordinate space. - */ static inline void xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t parent_width, int32_t parent_height) { @@ -937,14 +325,6 @@ xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t pa XDG_POSITIONER_SET_PARENT_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, parent_width, parent_height); } -/** - * @ingroup iface_xdg_positioner - * - * Set the serial of an xdg_surface.configure event this positioner will be - * used in response to. The compositor may use this information together - * with set_parent_size to determine what future state the popup should be - * constrained using. - */ static inline void xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint32_t serial) { @@ -955,70 +335,21 @@ xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint3 #ifndef XDG_SURFACE_ERROR_ENUM #define XDG_SURFACE_ERROR_ENUM enum xdg_surface_error { - /** - * Surface was not fully constructed - */ XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1, - /** - * Surface was already constructed - */ XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2, - /** - * Attaching a buffer to an unconfigured surface - */ XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3, - /** - * Invalid serial number when acking a configure event - */ XDG_SURFACE_ERROR_INVALID_SERIAL = 4, - /** - * Width or height was zero or negative - */ XDG_SURFACE_ERROR_INVALID_SIZE = 5, - /** - * Surface was destroyed before its role object - */ XDG_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 6, }; -#endif /* XDG_SURFACE_ERROR_ENUM */ +#endif -/** - * @ingroup iface_xdg_surface - * @struct xdg_surface_listener - */ struct xdg_surface_listener { - /** - * suggest a surface change - * - * The configure event marks the end of a configure sequence. A - * configure sequence is a set of one or more events configuring - * the state of the xdg_surface, including the final - * xdg_surface.configure event. - * - * Where applicable, xdg_surface surface roles will during a - * configure sequence extend this event as a latched state sent as - * events before the xdg_surface.configure event. Such events - * should be considered to make up a set of atomically applied - * configuration states, where the xdg_surface.configure commits - * the accumulated state. - * - * Clients should arrange their surface for the new states, and - * then send an ack_configure request with the serial sent in this - * configure event at some point before committing the new surface. - * - * If the client receives multiple configure events before it can - * respond to one, it is free to discard all but the last event it - * received. - * @param serial serial of the configure event - */ void (*configure)(void *data, struct xdg_surface *xdg_surface, uint32_t serial); }; -/** - * @ingroup iface_xdg_surface - */ static inline int xdg_surface_add_listener(struct xdg_surface *xdg_surface, const struct xdg_surface_listener *listener, void *data) @@ -1033,40 +364,20 @@ xdg_surface_add_listener(struct xdg_surface *xdg_surface, #define XDG_SURFACE_SET_WINDOW_GEOMETRY 3 #define XDG_SURFACE_ACK_CONFIGURE 4 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1 -/** @ingroup iface_xdg_surface */ static inline void xdg_surface_set_user_data(struct xdg_surface *xdg_surface, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) xdg_surface, user_data); } -/** @ingroup iface_xdg_surface */ static inline void * xdg_surface_get_user_data(struct xdg_surface *xdg_surface) { @@ -1079,13 +390,6 @@ xdg_surface_get_version(struct xdg_surface *xdg_surface) return wl_proxy_get_version((struct wl_proxy *) xdg_surface); } -/** - * @ingroup iface_xdg_surface - * - * Destroy the xdg_surface object. An xdg_surface must only be destroyed - * after its role object has been destroyed, otherwise - * a defunct_role_object error is raised. - */ static inline void xdg_surface_destroy(struct xdg_surface *xdg_surface) { @@ -1093,15 +397,6 @@ xdg_surface_destroy(struct xdg_surface *xdg_surface) XDG_SURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_xdg_surface - * - * This creates an xdg_toplevel object for the given xdg_surface and gives - * the associated wl_surface the xdg_toplevel role. - * - * See the documentation of xdg_toplevel for more details about what an - * xdg_toplevel is and how it is used. - */ static inline struct xdg_toplevel * xdg_surface_get_toplevel(struct xdg_surface *xdg_surface) { @@ -1113,18 +408,6 @@ xdg_surface_get_toplevel(struct xdg_surface *xdg_surface) return (struct xdg_toplevel *) id; } -/** - * @ingroup iface_xdg_surface - * - * This creates an xdg_popup object for the given xdg_surface and gives - * the associated wl_surface the xdg_popup role. - * - * If null is passed as a parent, a parent surface must be specified using - * some other protocol, before committing the initial state. - * - * See the documentation of xdg_popup for more details about what an - * xdg_popup is and how it is used. - */ static inline struct xdg_popup * xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *parent, struct xdg_positioner *positioner) { @@ -1136,39 +419,6 @@ xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *paren return (struct xdg_popup *) id; } -/** - * @ingroup iface_xdg_surface - * - * The window geometry of a surface is its "visible bounds" from the - * user's perspective. Client-side decorations often have invisible - * portions like drop-shadows which should be ignored for the - * purposes of aligning, placing and constraining windows. - * - * The window geometry is double buffered, and will be applied at the - * time wl_surface.commit of the corresponding wl_surface is called. - * - * When maintaining a position, the compositor should treat the (x, y) - * coordinate of the window geometry as the top left corner of the window. - * A client changing the (x, y) window geometry coordinate should in - * general not alter the position of the window. - * - * Once the window geometry of the surface is set, it is not possible to - * unset it, and it will remain the same until set_window_geometry is - * called again, even if a new subsurface or buffer is attached. - * - * If never set, the value is the full bounds of the surface, - * including any subsurfaces. This updates dynamically on every - * commit. This unset is meant for extremely simple clients. - * - * The arguments are given in the surface-local coordinate space of - * the wl_surface associated with this xdg_surface. - * - * The width and height must be greater than zero. Setting an invalid size - * will raise an invalid_size error. When applied, the effective window - * geometry will be the set window geometry clamped to the bounding - * rectangle of the combined geometry of the surface of the xdg_surface and - * the associated subsurfaces. - */ static inline void xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -1176,42 +426,6 @@ xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int3 XDG_SURFACE_SET_WINDOW_GEOMETRY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, x, y, width, height); } -/** - * @ingroup iface_xdg_surface - * - * When a configure event is received, if a client commits the - * surface in response to the configure event, then the client - * must make an ack_configure request sometime before the commit - * request, passing along the serial of the configure event. - * - * For instance, for toplevel surfaces the compositor might use this - * information to move a surface to the top left only when the client has - * drawn itself for the maximized or fullscreen state. - * - * If the client receives multiple configure events before it - * can respond to one, it only has to ack the last configure event. - * Acking a configure event that was never sent raises an invalid_serial - * error. - * - * A client is not required to commit immediately after sending - * an ack_configure request - it may even ack_configure several times - * before its next surface commit. - * - * A client may send multiple ack_configure requests before committing, but - * only the last request sent before a commit indicates which configure - * event the client really is responding to. - * - * Sending an ack_configure request consumes the serial number sent with - * the request, as well as serial numbers sent by all configure events - * sent on this xdg_surface prior to the configure event referenced by - * the committed serial. - * - * It is an error to issue multiple ack_configure requests referencing a - * serial from the same configure event, or to issue an ack_configure - * request referencing a serial from a configure event issued before the - * event identified by the last ack_configure request for the same - * xdg_surface. Doing so will raise an invalid_serial error. - */ static inline void xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial) { @@ -1222,30 +436,14 @@ xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial) #ifndef XDG_TOPLEVEL_ERROR_ENUM #define XDG_TOPLEVEL_ERROR_ENUM enum xdg_toplevel_error { - /** - * provided value is not a valid variant of the resize_edge enum - */ XDG_TOPLEVEL_ERROR_INVALID_RESIZE_EDGE = 0, - /** - * invalid parent toplevel - */ XDG_TOPLEVEL_ERROR_INVALID_PARENT = 1, - /** - * client provided an invalid min or max size - */ XDG_TOPLEVEL_ERROR_INVALID_SIZE = 2, }; -#endif /* XDG_TOPLEVEL_ERROR_ENUM */ +#endif #ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM #define XDG_TOPLEVEL_RESIZE_EDGE_ENUM -/** - * @ingroup iface_xdg_toplevel - * edge values for resizing - * - * These values are used to indicate which edge of a surface - * is being dragged in a resize operation. - */ enum xdg_toplevel_resize_edge { XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0, XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1, @@ -1257,246 +455,53 @@ enum xdg_toplevel_resize_edge { XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9, XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10, }; -#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */ +#endif #ifndef XDG_TOPLEVEL_STATE_ENUM #define XDG_TOPLEVEL_STATE_ENUM -/** - * @ingroup iface_xdg_toplevel - * types of state on the surface - * - * The different state values used on the surface. This is designed for - * state values like maximized, fullscreen. It is paired with the - * configure event to ensure that both the client and the compositor - * setting the state can be synchronized. - * - * States set in this way are double-buffered. They will get applied on - * the next commit. - */ enum xdg_toplevel_state { - /** - * the surface is maximized - * the surface is maximized - * - * The surface is maximized. The window geometry specified in the - * configure event must be obeyed by the client. - * - * The client should draw without shadow or other decoration - * outside of the window geometry. - */ XDG_TOPLEVEL_STATE_MAXIMIZED = 1, - /** - * the surface is fullscreen - * the surface is fullscreen - * - * The surface is fullscreen. The window geometry specified in - * the configure event is a maximum; the client cannot resize - * beyond it. For a surface to cover the whole fullscreened area, - * the geometry dimensions must be obeyed by the client. For more - * details, see xdg_toplevel.set_fullscreen. - */ XDG_TOPLEVEL_STATE_FULLSCREEN = 2, - /** - * the surface is being resized - * the surface is being resized - * - * The surface is being resized. The window geometry specified in - * the configure event is a maximum; the client cannot resize - * beyond it. Clients that have aspect ratio or cell sizing - * configuration can use a smaller size, however. - */ XDG_TOPLEVEL_STATE_RESIZING = 3, - /** - * the surface is now activated - * the surface is now activated - * - * Client window decorations should be painted as if the window - * is active. Do not assume this means that the window actually has - * keyboard or pointer focus. - */ XDG_TOPLEVEL_STATE_ACTIVATED = 4, - /** - * the surface’s left edge is tiled - * - * The window is currently in a tiled layout and the left edge is - * considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_LEFT = 5, - /** - * the surface’s right edge is tiled - * - * The window is currently in a tiled layout and the right edge - * is considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_RIGHT = 6, - /** - * the surface’s top edge is tiled - * - * The window is currently in a tiled layout and the top edge is - * considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_TOP = 7, - /** - * the surface’s bottom edge is tiled - * - * The window is currently in a tiled layout and the bottom edge - * is considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8, }; -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2 -#endif /* XDG_TOPLEVEL_STATE_ENUM */ +#endif #ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM #define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM enum xdg_toplevel_wm_capabilities { - /** - * show_window_menu is available - */ XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU = 1, - /** - * set_maximized and unset_maximized are available - */ XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE = 2, - /** - * set_fullscreen and unset_fullscreen are available - */ XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN = 3, - /** - * set_minimized is available - */ XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE = 4, }; -#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM */ +#endif -/** - * @ingroup iface_xdg_toplevel - * @struct xdg_toplevel_listener - */ struct xdg_toplevel_listener { - /** - * suggest a surface change - * - * This configure event asks the client to resize its toplevel - * surface or to change its state. The configured state should not - * be applied immediately. See xdg_surface.configure for details. - * - * The width and height arguments specify a hint to the window - * about how its surface should be resized in window geometry - * coordinates. See set_window_geometry. - * - * If the width or height arguments are zero, it means the client - * should decide its own window dimension. This may happen when the - * compositor needs to configure the state of the surface but - * doesn't have any information about any previous or expected - * dimension. - * - * The states listed in the event specify how the width/height - * arguments should be interpreted, and possibly how it should be - * drawn. - * - * Clients must send an ack_configure in response to this event. - * See xdg_surface.configure and xdg_surface.ack_configure for - * details. - */ void (*configure)(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height, struct wl_array *states); - /** - * surface wants to be closed - * - * The close event is sent by the compositor when the user wants - * the surface to be closed. This should be equivalent to the user - * clicking the close button in client-side decorations, if your - * application has any. - * - * This is only a request that the user intends to close the - * window. The client may choose to ignore this request, or show a - * dialog to ask the user to save their data, etc. - */ void (*close)(void *data, struct xdg_toplevel *xdg_toplevel); - /** - * recommended window geometry bounds - * - * The configure_bounds event may be sent prior to a - * xdg_toplevel.configure event to communicate the bounds a window - * geometry size is recommended to constrain to. - * - * The passed width and height are in surface coordinate space. If - * width and height are 0, it means bounds is unknown and - * equivalent to as if no configure_bounds event was ever sent for - * this surface. - * - * The bounds can for example correspond to the size of a monitor - * excluding any panels or other shell components, so that a - * surface isn't created in a way that it cannot fit. - * - * The bounds may change at any point, and in such a case, a new - * xdg_toplevel.configure_bounds will be sent, followed by - * xdg_toplevel.configure and xdg_surface.configure. - * @since 4 - */ void (*configure_bounds)(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height); - /** - * compositor capabilities - * - * This event advertises the capabilities supported by the - * compositor. If a capability isn't supported, clients should hide - * or disable the UI elements that expose this functionality. For - * instance, if the compositor doesn't advertise support for - * minimized toplevels, a button triggering the set_minimized - * request should not be displayed. - * - * The compositor will ignore requests it doesn't support. For - * instance, a compositor which doesn't advertise support for - * minimized will ignore set_minimized requests. - * - * Compositors must send this event once before the first - * xdg_surface.configure event. When the capabilities change, - * compositors must send this event again and then send an - * xdg_surface.configure event. - * - * The configured state should not be applied immediately. See - * xdg_surface.configure for details. - * - * The capabilities are sent as an array of 32-bit unsigned - * integers in native endianness. - * @param capabilities array of 32-bit capabilities - * @since 5 - */ void (*wm_capabilities)(void *data, struct xdg_toplevel *xdg_toplevel, struct wl_array *capabilities); }; -/** - * @ingroup iface_xdg_toplevel - */ static inline int xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel, const struct xdg_toplevel_listener *listener, void *data) @@ -1520,88 +525,32 @@ xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel, #define XDG_TOPLEVEL_UNSET_FULLSCREEN 12 #define XDG_TOPLEVEL_SET_MINIMIZED 13 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION 4 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION 5 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1 -/** @ingroup iface_xdg_toplevel */ static inline void xdg_toplevel_set_user_data(struct xdg_toplevel *xdg_toplevel, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) xdg_toplevel, user_data); } -/** @ingroup iface_xdg_toplevel */ static inline void * xdg_toplevel_get_user_data(struct xdg_toplevel *xdg_toplevel) { @@ -1614,12 +563,6 @@ xdg_toplevel_get_version(struct xdg_toplevel *xdg_toplevel) return wl_proxy_get_version((struct wl_proxy *) xdg_toplevel); } -/** - * @ingroup iface_xdg_toplevel - * - * This request destroys the role surface and unmaps the surface; - * see "Unmapping" behavior in interface section for details. - */ static inline void xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel) { @@ -1627,30 +570,6 @@ xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel) XDG_TOPLEVEL_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_xdg_toplevel - * - * Set the "parent" of this surface. This surface should be stacked - * above the parent surface and all other ancestor surfaces. - * - * Parent surfaces should be set on dialogs, toolboxes, or other - * "auxiliary" surfaces, so that the parent is raised when the dialog - * is raised. - * - * Setting a null parent for a child surface unsets its parent. Setting - * a null parent for a surface which currently has no parent is a no-op. - * - * Only mapped surfaces can have child surfaces. Setting a parent which - * is not mapped is equivalent to setting a null parent. If a surface - * becomes unmapped, its children's parent is set to the parent of - * the now-unmapped surface. If the now-unmapped surface has no parent, - * its children's parent is unset. If the now-unmapped surface becomes - * mapped again, its parent-child relationship is not restored. - * - * The parent toplevel must not be one of the child toplevel's - * descendants, and the parent must be different from the child toplevel, - * otherwise the invalid_parent protocol error is raised. - */ static inline void xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel *parent) { @@ -1658,17 +577,6 @@ xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel * XDG_TOPLEVEL_SET_PARENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, parent); } -/** - * @ingroup iface_xdg_toplevel - * - * Set a short title for the surface. - * - * This string may be used to identify the surface in a task bar, - * window list, or other user interface elements provided by the - * compositor. - * - * The string must be encoded in UTF-8. - */ static inline void xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title) { @@ -1676,33 +584,6 @@ xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title) XDG_TOPLEVEL_SET_TITLE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, title); } -/** - * @ingroup iface_xdg_toplevel - * - * Set an application identifier for the surface. - * - * The app ID identifies the general class of applications to which - * the surface belongs. The compositor can use this to group multiple - * surfaces together, or to determine how to launch a new application. - * - * For D-Bus activatable applications, the app ID is used as the D-Bus - * service name. - * - * The compositor shell will try to group application surfaces together - * by their app ID. As a best practice, it is suggested to select app - * ID's that match the basename of the application's .desktop file. - * For example, "org.freedesktop.FooViewer" where the .desktop file is - * "org.freedesktop.FooViewer.desktop". - * - * Like other properties, a set_app_id request can be sent after the - * xdg_toplevel has been mapped to update the property. - * - * See the desktop-entry specification [0] for more details on - * application identifiers and how they relate to well-known D-Bus - * names and .desktop files. - * - * [0] https://standards.freedesktop.org/desktop-entry-spec/ - */ static inline void xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id) { @@ -1710,22 +591,6 @@ xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id) XDG_TOPLEVEL_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, app_id); } -/** - * @ingroup iface_xdg_toplevel - * - * Clients implementing client-side decorations might want to show - * a context menu when right-clicking on the decorations, giving the - * user a menu that they can use to maximize or minimize the window. - * - * This request asks the compositor to pop up such a window menu at - * the given position, relative to the local surface coordinates of - * the parent surface. There are no guarantees as to what menu items - * the window menu contains, or even if a window menu will be drawn - * at all. - * - * This request must be used in response to some sort of user action - * like a button press, key press, or touch down event. - */ static inline void xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y) { @@ -1733,26 +598,6 @@ xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat XDG_TOPLEVEL_SHOW_WINDOW_MENU, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, x, y); } -/** - * @ingroup iface_xdg_toplevel - * - * Start an interactive, user-driven move of the surface. - * - * This request must be used in response to some sort of user action - * like a button press, key press, or touch down event. The passed - * serial is used to determine the type of interactive move (touch, - * pointer, etc). - * - * The server may ignore move requests depending on the state of - * the surface (e.g. fullscreen or maximized), or if the passed serial - * is no longer valid. - * - * If triggered, the surface will lose the focus of the device - * (wl_pointer, wl_touch, etc) used for the move. It is up to the - * compositor to visually indicate that the move is taking place, such as - * updating a pointer cursor, during the move. There is no guarantee - * that the device focus will return when the move is completed. - */ static inline void xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial) { @@ -1760,41 +605,6 @@ xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint3 XDG_TOPLEVEL_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial); } -/** - * @ingroup iface_xdg_toplevel - * - * Start a user-driven, interactive resize of the surface. - * - * This request must be used in response to some sort of user action - * like a button press, key press, or touch down event. The passed - * serial is used to determine the type of interactive resize (touch, - * pointer, etc). - * - * The server may ignore resize requests depending on the state of - * the surface (e.g. fullscreen or maximized). - * - * If triggered, the client will receive configure events with the - * "resize" state enum value and the expected sizes. See the "resize" - * enum value for more details about what is required. The client - * must also acknowledge configure events using "ack_configure". After - * the resize is completed, the client will receive another "configure" - * event without the resize state. - * - * If triggered, the surface also will lose the focus of the device - * (wl_pointer, wl_touch, etc) used for the resize. It is up to the - * compositor to visually indicate that the resize is taking place, - * such as updating a pointer cursor, during the resize. There is no - * guarantee that the device focus will return when the resize is - * completed. - * - * The edges parameter specifies how the surface should be resized, and - * is one of the values of the resize_edge enum. Values not matching - * a variant of the enum will cause a protocol error. The compositor - * may use this information to update the surface position for example - * when dragging the top left corner. The compositor may also use - * this information to adapt its behavior, e.g. choose an appropriate - * cursor image. - */ static inline void xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, uint32_t edges) { @@ -1802,44 +612,6 @@ xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uin XDG_TOPLEVEL_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, edges); } -/** - * @ingroup iface_xdg_toplevel - * - * Set a maximum size for the window. - * - * The client can specify a maximum size so that the compositor does - * not try to configure the window beyond this size. - * - * The width and height arguments are in window geometry coordinates. - * See xdg_surface.set_window_geometry. - * - * Values set in this way are double-buffered. They will get applied - * on the next commit. - * - * The compositor can use this information to allow or disallow - * different states like maximize or fullscreen and draw accurate - * animations. - * - * Similarly, a tiling window manager may use this information to - * place and resize client windows in a more effective way. - * - * The client should not rely on the compositor to obey the maximum - * size. The compositor may decide to ignore the values set by the - * client and request a larger size. - * - * If never set, or a value of zero in the request, means that the - * client has no expected maximum size in the given dimension. - * As a result, a client wishing to reset the maximum size - * to an unspecified state can use zero for width and height in the - * request. - * - * Requesting a maximum size to be smaller than the minimum size of - * a surface is illegal and will result in an invalid_size error. - * - * The width and height must be greater than or equal to zero. Using - * strictly negative values for width or height will result in a - * invalid_size error. - */ static inline void xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) { @@ -1847,44 +619,6 @@ xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int3 XDG_TOPLEVEL_SET_MAX_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); } -/** - * @ingroup iface_xdg_toplevel - * - * Set a minimum size for the window. - * - * The client can specify a minimum size so that the compositor does - * not try to configure the window below this size. - * - * The width and height arguments are in window geometry coordinates. - * See xdg_surface.set_window_geometry. - * - * Values set in this way are double-buffered. They will get applied - * on the next commit. - * - * The compositor can use this information to allow or disallow - * different states like maximize or fullscreen and draw accurate - * animations. - * - * Similarly, a tiling window manager may use this information to - * place and resize client windows in a more effective way. - * - * The client should not rely on the compositor to obey the minimum - * size. The compositor may decide to ignore the values set by the - * client and request a smaller size. - * - * If never set, or a value of zero in the request, means that the - * client has no expected minimum size in the given dimension. - * As a result, a client wishing to reset the minimum size - * to an unspecified state can use zero for width and height in the - * request. - * - * Requesting a minimum size to be larger than the maximum size of - * a surface is illegal and will result in an invalid_size error. - * - * The width and height must be greater than or equal to zero. Using - * strictly negative values for width and height will result in a - * invalid_size error. - */ static inline void xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) { @@ -1892,29 +626,6 @@ xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int3 XDG_TOPLEVEL_SET_MIN_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); } -/** - * @ingroup iface_xdg_toplevel - * - * Maximize the surface. - * - * After requesting that the surface should be maximized, the compositor - * will respond by emitting a configure event. Whether this configure - * actually sets the window maximized is subject to compositor policies. - * The client must then update its content, drawing in the configured - * state. The client must also acknowledge the configure when committing - * the new content (see ack_configure). - * - * It is up to the compositor to decide how and where to maximize the - * surface, for example which output and what region of the screen should - * be used. - * - * If the surface was already maximized, the compositor will still emit - * a configure event with the "maximized" state. - * - * If the surface is in a fullscreen state, this request has no direct - * effect. It may alter the state the surface is returned to when - * unmaximized unless overridden by the compositor. - */ static inline void xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel) { @@ -1922,31 +633,6 @@ xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel) XDG_TOPLEVEL_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); } -/** - * @ingroup iface_xdg_toplevel - * - * Unmaximize the surface. - * - * After requesting that the surface should be unmaximized, the compositor - * will respond by emitting a configure event. Whether this actually - * un-maximizes the window is subject to compositor policies. - * If available and applicable, the compositor will include the window - * geometry dimensions the window had prior to being maximized in the - * configure event. The client must then update its content, drawing it in - * the configured state. The client must also acknowledge the configure - * when committing the new content (see ack_configure). - * - * It is up to the compositor to position the surface after it was - * unmaximized; usually the position the surface had before maximizing, if - * applicable. - * - * If the surface was already not maximized, the compositor will still - * emit a configure event without the "maximized" state. - * - * If the surface is in a fullscreen state, this request has no direct - * effect. It may alter the state the surface is returned to when - * unmaximized unless overridden by the compositor. - */ static inline void xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel) { @@ -1954,33 +640,6 @@ xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel) XDG_TOPLEVEL_UNSET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); } -/** - * @ingroup iface_xdg_toplevel - * - * Make the surface fullscreen. - * - * After requesting that the surface should be fullscreened, the - * compositor will respond by emitting a configure event. Whether the - * client is actually put into a fullscreen state is subject to compositor - * policies. The client must also acknowledge the configure when - * committing the new content (see ack_configure). - * - * The output passed by the request indicates the client's preference as - * to which display it should be set fullscreen on. If this value is NULL, - * it's up to the compositor to choose which display will be used to map - * this surface. - * - * If the surface doesn't cover the whole output, the compositor will - * position the surface in the center of the output and compensate with - * with border fill covering the rest of the output. The content of the - * border fill is undefined, but should be assumed to be in some way that - * attempts to blend into the surrounding area (e.g. solid black). - * - * If the fullscreened surface is not opaque, the compositor must make - * sure that other screen content not part of the same surface tree (made - * up of subsurfaces, popups or similarly coupled surfaces) are not - * visible below the fullscreened surface. - */ static inline void xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output *output) { @@ -1988,27 +647,6 @@ xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output XDG_TOPLEVEL_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, output); } -/** - * @ingroup iface_xdg_toplevel - * - * Make the surface no longer fullscreen. - * - * After requesting that the surface should be unfullscreened, the - * compositor will respond by emitting a configure event. - * Whether this actually removes the fullscreen state of the client is - * subject to compositor policies. - * - * Making a surface unfullscreen sets states for the surface based on the following: - * * the state(s) it may have had before becoming fullscreen - * * any state(s) decided by the compositor - * * any state(s) requested by the client while the surface was fullscreen - * - * The compositor may include the previous window geometry dimensions in - * the configure event, if applicable. - * - * The client must also acknowledge the configure when committing the new - * content (see ack_configure). - */ static inline void xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel) { @@ -2016,18 +654,6 @@ xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel) XDG_TOPLEVEL_UNSET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); } -/** - * @ingroup iface_xdg_toplevel - * - * Request that the compositor minimize your surface. There is no - * way to know if the surface is currently minimized, nor is there - * any way to unset minimization on this surface. - * - * If you are looking to throttle redrawing when minimized, please - * instead use the wl_surface.frame event for this, as this will - * also work with live previews on windows in Alt-Tab, Expose or - * similar compositor features. - */ static inline void xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel) { @@ -2038,85 +664,24 @@ xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel) #ifndef XDG_POPUP_ERROR_ENUM #define XDG_POPUP_ERROR_ENUM enum xdg_popup_error { - /** - * tried to grab after being mapped - */ XDG_POPUP_ERROR_INVALID_GRAB = 0, }; -#endif /* XDG_POPUP_ERROR_ENUM */ +#endif -/** - * @ingroup iface_xdg_popup - * @struct xdg_popup_listener - */ struct xdg_popup_listener { - /** - * configure the popup surface - * - * This event asks the popup surface to configure itself given - * the configuration. The configured state should not be applied - * immediately. See xdg_surface.configure for details. - * - * The x and y arguments represent the position the popup was - * placed at given the xdg_positioner rule, relative to the upper - * left corner of the window geometry of the parent surface. - * - * For version 2 or older, the configure event for an xdg_popup is - * only ever sent once for the initial configuration. Starting with - * version 3, it may be sent again if the popup is setup with an - * xdg_positioner with set_reactive requested, or in response to - * xdg_popup.reposition requests. - * @param x x position relative to parent surface window geometry - * @param y y position relative to parent surface window geometry - * @param width window geometry width - * @param height window geometry height - */ void (*configure)(void *data, struct xdg_popup *xdg_popup, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * popup interaction is done - * - * The popup_done event is sent out when a popup is dismissed by - * the compositor. The client should destroy the xdg_popup object - * at this point. - */ void (*popup_done)(void *data, struct xdg_popup *xdg_popup); - /** - * signal the completion of a repositioned request - * - * The repositioned event is sent as part of a popup - * configuration sequence, together with xdg_popup.configure and - * lastly xdg_surface.configure to notify the completion of a - * reposition request. - * - * The repositioned event is to notify about the completion of a - * xdg_popup.reposition request. The token argument is the token - * passed in the xdg_popup.reposition request. - * - * Immediately after this event is emitted, xdg_popup.configure and - * xdg_surface.configure will be sent with the updated size and - * position, as well as a new configure serial. - * - * The client should optionally update the content of the popup, - * but must acknowledge the new popup configuration for the new - * position to take effect. See xdg_surface.ack_configure for - * details. - * @param token reposition request token - * @since 3 - */ void (*repositioned)(void *data, struct xdg_popup *xdg_popup, uint32_t token); }; -/** - * @ingroup iface_xdg_popup - */ static inline int xdg_popup_add_listener(struct xdg_popup *xdg_popup, const struct xdg_popup_listener *listener, void *data) @@ -2129,40 +694,20 @@ xdg_popup_add_listener(struct xdg_popup *xdg_popup, #define XDG_POPUP_GRAB 1 #define XDG_POPUP_REPOSITION 2 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_GRAB_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_REPOSITION_SINCE_VERSION 3 -/** @ingroup iface_xdg_popup */ static inline void xdg_popup_set_user_data(struct xdg_popup *xdg_popup, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) xdg_popup, user_data); } -/** @ingroup iface_xdg_popup */ static inline void * xdg_popup_get_user_data(struct xdg_popup *xdg_popup) { @@ -2175,15 +720,6 @@ xdg_popup_get_version(struct xdg_popup *xdg_popup) return wl_proxy_get_version((struct wl_proxy *) xdg_popup); } -/** - * @ingroup iface_xdg_popup - * - * This destroys the popup. Explicitly destroying the xdg_popup - * object will also dismiss the popup, and unmap the surface. - * - * If this xdg_popup is not the "topmost" popup, a protocol error - * will be sent. - */ static inline void xdg_popup_destroy(struct xdg_popup *xdg_popup) { @@ -2191,47 +727,6 @@ xdg_popup_destroy(struct xdg_popup *xdg_popup) XDG_POPUP_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_xdg_popup - * - * This request makes the created popup take an explicit grab. An explicit - * grab will be dismissed when the user dismisses the popup, or when the - * client destroys the xdg_popup. This can be done by the user clicking - * outside the surface, using the keyboard, or even locking the screen - * through closing the lid or a timeout. - * - * If the compositor denies the grab, the popup will be immediately - * dismissed. - * - * This request must be used in response to some sort of user action like a - * button press, key press, or touch down event. The serial number of the - * event should be passed as 'serial'. - * - * The parent of a grabbing popup must either be an xdg_toplevel surface or - * another xdg_popup with an explicit grab. If the parent is another - * xdg_popup it means that the popups are nested, with this popup now being - * the topmost popup. - * - * Nested popups must be destroyed in the reverse order they were created - * in, e.g. the only popup you are allowed to destroy at all times is the - * topmost one. - * - * When compositors choose to dismiss a popup, they may dismiss every - * nested grabbing popup as well. When a compositor dismisses popups, it - * will follow the same dismissing order as required from the client. - * - * If the topmost grabbing popup is destroyed, the grab will be returned to - * the parent of the popup, if that parent previously had an explicit grab. - * - * If the parent is a grabbing popup which has already been dismissed, this - * popup will be immediately dismissed. If the parent is a popup that did - * not take an explicit grab, an error will be raised. - * - * During a popup grab, the client owning the grab will receive pointer - * and touch events for all their surfaces as normal (similar to an - * "owner-events" grab in X11 parlance), while the top most grabbing popup - * will always have keyboard focus. - */ static inline void xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial) { @@ -2239,33 +734,6 @@ xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t seria XDG_POPUP_GRAB, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, seat, serial); } -/** - * @ingroup iface_xdg_popup - * - * Reposition an already-mapped popup. The popup will be placed given the - * details in the passed xdg_positioner object, and a - * xdg_popup.repositioned followed by xdg_popup.configure and - * xdg_surface.configure will be emitted in response. Any parameters set - * by the previous positioner will be discarded. - * - * The passed token will be sent in the corresponding - * xdg_popup.repositioned event. The new popup position will not take - * effect until the corresponding configure event is acknowledged by the - * client. See xdg_popup.repositioned for details. The token itself is - * opaque, and has no other special meaning. - * - * If multiple reposition requests are sent, the compositor may skip all - * but the last one. - * - * If the popup is repositioned in response to a configure event for its - * parent, the client should send an xdg_positioner.set_parent_configure - * and possibly an xdg_positioner.set_parent_size request to allow the - * compositor to properly constrain the popup. - * - * If the popup is repositioned together with a parent that is being - * resized, but not in response to a configure event, the client should - * send an xdg_positioner.set_parent_size request. - */ static inline void xdg_popup_reposition(struct xdg_popup *xdg_popup, struct xdg_positioner *positioner, uint32_t token) { diff --git a/app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c b/app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c index 72b394a2b..d6a52b5de 100644 --- a/app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2008-2013 Kristian Høgsberg * Copyright © 2013 Rafael Antognolli @@ -34,7 +32,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/xdg-shell-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/xdg-shell-server-protocol.h index 2683694ce..825c971b2 100644 --- a/app/src/main/cpp/waylandcomp/generated/xdg-shell-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/xdg-shell-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef XDG_SHELL_SERVER_PROTOCOL_H #define XDG_SHELL_SERVER_PROTOCOL_H @@ -14,16 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_xdg_shell The xdg_shell protocol - * @section page_ifaces_xdg_shell Interfaces - * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces - * - @subpage page_iface_xdg_positioner - child surface positioner - * - @subpage page_iface_xdg_surface - desktop user interface surface base interface - * - @subpage page_iface_xdg_toplevel - toplevel surface - * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus +/* * @section page_copyright_xdg_shell Copyright - *
  *
  * Copyright © 2008-2013 Kristian Høgsberg
  * Copyright © 2013      Rafael Antognolli
@@ -50,8 +41,16 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + struct wl_output; struct wl_seat; struct wl_surface; @@ -63,343 +62,40 @@ struct xdg_wm_base; #ifndef XDG_WM_BASE_INTERFACE #define XDG_WM_BASE_INTERFACE -/** - * @page page_iface_xdg_wm_base xdg_wm_base - * @section page_iface_xdg_wm_base_desc Description - * - * The xdg_wm_base interface is exposed as a global object enabling clients - * to turn their wl_surfaces into windows in a desktop environment. It - * defines the basic functionality needed for clients and the compositor to - * create windows that can be dragged, resized, maximized, etc, as well as - * creating transient windows such as popup menus. - * @section page_iface_xdg_wm_base_api API - * See @ref iface_xdg_wm_base. - */ -/** - * @defgroup iface_xdg_wm_base The xdg_wm_base interface - * - * The xdg_wm_base interface is exposed as a global object enabling clients - * to turn their wl_surfaces into windows in a desktop environment. It - * defines the basic functionality needed for clients and the compositor to - * create windows that can be dragged, resized, maximized, etc, as well as - * creating transient windows such as popup menus. - */ extern const struct wl_interface xdg_wm_base_interface; #endif #ifndef XDG_POSITIONER_INTERFACE #define XDG_POSITIONER_INTERFACE -/** - * @page page_iface_xdg_positioner xdg_positioner - * @section page_iface_xdg_positioner_desc Description - * - * The xdg_positioner provides a collection of rules for the placement of a - * child surface relative to a parent surface. Rules can be defined to ensure - * the child surface remains within the visible area's borders, and to - * specify how the child surface changes its position, such as sliding along - * an axis, or flipping around a rectangle. These positioner-created rules are - * constrained by the requirement that a child surface must intersect with or - * be at least partially adjacent to its parent surface. - * - * See the various requests for details about possible rules. - * - * At the time of the request, the compositor makes a copy of the rules - * specified by the xdg_positioner. Thus, after the request is complete the - * xdg_positioner object can be destroyed or reused; further changes to the - * object will have no effect on previous usages. - * - * For an xdg_positioner object to be considered complete, it must have a - * non-zero size set by set_size, and a non-zero anchor rectangle set by - * set_anchor_rect. Passing an incomplete xdg_positioner object when - * positioning a surface raises an invalid_positioner error. - * @section page_iface_xdg_positioner_api API - * See @ref iface_xdg_positioner. - */ -/** - * @defgroup iface_xdg_positioner The xdg_positioner interface - * - * The xdg_positioner provides a collection of rules for the placement of a - * child surface relative to a parent surface. Rules can be defined to ensure - * the child surface remains within the visible area's borders, and to - * specify how the child surface changes its position, such as sliding along - * an axis, or flipping around a rectangle. These positioner-created rules are - * constrained by the requirement that a child surface must intersect with or - * be at least partially adjacent to its parent surface. - * - * See the various requests for details about possible rules. - * - * At the time of the request, the compositor makes a copy of the rules - * specified by the xdg_positioner. Thus, after the request is complete the - * xdg_positioner object can be destroyed or reused; further changes to the - * object will have no effect on previous usages. - * - * For an xdg_positioner object to be considered complete, it must have a - * non-zero size set by set_size, and a non-zero anchor rectangle set by - * set_anchor_rect. Passing an incomplete xdg_positioner object when - * positioning a surface raises an invalid_positioner error. - */ extern const struct wl_interface xdg_positioner_interface; #endif #ifndef XDG_SURFACE_INTERFACE #define XDG_SURFACE_INTERFACE -/** - * @page page_iface_xdg_surface xdg_surface - * @section page_iface_xdg_surface_desc Description - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides a base set of functionality required to construct user - * interface elements requiring management by the compositor, such as - * toplevel windows, menus, etc. The types of functionality are split into - * xdg_surface roles. - * - * Creating an xdg_surface does not set the role for a wl_surface. In order - * to map an xdg_surface, the client must create a role-specific object - * using, e.g., get_toplevel, get_popup. The wl_surface for any given - * xdg_surface can have at most one role, and may not be assigned any role - * not based on xdg_surface. - * - * A role must be assigned before any other requests are made to the - * xdg_surface object. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_surface state to take effect. - * - * Creating an xdg_surface from a wl_surface which has a buffer attached or - * committed is a client error, and any attempts by a client to attach or - * manipulate a buffer prior to the first xdg_surface.configure call must - * also be treated as errors. - * - * After creating a role-specific object and setting it up, the client must - * perform an initial commit without any buffer attached. The compositor - * will reply with an xdg_surface.configure event. The client must - * acknowledge it and is then allowed to attach a buffer to map the surface. - * - * Mapping an xdg_surface-based role surface is defined as making it - * possible for the surface to be shown by the compositor. Note that - * a mapped surface is not guaranteed to be visible once it is mapped. - * - * For an xdg_surface to be mapped by the compositor, the following - * conditions must be met: - * (1) the client has assigned an xdg_surface-based role to the surface - * (2) the client has set and committed the xdg_surface state and the - * role-dependent state to the surface - * (3) the client has committed a buffer to the surface - * - * A newly-unmapped surface is considered to have met condition (1) out - * of the 3 required conditions for mapping a surface if its role surface - * has not been destroyed, i.e. the client must perform the initial commit - * again before attaching a buffer. - * @section page_iface_xdg_surface_api API - * See @ref iface_xdg_surface. - */ -/** - * @defgroup iface_xdg_surface The xdg_surface interface - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides a base set of functionality required to construct user - * interface elements requiring management by the compositor, such as - * toplevel windows, menus, etc. The types of functionality are split into - * xdg_surface roles. - * - * Creating an xdg_surface does not set the role for a wl_surface. In order - * to map an xdg_surface, the client must create a role-specific object - * using, e.g., get_toplevel, get_popup. The wl_surface for any given - * xdg_surface can have at most one role, and may not be assigned any role - * not based on xdg_surface. - * - * A role must be assigned before any other requests are made to the - * xdg_surface object. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_surface state to take effect. - * - * Creating an xdg_surface from a wl_surface which has a buffer attached or - * committed is a client error, and any attempts by a client to attach or - * manipulate a buffer prior to the first xdg_surface.configure call must - * also be treated as errors. - * - * After creating a role-specific object and setting it up, the client must - * perform an initial commit without any buffer attached. The compositor - * will reply with an xdg_surface.configure event. The client must - * acknowledge it and is then allowed to attach a buffer to map the surface. - * - * Mapping an xdg_surface-based role surface is defined as making it - * possible for the surface to be shown by the compositor. Note that - * a mapped surface is not guaranteed to be visible once it is mapped. - * - * For an xdg_surface to be mapped by the compositor, the following - * conditions must be met: - * (1) the client has assigned an xdg_surface-based role to the surface - * (2) the client has set and committed the xdg_surface state and the - * role-dependent state to the surface - * (3) the client has committed a buffer to the surface - * - * A newly-unmapped surface is considered to have met condition (1) out - * of the 3 required conditions for mapping a surface if its role surface - * has not been destroyed, i.e. the client must perform the initial commit - * again before attaching a buffer. - */ extern const struct wl_interface xdg_surface_interface; #endif #ifndef XDG_TOPLEVEL_INTERFACE #define XDG_TOPLEVEL_INTERFACE -/** - * @page page_iface_xdg_toplevel xdg_toplevel - * @section page_iface_xdg_toplevel_desc Description - * - * This interface defines an xdg_surface role which allows a surface to, - * among other things, set window-like properties such as maximize, - * fullscreen, and minimize, set application-specific metadata like title and - * id, and well as trigger user interactive operations such as interactive - * resize and move. - * - * Unmapping an xdg_toplevel means that the surface cannot be shown - * by the compositor until it is explicitly mapped again. - * All active operations (e.g., move, resize) are canceled and all - * attributes (e.g. title, state, stacking, ...) are discarded for - * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to - * the state it had right after xdg_surface.get_toplevel. The client - * can re-map the toplevel by performing a commit without any buffer - * attached, waiting for a configure event and handling it as usual (see - * xdg_surface description). - * - * Attaching a null buffer to a toplevel unmaps the surface. - * @section page_iface_xdg_toplevel_api API - * See @ref iface_xdg_toplevel. - */ -/** - * @defgroup iface_xdg_toplevel The xdg_toplevel interface - * - * This interface defines an xdg_surface role which allows a surface to, - * among other things, set window-like properties such as maximize, - * fullscreen, and minimize, set application-specific metadata like title and - * id, and well as trigger user interactive operations such as interactive - * resize and move. - * - * Unmapping an xdg_toplevel means that the surface cannot be shown - * by the compositor until it is explicitly mapped again. - * All active operations (e.g., move, resize) are canceled and all - * attributes (e.g. title, state, stacking, ...) are discarded for - * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to - * the state it had right after xdg_surface.get_toplevel. The client - * can re-map the toplevel by performing a commit without any buffer - * attached, waiting for a configure event and handling it as usual (see - * xdg_surface description). - * - * Attaching a null buffer to a toplevel unmaps the surface. - */ extern const struct wl_interface xdg_toplevel_interface; #endif #ifndef XDG_POPUP_INTERFACE #define XDG_POPUP_INTERFACE -/** - * @page page_iface_xdg_popup xdg_popup - * @section page_iface_xdg_popup_desc Description - * - * A popup surface is a short-lived, temporary surface. It can be used to - * implement for example menus, popovers, tooltips and other similar user - * interface concepts. - * - * A popup can be made to take an explicit grab. See xdg_popup.grab for - * details. - * - * When the popup is dismissed, a popup_done event will be sent out, and at - * the same time the surface will be unmapped. See the xdg_popup.popup_done - * event for details. - * - * Explicitly destroying the xdg_popup object will also dismiss the popup and - * unmap the surface. Clients that want to dismiss the popup when another - * surface of their own is clicked should dismiss the popup using the destroy - * request. - * - * A newly created xdg_popup will be stacked on top of all previously created - * xdg_popup surfaces associated with the same xdg_toplevel. - * - * The parent of an xdg_popup must be mapped (see the xdg_surface - * description) before the xdg_popup itself. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_popup state to take effect. - * @section page_iface_xdg_popup_api API - * See @ref iface_xdg_popup. - */ -/** - * @defgroup iface_xdg_popup The xdg_popup interface - * - * A popup surface is a short-lived, temporary surface. It can be used to - * implement for example menus, popovers, tooltips and other similar user - * interface concepts. - * - * A popup can be made to take an explicit grab. See xdg_popup.grab for - * details. - * - * When the popup is dismissed, a popup_done event will be sent out, and at - * the same time the surface will be unmapped. See the xdg_popup.popup_done - * event for details. - * - * Explicitly destroying the xdg_popup object will also dismiss the popup and - * unmap the surface. Clients that want to dismiss the popup when another - * surface of their own is clicked should dismiss the popup using the destroy - * request. - * - * A newly created xdg_popup will be stacked on top of all previously created - * xdg_popup surfaces associated with the same xdg_toplevel. - * - * The parent of an xdg_popup must be mapped (see the xdg_surface - * description) before the xdg_popup itself. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_popup state to take effect. - */ extern const struct wl_interface xdg_popup_interface; #endif #ifndef XDG_WM_BASE_ERROR_ENUM #define XDG_WM_BASE_ERROR_ENUM enum xdg_wm_base_error { - /** - * given wl_surface has another role - */ XDG_WM_BASE_ERROR_ROLE = 0, - /** - * xdg_wm_base was destroyed before children - */ XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1, - /** - * the client tried to map or destroy a non-topmost popup - */ XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2, - /** - * the client specified an invalid popup parent surface - */ XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3, - /** - * the client provided an invalid surface state - */ XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4, - /** - * the client provided an invalid positioner - */ XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5, - /** - * the client didn’t respond to a ping event in time - */ XDG_WM_BASE_ERROR_UNRESPONSIVE = 6, }; -#endif /* XDG_WM_BASE_ERROR_ENUM */ +#endif #ifndef XDG_WM_BASE_ERROR_ENUM_IS_VALID #define XDG_WM_BASE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_wm_base - * Validate a xdg_wm_base error value. - * - * @return true on success, false on error. - * @ref xdg_wm_base_error - */ static inline bool xdg_wm_base_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -421,64 +117,18 @@ xdg_wm_base_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_WM_BASE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_wm_base - * @struct xdg_wm_base_interface - */ struct xdg_wm_base_interface { - /** - * destroy xdg_wm_base - * - * Destroy this xdg_wm_base object. - * - * Destroying a bound xdg_wm_base object while there are surfaces - * still alive created by this xdg_wm_base object instance is - * illegal and will result in a defunct_surfaces error. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a positioner object - * - * Create a positioner object. A positioner object is used to - * position surfaces relative to some parent surface. See the - * interface description and xdg_surface.get_popup for details. - */ void (*create_positioner)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * create a shell surface from a surface - * - * This creates an xdg_surface for the given surface. While - * xdg_surface itself is not a role, the corresponding surface may - * only be assigned a role extending xdg_surface, such as - * xdg_toplevel or xdg_popup. It is illegal to create an - * xdg_surface for a wl_surface which already has an assigned role - * and this will result in a role error. - * - * This creates an xdg_surface for the given surface. An - * xdg_surface is used as basis to define a role to a given - * surface, such as xdg_toplevel or xdg_popup. It also manages - * functionality shared between xdg_surface based surface roles. - * - * See the documentation of xdg_surface for more details about what - * an xdg_surface is and how it is used. - */ void (*get_xdg_surface)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *surface); - /** - * respond to a ping event - * - * A client must respond to a ping event with a pong request or - * the client may be deemed unresponsive. See xdg_wm_base.ping and - * xdg_wm_base.error.unresponsive. - * @param serial serial of the ping event - */ void (*pong)(struct wl_client *client, struct wl_resource *resource, uint32_t serial); @@ -486,34 +136,13 @@ struct xdg_wm_base_interface { #define XDG_WM_BASE_PING 0 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_PING_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_PONG_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - * Sends an ping event to the client owning the resource. - * @param resource_ The client's resource - * @param serial pass this to the pong request - */ static inline void xdg_wm_base_send_ping(struct wl_resource *resource_, uint32_t serial) { @@ -523,22 +152,12 @@ xdg_wm_base_send_ping(struct wl_resource *resource_, uint32_t serial) #ifndef XDG_POSITIONER_ERROR_ENUM #define XDG_POSITIONER_ERROR_ENUM enum xdg_positioner_error { - /** - * invalid input provided - */ XDG_POSITIONER_ERROR_INVALID_INPUT = 0, }; -#endif /* XDG_POSITIONER_ERROR_ENUM */ +#endif #ifndef XDG_POSITIONER_ERROR_ENUM_IS_VALID #define XDG_POSITIONER_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_positioner - * Validate a xdg_positioner error value. - * - * @return true on success, false on error. - * @ref xdg_positioner_error - */ static inline bool xdg_positioner_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -548,7 +167,7 @@ xdg_positioner_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_POSITIONER_ERROR_ENUM_IS_VALID */ +#endif #ifndef XDG_POSITIONER_ANCHOR_ENUM #define XDG_POSITIONER_ANCHOR_ENUM @@ -563,17 +182,10 @@ enum xdg_positioner_anchor { XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7, XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8, }; -#endif /* XDG_POSITIONER_ANCHOR_ENUM */ +#endif #ifndef XDG_POSITIONER_ANCHOR_ENUM_IS_VALID #define XDG_POSITIONER_ANCHOR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_positioner - * Validate a xdg_positioner anchor value. - * - * @return true on success, false on error. - * @ref xdg_positioner_anchor - */ static inline bool xdg_positioner_anchor_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -599,7 +211,7 @@ xdg_positioner_anchor_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_POSITIONER_ANCHOR_ENUM_IS_VALID */ +#endif #ifndef XDG_POSITIONER_GRAVITY_ENUM #define XDG_POSITIONER_GRAVITY_ENUM @@ -614,17 +226,10 @@ enum xdg_positioner_gravity { XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8, }; -#endif /* XDG_POSITIONER_GRAVITY_ENUM */ +#endif #ifndef XDG_POSITIONER_GRAVITY_ENUM_IS_VALID #define XDG_POSITIONER_GRAVITY_ENUM_IS_VALID -/** - * @ingroup iface_xdg_positioner - * Validate a xdg_positioner gravity value. - * - * @return true on success, false on error. - * @ref xdg_positioner_gravity - */ static inline bool xdg_positioner_gravity_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -650,125 +255,23 @@ xdg_positioner_gravity_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_POSITIONER_GRAVITY_ENUM_IS_VALID */ +#endif #ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM #define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM -/** - * @ingroup iface_xdg_positioner - * constraint adjustments - * - * The constraint adjustment value define ways the compositor will adjust - * the position of the surface, if the unadjusted position would result - * in the surface being partly constrained. - * - * Whether a surface is considered 'constrained' is left to the compositor - * to determine. For example, the surface may be partly outside the - * compositor's defined 'work area', thus necessitating the child surface's - * position be adjusted until it is entirely inside the work area. - * - * The adjustments can be combined, according to a defined precedence: 1) - * Flip, 2) Slide, 3) Resize. - */ enum xdg_positioner_constraint_adjustment { - /** - * don't move the child surface when constrained - * - * Don't alter the surface position even if it is constrained on - * some axis, for example partially outside the edge of an output. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0, - /** - * move along the x axis until unconstrained - * - * Slide the surface along the x axis until it is no longer - * constrained. - * - * First try to slide towards the direction of the gravity on the x - * axis until either the edge in the opposite direction of the - * gravity is unconstrained or the edge in the direction of the - * gravity is constrained. - * - * Then try to slide towards the opposite direction of the gravity - * on the x axis until either the edge in the direction of the - * gravity is unconstrained or the edge in the opposite direction - * of the gravity is constrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1, - /** - * move along the y axis until unconstrained - * - * Slide the surface along the y axis until it is no longer - * constrained. - * - * First try to slide towards the direction of the gravity on the y - * axis until either the edge in the opposite direction of the - * gravity is unconstrained or the edge in the direction of the - * gravity is constrained. - * - * Then try to slide towards the opposite direction of the gravity - * on the y axis until either the edge in the direction of the - * gravity is unconstrained or the edge in the opposite direction - * of the gravity is constrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2, - /** - * invert the anchor and gravity on the x axis - * - * Invert the anchor and gravity on the x axis if the surface is - * constrained on the x axis. For example, if the left edge of the - * surface is constrained, the gravity is 'left' and the anchor is - * 'left', change the gravity to 'right' and the anchor to 'right'. - * - * If the adjusted position also ends up being constrained, the - * resulting position of the flip_x adjustment will be the one - * before the adjustment. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4, - /** - * invert the anchor and gravity on the y axis - * - * Invert the anchor and gravity on the y axis if the surface is - * constrained on the y axis. For example, if the bottom edge of - * the surface is constrained, the gravity is 'bottom' and the - * anchor is 'bottom', change the gravity to 'top' and the anchor - * to 'top'. - * - * The adjusted position is calculated given the original anchor - * rectangle and offset, but with the new flipped anchor and - * gravity values. - * - * If the adjusted position also ends up being constrained, the - * resulting position of the flip_y adjustment will be the one - * before the adjustment. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8, - /** - * horizontally resize the surface - * - * Resize the surface horizontally so that it is completely - * unconstrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16, - /** - * vertically resize the surface - * - * Resize the surface vertically so that it is completely - * unconstrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32, }; -#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */ +#endif #ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM_IS_VALID #define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM_IS_VALID -/** - * @ingroup iface_xdg_positioner - * Validate a xdg_positioner constraint_adjustment value. - * - * @return true on success, false on error. - * @ref xdg_positioner_constraint_adjustment - */ static inline bool xdg_positioner_constraint_adjustment_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -788,268 +291,71 @@ xdg_positioner_constraint_adjustment_is_valid(uint32_t value, uint32_t version) valid |= XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y; return (value & ~valid) == 0; } -#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_positioner - * @struct xdg_positioner_interface - */ struct xdg_positioner_interface { - /** - * destroy the xdg_positioner object - * - * Notify the compositor that the xdg_positioner will no longer - * be used. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the size of the to-be positioned rectangle - * - * Set the size of the surface that is to be positioned with the - * positioner object. The size is in surface-local coordinates and - * corresponds to the window geometry. See - * xdg_surface.set_window_geometry. - * - * If a zero or negative size is set the invalid_input error is - * raised. - * @param width width of positioned rectangle - * @param height height of positioned rectangle - */ void (*set_size)(struct wl_client *client, struct wl_resource *resource, int32_t width, int32_t height); - /** - * set the anchor rectangle within the parent surface - * - * Specify the anchor rectangle within the parent surface that - * the child surface will be placed relative to. The rectangle is - * relative to the window geometry as defined by - * xdg_surface.set_window_geometry of the parent surface. - * - * When the xdg_positioner object is used to position a child - * surface, the anchor rectangle may not extend outside the window - * geometry of the positioned child's parent surface. - * - * If a negative size is set the invalid_input error is raised. - * @param x x position of anchor rectangle - * @param y y position of anchor rectangle - * @param width width of anchor rectangle - * @param height height of anchor rectangle - */ void (*set_anchor_rect)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * set anchor rectangle anchor - * - * Defines the anchor point for the anchor rectangle. The - * specified anchor is used derive an anchor point that the child - * surface will be positioned relative to. If a corner anchor is - * set (e.g. 'top_left' or 'bottom_right'), the anchor point will - * be at the specified corner; otherwise, the derived anchor point - * will be centered on the specified edge, or in the center of the - * anchor rectangle if no edge is specified. - * @param anchor anchor - */ void (*set_anchor)(struct wl_client *client, struct wl_resource *resource, uint32_t anchor); - /** - * set child surface gravity - * - * Defines in what direction a surface should be positioned, - * relative to the anchor point of the parent surface. If a corner - * gravity is specified (e.g. 'bottom_right' or 'top_left'), then - * the child surface will be placed towards the specified gravity; - * otherwise, the child surface will be centered over the anchor - * point on any axis that had no gravity specified. If the gravity - * is not in the ‘gravity’ enum, an invalid_input error is - * raised. - * @param gravity gravity direction - */ void (*set_gravity)(struct wl_client *client, struct wl_resource *resource, uint32_t gravity); - /** - * set the adjustment to be done when constrained - * - * Specify how the window should be positioned if the originally - * intended position caused the surface to be constrained, meaning - * at least partially outside positioning boundaries set by the - * compositor. The adjustment is set by constructing a bitmask - * describing the adjustment to be made when the surface is - * constrained on that axis. - * - * If no bit for one axis is set, the compositor will assume that - * the child surface should not change its position on that axis - * when constrained. - * - * If more than one bit for one axis is set, the order of how - * adjustments are applied is specified in the corresponding - * adjustment descriptions. - * - * The default adjustment is none. - * @param constraint_adjustment bit mask of constraint adjustments - */ void (*set_constraint_adjustment)(struct wl_client *client, struct wl_resource *resource, uint32_t constraint_adjustment); - /** - * set surface position offset - * - * Specify the surface position offset relative to the position - * of the anchor on the anchor rectangle and the anchor on the - * surface. For example if the anchor of the anchor rectangle is at - * (x, y), the surface has the gravity bottom|right, and the offset - * is (ox, oy), the calculated surface position will be (x + ox, y - * + oy). The offset position of the surface is the one used for - * constraint testing. See set_constraint_adjustment. - * - * An example use case is placing a popup menu on top of a user - * interface element, while aligning the user interface element of - * the parent surface with some user interface element placed - * somewhere in the popup surface. - * @param x surface position x offset - * @param y surface position y offset - */ void (*set_offset)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y); - /** - * continuously reconstrain the surface - * - * When set reactive, the surface is reconstrained if the - * conditions used for constraining changed, e.g. the parent window - * moved. - * - * If the conditions changed and the popup was reconstrained, an - * xdg_popup.configure event is sent with updated geometry, - * followed by an xdg_surface.configure event. - * @since 3 - */ void (*set_reactive)(struct wl_client *client, struct wl_resource *resource); - /** - * - * - * Set the parent window geometry the compositor should use when - * positioning the popup. The compositor may use this information - * to determine the future state the popup should be constrained - * using. If this doesn't match the dimension of the parent the - * popup is eventually positioned against, the behavior is - * undefined. - * - * The arguments are given in the surface-local coordinate space. - * @param parent_width future window geometry width of parent - * @param parent_height future window geometry height of parent - * @since 3 - */ void (*set_parent_size)(struct wl_client *client, struct wl_resource *resource, int32_t parent_width, int32_t parent_height); - /** - * set parent configure this is a response to - * - * Set the serial of an xdg_surface.configure event this - * positioner will be used in response to. The compositor may use - * this information together with set_parent_size to determine what - * future state the popup should be constrained using. - * @param serial serial of parent configure event - * @since 3 - */ void (*set_parent_configure)(struct wl_client *client, struct wl_resource *resource, uint32_t serial); }; -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3 #ifndef XDG_SURFACE_ERROR_ENUM #define XDG_SURFACE_ERROR_ENUM enum xdg_surface_error { - /** - * Surface was not fully constructed - */ XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1, - /** - * Surface was already constructed - */ XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2, - /** - * Attaching a buffer to an unconfigured surface - */ XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3, - /** - * Invalid serial number when acking a configure event - */ XDG_SURFACE_ERROR_INVALID_SERIAL = 4, - /** - * Width or height was zero or negative - */ XDG_SURFACE_ERROR_INVALID_SIZE = 5, - /** - * Surface was destroyed before its role object - */ XDG_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 6, }; -#endif /* XDG_SURFACE_ERROR_ENUM */ +#endif #ifndef XDG_SURFACE_ERROR_ENUM_IS_VALID #define XDG_SURFACE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_surface - * Validate a xdg_surface error value. - * - * @return true on success, false on error. - * @ref xdg_surface_error - */ static inline bool xdg_surface_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1069,133 +375,25 @@ xdg_surface_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_SURFACE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_surface - * @struct xdg_surface_interface - */ struct xdg_surface_interface { - /** - * destroy the xdg_surface - * - * Destroy the xdg_surface object. An xdg_surface must only be - * destroyed after its role object has been destroyed, otherwise a - * defunct_role_object error is raised. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * assign the xdg_toplevel surface role - * - * This creates an xdg_toplevel object for the given xdg_surface - * and gives the associated wl_surface the xdg_toplevel role. - * - * See the documentation of xdg_toplevel for more details about - * what an xdg_toplevel is and how it is used. - */ void (*get_toplevel)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * assign the xdg_popup surface role - * - * This creates an xdg_popup object for the given xdg_surface and - * gives the associated wl_surface the xdg_popup role. - * - * If null is passed as a parent, a parent surface must be - * specified using some other protocol, before committing the - * initial state. - * - * See the documentation of xdg_popup for more details about what - * an xdg_popup is and how it is used. - */ void (*get_popup)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *parent, struct wl_resource *positioner); - /** - * set the new window geometry - * - * The window geometry of a surface is its "visible bounds" from - * the user's perspective. Client-side decorations often have - * invisible portions like drop-shadows which should be ignored for - * the purposes of aligning, placing and constraining windows. - * - * The window geometry is double buffered, and will be applied at - * the time wl_surface.commit of the corresponding wl_surface is - * called. - * - * When maintaining a position, the compositor should treat the (x, - * y) coordinate of the window geometry as the top left corner of - * the window. A client changing the (x, y) window geometry - * coordinate should in general not alter the position of the - * window. - * - * Once the window geometry of the surface is set, it is not - * possible to unset it, and it will remain the same until - * set_window_geometry is called again, even if a new subsurface or - * buffer is attached. - * - * If never set, the value is the full bounds of the surface, - * including any subsurfaces. This updates dynamically on every - * commit. This unset is meant for extremely simple clients. - * - * The arguments are given in the surface-local coordinate space of - * the wl_surface associated with this xdg_surface. - * - * The width and height must be greater than zero. Setting an - * invalid size will raise an invalid_size error. When applied, the - * effective window geometry will be the set window geometry - * clamped to the bounding rectangle of the combined geometry of - * the surface of the xdg_surface and the associated subsurfaces. - */ void (*set_window_geometry)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * ack a configure event - * - * When a configure event is received, if a client commits the - * surface in response to the configure event, then the client must - * make an ack_configure request sometime before the commit - * request, passing along the serial of the configure event. - * - * For instance, for toplevel surfaces the compositor might use - * this information to move a surface to the top left only when the - * client has drawn itself for the maximized or fullscreen state. - * - * If the client receives multiple configure events before it can - * respond to one, it only has to ack the last configure event. - * Acking a configure event that was never sent raises an - * invalid_serial error. - * - * A client is not required to commit immediately after sending an - * ack_configure request - it may even ack_configure several times - * before its next surface commit. - * - * A client may send multiple ack_configure requests before - * committing, but only the last request sent before a commit - * indicates which configure event the client really is responding - * to. - * - * Sending an ack_configure request consumes the serial number sent - * with the request, as well as serial numbers sent by all - * configure events sent on this xdg_surface prior to the configure - * event referenced by the committed serial. - * - * It is an error to issue multiple ack_configure requests - * referencing a serial from the same configure event, or to issue - * an ack_configure request referencing a serial from a configure - * event issued before the event identified by the last - * ack_configure request for the same xdg_surface. Doing so will - * raise an invalid_serial error. - * @param serial the serial from the configure event - */ void (*ack_configure)(struct wl_client *client, struct wl_resource *resource, uint32_t serial); @@ -1203,38 +401,14 @@ struct xdg_surface_interface { #define XDG_SURFACE_CONFIGURE 0 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - * Sends an configure event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial of the configure event - */ static inline void xdg_surface_send_configure(struct wl_resource *resource_, uint32_t serial) { @@ -1244,30 +418,14 @@ xdg_surface_send_configure(struct wl_resource *resource_, uint32_t serial) #ifndef XDG_TOPLEVEL_ERROR_ENUM #define XDG_TOPLEVEL_ERROR_ENUM enum xdg_toplevel_error { - /** - * provided value is not a valid variant of the resize_edge enum - */ XDG_TOPLEVEL_ERROR_INVALID_RESIZE_EDGE = 0, - /** - * invalid parent toplevel - */ XDG_TOPLEVEL_ERROR_INVALID_PARENT = 1, - /** - * client provided an invalid min or max size - */ XDG_TOPLEVEL_ERROR_INVALID_SIZE = 2, }; -#endif /* XDG_TOPLEVEL_ERROR_ENUM */ +#endif #ifndef XDG_TOPLEVEL_ERROR_ENUM_IS_VALID #define XDG_TOPLEVEL_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_toplevel - * Validate a xdg_toplevel error value. - * - * @return true on success, false on error. - * @ref xdg_toplevel_error - */ static inline bool xdg_toplevel_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1281,17 +439,10 @@ xdg_toplevel_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_TOPLEVEL_ERROR_ENUM_IS_VALID */ +#endif #ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM #define XDG_TOPLEVEL_RESIZE_EDGE_ENUM -/** - * @ingroup iface_xdg_toplevel - * edge values for resizing - * - * These values are used to indicate which edge of a surface - * is being dragged in a resize operation. - */ enum xdg_toplevel_resize_edge { XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0, XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1, @@ -1303,17 +454,10 @@ enum xdg_toplevel_resize_edge { XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9, XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10, }; -#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */ +#endif #ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM_IS_VALID #define XDG_TOPLEVEL_RESIZE_EDGE_ENUM_IS_VALID -/** - * @ingroup iface_xdg_toplevel - * Validate a xdg_toplevel resize_edge value. - * - * @return true on success, false on error. - * @ref xdg_toplevel_resize_edge - */ static inline bool xdg_toplevel_resize_edge_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1339,124 +483,28 @@ xdg_toplevel_resize_edge_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM_IS_VALID */ +#endif #ifndef XDG_TOPLEVEL_STATE_ENUM #define XDG_TOPLEVEL_STATE_ENUM -/** - * @ingroup iface_xdg_toplevel - * types of state on the surface - * - * The different state values used on the surface. This is designed for - * state values like maximized, fullscreen. It is paired with the - * configure event to ensure that both the client and the compositor - * setting the state can be synchronized. - * - * States set in this way are double-buffered. They will get applied on - * the next commit. - */ enum xdg_toplevel_state { - /** - * the surface is maximized - * the surface is maximized - * - * The surface is maximized. The window geometry specified in the - * configure event must be obeyed by the client. - * - * The client should draw without shadow or other decoration - * outside of the window geometry. - */ XDG_TOPLEVEL_STATE_MAXIMIZED = 1, - /** - * the surface is fullscreen - * the surface is fullscreen - * - * The surface is fullscreen. The window geometry specified in - * the configure event is a maximum; the client cannot resize - * beyond it. For a surface to cover the whole fullscreened area, - * the geometry dimensions must be obeyed by the client. For more - * details, see xdg_toplevel.set_fullscreen. - */ XDG_TOPLEVEL_STATE_FULLSCREEN = 2, - /** - * the surface is being resized - * the surface is being resized - * - * The surface is being resized. The window geometry specified in - * the configure event is a maximum; the client cannot resize - * beyond it. Clients that have aspect ratio or cell sizing - * configuration can use a smaller size, however. - */ XDG_TOPLEVEL_STATE_RESIZING = 3, - /** - * the surface is now activated - * the surface is now activated - * - * Client window decorations should be painted as if the window - * is active. Do not assume this means that the window actually has - * keyboard or pointer focus. - */ XDG_TOPLEVEL_STATE_ACTIVATED = 4, - /** - * the surface’s left edge is tiled - * - * The window is currently in a tiled layout and the left edge is - * considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_LEFT = 5, - /** - * the surface’s right edge is tiled - * - * The window is currently in a tiled layout and the right edge - * is considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_RIGHT = 6, - /** - * the surface’s top edge is tiled - * - * The window is currently in a tiled layout and the top edge is - * considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_TOP = 7, - /** - * the surface’s bottom edge is tiled - * - * The window is currently in a tiled layout and the bottom edge - * is considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8, }; -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2 -#endif /* XDG_TOPLEVEL_STATE_ENUM */ +#endif #ifndef XDG_TOPLEVEL_STATE_ENUM_IS_VALID #define XDG_TOPLEVEL_STATE_ENUM_IS_VALID -/** - * @ingroup iface_xdg_toplevel - * Validate a xdg_toplevel state value. - * - * @return true on success, false on error. - * @ref xdg_toplevel_state - */ static inline bool xdg_toplevel_state_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1480,39 +528,20 @@ xdg_toplevel_state_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_TOPLEVEL_STATE_ENUM_IS_VALID */ +#endif #ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM #define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM enum xdg_toplevel_wm_capabilities { - /** - * show_window_menu is available - */ XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU = 1, - /** - * set_maximized and unset_maximized are available - */ XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE = 2, - /** - * set_fullscreen and unset_fullscreen are available - */ XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN = 3, - /** - * set_minimized is available - */ XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE = 4, }; -#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM */ +#endif #ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM_IS_VALID #define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM_IS_VALID -/** - * @ingroup iface_xdg_toplevel - * Validate a xdg_toplevel wm_capabilities value. - * - * @return true on success, false on error. - * @ref xdg_toplevel_wm_capabilities - */ static inline bool xdg_toplevel_wm_capabilities_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1528,398 +557,52 @@ xdg_toplevel_wm_capabilities_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_toplevel - * @struct xdg_toplevel_interface - */ struct xdg_toplevel_interface { - /** - * destroy the xdg_toplevel - * - * This request destroys the role surface and unmaps the surface; - * see "Unmapping" behavior in interface section for details. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the parent of this surface - * - * Set the "parent" of this surface. This surface should be - * stacked above the parent surface and all other ancestor - * surfaces. - * - * Parent surfaces should be set on dialogs, toolboxes, or other - * "auxiliary" surfaces, so that the parent is raised when the - * dialog is raised. - * - * Setting a null parent for a child surface unsets its parent. - * Setting a null parent for a surface which currently has no - * parent is a no-op. - * - * Only mapped surfaces can have child surfaces. Setting a parent - * which is not mapped is equivalent to setting a null parent. If a - * surface becomes unmapped, its children's parent is set to the - * parent of the now-unmapped surface. If the now-unmapped surface - * has no parent, its children's parent is unset. If the - * now-unmapped surface becomes mapped again, its parent-child - * relationship is not restored. - * - * The parent toplevel must not be one of the child toplevel's - * descendants, and the parent must be different from the child - * toplevel, otherwise the invalid_parent protocol error is raised. - */ void (*set_parent)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *parent); - /** - * set surface title - * - * Set a short title for the surface. - * - * This string may be used to identify the surface in a task bar, - * window list, or other user interface elements provided by the - * compositor. - * - * The string must be encoded in UTF-8. - */ void (*set_title)(struct wl_client *client, struct wl_resource *resource, const char *title); - /** - * set application ID - * - * Set an application identifier for the surface. - * - * The app ID identifies the general class of applications to which - * the surface belongs. The compositor can use this to group - * multiple surfaces together, or to determine how to launch a new - * application. - * - * For D-Bus activatable applications, the app ID is used as the - * D-Bus service name. - * - * The compositor shell will try to group application surfaces - * together by their app ID. As a best practice, it is suggested to - * select app ID's that match the basename of the application's - * .desktop file. For example, "org.freedesktop.FooViewer" where - * the .desktop file is "org.freedesktop.FooViewer.desktop". - * - * Like other properties, a set_app_id request can be sent after - * the xdg_toplevel has been mapped to update the property. - * - * See the desktop-entry specification [0] for more details on - * application identifiers and how they relate to well-known D-Bus - * names and .desktop files. - * - * [0] https://standards.freedesktop.org/desktop-entry-spec/ - */ void (*set_app_id)(struct wl_client *client, struct wl_resource *resource, const char *app_id); - /** - * show the window menu - * - * Clients implementing client-side decorations might want to - * show a context menu when right-clicking on the decorations, - * giving the user a menu that they can use to maximize or minimize - * the window. - * - * This request asks the compositor to pop up such a window menu at - * the given position, relative to the local surface coordinates of - * the parent surface. There are no guarantees as to what menu - * items the window menu contains, or even if a window menu will be - * drawn at all. - * - * This request must be used in response to some sort of user - * action like a button press, key press, or touch down event. - * @param seat the wl_seat of the user event - * @param serial the serial of the user event - * @param x the x position to pop up the window menu at - * @param y the y position to pop up the window menu at - */ void (*show_window_menu)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial, int32_t x, int32_t y); - /** - * start an interactive move - * - * Start an interactive, user-driven move of the surface. - * - * This request must be used in response to some sort of user - * action like a button press, key press, or touch down event. The - * passed serial is used to determine the type of interactive move - * (touch, pointer, etc). - * - * The server may ignore move requests depending on the state of - * the surface (e.g. fullscreen or maximized), or if the passed - * serial is no longer valid. - * - * If triggered, the surface will lose the focus of the device - * (wl_pointer, wl_touch, etc) used for the move. It is up to the - * compositor to visually indicate that the move is taking place, - * such as updating a pointer cursor, during the move. There is no - * guarantee that the device focus will return when the move is - * completed. - * @param seat the wl_seat of the user event - * @param serial the serial of the user event - */ void (*move)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial); - /** - * start an interactive resize - * - * Start a user-driven, interactive resize of the surface. - * - * This request must be used in response to some sort of user - * action like a button press, key press, or touch down event. The - * passed serial is used to determine the type of interactive - * resize (touch, pointer, etc). - * - * The server may ignore resize requests depending on the state of - * the surface (e.g. fullscreen or maximized). - * - * If triggered, the client will receive configure events with the - * "resize" state enum value and the expected sizes. See the - * "resize" enum value for more details about what is required. The - * client must also acknowledge configure events using - * "ack_configure". After the resize is completed, the client will - * receive another "configure" event without the resize state. - * - * If triggered, the surface also will lose the focus of the device - * (wl_pointer, wl_touch, etc) used for the resize. It is up to the - * compositor to visually indicate that the resize is taking place, - * such as updating a pointer cursor, during the resize. There is - * no guarantee that the device focus will return when the resize - * is completed. - * - * The edges parameter specifies how the surface should be resized, - * and is one of the values of the resize_edge enum. Values not - * matching a variant of the enum will cause a protocol error. The - * compositor may use this information to update the surface - * position for example when dragging the top left corner. The - * compositor may also use this information to adapt its behavior, - * e.g. choose an appropriate cursor image. - * @param seat the wl_seat of the user event - * @param serial the serial of the user event - * @param edges which edge or corner is being dragged - */ void (*resize)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial, uint32_t edges); - /** - * set the maximum size - * - * Set a maximum size for the window. - * - * The client can specify a maximum size so that the compositor - * does not try to configure the window beyond this size. - * - * The width and height arguments are in window geometry - * coordinates. See xdg_surface.set_window_geometry. - * - * Values set in this way are double-buffered. They will get - * applied on the next commit. - * - * The compositor can use this information to allow or disallow - * different states like maximize or fullscreen and draw accurate - * animations. - * - * Similarly, a tiling window manager may use this information to - * place and resize client windows in a more effective way. - * - * The client should not rely on the compositor to obey the maximum - * size. The compositor may decide to ignore the values set by the - * client and request a larger size. - * - * If never set, or a value of zero in the request, means that the - * client has no expected maximum size in the given dimension. As a - * result, a client wishing to reset the maximum size to an - * unspecified state can use zero for width and height in the - * request. - * - * Requesting a maximum size to be smaller than the minimum size of - * a surface is illegal and will result in an invalid_size error. - * - * The width and height must be greater than or equal to zero. - * Using strictly negative values for width or height will result - * in a invalid_size error. - */ void (*set_max_size)(struct wl_client *client, struct wl_resource *resource, int32_t width, int32_t height); - /** - * set the minimum size - * - * Set a minimum size for the window. - * - * The client can specify a minimum size so that the compositor - * does not try to configure the window below this size. - * - * The width and height arguments are in window geometry - * coordinates. See xdg_surface.set_window_geometry. - * - * Values set in this way are double-buffered. They will get - * applied on the next commit. - * - * The compositor can use this information to allow or disallow - * different states like maximize or fullscreen and draw accurate - * animations. - * - * Similarly, a tiling window manager may use this information to - * place and resize client windows in a more effective way. - * - * The client should not rely on the compositor to obey the minimum - * size. The compositor may decide to ignore the values set by the - * client and request a smaller size. - * - * If never set, or a value of zero in the request, means that the - * client has no expected minimum size in the given dimension. As a - * result, a client wishing to reset the minimum size to an - * unspecified state can use zero for width and height in the - * request. - * - * Requesting a minimum size to be larger than the maximum size of - * a surface is illegal and will result in an invalid_size error. - * - * The width and height must be greater than or equal to zero. - * Using strictly negative values for width and height will result - * in a invalid_size error. - */ void (*set_min_size)(struct wl_client *client, struct wl_resource *resource, int32_t width, int32_t height); - /** - * maximize the window - * - * Maximize the surface. - * - * After requesting that the surface should be maximized, the - * compositor will respond by emitting a configure event. Whether - * this configure actually sets the window maximized is subject to - * compositor policies. The client must then update its content, - * drawing in the configured state. The client must also - * acknowledge the configure when committing the new content (see - * ack_configure). - * - * It is up to the compositor to decide how and where to maximize - * the surface, for example which output and what region of the - * screen should be used. - * - * If the surface was already maximized, the compositor will still - * emit a configure event with the "maximized" state. - * - * If the surface is in a fullscreen state, this request has no - * direct effect. It may alter the state the surface is returned to - * when unmaximized unless overridden by the compositor. - */ void (*set_maximized)(struct wl_client *client, struct wl_resource *resource); - /** - * unmaximize the window - * - * Unmaximize the surface. - * - * After requesting that the surface should be unmaximized, the - * compositor will respond by emitting a configure event. Whether - * this actually un-maximizes the window is subject to compositor - * policies. If available and applicable, the compositor will - * include the window geometry dimensions the window had prior to - * being maximized in the configure event. The client must then - * update its content, drawing it in the configured state. The - * client must also acknowledge the configure when committing the - * new content (see ack_configure). - * - * It is up to the compositor to position the surface after it was - * unmaximized; usually the position the surface had before - * maximizing, if applicable. - * - * If the surface was already not maximized, the compositor will - * still emit a configure event without the "maximized" state. - * - * If the surface is in a fullscreen state, this request has no - * direct effect. It may alter the state the surface is returned to - * when unmaximized unless overridden by the compositor. - */ void (*unset_maximized)(struct wl_client *client, struct wl_resource *resource); - /** - * set the window as fullscreen on an output - * - * Make the surface fullscreen. - * - * After requesting that the surface should be fullscreened, the - * compositor will respond by emitting a configure event. Whether - * the client is actually put into a fullscreen state is subject to - * compositor policies. The client must also acknowledge the - * configure when committing the new content (see ack_configure). - * - * The output passed by the request indicates the client's - * preference as to which display it should be set fullscreen on. - * If this value is NULL, it's up to the compositor to choose which - * display will be used to map this surface. - * - * If the surface doesn't cover the whole output, the compositor - * will position the surface in the center of the output and - * compensate with with border fill covering the rest of the - * output. The content of the border fill is undefined, but should - * be assumed to be in some way that attempts to blend into the - * surrounding area (e.g. solid black). - * - * If the fullscreened surface is not opaque, the compositor must - * make sure that other screen content not part of the same surface - * tree (made up of subsurfaces, popups or similarly coupled - * surfaces) are not visible below the fullscreened surface. - */ void (*set_fullscreen)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *output); - /** - * unset the window as fullscreen - * - * Make the surface no longer fullscreen. - * - * After requesting that the surface should be unfullscreened, the - * compositor will respond by emitting a configure event. Whether - * this actually removes the fullscreen state of the client is - * subject to compositor policies. - * - * Making a surface unfullscreen sets states for the surface based - * on the following: * the state(s) it may have had before becoming - * fullscreen * any state(s) decided by the compositor * any - * state(s) requested by the client while the surface was - * fullscreen - * - * The compositor may include the previous window geometry - * dimensions in the configure event, if applicable. - * - * The client must also acknowledge the configure when committing - * the new content (see ack_configure). - */ void (*unset_fullscreen)(struct wl_client *client, struct wl_resource *resource); - /** - * set the window as minimized - * - * Request that the compositor minimize your surface. There is no - * way to know if the surface is currently minimized, nor is there - * any way to unset minimization on this surface. - * - * If you are looking to throttle redrawing when minimized, please - * instead use the wl_surface.frame event for this, as this will - * also work with live previews on windows in Alt-Tab, Expose or - * similar compositor features. - */ void (*set_minimized)(struct wl_client *client, struct wl_resource *resource); }; @@ -1929,119 +612,44 @@ struct xdg_toplevel_interface { #define XDG_TOPLEVEL_CONFIGURE_BOUNDS 2 #define XDG_TOPLEVEL_WM_CAPABILITIES 3 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION 4 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION 5 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - * Sends an configure event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void xdg_toplevel_send_configure(struct wl_resource *resource_, int32_t width, int32_t height, struct wl_array *states) { wl_resource_post_event(resource_, XDG_TOPLEVEL_CONFIGURE, width, height, states); } -/** - * @ingroup iface_xdg_toplevel - * Sends an close event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void xdg_toplevel_send_close(struct wl_resource *resource_) { wl_resource_post_event(resource_, XDG_TOPLEVEL_CLOSE); } -/** - * @ingroup iface_xdg_toplevel - * Sends an configure_bounds event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void xdg_toplevel_send_configure_bounds(struct wl_resource *resource_, int32_t width, int32_t height) { wl_resource_post_event(resource_, XDG_TOPLEVEL_CONFIGURE_BOUNDS, width, height); } -/** - * @ingroup iface_xdg_toplevel - * Sends an wm_capabilities event to the client owning the resource. - * @param resource_ The client's resource - * @param capabilities array of 32-bit capabilities - */ static inline void xdg_toplevel_send_wm_capabilities(struct wl_resource *resource_, struct wl_array *capabilities) { @@ -2051,22 +659,12 @@ xdg_toplevel_send_wm_capabilities(struct wl_resource *resource_, struct wl_array #ifndef XDG_POPUP_ERROR_ENUM #define XDG_POPUP_ERROR_ENUM enum xdg_popup_error { - /** - * tried to grab after being mapped - */ XDG_POPUP_ERROR_INVALID_GRAB = 0, }; -#endif /* XDG_POPUP_ERROR_ENUM */ +#endif #ifndef XDG_POPUP_ERROR_ENUM_IS_VALID #define XDG_POPUP_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_popup - * Validate a xdg_popup error value. - * - * @return true on success, false on error. - * @ref xdg_popup_error - */ static inline bool xdg_popup_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2076,106 +674,15 @@ xdg_popup_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_POPUP_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_popup - * @struct xdg_popup_interface - */ struct xdg_popup_interface { - /** - * remove xdg_popup interface - * - * This destroys the popup. Explicitly destroying the xdg_popup - * object will also dismiss the popup, and unmap the surface. - * - * If this xdg_popup is not the "topmost" popup, a protocol error - * will be sent. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * make the popup take an explicit grab - * - * This request makes the created popup take an explicit grab. An - * explicit grab will be dismissed when the user dismisses the - * popup, or when the client destroys the xdg_popup. This can be - * done by the user clicking outside the surface, using the - * keyboard, or even locking the screen through closing the lid or - * a timeout. - * - * If the compositor denies the grab, the popup will be immediately - * dismissed. - * - * This request must be used in response to some sort of user - * action like a button press, key press, or touch down event. The - * serial number of the event should be passed as 'serial'. - * - * The parent of a grabbing popup must either be an xdg_toplevel - * surface or another xdg_popup with an explicit grab. If the - * parent is another xdg_popup it means that the popups are nested, - * with this popup now being the topmost popup. - * - * Nested popups must be destroyed in the reverse order they were - * created in, e.g. the only popup you are allowed to destroy at - * all times is the topmost one. - * - * When compositors choose to dismiss a popup, they may dismiss - * every nested grabbing popup as well. When a compositor dismisses - * popups, it will follow the same dismissing order as required - * from the client. - * - * If the topmost grabbing popup is destroyed, the grab will be - * returned to the parent of the popup, if that parent previously - * had an explicit grab. - * - * If the parent is a grabbing popup which has already been - * dismissed, this popup will be immediately dismissed. If the - * parent is a popup that did not take an explicit grab, an error - * will be raised. - * - * During a popup grab, the client owning the grab will receive - * pointer and touch events for all their surfaces as normal - * (similar to an "owner-events" grab in X11 parlance), while the - * top most grabbing popup will always have keyboard focus. - * @param seat the wl_seat of the user event - * @param serial the serial of the user event - */ void (*grab)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial); - /** - * recalculate the popup's location - * - * Reposition an already-mapped popup. The popup will be placed - * given the details in the passed xdg_positioner object, and a - * xdg_popup.repositioned followed by xdg_popup.configure and - * xdg_surface.configure will be emitted in response. Any - * parameters set by the previous positioner will be discarded. - * - * The passed token will be sent in the corresponding - * xdg_popup.repositioned event. The new popup position will not - * take effect until the corresponding configure event is - * acknowledged by the client. See xdg_popup.repositioned for - * details. The token itself is opaque, and has no other special - * meaning. - * - * If multiple reposition requests are sent, the compositor may - * skip all but the last one. - * - * If the popup is repositioned in response to a configure event - * for its parent, the client should send an - * xdg_positioner.set_parent_configure and possibly an - * xdg_positioner.set_parent_size request to allow the compositor - * to properly constrain the popup. - * - * If the popup is repositioned together with a parent that is - * being resized, but not in response to a configure event, the - * client should send an xdg_positioner.set_parent_size request. - * @param token reposition request token - * @since 3 - */ void (*reposition)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *positioner, @@ -2186,64 +693,26 @@ struct xdg_popup_interface { #define XDG_POPUP_POPUP_DONE 1 #define XDG_POPUP_REPOSITIONED 2 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_GRAB_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_REPOSITION_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_popup - * Sends an configure event to the client owning the resource. - * @param resource_ The client's resource - * @param x x position relative to parent surface window geometry - * @param y y position relative to parent surface window geometry - * @param width window geometry width - * @param height window geometry height - */ static inline void xdg_popup_send_configure(struct wl_resource *resource_, int32_t x, int32_t y, int32_t width, int32_t height) { wl_resource_post_event(resource_, XDG_POPUP_CONFIGURE, x, y, width, height); } -/** - * @ingroup iface_xdg_popup - * Sends an popup_done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void xdg_popup_send_popup_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, XDG_POPUP_POPUP_DONE); } -/** - * @ingroup iface_xdg_popup - * Sends an repositioned event to the client owning the resource. - * @param resource_ The client's resource - * @param token reposition request token - */ static inline void xdg_popup_send_repositioned(struct wl_resource *resource_, uint32_t token) { diff --git a/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-protocol.c index 73d39d207..264b8f29e 100644 --- a/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2023-2024 Matthias Klumpp * Copyright © 2024 David Edmundson @@ -30,7 +28,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-server-protocol.h index b186d8897..33e15fd5a 100644 --- a/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef XDG_TOPLEVEL_ICON_V1_SERVER_PROTOCOL_H #define XDG_TOPLEVEL_ICON_V1_SERVER_PROTOCOL_H @@ -14,33 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_xdg_toplevel_icon_v1 The xdg_toplevel_icon_v1 protocol - * protocol to assign icons to toplevels - * - * @section page_desc_xdg_toplevel_icon_v1 Description - * - * This protocol allows clients to set icons for their toplevel surfaces - * either via the XDG icon stock (using an icon name), or from pixel data. - * - * A toplevel icon represents the individual toplevel (unlike the application - * or launcher icon, which represents the application as a whole), and may be - * shown in window switchers, window overviews and taskbars that list - * individual windows. - * - * This document adheres to RFC 2119 when using words like "must", - * "should", "may", etc. - * - * Warning! The protocol described in this file is currently in the testing - * phase. Backward compatible changes may be added together with the - * corresponding interface version bump. Backward incompatible changes can - * only be done by creating a new major version of the extension. - * - * @section page_ifaces_xdg_toplevel_icon_v1 Interfaces - * - @subpage page_iface_xdg_toplevel_icon_manager_v1 - interface to manage toplevel icons - * - @subpage page_iface_xdg_toplevel_icon_v1 - a toplevel window icon +/* * @section page_copyright_xdg_toplevel_icon_v1 Copyright - *
  *
  * Copyright © 2023-2024 Matthias Klumpp
  * Copyright ©      2024 David Edmundson
@@ -63,8 +37,33 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_buffer; struct xdg_toplevel; struct xdg_toplevel_icon_manager_v1; @@ -72,104 +71,19 @@ struct xdg_toplevel_icon_v1; #ifndef XDG_TOPLEVEL_ICON_MANAGER_V1_INTERFACE #define XDG_TOPLEVEL_ICON_MANAGER_V1_INTERFACE -/** - * @page page_iface_xdg_toplevel_icon_manager_v1 xdg_toplevel_icon_manager_v1 - * @section page_iface_xdg_toplevel_icon_manager_v1_desc Description - * - * This interface allows clients to create toplevel window icons and set - * them on toplevel windows to be displayed to the user. - * @section page_iface_xdg_toplevel_icon_manager_v1_api API - * See @ref iface_xdg_toplevel_icon_manager_v1. - */ -/** - * @defgroup iface_xdg_toplevel_icon_manager_v1 The xdg_toplevel_icon_manager_v1 interface - * - * This interface allows clients to create toplevel window icons and set - * them on toplevel windows to be displayed to the user. - */ extern const struct wl_interface xdg_toplevel_icon_manager_v1_interface; #endif #ifndef XDG_TOPLEVEL_ICON_V1_INTERFACE #define XDG_TOPLEVEL_ICON_V1_INTERFACE -/** - * @page page_iface_xdg_toplevel_icon_v1 xdg_toplevel_icon_v1 - * @section page_iface_xdg_toplevel_icon_v1_desc Description - * - * This interface defines a toplevel icon. - * An icon can have a name, and multiple buffers. - * In order to be applied, the icon must have either a name, or at least - * one buffer assigned. Applying an empty icon (with no buffer or name) to - * a toplevel should reset its icon to the default icon. - * - * It is up to compositor policy whether to prefer using a buffer or loading - * an icon via its name. See 'set_name' and 'add_buffer' for details. - * @section page_iface_xdg_toplevel_icon_v1_api API - * See @ref iface_xdg_toplevel_icon_v1. - */ -/** - * @defgroup iface_xdg_toplevel_icon_v1 The xdg_toplevel_icon_v1 interface - * - * This interface defines a toplevel icon. - * An icon can have a name, and multiple buffers. - * In order to be applied, the icon must have either a name, or at least - * one buffer assigned. Applying an empty icon (with no buffer or name) to - * a toplevel should reset its icon to the default icon. - * - * It is up to compositor policy whether to prefer using a buffer or loading - * an icon via its name. See 'set_name' and 'add_buffer' for details. - */ extern const struct wl_interface xdg_toplevel_icon_v1_interface; #endif -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - * @struct xdg_toplevel_icon_manager_v1_interface - */ struct xdg_toplevel_icon_manager_v1_interface { - /** - * destroy the toplevel icon manager - * - * Destroy the toplevel icon manager. This does not destroy - * objects created with the manager. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a new icon instance - * - * Creates a new icon object. This icon can then be attached to a - * xdg_toplevel via the 'set_icon' request. - */ void (*create_icon)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * set an icon on a toplevel window - * - * This request assigns the icon 'icon' to 'toplevel', or clears - * the toplevel icon if 'icon' was null. This state is - * double-buffered and is applied on the next wl_surface.commit of - * the toplevel. - * - * After making this call, the xdg_toplevel_icon_v1 provided as - * 'icon' can be destroyed by the client without 'toplevel' losing - * its icon. The xdg_toplevel_icon_v1 is immutable from this point, - * and any future attempts to change it must raise the - * 'xdg_toplevel_icon_v1.immutable' protocol error. - * - * The compositor must set the toplevel icon from either the pixel - * data the icon provides, or by loading a stock icon using the - * icon name. See the description of 'xdg_toplevel_icon_v1' for - * details. - * - * If 'icon' is set to null, the icon of the respective toplevel is - * reset to its default icon (usually the icon of the application, - * derived from its desktop-entry file, or a placeholder icon). If - * this request is passed an icon with no pixel buffers or icon - * name assigned, the icon must be reset just like if 'icon' was - * null. - * @param toplevel the toplevel to act on - */ void (*set_icon)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *toplevel, @@ -179,45 +93,19 @@ struct xdg_toplevel_icon_manager_v1_interface { #define XDG_TOPLEVEL_ICON_MANAGER_V1_ICON_SIZE 0 #define XDG_TOPLEVEL_ICON_MANAGER_V1_DONE 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - */ #define XDG_TOPLEVEL_ICON_MANAGER_V1_ICON_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - */ #define XDG_TOPLEVEL_ICON_MANAGER_V1_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - */ #define XDG_TOPLEVEL_ICON_MANAGER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - */ #define XDG_TOPLEVEL_ICON_MANAGER_V1_CREATE_ICON_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - */ #define XDG_TOPLEVEL_ICON_MANAGER_V1_SET_ICON_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - * Sends an icon_size event to the client owning the resource. - * @param resource_ The client's resource - * @param size the edge size of the square icon in surface-local coordinates, e.g. 64 - */ static inline void xdg_toplevel_icon_manager_v1_send_icon_size(struct wl_resource *resource_, int32_t size) { wl_resource_post_event(resource_, XDG_TOPLEVEL_ICON_MANAGER_V1_ICON_SIZE, size); } -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void xdg_toplevel_icon_manager_v1_send_done(struct wl_resource *resource_) { @@ -227,30 +115,14 @@ xdg_toplevel_icon_manager_v1_send_done(struct wl_resource *resource_) #ifndef XDG_TOPLEVEL_ICON_V1_ERROR_ENUM #define XDG_TOPLEVEL_ICON_V1_ERROR_ENUM enum xdg_toplevel_icon_v1_error { - /** - * the provided buffer does not satisfy requirements - */ XDG_TOPLEVEL_ICON_V1_ERROR_INVALID_BUFFER = 1, - /** - * the icon has already been assigned to a toplevel and must not be changed - */ XDG_TOPLEVEL_ICON_V1_ERROR_IMMUTABLE = 2, - /** - * the provided buffer has been destroyed before the toplevel icon - */ XDG_TOPLEVEL_ICON_V1_ERROR_NO_BUFFER = 3, }; -#endif /* XDG_TOPLEVEL_ICON_V1_ERROR_ENUM */ +#endif #ifndef XDG_TOPLEVEL_ICON_V1_ERROR_ENUM_IS_VALID #define XDG_TOPLEVEL_ICON_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_toplevel_icon_v1 - * Validate a xdg_toplevel_icon_v1 error value. - * - * @return true on success, false on error. - * @ref xdg_toplevel_icon_v1_error - */ static inline bool xdg_toplevel_icon_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -264,77 +136,14 @@ xdg_toplevel_icon_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_TOPLEVEL_ICON_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_toplevel_icon_v1 - * @struct xdg_toplevel_icon_v1_interface - */ struct xdg_toplevel_icon_v1_interface { - /** - * destroy the icon object - * - * Destroys the 'xdg_toplevel_icon_v1' object. The icon must - * still remain set on every toplevel it was assigned to, until the - * toplevel icon is reset explicitly. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set an icon name - * - * This request assigns an icon name to this icon. Any previously - * set name is overridden. - * - * The compositor must resolve 'icon_name' according to the lookup - * rules described in the XDG icon theme specification[1] using the - * environment's current icon theme. - * - * If the compositor does not support icon names or cannot resolve - * 'icon_name' according to the XDG icon theme specification it - * must fall back to using pixel buffer data instead. - * - * If this request is made after the icon has been assigned to a - * toplevel via 'set_icon', an 'immutable' error must be raised. - * - * [1]: - * https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html - */ void (*set_name)(struct wl_client *client, struct wl_resource *resource, const char *icon_name); - /** - * add icon data from a pixel buffer - * - * This request adds pixel data supplied as wl_buffer to the - * icon. - * - * The client should add pixel data for all icon sizes and scales - * that it can provide, or which are explicitly requested by the - * compositor via 'icon_size' events on - * xdg_toplevel_icon_manager_v1. - * - * The wl_buffer supplying pixel data as 'buffer' must be backed by - * wl_shm and must be a square (width and height being equal). If - * any of these buffer requirements are not fulfilled, a - * 'invalid_buffer' error must be raised. - * - * If this icon instance already has a buffer of the same size and - * scale from a previous 'add_buffer' request, data from the last - * request overrides the preexisting pixel data. - * - * The wl_buffer must be kept alive for as long as the - * xdg_toplevel_icon it is associated with is not destroyed, - * otherwise a 'no_buffer' error is raised. The buffer contents - * must not be modified after it was assigned to the icon. As a - * result, the region of the wl_shm_pool's backing storage used for - * the wl_buffer must not be modified after this request is sent. - * The wl_buffer.release event is unused. - * - * If this request is made after the icon has been assigned to a - * toplevel via 'set_icon', an 'immutable' error must be raised. - * @param scale the scaling factor of the icon, e.g. 1 - */ void (*add_buffer)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *buffer, @@ -342,17 +151,8 @@ struct xdg_toplevel_icon_v1_interface { }; -/** - * @ingroup iface_xdg_toplevel_icon_v1 - */ #define XDG_TOPLEVEL_ICON_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_v1 - */ #define XDG_TOPLEVEL_ICON_V1_SET_NAME_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_v1 - */ #define XDG_TOPLEVEL_ICON_V1_ADD_BUFFER_SINCE_VERSION 1 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-core.h b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-core.h index 757f6a1f1..7c53fde66 100644 --- a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-core.h +++ b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-core.h @@ -43,87 +43,15 @@ enum { WL_EVENT_ERROR = 0x08 }; -/** File descriptor dispatch function type - * - * Functions of this type are used as callbacks for file descriptor events. - * - * \param fd The file descriptor delivering the event. - * \param mask Describes the kind of the event as a bitwise-or of: - * \c WL_EVENT_READABLE, \c WL_EVENT_WRITABLE, \c WL_EVENT_HANGUP, - * \c WL_EVENT_ERROR. - * \param data The user data argument of the related wl_event_loop_add_fd() - * call. - * \return If the event source is registered for re-check with - * wl_event_source_check(): 0 for all done, 1 for needing a re-check. - * If not registered, the return value is ignored and should be zero. - * - * \sa wl_event_loop_add_fd() - * \memberof wl_event_source - */ typedef int (*wl_event_loop_fd_func_t)(int fd, uint32_t mask, void *data); -/** Timer dispatch function type - * - * Functions of this type are used as callbacks for timer expiry. - * - * \param data The user data argument of the related wl_event_loop_add_timer() - * call. - * \return If the event source is registered for re-check with - * wl_event_source_check(): 0 for all done, 1 for needing a re-check. - * If not registered, the return value is ignored and should be zero. - * - * \sa wl_event_loop_add_timer() - * \memberof wl_event_source - */ typedef int (*wl_event_loop_timer_func_t)(void *data); -/** Signal dispatch function type - * - * Functions of this type are used as callbacks for (POSIX) signals. - * - * \param signal_number - * \param data The user data argument of the related wl_event_loop_add_signal() - * call. - * \return If the event source is registered for re-check with - * wl_event_source_check(): 0 for all done, 1 for needing a re-check. - * If not registered, the return value is ignored and should be zero. - * - * \sa wl_event_loop_add_signal() - * \memberof wl_event_source - */ typedef int (*wl_event_loop_signal_func_t)(int signal_number, void *data); -/** Idle task function type - * - * Functions of this type are used as callbacks before blocking in - * wl_event_loop_dispatch(). - * - * \param data The user data argument of the related wl_event_loop_add_idle() - * call. - * - * \sa wl_event_loop_add_idle() wl_event_loop_dispatch() - * \memberof wl_event_source - */ typedef void (*wl_event_loop_idle_func_t)(void *data); -/** \struct wl_event_loop - * - * \brief An event loop context - * - * Usually you create an event loop context, add sources to it, and call - * wl_event_loop_dispatch() in a loop to process events. - * - * \sa wl_event_source - */ -/** \struct wl_event_source - * - * \brief An abstract event source - * - * This is the generic type for fd, timer, signal, and idle sources. - * Functions that operate on specific source types must not be used with - * a different type, even if the function signature allows it. - */ struct wl_event_loop * wl_event_loop_create(void); @@ -266,21 +194,6 @@ wl_global_set_withdrawn_listener(struct wl_global *global, void wl_global_destroy(struct wl_global *global); -/** A filter function for wl_global objects - * - * \param client The client object - * \param global The global object to show or hide - * \param data The user data pointer - * - * A filter function enables the server to decide which globals to - * advertise to each client. - * - * When a wl_global filter is set, the given callback function will be - * called during wl_global advertisement and binding. - * - * This function should return true if the global object should be made - * visible to the client or false otherwise. - */ typedef bool (*wl_display_global_filter_func_t)(const struct wl_client *client, const struct wl_global *global, void *data); @@ -321,7 +234,6 @@ wl_client_get_link(struct wl_client *client); struct wl_client * wl_client_from_link(struct wl_list *link); -/** Iterate over a list of clients. */ #define wl_client_for_each(client, list) \ for (client = wl_client_from_link((list)->next); \ wl_client_get_link(client) != (list); \ @@ -370,7 +282,6 @@ void wl_client_add_resource_created_listener(struct wl_client *client, struct wl_listener *listener); -/** Callback function type for wl_client_for_each_resource() */ typedef enum wl_iterator_result (*wl_client_for_each_resource_iterator_func_t)( struct wl_resource *resource, void *user_data); @@ -393,112 +304,28 @@ wl_client_get_user_data(struct wl_client *client); void wl_client_set_max_buffer_size(struct wl_client *client, size_t max_buffer_size); -/** \class wl_listener - * - * \brief A single listener for Wayland signals - * - * wl_listener provides the means to listen for wl_signal notifications. Many - * Wayland objects use wl_listener for notification of significant events like - * object destruction. - * - * Clients should create wl_listener objects manually and can register them as - * listeners to signals using wl_signal_add(), assuming the signal is - * directly accessible. For opaque structs like wl_event_loop, adding a - * listener should be done through provided accessor methods. A listener can - * only listen to one signal at a time. - * - * \code - * struct wl_listener your_listener; - * - * your_listener.notify = your_callback_method; - * - * // Direct access - * wl_signal_add(&some_object->destroy_signal, &your_listener); - * - * // Accessor access - * wl_event_loop *loop = ...; - * wl_event_loop_add_destroy_listener(loop, &your_listener); - * \endcode - * - * If the listener is part of a larger struct, #wl_container_of can be used - * to retrieve a pointer to it: - * - * \code - * void your_listener(struct wl_listener *listener, void *data) - * { - * struct your_data *data; - * - * your_data = wl_container_of(listener, data, your_member_name); - * } - * \endcode - * - * If you need to remove a listener from a signal, use wl_list_remove(). - * - * \code - * wl_list_remove(&your_listener.link); - * \endcode - * - * \sa wl_signal - */ struct wl_listener { - /** Part of wl_signal::listener_list */ struct wl_list link; - /** Callback function pointer */ wl_notify_func_t notify; }; -/** \class wl_signal - * - * \brief A source of a type of observable event - * - * Signals are recognized points where significant events can be observed. - * Compositors as well as the server can provide signals. Observers are - * wl_listener's that are added through #wl_signal_add. Signals are emitted - * using #wl_signal_emit, which will invoke all listeners until that - * listener is removed by wl_list_remove() (or whenever the signal is - * destroyed). - * - * \sa wl_listener for more information on using wl_signal - */ struct wl_signal { struct wl_list listener_list; }; -/** Initialize a new \ref wl_signal for use. - * - * \param signal The signal that will be initialized - * - * \memberof wl_signal - */ static inline void wl_signal_init(struct wl_signal *signal) { wl_list_init(&signal->listener_list); } -/** Add the specified listener to this signal. - * - * \param signal The signal that will emit events to the listener - * \param listener The listener to add - * - * \memberof wl_signal - */ static inline void wl_signal_add(struct wl_signal *signal, struct wl_listener *listener) { wl_list_insert(signal->listener_list.prev, &listener->link); } -/** Gets the listener struct for the specified callback. - * - * \param signal The signal that contains the specified listener - * \param notify The listener that is the target of this search - * \return the list item that corresponds to the specified listener, or NULL - * if none was found - * - * \memberof wl_signal - */ static inline struct wl_listener * wl_signal_get(struct wl_signal *signal, wl_notify_func_t notify) { @@ -511,13 +338,6 @@ wl_signal_get(struct wl_signal *signal, wl_notify_func_t notify) return NULL; } -/** Emits this signal, notifying all registered listeners. - * - * \param signal The signal object that will emit the signal - * \param data The data that will be emitted with the signal - * - * \memberof wl_signal - */ static inline void wl_signal_emit(struct wl_signal *signal, void *data) { @@ -532,22 +352,6 @@ wl_signal_emit_mutable(struct wl_signal *signal, void *data); typedef void (*wl_resource_destroy_func_t)(struct wl_resource *resource); -/* - * Post an event to the client's object referred to by 'resource'. - * 'opcode' is the event number generated from the protocol XML - * description (the event name). The variable arguments are the event - * parameters, in the order they appear in the protocol XML specification. - * - * The variable arguments' types are: - * - type=uint: uint32_t - * - type=int: int32_t - * - type=fixed: wl_fixed_t - * - type=string: (const char *) to a nil-terminated string - * - type=array: (struct wl_array *) - * - type=fd: int, that is an open file descriptor - * - type=new_id: (struct wl_object *) or (struct wl_resource *) - * - type=object: (struct wl_object *) or (struct wl_resource *) - */ void wl_resource_post_event(struct wl_resource *resource, uint32_t opcode, ...); @@ -723,7 +527,6 @@ struct wl_protocol_logger_message { const union wl_argument *arguments; }; -/** Callback function type for wl_display_add_protocol_logger() */ typedef void (*wl_protocol_logger_func_t)(void *user_data, enum wl_protocol_logger_type direction, const struct wl_protocol_logger_message *message); diff --git a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-protocol.h b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-protocol.h index 484faff1b..86ac93f08 100644 --- a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.26.0 */ #ifndef WAYLAND_SERVER_PROTOCOL_H #define WAYLAND_SERVER_PROTOCOL_H @@ -14,34 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_wayland The wayland protocol - * @section page_ifaces_wayland Interfaces - * - @subpage page_iface_wl_display - core global object - * - @subpage page_iface_wl_registry - global registry object - * - @subpage page_iface_wl_callback - callback object - * - @subpage page_iface_wl_compositor - the compositor singleton - * - @subpage page_iface_wl_shm_pool - a shared memory pool - * - @subpage page_iface_wl_shm - shared memory support - * - @subpage page_iface_wl_buffer - content for a wl_surface - * - @subpage page_iface_wl_data_offer - offer to transfer data - * - @subpage page_iface_wl_data_source - offer to transfer data - * - @subpage page_iface_wl_data_device - data transfer device - * - @subpage page_iface_wl_data_device_manager - data transfer interface - * - @subpage page_iface_wl_shell - create desktop-style surfaces - * - @subpage page_iface_wl_shell_surface - desktop-style metadata interface - * - @subpage page_iface_wl_surface - an onscreen surface - * - @subpage page_iface_wl_seat - group of input devices - * - @subpage page_iface_wl_pointer - pointer input device - * - @subpage page_iface_wl_keyboard - keyboard input device - * - @subpage page_iface_wl_touch - touchscreen input device - * - @subpage page_iface_wl_output - compositor output region - * - @subpage page_iface_wl_region - region interface - * - @subpage page_iface_wl_subcompositor - sub-surface compositing - * - @subpage page_iface_wl_subsurface - sub-surface interface to a wl_surface - * - @subpage page_iface_wl_fixes - wayland protocol fixes +/* * @section page_copyright_wayland Copyright - *
  *
  * Copyright © 2008-2011 Kristian Høgsberg
  * Copyright © 2010-2011 Intel Corporation
@@ -67,8 +40,34 @@ struct wl_resource;
  * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  * SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_buffer; struct wl_callback; struct wl_compositor; @@ -95,904 +94,109 @@ struct wl_touch; #ifndef WL_DISPLAY_INTERFACE #define WL_DISPLAY_INTERFACE -/** - * @page page_iface_wl_display wl_display - * @section page_iface_wl_display_desc Description - * - * The core global object. This is a special singleton object. It - * is used for internal Wayland protocol features. - * @section page_iface_wl_display_api API - * See @ref iface_wl_display. - */ -/** - * @defgroup iface_wl_display The wl_display interface - * - * The core global object. This is a special singleton object. It - * is used for internal Wayland protocol features. - */ extern const struct wl_interface wl_display_interface; #endif #ifndef WL_REGISTRY_INTERFACE #define WL_REGISTRY_INTERFACE -/** - * @page page_iface_wl_registry wl_registry - * @section page_iface_wl_registry_desc Description - * - * The singleton global registry object. The server has a number of - * global objects that are available to all clients. These objects - * typically represent an actual object in the server (for example, - * an input device) or they are singleton objects that provide - * extension functionality. - * - * When a client creates a registry object, the registry object - * will emit a global event for each global currently in the - * registry. Globals come and go as a result of device or - * monitor hotplugs, reconfiguration or other events, and the - * registry will send out global and global_remove events to - * keep the client up to date with the changes. To mark the end - * of the initial burst of events, the client can use the - * wl_display.sync request immediately after calling - * wl_display.get_registry. - * - * A client can bind to a global object by using the bind - * request. This creates a client-side handle that lets the object - * emit events to the client and lets the client invoke requests on - * the object. - * @section page_iface_wl_registry_api API - * See @ref iface_wl_registry. - */ -/** - * @defgroup iface_wl_registry The wl_registry interface - * - * The singleton global registry object. The server has a number of - * global objects that are available to all clients. These objects - * typically represent an actual object in the server (for example, - * an input device) or they are singleton objects that provide - * extension functionality. - * - * When a client creates a registry object, the registry object - * will emit a global event for each global currently in the - * registry. Globals come and go as a result of device or - * monitor hotplugs, reconfiguration or other events, and the - * registry will send out global and global_remove events to - * keep the client up to date with the changes. To mark the end - * of the initial burst of events, the client can use the - * wl_display.sync request immediately after calling - * wl_display.get_registry. - * - * A client can bind to a global object by using the bind - * request. This creates a client-side handle that lets the object - * emit events to the client and lets the client invoke requests on - * the object. - */ extern const struct wl_interface wl_registry_interface; #endif #ifndef WL_CALLBACK_INTERFACE #define WL_CALLBACK_INTERFACE -/** - * @page page_iface_wl_callback wl_callback - * @section page_iface_wl_callback_desc Description - * - * Clients can handle the 'done' event to get notified when - * the related request is done. - * - * Note, because wl_callback objects are created from multiple independent - * factory interfaces, the wl_callback interface is frozen at version 1. - * @section page_iface_wl_callback_api API - * See @ref iface_wl_callback. - */ -/** - * @defgroup iface_wl_callback The wl_callback interface - * - * Clients can handle the 'done' event to get notified when - * the related request is done. - * - * Note, because wl_callback objects are created from multiple independent - * factory interfaces, the wl_callback interface is frozen at version 1. - */ extern const struct wl_interface wl_callback_interface; #endif #ifndef WL_COMPOSITOR_INTERFACE #define WL_COMPOSITOR_INTERFACE -/** - * @page page_iface_wl_compositor wl_compositor - * @section page_iface_wl_compositor_desc Description - * - * A compositor. This object is a singleton global. The - * compositor is in charge of combining the contents of multiple - * surfaces into one displayable output. - * @section page_iface_wl_compositor_api API - * See @ref iface_wl_compositor. - */ -/** - * @defgroup iface_wl_compositor The wl_compositor interface - * - * A compositor. This object is a singleton global. The - * compositor is in charge of combining the contents of multiple - * surfaces into one displayable output. - */ extern const struct wl_interface wl_compositor_interface; #endif #ifndef WL_SHM_POOL_INTERFACE #define WL_SHM_POOL_INTERFACE -/** - * @page page_iface_wl_shm_pool wl_shm_pool - * @section page_iface_wl_shm_pool_desc Description - * - * The wl_shm_pool object encapsulates a piece of memory shared - * between the compositor and client. Through the wl_shm_pool - * object, the client can allocate shared memory wl_buffer objects. - * All objects created through the same pool share the same - * underlying mapped memory. Reusing the mapped memory avoids the - * setup/teardown overhead and is useful when interactively resizing - * a surface or for many small buffers. - * @section page_iface_wl_shm_pool_api API - * See @ref iface_wl_shm_pool. - */ -/** - * @defgroup iface_wl_shm_pool The wl_shm_pool interface - * - * The wl_shm_pool object encapsulates a piece of memory shared - * between the compositor and client. Through the wl_shm_pool - * object, the client can allocate shared memory wl_buffer objects. - * All objects created through the same pool share the same - * underlying mapped memory. Reusing the mapped memory avoids the - * setup/teardown overhead and is useful when interactively resizing - * a surface or for many small buffers. - */ extern const struct wl_interface wl_shm_pool_interface; #endif #ifndef WL_SHM_INTERFACE #define WL_SHM_INTERFACE -/** - * @page page_iface_wl_shm wl_shm - * @section page_iface_wl_shm_desc Description - * - * A singleton global object that provides support for shared - * memory. - * - * Clients can create wl_shm_pool objects using the create_pool - * request. - * - * On binding the wl_shm object one or more format events - * are emitted to inform clients about the valid pixel formats - * that can be used for buffers. - * @section page_iface_wl_shm_api API - * See @ref iface_wl_shm. - */ -/** - * @defgroup iface_wl_shm The wl_shm interface - * - * A singleton global object that provides support for shared - * memory. - * - * Clients can create wl_shm_pool objects using the create_pool - * request. - * - * On binding the wl_shm object one or more format events - * are emitted to inform clients about the valid pixel formats - * that can be used for buffers. - */ extern const struct wl_interface wl_shm_interface; #endif #ifndef WL_BUFFER_INTERFACE #define WL_BUFFER_INTERFACE -/** - * @page page_iface_wl_buffer wl_buffer - * @section page_iface_wl_buffer_desc Description - * - * A buffer provides the content for a wl_surface. Buffers are - * created through factory interfaces such as wl_shm, wp_linux_buffer_params - * (from the linux-dmabuf protocol extension) or similar. It has a width and - * a height and can be attached to a wl_surface, but the mechanism by which a - * client provides and updates the contents is defined by the buffer factory - * interface. - * - * Color channels are assumed to be electrical rather than optical (in other - * words, encoded with a transfer function) unless otherwise specified. If - * the buffer uses a format that has an alpha channel, the alpha channel is - * assumed to be premultiplied into the electrical color channel values - * (after transfer function encoding) unless otherwise specified. - * - * Note, because wl_buffer objects are created from multiple independent - * factory interfaces, the wl_buffer interface is frozen at version 1. - * @section page_iface_wl_buffer_api API - * See @ref iface_wl_buffer. - */ -/** - * @defgroup iface_wl_buffer The wl_buffer interface - * - * A buffer provides the content for a wl_surface. Buffers are - * created through factory interfaces such as wl_shm, wp_linux_buffer_params - * (from the linux-dmabuf protocol extension) or similar. It has a width and - * a height and can be attached to a wl_surface, but the mechanism by which a - * client provides and updates the contents is defined by the buffer factory - * interface. - * - * Color channels are assumed to be electrical rather than optical (in other - * words, encoded with a transfer function) unless otherwise specified. If - * the buffer uses a format that has an alpha channel, the alpha channel is - * assumed to be premultiplied into the electrical color channel values - * (after transfer function encoding) unless otherwise specified. - * - * Note, because wl_buffer objects are created from multiple independent - * factory interfaces, the wl_buffer interface is frozen at version 1. - */ extern const struct wl_interface wl_buffer_interface; #endif #ifndef WL_DATA_OFFER_INTERFACE #define WL_DATA_OFFER_INTERFACE -/** - * @page page_iface_wl_data_offer wl_data_offer - * @section page_iface_wl_data_offer_desc Description - * - * A wl_data_offer represents a piece of data offered for transfer - * by another client (the source client). It is used by the - * copy-and-paste and drag-and-drop mechanisms. The offer - * describes the different mime types that the data can be - * converted to and provides the mechanism for transferring the - * data directly from the source client. - * @section page_iface_wl_data_offer_api API - * See @ref iface_wl_data_offer. - */ -/** - * @defgroup iface_wl_data_offer The wl_data_offer interface - * - * A wl_data_offer represents a piece of data offered for transfer - * by another client (the source client). It is used by the - * copy-and-paste and drag-and-drop mechanisms. The offer - * describes the different mime types that the data can be - * converted to and provides the mechanism for transferring the - * data directly from the source client. - */ extern const struct wl_interface wl_data_offer_interface; #endif #ifndef WL_DATA_SOURCE_INTERFACE #define WL_DATA_SOURCE_INTERFACE -/** - * @page page_iface_wl_data_source wl_data_source - * @section page_iface_wl_data_source_desc Description - * - * The wl_data_source object is the source side of a wl_data_offer. - * It is created by the source client in a data transfer and - * provides a way to describe the offered data and a way to respond - * to requests to transfer the data. - * @section page_iface_wl_data_source_api API - * See @ref iface_wl_data_source. - */ -/** - * @defgroup iface_wl_data_source The wl_data_source interface - * - * The wl_data_source object is the source side of a wl_data_offer. - * It is created by the source client in a data transfer and - * provides a way to describe the offered data and a way to respond - * to requests to transfer the data. - */ extern const struct wl_interface wl_data_source_interface; #endif #ifndef WL_DATA_DEVICE_INTERFACE #define WL_DATA_DEVICE_INTERFACE -/** - * @page page_iface_wl_data_device wl_data_device - * @section page_iface_wl_data_device_desc Description - * - * There is one wl_data_device per seat which can be obtained - * from the global wl_data_device_manager singleton. - * - * A wl_data_device provides access to inter-client data transfer - * mechanisms such as copy-and-paste and drag-and-drop. - * @section page_iface_wl_data_device_api API - * See @ref iface_wl_data_device. - */ -/** - * @defgroup iface_wl_data_device The wl_data_device interface - * - * There is one wl_data_device per seat which can be obtained - * from the global wl_data_device_manager singleton. - * - * A wl_data_device provides access to inter-client data transfer - * mechanisms such as copy-and-paste and drag-and-drop. - */ extern const struct wl_interface wl_data_device_interface; #endif #ifndef WL_DATA_DEVICE_MANAGER_INTERFACE #define WL_DATA_DEVICE_MANAGER_INTERFACE -/** - * @page page_iface_wl_data_device_manager wl_data_device_manager - * @section page_iface_wl_data_device_manager_desc Description - * - * The wl_data_device_manager is a singleton global object that - * provides access to inter-client data transfer mechanisms such as - * copy-and-paste and drag-and-drop. These mechanisms are tied to - * a wl_seat and this interface lets a client get a wl_data_device - * corresponding to a wl_seat. - * - * Depending on the version bound, the objects created from the bound - * wl_data_device_manager object will have different requirements for - * functioning properly. See wl_data_source.set_actions, - * wl_data_offer.accept and wl_data_offer.finish for details. - * @section page_iface_wl_data_device_manager_api API - * See @ref iface_wl_data_device_manager. - */ -/** - * @defgroup iface_wl_data_device_manager The wl_data_device_manager interface - * - * The wl_data_device_manager is a singleton global object that - * provides access to inter-client data transfer mechanisms such as - * copy-and-paste and drag-and-drop. These mechanisms are tied to - * a wl_seat and this interface lets a client get a wl_data_device - * corresponding to a wl_seat. - * - * Depending on the version bound, the objects created from the bound - * wl_data_device_manager object will have different requirements for - * functioning properly. See wl_data_source.set_actions, - * wl_data_offer.accept and wl_data_offer.finish for details. - */ extern const struct wl_interface wl_data_device_manager_interface; #endif #ifndef WL_SHELL_INTERFACE #define WL_SHELL_INTERFACE -/** - * @page page_iface_wl_shell wl_shell - * @section page_iface_wl_shell_desc Description - * - * This interface is implemented by servers that provide - * desktop-style user interfaces. - * - * It allows clients to associate a wl_shell_surface with - * a basic surface. - * - * Note! This protocol is deprecated and not intended for production use. - * For desktop-style user interfaces, use xdg_shell. Compositors and clients - * should not implement this interface. - * @section page_iface_wl_shell_api API - * See @ref iface_wl_shell. - */ -/** - * @defgroup iface_wl_shell The wl_shell interface - * - * This interface is implemented by servers that provide - * desktop-style user interfaces. - * - * It allows clients to associate a wl_shell_surface with - * a basic surface. - * - * Note! This protocol is deprecated and not intended for production use. - * For desktop-style user interfaces, use xdg_shell. Compositors and clients - * should not implement this interface. - */ extern const struct wl_interface wl_shell_interface; #endif #ifndef WL_SHELL_SURFACE_INTERFACE #define WL_SHELL_SURFACE_INTERFACE -/** - * @page page_iface_wl_shell_surface wl_shell_surface - * @section page_iface_wl_shell_surface_desc Description - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides requests to treat surfaces like toplevel, fullscreen - * or popup windows, move, resize or maximize them, associate - * metadata like title and class, etc. - * - * On the server side the object is automatically destroyed when - * the related wl_surface is destroyed. On the client side, - * wl_shell_surface_destroy() must be called before destroying - * the wl_surface object. - * @section page_iface_wl_shell_surface_api API - * See @ref iface_wl_shell_surface. - */ -/** - * @defgroup iface_wl_shell_surface The wl_shell_surface interface - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides requests to treat surfaces like toplevel, fullscreen - * or popup windows, move, resize or maximize them, associate - * metadata like title and class, etc. - * - * On the server side the object is automatically destroyed when - * the related wl_surface is destroyed. On the client side, - * wl_shell_surface_destroy() must be called before destroying - * the wl_surface object. - */ extern const struct wl_interface wl_shell_surface_interface; #endif #ifndef WL_SURFACE_INTERFACE #define WL_SURFACE_INTERFACE -/** - * @page page_iface_wl_surface wl_surface - * @section page_iface_wl_surface_desc Description - * - * A surface is a rectangular area that may be displayed on zero - * or more outputs, and shown any number of times at the compositor's - * discretion. They can present wl_buffers, receive user input, and - * define a local coordinate system. - * - * The size of a surface (and relative positions on it) is described - * in surface-local coordinates, which may differ from the buffer - * coordinates of the pixel content, in case a buffer_transform - * or a buffer_scale is used. - * - * A surface without a "role" is fairly useless: a compositor does - * not know where, when or how to present it. The role is the - * purpose of a wl_surface. Examples of roles are a cursor for a - * pointer (as set by wl_pointer.set_cursor), a drag icon - * (wl_data_device.start_drag), a sub-surface - * (wl_subcompositor.get_subsurface), and a window as defined by a - * shell protocol (e.g. wl_shell.get_shell_surface). - * - * A surface can have only one role at a time. Initially a - * wl_surface does not have a role. Once a wl_surface is given a - * role, it is set permanently for the whole lifetime of the - * wl_surface object. Giving the current role again is allowed, - * unless explicitly forbidden by the relevant interface - * specification. - * - * Surface roles are given by requests in other interfaces such as - * wl_pointer.set_cursor. The request should explicitly mention - * that this request gives a role to a wl_surface. Often, this - * request also creates a new protocol object that represents the - * role and adds additional functionality to wl_surface. When a - * client wants to destroy a wl_surface, they must destroy this role - * object before the wl_surface, otherwise a defunct_role_object error is - * sent. - * - * Destroying the role object does not remove the role from the - * wl_surface, but it may stop the wl_surface from "playing the role". - * For instance, if a wl_subsurface object is destroyed, the wl_surface - * it was created for will be unmapped and forget its position and - * z-order. It is allowed to create a wl_subsurface for the same - * wl_surface again, but it is not allowed to use the wl_surface as - * a cursor (cursor is a different role than sub-surface, and role - * switching is not allowed). - * @section page_iface_wl_surface_api API - * See @ref iface_wl_surface. - */ -/** - * @defgroup iface_wl_surface The wl_surface interface - * - * A surface is a rectangular area that may be displayed on zero - * or more outputs, and shown any number of times at the compositor's - * discretion. They can present wl_buffers, receive user input, and - * define a local coordinate system. - * - * The size of a surface (and relative positions on it) is described - * in surface-local coordinates, which may differ from the buffer - * coordinates of the pixel content, in case a buffer_transform - * or a buffer_scale is used. - * - * A surface without a "role" is fairly useless: a compositor does - * not know where, when or how to present it. The role is the - * purpose of a wl_surface. Examples of roles are a cursor for a - * pointer (as set by wl_pointer.set_cursor), a drag icon - * (wl_data_device.start_drag), a sub-surface - * (wl_subcompositor.get_subsurface), and a window as defined by a - * shell protocol (e.g. wl_shell.get_shell_surface). - * - * A surface can have only one role at a time. Initially a - * wl_surface does not have a role. Once a wl_surface is given a - * role, it is set permanently for the whole lifetime of the - * wl_surface object. Giving the current role again is allowed, - * unless explicitly forbidden by the relevant interface - * specification. - * - * Surface roles are given by requests in other interfaces such as - * wl_pointer.set_cursor. The request should explicitly mention - * that this request gives a role to a wl_surface. Often, this - * request also creates a new protocol object that represents the - * role and adds additional functionality to wl_surface. When a - * client wants to destroy a wl_surface, they must destroy this role - * object before the wl_surface, otherwise a defunct_role_object error is - * sent. - * - * Destroying the role object does not remove the role from the - * wl_surface, but it may stop the wl_surface from "playing the role". - * For instance, if a wl_subsurface object is destroyed, the wl_surface - * it was created for will be unmapped and forget its position and - * z-order. It is allowed to create a wl_subsurface for the same - * wl_surface again, but it is not allowed to use the wl_surface as - * a cursor (cursor is a different role than sub-surface, and role - * switching is not allowed). - */ extern const struct wl_interface wl_surface_interface; #endif #ifndef WL_SEAT_INTERFACE #define WL_SEAT_INTERFACE -/** - * @page page_iface_wl_seat wl_seat - * @section page_iface_wl_seat_desc Description - * - * A seat is a group of keyboards, pointer and touch devices. This - * object is published as a global during start up, or when such a - * device is hot plugged. A seat typically has a pointer and - * maintains a keyboard focus and a pointer focus. - * @section page_iface_wl_seat_api API - * See @ref iface_wl_seat. - */ -/** - * @defgroup iface_wl_seat The wl_seat interface - * - * A seat is a group of keyboards, pointer and touch devices. This - * object is published as a global during start up, or when such a - * device is hot plugged. A seat typically has a pointer and - * maintains a keyboard focus and a pointer focus. - */ extern const struct wl_interface wl_seat_interface; #endif #ifndef WL_POINTER_INTERFACE #define WL_POINTER_INTERFACE -/** - * @page page_iface_wl_pointer wl_pointer - * @section page_iface_wl_pointer_desc Description - * - * The wl_pointer interface represents one or more input devices, - * such as mice, which control the pointer location and pointer_focus - * of a seat. - * - * The wl_pointer interface generates motion, enter and leave - * events for the surfaces that the pointer is located over, - * and button and axis events for button presses, button releases - * and scrolling. - * @section page_iface_wl_pointer_api API - * See @ref iface_wl_pointer. - */ -/** - * @defgroup iface_wl_pointer The wl_pointer interface - * - * The wl_pointer interface represents one or more input devices, - * such as mice, which control the pointer location and pointer_focus - * of a seat. - * - * The wl_pointer interface generates motion, enter and leave - * events for the surfaces that the pointer is located over, - * and button and axis events for button presses, button releases - * and scrolling. - */ extern const struct wl_interface wl_pointer_interface; #endif #ifndef WL_KEYBOARD_INTERFACE #define WL_KEYBOARD_INTERFACE -/** - * @page page_iface_wl_keyboard wl_keyboard - * @section page_iface_wl_keyboard_desc Description - * - * The wl_keyboard interface represents one or more keyboards - * associated with a seat. - * - * Each wl_keyboard has the following logical state: - * - * - an active surface (possibly null), - * - the keys currently logically down, - * - the active modifiers, - * - the active group. - * - * By default, the active surface is null, the keys currently logically down - * are empty, the active modifiers and the active group are 0. - * @section page_iface_wl_keyboard_api API - * See @ref iface_wl_keyboard. - */ -/** - * @defgroup iface_wl_keyboard The wl_keyboard interface - * - * The wl_keyboard interface represents one or more keyboards - * associated with a seat. - * - * Each wl_keyboard has the following logical state: - * - * - an active surface (possibly null), - * - the keys currently logically down, - * - the active modifiers, - * - the active group. - * - * By default, the active surface is null, the keys currently logically down - * are empty, the active modifiers and the active group are 0. - */ extern const struct wl_interface wl_keyboard_interface; #endif #ifndef WL_TOUCH_INTERFACE #define WL_TOUCH_INTERFACE -/** - * @page page_iface_wl_touch wl_touch - * @section page_iface_wl_touch_desc Description - * - * The wl_touch interface represents a touchscreen - * associated with a seat. - * - * Touch interactions can consist of one or more contacts. - * For each contact, a series of events is generated, starting - * with a down event, followed by zero or more motion events, - * and ending with an up event. Events relating to the same - * contact point can be identified by the ID of the sequence. - * @section page_iface_wl_touch_api API - * See @ref iface_wl_touch. - */ -/** - * @defgroup iface_wl_touch The wl_touch interface - * - * The wl_touch interface represents a touchscreen - * associated with a seat. - * - * Touch interactions can consist of one or more contacts. - * For each contact, a series of events is generated, starting - * with a down event, followed by zero or more motion events, - * and ending with an up event. Events relating to the same - * contact point can be identified by the ID of the sequence. - */ extern const struct wl_interface wl_touch_interface; #endif #ifndef WL_OUTPUT_INTERFACE #define WL_OUTPUT_INTERFACE -/** - * @page page_iface_wl_output wl_output - * @section page_iface_wl_output_desc Description - * - * An output describes part of the compositor geometry. The - * compositor works in the 'compositor coordinate system' and an - * output corresponds to a rectangular area in that space that is - * actually visible. This typically corresponds to a monitor that - * displays part of the compositor space. This object is published - * as global during start up, or when a monitor is hotplugged. - * @section page_iface_wl_output_api API - * See @ref iface_wl_output. - */ -/** - * @defgroup iface_wl_output The wl_output interface - * - * An output describes part of the compositor geometry. The - * compositor works in the 'compositor coordinate system' and an - * output corresponds to a rectangular area in that space that is - * actually visible. This typically corresponds to a monitor that - * displays part of the compositor space. This object is published - * as global during start up, or when a monitor is hotplugged. - */ extern const struct wl_interface wl_output_interface; #endif #ifndef WL_REGION_INTERFACE #define WL_REGION_INTERFACE -/** - * @page page_iface_wl_region wl_region - * @section page_iface_wl_region_desc Description - * - * A region object describes an area. - * - * Region objects are used to describe the opaque and input - * regions of a surface. - * @section page_iface_wl_region_api API - * See @ref iface_wl_region. - */ -/** - * @defgroup iface_wl_region The wl_region interface - * - * A region object describes an area. - * - * Region objects are used to describe the opaque and input - * regions of a surface. - */ extern const struct wl_interface wl_region_interface; #endif #ifndef WL_SUBCOMPOSITOR_INTERFACE #define WL_SUBCOMPOSITOR_INTERFACE -/** - * @page page_iface_wl_subcompositor wl_subcompositor - * @section page_iface_wl_subcompositor_desc Description - * - * The global interface exposing sub-surface compositing capabilities. - * A wl_surface, that has sub-surfaces associated, is called the - * parent surface. Sub-surfaces can be arbitrarily nested and create - * a tree of sub-surfaces. - * - * The root surface in a tree of sub-surfaces is the main - * surface. The main surface cannot be a sub-surface, because - * sub-surfaces must always have a parent. - * - * A main surface with its sub-surfaces forms a (compound) window. - * For window management purposes, this set of wl_surface objects is - * to be considered as a single window, and it should also behave as - * such. - * - * The aim of sub-surfaces is to offload some of the compositing work - * within a window from clients to the compositor. A prime example is - * a video player with decorations and video in separate wl_surface - * objects. This should allow the compositor to pass YUV video buffer - * processing to dedicated overlay hardware when possible. - * @section page_iface_wl_subcompositor_api API - * See @ref iface_wl_subcompositor. - */ -/** - * @defgroup iface_wl_subcompositor The wl_subcompositor interface - * - * The global interface exposing sub-surface compositing capabilities. - * A wl_surface, that has sub-surfaces associated, is called the - * parent surface. Sub-surfaces can be arbitrarily nested and create - * a tree of sub-surfaces. - * - * The root surface in a tree of sub-surfaces is the main - * surface. The main surface cannot be a sub-surface, because - * sub-surfaces must always have a parent. - * - * A main surface with its sub-surfaces forms a (compound) window. - * For window management purposes, this set of wl_surface objects is - * to be considered as a single window, and it should also behave as - * such. - * - * The aim of sub-surfaces is to offload some of the compositing work - * within a window from clients to the compositor. A prime example is - * a video player with decorations and video in separate wl_surface - * objects. This should allow the compositor to pass YUV video buffer - * processing to dedicated overlay hardware when possible. - */ extern const struct wl_interface wl_subcompositor_interface; #endif #ifndef WL_SUBSURFACE_INTERFACE #define WL_SUBSURFACE_INTERFACE -/** - * @page page_iface_wl_subsurface wl_subsurface - * @section page_iface_wl_subsurface_desc Description - * - * An additional interface to a wl_surface object, which has been - * made a sub-surface. A sub-surface has one parent surface. A - * sub-surface's size and position are not limited to that of the parent. - * Particularly, a sub-surface is not automatically clipped to its - * parent's area. - * - * A sub-surface becomes mapped, when a non-NULL wl_buffer is applied - * and the parent surface is mapped. The order of which one happens - * first is irrelevant. A sub-surface is hidden if the parent becomes - * hidden, or if a NULL wl_buffer is applied. These rules apply - * recursively through the tree of surfaces. - * - * A sub-surface can be in one of two modes. The possible modes are - * synchronized and desynchronized, see methods wl_subsurface.set_sync and - * wl_subsurface.set_desync. - * - * The main surface can be thought to be always in desynchronized mode, - * since it does not have a parent in the sub-surfaces sense. - * - * Even if a sub-surface is in desynchronized mode, it will behave as - * in synchronized mode, if its parent surface behaves as in - * synchronized mode. This rule is applied recursively throughout the - * tree of surfaces. This means, that one can set a sub-surface into - * synchronized mode, and then assume that all its child and grand-child - * sub-surfaces are synchronized, too, without explicitly setting them. - * - * If a surface behaves as in synchronized mode, it is effectively - * synchronized, otherwise it is effectively desynchronized. - * - * A sub-surface is initially in the synchronized mode. - * - * The wl_subsurface interface has requests which modify double-buffered - * state of the parent surface (wl_subsurface.set_position, .place_above and - * .place_below). - * - * Destroying a sub-surface takes effect immediately. If you need to - * synchronize the removal of a sub-surface to the parent surface update, - * unmap the sub-surface first by attaching a NULL wl_buffer, update parent, - * and then destroy the sub-surface. - * - * If the parent wl_surface object is destroyed, the sub-surface is - * unmapped. - * - * A sub-surface never has the keyboard focus of any seat. - * - * The wl_surface.offset request is ignored: clients must use set_position - * instead to move the sub-surface. - * @section page_iface_wl_subsurface_api API - * See @ref iface_wl_subsurface. - */ -/** - * @defgroup iface_wl_subsurface The wl_subsurface interface - * - * An additional interface to a wl_surface object, which has been - * made a sub-surface. A sub-surface has one parent surface. A - * sub-surface's size and position are not limited to that of the parent. - * Particularly, a sub-surface is not automatically clipped to its - * parent's area. - * - * A sub-surface becomes mapped, when a non-NULL wl_buffer is applied - * and the parent surface is mapped. The order of which one happens - * first is irrelevant. A sub-surface is hidden if the parent becomes - * hidden, or if a NULL wl_buffer is applied. These rules apply - * recursively through the tree of surfaces. - * - * A sub-surface can be in one of two modes. The possible modes are - * synchronized and desynchronized, see methods wl_subsurface.set_sync and - * wl_subsurface.set_desync. - * - * The main surface can be thought to be always in desynchronized mode, - * since it does not have a parent in the sub-surfaces sense. - * - * Even if a sub-surface is in desynchronized mode, it will behave as - * in synchronized mode, if its parent surface behaves as in - * synchronized mode. This rule is applied recursively throughout the - * tree of surfaces. This means, that one can set a sub-surface into - * synchronized mode, and then assume that all its child and grand-child - * sub-surfaces are synchronized, too, without explicitly setting them. - * - * If a surface behaves as in synchronized mode, it is effectively - * synchronized, otherwise it is effectively desynchronized. - * - * A sub-surface is initially in the synchronized mode. - * - * The wl_subsurface interface has requests which modify double-buffered - * state of the parent surface (wl_subsurface.set_position, .place_above and - * .place_below). - * - * Destroying a sub-surface takes effect immediately. If you need to - * synchronize the removal of a sub-surface to the parent surface update, - * unmap the sub-surface first by attaching a NULL wl_buffer, update parent, - * and then destroy the sub-surface. - * - * If the parent wl_surface object is destroyed, the sub-surface is - * unmapped. - * - * A sub-surface never has the keyboard focus of any seat. - * - * The wl_surface.offset request is ignored: clients must use set_position - * instead to move the sub-surface. - */ extern const struct wl_interface wl_subsurface_interface; #endif #ifndef WL_FIXES_INTERFACE #define WL_FIXES_INTERFACE -/** - * @page page_iface_wl_fixes wl_fixes - * @section page_iface_wl_fixes_desc Description - * - * This global fixes problems with other core-protocol interfaces that - * cannot be fixed in these interfaces themselves. - * @section page_iface_wl_fixes_api API - * See @ref iface_wl_fixes. - */ -/** - * @defgroup iface_wl_fixes The wl_fixes interface - * - * This global fixes problems with other core-protocol interfaces that - * cannot be fixed in these interfaces themselves. - */ extern const struct wl_interface wl_fixes_interface; #endif #ifndef WL_DISPLAY_ERROR_ENUM #define WL_DISPLAY_ERROR_ENUM -/** - * @ingroup iface_wl_display - * global error values - * - * These errors are global and can be emitted in response to any - * server request. - */ enum wl_display_error { - /** - * server couldn't find object - */ WL_DISPLAY_ERROR_INVALID_OBJECT = 0, - /** - * method doesn't exist on the specified interface or malformed request - */ WL_DISPLAY_ERROR_INVALID_METHOD = 1, - /** - * server is out of memory - */ WL_DISPLAY_ERROR_NO_MEMORY = 2, - /** - * implementation error in compositor - */ WL_DISPLAY_ERROR_IMPLEMENTATION = 3, }; -#endif /* WL_DISPLAY_ERROR_ENUM */ +#endif #ifndef WL_DISPLAY_ERROR_ENUM_IS_VALID #define WL_DISPLAY_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_display - * Validate a wl_display error value. - * - * @return true on success, false on error. - * @ref wl_display_error - */ static inline bool wl_display_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1008,47 +212,12 @@ wl_display_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_DISPLAY_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_display - * @struct wl_display_interface - */ struct wl_display_interface { - /** - * asynchronous roundtrip - * - * The sync request asks the server to emit the 'done' event on - * the returned wl_callback object. Since requests are handled - * in-order and events are delivered in-order, this can be used as - * a barrier to ensure all previous requests and the resulting - * events have been handled. - * - * The object returned by this request will be destroyed by the - * compositor after the callback is fired and as such the client - * must not attempt to use it after that point. - * - * The callback_data passed in the callback is undefined and should - * be ignored. - * @param callback callback object for the sync request - */ void (*sync)(struct wl_client *client, struct wl_resource *resource, uint32_t callback); - /** - * get global registry object - * - * This request creates a registry object that allows the client - * to list and bind the global objects available from the - * compositor. - * - * It should be noted that the server side resources consumed in - * response to a get_registry request can only be released when the - * client disconnects, not when the client side proxy is destroyed. - * Therefore, clients should invoke get_registry as infrequently as - * possible to avoid wasting memory. - * @param registry global registry object - */ void (*get_registry)(struct wl_client *client, struct wl_resource *resource, uint32_t registry); @@ -1057,39 +226,13 @@ struct wl_display_interface { #define WL_DISPLAY_ERROR 0 #define WL_DISPLAY_DELETE_ID 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_ERROR_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_DELETE_ID_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_SYNC_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_GET_REGISTRY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - * @struct wl_registry_interface - */ struct wl_registry_interface { - /** - * bind an object to the display - * - * Binds a new, client-created object to the server using the - * specified name as the identifier. - * @param name unique numeric name of the object - * @param interface name of the objects interface - * @param version version of the objects interface - * @param id bound object - */ void (*bind)(struct wl_client *client, struct wl_resource *resource, uint32_t name, @@ -1099,40 +242,17 @@ struct wl_registry_interface { #define WL_REGISTRY_GLOBAL 0 #define WL_REGISTRY_GLOBAL_REMOVE 1 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_GLOBAL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_GLOBAL_REMOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_BIND_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - * Sends an global event to the client owning the resource. - * @param resource_ The client's resource - * @param name numeric name of the global object - * @param interface interface implemented by the object - * @param version interface version - */ static inline void wl_registry_send_global(struct wl_resource *resource_, uint32_t name, const char *interface, uint32_t version) { wl_resource_post_event(resource_, WL_REGISTRY_GLOBAL, name, interface, version); } -/** - * @ingroup iface_wl_registry - * Sends an global_remove event to the client owning the resource. - * @param resource_ The client's resource - * @param name numeric name of the global object - */ static inline void wl_registry_send_global_remove(struct wl_resource *resource_, uint32_t name) { @@ -1141,101 +261,41 @@ wl_registry_send_global_remove(struct wl_resource *resource_, uint32_t name) #define WL_CALLBACK_DONE 0 -/** - * @ingroup iface_wl_callback - */ #define WL_CALLBACK_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_callback - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - * @param callback_data request-specific data for the callback - */ static inline void wl_callback_send_done(struct wl_resource *resource_, uint32_t callback_data) { wl_resource_post_event(resource_, WL_CALLBACK_DONE, callback_data); } -/** - * @ingroup iface_wl_compositor - * @struct wl_compositor_interface - */ struct wl_compositor_interface { - /** - * create new surface - * - * Ask the compositor to create a new surface. - * @param id the new surface - */ void (*create_surface)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * create new region - * - * Ask the compositor to create a new region. - * @param id the new region - */ void (*create_region)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * destroy wl_compositor - * - * This request destroys the wl_compositor. This has no effect on - * any other objects. - * @since 7 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_CREATE_SURFACE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_CREATE_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_RELEASE_SINCE_VERSION 7 #ifndef WL_SHM_POOL_ERROR_ENUM #define WL_SHM_POOL_ERROR_ENUM -/** - * @ingroup iface_wl_shm_pool - * wl_shm_pool error values - * - * These errors can be emitted in response to wl_shm_pool requests. - */ enum wl_shm_pool_error { - /** - * buffer format is not known - */ WL_SHM_POOL_ERROR_INVALID_FORMAT = 0, - /** - * invalid size or stride during buffer creation - */ WL_SHM_POOL_ERROR_INVALID_STRIDE = 1, }; -#endif /* WL_SHM_POOL_ERROR_ENUM */ +#endif #ifndef WL_SHM_POOL_ERROR_ENUM_IS_VALID #define WL_SHM_POOL_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_shm_pool - * Validate a wl_shm_pool error value. - * - * @return true on success, false on error. - * @ref wl_shm_pool_error - */ static inline bool wl_shm_pool_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1247,34 +307,9 @@ wl_shm_pool_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SHM_POOL_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_shm_pool - * @struct wl_shm_pool_interface - */ struct wl_shm_pool_interface { - /** - * create a buffer from the pool - * - * Create a wl_buffer object from the pool. - * - * The buffer is created offset bytes into the pool and has width - * and height as specified. The stride argument specifies the - * number of bytes from the beginning of one row to the beginning - * of the next. The format is the pixel format of the buffer and - * must be one of those advertised through the wl_shm.format event. - * - * A buffer will keep a reference to the pool it was created from - * so it is valid to destroy the pool immediately after creating a - * buffer from it. - * @param id buffer to create - * @param offset buffer byte offset within the pool - * @param width buffer width, in pixels - * @param height buffer height, in pixels - * @param stride number of bytes from the beginning of one row to the beginning of the next row - * @param format buffer pixel format - */ void (*create_buffer)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -1283,84 +318,29 @@ struct wl_shm_pool_interface { int32_t height, int32_t stride, uint32_t format); - /** - * destroy the pool - * - * Destroy the shared memory pool. - * - * The mmapped memory will be released when all buffers that have - * been created from this pool are gone. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * change the size of the pool mapping - * - * This request will cause the server to remap the backing memory - * for the pool from the file descriptor passed when the pool was - * created, but using the new size. This request can only be used - * to make the pool bigger. - * - * This request only changes the amount of bytes that are mmapped - * by the server and does not touch the file corresponding to the - * file descriptor passed at creation time. It is the client's - * responsibility to ensure that the file is at least as big as the - * new pool size. - * @param size new size of the pool, in bytes - */ void (*resize)(struct wl_client *client, struct wl_resource *resource, int32_t size); }; -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_CREATE_BUFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_RESIZE_SINCE_VERSION 1 #ifndef WL_SHM_ERROR_ENUM #define WL_SHM_ERROR_ENUM -/** - * @ingroup iface_wl_shm - * wl_shm error values - * - * These errors can be emitted in response to wl_shm requests. - */ enum wl_shm_error { - /** - * buffer format is not known - * @deprecated Deprecated since version 3 - */ WL_SHM_ERROR_INVALID_FORMAT = 0, - /** - * invalid size or stride during pool creation - */ WL_SHM_ERROR_INVALID_STRIDE = 1, - /** - * mmapping the file descriptor failed - */ WL_SHM_ERROR_INVALID_FD = 2, }; -#endif /* WL_SHM_ERROR_ENUM */ +#endif #ifndef WL_SHM_ERROR_ENUM_IS_VALID #define WL_SHM_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_shm - * Validate a wl_shm error value. - * - * @return true on success, false on error. - * @ref wl_shm_error - */ static inline bool wl_shm_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1374,364 +354,94 @@ wl_shm_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SHM_ERROR_ENUM_IS_VALID */ +#endif #ifndef WL_SHM_FORMAT_ENUM #define WL_SHM_FORMAT_ENUM -/** - * @ingroup iface_wl_shm - * pixel formats - * - * This describes the memory layout of an individual pixel. - * - * All renderers should support argb8888 and xrgb8888 but any other - * formats are optional and may not be supported by the particular - * renderer in use. - * - * The drm format codes match the macros defined in drm_fourcc.h, except - * argb8888 and xrgb8888. The formats actually supported by the compositor - * will be reported by the format event. See drm_fourcc.h for more detailed - * format descriptions. - * - * For all wl_shm formats and unless specified in another protocol - * extension, pre-multiplied alpha is used for pixel values. - */ enum wl_shm_format { - /** - * 32-bit ARGB format, [31:0] A:R:G:B 8:8:8:8 little endian - */ WL_SHM_FORMAT_ARGB8888 = 0, - /** - * 32-bit RGB format, [31:0] x:R:G:B 8:8:8:8 little endian - */ WL_SHM_FORMAT_XRGB8888 = 1, - /** - * 8-bit color index format, [7:0] C - */ WL_SHM_FORMAT_C8 = 0x20203843, - /** - * 8-bit RGB format, [7:0] R:G:B 3:3:2 - */ WL_SHM_FORMAT_RGB332 = 0x38424752, - /** - * 8-bit BGR format, [7:0] B:G:R 2:3:3 - */ WL_SHM_FORMAT_BGR233 = 0x38524742, - /** - * 16-bit xRGB format, [15:0] x:R:G:B 4:4:4:4 little endian - */ WL_SHM_FORMAT_XRGB4444 = 0x32315258, - /** - * 16-bit xBGR format, [15:0] x:B:G:R 4:4:4:4 little endian - */ WL_SHM_FORMAT_XBGR4444 = 0x32314258, - /** - * 16-bit RGBx format, [15:0] R:G:B:x 4:4:4:4 little endian - */ WL_SHM_FORMAT_RGBX4444 = 0x32315852, - /** - * 16-bit BGRx format, [15:0] B:G:R:x 4:4:4:4 little endian - */ WL_SHM_FORMAT_BGRX4444 = 0x32315842, - /** - * 16-bit ARGB format, [15:0] A:R:G:B 4:4:4:4 little endian - */ WL_SHM_FORMAT_ARGB4444 = 0x32315241, - /** - * 16-bit ABGR format, [15:0] A:B:G:R 4:4:4:4 little endian - */ WL_SHM_FORMAT_ABGR4444 = 0x32314241, - /** - * 16-bit RGBA format, [15:0] R:G:B:A 4:4:4:4 little endian - */ WL_SHM_FORMAT_RGBA4444 = 0x32314152, - /** - * 16-bit BGRA format, [15:0] B:G:R:A 4:4:4:4 little endian - */ WL_SHM_FORMAT_BGRA4444 = 0x32314142, - /** - * 16-bit xRGB format, [15:0] x:R:G:B 1:5:5:5 little endian - */ WL_SHM_FORMAT_XRGB1555 = 0x35315258, - /** - * 16-bit xBGR 1555 format, [15:0] x:B:G:R 1:5:5:5 little endian - */ WL_SHM_FORMAT_XBGR1555 = 0x35314258, - /** - * 16-bit RGBx 5551 format, [15:0] R:G:B:x 5:5:5:1 little endian - */ WL_SHM_FORMAT_RGBX5551 = 0x35315852, - /** - * 16-bit BGRx 5551 format, [15:0] B:G:R:x 5:5:5:1 little endian - */ WL_SHM_FORMAT_BGRX5551 = 0x35315842, - /** - * 16-bit ARGB 1555 format, [15:0] A:R:G:B 1:5:5:5 little endian - */ WL_SHM_FORMAT_ARGB1555 = 0x35315241, - /** - * 16-bit ABGR 1555 format, [15:0] A:B:G:R 1:5:5:5 little endian - */ WL_SHM_FORMAT_ABGR1555 = 0x35314241, - /** - * 16-bit RGBA 5551 format, [15:0] R:G:B:A 5:5:5:1 little endian - */ WL_SHM_FORMAT_RGBA5551 = 0x35314152, - /** - * 16-bit BGRA 5551 format, [15:0] B:G:R:A 5:5:5:1 little endian - */ WL_SHM_FORMAT_BGRA5551 = 0x35314142, - /** - * 16-bit RGB 565 format, [15:0] R:G:B 5:6:5 little endian - */ WL_SHM_FORMAT_RGB565 = 0x36314752, - /** - * 16-bit BGR 565 format, [15:0] B:G:R 5:6:5 little endian - */ WL_SHM_FORMAT_BGR565 = 0x36314742, - /** - * 24-bit RGB format, [23:0] R:G:B little endian - */ WL_SHM_FORMAT_RGB888 = 0x34324752, - /** - * 24-bit BGR format, [23:0] B:G:R little endian - */ WL_SHM_FORMAT_BGR888 = 0x34324742, - /** - * 32-bit xBGR format, [31:0] x:B:G:R 8:8:8:8 little endian - */ WL_SHM_FORMAT_XBGR8888 = 0x34324258, - /** - * 32-bit RGBx format, [31:0] R:G:B:x 8:8:8:8 little endian - */ WL_SHM_FORMAT_RGBX8888 = 0x34325852, - /** - * 32-bit BGRx format, [31:0] B:G:R:x 8:8:8:8 little endian - */ WL_SHM_FORMAT_BGRX8888 = 0x34325842, - /** - * 32-bit ABGR format, [31:0] A:B:G:R 8:8:8:8 little endian - */ WL_SHM_FORMAT_ABGR8888 = 0x34324241, - /** - * 32-bit RGBA format, [31:0] R:G:B:A 8:8:8:8 little endian - */ WL_SHM_FORMAT_RGBA8888 = 0x34324152, - /** - * 32-bit BGRA format, [31:0] B:G:R:A 8:8:8:8 little endian - */ WL_SHM_FORMAT_BGRA8888 = 0x34324142, - /** - * 32-bit xRGB format, [31:0] x:R:G:B 2:10:10:10 little endian - */ WL_SHM_FORMAT_XRGB2101010 = 0x30335258, - /** - * 32-bit xBGR format, [31:0] x:B:G:R 2:10:10:10 little endian - */ WL_SHM_FORMAT_XBGR2101010 = 0x30334258, - /** - * 32-bit RGBx format, [31:0] R:G:B:x 10:10:10:2 little endian - */ WL_SHM_FORMAT_RGBX1010102 = 0x30335852, - /** - * 32-bit BGRx format, [31:0] B:G:R:x 10:10:10:2 little endian - */ WL_SHM_FORMAT_BGRX1010102 = 0x30335842, - /** - * 32-bit ARGB format, [31:0] A:R:G:B 2:10:10:10 little endian - */ WL_SHM_FORMAT_ARGB2101010 = 0x30335241, - /** - * 32-bit ABGR format, [31:0] A:B:G:R 2:10:10:10 little endian - */ WL_SHM_FORMAT_ABGR2101010 = 0x30334241, - /** - * 32-bit RGBA format, [31:0] R:G:B:A 10:10:10:2 little endian - */ WL_SHM_FORMAT_RGBA1010102 = 0x30334152, - /** - * 32-bit BGRA format, [31:0] B:G:R:A 10:10:10:2 little endian - */ WL_SHM_FORMAT_BGRA1010102 = 0x30334142, - /** - * packed YCbCr format, [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian - */ WL_SHM_FORMAT_YUYV = 0x56595559, - /** - * packed YCbCr format, [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian - */ WL_SHM_FORMAT_YVYU = 0x55595659, - /** - * packed YCbCr format, [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian - */ WL_SHM_FORMAT_UYVY = 0x59565955, - /** - * packed YCbCr format, [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian - */ WL_SHM_FORMAT_VYUY = 0x59555956, - /** - * packed AYCbCr format, [31:0] A:Y:Cb:Cr 8:8:8:8 little endian - */ WL_SHM_FORMAT_AYUV = 0x56555941, - /** - * 2 plane YCbCr Cr:Cb format, 2x2 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV12 = 0x3231564e, - /** - * 2 plane YCbCr Cb:Cr format, 2x2 subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV21 = 0x3132564e, - /** - * 2 plane YCbCr Cr:Cb format, 2x1 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV16 = 0x3631564e, - /** - * 2 plane YCbCr Cb:Cr format, 2x1 subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV61 = 0x3136564e, - /** - * 3 plane YCbCr format, 4x4 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV410 = 0x39565559, - /** - * 3 plane YCbCr format, 4x4 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU410 = 0x39555659, - /** - * 3 plane YCbCr format, 4x1 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV411 = 0x31315559, - /** - * 3 plane YCbCr format, 4x1 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU411 = 0x31315659, - /** - * 3 plane YCbCr format, 2x2 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV420 = 0x32315559, - /** - * 3 plane YCbCr format, 2x2 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU420 = 0x32315659, - /** - * 3 plane YCbCr format, 2x1 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV422 = 0x36315559, - /** - * 3 plane YCbCr format, 2x1 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU422 = 0x36315659, - /** - * 3 plane YCbCr format, non-subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV444 = 0x34325559, - /** - * 3 plane YCbCr format, non-subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU444 = 0x34325659, - /** - * [7:0] R - */ WL_SHM_FORMAT_R8 = 0x20203852, - /** - * [15:0] R little endian - */ WL_SHM_FORMAT_R16 = 0x20363152, - /** - * [15:0] R:G 8:8 little endian - */ WL_SHM_FORMAT_RG88 = 0x38384752, - /** - * [15:0] G:R 8:8 little endian - */ WL_SHM_FORMAT_GR88 = 0x38385247, - /** - * [31:0] R:G 16:16 little endian - */ WL_SHM_FORMAT_RG1616 = 0x32334752, - /** - * [31:0] G:R 16:16 little endian - */ WL_SHM_FORMAT_GR1616 = 0x32335247, - /** - * [63:0] x:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_XRGB16161616F = 0x48345258, - /** - * [63:0] x:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_XBGR16161616F = 0x48344258, - /** - * [63:0] A:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_ARGB16161616F = 0x48345241, - /** - * [63:0] A:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_ABGR16161616F = 0x48344241, - /** - * [31:0] X:Y:Cb:Cr 8:8:8:8 little endian - */ WL_SHM_FORMAT_XYUV8888 = 0x56555958, - /** - * [23:0] Cr:Cb:Y 8:8:8 little endian - */ WL_SHM_FORMAT_VUY888 = 0x34325556, - /** - * Y followed by U then V, 10:10:10. Non-linear modifier only - */ WL_SHM_FORMAT_VUY101010 = 0x30335556, - /** - * [63:0] Cr0:0:Y1:0:Cb0:0:Y0:0 10:6:10:6:10:6:10:6 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y210 = 0x30313259, - /** - * [63:0] Cr0:0:Y1:0:Cb0:0:Y0:0 12:4:12:4:12:4:12:4 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y212 = 0x32313259, - /** - * [63:0] Cr0:Y1:Cb0:Y0 16:16:16:16 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y216 = 0x36313259, - /** - * [31:0] A:Cr:Y:Cb 2:10:10:10 little endian - */ WL_SHM_FORMAT_Y410 = 0x30313459, - /** - * [63:0] A:0:Cr:0:Y:0:Cb:0 12:4:12:4:12:4:12:4 little endian - */ WL_SHM_FORMAT_Y412 = 0x32313459, - /** - * [63:0] A:Cr:Y:Cb 16:16:16:16 little endian - */ WL_SHM_FORMAT_Y416 = 0x36313459, - /** - * [31:0] X:Cr:Y:Cb 2:10:10:10 little endian - */ WL_SHM_FORMAT_XVYU2101010 = 0x30335658, - /** - * [63:0] X:0:Cr:0:Y:0:Cb:0 12:4:12:4:12:4:12:4 little endian - */ WL_SHM_FORMAT_XVYU12_16161616 = 0x36335658, - /** - * [63:0] X:Cr:Y:Cb 16:16:16:16 little endian - */ WL_SHM_FORMAT_XVYU16161616 = 0x38345658, - /** - * [63:0] A3:A2:Y3:0:Cr0:0:Y2:0:A1:A0:Y1:0:Cb0:0:Y0:0 1:1:8:2:8:2:8:2:1:1:8:2:8:2:8:2 little endian - */ WL_SHM_FORMAT_Y0L0 = 0x304c3059, - /** - * [63:0] X3:X2:Y3:0:Cr0:0:Y2:0:X1:X0:Y1:0:Cb0:0:Y0:0 1:1:8:2:8:2:8:2:1:1:8:2:8:2:8:2 little endian - */ WL_SHM_FORMAT_X0L0 = 0x304c3058, - /** - * [63:0] A3:A2:Y3:Cr0:Y2:A1:A0:Y1:Cb0:Y0 1:1:10:10:10:1:1:10:10:10 little endian - */ WL_SHM_FORMAT_Y0L2 = 0x324c3059, - /** - * [63:0] X3:X2:Y3:Cr0:Y2:X1:X0:Y1:Cb0:Y0 1:1:10:10:10:1:1:10:10:10 little endian - */ WL_SHM_FORMAT_X0L2 = 0x324c3058, WL_SHM_FORMAT_YUV420_8BIT = 0x38305559, WL_SHM_FORMAT_YUV420_10BIT = 0x30315559, @@ -1743,225 +453,65 @@ enum wl_shm_format { WL_SHM_FORMAT_BGR888_A8 = 0x38413842, WL_SHM_FORMAT_RGB565_A8 = 0x38413552, WL_SHM_FORMAT_BGR565_A8 = 0x38413542, - /** - * non-subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV24 = 0x3432564e, - /** - * non-subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV42 = 0x3234564e, - /** - * 2x1 subsampled Cr:Cb plane, 10 bit per channel - */ WL_SHM_FORMAT_P210 = 0x30313250, - /** - * 2x2 subsampled Cr:Cb plane 10 bits per channel - */ WL_SHM_FORMAT_P010 = 0x30313050, - /** - * 2x2 subsampled Cr:Cb plane 12 bits per channel - */ WL_SHM_FORMAT_P012 = 0x32313050, - /** - * 2x2 subsampled Cr:Cb plane 16 bits per channel - */ WL_SHM_FORMAT_P016 = 0x36313050, - /** - * [63:0] A:x:B:x:G:x:R:x 10:6:10:6:10:6:10:6 little endian - */ WL_SHM_FORMAT_AXBXGXRX106106106106 = 0x30314241, - /** - * 2x2 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV15 = 0x3531564e, WL_SHM_FORMAT_Q410 = 0x30313451, WL_SHM_FORMAT_Q401 = 0x31303451, - /** - * [63:0] x:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_XRGB16161616 = 0x38345258, - /** - * [63:0] x:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_XBGR16161616 = 0x38344258, - /** - * [63:0] A:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_ARGB16161616 = 0x38345241, - /** - * [63:0] A:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_ABGR16161616 = 0x38344241, - /** - * [7:0] C0:C1:C2:C3:C4:C5:C6:C7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_C1 = 0x20203143, - /** - * [7:0] C0:C1:C2:C3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_C2 = 0x20203243, - /** - * [7:0] C0:C1 4:4 two pixels/byte - */ WL_SHM_FORMAT_C4 = 0x20203443, - /** - * [7:0] D0:D1:D2:D3:D4:D5:D6:D7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_D1 = 0x20203144, - /** - * [7:0] D0:D1:D2:D3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_D2 = 0x20203244, - /** - * [7:0] D0:D1 4:4 two pixels/byte - */ WL_SHM_FORMAT_D4 = 0x20203444, - /** - * [7:0] D - */ WL_SHM_FORMAT_D8 = 0x20203844, - /** - * [7:0] R0:R1:R2:R3:R4:R5:R6:R7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_R1 = 0x20203152, - /** - * [7:0] R0:R1:R2:R3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_R2 = 0x20203252, - /** - * [7:0] R0:R1 4:4 two pixels/byte - */ WL_SHM_FORMAT_R4 = 0x20203452, - /** - * [15:0] x:R 6:10 little endian - */ WL_SHM_FORMAT_R10 = 0x20303152, - /** - * [15:0] x:R 4:12 little endian - */ WL_SHM_FORMAT_R12 = 0x20323152, - /** - * [31:0] A:Cr:Cb:Y 8:8:8:8 little endian - */ WL_SHM_FORMAT_AVUY8888 = 0x59555641, - /** - * [31:0] X:Cr:Cb:Y 8:8:8:8 little endian - */ WL_SHM_FORMAT_XVUY8888 = 0x59555658, - /** - * 2x2 subsampled Cr:Cb plane 10 bits per channel packed - */ WL_SHM_FORMAT_P030 = 0x30333050, - /** - * [47:0] R:G:B 16:16:16 little endian - */ WL_SHM_FORMAT_RGB161616 = 0x38344752, - /** - * [47:0] B:G:R 16:16:16 little endian - */ WL_SHM_FORMAT_BGR161616 = 0x38344742, - /** - * [15:0] R 16 little endian - */ WL_SHM_FORMAT_R16F = 0x48202052, - /** - * [31:0] G:R 16:16 little endian - */ WL_SHM_FORMAT_GR1616F = 0x48205247, - /** - * [47:0] B:G:R 16:16:16 little endian - */ WL_SHM_FORMAT_BGR161616F = 0x48524742, - /** - * [31:0] R 32 little endian - */ WL_SHM_FORMAT_R32F = 0x46202052, - /** - * [63:0] G:R 32:32 little endian - */ WL_SHM_FORMAT_GR3232F = 0x46205247, - /** - * [95:0] B:G:R 32:32:32 little endian - */ WL_SHM_FORMAT_BGR323232F = 0x46524742, - /** - * [127:0] A:B:G:R 32:32:32:32 little endian - */ WL_SHM_FORMAT_ABGR32323232F = 0x46384241, - /** - * 2x1 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV20 = 0x3032564e, - /** - * non-subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV30 = 0x3033564e, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S010 = 0x30313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S210 = 0x30313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S410 = 0x30313453, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S012 = 0x32313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S212 = 0x32313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S412 = 0x32313453, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S016 = 0x36313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S216 = 0x36313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S416 = 0x36313453, - /** - * [31:0] x:Cr:Cb:Y 2:10:10:10 little endian - */ WL_SHM_FORMAT_XVUY2101010 = 0x30335958, - /** - * 2x1 subsampled Cr:Cb plane 10 bits per channel packed - */ WL_SHM_FORMAT_P230 = 0x30333250, WL_SHM_FORMAT_T430 = 0x30333454, - /** - * 8-bit Y-only - */ WL_SHM_FORMAT_Y8 = 0x59455247, - /** - * [31:0] x:Y2:Y1:Y0 2:10:10:10 little endian - */ WL_SHM_FORMAT_XYYY2101010 = 0x34415059, }; -#endif /* WL_SHM_FORMAT_ENUM */ +#endif #ifndef WL_SHM_FORMAT_ENUM_IS_VALID #define WL_SHM_FORMAT_ENUM_IS_VALID -/** - * @ingroup iface_wl_shm - * Validate a wl_shm format value. - * - * @return true on success, false on error. - * @ref wl_shm_format - */ static inline bool wl_shm_format_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2265,105 +815,42 @@ wl_shm_format_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SHM_FORMAT_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_shm - * @struct wl_shm_interface - */ struct wl_shm_interface { - /** - * create a shm pool - * - * Create a new wl_shm_pool object. - * - * The pool can be used to create shared memory based buffer - * objects. The server will mmap size bytes of the passed file - * descriptor, to use as backing memory for the pool. - * @param id pool to create - * @param fd file descriptor for the pool - * @param size pool size, in bytes - */ void (*create_pool)(struct wl_client *client, struct wl_resource *resource, uint32_t id, int32_t fd, int32_t size); - /** - * release the shm object - * - * Using this request a client can tell the server that it is not - * going to use the shm object anymore. - * - * Objects created via this interface remain unaffected. - * @since 2 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; #define WL_SHM_FORMAT 0 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_FORMAT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_CREATE_POOL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_RELEASE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_shm - * Sends an format event to the client owning the resource. - * @param resource_ The client's resource - * @param format buffer pixel format - */ static inline void wl_shm_send_format(struct wl_resource *resource_, uint32_t format) { wl_resource_post_event(resource_, WL_SHM_FORMAT, format); } -/** - * @ingroup iface_wl_buffer - * @struct wl_buffer_interface - */ struct wl_buffer_interface { - /** - * destroy a buffer - * - * Destroy a buffer. If and how you need to release the backing - * storage is defined by the buffer factory interface. - * - * For possible side-effects to a surface, see wl_surface.attach. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; #define WL_BUFFER_RELEASE 0 -/** - * @ingroup iface_wl_buffer - */ #define WL_BUFFER_RELEASE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_buffer - */ #define WL_BUFFER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_buffer - * Sends an release event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_buffer_send_release(struct wl_resource *resource_) { @@ -2373,34 +860,15 @@ wl_buffer_send_release(struct wl_resource *resource_) #ifndef WL_DATA_OFFER_ERROR_ENUM #define WL_DATA_OFFER_ERROR_ENUM enum wl_data_offer_error { - /** - * finish request was called untimely - */ WL_DATA_OFFER_ERROR_INVALID_FINISH = 0, - /** - * action mask contains invalid values - */ WL_DATA_OFFER_ERROR_INVALID_ACTION_MASK = 1, - /** - * action argument has an invalid value - */ WL_DATA_OFFER_ERROR_INVALID_ACTION = 2, - /** - * offer doesn't accept this request - */ WL_DATA_OFFER_ERROR_INVALID_OFFER = 3, }; -#endif /* WL_DATA_OFFER_ERROR_ENUM */ +#endif #ifndef WL_DATA_OFFER_ERROR_ENUM_IS_VALID #define WL_DATA_OFFER_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_data_offer - * Validate a wl_data_offer error value. - * - * @return true on success, false on error. - * @ref wl_data_offer_error - */ static inline bool wl_data_offer_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2416,129 +884,21 @@ wl_data_offer_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_DATA_OFFER_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_data_offer - * @struct wl_data_offer_interface - */ struct wl_data_offer_interface { - /** - * accept one of the offered mime types - * - * Indicate that the client can accept the given mime type, or - * NULL for not accepted. - * - * For objects of version 2 or older, this request is used by the - * client to give feedback whether the client can receive the given - * mime type, or NULL if none is accepted; the feedback does not - * determine whether the drag-and-drop operation succeeds or not. - * - * For objects of version 3 or newer, this request determines the - * final result of the drag-and-drop operation. If the end result - * is that no mime types were accepted, the drag-and-drop operation - * will be cancelled and the corresponding drag source will receive - * wl_data_source.cancelled. Clients may still use this event in - * conjunction with wl_data_source.action for feedback. - * @param serial serial number of the accept request - * @param mime_type mime type accepted by the client - */ void (*accept)(struct wl_client *client, struct wl_resource *resource, uint32_t serial, const char *mime_type); - /** - * request that the data is transferred - * - * To transfer the offered data, the client issues this request - * and indicates the mime type it wants to receive. The transfer - * happens through the passed file descriptor (typically created - * with the pipe system call). The source client writes the data in - * the mime type representation requested and then closes the file - * descriptor. - * - * The receiving client reads from the read end of the pipe until - * EOF and then closes its end, at which point the transfer is - * complete. - * - * This request may happen multiple times for different mime types, - * both before and after wl_data_device.drop. Drag-and-drop - * destination clients may preemptively fetch data or examine it - * more closely to determine acceptance. - * @param mime_type mime type desired by receiver - * @param fd file descriptor for data transfer - */ void (*receive)(struct wl_client *client, struct wl_resource *resource, const char *mime_type, int32_t fd); - /** - * destroy data offer - * - * Destroy the data offer. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * the offer will no longer be used - * - * Notifies the compositor that the drag destination successfully - * finished the drag-and-drop operation. - * - * Upon receiving this request, the compositor will emit - * wl_data_source.dnd_finished on the drag source client. - * - * It is a client error to perform other requests than - * wl_data_offer.destroy after this one. It is also an error to - * perform this request after a NULL mime type has been set in - * wl_data_offer.accept or no action was received through - * wl_data_offer.action. - * - * If wl_data_offer.finish request is received for a non drag and - * drop operation, the invalid_finish protocol error is raised. - * @since 3 - */ void (*finish)(struct wl_client *client, struct wl_resource *resource); - /** - * set the available/preferred drag-and-drop actions - * - * Sets the actions that the destination side client supports for - * this operation. This request may trigger the emission of - * wl_data_source.action and wl_data_offer.action events if the - * compositor needs to change the selected action. - * - * This request can be called multiple times throughout the - * drag-and-drop operation, typically in response to - * wl_data_device.enter or wl_data_device.motion events. - * - * This request determines the final result of the drag-and-drop - * operation. If the end result is that no action is accepted, the - * drag source will receive wl_data_source.cancelled. - * - * The dnd_actions argument must contain only values expressed in - * the wl_data_device_manager.dnd_actions enum, and the - * preferred_action argument must only contain one of those values - * set, otherwise it will result in a protocol error. - * - * While managing an "ask" action, the destination drag-and-drop - * client may perform further wl_data_offer.receive requests, and - * is expected to perform one last wl_data_offer.set_actions - * request with a preferred action other than "ask" (and optionally - * wl_data_offer.accept) before requesting wl_data_offer.finish, in - * order to convey the action selected by the user. If the - * preferred action is not in the wl_data_offer.source_actions - * mask, an error will be raised. - * - * If the "ask" action is dismissed (e.g. user cancellation), the - * client is expected to perform wl_data_offer.destroy right away. - * - * This request can only be made on drag-and-drop offers, a - * protocol error will be raised otherwise. - * @param dnd_actions actions supported by the destination client - * @param preferred_action action preferred by the destination client - * @since 3 - */ void (*set_actions)(struct wl_client *client, struct wl_resource *resource, uint32_t dnd_actions, @@ -2549,70 +909,28 @@ struct wl_data_offer_interface { #define WL_DATA_OFFER_SOURCE_ACTIONS 1 #define WL_DATA_OFFER_ACTION 2 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_SOURCE_ACTIONS_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_ACTION_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_ACCEPT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_RECEIVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_FINISH_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_SET_ACTIONS_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - * Sends an offer event to the client owning the resource. - * @param resource_ The client's resource - * @param mime_type offered mime type - */ static inline void wl_data_offer_send_offer(struct wl_resource *resource_, const char *mime_type) { wl_resource_post_event(resource_, WL_DATA_OFFER_OFFER, mime_type); } -/** - * @ingroup iface_wl_data_offer - * Sends an source_actions event to the client owning the resource. - * @param resource_ The client's resource - * @param source_actions actions offered by the data source - */ static inline void wl_data_offer_send_source_actions(struct wl_resource *resource_, uint32_t source_actions) { wl_resource_post_event(resource_, WL_DATA_OFFER_SOURCE_ACTIONS, source_actions); } -/** - * @ingroup iface_wl_data_offer - * Sends an action event to the client owning the resource. - * @param resource_ The client's resource - * @param dnd_action action selected by the compositor - */ static inline void wl_data_offer_send_action(struct wl_resource *resource_, uint32_t dnd_action) { @@ -2622,26 +940,13 @@ wl_data_offer_send_action(struct wl_resource *resource_, uint32_t dnd_action) #ifndef WL_DATA_SOURCE_ERROR_ENUM #define WL_DATA_SOURCE_ERROR_ENUM enum wl_data_source_error { - /** - * action mask contains invalid values - */ WL_DATA_SOURCE_ERROR_INVALID_ACTION_MASK = 0, - /** - * source doesn't accept this request - */ WL_DATA_SOURCE_ERROR_INVALID_SOURCE = 1, }; -#endif /* WL_DATA_SOURCE_ERROR_ENUM */ +#endif #ifndef WL_DATA_SOURCE_ERROR_ENUM_IS_VALID #define WL_DATA_SOURCE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_data_source - * Validate a wl_data_source error value. - * - * @return true on success, false on error. - * @ref wl_data_source_error - */ static inline bool wl_data_source_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2653,50 +958,14 @@ wl_data_source_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_DATA_SOURCE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_data_source - * @struct wl_data_source_interface - */ struct wl_data_source_interface { - /** - * add an offered mime type - * - * This request adds a mime type to the set of mime types - * advertised to targets. Can be called several times to offer - * multiple types. - * @param mime_type mime type offered by the data source - */ void (*offer)(struct wl_client *client, struct wl_resource *resource, const char *mime_type); - /** - * destroy the data source - * - * Destroy the data source. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the available drag-and-drop actions - * - * Sets the actions that the source side client supports for this - * operation. This request may trigger wl_data_source.action and - * wl_data_offer.action events if the compositor needs to change - * the selected action. - * - * The dnd_actions argument must contain only values expressed in - * the wl_data_device_manager.dnd_actions enum, otherwise it will - * result in a protocol error. - * - * This request must be made once only, and can only be made on - * sources used in drag-and-drop, so it must be performed before - * wl_data_device.start_drag. Attempting to use the source other - * than for drag-and-drop will raise a protocol error. - * @param dnd_actions actions supported by the data source - * @since 3 - */ void (*set_actions)(struct wl_client *client, struct wl_resource *resource, uint32_t dnd_actions); @@ -2709,108 +978,47 @@ struct wl_data_source_interface { #define WL_DATA_SOURCE_DND_FINISHED 4 #define WL_DATA_SOURCE_ACTION 5 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_TARGET_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_SEND_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_CANCELLED_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DND_DROP_PERFORMED_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DND_FINISHED_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_ACTION_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_SET_ACTIONS_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - * Sends an target event to the client owning the resource. - * @param resource_ The client's resource - * @param mime_type mime type accepted by the target - */ static inline void wl_data_source_send_target(struct wl_resource *resource_, const char *mime_type) { wl_resource_post_event(resource_, WL_DATA_SOURCE_TARGET, mime_type); } -/** - * @ingroup iface_wl_data_source - * Sends an send event to the client owning the resource. - * @param resource_ The client's resource - * @param mime_type mime type for the data - * @param fd file descriptor for the data - */ static inline void wl_data_source_send_send(struct wl_resource *resource_, const char *mime_type, int32_t fd) { wl_resource_post_event(resource_, WL_DATA_SOURCE_SEND, mime_type, fd); } -/** - * @ingroup iface_wl_data_source - * Sends an cancelled event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_data_source_send_cancelled(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_DATA_SOURCE_CANCELLED); } -/** - * @ingroup iface_wl_data_source - * Sends an dnd_drop_performed event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_data_source_send_dnd_drop_performed(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_DATA_SOURCE_DND_DROP_PERFORMED); } -/** - * @ingroup iface_wl_data_source - * Sends an dnd_finished event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_data_source_send_dnd_finished(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_DATA_SOURCE_DND_FINISHED); } -/** - * @ingroup iface_wl_data_source - * Sends an action event to the client owning the resource. - * @param resource_ The client's resource - * @param dnd_action action selected by the compositor - */ static inline void wl_data_source_send_action(struct wl_resource *resource_, uint32_t dnd_action) { @@ -2820,26 +1028,13 @@ wl_data_source_send_action(struct wl_resource *resource_, uint32_t dnd_action) #ifndef WL_DATA_DEVICE_ERROR_ENUM #define WL_DATA_DEVICE_ERROR_ENUM enum wl_data_device_error { - /** - * given wl_surface has another role - */ WL_DATA_DEVICE_ERROR_ROLE = 0, - /** - * source has already been used - */ WL_DATA_DEVICE_ERROR_USED_SOURCE = 1, }; -#endif /* WL_DATA_DEVICE_ERROR_ENUM */ +#endif #ifndef WL_DATA_DEVICE_ERROR_ENUM_IS_VALID #define WL_DATA_DEVICE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_data_device - * Validate a wl_data_device error value. - * - * @return true on success, false on error. - * @ref wl_data_device_error - */ static inline bool wl_data_device_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2851,80 +1046,19 @@ wl_data_device_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_DATA_DEVICE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_data_device - * @struct wl_data_device_interface - */ struct wl_data_device_interface { - /** - * start drag-and-drop operation - * - * This request asks the compositor to start a drag-and-drop - * operation on behalf of the client. - * - * The source argument is the data source that provides the data - * for the eventual data transfer. If source is NULL, enter, leave - * and motion events are sent only to the client that initiated the - * drag and the client is expected to handle the data passing - * internally. If source is destroyed, the drag-and-drop session - * will be cancelled. - * - * The origin surface is the surface where the drag originates and - * the client must have an active implicit grab that matches the - * serial. - * - * The icon surface is an optional (can be NULL) surface that - * provides an icon to be moved around with the cursor. Initially, - * the top-left corner of the icon surface is placed at the cursor - * hotspot, but subsequent wl_surface.offset requests can move the - * relative position. Attach requests must be confirmed with - * wl_surface.commit as usual. The icon surface is given the role - * of a drag-and-drop icon. If the icon surface already has another - * role, it raises a protocol error. - * - * The input region is ignored for wl_surfaces with the role of a - * drag-and-drop icon. - * - * The given source may not be used in any further set_selection or - * start_drag requests. Attempting to reuse a previously-used - * source may send a used_source error. - * @param source data source for the eventual transfer - * @param origin surface where the drag originates - * @param icon drag-and-drop icon surface - * @param serial serial number of the implicit grab on the origin - */ void (*start_drag)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *source, struct wl_resource *origin, struct wl_resource *icon, uint32_t serial); - /** - * copy data to the selection - * - * This request asks the compositor to set the selection to the - * data from the source on behalf of the client. - * - * To unset the selection, set the source to NULL. - * - * The given source may not be used in any further set_selection or - * start_drag requests. Attempting to reuse a previously-used - * source may send a used_source error. - * @param source data source for the selection - * @param serial serial number of the event that triggered this request - */ void (*set_selection)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *source, uint32_t serial); - /** - * destroy data device - * - * This request destroys the data device. - * @since 2 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -2936,114 +1070,47 @@ struct wl_data_device_interface { #define WL_DATA_DEVICE_DROP 4 #define WL_DATA_DEVICE_SELECTION 5 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_DATA_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_DROP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_START_DRAG_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_SET_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_RELEASE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_data_device - * Sends an data_offer event to the client owning the resource. - * @param resource_ The client's resource - * @param id the new data_offer object - */ static inline void wl_data_device_send_data_offer(struct wl_resource *resource_, struct wl_resource *id) { wl_resource_post_event(resource_, WL_DATA_DEVICE_DATA_OFFER, id); } -/** - * @ingroup iface_wl_data_device - * Sends an enter event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the enter event - * @param surface client surface entered - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @param id source data_offer object - */ static inline void wl_data_device_send_enter(struct wl_resource *resource_, uint32_t serial, struct wl_resource *surface, wl_fixed_t x, wl_fixed_t y, struct wl_resource *id) { wl_resource_post_event(resource_, WL_DATA_DEVICE_ENTER, serial, surface, x, y, id); } -/** - * @ingroup iface_wl_data_device - * Sends an leave event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_data_device_send_leave(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_DATA_DEVICE_LEAVE); } -/** - * @ingroup iface_wl_data_device - * Sends an motion event to the client owning the resource. - * @param resource_ The client's resource - * @param time timestamp with millisecond granularity - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ static inline void wl_data_device_send_motion(struct wl_resource *resource_, uint32_t time, wl_fixed_t x, wl_fixed_t y) { wl_resource_post_event(resource_, WL_DATA_DEVICE_MOTION, time, x, y); } -/** - * @ingroup iface_wl_data_device - * Sends an drop event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_data_device_send_drop(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_DATA_DEVICE_DROP); } -/** - * @ingroup iface_wl_data_device - * Sends an selection event to the client owning the resource. - * @param resource_ The client's resource - * @param id selection data_offer object - */ static inline void wl_data_device_send_selection(struct wl_resource *resource_, struct wl_resource *id) { @@ -3052,63 +1119,16 @@ wl_data_device_send_selection(struct wl_resource *resource_, struct wl_resource #ifndef WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM #define WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM -/** - * @ingroup iface_wl_data_device_manager - * drag and drop actions - * - * This is a bitmask of the available/preferred actions in a - * drag-and-drop operation. - * - * In the compositor, the selected action is a result of matching the - * actions offered by the source and destination sides. "action" events - * with a "none" action will be sent to both source and destination if - * there is no match. All further checks will effectively happen on - * (source actions ∩ destination actions). - * - * In addition, compositors may also pick different actions in - * reaction to key modifiers being pressed. One common design that - * is used in major toolkits (and the behavior recommended for - * compositors) is: - * - * - If no modifiers are pressed, the first match (in bit order) - * will be used. - * - Pressing Shift selects "move", if enabled in the mask. - * - Pressing Control selects "copy", if enabled in the mask. - * - * Behavior beyond that is considered implementation-dependent. - * Compositors may for example bind other modifiers (like Alt/Meta) - * or drags initiated with other buttons than BTN_LEFT to specific - * actions (e.g. "ask"). - */ enum wl_data_device_manager_dnd_action { - /** - * no action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE = 0, - /** - * copy action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY = 1, - /** - * move action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE = 2, - /** - * ask action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK = 4, }; -#endif /* WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM */ +#endif #ifndef WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM_IS_VALID #define WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM_IS_VALID -/** - * @ingroup iface_wl_data_device_manager - * Validate a wl_data_device_manager dnd_action value. - * - * @return true on success, false on error. - * @ref wl_data_device_manager_dnd_action - */ static inline bool wl_data_device_manager_dnd_action_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -3122,77 +1142,34 @@ wl_data_device_manager_dnd_action_is_valid(uint32_t value, uint32_t version) { valid |= WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK; return (value & ~valid) == 0; } -#endif /* WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_data_device_manager - * @struct wl_data_device_manager_interface - */ struct wl_data_device_manager_interface { - /** - * create a new data source - * - * Create a new data source. - * @param id data source to create - */ void (*create_data_source)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * create a new data device - * - * Create a new data device for a given seat. - * @param id data device to create - * @param seat seat associated with the data device - */ void (*get_data_device)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *seat); - /** - * destroy wl_data_device_manager - * - * This request destroys the wl_data_device_manager. This has no - * effect on any other objects. - * @since 4 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_RELEASE_SINCE_VERSION 4 #ifndef WL_SHELL_ERROR_ENUM #define WL_SHELL_ERROR_ENUM enum wl_shell_error { - /** - * given wl_surface has another role - */ WL_SHELL_ERROR_ROLE = 0, }; -#endif /* WL_SHELL_ERROR_ENUM */ +#endif #ifndef WL_SHELL_ERROR_ENUM_IS_VALID #define WL_SHELL_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_shell - * Validate a wl_shell error value. - * - * @return true on success, false on error. - * @ref wl_shell_error - */ static inline bool wl_shell_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -3202,24 +1179,9 @@ wl_shell_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SHELL_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_shell - * @struct wl_shell_interface - */ struct wl_shell_interface { - /** - * create a shell surface from a surface - * - * Create a shell surface for an existing surface. This gives the - * wl_surface the role of a shell surface. If the wl_surface - * already has another role, it raises a protocol error. - * - * Only one shell surface can be associated with a given surface. - * @param id shell surface to create - * @param surface surface to be given the shell surface role - */ void (*get_shell_surface)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -3227,71 +1189,25 @@ struct wl_shell_interface { }; -/** - * @ingroup iface_wl_shell - */ #define WL_SHELL_GET_SHELL_SURFACE_SINCE_VERSION 1 #ifndef WL_SHELL_SURFACE_RESIZE_ENUM #define WL_SHELL_SURFACE_RESIZE_ENUM -/** - * @ingroup iface_wl_shell_surface - * edge values for resizing - * - * These values are used to indicate which edge of a surface - * is being dragged in a resize operation. The server may - * use this information to adapt its behavior, e.g. choose - * an appropriate cursor image. - */ enum wl_shell_surface_resize { - /** - * no edge - */ WL_SHELL_SURFACE_RESIZE_NONE = 0, - /** - * top edge - */ WL_SHELL_SURFACE_RESIZE_TOP = 1, - /** - * bottom edge - */ WL_SHELL_SURFACE_RESIZE_BOTTOM = 2, - /** - * left edge - */ WL_SHELL_SURFACE_RESIZE_LEFT = 4, - /** - * top and left edges - */ WL_SHELL_SURFACE_RESIZE_TOP_LEFT = 5, - /** - * bottom and left edges - */ WL_SHELL_SURFACE_RESIZE_BOTTOM_LEFT = 6, - /** - * right edge - */ WL_SHELL_SURFACE_RESIZE_RIGHT = 8, - /** - * top and right edges - */ WL_SHELL_SURFACE_RESIZE_TOP_RIGHT = 9, - /** - * bottom and right edges - */ WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT = 10, }; -#endif /* WL_SHELL_SURFACE_RESIZE_ENUM */ +#endif #ifndef WL_SHELL_SURFACE_RESIZE_ENUM_IS_VALID #define WL_SHELL_SURFACE_RESIZE_ENUM_IS_VALID -/** - * @ingroup iface_wl_shell_surface - * Validate a wl_shell_surface resize value. - * - * @return true on success, false on error. - * @ref wl_shell_surface_resize - */ static inline bool wl_shell_surface_resize_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -3315,34 +1231,17 @@ wl_shell_surface_resize_is_valid(uint32_t value, uint32_t version) { valid |= WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT; return (value & ~valid) == 0; } -#endif /* WL_SHELL_SURFACE_RESIZE_ENUM_IS_VALID */ +#endif #ifndef WL_SHELL_SURFACE_TRANSIENT_ENUM #define WL_SHELL_SURFACE_TRANSIENT_ENUM -/** - * @ingroup iface_wl_shell_surface - * details of transient behaviour - * - * These flags specify details of the expected behaviour - * of transient surfaces. Used in the set_transient request. - */ enum wl_shell_surface_transient { - /** - * do not set keyboard focus - */ WL_SHELL_SURFACE_TRANSIENT_INACTIVE = 0x1, }; -#endif /* WL_SHELL_SURFACE_TRANSIENT_ENUM */ +#endif #ifndef WL_SHELL_SURFACE_TRANSIENT_ENUM_IS_VALID #define WL_SHELL_SURFACE_TRANSIENT_ENUM_IS_VALID -/** - * @ingroup iface_wl_shell_surface - * Validate a wl_shell_surface transient value. - * - * @return true on success, false on error. - * @ref wl_shell_surface_transient - */ static inline bool wl_shell_surface_transient_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -3350,47 +1249,20 @@ wl_shell_surface_transient_is_valid(uint32_t value, uint32_t version) { valid |= WL_SHELL_SURFACE_TRANSIENT_INACTIVE; return (value & ~valid) == 0; } -#endif /* WL_SHELL_SURFACE_TRANSIENT_ENUM_IS_VALID */ +#endif #ifndef WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM #define WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM -/** - * @ingroup iface_wl_shell_surface - * different method to set the surface fullscreen - * - * Hints to indicate to the compositor how to deal with a conflict - * between the dimensions of the surface and the dimensions of the - * output. The compositor is free to ignore this parameter. - */ enum wl_shell_surface_fullscreen_method { - /** - * no preference, apply default policy - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_DEFAULT = 0, - /** - * scale, preserve the surface's aspect ratio and center on output - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_SCALE = 1, - /** - * switch output mode to the smallest mode that can fit the surface, add black borders to compensate size mismatch - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_DRIVER = 2, - /** - * no upscaling, center on output and add black borders to compensate size mismatch - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_FILL = 3, }; -#endif /* WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM */ +#endif #ifndef WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM_IS_VALID #define WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM_IS_VALID -/** - * @ingroup iface_wl_shell_surface - * Validate a wl_shell_surface fullscreen_method value. - * - * @return true on success, false on error. - * @ref wl_shell_surface_fullscreen_method - */ static inline bool wl_shell_surface_fullscreen_method_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -3406,160 +1278,34 @@ wl_shell_surface_fullscreen_method_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_shell_surface - * @struct wl_shell_surface_interface - */ struct wl_shell_surface_interface { - /** - * respond to a ping event - * - * A client must respond to a ping event with a pong request or - * the client may be deemed unresponsive. - * @param serial serial number of the ping event - */ void (*pong)(struct wl_client *client, struct wl_resource *resource, uint32_t serial); - /** - * start an interactive move - * - * Start a pointer-driven move of the surface. - * - * This request must be used in response to a button press event. - * The server may ignore move requests depending on the state of - * the surface (e.g. fullscreen or maximized). - * @param seat seat whose pointer is used - * @param serial serial number of the implicit grab on the pointer - */ void (*move)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial); - /** - * start an interactive resize - * - * Start a pointer-driven resizing of the surface. - * - * This request must be used in response to a button press event. - * The server may ignore resize requests depending on the state of - * the surface (e.g. fullscreen or maximized). - * @param seat seat whose pointer is used - * @param serial serial number of the implicit grab on the pointer - * @param edges which edge or corner is being dragged - */ void (*resize)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial, uint32_t edges); - /** - * make the surface a toplevel surface - * - * Map the surface as a toplevel surface. - * - * A toplevel surface is not fullscreen, maximized or transient. - */ void (*set_toplevel)(struct wl_client *client, struct wl_resource *resource); - /** - * make the surface a transient surface - * - * Map the surface relative to an existing surface. - * - * The x and y arguments specify the location of the upper left - * corner of the surface relative to the upper left corner of the - * parent surface, in surface-local coordinates. - * - * The flags argument controls details of the transient behaviour. - * @param parent parent surface - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @param flags transient surface behavior - */ void (*set_transient)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *parent, int32_t x, int32_t y, uint32_t flags); - /** - * make the surface a fullscreen surface - * - * Map the surface as a fullscreen surface. - * - * If an output parameter is given then the surface will be made - * fullscreen on that output. If the client does not specify the - * output then the compositor will apply its policy - usually - * choosing the output on which the surface has the biggest surface - * area. - * - * The client may specify a method to resolve a size conflict - * between the output size and the surface size - this is provided - * through the method parameter. - * - * The framerate parameter is used only when the method is set to - * "driver", to indicate the preferred framerate. A value of 0 - * indicates that the client does not care about framerate. The - * framerate is specified in mHz, that is framerate of 60000 is - * 60Hz. - * - * A method of "scale" or "driver" implies a scaling operation of - * the surface, either via a direct scaling operation or a change - * of the output mode. This will override any kind of output - * scaling, so that mapping a surface with a buffer size equal to - * the mode can fill the screen independent of buffer_scale. - * - * A method of "fill" means we don't scale up the buffer, however - * any output scale is applied. This means that you may run into an - * edge case where the application maps a buffer with the same size - * of the output mode but buffer_scale 1 (thus making a surface - * larger than the output). In this case it is allowed to downscale - * the results to fit the screen. - * - * The compositor must reply to this request with a configure event - * with the dimensions for the output on which the surface will be - * made fullscreen. - * @param method method for resolving size conflict - * @param framerate framerate in mHz - * @param output output on which the surface is to be fullscreen - */ void (*set_fullscreen)(struct wl_client *client, struct wl_resource *resource, uint32_t method, uint32_t framerate, struct wl_resource *output); - /** - * make the surface a popup surface - * - * Map the surface as a popup. - * - * A popup surface is a transient surface with an added pointer - * grab. - * - * An existing implicit grab will be changed to owner-events mode, - * and the popup grab will continue after the implicit grab ends - * (i.e. releasing the mouse button does not cause the popup to be - * unmapped). - * - * The popup grab continues until the window is destroyed or a - * mouse button is pressed in any other client's window. A click in - * any of the client's surfaces is reported as normal, however, - * clicks in other clients' surfaces will be discarded and trigger - * the callback. - * - * The x and y arguments specify the location of the upper left - * corner of the surface relative to the upper left corner of the - * parent surface, in surface-local coordinates. - * @param seat seat whose pointer is used - * @param serial serial number of the implicit grab on the pointer - * @param parent parent surface - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @param flags transient surface behavior - */ void (*set_popup)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, @@ -3568,58 +1314,12 @@ struct wl_shell_surface_interface { int32_t x, int32_t y, uint32_t flags); - /** - * make the surface a maximized surface - * - * Map the surface as a maximized surface. - * - * If an output parameter is given then the surface will be - * maximized on that output. If the client does not specify the - * output then the compositor will apply its policy - usually - * choosing the output on which the surface has the biggest surface - * area. - * - * The compositor will reply with a configure event telling the - * expected new surface size. The operation is completed on the - * next buffer attach to this surface. - * - * A maximized surface typically fills the entire output it is - * bound to, except for desktop elements such as panels. This is - * the main difference between a maximized shell surface and a - * fullscreen shell surface. - * - * The details depend on the compositor implementation. - * @param output output on which the surface is to be maximized - */ void (*set_maximized)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *output); - /** - * set surface title - * - * Set a short title for the surface. - * - * This string may be used to identify the surface in a task bar, - * window list, or other user interface elements provided by the - * compositor. - * - * The string must be encoded in UTF-8. - * @param title surface title - */ void (*set_title)(struct wl_client *client, struct wl_resource *resource, const char *title); - /** - * set surface class - * - * Set a class for the surface. - * - * The surface class identifies the general class of applications - * to which the surface belongs. A common convention is to use the - * file name (or the full path if it is a non-standard location) of - * the application's .desktop file as the class. - * @param class_ surface class - */ void (*set_class)(struct wl_client *client, struct wl_resource *resource, const char *class_); @@ -3629,91 +1329,33 @@ struct wl_shell_surface_interface { #define WL_SHELL_SURFACE_CONFIGURE 1 #define WL_SHELL_SURFACE_POPUP_DONE 2 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_PING_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_POPUP_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_PONG_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_MOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_RESIZE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TOPLEVEL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TRANSIENT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_POPUP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TITLE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_CLASS_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - * Sends an ping event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the ping - */ static inline void wl_shell_surface_send_ping(struct wl_resource *resource_, uint32_t serial) { wl_resource_post_event(resource_, WL_SHELL_SURFACE_PING, serial); } -/** - * @ingroup iface_wl_shell_surface - * Sends an configure event to the client owning the resource. - * @param resource_ The client's resource - * @param edges how the surface was resized - * @param width new width of the surface - * @param height new height of the surface - */ static inline void wl_shell_surface_send_configure(struct wl_resource *resource_, uint32_t edges, int32_t width, int32_t height) { wl_resource_post_event(resource_, WL_SHELL_SURFACE_CONFIGURE, edges, width, height); } -/** - * @ingroup iface_wl_shell_surface - * Sends an popup_done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_shell_surface_send_popup_done(struct wl_resource *resource_) { @@ -3722,49 +1364,18 @@ wl_shell_surface_send_popup_done(struct wl_resource *resource_) #ifndef WL_SURFACE_ERROR_ENUM #define WL_SURFACE_ERROR_ENUM -/** - * @ingroup iface_wl_surface - * wl_surface error values - * - * These errors can be emitted in response to wl_surface requests. - */ enum wl_surface_error { - /** - * buffer scale value is invalid - */ WL_SURFACE_ERROR_INVALID_SCALE = 0, - /** - * buffer transform value is invalid - */ WL_SURFACE_ERROR_INVALID_TRANSFORM = 1, - /** - * buffer size is invalid - */ WL_SURFACE_ERROR_INVALID_SIZE = 2, - /** - * buffer offset is invalid - */ WL_SURFACE_ERROR_INVALID_OFFSET = 3, - /** - * surface was destroyed before its role object - */ WL_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 4, - /** - * no buffer was attached - */ WL_SURFACE_ERROR_NO_BUFFER = 5, }; -#endif /* WL_SURFACE_ERROR_ENUM */ +#endif #ifndef WL_SURFACE_ERROR_ENUM_IS_VALID #define WL_SURFACE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_surface - * Validate a wl_surface error value. - * - * @return true on success, false on error. - * @ref wl_surface_error - */ static inline bool wl_surface_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -3784,479 +1395,49 @@ wl_surface_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SURFACE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_surface - * @struct wl_surface_interface - */ struct wl_surface_interface { - /** - * delete surface - * - * Deletes the surface and invalidates its object ID. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the surface contents - * - * Set a buffer as the content of this surface. - * - * The new size of the surface is calculated based on the buffer - * size transformed by the inverse buffer_transform and the inverse - * buffer_scale. This means that at commit time the supplied buffer - * size must be an integer multiple of the buffer_scale. If that's - * not the case, an invalid_size error is sent. - * - * The x and y arguments specify the location of the new pending - * buffer's upper left corner, relative to the current buffer's - * upper left corner, in surface-local coordinates. In other words, - * the x and y, combined with the new surface size define in which - * directions the surface's size changes. Setting anything other - * than 0 as x and y arguments is discouraged, and should instead - * be replaced with using the separate wl_surface.offset request. - * - * When the bound wl_surface version is 5 or higher, passing any - * non-zero x or y is a protocol violation, and will result in an - * 'invalid_offset' error being raised. The x and y arguments are - * ignored and do not change the pending state. To achieve - * equivalent semantics, use wl_surface.offset. - * - * Surface contents are double-buffered state, see - * wl_surface.commit. - * - * The initial surface contents are void; there is no content. - * wl_surface.attach assigns the given wl_buffer as the pending - * wl_buffer. wl_surface.commit makes the pending wl_buffer the new - * surface contents, and the size of the surface becomes the size - * calculated from the wl_buffer, as described above. After commit, - * there is no pending buffer until the next attach. - * - * Committing a pending wl_buffer allows the compositor to read the - * pixels in the wl_buffer. The compositor may access the pixels at - * any time after the wl_surface.commit request. When the - * compositor will not access the pixels anymore, it will send the - * wl_buffer.release event. Only after receiving wl_buffer.release, - * the client may reuse the wl_buffer. A wl_buffer that has been - * attached and then replaced by another attach instead of - * committed will not receive a release event, and is not used by - * the compositor. - * - * If a pending wl_buffer has been committed to more than one - * wl_surface, the delivery of wl_buffer.release events becomes - * undefined. A well behaved client should not rely on - * wl_buffer.release events in this case. Instead, clients hitting - * this case should use wl_surface.get_release or use a protocol - * extension providing per-commit release notifications (if none of - * these options are available, a fallback can be implemented by - * creating multiple wl_buffer objects from the same backing - * storage). - * - * Destroying the wl_buffer after wl_buffer.release does not change - * the surface contents. Destroying the wl_buffer before - * wl_buffer.release is allowed as long as the underlying buffer - * storage isn't re-used (this can happen e.g. on client process - * termination). However, if the client destroys the wl_buffer - * before receiving the wl_buffer.release event and mutates the - * underlying buffer storage, the surface contents become undefined - * immediately. - * - * If wl_surface.attach is sent with a NULL wl_buffer, the - * following wl_surface.commit will remove the surface content. - * - * If a pending wl_buffer has been destroyed, the result is not - * specified. Many compositors are known to remove the surface - * content on the following wl_surface.commit, but this behaviour - * is not universal. Clients seeking to maximise compatibility - * should not destroy pending buffers and should ensure that they - * explicitly remove content from surfaces, even after destroying - * buffers. - * @param buffer buffer of surface contents - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ void (*attach)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *buffer, int32_t x, int32_t y); - /** - * mark part of the surface damaged - * - * This request is used to describe the regions where the pending - * buffer is different from the current surface contents, and where - * the surface therefore needs to be repainted. The compositor - * ignores the parts of the damage that fall outside of the - * surface. - * - * Damage is double-buffered state, see wl_surface.commit. - * - * The damage rectangle is specified in surface-local coordinates, - * where x and y specify the upper left corner of the damage - * rectangle. - * - * The initial value for pending damage is empty: no damage. - * wl_surface.damage adds pending damage: the new pending damage is - * the union of old pending damage and the given rectangle. - * - * wl_surface.commit assigns pending damage as the current damage, - * and clears pending damage. The server will clear the current - * damage as it repaints the surface. - * - * Note! New clients should not use this request. Instead damage - * can be posted with wl_surface.damage_buffer which uses buffer - * coordinates instead of surface coordinates. - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @param width width of damage rectangle - * @param height height of damage rectangle - */ void (*damage)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * request a frame throttling hint - * - * Request a notification when it is a good time to start drawing - * a new frame, by creating a frame callback. This is useful for - * throttling redrawing operations, and driving animations. - * - * When a client is animating on a wl_surface, it can use the - * 'frame' request to get notified when it is a good time to draw - * and commit the next frame of animation. If the client commits an - * update earlier than that, it is likely that some updates will - * not make it to the display, and the client is wasting resources - * by drawing too often. - * - * The frame request will take effect on the next - * wl_surface.commit. The notification will only be posted for one - * frame unless requested again. For a wl_surface, the - * notifications are posted in the order the frame requests were - * committed. - * - * The server must send the notifications so that a client will not - * send excessive updates, while still allowing the highest - * possible update rate for clients that wait for the reply before - * drawing again. The server should give some time for the client - * to draw and commit after sending the frame callback events to - * let it hit the next output refresh. - * - * A server should avoid signaling the frame callbacks if the - * surface is not visible in any way, e.g. the surface is - * off-screen, or completely obscured by other opaque surfaces. - * - * The object returned by this request will be destroyed by the - * compositor after the callback is fired and as such the client - * must not attempt to use it after that point. - * - * The callback_data passed in the callback is the current time, in - * milliseconds, with an undefined base. - * @param callback callback object for the frame request - */ void (*frame)(struct wl_client *client, struct wl_resource *resource, uint32_t callback); - /** - * set opaque region - * - * This request sets the region of the surface that contains - * opaque content. - * - * The opaque region is an optimization hint for the compositor - * that lets it optimize the redrawing of content behind opaque - * regions. Setting an opaque region is not required for correct - * behaviour, but marking transparent content as opaque will result - * in repaint artifacts. - * - * The opaque region is specified in surface-local coordinates. - * - * The compositor ignores the parts of the opaque region that fall - * outside of the surface. - * - * Opaque region is double-buffered state, see wl_surface.commit. - * - * wl_surface.set_opaque_region changes the pending opaque region. - * wl_surface.commit copies the pending region to the current - * region. Otherwise, the pending and current regions are never - * changed. - * - * The initial value for an opaque region is empty. Setting the - * pending opaque region has copy semantics, and the wl_region - * object can be destroyed immediately. A NULL wl_region causes the - * pending opaque region to be set to empty. - * @param region opaque region of the surface - */ void (*set_opaque_region)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *region); - /** - * set input region - * - * This request sets the region of the surface that can receive - * pointer and touch events. - * - * Input events happening outside of this region will try the next - * surface in the server surface stack. The compositor ignores the - * parts of the input region that fall outside of the surface. - * - * The input region is specified in surface-local coordinates. - * - * Input region is double-buffered state, see wl_surface.commit. - * - * wl_surface.set_input_region changes the pending input region. - * wl_surface.commit copies the pending region to the current - * region. Otherwise the pending and current regions are never - * changed, except cursor and icon surfaces are special cases, see - * wl_pointer.set_cursor and wl_data_device.start_drag. - * - * The initial value for an input region is infinite. That means - * the whole surface will accept input. Setting the pending input - * region has copy semantics, and the wl_region object can be - * destroyed immediately. A NULL wl_region causes the input region - * to be set to infinite. - * @param region input region of the surface - */ void (*set_input_region)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *region); - /** - * commit pending surface state - * - * Surface state (input, opaque, and damage regions, attached - * buffers, etc.) is double-buffered. Protocol requests modify the - * pending state, as opposed to the active state in use by the - * compositor. - * - * All requests that need a commit to become effective are - * documented to affect double-buffered state. - * - * Other interfaces may add further double-buffered surface state. - * - * A commit request atomically creates a Content Update (CU) from - * the pending state, even if the pending state has not been - * touched. The content update is placed at the end of a - * per-surface queue until it becomes active. After commit, the new - * pending state is as documented for each related request. - * - * A CU is either a Desync Content Update (DCU) or a Sync Content - * Update (SCU). If the surface is effectively synchronized at the - * commit request, it is a SCU, otherwise a DCU. - * - * When a surface transitions from effectively synchronized to - * effectively desynchronized, all SCUs in its queue which are not - * reachable by any DCU become DCUs and dependency edges from - * outside the queue to these CUs are removed. - * - * See wl_subsurface for the definition of 'effectively - * synchronized' and 'effectively desynchronized'. - * - * When a CU is placed in the queue, the CU has a dependency on the - * CU in front of it and to the SCU at end of the queue of every - * direct child surface if that SCU exists and does not have - * another dependent. This can form a directed acyclic graph of CUs - * with dependencies as edges. - * - * In addition to surface state, the CU can have constraints that - * must be satisfied before it can be applied. Other interfaces may - * add CU constraints. - * - * All DCUs which do not have a SCU in front of themselves in their - * queue, are candidates. If the graph that's reachable by a - * candidate does not have any unsatisfied constraints, the entire - * graph must be applied atomically. - * - * When a CU is applied, the wl_buffer is applied before all other - * state. This means that all coordinates in double-buffered state - * are relative to the newly attached wl_buffers, except for - * wl_surface.attach itself. If there is no newly attached - * wl_buffer, the coordinates are relative to the previous content - * update. - */ void (*commit)(struct wl_client *client, struct wl_resource *resource); - /** - * sets the buffer transformation - * - * This request sets the transformation that the client has - * already applied to the content of the buffer. The accepted - * values for the transform parameter are the values for - * wl_output.transform. - * - * The compositor applies the inverse of this transformation - * whenever it uses the buffer contents. - * - * Buffer transform is double-buffered state, see - * wl_surface.commit. - * - * A newly created surface has its buffer transformation set to - * normal. - * - * wl_surface.set_buffer_transform changes the pending buffer - * transformation. wl_surface.commit copies the pending buffer - * transformation to the current one. Otherwise, the pending and - * current values are never changed. - * - * The purpose of this request is to allow clients to render - * content according to the output transform, thus permitting the - * compositor to use certain optimizations even if the display is - * rotated. Using hardware overlays and scanning out a client - * buffer for fullscreen surfaces are examples of such - * optimizations. Those optimizations are highly dependent on the - * compositor implementation, so the use of this request should be - * considered on a case-by-case basis. - * - * Note that if the transform value includes 90 or 270 degree - * rotation, the width of the buffer will become the surface height - * and the height of the buffer will become the surface width. - * - * If transform is not one of the values from the - * wl_output.transform enum the invalid_transform protocol error is - * raised. - * @param transform transform for interpreting buffer contents - * @since 2 - */ void (*set_buffer_transform)(struct wl_client *client, struct wl_resource *resource, int32_t transform); - /** - * sets the buffer scaling factor - * - * This request sets an optional scaling factor on how the - * compositor interprets the contents of the buffer attached to the - * window. - * - * Buffer scale is double-buffered state, see wl_surface.commit. - * - * A newly created surface has its buffer scale set to 1. - * - * wl_surface.set_buffer_scale changes the pending buffer scale. - * wl_surface.commit copies the pending buffer scale to the current - * one. Otherwise, the pending and current values are never - * changed. - * - * The purpose of this request is to allow clients to supply higher - * resolution buffer data for use on high resolution outputs. It is - * intended that you pick the same buffer scale as the scale of the - * output that the surface is displayed on. This means the - * compositor can avoid scaling when rendering the surface on that - * output. - * - * Note that if the scale is larger than 1, then you have to attach - * a buffer that is larger (by a factor of scale in each dimension) - * than the desired surface size. - * - * If scale is not greater than 0 the invalid_scale protocol error - * is raised. - * @param scale scale for interpreting buffer contents - * @since 3 - */ void (*set_buffer_scale)(struct wl_client *client, struct wl_resource *resource, int32_t scale); - /** - * mark part of the surface damaged using buffer coordinates - * - * This request is used to describe the regions where the pending - * buffer is different from the current surface contents, and where - * the surface therefore needs to be repainted. The compositor - * ignores the parts of the damage that fall outside of the - * surface. - * - * Damage is double-buffered state, see wl_surface.commit. - * - * The damage rectangle is specified in buffer coordinates, where x - * and y specify the upper left corner of the damage rectangle. - * - * The initial value for pending damage is empty: no damage. - * wl_surface.damage_buffer adds pending damage: the new pending - * damage is the union of old pending damage and the given - * rectangle. - * - * wl_surface.commit assigns pending damage as the current damage, - * and clears pending damage. The server will clear the current - * damage as it repaints the surface. - * - * This request differs from wl_surface.damage in only one way - it - * takes damage in buffer coordinates instead of surface-local - * coordinates. While this generally is more intuitive than surface - * coordinates, it is especially desirable when using wp_viewport - * or when a drawing library (like EGL) is unaware of buffer scale - * and buffer transform. - * - * Note: Because buffer transformation changes and damage requests - * may be interleaved in the protocol stream, it is impossible to - * determine the actual mapping between surface and buffer damage - * until wl_surface.commit time. Therefore, compositors wishing to - * take both kinds of damage into account will have to accumulate - * damage from the two requests separately and only transform from - * one to the other after receiving the wl_surface.commit. - * @param x buffer-local x coordinate - * @param y buffer-local y coordinate - * @param width width of damage rectangle - * @param height height of damage rectangle - * @since 4 - */ void (*damage_buffer)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * set the surface contents offset - * - * The x and y arguments specify the location of the new pending - * buffer's upper left corner, relative to the current buffer's - * upper left corner, in surface-local coordinates. In other words, - * the x and y, combined with the new surface size define in which - * directions the surface's size changes. - * - * The exact semantics of wl_surface.offset are role-specific. - * Refer to the documentation of specific roles for more - * information. - * - * Surface location offset is double-buffered state, see - * wl_surface.commit. - * - * This request is semantically equivalent to and the replaces the - * x and y arguments in the wl_surface.attach request in wl_surface - * versions prior to 5. See wl_surface.attach for details. - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @since 5 - */ void (*offset)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y); - /** - * get a release callback - * - * Create a callback for the release of the buffer attached by - * the client with wl_surface.attach. - * - * The compositor will release the buffer when it has finished its - * usage of the underlying storage for the relevant commit. Once - * the client receives this event, and assuming the associated - * buffer is not pending release from other wl_surface.commit - * requests, the client can safely re-use the buffer. - * - * Release callbacks are double-buffered state, and will be - * associated with the pending buffer at wl_surface.commit time. - * - * The callback_data passed in the wl_callback.done event is unused - * and is always zero. - * - * Sending this request without attaching a non-null buffer in the - * same content update is a protocol error. The compositor will - * send the no_buffer error in this case. - * @param callback callback object for the release - * @since 7 - */ void (*get_release)(struct wl_client *client, struct wl_resource *resource, uint32_t callback); @@ -4267,114 +1448,42 @@ struct wl_surface_interface { #define WL_SURFACE_PREFERRED_BUFFER_SCALE 2 #define WL_SURFACE_PREFERRED_BUFFER_TRANSFORM 3 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION 6 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_PREFERRED_BUFFER_TRANSFORM_SINCE_VERSION 6 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_ATTACH_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DAMAGE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_FRAME_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_OPAQUE_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_INPUT_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_COMMIT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_BUFFER_TRANSFORM_SINCE_VERSION 2 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_BUFFER_SCALE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION 4 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_OFFSET_SINCE_VERSION 5 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_GET_RELEASE_SINCE_VERSION 7 -/** - * @ingroup iface_wl_surface - * Sends an enter event to the client owning the resource. - * @param resource_ The client's resource - * @param output output entered by the surface - */ static inline void wl_surface_send_enter(struct wl_resource *resource_, struct wl_resource *output) { wl_resource_post_event(resource_, WL_SURFACE_ENTER, output); } -/** - * @ingroup iface_wl_surface - * Sends an leave event to the client owning the resource. - * @param resource_ The client's resource - * @param output output left by the surface - */ static inline void wl_surface_send_leave(struct wl_resource *resource_, struct wl_resource *output) { wl_resource_post_event(resource_, WL_SURFACE_LEAVE, output); } -/** - * @ingroup iface_wl_surface - * Sends an preferred_buffer_scale event to the client owning the resource. - * @param resource_ The client's resource - * @param factor preferred scaling factor - */ static inline void wl_surface_send_preferred_buffer_scale(struct wl_resource *resource_, int32_t factor) { wl_resource_post_event(resource_, WL_SURFACE_PREFERRED_BUFFER_SCALE, factor); } -/** - * @ingroup iface_wl_surface - * Sends an preferred_buffer_transform event to the client owning the resource. - * @param resource_ The client's resource - * @param transform preferred transform - */ static inline void wl_surface_send_preferred_buffer_transform(struct wl_resource *resource_, uint32_t transform) { @@ -4383,38 +1492,15 @@ wl_surface_send_preferred_buffer_transform(struct wl_resource *resource_, uint32 #ifndef WL_SEAT_CAPABILITY_ENUM #define WL_SEAT_CAPABILITY_ENUM -/** - * @ingroup iface_wl_seat - * seat capability bitmask - * - * This is a bitmask of capabilities this seat has; if a member is - * set, then it is present on the seat. - */ enum wl_seat_capability { - /** - * the seat has pointer devices - */ WL_SEAT_CAPABILITY_POINTER = 1, - /** - * the seat has one or more keyboards - */ WL_SEAT_CAPABILITY_KEYBOARD = 2, - /** - * the seat has touch devices - */ WL_SEAT_CAPABILITY_TOUCH = 4, }; -#endif /* WL_SEAT_CAPABILITY_ENUM */ +#endif #ifndef WL_SEAT_CAPABILITY_ENUM_IS_VALID #define WL_SEAT_CAPABILITY_ENUM_IS_VALID -/** - * @ingroup iface_wl_seat - * Validate a wl_seat capability value. - * - * @return true on success, false on error. - * @ref wl_seat_capability - */ static inline bool wl_seat_capability_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -4426,33 +1512,17 @@ wl_seat_capability_is_valid(uint32_t value, uint32_t version) { valid |= WL_SEAT_CAPABILITY_TOUCH; return (value & ~valid) == 0; } -#endif /* WL_SEAT_CAPABILITY_ENUM_IS_VALID */ +#endif #ifndef WL_SEAT_ERROR_ENUM #define WL_SEAT_ERROR_ENUM -/** - * @ingroup iface_wl_seat - * wl_seat error values - * - * These errors can be emitted in response to wl_seat requests. - */ enum wl_seat_error { - /** - * get_pointer, get_keyboard or get_touch called on seat without the matching capability - */ WL_SEAT_ERROR_MISSING_CAPABILITY = 0, }; -#endif /* WL_SEAT_ERROR_ENUM */ +#endif #ifndef WL_SEAT_ERROR_ENUM_IS_VALID #define WL_SEAT_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_seat - * Validate a wl_seat error value. - * - * @return true on success, false on error. - * @ref wl_seat_error - */ static inline bool wl_seat_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4462,68 +1532,18 @@ wl_seat_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SEAT_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_seat - * @struct wl_seat_interface - */ struct wl_seat_interface { - /** - * return pointer object - * - * The ID provided will be initialized to the wl_pointer - * interface for this seat. - * - * This request only takes effect if the seat has the pointer - * capability, or has had the pointer capability in the past. It is - * a protocol violation to issue this request on a seat that has - * never had the pointer capability. The missing_capability error - * will be sent in this case. - * @param id seat pointer - */ void (*get_pointer)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * return keyboard object - * - * The ID provided will be initialized to the wl_keyboard - * interface for this seat. - * - * This request only takes effect if the seat has the keyboard - * capability, or has had the keyboard capability in the past. It - * is a protocol violation to issue this request on a seat that has - * never had the keyboard capability. The missing_capability error - * will be sent in this case. - * @param id seat keyboard - */ void (*get_keyboard)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * return touch object - * - * The ID provided will be initialized to the wl_touch interface - * for this seat. - * - * This request only takes effect if the seat has the touch - * capability, or has had the touch capability in the past. It is a - * protocol violation to issue this request on a seat that has - * never had the touch capability. The missing_capability error - * will be sent in this case. - * @param id seat touch interface - */ void (*get_touch)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * release the seat object - * - * Using this request a client can tell the server that it is not - * going to use the seat object anymore. - * @since 5 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -4531,50 +1551,20 @@ struct wl_seat_interface { #define WL_SEAT_CAPABILITIES 0 #define WL_SEAT_NAME 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_CAPABILITIES_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_NAME_SINCE_VERSION 2 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_POINTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_KEYBOARD_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_TOUCH_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_RELEASE_SINCE_VERSION 5 -/** - * @ingroup iface_wl_seat - * Sends an capabilities event to the client owning the resource. - * @param resource_ The client's resource - * @param capabilities capabilities of the seat - */ static inline void wl_seat_send_capabilities(struct wl_resource *resource_, uint32_t capabilities) { wl_resource_post_event(resource_, WL_SEAT_CAPABILITIES, capabilities); } -/** - * @ingroup iface_wl_seat - * Sends an name event to the client owning the resource. - * @param resource_ The client's resource - * @param name seat identifier - */ static inline void wl_seat_send_name(struct wl_resource *resource_, const char *name) { @@ -4584,22 +1574,12 @@ wl_seat_send_name(struct wl_resource *resource_, const char *name) #ifndef WL_POINTER_ERROR_ENUM #define WL_POINTER_ERROR_ENUM enum wl_pointer_error { - /** - * given wl_surface has another role - */ WL_POINTER_ERROR_ROLE = 0, }; -#endif /* WL_POINTER_ERROR_ENUM */ +#endif #ifndef WL_POINTER_ERROR_ENUM_IS_VALID #define WL_POINTER_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_pointer - * Validate a wl_pointer error value. - * - * @return true on success, false on error. - * @ref wl_pointer_error - */ static inline bool wl_pointer_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4609,38 +1589,18 @@ wl_pointer_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_POINTER_ERROR_ENUM_IS_VALID */ +#endif #ifndef WL_POINTER_BUTTON_STATE_ENUM #define WL_POINTER_BUTTON_STATE_ENUM -/** - * @ingroup iface_wl_pointer - * physical button state - * - * Describes the physical state of a button that produced the button - * event. - */ enum wl_pointer_button_state { - /** - * the button is not pressed - */ WL_POINTER_BUTTON_STATE_RELEASED = 0, - /** - * the button is pressed - */ WL_POINTER_BUTTON_STATE_PRESSED = 1, }; -#endif /* WL_POINTER_BUTTON_STATE_ENUM */ +#endif #ifndef WL_POINTER_BUTTON_STATE_ENUM_IS_VALID #define WL_POINTER_BUTTON_STATE_ENUM_IS_VALID -/** - * @ingroup iface_wl_pointer - * Validate a wl_pointer button_state value. - * - * @return true on success, false on error. - * @ref wl_pointer_button_state - */ static inline bool wl_pointer_button_state_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4652,37 +1612,18 @@ wl_pointer_button_state_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_POINTER_BUTTON_STATE_ENUM_IS_VALID */ +#endif #ifndef WL_POINTER_AXIS_ENUM #define WL_POINTER_AXIS_ENUM -/** - * @ingroup iface_wl_pointer - * axis types - * - * Describes the axis types of scroll events. - */ enum wl_pointer_axis { - /** - * vertical axis - */ WL_POINTER_AXIS_VERTICAL_SCROLL = 0, - /** - * horizontal axis - */ WL_POINTER_AXIS_HORIZONTAL_SCROLL = 1, }; -#endif /* WL_POINTER_AXIS_ENUM */ +#endif #ifndef WL_POINTER_AXIS_ENUM_IS_VALID #define WL_POINTER_AXIS_ENUM_IS_VALID -/** - * @ingroup iface_wl_pointer - * Validate a wl_pointer axis value. - * - * @return true on success, false on error. - * @ref wl_pointer_axis - */ static inline bool wl_pointer_axis_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4694,65 +1635,21 @@ wl_pointer_axis_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_POINTER_AXIS_ENUM_IS_VALID */ +#endif #ifndef WL_POINTER_AXIS_SOURCE_ENUM #define WL_POINTER_AXIS_SOURCE_ENUM -/** - * @ingroup iface_wl_pointer - * axis source types - * - * Describes the source types for axis events. This indicates to the - * client how an axis event was physically generated; a client may - * adjust the user interface accordingly. For example, scroll events - * from a "finger" source may be in a smooth coordinate space with - * kinetic scrolling whereas a "wheel" source may be in discrete steps - * of a number of lines. - * - * The "continuous" axis source is a device generating events in a - * continuous coordinate space, but using something other than a - * finger. One example for this source is button-based scrolling where - * the vertical motion of a device is converted to scroll events while - * a button is held down. - * - * The "wheel tilt" axis source indicates that the actual device is a - * wheel but the scroll event is not caused by a rotation but a - * (usually sideways) tilt of the wheel. - */ enum wl_pointer_axis_source { - /** - * a physical wheel rotation - */ WL_POINTER_AXIS_SOURCE_WHEEL = 0, - /** - * finger on a touch surface - */ WL_POINTER_AXIS_SOURCE_FINGER = 1, - /** - * continuous coordinate space - */ WL_POINTER_AXIS_SOURCE_CONTINUOUS = 2, - /** - * a physical wheel tilt - * @since 6 - */ WL_POINTER_AXIS_SOURCE_WHEEL_TILT = 3, }; -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SOURCE_WHEEL_TILT_SINCE_VERSION 6 -#endif /* WL_POINTER_AXIS_SOURCE_ENUM */ +#endif #ifndef WL_POINTER_AXIS_SOURCE_ENUM_IS_VALID #define WL_POINTER_AXIS_SOURCE_ENUM_IS_VALID -/** - * @ingroup iface_wl_pointer - * Validate a wl_pointer axis_source value. - * - * @return true on success, false on error. - * @ref wl_pointer_axis_source - */ static inline bool wl_pointer_axis_source_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4768,38 +1665,18 @@ wl_pointer_axis_source_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_POINTER_AXIS_SOURCE_ENUM_IS_VALID */ +#endif #ifndef WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM #define WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM -/** - * @ingroup iface_wl_pointer - * axis relative direction - * - * This specifies the direction of the physical motion that caused a - * wl_pointer.axis event, relative to the wl_pointer.axis direction. - */ enum wl_pointer_axis_relative_direction { - /** - * physical motion matches axis direction - */ WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL = 0, - /** - * physical motion is the inverse of the axis direction - */ WL_POINTER_AXIS_RELATIVE_DIRECTION_INVERTED = 1, }; -#endif /* WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM */ +#endif #ifndef WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM_IS_VALID #define WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM_IS_VALID -/** - * @ingroup iface_wl_pointer - * Validate a wl_pointer axis_relative_direction value. - * - * @return true on success, false on error. - * @ref wl_pointer_axis_relative_direction - */ static inline bool wl_pointer_axis_relative_direction_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4811,70 +1688,15 @@ wl_pointer_axis_relative_direction_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_pointer - * @struct wl_pointer_interface - */ struct wl_pointer_interface { - /** - * set the pointer surface - * - * Set the pointer surface, i.e., the surface that contains the - * pointer image (cursor). This request gives the surface the role - * of a cursor. If the surface already has another role, it raises - * a protocol error. - * - * The cursor actually changes only if the pointer focus for this - * device is one of the requesting client's surfaces or the surface - * parameter is the current pointer surface. If there was a - * previous surface set with this request it is replaced. If - * surface is NULL, the pointer image is hidden. - * - * The parameters hotspot_x and hotspot_y define the position of - * the pointer surface relative to the pointer location. Its - * top-left corner is always at (x, y) - (hotspot_x, hotspot_y), - * where (x, y) are the coordinates of the pointer location, in - * surface-local coordinates. - * - * On wl_surface.offset requests to the pointer surface, hotspot_x - * and hotspot_y are decremented by the x and y parameters passed - * to the request. The offset must be applied by wl_surface.commit - * as usual. - * - * The hotspot can also be updated by passing the currently set - * pointer surface to this request with new values for hotspot_x - * and hotspot_y. - * - * The input region is ignored for wl_surfaces with the role of a - * cursor. When the use as a cursor ends, the wl_surface is - * unmapped. - * - * The serial parameter must match the latest wl_pointer.enter - * serial number sent to the client. Otherwise the request will be - * ignored. - * @param serial serial number of the enter event - * @param surface pointer surface - * @param hotspot_x surface-local x coordinate - * @param hotspot_y surface-local y coordinate - */ void (*set_cursor)(struct wl_client *client, struct wl_resource *resource, uint32_t serial, struct wl_resource *surface, int32_t hotspot_x, int32_t hotspot_y); - /** - * release the pointer object - * - * Using this request a client can tell the server that it is not - * going to use the pointer object anymore. - * - * This request destroys the pointer proxy object, so clients must - * not call wl_pointer_destroy() after using this request. - * @since 3 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -4892,217 +1714,88 @@ struct wl_pointer_interface { #define WL_POINTER_AXIS_RELATIVE_DIRECTION 10 #define WL_POINTER_WARP 11 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_BUTTON_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_FRAME_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SOURCE_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_STOP_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_DISCRETE_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_VALUE120_SINCE_VERSION 8 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_RELATIVE_DIRECTION_SINCE_VERSION 9 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_WARP_SINCE_VERSION 11 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_SET_CURSOR_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_RELEASE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_pointer - * Sends an enter event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the enter event - * @param surface surface entered by the pointer - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ static inline void wl_pointer_send_enter(struct wl_resource *resource_, uint32_t serial, struct wl_resource *surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { wl_resource_post_event(resource_, WL_POINTER_ENTER, serial, surface, surface_x, surface_y); } -/** - * @ingroup iface_wl_pointer - * Sends an leave event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the leave event - * @param surface surface left by the pointer - */ static inline void wl_pointer_send_leave(struct wl_resource *resource_, uint32_t serial, struct wl_resource *surface) { wl_resource_post_event(resource_, WL_POINTER_LEAVE, serial, surface); } -/** - * @ingroup iface_wl_pointer - * Sends an motion event to the client owning the resource. - * @param resource_ The client's resource - * @param time timestamp with millisecond granularity - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ static inline void wl_pointer_send_motion(struct wl_resource *resource_, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { wl_resource_post_event(resource_, WL_POINTER_MOTION, time, surface_x, surface_y); } -/** - * @ingroup iface_wl_pointer - * Sends an button event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the button event - * @param time timestamp with millisecond granularity - * @param button button that produced the event - * @param state physical state of the button - */ static inline void wl_pointer_send_button(struct wl_resource *resource_, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { wl_resource_post_event(resource_, WL_POINTER_BUTTON, serial, time, button, state); } -/** - * @ingroup iface_wl_pointer - * Sends an axis event to the client owning the resource. - * @param resource_ The client's resource - * @param time timestamp with millisecond granularity - * @param axis axis type - * @param value length of vector in surface-local coordinate space - */ static inline void wl_pointer_send_axis(struct wl_resource *resource_, uint32_t time, uint32_t axis, wl_fixed_t value) { wl_resource_post_event(resource_, WL_POINTER_AXIS, time, axis, value); } -/** - * @ingroup iface_wl_pointer - * Sends an frame event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_pointer_send_frame(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_POINTER_FRAME); } -/** - * @ingroup iface_wl_pointer - * Sends an axis_source event to the client owning the resource. - * @param resource_ The client's resource - * @param axis_source source of the axis event - */ static inline void wl_pointer_send_axis_source(struct wl_resource *resource_, uint32_t axis_source) { wl_resource_post_event(resource_, WL_POINTER_AXIS_SOURCE, axis_source); } -/** - * @ingroup iface_wl_pointer - * Sends an axis_stop event to the client owning the resource. - * @param resource_ The client's resource - * @param time timestamp with millisecond granularity - * @param axis the axis stopped with this event - */ static inline void wl_pointer_send_axis_stop(struct wl_resource *resource_, uint32_t time, uint32_t axis) { wl_resource_post_event(resource_, WL_POINTER_AXIS_STOP, time, axis); } -/** - * @ingroup iface_wl_pointer - * Sends an axis_discrete event to the client owning the resource. - * @param resource_ The client's resource - * @param axis axis type - * @param discrete number of steps - */ static inline void wl_pointer_send_axis_discrete(struct wl_resource *resource_, uint32_t axis, int32_t discrete) { wl_resource_post_event(resource_, WL_POINTER_AXIS_DISCRETE, axis, discrete); } -/** - * @ingroup iface_wl_pointer - * Sends an axis_value120 event to the client owning the resource. - * @param resource_ The client's resource - * @param axis axis type - * @param value120 scroll distance as fraction of 120 - */ static inline void wl_pointer_send_axis_value120(struct wl_resource *resource_, uint32_t axis, int32_t value120) { wl_resource_post_event(resource_, WL_POINTER_AXIS_VALUE120, axis, value120); } -/** - * @ingroup iface_wl_pointer - * Sends an axis_relative_direction event to the client owning the resource. - * @param resource_ The client's resource - * @param axis axis type - * @param direction physical direction relative to axis motion - */ static inline void wl_pointer_send_axis_relative_direction(struct wl_resource *resource_, uint32_t axis, uint32_t direction) { wl_resource_post_event(resource_, WL_POINTER_AXIS_RELATIVE_DIRECTION, axis, direction); } -/** - * @ingroup iface_wl_pointer - * Sends an warp event to the client owning the resource. - * @param resource_ The client's resource - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ static inline void wl_pointer_send_warp(struct wl_resource *resource_, wl_fixed_t surface_x, wl_fixed_t surface_y) { @@ -5111,34 +1804,14 @@ wl_pointer_send_warp(struct wl_resource *resource_, wl_fixed_t surface_x, wl_fix #ifndef WL_KEYBOARD_KEYMAP_FORMAT_ENUM #define WL_KEYBOARD_KEYMAP_FORMAT_ENUM -/** - * @ingroup iface_wl_keyboard - * keyboard mapping format - * - * This specifies the format of the keymap provided to the - * client with the wl_keyboard.keymap event. - */ enum wl_keyboard_keymap_format { - /** - * no keymap; client must understand how to interpret the raw keycode - */ WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP = 0, - /** - * libxkbcommon compatible, null-terminated string; to determine the xkb keycode, clients must add 8 to the key event keycode - */ WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 = 1, }; -#endif /* WL_KEYBOARD_KEYMAP_FORMAT_ENUM */ +#endif #ifndef WL_KEYBOARD_KEYMAP_FORMAT_ENUM_IS_VALID #define WL_KEYBOARD_KEYMAP_FORMAT_ENUM_IS_VALID -/** - * @ingroup iface_wl_keyboard - * Validate a wl_keyboard keymap_format value. - * - * @return true on success, false on error. - * @ref wl_keyboard_keymap_format - */ static inline bool wl_keyboard_keymap_format_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -5150,54 +1823,20 @@ wl_keyboard_keymap_format_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_KEYBOARD_KEYMAP_FORMAT_ENUM_IS_VALID */ +#endif #ifndef WL_KEYBOARD_KEY_STATE_ENUM #define WL_KEYBOARD_KEY_STATE_ENUM -/** - * @ingroup iface_wl_keyboard - * physical key state - * - * Describes the physical state of a key that produced the key event. - * - * Since version 10, the key can be in a "repeated" pseudo-state which - * means the same as "pressed", but is used to signal repetition in the - * key event. - * - * The key may only enter the repeated state after entering the pressed - * state and before entering the released state. This event may be - * generated multiple times while the key is down. - */ enum wl_keyboard_key_state { - /** - * key is not pressed - */ WL_KEYBOARD_KEY_STATE_RELEASED = 0, - /** - * key is pressed - */ WL_KEYBOARD_KEY_STATE_PRESSED = 1, - /** - * key was repeated - * @since 10 - */ WL_KEYBOARD_KEY_STATE_REPEATED = 2, }; -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEY_STATE_REPEATED_SINCE_VERSION 10 -#endif /* WL_KEYBOARD_KEY_STATE_ENUM */ +#endif #ifndef WL_KEYBOARD_KEY_STATE_ENUM_IS_VALID #define WL_KEYBOARD_KEY_STATE_ENUM_IS_VALID -/** - * @ingroup iface_wl_keyboard - * Validate a wl_keyboard key_state value. - * - * @return true on success, false on error. - * @ref wl_keyboard_key_state - */ static inline bool wl_keyboard_key_state_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -5211,19 +1850,9 @@ wl_keyboard_key_state_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_KEYBOARD_KEY_STATE_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_keyboard - * @struct wl_keyboard_interface - */ struct wl_keyboard_interface { - /** - * release the keyboard object - * - * - * @since 3 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -5235,132 +1864,52 @@ struct wl_keyboard_interface { #define WL_KEYBOARD_MODIFIERS 4 #define WL_KEYBOARD_REPEAT_INFO 5 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEYMAP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_MODIFIERS_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_REPEAT_INFO_SINCE_VERSION 4 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_RELEASE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_keyboard - * Sends an keymap event to the client owning the resource. - * @param resource_ The client's resource - * @param format keymap format - * @param fd keymap file descriptor - * @param size keymap size, in bytes - */ static inline void wl_keyboard_send_keymap(struct wl_resource *resource_, uint32_t format, int32_t fd, uint32_t size) { wl_resource_post_event(resource_, WL_KEYBOARD_KEYMAP, format, fd, size); } -/** - * @ingroup iface_wl_keyboard - * Sends an enter event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the enter event - * @param surface surface gaining keyboard focus - * @param keys the keys currently logically down - */ static inline void wl_keyboard_send_enter(struct wl_resource *resource_, uint32_t serial, struct wl_resource *surface, struct wl_array *keys) { wl_resource_post_event(resource_, WL_KEYBOARD_ENTER, serial, surface, keys); } -/** - * @ingroup iface_wl_keyboard - * Sends an leave event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the leave event - * @param surface surface that lost keyboard focus - */ static inline void wl_keyboard_send_leave(struct wl_resource *resource_, uint32_t serial, struct wl_resource *surface) { wl_resource_post_event(resource_, WL_KEYBOARD_LEAVE, serial, surface); } -/** - * @ingroup iface_wl_keyboard - * Sends an key event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the key event - * @param time timestamp with millisecond granularity - * @param key key that produced the event - * @param state physical state of the key - */ static inline void wl_keyboard_send_key(struct wl_resource *resource_, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { wl_resource_post_event(resource_, WL_KEYBOARD_KEY, serial, time, key, state); } -/** - * @ingroup iface_wl_keyboard - * Sends an modifiers event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the modifiers event - * @param mods_depressed depressed modifiers - * @param mods_latched latched modifiers - * @param mods_locked locked modifiers - * @param group keyboard layout - */ static inline void wl_keyboard_send_modifiers(struct wl_resource *resource_, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { wl_resource_post_event(resource_, WL_KEYBOARD_MODIFIERS, serial, mods_depressed, mods_latched, mods_locked, group); } -/** - * @ingroup iface_wl_keyboard - * Sends an repeat_info event to the client owning the resource. - * @param resource_ The client's resource - * @param rate the rate of repeating keys in characters per second - * @param delay delay in milliseconds since key down until repeating starts - */ static inline void wl_keyboard_send_repeat_info(struct wl_resource *resource_, int32_t rate, int32_t delay) { wl_resource_post_event(resource_, WL_KEYBOARD_REPEAT_INFO, rate, delay); } -/** - * @ingroup iface_wl_touch - * @struct wl_touch_interface - */ struct wl_touch_interface { - /** - * release the touch object - * - * - * @since 3 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -5373,129 +1922,52 @@ struct wl_touch_interface { #define WL_TOUCH_SHAPE 5 #define WL_TOUCH_ORIENTATION 6 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_DOWN_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_UP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_FRAME_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_CANCEL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_SHAPE_SINCE_VERSION 6 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_ORIENTATION_SINCE_VERSION 6 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_RELEASE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_touch - * Sends an down event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the touch down event - * @param time timestamp with millisecond granularity - * @param surface surface touched - * @param id the unique ID of this touch point - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ static inline void wl_touch_send_down(struct wl_resource *resource_, uint32_t serial, uint32_t time, struct wl_resource *surface, int32_t id, wl_fixed_t x, wl_fixed_t y) { wl_resource_post_event(resource_, WL_TOUCH_DOWN, serial, time, surface, id, x, y); } -/** - * @ingroup iface_wl_touch - * Sends an up event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the touch up event - * @param time timestamp with millisecond granularity - * @param id the unique ID of this touch point - */ static inline void wl_touch_send_up(struct wl_resource *resource_, uint32_t serial, uint32_t time, int32_t id) { wl_resource_post_event(resource_, WL_TOUCH_UP, serial, time, id); } -/** - * @ingroup iface_wl_touch - * Sends an motion event to the client owning the resource. - * @param resource_ The client's resource - * @param time timestamp with millisecond granularity - * @param id the unique ID of this touch point - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ static inline void wl_touch_send_motion(struct wl_resource *resource_, uint32_t time, int32_t id, wl_fixed_t x, wl_fixed_t y) { wl_resource_post_event(resource_, WL_TOUCH_MOTION, time, id, x, y); } -/** - * @ingroup iface_wl_touch - * Sends an frame event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_touch_send_frame(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_TOUCH_FRAME); } -/** - * @ingroup iface_wl_touch - * Sends an cancel event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_touch_send_cancel(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_TOUCH_CANCEL); } -/** - * @ingroup iface_wl_touch - * Sends an shape event to the client owning the resource. - * @param resource_ The client's resource - * @param id the unique ID of this touch point - * @param major length of the major axis in surface-local coordinates - * @param minor length of the minor axis in surface-local coordinates - */ static inline void wl_touch_send_shape(struct wl_resource *resource_, int32_t id, wl_fixed_t major, wl_fixed_t minor) { wl_resource_post_event(resource_, WL_TOUCH_SHAPE, id, major, minor); } -/** - * @ingroup iface_wl_touch - * Sends an orientation event to the client owning the resource. - * @param resource_ The client's resource - * @param id the unique ID of this touch point - * @param orientation angle between major axis and positive surface y-axis in degrees - */ static inline void wl_touch_send_orientation(struct wl_resource *resource_, int32_t id, wl_fixed_t orientation) { @@ -5504,50 +1976,18 @@ wl_touch_send_orientation(struct wl_resource *resource_, int32_t id, wl_fixed_t #ifndef WL_OUTPUT_SUBPIXEL_ENUM #define WL_OUTPUT_SUBPIXEL_ENUM -/** - * @ingroup iface_wl_output - * subpixel geometry information - * - * This enumeration describes how the physical - * pixels on an output are laid out. - */ enum wl_output_subpixel { - /** - * unknown geometry - */ WL_OUTPUT_SUBPIXEL_UNKNOWN = 0, - /** - * no geometry - */ WL_OUTPUT_SUBPIXEL_NONE = 1, - /** - * horizontal RGB - */ WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB = 2, - /** - * horizontal BGR - */ WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR = 3, - /** - * vertical RGB - */ WL_OUTPUT_SUBPIXEL_VERTICAL_RGB = 4, - /** - * vertical BGR - */ WL_OUTPUT_SUBPIXEL_VERTICAL_BGR = 5, }; -#endif /* WL_OUTPUT_SUBPIXEL_ENUM */ +#endif #ifndef WL_OUTPUT_SUBPIXEL_ENUM_IS_VALID #define WL_OUTPUT_SUBPIXEL_ENUM_IS_VALID -/** - * @ingroup iface_wl_output - * Validate a wl_output subpixel value. - * - * @return true on success, false on error. - * @ref wl_output_subpixel - */ static inline bool wl_output_subpixel_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -5567,70 +2007,24 @@ wl_output_subpixel_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_OUTPUT_SUBPIXEL_ENUM_IS_VALID */ +#endif #ifndef WL_OUTPUT_TRANSFORM_ENUM #define WL_OUTPUT_TRANSFORM_ENUM -/** - * @ingroup iface_wl_output - * transformation applied to buffer contents - * - * This describes transformations that clients and compositors apply to - * buffer contents. - * - * The flipped values correspond to an initial flip around a - * vertical axis followed by rotation. - * - * The purpose is mainly to allow clients to render accordingly and - * tell the compositor, so that for fullscreen surfaces, the - * compositor will still be able to scan out directly from client - * surfaces. - */ enum wl_output_transform { - /** - * no transform - */ WL_OUTPUT_TRANSFORM_NORMAL = 0, - /** - * 90 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_90 = 1, - /** - * 180 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_180 = 2, - /** - * 270 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_270 = 3, - /** - * 180 degree flip around a vertical axis - */ WL_OUTPUT_TRANSFORM_FLIPPED = 4, - /** - * flip and rotate 90 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_90 = 5, - /** - * flip and rotate 180 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_180 = 6, - /** - * flip and rotate 270 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_270 = 7, }; -#endif /* WL_OUTPUT_TRANSFORM_ENUM */ +#endif #ifndef WL_OUTPUT_TRANSFORM_ENUM_IS_VALID #define WL_OUTPUT_TRANSFORM_ENUM_IS_VALID -/** - * @ingroup iface_wl_output - * Validate a wl_output transform value. - * - * @return true on success, false on error. - * @ref wl_output_transform - */ static inline bool wl_output_transform_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -5654,38 +2048,18 @@ wl_output_transform_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_OUTPUT_TRANSFORM_ENUM_IS_VALID */ +#endif #ifndef WL_OUTPUT_MODE_ENUM #define WL_OUTPUT_MODE_ENUM -/** - * @ingroup iface_wl_output - * mode information - * - * These flags describe properties of an output mode. - * They are used in the flags bitfield of the mode event. - */ enum wl_output_mode { - /** - * indicates this is the current mode - */ WL_OUTPUT_MODE_CURRENT = 0x1, - /** - * indicates this is the preferred mode - */ WL_OUTPUT_MODE_PREFERRED = 0x2, }; -#endif /* WL_OUTPUT_MODE_ENUM */ +#endif #ifndef WL_OUTPUT_MODE_ENUM_IS_VALID #define WL_OUTPUT_MODE_ENUM_IS_VALID -/** - * @ingroup iface_wl_output - * Validate a wl_output mode value. - * - * @return true on success, false on error. - * @ref wl_output_mode - */ static inline bool wl_output_mode_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -5695,20 +2069,9 @@ wl_output_mode_is_valid(uint32_t value, uint32_t version) { valid |= WL_OUTPUT_MODE_PREFERRED; return (value & ~valid) == 0; } -#endif /* WL_OUTPUT_MODE_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_output - * @struct wl_output_interface - */ struct wl_output_interface { - /** - * release the output object - * - * Using this request a client can tell the server that it is not - * going to use the output object anymore. - * @since 3 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -5720,153 +2083,60 @@ struct wl_output_interface { #define WL_OUTPUT_NAME 4 #define WL_OUTPUT_DESCRIPTION 5 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_GEOMETRY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_MODE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_DONE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_SCALE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_NAME_SINCE_VERSION 4 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_DESCRIPTION_SINCE_VERSION 4 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_RELEASE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_output - * Sends an geometry event to the client owning the resource. - * @param resource_ The client's resource - * @param x x position within the global compositor space - * @param y y position within the global compositor space - * @param physical_width width in millimeters of the output - * @param physical_height height in millimeters of the output - * @param subpixel subpixel orientation of the output - * @param make textual description of the manufacturer - * @param model textual description of the model - * @param transform additional transformation applied to buffer contents during presentation - */ static inline void wl_output_send_geometry(struct wl_resource *resource_, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char *make, const char *model, int32_t transform) { wl_resource_post_event(resource_, WL_OUTPUT_GEOMETRY, x, y, physical_width, physical_height, subpixel, make, model, transform); } -/** - * @ingroup iface_wl_output - * Sends an mode event to the client owning the resource. - * @param resource_ The client's resource - * @param flags bitfield of mode flags - * @param width width of the mode in hardware units - * @param height height of the mode in hardware units - * @param refresh vertical refresh rate in mHz - */ static inline void wl_output_send_mode(struct wl_resource *resource_, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { wl_resource_post_event(resource_, WL_OUTPUT_MODE, flags, width, height, refresh); } -/** - * @ingroup iface_wl_output - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_output_send_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_OUTPUT_DONE); } -/** - * @ingroup iface_wl_output - * Sends an scale event to the client owning the resource. - * @param resource_ The client's resource - * @param factor scaling factor of output - */ static inline void wl_output_send_scale(struct wl_resource *resource_, int32_t factor) { wl_resource_post_event(resource_, WL_OUTPUT_SCALE, factor); } -/** - * @ingroup iface_wl_output - * Sends an name event to the client owning the resource. - * @param resource_ The client's resource - * @param name output name - */ static inline void wl_output_send_name(struct wl_resource *resource_, const char *name) { wl_resource_post_event(resource_, WL_OUTPUT_NAME, name); } -/** - * @ingroup iface_wl_output - * Sends an description event to the client owning the resource. - * @param resource_ The client's resource - * @param description output description - */ static inline void wl_output_send_description(struct wl_resource *resource_, const char *description) { wl_resource_post_event(resource_, WL_OUTPUT_DESCRIPTION, description); } -/** - * @ingroup iface_wl_region - * @struct wl_region_interface - */ struct wl_region_interface { - /** - * destroy region - * - * Destroy the region. This will invalidate the object ID. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * add rectangle to region - * - * Add the specified rectangle to the region. - * @param x region-local x coordinate - * @param y region-local y coordinate - * @param width rectangle width - * @param height rectangle height - */ void (*add)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * subtract rectangle from region - * - * Subtract the specified rectangle from the region. - * @param x region-local x coordinate - * @param y region-local y coordinate - * @param width rectangle width - * @param height rectangle height - */ void (*subtract)(struct wl_client *client, struct wl_resource *resource, int32_t x, @@ -5876,42 +2146,20 @@ struct wl_region_interface { }; -/** - * @ingroup iface_wl_region - */ #define WL_REGION_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_region - */ #define WL_REGION_ADD_SINCE_VERSION 1 -/** - * @ingroup iface_wl_region - */ #define WL_REGION_SUBTRACT_SINCE_VERSION 1 #ifndef WL_SUBCOMPOSITOR_ERROR_ENUM #define WL_SUBCOMPOSITOR_ERROR_ENUM enum wl_subcompositor_error { - /** - * the to-be sub-surface is invalid - */ WL_SUBCOMPOSITOR_ERROR_BAD_SURFACE = 0, - /** - * the to-be sub-surface parent is invalid - */ WL_SUBCOMPOSITOR_ERROR_BAD_PARENT = 1, }; -#endif /* WL_SUBCOMPOSITOR_ERROR_ENUM */ +#endif #ifndef WL_SUBCOMPOSITOR_ERROR_ENUM_IS_VALID #define WL_SUBCOMPOSITOR_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_subcompositor - * Validate a wl_subcompositor error value. - * - * @return true on success, false on error. - * @ref wl_subcompositor_error - */ static inline bool wl_subcompositor_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -5923,49 +2171,11 @@ wl_subcompositor_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SUBCOMPOSITOR_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_subcompositor - * @struct wl_subcompositor_interface - */ struct wl_subcompositor_interface { - /** - * unbind from the subcompositor interface - * - * Informs the server that the client will not be using this - * protocol object anymore. This does not affect any other objects, - * wl_subsurface objects included. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * give a surface the role sub-surface - * - * Create a sub-surface interface for the given surface, and - * associate it with the given parent surface. This turns a plain - * wl_surface into a sub-surface. - * - * The to-be sub-surface must not already have another role, and it - * must not have an existing wl_subsurface object. Otherwise the - * bad_surface protocol error is raised. - * - * Adding sub-surfaces to a parent is a double-buffered operation - * on the parent (see wl_surface.commit). The effect of adding a - * sub-surface becomes visible on the next time the state of the - * parent surface is applied. - * - * The parent surface must not be one of the child surface's - * descendants, and the parent must be different from the child - * surface, otherwise the bad_parent protocol error is raised. - * - * This request modifies the behaviour of wl_surface.commit request - * on the sub-surface, see the documentation on wl_subsurface - * interface. - * @param id the new sub-surface object ID - * @param surface the surface to be turned into a sub-surface - * @param parent the parent surface - */ void (*get_subsurface)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -5974,34 +2184,18 @@ struct wl_subcompositor_interface { }; -/** - * @ingroup iface_wl_subcompositor - */ #define WL_SUBCOMPOSITOR_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subcompositor - */ #define WL_SUBCOMPOSITOR_GET_SUBSURFACE_SINCE_VERSION 1 #ifndef WL_SUBSURFACE_ERROR_ENUM #define WL_SUBSURFACE_ERROR_ENUM enum wl_subsurface_error { - /** - * wl_surface is not a sibling or the parent - */ WL_SUBSURFACE_ERROR_BAD_SURFACE = 0, }; -#endif /* WL_SUBSURFACE_ERROR_ENUM */ +#endif #ifndef WL_SUBSURFACE_ERROR_ENUM_IS_VALID #define WL_SUBSURFACE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_subsurface - * Validate a wl_subsurface error value. - * - * @return true on success, false on error. - * @ref wl_subsurface_error - */ static inline bool wl_subsurface_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -6011,149 +2205,44 @@ wl_subsurface_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SUBSURFACE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_subsurface - * @struct wl_subsurface_interface - */ struct wl_subsurface_interface { - /** - * remove sub-surface interface - * - * The sub-surface interface is removed from the wl_surface - * object that was turned into a sub-surface with a - * wl_subcompositor.get_subsurface request. The wl_surface's - * association to the parent is deleted. The wl_surface is unmapped - * immediately. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * reposition the sub-surface - * - * This sets the position of the sub-surface, relative to the - * parent surface. - * - * The sub-surface will be moved so that its origin (top left - * corner pixel) will be at the location x, y of the parent surface - * coordinate system. The coordinates are not restricted to the - * parent surface area. Negative values are allowed. - * - * The initial position is 0, 0. - * - * Position is double-buffered state on the parent surface, see - * wl_subsurface and wl_surface.commit for more information. - * @param x x coordinate in the parent surface - * @param y y coordinate in the parent surface - */ void (*set_position)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y); - /** - * restack the sub-surface - * - * This sub-surface is taken from the stack, and put back just - * above the reference surface, changing the z-order of the - * sub-surfaces. The reference surface must be one of the sibling - * surfaces, or the parent surface. Using any other surface, - * including this sub-surface, will cause a protocol error. - * - * A new sub-surface is initially added as the top-most in the - * stack of its siblings and parent. - * - * Z-order is double-buffered state on the parent surface, see - * wl_subsurface and wl_surface.commit for more information. - * @param sibling the reference surface - */ void (*place_above)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *sibling); - /** - * restack the sub-surface - * - * The sub-surface is placed just below the reference surface. - * - * See wl_subsurface.place_above. - * @param sibling the reference surface - */ void (*place_below)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *sibling); - /** - * set sub-surface to synchronized mode - * - * Change the commit behaviour of the sub-surface to synchronized - * mode. - * - * See wl_subsurface and wl_surface.commit for more information. - */ void (*set_sync)(struct wl_client *client, struct wl_resource *resource); - /** - * set sub-surface to desynchronized mode - * - * Change the commit behaviour of the sub-surface to - * desynchronized mode. - * - * See wl_subsurface and wl_surface.commit for more information. - */ void (*set_desync)(struct wl_client *client, struct wl_resource *resource); }; -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_POSITION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_PLACE_ABOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_PLACE_BELOW_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_SYNC_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_DESYNC_SINCE_VERSION 1 #ifndef WL_FIXES_ERROR_ENUM #define WL_FIXES_ERROR_ENUM -/** - * @ingroup iface_wl_fixes - * wl_fixes error values - * - * These errors can be emitted in response to wl_fixes requests. - */ enum wl_fixes_error { - /** - * unknown global or the global is not removed - */ WL_FIXES_ERROR_INVALID_ACK_REMOVE = 0, }; -#endif /* WL_FIXES_ERROR_ENUM */ +#endif #ifndef WL_FIXES_ERROR_ENUM_IS_VALID #define WL_FIXES_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_fixes - * Validate a wl_fixes error value. - * - * @return true on success, false on error. - * @ref wl_fixes_error - */ static inline bool wl_fixes_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -6163,68 +2252,14 @@ wl_fixes_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_FIXES_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_fixes - * @struct wl_fixes_interface - */ struct wl_fixes_interface { - /** - * destroys this object - * - * - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * destroy a wl_registry - * - * This request destroys a wl_registry object. - * - * The client should no longer use the wl_registry after making - * this request. - * - * The compositor will emit a wl_display.delete_id event with the - * object ID of the registry and will no longer emit any events on - * the registry. The client should re-use the object ID once it - * receives the wl_display.delete_id event. - * @param registry the registry to destroy - */ void (*destroy_registry)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *registry); - /** - * acknowledge global removal - * - * Acknowledge the removal of the specified global. - * - * If no global with the specified name exists or the global is not - * removed, the wl_fixes.invalid_ack_remove protocol error will be - * posted. - * - * Due to the Wayland protocol being asynchronous, the wl_global - * objects cannot be destroyed immediately. For example, if a - * wl_global is removed and a client attempts to bind that global - * around same time, it can result in a protocol error due to an - * unknown global name in the bind request. - * - * In order to avoid crashing clients, the compositor should remove - * the wl_global once it is guaranteed that no more bind requests - * will come. - * - * The wl_fixes.ack_global_remove() request is used to signal to - * the compositor that the client will not bind the given global - * anymore. After all clients acknowledge the removal of the - * global, the compositor can safely destroy it. - * - * The client must call the wl_fixes.ack_global_remove() request in - * response to a wl_registry.global_remove() event even if it did - * not bind the corresponding global. - * @param registry the registry object - * @param name unique name of the global - * @since 2 - */ void (*ack_global_remove)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *registry, @@ -6232,17 +2267,8 @@ struct wl_fixes_interface { }; -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_DESTROY_REGISTRY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_ACK_GLOBAL_REMOVE_SINCE_VERSION 2 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server.h b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server.h index 48fab1dda..6a45c5b58 100644 --- a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server.h +++ b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server.h @@ -23,15 +23,6 @@ * SOFTWARE. */ -/** \file - * - * \brief Include the server API, deprecations and protocol C API. - * - * \warning Use of this header file is discouraged. Prefer including - * wayland-server-core.h instead, which does not include the - * server protocol header and as such only defines the library - * API, excluding the deprecated API below. - */ #ifndef WAYLAND_SERVER_H #define WAYLAND_SERVER_H @@ -43,16 +34,6 @@ extern "C" { #endif -/* - * The user can set this macro to hide the wl_object, wl_resource and wl_buffer - * objects alongside the associated API. - * - * The structs were meant to be opaque, although we missed that in the early days. - * - * NOTE: the list of structs, functions, etc in this section MUST NEVER GROW. - * Otherwise we will break forward compatibility and applications that used to - * build fine will no longer be able to do so. - */ #ifndef WL_HIDE_DEPRECATED struct wl_object { diff --git a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-util.h b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-util.h index 98c72fded..d411ddd4f 100644 --- a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-util.h +++ b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-util.h @@ -23,10 +23,6 @@ * SOFTWARE. */ -/** \file wayland-util.h - * - * \brief Utility classes, functions, and macros. - */ #ifndef WAYLAND_UTIL_H #define WAYLAND_UTIL_H @@ -40,14 +36,12 @@ extern "C" { #endif -/** Visibility attribute */ #if defined(__GNUC__) && __GNUC__ >= 4 #define WL_EXPORT __attribute__ ((visibility("default"))) #else #define WL_EXPORT #endif -/** Deprecated attribute */ #if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || (defined(__cplusplus) && __cplusplus >= 201402L) #define WL_DEPRECATED [[deprecated]] #elif defined(__GNUC__) && __GNUC__ >= 4 @@ -56,14 +50,6 @@ extern "C" { #define WL_DEPRECATED #endif -/** - * Printf-style argument attribute - * - * \param x Ordinality of the format string argument - * \param y Ordinality of the argument to check against the format string - * - * \sa https://gcc.gnu.org/onlinedocs/gcc-3.2.1/gcc/Function-Attributes.html - */ #if defined(__GNUC__) && __GNUC__ >= 4 #define WL_PRINTF(x, y) __attribute__((__format__(__printf__, x, y))) #else @@ -76,405 +62,57 @@ extern "C" { #define WL_TYPEOF(expr) __typeof__(expr) #endif -/** \class wl_object - * - * \brief A protocol object. - * - * A `wl_object` is an opaque struct identifying the protocol object - * underlying a `wl_proxy` or `wl_resource`. - * - * \note Functions accessing a `wl_object` are not normally used by client code. - * Clients should normally use the higher level interface generated by the - * scanner to interact with compositor objects. - * - */ struct wl_object; -/** - * The maximum size of a protocol message. - * - * If a message size exceeds this value, the connection will be dropped. - * Servers will send an invalid_method error before disconnecting. - */ #define WL_MAX_MESSAGE_SIZE 4096 -/** - * Protocol message signature - * - * A wl_message describes the signature of an actual protocol message, such as a - * request or event, that adheres to the Wayland protocol wire format. The - * protocol implementation uses a wl_message within its demarshal machinery for - * decoding messages between a compositor and its clients. In a sense, a - * wl_message is to a protocol message like a class is to an object. - * - * The `name` of a wl_message is the name of the corresponding protocol message. - * - * The `signature` is an ordered list of symbols representing the data types - * of message arguments and, optionally, a protocol version and indicators for - * nullability. A leading integer in the `signature` indicates the _since_ - * version of the protocol message. A `?` preceding a data type symbol indicates - * that the following argument type is nullable. While it is a protocol violation - * to send messages with non-nullable arguments set to `NULL`, event handlers in - * clients might still get called with non-nullable object arguments set to - * `NULL`. This can happen when the client destroyed the object being used as - * argument on its side and an event referencing that object was sent before the - * server knew about its destruction. As this race cannot be prevented, clients - * should - as a general rule - program their event handlers such that they can - * handle object arguments declared non-nullable being `NULL` gracefully. - * - * When no arguments accompany a message, `signature` is an empty string. - * - * Symbols: - * - * * `i`: int - * * `u`: uint - * * `f`: fixed - * * `s`: string - * * `o`: object - * * `n`: new_id - * * `a`: array - * * `h`: fd - * * `?`: following argument (`o` or `s`) is nullable - * - * While demarshaling primitive arguments is straightforward, when demarshaling - * messages containing `object` or `new_id` arguments, the protocol - * implementation often must determine the type of the object. The `types` of a - * wl_message is an array of wl_interface references that correspond to `o` and - * `n` arguments in `signature`, with `NULL` placeholders for arguments with - * non-object types. - * - * Consider the protocol event wl_display `delete_id` that has a single `uint` - * argument. The wl_message is: - * - * \code - * { "delete_id", "u", [NULL] } - * \endcode - * - * Here, the message `name` is `"delete_id"`, the `signature` is `"u"`, and the - * argument `types` is `[NULL]`, indicating that the `uint` argument has no - * corresponding wl_interface since it is a primitive argument. - * - * In contrast, consider a `wl_foo` interface supporting protocol request `bar` - * that has existed since version 2, and has two arguments: a `uint` and an - * object of type `wl_baz_interface` that may be `NULL`. Such a `wl_message` - * might be: - * - * \code - * { "bar", "2u?o", [NULL, &wl_baz_interface] } - * \endcode - * - * Here, the message `name` is `"bar"`, and the `signature` is `"2u?o"`. Notice - * how the `2` indicates the protocol version, the `u` indicates the first - * argument type is `uint`, and the `?o` indicates that the second argument - * is an object that may be `NULL`. Lastly, the argument `types` array indicates - * that no wl_interface corresponds to the first argument, while the type - * `wl_baz_interface` corresponds to the second argument. - * - * \sa wl_argument - * \sa wl_interface - * \sa Wire Format - */ struct wl_message { - /** Message name */ const char *name; - /** Message signature */ const char *signature; - /** Object argument interfaces */ const struct wl_interface **types; }; -/** - * Protocol object interface - * - * A wl_interface describes the API of a protocol object defined in the Wayland - * protocol specification. The protocol implementation uses a wl_interface - * within its marshalling machinery for encoding client requests. - * - * The `name` of a wl_interface is the name of the corresponding protocol - * interface, and `version` represents the version of the interface. The members - * `method_count` and `event_count` represent the number of `methods` (requests) - * and `events` in the respective wl_message members. - * - * For example, consider a protocol interface `foo`, marked as version `1`, with - * two requests and one event. - * - * \code{.xml} - * - * - * - * - * - * \endcode - * - * Given two wl_message arrays `foo_requests` and `foo_events`, a wl_interface - * for `foo` might be: - * - * \code - * struct wl_interface foo_interface = { - * "foo", 1, - * 2, foo_requests, - * 1, foo_events - * }; - * \endcode - * - * \note The server side of the protocol may define interface implementation - * types that incorporate the term `interface` in their name. Take - * care to not confuse these server-side `struct`s with a wl_interface - * variable whose name also ends in `interface`. For example, while the - * server may define a type `struct wl_foo_interface`, the client may - * define a `struct wl_interface wl_foo_interface`. - * - * \sa wl_message - * \sa wl_proxy - * \sa Interfaces - * \sa Versioning - */ struct wl_interface { - /** Interface name */ const char *name; - /** Interface version */ int version; - /** Number of methods (requests) */ int method_count; - /** Method (request) signatures */ const struct wl_message *methods; - /** Number of events */ int event_count; - /** Event signatures */ const struct wl_message *events; }; -/** \class wl_list - * - * \brief Doubly-linked list - * - * On its own, an instance of `struct wl_list` represents the sentinel head of - * a doubly-linked list, and must be initialized using wl_list_init(). - * When empty, the list head's `next` and `prev` members point to the list head - * itself, otherwise `next` references the first element in the list, and `prev` - * refers to the last element in the list. - * - * Use the `struct wl_list` type to represent both the list head and the links - * between elements within the list. Use wl_list_empty() to determine if the - * list is empty in O(1). - * - * All elements in the list must be of the same type. The element type must have - * a `struct wl_list` member, often named `link` by convention. Prior to - * insertion, there is no need to initialize an element's `link` - invoking - * wl_list_init() on an individual list element's `struct wl_list` member is - * unnecessary if the very next operation is wl_list_insert(). However, a - * common idiom is to initialize an element's `link` prior to removal - ensure - * safety by invoking wl_list_init() before wl_list_remove(). - * - * Consider a list reference `struct wl_list foo_list`, an element type as - * `struct element`, and an element's link member as `struct wl_list link`. - * - * The following code initializes a list and adds three elements to it. - * - * \code - * struct wl_list foo_list; - * - * struct element { - * int foo; - * struct wl_list link; - * }; - * struct element e1, e2, e3; - * - * wl_list_init(&foo_list); - * wl_list_insert(&foo_list, &e1.link); // e1 is the first element - * wl_list_insert(&foo_list, &e2.link); // e2 is now the first element - * wl_list_insert(&e2.link, &e3.link); // insert e3 after e2 - * \endcode - * - * The list now looks like [e2, e3, e1]. - * - * The `wl_list` API provides some iterator macros. For example, to iterate - * a list in ascending order: - * - * \code - * struct element *e; - * wl_list_for_each(e, foo_list, link) { - * do_something_with_element(e); - * } - * \endcode - * - * See the documentation of each iterator for details. - * \sa http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/linux/list.h - */ struct wl_list { - /** Previous list element */ struct wl_list *prev; - /** Next list element */ struct wl_list *next; }; -/** - * Initializes the list. - * - * \param list List to initialize - * - * \memberof wl_list - */ void wl_list_init(struct wl_list *list); -/** - * Inserts an element into the list, after the element represented by \p list. - * When \p list is a reference to the list itself (the head), set the containing - * struct of \p elm as the first element in the list. - * - * \note If \p elm is already part of a list, inserting it again will lead to - * list corruption. - * - * \param list List element after which the new element is inserted - * \param elm Link of the containing struct to insert into the list - * - * \memberof wl_list - */ void wl_list_insert(struct wl_list *list, struct wl_list *elm); -/** - * Removes an element from the list. - * - * \note This operation leaves \p elm in an invalid state. - * - * \param elm Link of the containing struct to remove from the list - * - * \memberof wl_list - */ void wl_list_remove(struct wl_list *elm); -/** - * Determines the length of the list. - * - * \note This is an O(n) operation. - * - * \param list List whose length is to be determined - * - * \return Number of elements in the list - * - * \memberof wl_list - */ int wl_list_length(const struct wl_list *list); -/** - * Determines if the list is empty. - * - * \param list List whose emptiness is to be determined - * - * \return 1 if empty, or 0 if not empty - * - * \memberof wl_list - */ int wl_list_empty(const struct wl_list *list); -/** - * Inserts all of the elements of one list into another, after the element - * represented by \p list. - * - * \note This leaves \p other in an invalid state. - * - * \param list List element after which the other list elements will be inserted - * \param other List of elements to insert - * - * \memberof wl_list - */ void wl_list_insert_list(struct wl_list *list, struct wl_list *other); -/** - * Retrieves a pointer to a containing struct, given a member name. - * - * This macro allows "conversion" from a pointer to a member to its containing - * struct. This is useful if you have a contained item like a wl_list, - * wl_listener, or wl_signal, provided via a callback or other means, and would - * like to retrieve the struct that contains it. - * - * To demonstrate, the following example retrieves a pointer to - * `example_container` given only its `destroy_listener` member: - * - * \code - * struct example_container { - * struct wl_listener destroy_listener; - * // other members... - * }; - * - * void example_container_destroy(struct wl_listener *listener, void *data) - * { - * struct example_container *ctr; - * - * ctr = wl_container_of(listener, ctr, destroy_listener); - * // destroy ctr... - * } - * \endcode - * - * \note `sample` need not be a valid pointer. A null or uninitialised pointer - * is sufficient. - * - * \param ptr Valid pointer to the contained member - * \param sample Pointer to a struct whose type contains \p ptr - * \param member Named location of \p ptr within the \p sample type - * - * \return The container for the specified pointer - */ #define wl_container_of(ptr, sample, member) \ (WL_TYPEOF(sample))((char *)(ptr) - \ offsetof(WL_TYPEOF(*sample), member)) -/** - * Iterates over a list. - * - * This macro expresses a for-each iterator for wl_list. Given a list and - * wl_list link member name (often named `link` by convention), this macro - * assigns each element in the list to \p pos, which can then be referenced in - * a trailing code block. For example, given a wl_list of `struct message` - * elements: - * - * \code - * struct message { - * char *contents; - * wl_list link; - * }; - * - * struct wl_list *message_list; - * // Assume message_list now "contains" many messages - * - * struct message *m; - * wl_list_for_each(m, message_list, link) { - * do_something_with_message(m); - * } - * \endcode - * - * \param pos Cursor that each list element will be assigned to - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each(pos, head, member) \ for (pos = wl_container_of((head)->next, pos, member); \ &pos->member != (head); \ pos = wl_container_of(pos->member.next, pos, member)) -/** - * Iterates over a list, safe against removal of the list element. - * - * \note Only removal of the current element, \p pos, is safe. Removing - * any other element during traversal may lead to a loop malfunction. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param tmp Temporary pointer of the same type as \p pos - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_safe(pos, tmp, head, member) \ for (pos = wl_container_of((head)->next, pos, member), \ tmp = wl_container_of((pos)->member.next, tmp, member); \ @@ -482,37 +120,11 @@ wl_list_insert_list(struct wl_list *list, struct wl_list *other); pos = tmp, \ tmp = wl_container_of(pos->member.next, tmp, member)) -/** - * Iterates backwards over a list. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_reverse(pos, head, member) \ for (pos = wl_container_of((head)->prev, pos, member); \ &pos->member != (head); \ pos = wl_container_of(pos->member.prev, pos, member)) -/** - * Iterates backwards over a list, safe against removal of the list element. - * - * \note Only removal of the current element, \p pos, is safe. Removing - * any other element during traversal may lead to a loop malfunction. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param tmp Temporary pointer of the same type as \p pos - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_reverse_safe(pos, tmp, head, member) \ for (pos = wl_container_of((head)->prev, pos, member), \ tmp = wl_container_of((pos)->member.prev, tmp, member); \ @@ -520,244 +132,75 @@ wl_list_insert_list(struct wl_list *list, struct wl_list *other); pos = tmp, \ tmp = wl_container_of(pos->member.prev, tmp, member)) -/** - * \class wl_array - * - * Dynamic array - * - * A wl_array is a dynamic array that can only grow until released. It is - * intended for relatively small allocations whose size is variable or not known - * in advance. While construction of a wl_array does not require all elements to - * be of the same size, wl_array_for_each() does require all elements to have - * the same type and size. - * - */ struct wl_array { - /** Array size */ size_t size; - /** Allocated space */ size_t alloc; - /** Array data */ void *data; }; -/** - * Initializes the array. - * - * \param array Array to initialize - * - * \memberof wl_array - */ void wl_array_init(struct wl_array *array); -/** - * Releases the array data. - * - * \note Leaves the array in an invalid state. - * - * \param array Array whose data is to be released - * - * \memberof wl_array - */ void wl_array_release(struct wl_array *array); -/** - * Increases the size of the array by \p size bytes. - * - * \param array Array whose size is to be increased - * \param size Number of bytes to increase the size of the array by - * - * \return A pointer to the beginning of the newly appended space, or NULL when - * resizing fails. - * - * \memberof wl_array - */ void * wl_array_add(struct wl_array *array, size_t size); -/** - * Copies the contents of \p source to \p array. - * - * \param array Destination array to copy to - * \param source Source array to copy from - * - * \return 0 on success, or -1 on failure - * - * \memberof wl_array - */ int wl_array_copy(struct wl_array *array, struct wl_array *source); -/** - * Iterates over an array. - * - * This macro expresses a for-each iterator for wl_array. It assigns each - * element in the array to \p pos, which can then be referenced in a trailing - * code block. \p pos must be a pointer to the array element type, and all - * array elements must be of the same type and size. - * - * \param pos Cursor that each array element will be assigned to - * \param array Array to iterate over - * - * \relates wl_array - * \sa wl_list_for_each() - */ #define wl_array_for_each(pos, array) \ for (pos = (array)->data; \ (array)->size != 0 && \ (const char *) pos < ((const char *) (array)->data + (array)->size); \ (pos)++) -/** - * Fixed-point number - * - * A `wl_fixed_t` is a 24.8 signed fixed-point number with a sign bit, 23 bits - * of integer precision and 8 bits of decimal precision. Consider `wl_fixed_t` - * as an opaque struct with methods that facilitate conversion to and from - * `double` and `int` types. - */ typedef int32_t wl_fixed_t; -/** - * Converts a fixed-point number to a floating-point number. - * - * \param f Fixed-point number to convert - * - * \return Floating-point representation of the fixed-point argument - */ static inline double wl_fixed_to_double(wl_fixed_t f) { return f / 256.0; } -/** - * Converts a floating-point number to a fixed-point number. - * - * \param d Floating-point number to convert - * - * \return Fixed-point representation of the floating-point argument - */ static inline wl_fixed_t wl_fixed_from_double(double d) { return (wl_fixed_t) (round(d * 256.0)); } -/** - * Converts a fixed-point number to an integer. - * - * \param f Fixed-point number to convert - * - * \return Integer component of the fixed-point argument - */ static inline int wl_fixed_to_int(wl_fixed_t f) { return f / 256; } -/** - * Converts an integer to a fixed-point number. - * - * \param i Integer to convert - * - * \return Fixed-point representation of the integer argument - */ static inline wl_fixed_t wl_fixed_from_int(int i) { return i * 256; } -/** - * Protocol message argument data types - * - * This union represents all of the argument types in the Wayland protocol wire - * format. The protocol implementation uses wl_argument within its marshalling - * machinery for dispatching messages between a client and a compositor. - * - * \sa wl_message - * \sa wl_interface - * \sa Wire Format - */ union wl_argument { - int32_t i; /**< `int` */ - uint32_t u; /**< `uint` */ - wl_fixed_t f; /**< `fixed` */ - const char *s; /**< `string` */ - struct wl_object *o; /**< `object` */ - uint32_t n; /**< `new_id` */ - struct wl_array *a; /**< `array` */ - int32_t h; /**< `fd` */ + int32_t i; + uint32_t u; + wl_fixed_t f; + const char *s; + struct wl_object *o; + uint32_t n; + struct wl_array *a; + int32_t h; }; -/** - * Dispatcher function type alias - * - * A dispatcher is a function that handles the emitting of callbacks in client - * code. For programs directly using the C library, this is done by using - * libffi to call function pointers. When binding to languages other than C, - * dispatchers provide a way to abstract the function calling process to be - * friendlier to other function calling systems. - * - * A dispatcher takes five arguments: The first is the dispatcher-specific - * implementation associated with the target object. The second is the object - * upon which the callback is being invoked (either wl_proxy or wl_resource). - * The third and fourth arguments are the opcode and the wl_message - * corresponding to the callback. The final argument is an array of arguments - * received from the other process via the wire protocol. - * - * \param user_data Dispatcher-specific implementation data - * \param target Callback invocation target (wl_proxy or `wl_resource`) - * \param opcode Callback opcode - * \param msg Callback message signature - * \param args Array of received arguments - * - * \return 0 on success, or -1 on failure - */ typedef int (*wl_dispatcher_func_t)(const void *user_data, void *target, uint32_t opcode, const struct wl_message *msg, union wl_argument *args); -/** - * Log function type alias - * - * The C implementation of the Wayland protocol abstracts the details of - * logging. Users may customize the logging behavior, with a function conforming - * to the `wl_log_func_t` type, via `wl_log_set_handler_client` and - * `wl_log_set_handler_server`. - * - * A `wl_log_func_t` must conform to the expectations of `vprintf`, and - * expects two arguments: a string to write and a corresponding variable - * argument list. While the string to write may contain format specifiers and - * use values in the variable argument list, the behavior of any `wl_log_func_t` - * depends on the implementation. - * - * \note Take care to not confuse this with `wl_protocol_logger_func_t`, which - * is a specific server-side logger for requests and events. - * - * \param fmt String to write to the log, containing optional format - * specifiers - * \param args Variable argument list - * - * \sa wl_log_set_handler_client - * \sa wl_log_set_handler_server - */ typedef void (*wl_log_func_t)(const char *fmt, va_list args) WL_PRINTF(1, 0); -/** - * Return value of an iterator function - * - * \sa wl_client_for_each_resource_iterator_func_t - * \sa wl_client_for_each_resource - */ enum wl_iterator_result { - /** Stop the iteration */ WL_ITERATOR_STOP, - /** Continue the iteration */ WL_ITERATOR_CONTINUE }; diff --git a/app/src/main/cpp/waylandcomp/protocols/color-management-v1.xml b/app/src/main/cpp/waylandcomp/protocols/color-management-v1.xml index 7f8da78f1..ef02406fd 100644 --- a/app/src/main/cpp/waylandcomp/protocols/color-management-v1.xml +++ b/app/src/main/cpp/waylandcomp/protocols/color-management-v1.xml @@ -1481,7 +1481,6 @@ - diff --git a/app/src/main/cpp/waylandcomp/protocols/linux-dmabuf-v1.xml b/app/src/main/cpp/waylandcomp/protocols/linux-dmabuf-v1.xml index 733717545..a88a20ac4 100644 --- a/app/src/main/cpp/waylandcomp/protocols/linux-dmabuf-v1.xml +++ b/app/src/main/cpp/waylandcomp/protocols/linux-dmabuf-v1.xml @@ -158,7 +158,6 @@ summary="low 32 bits of layout modifier"/> - diff --git a/app/src/main/cpp/waylandcomp/protocols/presentation-time.xml b/app/src/main/cpp/waylandcomp/protocols/presentation-time.xml index c2431c500..c71c2fbe3 100644 --- a/app/src/main/cpp/waylandcomp/protocols/presentation-time.xml +++ b/app/src/main/cpp/waylandcomp/protocols/presentation-time.xml @@ -1,6 +1,5 @@ - Copyright © 2013-2014 Collabora, Ltd. @@ -28,7 +27,6 @@ - The main feature of this interface is accurate presentation timing feedback to ensure smooth video playback while maintaining @@ -41,7 +39,6 @@ the wl_surface.commit and provides feedback on the content update, particularly the final realized presentation time. - When the final realized presentation time is available, e.g. after a framebuffer flip completes, the requested diff --git a/app/src/main/cpp/waylandcomp/protocols/text-input-unstable-v3.xml b/app/src/main/cpp/waylandcomp/protocols/text-input-unstable-v3.xml index 01c1e2104..f75317ace 100644 --- a/app/src/main/cpp/waylandcomp/protocols/text-input-unstable-v3.xml +++ b/app/src/main/cpp/waylandcomp/protocols/text-input-unstable-v3.xml @@ -458,7 +458,6 @@ - diff --git a/app/src/main/cpp/waylandcomp/protocols/wlr-data-control-unstable-v1.xml b/app/src/main/cpp/waylandcomp/protocols/wlr-data-control-unstable-v1.xml index 567a09e52..2771e9e0b 100644 --- a/app/src/main/cpp/waylandcomp/protocols/wlr-data-control-unstable-v1.xml +++ b/app/src/main/cpp/waylandcomp/protocols/wlr-data-control-unstable-v1.xml @@ -142,7 +142,6 @@ - diff --git a/app/src/main/cpp/waylandcomp/protocols/xdg-shell.xml b/app/src/main/cpp/waylandcomp/protocols/xdg-shell.xml index 1df700146..85e45044e 100644 --- a/app/src/main/cpp/waylandcomp/protocols/xdg-shell.xml +++ b/app/src/main/cpp/waylandcomp/protocols/xdg-shell.xml @@ -366,7 +366,6 @@ - @@ -1115,7 +1114,6 @@ - @@ -1139,7 +1137,6 @@ - @@ -1294,7 +1291,6 @@ - diff --git a/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c b/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c index 4a981f1ee..6cf0c8037 100644 --- a/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c +++ b/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c @@ -1,5 +1,3 @@ -/* Zero-copy game frames: banner_ahb_v1 + the game's AHardwareBuffers on the sc_layer SurfaceControl. - * See ahb_swapchain.h and ZERO_COPY_SPIKE.md. */ #define _GNU_SOURCE #include "ahb_swapchain.h" #include "droiddeck_color.h" @@ -19,9 +17,8 @@ #include #include -#define MOD_QCOM_COMPRESSED 0x0500000000000001ULL /* DRM_FORMAT_MOD_QCOM_COMPRESSED (UBWC) */ +#define MOD_QCOM_COMPRESSED 0x0500000000000001ULL -/* linux/dma-buf.h (not in the NDK sysroot's older headers): sync_file export / import. */ struct droiddeck_dma_buf_sync_file { uint32_t flags; int32_t fd; }; #define DROIDDECK_DMA_BUF_SYNC_READ 1u #define DROIDDECK_DMA_BUF_IOCTL_EXPORT_SYNC_FILE _IOWR('b', 2, struct droiddeck_dma_buf_sync_file) @@ -29,47 +26,43 @@ struct droiddeck_dma_buf_sync_file { uint32_t flags; int32_t fd; }; extern volatile int g_zero_copy; -/* One per wl_buffer the game attached an AHardwareBuffer to. Lives while the wl_buffer exists or - * the buffer is still on the layer / held by SurfaceFlinger, whichever is longer. */ struct ahb_buf { - uint64_t id; /* the token sc_layer.c hands back */ - struct dmabuf_buffer *b; /* one reference held for the life of this record */ - AHardwareBuffer *ahb; /* our reference to the game's buffer */ + uint64_t id; + struct dmabuf_buffer *b; + AHardwareBuffer *ahb; int w, h; uint32_t stride, image_count; - uint32_t format; /* AHARDWAREBUFFER_FORMAT_* gralloc gave it (10-bit for HDR10) */ + uint32_t format; uint64_t modifier; struct wl_client *client; - struct wl_resource *resource; /* the wl_buffer; NULL once the client destroyed it */ + struct wl_resource *resource; struct wl_listener resource_destroy; - int on_layer; /* set on the SurfaceControl (or not yet released by SurfaceFlinger) */ - int release_pending; /* the surface let go of it while on the layer */ - int64_t deferred_ns; /* when it did (the perf line: how long the display kept it) */ - struct surface *surface; /* for the paced release; NULL = release at once */ - struct wl_event_source *fence_src; /* fallback: waiting the release fence in the event loop */ + int on_layer; + int release_pending; + int64_t deferred_ns; + struct surface *surface; + struct wl_event_source *fence_src; int fence_fd; struct wl_list link; }; static struct wl_list g_bufs; -static struct wl_list g_clients; /* bound banner_ahb_v1 resources (wl_resource links) */ +static struct wl_list g_clients; static uint64_t g_next_id = 1; static struct wl_event_loop *g_loop; static unsigned g_stat_zero_copy; static int g_export_failed_logged, g_import_failed_logged; -static int g_advertised; /* the global exists */ -static int g_mode_sent = -1; /* the mode clients were last told (-1 = none yet) */ -static struct surface *g_announced; /* "presenting X without a copy" said for this surface */ -static volatile int64_t g_last_zero_copy_ns; /* when a frame last went on the layer without a copy */ +static int g_advertised; +static int g_mode_sent = -1; +static struct surface *g_announced; +static volatile int64_t g_last_zero_copy_ns; -/* SurfaceFlinger's callback thread -> compositor thread: released tokens + their fences. */ struct released { uint64_t id; int fd; }; static pthread_mutex_t g_rel_lock = PTHREAD_MUTEX_INITIALIZER; static struct released *g_rel; static int g_rel_n, g_rel_cap; static int g_rel_pipe[2] = {-1, -1}; -/* Which swapchains were announced (one log line per swapchain, not per image). */ struct chain_seen { struct wl_client *client; int w, h; uint32_t image_count; uint64_t modifier; uint32_t format; }; static struct chain_seen g_last_chain; @@ -96,7 +89,6 @@ static void buf_free(struct ahb_buf *ab) { free(ab); } -/* Free once neither the client (wl_buffer) nor the display (layer) refers to it. */ static void maybe_free(struct ahb_buf *ab) { if (!ab->resource && !ab->on_layer && !ab->fence_src) buf_free(ab); } @@ -105,11 +97,10 @@ static void on_buffer_resource_destroyed(struct wl_listener *l, void *data) { struct ahb_buf *ab = wl_container_of(l, ab, resource_destroy); wl_list_remove(&ab->resource_destroy.link); ab->resource = NULL; - ab->release_pending = 0; /* nothing left to release to */ + ab->release_pending = 0; maybe_free(ab); } -/* ---- release: SurfaceFlinger is done with the buffer -> the game's next acquire waits, then wl_buffer.release */ static void send_deferred_release(struct ahb_buf *ab) { if (ab->release_pending && ab->resource) @@ -129,10 +120,6 @@ static int on_fence_readable(int fd, uint32_t mask, void *data) { return 0; } -/* Compositor thread. Puts the display's release fence into the dma-buf so the game's driver waits - * on it before rendering into the buffer again (Mesa exports every fence of the dma-buf as the - * acquire semaphore), then gives the buffer back. If the kernel refuses the import, the release - * itself waits for the fence instead. */ static void handle_released(uint64_t id, int fd) { struct ahb_buf *ab = find_buf(id); if (!ab) { if (fd >= 0) close(fd); return; } @@ -148,7 +135,6 @@ static void handle_released(uint64_t id, int fd) { droiddeck_log("layer", "zero-copy: DMA_BUF_IOCTL_IMPORT_SYNC_FILE failed (%s): releases wait for the display's fence here instead", strerror(errno)); } - /* Wait it out in the event loop (a sync_file is readable once signalled). */ struct pollfd p = {.fd = fd, .events = POLLIN}; if (poll(&p, 1, 0) > 0) { close(fd); @@ -156,7 +142,7 @@ static void handle_released(uint64_t id, int fd) { ab->fence_fd = fd; ab->fence_src = wl_event_loop_add_fd(g_loop, fd, WL_EVENT_READABLE, on_fence_readable, ab); if (!ab->fence_src) { close(fd); ab->fence_fd = -1; } - else return; /* released from on_fence_readable */ + else return; } } } @@ -179,7 +165,6 @@ static int on_release_pipe(int fd, uint32_t mask, void *data) { return 0; } -/* SurfaceFlinger's callback thread. */ void ahb_swapchain_layer_released(void *token, int release_fd) { pthread_mutex_lock(&g_rel_lock); if (g_rel_n == g_rel_cap) { @@ -193,7 +178,6 @@ void ahb_swapchain_layer_released(void *token, int release_fd) { if (g_rel_pipe[1] >= 0) { char c = 1; if (write(g_rel_pipe[1], &c, 1) < 0) {} } } -/* ---- present */ int ahb_swapchain_has_ahb(const struct dmabuf_buffer *b) { return b && *droiddeck_dmabuf_ahb_slot((struct dmabuf_buffer *)b) != NULL; @@ -203,8 +187,6 @@ int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_ struct ahb_buf *ab = b ? *droiddeck_dmabuf_ahb_slot(b) : NULL; if (!ab) return -1; int dmabuf_fd = droiddeck_dmabuf_fd(b); - /* Acquire fence: the game's render fence, which its driver put into the dma-buf before the - * commit (Mesa's implicit sync). The display waits on it, not the CPU. */ int acquire = -1; struct droiddeck_dma_buf_sync_file exp = {.flags = DROIDDECK_DMA_BUF_SYNC_READ, .fd = -1}; if (dmabuf_fd >= 0 && ioctl(dmabuf_fd, DROIDDECK_DMA_BUF_IOCTL_EXPORT_SYNC_FILE, &exp) == 0 && exp.fd >= 0) { @@ -215,14 +197,14 @@ int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_ droiddeck_log("layer", "zero-copy: DMA_BUF_IOCTL_EXPORT_SYNC_FILE failed (%s): waiting for each frame on the CPU instead", strerror(errno)); } - struct pollfd p = {.fd = dmabuf_fd, .events = POLLIN}; /* readable = the writers are done */ + struct pollfd p = {.fd = dmabuf_fd, .events = POLLIN}; int r; do { r = poll(&p, 1, 100); } while (r < 0 && errno == EINTR); } int r = sc_layer_present_ahb(ab->ahb, ab->w, ab->h, acquire, (void *)(uintptr_t)ab->id, scene_w, scene_h, s ? droiddeck_surface_color(s) : NULL, ab->format); if (r < 0) return -1; - if (r == 1) return 0; /* nothing of it on screen: not on the layer, nothing to release later */ + if (r == 1) return 0; if (!ab->on_layer) { ab->on_layer = 1; g_stat_zero_copy++; @@ -272,7 +254,6 @@ uint32_t ahb_swapchain_ahb_format(const struct dmabuf_buffer *b) { return ab ? ab->format : 0; } -/* ---- banner_ahb_v1 */ static void ahb_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } @@ -287,10 +268,8 @@ static void ahb_attach(struct wl_client *c, struct wl_resource *r, struct wl_res return; } void **slot = droiddeck_dmabuf_ahb_slot(b); - if (*slot) { close(sock); return; } /* once per buffer */ + if (*slot) { close(sock); return; } - /* The handle was written into the socket before the request was sent; a short wait is just - * insurance against a slow sender. */ struct pollfd p = {.fd = sock, .events = POLLIN}; int pr; do { pr = poll(&p, 1, 1000); } while (pr < 0 && errno == EINTR); @@ -357,10 +336,7 @@ static void bind_ahb(struct wl_client *c, void *data, uint32_t ver, uint32_t id) if (!r) { wl_client_post_no_memory(c); return; } wl_resource_set_implementation(r, &ahb_impl, NULL, on_client_resource_destroyed); wl_list_insert(&g_clients, wl_resource_get_link(r)); - /* The mode travels with the bind, so the client's registry roundtrip already has it when it - * decides about its first swapchain. */ if (ver >= BANNER_AHB_V1_MODE_SINCE_VERSION) banner_ahb_v1_send_mode(r, g_zero_copy ? 1u : 0u); - /* One line per program, not per surface-format query (each binds its own). */ if (last_named != c) { last_named = c; droiddeck_log("layer", "zero-copy: %s bound banner_ahb_v1 version %u (%s)", droiddeck_client_name(c), ver, @@ -386,7 +362,6 @@ void ahb_swapchain_set_mode(int on, int live) { told++; } } - /* The next chain and the next fullscreen frame get announced again. */ memset(&g_last_chain, 0, sizeof(g_last_chain)); g_announced = NULL; if (live) diff --git a/app/src/main/cpp/waylandcomp/src/ahb_swapchain.h b/app/src/main/cpp/waylandcomp/src/ahb_swapchain.h index 2c72a12b5..777448273 100644 --- a/app/src/main/cpp/waylandcomp/src/ahb_swapchain.h +++ b/app/src/main/cpp/waylandcomp/src/ahb_swapchain.h @@ -1,28 +1,5 @@ #ifndef AHB_SWAPCHAIN_H #define AHB_SWAPCHAIN_H -/* - * Zero-copy game frames (layer mode, DROIDDECK_WAYLAND_ZERO_COPY=1 at launch, the drawer's "Zero-copy - * presentation" switch live): the guest half of the design in ZERO_COPY_SPIKE.md. Our Wayland Turnip - * (banners-turnip-wayland, patches/wayland/banner_ahb_wsi.py) binds the private global banner_ahb_v1 - * (version 2), which this compositor advertises on every session where a display layer is possible, - * and follows its `mode` event: while the mode is on, each swapchain the game creates has its images - * allocated as gralloc AHardwareBuffers, shared as dma-bufs through zwp_linux_dmabuf_v1 as before (so - * the blit path still works) and handed to us once per wl_buffer over a socketpair - * (banner_ahb_v1.attach). When such a buffer is the one fullscreen frame, it goes straight onto the - * sc_layer SurfaceControl: SurfaceFlinger / the display scan out the game's own buffer, no copy - * anywhere. A mode flip (ahb_swapchain_set_mode, from the drawer through the host queue) is - * broadcast to every bound client; a client whose live swapchain was built for the other mode - * retires it (VK_ERROR_OUT_OF_DATE_KHR) so the program rebuilds it, and until then its buffers keep - * presenting as before: an attached AHardwareBuffer stays with its wl_buffer whatever the mode is. - * - * Fences stay implicit, in the dma-buf itself: Mesa imports the render fence into the dma-buf - * before it commits, we export it as the layer's acquire fence (DMA_BUF_IOCTL_EXPORT_SYNC_FILE), - * and import SurfaceFlinger's release fence back (DMA_BUF_IOCTL_IMPORT_SYNC_FILE) before sending - * wl_buffer.release, so Mesa's acquire (wsi_create_sync_for_dma_buf_wait) waits for the display. - * - * Compositor thread unless noted. Without a usable display layer (sc_layer_available() == 0) the - * global is not created and nothing here runs. - */ #include #include #include @@ -30,53 +7,28 @@ struct dmabuf_buffer; struct surface; -/* ---- compositor.c -> ahb_swapchain.c */ -/* Create the banner_ahb_v1 global (when a display layer is possible at all) and the release queue. */ void ahb_swapchain_init(struct wl_display *display); -/* Zero-copy on/off, live: sets g_zero_copy, tells every bound client (banner_ahb_v1.mode) and logs - * it (live = 1: a drawer switch mid-session; 0: the launch default). Compositor thread. */ void ahb_swapchain_set_mode(int on, int live); -/* Milliseconds since a game frame was last put on the layer without a copy (-1 = never). Any thread: - * the drawer's status line. */ int ahb_swapchain_last_frame_age_ms(void); -/* 1 if the buffer carries an AHardwareBuffer from the game. */ int ahb_swapchain_has_ahb(const struct dmabuf_buffer *b); -/* Show b (the topmost fullscreen frame, shown by surface s) on the layer without a copy. - * 0 = done (or the same frame is already up), -1 = unavailable (draw the old way). */ int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_w, int scene_h); -/* Surface s lets go of buffer b (its wl_buffer `buffer`, NULL if the client destroyed it); paced = - * the limiter's cadence applies. Returns 1 when the buffer is on the layer: the release is sent - * from here once SurfaceFlinger's release fence is known. 0 = the caller releases as usual. */ int ahb_swapchain_defer_release(struct dmabuf_buffer *b, struct wl_resource *buffer, struct surface *s, int paced); -/* s is being destroyed: forget it (its deferred releases still go out, unpaced). */ void ahb_swapchain_surface_gone(struct surface *s); -/* Zero-copy frames since the last call (the 10 s summary). */ unsigned ahb_swapchain_stats_take(void); -/* 1 when the banner_ahb_v1 global exists (a display layer is possible): part of the HDR gate. */ int ahb_swapchain_advertised(void); -/* The AHARDWAREBUFFER_FORMAT_* of the game's buffer behind b, 0 when it has none. */ uint32_t ahb_swapchain_ahb_format(const struct dmabuf_buffer *b); -/* ---- sc_layer.c -> ahb_swapchain.c (SurfaceFlinger's callback thread): the layer let go of the - * buffer behind `token`; release_fd (owned by the callee, -1 = none) signals when the display is - * done reading it. */ void ahb_swapchain_layer_released(void *token, int release_fd); -/* ---- ahb_swapchain.c -> compositor.c (hooks) */ struct dmabuf_buffer *droiddeck_dmabuf_from_resource(struct wl_resource *buffer); -int droiddeck_dmabuf_fd(const struct dmabuf_buffer *b); /* plane 0's dma-buf */ +int droiddeck_dmabuf_fd(const struct dmabuf_buffer *b); void droiddeck_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h); -void **droiddeck_dmabuf_ahb_slot(struct dmabuf_buffer *b); /* this module's per-buffer state */ +void **droiddeck_dmabuf_ahb_slot(struct dmabuf_buffer *b); void droiddeck_dmabuf_ref(struct dmabuf_buffer *b); void droiddeck_dmabuf_unref(struct dmabuf_buffer *b); -/* Give a wl_buffer back to its client now (paced = 0) or on the FPS limiter's cadence (s != NULL). - * since_ns = when the compositor let go of it (CLOCK_MONOTONIC; the perf line's release latency). */ void droiddeck_release_buffer(struct surface *s, struct wl_resource *buffer, int paced, int64_t since_ns); -/* Redraw the scene on the next tick (the present path may have changed). */ void droiddeck_request_redraw(void); -/* "" (program) of the window a surface belongs to, for the log. */ void droiddeck_surface_describe(const struct surface *s, char *out, size_t size); -/* The surface's current image description (droiddeck_color.h), NULL = none / HDR gate closed. */ struct droiddeck_color; const struct droiddeck_color *droiddeck_surface_color(const struct surface *s); diff --git a/app/src/main/cpp/waylandcomp/src/blend.frag b/app/src/main/cpp/waylandcomp/src/blend.frag index 88739de25..54e1ac495 100644 --- a/app/src/main/cpp/waylandcomp/src/blend.frag +++ b/app/src/main/cpp/waylandcomp/src/blend.frag @@ -1,5 +1,4 @@ #version 450 -// Wayland buffers carry premultiplied alpha; the pipeline blends ONE, ONE_MINUS_SRC_ALPHA. layout(binding = 0) uniform sampler2D Surface; layout(location = 0) in vec2 uv; layout(location = 0) out vec4 outColor; diff --git a/app/src/main/cpp/waylandcomp/src/blend.vert b/app/src/main/cpp/waylandcomp/src/blend.vert index 5c87cc352..660082100 100644 --- a/app/src/main/cpp/waylandcomp/src/blend.vert +++ b/app/src/main/cpp/waylandcomp/src/blend.vert @@ -1,5 +1,4 @@ #version 450 -// One surface as a quad: dst = the target rectangle in NDC, src = the buffer rectangle in UV. layout(push_constant) uniform PC { vec4 dst; vec4 src; } pc; layout(location = 0) out vec2 uv; diff --git a/app/src/main/cpp/waylandcomp/src/blend_frag.h b/app/src/main/cpp/waylandcomp/src/blend_frag.h index 9d2af54a3..d4409cefa 100644 --- a/app/src/main/cpp/waylandcomp/src/blend_frag.h +++ b/app/src/main/cpp/waylandcomp/src/blend_frag.h @@ -1,4 +1,3 @@ - // 1115.1.0 #pragma once const uint32_t blend_frag_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000014,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/waylandcomp/src/blend_pass.c b/app/src/main/cpp/waylandcomp/src/blend_pass.c index 842287b07..9a4d48d0b 100644 --- a/app/src/main/cpp/waylandcomp/src/blend_pass.c +++ b/app/src/main/cpp/waylandcomp/src/blend_pass.c @@ -1,19 +1,16 @@ -/* Alpha composition: one surface drawn source-over onto the scene - see blend_pass.h. Compositor thread. */ #define _POSIX_C_SOURCE 200809L #include "blend_pass.h" #include "vk_loader.h" -#include "vk_present.h" /* droiddeck_log, vkp_gpu_name */ +#include "vk_present.h" #include <string.h> -/* SPIR-V, pre-compiled (glslangValidator -V blend.vert --vn blend_vert_code, the same for blend.frag) - * and committed next to the GLSL: the NDK build compiles no shaders. */ #include "blend_vert.h" #include "blend_frag.h" struct pc_blend { float dst[4]; float src[4]; }; static VkDevice g_dev; -static int g_ready; /* 0 = not built, 1 = ok, -1 = unavailable (said once) */ +static int g_ready; static VkSampler g_sampler; static VkDescriptorSetLayout g_dsl; static VkPipelineLayout g_pl; @@ -21,8 +18,6 @@ static VkDescriptorPool g_dpool; static VkShaderModule g_vert, g_frag; static unsigned g_frame = 1; -/* One render pass + pipeline per target format: the screen swapchain's, the scene image's, the HDR - * composition's. */ #define NFMT 4 static struct { VkFormat fmt; VkRenderPass rp; VkPipeline pipe; } g_out[NFMT]; @@ -60,8 +55,6 @@ static VkShaderModule shader(const uint32_t *code, size_t size) { return m; } -/* The target keeps what the blits put there (LOAD) and stays in TRANSFER_DST_OPTIMAL for the blits - * that follow. The dependencies order the pass between them. */ static VkRenderPass make_rp(VkFormat fmt) { VkAttachmentDescription att = {.format = fmt, .samples = VK_SAMPLE_COUNT_1_BIT, .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, @@ -111,7 +104,6 @@ static VkPipeline make_pipe(VkRenderPass rp) { .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, .lineWidth = 1.0f}; VkPipelineMultisampleStateCreateInfo ms = {.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT}; - /* The target's alpha is left alone: the screen is opaque whatever is drawn onto it. */ VkPipelineColorBlendAttachmentState ba = { .blendEnable = VK_TRUE, .srcColorBlendFactor = VK_BLEND_FACTOR_ONE, .dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, .colorBlendOp = VK_BLEND_OP_ADD, @@ -201,8 +193,6 @@ static VkImageView make_view(VkImage img, VkFormat fmt) { return v; } -/* The slot to recycle: a free one, else the one unused for longest - never one this command buffer - * already refers to. -1 = every slot is in this frame. */ #define OLDEST(arr, n, out) do { \ unsigned age_ = 0; \ (out) = -1; \ diff --git a/app/src/main/cpp/waylandcomp/src/blend_pass.h b/app/src/main/cpp/waylandcomp/src/blend_pass.h index 2faade252..d6f5a8fff 100644 --- a/app/src/main/cpp/waylandcomp/src/blend_pass.h +++ b/app/src/main/cpp/waylandcomp/src/blend_pass.h @@ -1,27 +1,12 @@ #ifndef BLEND_PASS_H #define BLEND_PASS_H -/* - * Alpha composition. The scene is composed with blits, which replace what is under them; a surface - * with translucent pixels (gamescope's overlay and notification planes over a game, a toolkit's - * shadowed popup) is drawn through this pass instead, source-over with premultiplied alpha. - * - * Compositor thread. Objects are built on the first use; if the driver refuses them the pass is - * unavailable for the session (logged once) and the caller keeps blitting. - */ #define VK_USE_PLATFORM_ANDROID_KHR #include <vulkan/vulkan.h> -/* Called from dev_init (device up). */ void blendp_bind_device(VkDevice dev); -/* A source or target image is about to be destroyed: drop what is cached for it. Frames are fenced, - * so nothing submitted still uses it. */ void blendp_forget_image(VkImage img); -/* A new command buffer is being recorded: what the previous one used may be recycled again. */ void blendp_begin_frame(void); -/* Record one surface over `dst`. `dst` is in TRANSFER_DST_OPTIMAL before and after. `src` was created - * with SAMPLED usage and is in GENERAL (src_general) or TRANSFER_SRC_OPTIMAL, and is left there. - * `blit` is the draw as the blit path maps it. 0 = recorded, -1 = unavailable (nothing recorded). */ int blendp_draw(VkCommandBuffer cmd, VkImage src, VkFormat src_fmt, int src_w, int src_h, int src_general, VkImage dst, VkFormat dst_fmt, int dst_w, int dst_h, const VkImageBlit *blit); diff --git a/app/src/main/cpp/waylandcomp/src/blend_vert.h b/app/src/main/cpp/waylandcomp/src/blend_vert.h index f402c6a92..a296cb0c0 100644 --- a/app/src/main/cpp/waylandcomp/src/blend_vert.h +++ b/app/src/main/cpp/waylandcomp/src/blend_vert.h @@ -1,4 +1,3 @@ - // 1115.1.0 #pragma once const uint32_t blend_vert_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000003a,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/waylandcomp/src/cardboard.c b/app/src/main/cpp/waylandcomp/src/cardboard.c index ffb1918b0..8031d61d7 100644 --- a/app/src/main/cpp/waylandcomp/src/cardboard.c +++ b/app/src/main/cpp/waylandcomp/src/cardboard.c @@ -1,8 +1,3 @@ -/* Cardboard pass - see cardboard.h. Compositor thread unless noted. - * - * The shader is cardboard.frag, pre-compiled like the rest of the compositor's SPIR-V: - * glslangValidator -V --vn cardboard_frag_code -o cardboard_frag.h cardboard.frag - */ #define _POSIX_C_SOURCE 200809L #include "cardboard.h" #include <math.h> @@ -20,15 +15,14 @@ #ifndef CARDBOARD_HOST_TEST #include "vk_loader.h" -#include "vk_present.h" /* droiddeck_log */ -/* upscale_vert.h defines its array non-static, and effects_chain.c includes it too: a private name here. */ +#include "vk_present.h" + #define upscale_vert_code cardboard_upscale_vert_code #include "upscale_vert.h" #undef upscale_vert_code #include "cardboard_frag.h" #endif -/* ------------------------------------------------------------------ pure helpers */ static float clampf(float v, float lo, float hi) { return v < lo ? lo : v > hi ? hi : v; } @@ -47,29 +41,25 @@ void cardboard_clamp(struct cardboard_params *p) { void cardboard_push_constants(const struct cardboard_params *p, const float pose[9], int src_w, int src_h, int out_w, int out_h, float out[24]) { - /* rot0..rot2: the pose's rows, each with one scalar riding in w. */ for (int r = 0; r < 3; r++) for (int c = 0; c < 3; c++) out[r * 4 + c] = pose[r * 3 + c]; out[3] = p->ipd_m * 0.5f; out[7] = p->screen_dist_m; out[11] = p->screen_width_m * 0.5f; - /* lens */ out[12] = p->k1; out[13] = p->k2; out[14] = tanf(p->fov_deg * 0.5f * (float)M_PI / 180.0f); out[15] = p->chroma; - /* view */ out[16] = p->lens_inset; out[17] = (src_w > 0 && src_h > 0) ? (float)src_w / (float)src_h : 16.0f / 9.0f; - if (p->source == CARDBOARD_SOURCE_SBS) out[17] *= 0.5f; /* one eye's half */ + if (p->source == CARDBOARD_SOURCE_SBS) out[17] *= 0.5f; out[18] = (float)p->source; out[19] = (out_w > 0 && out_h > 0) ? ((float)out_w * 0.5f) / (float)out_h : 1.0f; - out[20] = out[21] = out[22] = out[23] = 0.0f; /* reserved */ + out[20] = out[21] = out[22] = out[23] = 0.0f; } #ifndef CARDBOARD_HOST_TEST -/* ------------------------------------------------------------------ settings + pose */ static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; static struct cardboard_params g_set = { @@ -78,8 +68,8 @@ static struct cardboard_params g_set = { static float g_pose[9] = {1, 0, 0, 0, 1, 0, 0, 0, 1}; static volatile int g_changed = 1; -static int g_live; /* the last pose came from a sensor */ -static void shm_publish(int live); /* below: the guest's copy of the pose */ +static int g_live; +static void shm_publish(int live); void vkp_cardboard_set(const struct cardboard_params *p) { struct cardboard_params c = *p; @@ -96,9 +86,8 @@ void vkp_cardboard_set(const struct cardboard_params *p) { c.ipd_m * 1000.0f, c.screen_width_m, c.screen_dist_m, c.k1, c.k2, c.fov_deg, c.lens_inset, c.chroma); } -/* The guest's copy of the pose (headpose_shm.h), mapped once a path is known. */ static struct droiddeck_headpose *g_shm; -static int g_eye_w, g_eye_h; /* the last eye viewport drawn, for the guest */ +static int g_eye_w, g_eye_h; void vkp_cardboard_set_pose_file(const char *path) { pthread_mutex_lock(&g_lock); @@ -120,7 +109,6 @@ void vkp_cardboard_set_pose_file(const char *path) { pthread_mutex_unlock(&g_lock); } -/* Under g_lock. */ static void shm_publish(int live) { if (!g_shm) return; struct timespec ts; @@ -174,15 +162,14 @@ int vkp_cardboard_pose_changed(void) { return 1; } -/* ------------------------------------------------------------------ Vulkan objects */ #define CB_FORMAT VK_FORMAT_R8G8B8A8_UNORM -#define CB_PC_SIZE (4 * 4 + 24 * 4) /* ndc + the 24 floats: 112 bytes, under the 128 guaranteed */ -#define CB_MAX_SOURCES 8 /* source views per frame: the real frame + generated ones */ +#define CB_PC_SIZE (4 * 4 + 24 * 4) +#define CB_MAX_SOURCES 8 static VkDevice g_dev; static VkPhysicalDeviceMemoryProperties g_mp; -static int g_ready; /* 0 = not built, 1 = ok, -1 = failed (said once) */ +static int g_ready; static VkRenderPass g_rp; static VkDescriptorSetLayout g_dsl; static VkPipelineLayout g_pl; @@ -234,9 +221,6 @@ static VkImageView make_view(VkImage img) { return v; } -/* The panel-sized output: rendered into, then blitted 1:1 to the swapchain. Kept uncompressed - * (mutable format) like the effect chain's targets: UBWC render-then-blit targets corrupt on - * Adreno 740. */ static int target_ensure(int w, int h) { if (g_t.img && g_t.w == w && g_t.h == h) return 0; target_destroy(); @@ -295,7 +279,6 @@ static int objects_ensure(void) { if (g_ready) return g_ready == 1 ? 0 : -1; if (!g_dev || !g_vk.CreateGraphicsPipelines) goto fail; { - /* One colour target, overwritten whole, left GENERAL for the 1:1 blit that follows. */ VkAttachmentDescription att = {.format = CB_FORMAT, .samples = VK_SAMPLE_COUNT_1_BIT, .loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, @@ -444,4 +427,4 @@ VkImage vkp_cardboard_run(VkCommandBuffer cmd, VkImage src, int src_w, int src_h return g_t.img; } -#endif /* CARDBOARD_HOST_TEST */ +#endif diff --git a/app/src/main/cpp/waylandcomp/src/cardboard.frag b/app/src/main/cpp/waylandcomp/src/cardboard.frag index 55167fc88..4dbc67fc2 100644 --- a/app/src/main/cpp/waylandcomp/src/cardboard.frag +++ b/app/src/main/cpp/waylandcomp/src/cardboard.frag @@ -1,55 +1,36 @@ #version 450 -// Cardboard pass (cardboard.c): the finished frame -> two eye viewports, side by side, each -// barrel-distorted for a phone-in-a-viewer lens. Runs on upscale.vert's full quad, so uv covers -// the whole panel with (0,0) at the top left. -// -// Source modes: -// 0 = virtual screen: the frame is a flat screen hanging in front of the viewer. Each eye casts -// a ray through its lens, rotated by the head pose, from its own position (half the IPD to -// either side), so the screen has real stereo depth and stays put while the head turns. -// 1 = side by side: the frame already holds a left and a right eye image (SteamVR, an SBS 3D -// game or video); each eye samples its own half. The head pose belongs to whoever rendered -// it, so none is applied here. -// -// Regenerate cardboard_frag.h after editing (glslangValidator, see cardboard.c). layout(binding = 0) uniform sampler2D Source; layout(location = 0) in vec2 uv; layout(location = 0) out vec4 outColor; layout(push_constant) uniform PC { - vec4 ndc; // upscale.vert's quad - vec4 rot0; // xyz: head -> world rotation, row 0; w: half the IPD (m) - vec4 rot1; // xyz: row 1; w: distance to the virtual screen (m) - vec4 rot2; // xyz: row 2; w: half the virtual screen's width (m) - vec4 lens; // x: k1, y: k2, z: tan of half the eye's vertical field of view, w: chromatic spread - vec4 view; // x: lens centre inset (fraction of an eye viewport), y: source aspect w/h, - // z: source mode, w: eye viewport aspect w/h + vec4 ndc; + vec4 rot0; + vec4 rot1; + vec4 rot2; + vec4 lens; + vec4 view; + } pc; const vec3 BACKDROP = vec3(0.015, 0.015, 0.02); -// Lens: a point on the panel at tangent-space offset t (from the lens centre) shows the ray that -// leaves the eye at t * (1 + k1 r^2 + k2 r^4). Sampling outwards like this draws the picture -// barrel-shaped; the viewer's lens pincushions it back. vec2 undistort(vec2 t, float scale) { float r2 = dot(t, t); return t * scale * (1.0 + pc.lens.x * r2 + pc.lens.y * r2 * r2); } -// What one eye sees along tangent direction tan (x right, y down): colour, or the backdrop. vec3 eyeSample(vec2 tan, float eye) { if (pc.view.z > 0.5) { - // Side by side: this eye's half of the source covers the eye's field of view. float tanH = pc.lens.z; vec2 s = vec2(tan.x / (tanH * pc.view.w), tan.y / tanH) * 0.5 + 0.5; if (any(lessThan(s, vec2(0.0))) || any(greaterThan(s, vec2(1.0)))) return vec3(0.0); return texture(Source, vec2((s.x + eye) * 0.5, s.y)).rgb; } - // mat3() takes columns; the push constants carry rows. mat3 R = transpose(mat3(pc.rot0.xyz, pc.rot1.xyz, pc.rot2.xyz)); vec3 d = R * normalize(vec3(tan.x, -tan.y, -1.0)); vec3 o = R * vec3((eye < 0.5 ? -1.0 : 1.0) * pc.rot0.w, 0.0, 0.0); - if (d.z > -1e-4) return BACKDROP; // looking away from the screen + if (d.z > -1e-4) return BACKDROP; float t = (-pc.rot1.w - o.z) / d.z; vec3 hit = o + d * t; float halfW = pc.rot2.w, halfH = halfW / pc.view.y; @@ -61,7 +42,6 @@ vec3 eyeSample(vec2 tan, float eye) { void main() { float eye = uv.x < 0.5 ? 0.0 : 1.0; vec2 local = vec2(uv.x * 2.0 - eye, uv.y); - // Lens centres sit inset towards the nose by view.x of a viewport. float cx = 0.5 + (eye < 0.5 ? pc.view.x : -pc.view.x); vec2 p = local - vec2(cx, 0.5); float tanH = pc.lens.z; @@ -69,14 +49,12 @@ void main() { vec3 c; if (pc.lens.w > 0.0) { - // Chromatic aberration: the lens bends red least and blue most; pre-spread to match. c.r = eyeSample(undistort(t, 1.0 - pc.lens.w), eye).r; c.g = eyeSample(undistort(t, 1.0), eye).g; c.b = eyeSample(undistort(t, 1.0 + pc.lens.w), eye).b; } else { c = eyeSample(undistort(t, 1.0), eye); } - // Fade out towards each viewport's edge so the lens rim never shows a hard cut. vec2 e = min(local, 1.0 - local); float fade = smoothstep(0.0, 0.03, min(e.x * pc.view.w, e.y)); outColor = vec4(c * fade, 1.0); diff --git a/app/src/main/cpp/waylandcomp/src/cardboard.h b/app/src/main/cpp/waylandcomp/src/cardboard.h index ce39d8645..3fd99e6b7 100644 --- a/app/src/main/cpp/waylandcomp/src/cardboard.h +++ b/app/src/main/cpp/waylandcomp/src/cardboard.h @@ -1,20 +1,5 @@ #ifndef CARDBOARD_H #define CARDBOARD_H -/* - * Cardboard mode: the last stage of the compositor pass. The finished frame (after the effects and - * frame generation) is drawn twice, once per eye, side by side on the panel, barrel-distorted for a - * phone-in-a-viewer lens (cardboard.frag). The frame either hangs as a flat virtual screen in front - * of the viewer, world-locked by the phone's rotation sensor (3DoF), or is already a side-by-side - * stereo pair (SteamVR, SBS 3D content) that only gets the lens treatment. - * - * Present-pass order with Cardboard on (vk_present.c, render_impl): - * scene -> effects -> frame generation -> vkp_cardboard_run (whole panel) -> 1:1 blit -> present - * The Fullscreen Mode mapping does not apply: the pass fills the output itself. - * - * Settings and the head pose come from any thread (JNI, the sensor thread); the compositor thread - * snapshots them per frame. A new pose redraws even a static scene (vkp_cardboard_pose_changed), so - * the virtual screen stays put while the head turns. - */ #define VK_USE_PLATFORM_ANDROID_KHR #include <vulkan/vulkan.h> @@ -22,41 +7,27 @@ enum { CARDBOARD_SOURCE_SCREEN = 0, CARDBOARD_SOURCE_SBS = 1 }; struct cardboard_params { int enabled; - int source; /* CARDBOARD_SOURCE_* */ - float ipd_m; /* distance between the eyes, metres */ - float screen_dist_m; /* virtual screen distance, metres */ - float screen_width_m;/* virtual screen width, metres */ - float k1, k2; /* lens distortion coefficients */ - float fov_deg; /* vertical field of view of one eye before distortion */ - float lens_inset; /* lens centre inset towards the nose, fraction of an eye viewport */ - float chroma; /* chromatic aberration pre-correction (0 = off) */ + int source; + float ipd_m; + float screen_dist_m; + float screen_width_m; + float k1, k2; + float fov_deg; + float lens_inset; + float chroma; }; -/* Any thread. Values are clamped to sane ranges. */ void vkp_cardboard_set(const struct cardboard_params *p); -/* Any thread: the head -> world rotation, row-major 3x3. Identity = looking straight at the screen. - * live = a sensor produced it. Also published to the guest (headpose_shm.h) once a file is set. */ void vkp_cardboard_set_pose(const float m[9], int live); -/* Any thread, once: where the guest's copy of the pose lives (<files>/.wayland-rt/droiddeck-headpose). */ void vkp_cardboard_set_pose_file(const char *path); -/* Compositor thread. */ void vkp_cardboard_bind_device(VkDevice dev, const VkPhysicalDeviceMemoryProperties *memprops); -/* 1 while Cardboard mode is on (the frame must take the compositor pass). */ int vkp_cardboard_active(void); -/* 1 when the settings or the pose changed since the last call: the caller redraws. */ int vkp_cardboard_pose_changed(void); -/* Called once per frame before any vkp_cardboard_run: releases the previous frame's source views - * (that frame is fenced by now). */ void vkp_cardboard_begin_frame(void); -/* Records the pass into cmd: src (src_w x src_h, R8G8B8A8, in GENERAL) -> a panel-sized image - * (out_w x out_h), returned in GENERAL. VK_NULL_HANDLE when the pass is unavailable (the caller - * presents the frame the usual way). */ VkImage vkp_cardboard_run(VkCommandBuffer cmd, VkImage src, int src_w, int src_h, int out_w, int out_h); -/* Pure helpers, shared with the host test (tools/tests). */ void cardboard_clamp(struct cardboard_params *p); -/* The 24 floats after the quad's ndc in the shader's push constants. */ void cardboard_push_constants(const struct cardboard_params *p, const float pose[9], int src_w, int src_h, int out_w, int out_h, float out[24]); diff --git a/app/src/main/cpp/waylandcomp/src/cardboard_frag.h b/app/src/main/cpp/waylandcomp/src/cardboard_frag.h index 7b60cd1f0..76f72467a 100644 --- a/app/src/main/cpp/waylandcomp/src/cardboard_frag.h +++ b/app/src/main/cpp/waylandcomp/src/cardboard_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t cardboard_frag_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000016d,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index 3d0794667..68911fc20 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -1,23 +1,3 @@ -/* - * droiddeck-wayland - the embedded compositor of the Wayland display path. - * - * Clients are the Wine processes of a container (winewayland.drv) and, through them, - * Mesa's Vulkan WSI. Each process is its own client, so a Windows virtual desktop is - * described with banner_desktop_v1: the desktop owner marks one surface as the desktop, - * and every process reports its top-level windows' desktop positions and the Windows - * stacking order. - * - * The scene is the desktop surface at the bottom, then every mapped xdg_toplevel at its - * reported position in stacking order, each with its subsurface tree (a game's Vulkan - * swapchain is a subsurface of its window) and wp_viewport crop/scale applied. It is - * redrawn once per event-loop pass after any commit or layout change, stretched to the - * Android surface. - * - * Input goes to the desktop surface in desktop coordinates: the Wine server routes it to - * the window under the pointer (or the focus window) in whichever process owns it, the - * same as winex11's virtual desktop. Without a desktop (a client that doesn't speak - * banner_desktop_v1) the topmost window under the pointer gets it. - */ #define _GNU_SOURCE 1 #define _POSIX_C_SOURCE 200809L #include <stdio.h> @@ -58,19 +38,11 @@ #define WLOGI(...) __android_log_print(ANDROID_LOG_INFO, "DroidDeckWayland", __VA_ARGS__) #define WLOGE(...) __android_log_print(ANDROID_LOG_ERROR, "DroidDeckWayland", __VA_ARGS__) -/* ------------------------------------------------------------------ session log - * One readable, time-stamped file per Wayland session in Download/Wayland-logs, also - * mirrored to logcat: which programs connect, the desktop, windows opening and closing, - * Vulkan frames arriving, a frame-rate summary every 10 seconds, and errors. */ #define SESSION_LOG_DIR "/storage/emulated/0/Download/Wayland-logs" static FILE *g_log; -/* Lines logged before the session file exists are kept here and written into it, in order, the - * moment it opens - the compositor runs on its own thread, so anything the app reports as that - * thread starts (the display's HDR capability, for one) would otherwise only reach logcat. The - * lock also serialises the file writes, which now come from the app's threads too. */ #define PRELOG_MAX 32 static pthread_mutex_t g_log_lock = PTHREAD_MUTEX_INITIALIZER; static char *g_prelog[PRELOG_MAX]; @@ -104,8 +76,6 @@ static void open_session_log(void) { localtime_r(&now, &tm); strftime(stamp, sizeof(stamp), "%Y-%m-%d_%H-%M-%S", &tm); - /* The app names the file when it has a session folder to put it in, so one session's logs sit - * together; without that we keep our own dated file. The folder is made on the Java side. */ const char *given = getenv("BL_WAYLAND_LOG"); if (given && *given) { snprintf(path, sizeof(path), "%s", given); @@ -131,8 +101,6 @@ static void open_session_log(void) { "Time Area Event\n" "------------ -------- -----------------------------------------------------\n", stamp, path); - /* Publish the file and flush anything logged before it existed, in one critical section, so a - * line from another thread can never land ahead of the header or be dropped between the two. */ pthread_mutex_lock(&g_log_lock); for (int i = 0; i < g_prelog_n; i++) { fputs(g_prelog[i], f); free(g_prelog[i]); g_prelog[i] = NULL; } g_prelog_n = 0; @@ -141,7 +109,6 @@ static void open_session_log(void) { WLOGI("session log: %s", path); } -/* At most ~10 lines a second for chatty events (window moves), so a drag can't flood logcat. */ static int log_budget(void) { static struct timespec window; static int used; @@ -152,13 +119,12 @@ static int log_budget(void) { } #ifndef BTN_LEFT -#define BTN_LEFT 0x110 /* linux/input-event-codes.h */ +#define BTN_LEFT 0x110 #endif #ifndef BTN_RIGHT #define BTN_RIGHT 0x111 #endif -/* Java sends pointer coordinates in this space, covering the whole output surface. */ #define INPUT_SPACE_W 1920 #define INPUT_SPACE_H 1080 @@ -180,7 +146,7 @@ static const char *client_name(struct wl_client *client) { static void on_client_destroyed(struct wl_listener *l, void *data) { struct client_info *ci = wl_container_of(l, ci, destroy), **pp; droiddeck_log("program", "disconnected: %s (pid %d)", ci->name, (int)ci->pid); - droiddeck_color_client_gone(ci->client); /* HDR summary for a program that presented HDR (gate open only) */ + droiddeck_color_client_gone(ci->client); for (pp = &g_clients; *pp; pp = &(*pp)->next) if (*pp == ci) { *pp = ci->next; break; } free(ci); @@ -202,7 +168,7 @@ static void on_client_created(struct wl_listener *l, void *data) { if ((fd = open(path, O_RDONLY | O_CLOEXEC)) >= 0) { if ((n = read(fd, buf, sizeof(buf) - 1)) > 0) { char *base = buf, *p; - buf[n] = 0; /* argv[0] only; Wine puts the Windows program path there */ + buf[n] = 0; for (p = buf; *p; p++) if (*p == '\\' || *p == '/') base = p + 1; if (*base) snprintf(ci->name, sizeof(ci->name), "%s", base); } @@ -215,149 +181,121 @@ static void on_client_created(struct wl_listener *l, void *data) { droiddeck_log("program", "connected over Wayland: %s (pid %d)", ci->name, (int)ci->pid); } -/* ------------------------------------------------------------------ surfaces */ enum surface_role { ROLE_NONE, ROLE_TOPLEVEL, ROLE_SUBSURFACE, ROLE_DESKTOP, ROLE_CURSOR }; struct surface { struct wl_resource *resource; - struct wl_list link; /* g_surfaces */ + struct wl_list link; - /* Pending state, applied on commit. */ struct wl_resource *pending_buffer; struct wl_listener pending_buffer_destroy; int pending_attach; int pending_src_set, pending_dst_set; - float pending_src[4]; /* x, y, w, h; w < 0 = unset */ - int pending_dst[2]; /* w, h; w < 0 = unset */ + float pending_src[4]; + int pending_dst[2]; struct wl_list pending_frames; - struct wl_list pending_feedback; /* wp_presentation feedback asked for before commit */ + struct wl_list pending_feedback; - /* Current content. */ - struct vkp_image *shm_img; /* our copy of the last wl_shm buffer */ - struct wl_resource *dmabuf; /* current dmabuf wl_buffer (NULL once the client destroyed it) */ - struct dmabuf_buffer *dmabuf_buf; /* its imported image, kept until replaced or the surface goes */ + + struct vkp_image *shm_img; + struct wl_resource *dmabuf; + struct dmabuf_buffer *dmabuf_buf; struct wl_listener dmabuf_destroy; int buf_w, buf_h, has_content; int buf_alpha; - int opaque[4], pending_opaque[4], pending_opaque_set; /* x, y, w, h; w = 0: none */ + int opaque[4], pending_opaque[4], pending_opaque_set; int src_set, dst_set; float src[4]; int dst[2]; - struct wl_list frames; /* frame callbacks for the next redraw */ - struct wl_list feedback; /* presentation feedback for the current content */ - int drawn; /* part of the last rendered scene */ - int64_t next_release_ns; /* FPS limiter: when this surface's last buffer goes back */ - int releases_pending; /* FPS limiter: buffers of this surface still to be released */ + struct wl_list frames; + struct wl_list feedback; + int drawn; + int64_t next_release_ns; + int releases_pending; enum surface_role role; - /* A role-less surface's last buffer, held unreleased: it may become the pointer's image - * (wl_pointer.set_cursor after the commit, which is wlroots' order). */ struct wl_resource *idle_buffer; struct wl_listener idle_buffer_destroy; struct wl_resource *xdg_surface, *xdg_toplevel; struct wl_resource *viewport; - /* Toplevel placement on the scene. */ - int mapped; /* in g_toplevels */ + + int mapped; struct wl_list toplevel_link; - int keep_on_top; /* Cardboard mode's SteamVR window: above every other toplevel (raise_kept_on_top) */ + int keep_on_top; int x, y, placed; uint32_t hwnd; - /* Subsurface tree. */ struct surface *parent; struct wl_resource *subsurface; - struct wl_list children; /* bottom to top */ + struct wl_list children; struct wl_list child_link; int sub_x, sub_y, sub_pending_x, sub_pending_y, sub_pending; int below_parent; - int fullscreen; /* xdg_toplevel.set_fullscreen: no client decorations */ - int toplevel_committed; /* the toplevel's initial commit was answered */ + int fullscreen; + int toplevel_committed; - char *title; /* xdg_toplevel title, for the session log */ - int announced_vulkan; /* logged its first dmabuf frame */ - uint32_t hdr_fmt_logged; /* HDR session: the buffer format last named in the log */ + char *title; + int announced_vulkan; + uint32_t hdr_fmt_logged; }; -static struct wl_list g_surfaces; /* every surface */ -static struct wl_list g_toplevels; /* mapped toplevels, bottom to top */ +static struct wl_list g_surfaces; +static struct wl_list g_toplevels; static struct surface *g_desktop; -static uint32_t *g_zorder; /* last reported Windows stacking order, top first */ +static uint32_t *g_zorder; static size_t g_zorder_count; static struct wl_event_source *g_render_idle, *g_frame_timer; -/* Rendering is driven by the screen's vsync (Java's Choreographer ticks, nativeVsync): one scene - * per refresh showing the newest buffers, so the event loop is never parked in the swapchain and - * clients get their buffers back as soon as they're replaced. Without ticks (an older app, no - * window) a fallback timer renders instead. */ -static int g_dirty; /* something changed since the last render */ -static int64_t g_last_vsync_ns; /* last tick, 0 = none yet */ -static int64_t g_tick_vsync_ns; /* the vsync that tick is for (Choreographer's frame time) */ -static int64_t g_refresh_ns = 16666667; /* measured tick interval (pacing only, never reported) */ + + + + +static int g_dirty; +static int64_t g_last_vsync_ns; +static int64_t g_tick_vsync_ns; +static int64_t g_refresh_ns = 16666667; static struct wl_event_source *g_fallback_timer; static int g_fallback_armed; -/* FPS limiter (the in-game drawer's): frames per second, 0 = unlimited. Set from the app thread, - * read here. Like the X11 path, which delays the "your buffer is free" notice, the limit paces - * when a replaced buffer is released to its client, so the game itself slows to the cap. */ volatile int g_fps_limit; -/* Shortcut launches: explorer's windows (the desktop, taskbar, Start menu) aren't drawn, matching - * the X11 renderer's unviewable "explorer.exe". They still exist for input routing. */ volatile int g_hide_shell; -/* Experimental layer mode (DROIDDECK_WAYLAND_ZERO_COPY=1 in the container's environment): a single - * fullscreen window is shown on its own Android layer (sc_layer.c) instead of being blitted into the - * screen swapchain. Set from the app before the compositor starts; read per frame. */ volatile int g_zero_copy; -static int g_zero_copy_paused; /* effects hold the layer path off (render_scene) */ +static int g_zero_copy_paused; static int g_zero_copy_fx_skip_said; -/* Compressed (UBWC) game buffers: zwp_linux_dmabuf_v1 advertises DRM_FORMAT_MOD_QCOM_COMPRESSED next - * to LINEAR for every format the renderer's own driver can import that way, so Turnip's Wayland WSI - * in the game allocates UBWC swapchain images instead of resolving each frame to a linear copy. - * DROIDDECK_WAYLAND_UBWC=0 in the container's environment turns the advertisement off (A/B switch). Set - * from the app before the compositor starts; read at every zwp_linux_dmabuf_v1 bind. */ volatile int g_ubwc = 1; -/* Debug (DROIDDECK_WAYLAND_NO_RENDER_NODE=1): name no DRM device in the dma-buf feedback, as a phone - * that exposes no /dev/dri node to apps does, to reproduce its OpenGL path here. Set from the app - * before the compositor starts. */ volatile int g_no_render_node; -/* The panel's refresh rate in mHz, from the app; wl_output advertises it so Wine's display modes - * carry the real rate (games pick their saved 144 Hz mode, as on X11). 0 = 60 Hz. */ volatile int g_output_refresh_mhz; -/* The advertised output size: the container's screen (what the app's X server reports on X11), - * so Wine's display-mode list stops at the desktop size. 0 = 1920x1080. */ volatile int g_output_w, g_output_h; struct pending_release { struct wl_resource *buffer; struct wl_listener destroy; - struct surface *surface; /* NULL once the surface is gone */ + struct surface *surface; int64_t at_ns; - int64_t since_ns; /* when the compositor let go of it (the perf line) */ + int64_t since_ns; struct wl_list link; }; static struct wl_list g_pending_releases; static int g_release_timer_fd = -1; static struct wl_event_source *g_release_source; -static int g_scene_w, g_scene_h; /* size of the last drawn scene */ -static int g_desktop_w, g_desktop_h; /* last size the desktop had content at */ -static unsigned g_stat_frames, g_stat_dmabuf, g_stat_shm; /* since the last 10 s summary */ +static int g_scene_w, g_scene_h; +static int g_desktop_w, g_desktop_h; +static unsigned g_stat_frames, g_stat_dmabuf, g_stat_shm; -/* The 10 s `perf` line (on_stats_timer): where the compositor thread's time goes and how long games - * wait for their buffers back. The driver-side times (acquire, present, fence waits) are counted in - * vk_present.c (vkp_perf_take). Compositor thread only. */ static struct { - unsigned ticks; /* screen refreshes (Choreographer ticks) */ - unsigned scenes; /* render_scene() calls ... */ - int64_t scene_ns, scene_max_ns; /* ... and their wall time */ - unsigned copy_scenes; /* scenes drawn into the screen swapchain (the copy path) */ - unsigned releases, releases_held; /* wl_buffer.release after a replacement; held = 1 ms or more */ - int64_t release_ns, release_max_ns; /* replaced (or taken off the layer) -> released to the game */ + unsigned ticks; + unsigned scenes; + int64_t scene_ns, scene_max_ns; + unsigned copy_scenes; + unsigned releases, releases_held; + int64_t release_ns, release_max_ns; } g_perf; static void schedule_render(void); @@ -367,7 +305,6 @@ static int64_t now_ns(void) { return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec; } -/* A game got a replaced buffer back; since_ns = when the compositor let go of it (0 = not counted). */ static void perf_note_release(int64_t since_ns) { if (since_ns <= 0) return; int64_t d = now_ns() - since_ns; @@ -378,7 +315,6 @@ static void perf_note_release(int64_t since_ns) { if (d >= 1000000LL) g_perf.releases_held++; } -/* ------------------------------------------------------------------ buffer release pacing */ static void pending_release_free(struct pending_release *pr) { if (pr->surface && pr->surface->releases_pending > 0) pr->surface->releases_pending--; @@ -387,7 +323,6 @@ static void pending_release_free(struct pending_release *pr) { free(pr); } -/* A surface is going away: its queued releases still go out on time, they just stop counting. */ static void pending_releases_forget_surface(struct surface *s) { struct pending_release *pr; wl_list_for_each(pr, &g_pending_releases, link) @@ -413,7 +348,7 @@ static void arm_release_timer(void) { static int on_release_timer(int fd, uint32_t mask, void *data) { struct pending_release *pr, *tmp; uint64_t expirations; - int64_t now = now_ns() + 300000; /* anything due within 0.3 ms goes now */ + int64_t now = now_ns() + 300000; if (read(fd, &expirations, sizeof(expirations)) < 0 && errno != EAGAIN) return 0; wl_list_for_each_safe(pr, tmp, &g_pending_releases, link) { if (pr->at_ns <= now) { @@ -427,24 +362,8 @@ static int on_release_timer(int fd, uint32_t mask, void *data) { return 0; } -/* Give a replaced buffer back to its client: now, or on the limiter's cadence. - * - * Each release takes the next slot of a per-surface cadence (one slot per interval), so a game - * gets one buffer back per interval however fast it commits. The schedule is kept honest at both - * ends: a slot in the past is brought to now (the game was slower than the cap, no catch-up burst - * is owed), and it never runs further ahead than the releases actually queued - with p releases - * still pending the new one lands at most (p + 1) intervals out, nothing pending means at most - * one interval. Without that bound the schedule kept slots that were consumed but never - * delivered (a swapchain rebuild destroys buffers with queued releases) or that a coarser earlier - * limit had spaced out, and the game's first frames after that waited on empty slots. */ static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_t since_ns) { const int limit = g_fps_limit; - /* Without a limit of its own a game is paced to the screen. A swapchain that does not wait for - * frame callbacks (MAILBOX, IMMEDIATE) is held back by nothing else here, so handing a replaced - * buffer straight back let it draw as fast as the GPU allowed and everything above the refresh - * rate was thrown away - 178 frames a second for 91 on screen, measured (WinNative, maxjivi05). - * One release per refresh is the back-pressure X11 gives, and the screen shows the same frames - * either way. */ const int64_t interval = limit > 0 ? 1000000000LL / limit : g_refresh_ns; if (interval <= 0 || g_release_timer_fd < 0) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; } int64_t now = now_ns(); @@ -466,9 +385,7 @@ static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_ arm_release_timer(); } -/* ------------------------------------------------------------------ seat state */ -/* Each Wine process is a separate client with its own wl_pointer / wl_keyboard. */ struct seat_pointer { struct wl_resource *ptr; struct wl_resource *focus; }; struct seat_keyboard { struct wl_resource *kb; struct wl_resource *focus; }; struct seat_touch { struct wl_resource *touch; struct wl_resource *focus; }; @@ -478,39 +395,25 @@ static struct seat_pointer g_ptrs[MAX_PTRS]; static int g_nptrs; static struct seat_keyboard g_kbs[MAX_PTRS]; static int g_nkbs; -/* Modifier state in the keymap's real-modifier bits (Shift=0x1, Lock=0x2, Control=0x4, Mod1=0x8, - * Mod4=0x40). A client of the wayland backend - gamescope, labwc - takes modifiers only from - * wl_keyboard.modifiers and ignores what a Shift key event does to its xkb state, so without this - * every capital arrived lowercase. */ static uint32_t g_mods_depressed, g_mods_locked; -static uint32_t g_mod_keys_held; /* one bit per modifier key below, so a repeat press is harmless */ +static uint32_t g_mod_keys_held; static struct seat_touch g_touches[MAX_PTRS]; static int g_ntouches; -/* ---- the client's cursor (wl_pointer.set_cursor) ------------------------------------------- - * Not composited: under zero-copy the program's buffer goes straight to an Android layer and there - * is nothing to composite into. The cursor surface is snapshotted here instead and the app draws it - * as its overlay, above every present path (Bannerlator's design). What it buys: the real SHAPE - - * labwc's resize arrows at a window's edges and corners, a text field's I-beam - and an - * authoritative HIDE (set_cursor with a NULL surface, what a mouse-look game does). - * wl_shm cursors (Wine, pixman) are copied directly; a GPU cursor (labwc on Vulkan/GLES, which - * wlroots hands over as a dma-buf, usually UBWC) is read back through vkp_image_readback. */ #define CURSOR_MAX_PX (256 * 256) static pthread_mutex_t g_cursor_lock = PTHREAD_MUTEX_INITIALIZER; -static struct surface *g_cursor_surface; /* compositor thread only */ -static struct surface *g_cursor_shown; /* compositor thread: whose image g_cursor_px holds */ -static int g_cursor_hx, g_cursor_hy; /* hotspot, surface-local */ -static uint32_t g_cursor_px[CURSOR_MAX_PX]; /* guarded by g_cursor_lock: ARGB8888 snapshot */ -static uint32_t g_cursor_rb[CURSOR_MAX_PX]; /* compositor thread: GPU readback staging */ -static int g_cursor_w, g_cursor_h; /* 0 = nothing to draw */ -static int g_cursor_hidden = 1; /* the client asked for no pointer */ -static int g_cursor_serial; /* bumped on every change; the app polls it */ +static struct surface *g_cursor_surface; +static struct surface *g_cursor_shown; +static int g_cursor_hx, g_cursor_hy; +static uint32_t g_cursor_px[CURSOR_MAX_PX]; +static uint32_t g_cursor_rb[CURSOR_MAX_PX]; +static int g_cursor_w, g_cursor_h; +static int g_cursor_hidden = 1; +static int g_cursor_serial; -/* The last image again, after a hide: the same surface set as the pointer once more with nothing - * newly committed to it (wlroots re-sets its cursor surface each time the pointer enters). */ static void cursor_publish_shown(void) { pthread_mutex_lock(&g_cursor_lock); - if (g_cursor_w > 0) { g_cursor_hidden = 0; g_cursor_serial++; } /* serial: the hotspot may be new */ + if (g_cursor_w > 0) { g_cursor_hidden = 0; g_cursor_serial++; } pthread_mutex_unlock(&g_cursor_lock); } @@ -536,23 +439,16 @@ static void cursor_publish_shm(struct wl_shm_buffer *shm, int hx, int hy) { int32_t w = wl_shm_buffer_get_width(shm), h = wl_shm_buffer_get_height(shm); int32_t stride = wl_shm_buffer_get_stride(shm); if (w <= 0 || h <= 0 || (int64_t)w * h > CURSOR_MAX_PX) return; - /* libwayland only promises stride >= width; each row copied here is width * 4 bytes, so a - * shorter stride would read past the end of the client's pool. */ if ((int64_t)stride < (int64_t)w * 4) return; wl_shm_buffer_begin_access(shm); cursor_publish_pixels((const uint8_t *)wl_shm_buffer_get_data(shm), w, h, (size_t)stride, hx, hy); wl_shm_buffer_end_access(shm); } -/* The serial alone, without the lock: the app asks on every pointer move, and the snapshot below - * copies up to 256 KB of pixels across JNI and holds the lock the compositor thread takes to - * change the cursor. A torn read is only a missed or an extra snapshot. */ int droiddeck_cursor_serial(void) { return __atomic_load_n(&g_cursor_serial, __ATOMIC_RELAXED); } -/* Read by the app (UI thread). out = [serial, hidden, w, h, hotspotX, hotspotY, pixels...]. - * Returns the number of ints written, or 0 if out is too small. */ int droiddeck_cursor_snapshot(int *out, int cap) { int n = 0; pthread_mutex_lock(&g_cursor_lock); @@ -571,14 +467,12 @@ int droiddeck_cursor_snapshot(int *out, int cap) { struct active_touch { struct surface *surface; int pointer_fallback; }; static struct active_touch g_touch_points[MAX_TOUCH_POINTS]; static void touch_cancel_surface(struct surface *surface); -static struct surface *g_grab; /* no-desktop fallback: surface holding the button */ -static struct surface *g_key_target; /* no-desktop fallback: last clicked surface */ -static struct surface *g_ime_click; /* last clicked program window: where text input goes */ +static struct surface *g_grab; +static struct surface *g_key_target; +static struct surface *g_ime_click; static int g_input_pipe[2] = {-1, -1}; struct input_msg { int type; int p1; int p2; int p3; int p4; }; -/* Pointer constraints (zwp_pointer_constraints_v1) and relative pointers - * (zwp_relative_pointer_manager_v1); see the "pointer constraints" section. */ struct surface; static void constraints_surface_gone(struct surface *s); static void constraints_surface_commit(struct surface *s); @@ -586,7 +480,6 @@ static void constraints_pointer_gone(struct wl_resource *pointer); static void relative_pointers_pointer_gone(struct wl_resource *pointer); static void constraints_focus_entered(struct wl_resource *target, struct wl_client *client); -/* ------------------------------------------------------------------ dmabuf buffers */ void dmabuf_buffer_unref(struct dmabuf_buffer *b) { if (!b || --b->refs > 0) return; @@ -609,7 +502,6 @@ static struct dmabuf_buffer *get_dmabuf(struct wl_resource *buffer) { return NULL; } -/* ------------------------------------------------------------------ surface state */ static void surface_size(const struct surface *s, int *w, int *h) { if (s->dst_set) { *w = s->dst[0]; *h = s->dst[1]; } @@ -617,7 +509,6 @@ static void surface_size(const struct surface *s, int *w, int *h) { else { *w = s->buf_w; *h = s->buf_h; } } -/* "Title" (program) for the log; subsurfaces are named after the window they belong to. */ static void describe(const struct surface *s, char *out, size_t size) { const struct surface *w = s; while (w->parent) w = w->parent; @@ -636,8 +527,6 @@ static struct surface *toplevel_by_hwnd(uint32_t hwnd) { static void raise_kept_on_top(void); -/* Reorder the mapped toplevels to match the last reported Windows z-order. Windows the - * report doesn't mention keep their relative order below the ones it does. */ static void apply_zorder(void) { for (size_t i = g_zorder_count; i-- > 0;) { struct surface *s = toplevel_by_hwnd(g_zorder[i]); @@ -648,7 +537,6 @@ static void apply_zorder(void) { raise_kept_on_top(); } -/* Windows marked keep_on_top go back to the top of the stack (the end of g_toplevels). */ static void raise_kept_on_top(void) { struct surface *s, *tmp, *kept[4]; int n = 0; @@ -669,11 +557,8 @@ static void map_toplevel(struct surface *s) { droiddeck_log("window", "opened %s %dx%d at %d,%d%s", name, w, h, s->x, s->y, s->placed ? "" : " (no desktop position yet)"); } - wl_list_insert(g_toplevels.prev, &s->toplevel_link); /* new windows start on top */ + wl_list_insert(g_toplevels.prev, &s->toplevel_link); raise_kept_on_top(); - /* Without a Wine desktop the clients are whole programs (the Linux desktop, a gamescope a - * program on it started over it): a window that opens on top takes the keys too, as it would - * on any desktop, rather than the one clicked last that is now underneath it. */ if (!g_desktop) g_key_target = s; apply_zorder(); } @@ -692,14 +577,9 @@ static void unmap_toplevel(struct surface *s) { droiddeck_text_input_refocus(); } -/* Let go of the surface's dmabuf content. paced = 1: the buffer was replaced, give it back on the - * limiter's cadence; paced = 0: the surface is going away, give it back at once (a buffer of a - * destroyed surface is not shown again and must not sit unreleased with the client). */ static void drop_dmabuf(struct surface *s, int paced) { if (s->dmabuf) { wl_list_remove(&s->dmabuf_destroy.link); - /* A buffer on the zero-copy layer is the display's until SurfaceFlinger says otherwise: - * ahb_swapchain.c releases it then. */ if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) { if (paced) release_buffer(s, s->dmabuf, now_ns()); else wl_buffer_send_release(s->dmabuf); @@ -710,8 +590,6 @@ static void drop_dmabuf(struct surface *s, int paced) { s->dmabuf_buf = NULL; } -/* The client destroyed the wl_buffer we are showing (a swapchain rebuild): keep showing its - * image (s->dmabuf_buf holds it) until the next commit replaces it; only the resource is gone. */ static void on_dmabuf_destroyed(struct wl_listener *l, void *data) { struct surface *s = wl_container_of(l, s, dmabuf_destroy); wl_list_remove(&s->dmabuf_destroy.link); @@ -737,7 +615,6 @@ static void fire_frames(struct wl_list *frames) { } } -/* Copy a committed wl_shm buffer into the surface's image and release the buffer. */ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_resource *buffer) { int32_t w = wl_shm_buffer_get_width(shm), h = wl_shm_buffer_get_height(shm); int32_t stride = wl_shm_buffer_get_stride(shm); @@ -759,10 +636,9 @@ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_res s->has_content = s->shm_img != NULL; g_stat_shm++; struct client_info *ci = client_info_of(wl_resource_get_client(s->resource)); - if (ci && ci->asked_feedback) ci->shm_frames++; /* since it asked for GPU buffers */ + if (ci && ci->asked_feedback) ci->shm_frames++; } -/* A single-pixel buffer (wl_single_pixel.c) as a 1x1 image, which the surface's viewport stretches. */ static void take_pixel(struct surface *s, const uint8_t bgra[4], struct wl_resource *buffer) { if (s->shm_img && (vkp_image_width(s->shm_img) != 1 || vkp_image_height(s->shm_img) != 1)) { vkp_image_destroy(s->shm_img); @@ -777,29 +653,14 @@ static void take_pixel(struct surface *s, const uint8_t bgra[4], struct wl_resou s->has_content = s->shm_img != NULL; } -/* The window the app's performance HUD follows: the latest one at least as big as the last to - * start presenting GPU frames (take_dmabuf) (X11 binds the HUD to the _MESA_DRV window and counts X presents instead). JNI upcalls. */ static struct surface *g_hud_surface; -/* When the HUD's window last presented a new frame (now_ns), and when it last committed at all, - * a repeat of the frame it shows included. A replacement may take over only once it has gone - * quiet in the second sense: gamescope repeats a paused game's frame under a busy overlay, and - * that overlay must not become the game. */ static int64_t g_hud_last_ns, g_hud_alive_ns; -/* That window committed a frame the screen has not drawn yet. */ static int g_hud_fresh; -extern void droiddeck_on_game_surface(const char *window, const char *gpu); /* window NULL = gone */ +extern void droiddeck_on_game_surface(const char *window, const char *gpu); extern void droiddeck_on_game_frame(void); -/* The program behind that window: its Linux pid (the Wayland client's credentials) and executable name - * ("" when /proc gave none) - the app arms its CPU affinity on it (X11 does that from window events). */ extern void droiddeck_on_game_program(int pid, const char *program); static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_resource *buffer) { - /* The buffer this surface already shows, committed again. Gamescope's Wayland backend attaches - * every plane on every frame it outputs, so its game plane comes back unchanged whenever only - * something above it moved (MangoHud's graph, Steam's overlay). The surface still holds it and - * never released it, so the client cannot have drawn into it: it is no new frame. Counted as - * one, a game capped at 30 read 50-60 fps in the HUD, and frame generation took those repeats - * as source frames ("LSFG 2x 59 -> 54" instead of 30 -> 60). */ const int repeat = s->dmabuf == buffer && s->dmabuf_buf == b; if (!repeat) { drop_dmabuf(s, 1); @@ -820,11 +681,6 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re } } if (s->shm_img) { vkp_image_destroy(s->shm_img); s->shm_img = NULL; } - /* The size of the frames a window commits, when it changes after the first announcement: the - * program rebuilt its swapchain. It earns a line of its own, because a game whose swapchain - * came back smaller than it was is exactly what a picture suddenly filling one corner of the - * screen looks like from here - and the two sizes are the whole of the evidence. Rare by - * nature, so there is nothing to rate-limit. */ if (s->announced_vulkan && s->buf_w && s->buf_h && (s->buf_w != b->width || s->buf_h != b->height)) { char name[160]; @@ -843,8 +699,6 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re s->announced_vulkan = 1; describe(s, name, sizeof(name)); if (b->img) - /* Imported for the copy path. Whether its frames ALSO go on the display layer without a copy - * is decided per frame (fullscreen, zero-copy on) and counted in the 10 s lines. */ droiddeck_log("vulkan", "%s is presenting GPU frames through Wayland: %dx%d, format %c%c%c%c, %s (%s)", name, b->width, b->height, b->format & 0xff, (b->format >> 8) & 0xff, (b->format >> 16) & 0xff, (b->format >> 24) & 0xff, @@ -859,13 +713,6 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re droiddeck_log("error", "could not import GPU frames from %s (%dx%d, modifier %#llx)", name, b->width, b->height, (unsigned long long)b->modifier); } - /* The HUD follows the window whether its frames are copied or go straight to the layer: a - * zero-copy frame the compositor never imported is still a presented game frame. A - * program's small surfaces - gamescope's 1x1 cursor, Qt's menus and tooltips (melonDS) - - * came after the game's and drew too rarely to count, which left the HUD on 0 fps while the - * game ran at 60. So a new window takes the HUD only when it is at least as big (a - * full-screen gamescope over the desktop still does), and a window gone quiet for a second - * gives way to whatever draws next. */ if (s != g_hud_surface && (b->img || ahb_swapchain_has_ahb(b)) && (!g_hud_surface || now_ns() - g_hud_alive_ns > 1000000000LL || (announced_now && (int64_t)b->width * b->height >= (int64_t)g_hud_surface->buf_w * g_hud_surface->buf_h))) { @@ -876,8 +723,6 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re struct client_info *ci = client_info_of(wl_resource_get_client(s->resource)); if (ci) droiddeck_on_game_program((int)ci->pid, strncmp(ci->name, "pid ", 4) ? ci->name : ""); } - /* HDR session (gate open): a DXVK game switches to HDR by REBUILDING its swapchain on the same - * surface, so the one-shot line above never sees the 10-bit buffers - name every format change. */ if (b->format != s->hdr_fmt_logged && droiddeck_color_hdr_open()) { char name[160]; uint32_t af = ahb_swapchain_ahb_format(b); @@ -892,17 +737,12 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re : "the compositor's driver could NOT import it (nothing can show it)"); s->hdr_fmt_logged = b->format; } - /* Every buffer that reaches take_dmabuf is a presented GPU frame, so the HUD counts it. - * Asking whether the compositor could import it, as this used to, is a question about the - * copy path and not about whether a frame happened: a game whose buffers go straight to - * the display layer draws on screen while the counter sat at 0.0 fps and 1000.0 ms. */ if (s == g_hud_surface) { g_hud_alive_ns = now_ns(); if (!repeat) { g_hud_last_ns = g_hud_alive_ns; g_hud_fresh = 1; droiddeck_on_game_frame(); } } } -/* ---- hooks for ahb_swapchain.c (zero-copy layers) */ struct dmabuf_buffer *droiddeck_dmabuf_from_resource(struct wl_resource *buffer) { return get_dmabuf(buffer); } int droiddeck_dmabuf_fd(const struct dmabuf_buffer *b) { return b && b->n_planes > 0 ? b->fd[0] : -1; } void droiddeck_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h) { *w = b->width; *h = b->height; } @@ -918,7 +758,6 @@ const struct droiddeck_color *droiddeck_surface_color(const struct surface *s) { return s ? droiddeck_color_of(s->resource) : NULL; } -/* ------------------------------------------------------------------ wl_surface */ static void surface_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); @@ -941,9 +780,6 @@ static void frame_callback_destroy(struct wl_resource *r) { wl_list_remove(wl_resource_get_link(r)); } -/* wp_presentation: tells Mesa's Vulkan driver when a frame reached the screen or was replaced - * before it did. Without it the driver waits for a frame callback (one per refresh) for every - * frame, which paced games to the screen rate even in mailbox mode. */ static void feedback_resource_destroy(struct wl_resource *r) { wl_list_remove(wl_resource_get_link(r)); } @@ -956,20 +792,12 @@ static void feedback_discard_all(struct wl_list *list) { } } -/* The current mode's refresh interval in ns, from the rate the app read off the display. */ static uint32_t nominal_refresh_ns(void) { int mhz = g_output_refresh_mhz; if (mhz <= 0) mhz = 60000; return (uint32_t)(1000000000000LL / (int64_t)mhz); } -/* The vblank a scene drawn at render_ns belongs to, for wp_presentation: the vsync of the tick that - * drew it (Choreographer's frame time). Gamescope's Wayland backend anchors its vblank clock to this - * and wakes a red zone before the next refresh. Reporting the end of the render instead (a few ms - * past the tick) put its wakeups on top of the next tick: some frames missed it and showed twice, - * others were replaced before they were shown (a 60 Hz Thor got ~50 distinct frames a second from - * gamescope). Not the refresh after it either: gamescope then wakes a whole refresh late and runs - * at half rate. Without a fresh tick (the fallback timer) it is the render time. */ static int64_t presented_ns(int64_t render_ns) { if (!g_tick_vsync_ns || render_ns - g_last_vsync_ns > (int64_t)nominal_refresh_ns() || render_ns < g_tick_vsync_ns) @@ -982,12 +810,6 @@ static void feedback_present_all(struct wl_list *list, int64_t t) { uint64_t sec = (uint64_t)(t / 1000000000LL); uint32_t nsec = (uint32_t)(t % 1000000000LL); wl_resource_for_each_safe(fb, tmp, list) { - /* The protocol's "refresh" is the NOMINAL interval of the current mode, a constant - not - * the interval this frame happened to take. Reporting the measured average instead makes a - * variable-refresh panel look like a mode change on every frame: gamescope re-derives its - * pacing each time ("Changed refresh to: 119.2 ... 121.4hz", 826 times in a minute on a - * Galaxy Z Fold, none on a fixed-refresh device). Report the panel's rate, which the app - * gives us and wl_output already advertises. */ wp_presentation_feedback_send_presented(fb, (uint32_t)(sec >> 32), (uint32_t)sec, nsec, nominal_refresh_ns(), 0, 0, WP_PRESENTATION_FEEDBACK_KIND_VSYNC); @@ -1023,12 +845,8 @@ static void surface_frame(struct wl_client *c, struct wl_resource *r, uint32_t c wl_resource_set_implementation(callback, NULL, NULL, frame_callback_destroy); wl_list_insert(s->pending_frames.prev, wl_resource_get_link(callback)); } -/* The bounding box serves pointer confinement. The exact flag also tells opaque-region handling - * whether the bounding box itself is the whole region. */ struct region { int set, exact; int x, y, w, h; }; -/* A wl_surface's opaque region is pending until its next commit. An exact region that covers - * the surface lets the blit path keep handling an otherwise alpha-capable buffer. */ static void surface_set_opaque(struct wl_client *c, struct wl_resource *r, struct wl_resource *region) { struct surface *s = wl_resource_get_user_data(r); @@ -1049,9 +867,6 @@ static void surface_set_input(struct wl_client *c, struct wl_resource *r, static void send_toplevel_configure(struct surface *s); -/* The pointer image from a cursor surface's buffer: wl_shm is copied, a dma-buf (labwc on a GPU - * renderer) is read back once the client's render into it is done - its implicit fence, as the - * zero-copy path waits for it (ahb_swapchain_present). */ static void cursor_publish_buffer(struct surface *s, struct wl_resource *buffer) { struct dmabuf_buffer *db = get_dmabuf(buffer); struct wl_shm_buffer *shm = db ? NULL : wl_shm_buffer_get(buffer); @@ -1084,7 +899,6 @@ static void on_idle_buffer_destroyed(struct wl_listener *l, void *data) { s->idle_buffer = NULL; } -/* Only what could be a pointer image is held; anything larger goes straight back. */ static void surface_hold_idle(struct surface *s, struct wl_resource *buffer) { struct dmabuf_buffer *db = get_dmabuf(buffer); struct wl_shm_buffer *shm = db ? NULL : wl_shm_buffer_get(buffer); @@ -1096,7 +910,6 @@ static void surface_hold_idle(struct surface *s, struct wl_resource *buffer) { wl_resource_add_destroy_listener(buffer, &s->idle_buffer_destroy); } -/* Lets go of the held buffer; release = give it back to the client (not while it is going away). */ static struct wl_resource *surface_take_idle(struct surface *s) { struct wl_resource *buffer = s->idle_buffer; if (!buffer) return NULL; @@ -1115,10 +928,6 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { struct surface *s = wl_resource_get_user_data(r); struct surface *child; - /* xdg-shell: the initial commit of a toplevel is answered with a configure. One is also sent - * at get_toplevel, which Wine's driver has always had; a client that waits for the reply to its - * commit on a queue of its own (wlroots' Wayland backend - the desktop's labwc) can read that - * early one onto the wrong queue and would wait for ever. */ if (s->xdg_toplevel && !s->toplevel_committed) { s->toplevel_committed = 1; send_toplevel_configure(s); @@ -1145,7 +954,6 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { struct wl_shm_buffer *shm = buffer && !db ? wl_shm_buffer_get(buffer) : NULL; uint8_t pixel[4]; - /* The previous content is replaced before reaching the screen. */ feedback_discard_all(&s->feedback); if (buffer) { @@ -1155,18 +963,15 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { s->pending_buffer = NULL; s->pending_attach = 0; - surface_drop_idle(s); /* replaced, whatever the surface is now */ + surface_drop_idle(s); if (s->role == ROLE_CURSOR) { - /* The client's pointer image, copied out for the app's overlay (see cursor_publish_*) - * rather than composited, so it survives the zero-copy and HDR layer paths. */ if (!buffer) { - cursor_publish_hidden(); /* wlroots clears its cursor surface to hide the pointer */ + cursor_publish_hidden(); } else { cursor_publish_buffer(s, buffer); wl_buffer_send_release(buffer); } } else if (s->role == ROLE_NONE) { - /* Role-less surface: never drawn, but its buffer is kept for a set_cursor to come. */ if (buffer) surface_hold_idle(s, buffer); } else if (db) { take_dmabuf(s, db, buffer); @@ -1183,7 +988,6 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { } } - /* Subsurface positions are applied when the parent commits. */ wl_list_for_each(child, &s->children, child_link) { if (child->sub_pending) { child->sub_x = child->sub_pending_x; @@ -1197,7 +1001,7 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { wl_list_insert_list(s->feedback.prev, &s->pending_feedback); wl_list_init(&s->pending_feedback); constraints_surface_commit(s); - droiddeck_color_commit(s->resource); /* wp_color_management_surface_v1 state (returns at once when HDR is off) */ + droiddeck_color_commit(s->resource); if (s->role == ROLE_TOPLEVEL && s->xdg_toplevel) { if (s->has_content) map_toplevel(s); @@ -1278,7 +1082,6 @@ static void surface_resource_destroy(struct wl_resource *r) { schedule_render(); } -/* ------------------------------------------------------------------ wl_region */ static void region_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void region_add(struct wl_client *c, struct wl_resource *r, @@ -1310,7 +1113,6 @@ static const struct wl_region_interface region_impl = { .subtract = region_subtract, }; -/* ------------------------------------------------------------------ wl_compositor */ static void compositor_create_surface(struct wl_client *c, struct wl_resource *r, uint32_t id) { struct surface *s = calloc(1, sizeof(*s)); @@ -1344,8 +1146,6 @@ static void bind_compositor(struct wl_client *c, void *data, uint32_t ver, uint3 wl_resource_set_implementation(r, &compositor_impl, NULL, NULL); } -/* --------------------------------------------------------------- wl_subcompositor - * winewayland puts a window's Vulkan/GL swapchain in a subsurface over the window. */ static void subsurface_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void subsurface_set_position(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y) { @@ -1358,7 +1158,6 @@ static void subsurface_set_position(struct wl_client *c, struct wl_resource *r, static void restack_child(struct surface *s, struct surface *sibling, int above) { if (!s || !s->parent || !sibling) return; if (sibling == s->parent) { - /* Relative to the parent: directly above it or directly below it. */ s->below_parent = !above; wl_list_remove(&s->child_link); if (above) wl_list_insert(&s->parent->children, &s->child_link); @@ -1415,7 +1214,7 @@ static void subcompositor_get_subsurface(struct wl_client *c, struct wl_resource s->parent = p; s->below_parent = 0; s->sub_x = s->sub_y = 0; - wl_list_insert(p->children.prev, &s->child_link); /* new subsurfaces go on top */ + wl_list_insert(p->children.prev, &s->child_link); } static const struct wl_subcompositor_interface subcompositor_impl = { .destroy = subcompositor_destroy, @@ -1426,9 +1225,6 @@ static void bind_subcompositor(struct wl_client *c, void *data, uint32_t ver, ui wl_resource_set_implementation(r, &subcompositor_impl, NULL, NULL); } -/* ---------------------------------------------------------------- wp_viewporter - * winewayland crops window buffers to the window size and scales swapchains to the - * client area with viewports, so they must be honoured for things to line up. */ static void viewport_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void viewport_set_source(struct wl_client *c, struct wl_resource *r, @@ -1479,18 +1275,12 @@ static void bind_viewporter(struct wl_client *c, void *data, uint32_t ver, uint3 wl_resource_set_implementation(r, &viewporter_impl, NULL, NULL); } -/* ------------------------------------------------------------------ xdg_shell */ static void xdg_toplevel_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void xdg_toplevel_noop_parent(struct wl_client *c, struct wl_resource *r, struct wl_resource *p) {} static void xdg_toplevel_set_title(struct wl_client *c, struct wl_resource *r, const char *title) { struct surface *s = wl_resource_get_user_data(r); if (!s) return; - /* A window already on screen being renamed is something else taking it over: gamescope keeps - * one toplevel and retitles it as the game takes the screen. Unlogged, that left the log - * showing a window "closed" under a name it had never been logged as opening - which reads - * like a missing event and is really a rename. A title set before the window is mapped needs - * no line: the "opened" line carries it. */ int announce = s->mapped && title && *title && (!s->title || strcmp(s->title, title) != 0); char before[160]; if (announce) describe(s, before, sizeof(before)); @@ -1504,9 +1294,6 @@ static void xdg_toplevel_set_title(struct wl_client *c, struct wl_resource *r, c } static void xdg_toplevel_set_app_id(struct wl_client *c, struct wl_resource *r, const char *id) { struct surface *s = wl_resource_get_user_data(r); - /* Cardboard mode's SteamVR window (tools/vr/src/presenter.cpp) stays above every other window, - * gamescope's included: gamescope reopens its window when Steam notices VR, and a reopened window - * would otherwise come up over it (new windows start on top). */ if (s && id && !strcmp(id, "droiddeck-cardboard-vr")) s->keep_on_top = 1; } static void xdg_toplevel_show_menu(struct wl_client *c, struct wl_resource *r, struct wl_resource *seat, @@ -1517,10 +1304,6 @@ static void xdg_toplevel_resize(struct wl_client *c, struct wl_resource *r, stru uint32_t serial, uint32_t edges) {} static void xdg_toplevel_set_i32(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {} static void xdg_toplevel_noop(struct wl_client *c, struct wl_resource *r) {} -/* The output's size; active, and fullscreen when asked. gamescope waits for the fullscreen - * state before it drops its libdecor frame, and never draws until it arrives; it was told the - * same size on its command line. A nested wlroots compositor (labwc) has no other way to learn - * it: given 0x0 it opens its default 1280x720 and the whole desktop is upscaled from that. */ static void send_toplevel_configure(struct surface *s) { struct wl_array states; wl_array_init(&states); @@ -1586,7 +1369,6 @@ static void xdg_surface_get_toplevel(struct wl_client *c, struct wl_resource *r, s->xdg_toplevel = tl; send_toplevel_configure(s); } -/* A popup is dismissed as soon as it is made (see the positioner below). */ static void popup_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void popup_grab(struct wl_client *c, struct wl_resource *r, struct wl_resource *seat, uint32_t serial) {} static void popup_reposition(struct wl_client *c, struct wl_resource *r, struct wl_resource *positioner, @@ -1620,11 +1402,6 @@ static void xdg_surface_resource_destroy(struct wl_resource *r) { static void xdg_wm_base_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } -/* Popups are not drawn here, but their requests must still land somewhere: a resource without an - * implementation makes libwayland abort on the first request to it - the whole compositor, and the - * app with it. A positioner takes every request and keeps nothing; a popup is dismissed at once - * (popup_done), which is what a client does with a popup it cannot show. Seen with a gamescope - * started from the Linux desktop beside labwc. */ static void positioner_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void positioner_set_size(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {} static void positioner_set_anchor_rect(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y, @@ -1674,7 +1451,6 @@ static void bind_xdg_wm_base(struct wl_client *c, void *data, uint32_t ver, uint wl_resource_set_implementation(r, &xdg_wm_base_impl, NULL, NULL); } -/* ------------------------------------------------------------------ banner_desktop_v1 */ static void desktop_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void desktop_set_desktop(struct wl_client *c, struct wl_resource *r, struct wl_resource *surface) { @@ -1723,14 +1499,10 @@ static void bind_desktop(struct wl_client *c, void *data, uint32_t ver, uint32_t wl_resource_set_implementation(r, &desktop_impl, NULL, NULL); } -/* ------------------------------------------------------------------ wl_output */ -/* wl_output v3+ has a request (release); without an implementation the first client to send it - * aborted the compositor. */ static void output_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static const struct wl_output_interface output_impl = { .release = output_release }; -/* Every bound wl_output, so a resized output (a foldable opening) can send its new mode. */ static struct wl_list g_outputs; static void output_resource_destroy(struct wl_resource *r) { wl_list_remove(wl_resource_get_link(r)); } @@ -1758,7 +1530,6 @@ static void bind_output(struct wl_client *c, void *data, uint32_t ver, uint32_t } } -/* ------------------------------------------------------------------ rendering */ struct draw_list { struct vkp_draw *d; int n, cap; }; @@ -1768,21 +1539,15 @@ static struct vkp_image *surface_image(struct surface *s) { return s->dmabuf_buf ? s->dmabuf_buf->img : NULL; } -/* HDR (gate open) only: the scene's HDR surface whose current frame is one of the game's gralloc buffers - * that the compositor's own driver could NOT import (a 10-bit UBWC layout it does not take, say). Such a - * frame has no draw, so neither layer_candidate() nor ahb_layer_only_candidate() (toplevels only) can - * find it - and a Vulkan game's swapchain is a SUBSURFACE of its window. Remembered here while the scene - * is built so it can still go on the display layer, which never needed the import. Reset per scene; - * never set while the HDR gate is closed. */ static struct surface *g_hdr_unimported; -static int g_hdr_unimported_rect[4], g_hdr_unimported_below; /* scene x,y,w,h; draws under it */ +static int g_hdr_unimported_rect[4], g_hdr_unimported_below; static void note_hdr_unimported(const struct draw_list *dl, struct surface *s, int ox, int oy) { if (!s->dmabuf_buf || s->dmabuf_buf->img || !ahb_swapchain_has_ahb(s->dmabuf_buf)) return; const struct droiddeck_color *c = droiddeck_color_of(s->resource); if (!c || !c->dataspace) return; if (s->src_set && (s->src[0] != 0 || s->src[1] != 0 || (int)(s->src[2] + 0.5f) != s->buf_w || - (int)(s->src[3] + 0.5f) != s->buf_h)) return; /* cropped: the layer shows whole buffers */ + (int)(s->src[3] + 0.5f) != s->buf_h)) return; int dw, dh; surface_size(s, &dw, &dh); g_hdr_unimported = s; @@ -1791,8 +1556,6 @@ static void note_hdr_unimported(const struct draw_list *dl, struct surface *s, i g_hdr_unimported_below = dl->n; } -/* A client that describes opaque regions can intentionally leave part of an alpha buffer see-through. - * Gamescope uses this for its full-size Steam notification and overlay planes. */ static int surface_translucent(const struct surface *s, int w, int h) { if (!s->buf_alpha) return 0; const struct client_info *ci = client_info_of(wl_resource_get_client(s->resource)); @@ -1836,8 +1599,6 @@ static void add_tree(struct draw_list *dl, struct surface *s, int ox, int oy, in if (!c->below_parent) add_tree(dl, c, ox + c->sub_x, oy + c->sub_y, depth + 1); } -/* Scene size: the desktop's, or without one the largest window's (a single - * fullscreen game then fills the screen exactly as before). */ static void scene_size(int *w, int *h) { struct surface *s; long long best = 0; @@ -1863,13 +1624,6 @@ static void fire_all_frames(void) { wl_list_for_each(s, &g_surfaces, link) fire_frames(&s->frames); } -/* With no output window (the app is in the background, or the SurfaceView is being rebuilt) - * nothing gets drawn, but the clients must not be left waiting on us. A FIFO swapchain blocks - * in its present until its wp_presentation feedback and frame callback arrive, and a client - * blocked there stops pumping messages: winhandler.exe then wedges behind it and the app - * concludes the program has exited and closes the whole session. (DXVK's mailbox presents - * never wait, which is why only OpenGL/Zink clients died on a background/resume.) Answer every - * pending frame and feedback as discarded, and keep doing so until a window is back. */ static void pace_without_output(void) { struct surface *s; wl_list_for_each(s, &g_surfaces, link) { @@ -1881,19 +1635,15 @@ static void pace_without_output(void) { static int on_frame_timer(void *data) { pace_without_output(); - if (vkp_has_window()) schedule_render(); /* it is back: draw the current scene */ + if (vkp_has_window()) schedule_render(); else if (g_frame_timer) wl_event_source_timer_update(g_frame_timer, 16); return 0; } -/* Layer mode: the index of a draw that shows a whole client GPU frame over the whole scene (one - * fullscreen game), or -1. Only the top two positions are looked at: the game must be the topmost - * draw, or have exactly ONE draw above it, which then goes on the overlay layer (sc_layer.h). - * Anything deeper would need more display layers than HWC will compose. */ static int layer_candidate(const struct draw_list *dl, int scene_w, int scene_h) { for (int i = dl->n - 1; i >= 0 && i >= dl->n - 2; i--) { const struct vkp_draw *d = &dl->d[i]; - if (d->blend) continue; /* the translucent plane must not replace the opaque game layer */ + if (d->blend) continue; if (!vkp_image_is_dmabuf(d->img)) continue; if (d->dx != 0 || d->dy != 0 || d->dw != scene_w || d->dh != scene_h) continue; if (d->sx != 0 || d->sy != 0 || (int)d->sw != vkp_image_width(d->img) || @@ -1903,7 +1653,6 @@ static int layer_candidate(const struct draw_list *dl, int scene_w, int scene_h) return -1; } -/* Zero-copy: the surface whose current GPU frame `img` is (layer_candidate found the draw). */ static struct surface *surface_for_image(const struct vkp_image *img) { struct surface *s; wl_list_for_each(s, &g_surfaces, link) @@ -1911,9 +1660,6 @@ static struct surface *surface_for_image(const struct vkp_image *img) { return NULL; } -/* Zero-copy: the topmost window whose frame is one of the game's AHardwareBuffers that this - * renderer could NOT import (so it has no draw), covering the whole scene at 0,0 with nothing of - * its own above it: it can only be shown on the layer. NULL otherwise. */ static struct surface *ahb_layer_only_candidate(int scene_w, int scene_h) { struct surface *s; int w, h; @@ -1928,39 +1674,16 @@ static struct surface *ahb_layer_only_candidate(int scene_w, int scene_h) { return s; } -/* ---- HDR (droiddeck_color.h) in the scene - * An HDR frame is only right on the game's own display layer: everything the compositor draws itself - - * the scene blit, the effects chain, frame generation, its swapchain - is 8-bit sRGB, and a PQ-encoded - * frame pushed through it as it stands comes out washed out. So a scene with HDR in it takes one of - * three routes (hdr_plan): - * 0 the HDR game ALONE on its display layer (zero-copy, or one copy) - with at most one SDR window - * above it on the overlay layer where this display can compose that. Best: nothing is converted. - * 1 the HDR PICTURE: everything else without frame generation - screen effects on, a window above - * the game on a display that cannot take a second layer, a windowed game, zero-copy off. The whole - * scene is composed into ONE 10-bit PQ BT.2020 picture (SDR content placed at the SDR white; the - * effects run on it in 10-bit) and put on the game layer tagged BT2020_PQ (hdr_compose.h). - * 2 frame generation: its extra frames need presents of their own, so the scene is composed into PQ - * and presented through an HDR10 swapchain - or, where the screen surface offers none, - * tone-mapped to SDR (correct colours instead of washed out). - * 3 a frame the compositor could not import: only its display layer can show it, so effects and - * frame generation are skipped for it (said in the log). - * The drawer's HDR output switch (droiddeck_color_output) OFF: the same routes, tone-mapped - route 0 - * becomes route 1 (the whole scene composed, tone-mapped to sRGB, onto the game layer UNtagged), route 2 - * tone-maps into an SDR swapchain, route 3 cannot (nothing can read those frames) and stays HDR, said - * in the log. The game is told nothing; it keeps rendering HDR. - * Nothing here runs while the HDR gate is closed. */ struct hdr_scene { struct vkp_hdr_frame hf; - unsigned char *is_hdr; /* per draw (calloc'd; free after the frame) */ + unsigned char *is_hdr; int n_hdr; - const struct droiddeck_color *color; /* the topmost HDR draw's description: what the picture is tagged */ - struct surface *who; /* its surface, for the log */ - const char *why; /* route 1: why the game cannot be alone on its layer */ + const struct droiddeck_color *color; + struct surface *who; + const char *why; }; -/* The HDR surface note_hdr_unimported() saw in THIS scene, when it covers the whole scene at 0,0 and - * nothing is drawn above it: the display layer is the only place its frame can be shown. */ static struct surface *hdr_unimported_fullscreen(const struct draw_list *dl, int w, int h) { if (!g_hdr_unimported || g_hdr_unimported_below != dl->n) return NULL; if (g_hdr_unimported_rect[0] != 0 || g_hdr_unimported_rect[1] != 0 || @@ -1973,11 +1696,9 @@ static int is_hdr_surface(struct surface *s) { return c && c->dataspace; } -/* Which route this scene takes (see above); fills hs. 0 whenever the gate is closed or no HDR is drawn. */ static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int framegen, struct hdr_scene *hs) { memset(hs, 0, sizeof(*hs)); if (!droiddeck_color_hdr_open()) return 0; - /* A fullscreen HDR frame this compositor could not import has no draw: layer only (route 3). */ struct surface *un = hdr_unimported_fullscreen(dl, w, h); if (!un) { struct surface *lo = ahb_layer_only_candidate(w, h); @@ -1991,7 +1712,7 @@ static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int fra if (!is_hdr_surface(s)) continue; hs->is_hdr[i] = 1; hs->n_hdr++; - hs->color = droiddeck_color_of(s->resource); /* later draws are above: the topmost wins */ + hs->color = droiddeck_color_of(s->resource); hs->who = s; } if (!hs->n_hdr) { free(hs->is_hdr); hs->is_hdr = NULL; return 0; } @@ -2000,9 +1721,6 @@ static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int fra hs->hf.peak_nits = hs->color->max_cll > 0.0f ? hs->color->max_cll : hs->color->has_st2086 ? hs->color->max_lum : 1000.0f; if (framegen) return 2; - /* Route 0 when the game can be alone on its layer: no effects, zero-copy on, the fullscreen draw - * is the HDR game, and above it nothing - or one window the overlay layer can carry. Not while the - * HDR output switch is off: its frame as it stands is PQ, and only the composition can tone-map it. */ int li = g_zero_copy ? layer_candidate(dl, w, h) : -1; int over = li >= 0 ? dl->n - 1 - li : 0; if (!hs->hf.tonemap && !fx_on && li >= 0 && hs->is_hdr[li] && @@ -2016,11 +1734,10 @@ static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int fra return 1; } -/* One line per change of route (and of its reason, and of the HDR output switch). */ static void hdr_note_route(int route, const struct hdr_scene *hs) { static int said = -1, said_tm = -1; static const char *said_why; - const int tm = route != 0 && !droiddeck_color_output(); /* route 0 never runs with the switch off */ + const int tm = route != 0 && !droiddeck_color_output(); if (route == said && tm == said_tm && (route != 1 || hs->why == said_why)) return; int was = said; said = route; said_why = hs->why; said_tm = tm; @@ -2049,7 +1766,7 @@ static void hdr_note_route(int route, const struct hdr_scene *hs) { if (tm) droiddeck_log("color", "HDR output is switched off, but %s's HDR frames cannot be imported by the compositor, " "so nothing can tone-map them: they stay HDR on its own display layer", name); - break; /* otherwise hdr_note_precedence says it */ + break; default: if (was == 1 || was == 2) droiddeck_log("color", "%s is back on its own display layer (no composition needed)", name); @@ -2057,7 +1774,6 @@ static void hdr_note_route(int route, const struct hdr_scene *hs) { } } -/* Say when effects / frame generation start or stop being skipped for an HDR game (once per change). */ static void hdr_note_precedence(struct surface *hs, int fx_on, int framegen) { static int fx_said = -1, fg_said = -1; int fx_skip = hs && fx_on, fg_skip = hs && framegen; @@ -2081,7 +1797,6 @@ static void hdr_note_precedence(struct surface *hs, int fx_on, int framegen) { } } -/* This scene went through the copy path: count every HDR-described surface in it, with the reason. */ static void hdr_count_copied(const struct draw_list *dl, const char *reason) { for (int i = 0; i < dl->n; i++) { struct surface *s = surface_for_image(dl->d[i].img); @@ -2098,11 +1813,11 @@ static void render_scene(void) { struct surface *s; int w, h; const int64_t t_scene = now_ns(); - int copy = 0; /* this scene went through the screen swapchain (the perf line's "copy") */ + int copy = 0; g_dirty = 0; g_hud_fresh = 0; - g_hdr_unimported = NULL; /* found again while the scene is built (HDR gate open only) */ + g_hdr_unimported = NULL; wl_list_for_each(s, &g_surfaces, link) s->drawn = 0; scene_size(&w, &h); if (w != g_scene_w || h != g_scene_h) { @@ -2113,7 +1828,7 @@ static void render_scene(void) { } if (g_desktop && !g_hide_shell) add_tree(&dl, g_desktop, 0, 0, 0); wl_list_for_each(s, &g_toplevels, toplevel_link) { - if (g_desktop && !s->placed) continue; /* wait for its position */ + if (g_desktop && !s->placed) continue; if (g_hide_shell && !strcmp(client_name(wl_resource_get_client(s->resource)), "explorer.exe")) continue; add_tree(&dl, s, s->placed ? s->x : 0, s->placed ? s->y : 0, 0); } @@ -2121,13 +1836,10 @@ static void render_scene(void) { int rendered = 0; int fx_on = vkp_effects_active(); int framegen = vkp_framegen_active(); - /* Cardboard mode draws the whole panel itself, so the game never gets a display layer of its own. */ const int cardboard = vkp_cardboard_active(); - /* HDR (see hdr_plan): route 0 = the old paths below; 1 / 2 = composed; 3 = layer only, effects and - * frame generation skipped. Always 0 while the HDR gate is closed: nothing changes for that session. */ struct hdr_scene hs; int hdr_route = hdr_plan(&dl, w, h, fx_on, framegen, &hs); - const char *hdr_copy = NULL; /* why this scene's HDR frames (if any) took the copy path */ + const char *hdr_copy = NULL; if (droiddeck_color_hdr_open()) { hdr_note_route(hdr_route, &hs); hdr_note_precedence(hdr_route == 3 ? hs.who : NULL, fx_on, framegen); @@ -2138,10 +1850,10 @@ static void render_scene(void) { vkp_apply_window_request(); sc_layer_hide_overlay(); if (sc_layer_present_hdr_scene(dl.d, dl.n, &hs.hf, w, h, hs.color) == 0) { - rendered = vkp_base_black(w, h) == 0; /* black under the composed picture, presented only when it is not already */ + rendered = vkp_base_black(w, h) == 0; if (fell_back) { fell_back = 0; droiddeck_log("color", "the composed picture is on the game's display layer again"); } } else { - hdr_route = 2; /* no layer this frame: the picture goes through the swapchain instead */ + hdr_route = 2; if (!fell_back) { fell_back = 1; droiddeck_log("color", "the composed picture could not go on the game's display layer (no layer, or the " @@ -2159,25 +1871,11 @@ static void render_scene(void) { else if (rendered) hdr_copy = "the HDR composition pass is unavailable on this driver"; } if (hdr_route == 0 || hdr_route == 3) { - /* What still forces the whole scene back through the compositor pass (and so through the app's - * own swapchain), and what no longer does: - * - frame generation ALWAYS does: its extra frames each need a present of their own on - * consecutive vblanks, and a display layer latches one buffer per refresh; - * - screen effects do NOT any more when the game is alone on screen - the chain runs and its - * result is copied into the game layer's own gralloc buffer (sc_layer_present_pass), so the - * game keeps its layer and the display does the scene -> output mapping; - * - screen effects DO when a window is drawn above the game: the chain has to see the whole - * scene to look the way it does on the copy path, and here the game is only part of it. */ const int pass_on = fx_on || framegen; int li = (g_zero_copy && !framegen && !cardboard) ? layer_candidate(&dl, w, h) : -1; - int over = li >= 0 ? dl.n - 1 - li : 0; /* draws above the fullscreen game (0 or 1) */ + int over = li >= 0 ? dl.n - 1 - li : 0; const int fx_blocks = (li >= 0 && fx_on && over > 0); if (fx_blocks) { li = -1; over = 0; } - /* A window above the game needs a SECOND display layer, and on some displays that costs the - * hardware composition the first layer was worth having (sc_layer_overlay_affordable). Where it - * does, the whole scene goes down the copy path exactly as it did before the layer set existed. - * Decided HERE, before the game is committed to a layer: deciding it after the present would - * show the game layer and hide it again on every frame. */ const int ov_blocks = (li >= 0 && over > 0 && !fx_blocks && !sc_layer_overlay_affordable()); if (ov_blocks) { li = -1; over = 0; } const int blocked = framegen ? 1 : (fx_blocks ? 2 : (ov_blocks ? 3 : 0)); @@ -2194,10 +1892,7 @@ static void render_scene(void) { droiddeck_log("effects", "zero-copy resumed: the game is back on its own display layer"); } struct surface *ls = li >= 0 ? surface_for_image(dl.d[li].img) : NULL; - /* A frame this renderer could not import can only be shown on the layer, effects or not. */ if (li < 0) ls = ahb_layer_only_candidate(w, h); - /* HDR: the same, for a game's SUBSURFACE swapchain whose 10-bit frame the compositor could not - * import (NULL whenever the HDR gate is closed - nothing is ever noted then). */ if (li < 0 && !ls) ls = hdr_unimported_fullscreen(&dl, w, h); if (cardboard) ls = NULL; if (ls && pass_on && li < 0 && !g_zero_copy_fx_skip_said) { @@ -2207,24 +1902,8 @@ static void render_scene(void) { framegen ? "frame generation" : "effects"); } if (li >= 0 || ls) { - /* Layer mode: the fullscreen window goes on the game layer and (at most) one window above - * it on the overlay layer; whatever is under the game is hidden by it, so the screen - * surface only needs to be black. The game's frame goes on its layer as is when it is one - * of the game's own AHardwareBuffers (zero-copy, ahb_swapchain.c), through the effects - * chain's result when a Look is on, else through one blit into the pool. If a layer can't - * take its frame, the whole scene is drawn the usual way. */ - /* The layers go up FIRST and the base surface's black frame is presented after them. The - * order matters: a present holds an acquired swapchain image, and the acquire semaphore is - * a vblank gate - putting the effects chain or the layer transaction behind it costs a - * whole refresh (measured on the Pocket FIT: 72 fps that way, 111 fps this way, with Retro - * CRT at 1920x1080). The window request the app may have left is applied here instead, - * since vkp_render no longer runs before the layer path. */ vkp_apply_window_request(); int r = -1; - /* The game's own buffer can only go on the layer AS IS - with a Look on, the frame has to - * go through the chain first, so the raw buffer is skipped in favour of the pass (a frame - * this renderer could not import has no draw, li < 0, and stays zero-copy with the effects - * skipped, said once above). */ if (!(fx_on && li >= 0) && ls && ls->dmabuf_buf && ahb_swapchain_has_ahb(ls->dmabuf_buf)) r = ahb_swapchain_present(ls->dmabuf_buf, ls, w, h); if (r != 0 && li >= 0) @@ -2232,24 +1911,16 @@ static void render_scene(void) { : sc_layer_present(dl.d[li].img, w, h, ls ? droiddeck_surface_color(ls) : NULL); if (r == 0) { if (ls) ls->drawn = 1; - /* The one window above the game keeps the game off the copy path entirely: it goes on - * its own layer, cropped and placed by the display. */ int go[8], ov = 0; if (over == 1 && li >= 0 && vkp_map_draw(&dl.d[li + 1], go)) ov = sc_layer_present_overlay(dl.d[li + 1].img, go, dl.d[li + 1].blend) == 0 ? 1 : -1; if (ov <= 0) sc_layer_hide_overlay(); - if (ov < 0) { /* the overlay layer refused it: draw the whole scene the old way */ + if (ov < 0) { sc_layer_hide(); rendered = vkp_render(w, h, dl.d, dl.n) == 0; copy = 1; hdr_copy = "the overlay layer refused the window above it"; } else { - /* Black under the opaque layers. It is a plain black frame (no effects, no frame - * generation on it: they would only run on black), and it is presented only when the - * base surface does not already show one - entering layer mode, after a copy-path or - * frame-generation frame, after a swapchain rebuild. Every other layer frame draws and - * presents nothing here: the display layers are all that changes on screen. This loop - * is paced by the Choreographer's ticks (on_vsync), not by this present. */ rendered = vkp_base_black(w, h) == 0; } } else { @@ -2267,24 +1938,18 @@ static void render_scene(void) { : framegen ? "frame generation needs the compositor pass" : "it is not the one fullscreen window (only that one gets its own display layer)"; } - } /* routes 0 and 3 */ + } free(hs.is_hdr); if (hdr_copy && rendered && droiddeck_color_hdr_open()) hdr_count_copied(&dl, hdr_copy); if (rendered) { int64_t t = presented_ns(t_scene); g_stat_frames++; - /* A surface outside the scene (role-less, not placed yet, a hidden helper window such - * as wined3d's device window) was not shown, so its feedback is discarded rather than - * left pending: a FIFO present waits on it, and a client blocked there never commits - * again. */ wl_list_for_each(s, &g_surfaces, link) { if (s->drawn) feedback_present_all(&s->feedback, t); else feedback_discard_all(&s->feedback); } fire_all_frames(); } else { - /* No output surface yet (or it went away): keep clients paced without it, now and on a - * timer that re-arms itself until a window is back (see pace_without_output). */ pace_without_output(); if (!g_frame_timer) g_frame_timer = wl_event_loop_add_timer(wl_display_get_event_loop(g_display), @@ -2309,7 +1974,6 @@ static int on_fallback_timer(void *data) { static void schedule_render(void) { g_dirty = 1; if (!g_display) return; - /* With vsync ticks flowing the next tick renders; otherwise render on a timer. */ if (g_last_vsync_ns && now_ns() - g_last_vsync_ns < 100000000LL) return; if (g_fallback_armed) return; if (!g_fallback_timer) @@ -2320,14 +1984,10 @@ static void schedule_render(void) { wl_event_source_timer_update(g_fallback_timer, 8); } -/* With frame generation on, every scene is the engine's next source frame, so one drawn for - * another window (Steam's overlay) counted as a game frame and took its present slots. Such a - * change waits for the game's next frame, unless the game has been still for 100 ms. */ static int scene_waits_for_game(int64_t now) { return g_hud_surface && !g_hud_fresh && now - g_hud_last_ns < 100000000LL && vkp_framegen_active(); } -/* A screen refresh (Choreographer tick): draw the newest state once. */ static void on_vsync(int64_t frame_time_ns) { int64_t now = now_ns(); g_perf.ticks++; @@ -2337,25 +1997,18 @@ static void on_vsync(int64_t frame_time_ns) { } g_last_vsync_ns = now; g_tick_vsync_ns = frame_time_ns; - /* The app's surface may have been replaced or taken away since the last frame: apply that now - * (the app never waits for us), and redraw the scene onto a new one. */ if (vkp_apply_window_request()) g_dirty = 1; - /* A screen-effect setting changed (JNI, any thread): redraw so it shows on a static scene too. */ if (vkp_effects_sync()) g_dirty = 1; - /* Cardboard mode: a new head pose (or setting) moves the virtual screen, static scene or not. */ if (vkp_cardboard_pose_changed()) g_dirty = 1; if (g_dirty && !scene_waits_for_game(now)) render_scene(); } -/* ------------------------------------------------------------------ wl_seat */ static uint32_t now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000); } -/* wl_pointer.set_cursor: the client tells us its pointer image, or asks for none. A NULL surface - * means "no cursor" and is authoritative. The surface itself is snapshotted on its next commit. */ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint32_t serial, struct wl_resource *surface, int32_t hx, int32_t hy) { if (!surface) { @@ -2366,14 +2019,11 @@ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint3 } struct surface *s = wl_resource_get_user_data(surface); if (!s) return; - if (s->role != ROLE_NONE && s->role != ROLE_CURSOR) return; /* already has another role */ + if (s->role != ROLE_NONE && s->role != ROLE_CURSOR) return; s->role = ROLE_CURSOR; g_cursor_surface = s; g_cursor_hx = hx; g_cursor_hy = hy; - /* Its content may already be committed: wlroots' Wayland backend attaches and commits the - * cursor surface first and sets it as the pointer after, and sets it again with nothing new - * committed each time the pointer re-enters. */ struct wl_resource *buffer = surface_take_idle(s); if (buffer) { cursor_publish_buffer(s, buffer); @@ -2417,8 +2067,6 @@ static void seat_get_keyboard(struct wl_client *c, struct wl_resource *r, uint32 if (!k) { wl_client_post_no_memory(c); return; } wl_resource_set_implementation(k, &keyboard_impl, NULL, keyboard_res_destroy); if (g_nkbs < MAX_PTRS) { g_kbs[g_nkbs].kb = k; g_kbs[g_nkbs].focus = NULL; g_nkbs++; } - /* Send the xkb keymap the app extracted to $XDG_RUNTIME_DIR/keymap.xkb. Without a keymap the - * client can't interpret our evdev key codes. */ const char *rt = getenv("XDG_RUNTIME_DIR"); char path[512]; snprintf(path, sizeof(path), "%s/keymap.xkb", rt ? rt : "."); @@ -2467,9 +2115,6 @@ static struct seat_keyboard *keyboard_for(struct wl_client *client) { return NULL; } -/* A client may hold more than one wl_pointer/wl_keyboard: gamescope creates a second pair and - * reads input on its own thread, so an event sent only to the first object is never seen. - * Every delivery below walks all of the client's objects. */ #define for_each_pointer_of(client, sp) \ for (int _i = 0; _i < g_nptrs; _i++) \ if (((sp) = &g_ptrs[_i]), wl_resource_get_client((sp)->ptr) == (client)) @@ -2480,7 +2125,6 @@ static struct seat_keyboard *keyboard_for(struct wl_client *client) { for (int _i = 0; _i < g_ntouches; _i++) \ if (((st) = &g_touches[_i]), wl_resource_get_client((st)->touch) == (client)) -/* Topmost window whose scene rectangle contains the point (no-desktop fallback). */ static struct surface *toplevel_at(double x, double y) { struct surface *s; wl_list_for_each_reverse(s, &g_toplevels, toplevel_link) { @@ -2532,59 +2176,38 @@ static void keyboard_focus(struct wl_resource *target) { g_mods_depressed, 0, g_mods_locked, 0); } wl_array_release(&keys); - droiddeck_clipboard_keyboard_focus(client); /* wl_data_device selection follows focus */ + droiddeck_clipboard_keyboard_focus(client); } -/* ------------------------------------------------------------------ pointer constraints - * zwp_pointer_constraints_v1 and zwp_relative_pointer_manager_v1: what winewayland uses for - * SetCursorPos, ClipCursor and hidden-cursor (mouse-look) games. - * - * A lock freezes the pointer where it is: no wl_pointer.motion is sent while it holds, and - * every input delta (a relative event from the app, or the difference between absolute - * positions) reaches the locking program as zwp_relative_pointer_v1.relative_motion. A confine - * clamps the pointer to the surface (and the program's region inside it). Relative motion is - * also sent while unlocked, to any program holding a relative pointer. - * - * Input normally goes to the desktop surface; while a constraint holds, pointer focus moves - * to the constrained surface, so the constraining program receives the events itself - * (winewayland only enables relative motion for a window its pointer has entered). One - * constraint holds at a time; a newer request ends the older one. A lock's cursor position - * hint is applied on the surface's commit and is where the pointer ends up when the lock - * ends (winewayland's SetCursorPos: lock, hint, commit, unlock). */ struct constraint { - struct wl_list link; /* g_constraints */ - struct wl_resource *resource; /* zwp_locked_pointer_v1 or zwp_confined_pointer_v1 */ - struct wl_resource *pointer; /* the program's wl_pointer; NULL once released */ - struct surface *surface; /* NULL once the surface is gone */ + struct wl_list link; + struct wl_resource *resource; + struct wl_resource *pointer; + struct surface *surface; int is_lock; uint32_t lifetime; int active; - int defunct; /* a oneshot that ended, or surface/pointer gone */ - struct region region, pending_region; /* confine area, surface-local; unset = whole surface */ + int defunct; + struct region region, pending_region; int pending_region_set; - int hint_set, pending_hint_set; /* lock: cursor position hint, surface-local */ + int hint_set, pending_hint_set; double hint_x, hint_y, pending_hint_x, pending_hint_y; }; static struct wl_list g_constraints; static struct constraint *g_active_constraint; struct relative_pointer { - struct wl_list link; /* g_relative_pointers */ + struct wl_list link; struct wl_resource *resource; - struct wl_resource *pointer; /* the program's wl_pointer; NULL once released */ + struct wl_resource *pointer; }; static struct wl_list g_relative_pointers; -/* Last pointer position in scene coordinates (buttons and scrolls from the app's X-server - * input path arrive without one). */ static double g_ptr_x, g_ptr_y; -/* Last absolute input position: absolute input becomes deltas while the pointer is locked. */ static double g_raw_x, g_raw_y; static int g_raw_valid; -/* JNI: the app switches its touch/mouse path to deltas while a lock holds and re-syncs its - * pointer to x,y when it ends. */ extern void droiddeck_on_pointer_lock(int locked, int x, int y); static void sync_lock_notify(void) { @@ -2600,13 +2223,11 @@ static void constraint_describe(const struct constraint *k, char *out, size_t si else snprintf(out, size, "a closed window"); } -/* Scene position of the constrained surface. */ static void constraint_origin(const struct constraint *k, int *x, int *y) { *x = *y = 0; if (k->surface && k->surface != g_desktop && k->surface->placed) { *x = k->surface->x; *y = k->surface->y; } } -/* Keep x,y (scene coordinates) inside the confine area. */ static void confine_clamp(const struct constraint *k, double *x, double *y) { int ox, oy, w, h, rx = 0, ry = 0, rw, rh; if (!k->surface) return; @@ -2637,7 +2258,6 @@ static void constraint_send_state(struct constraint *k, int on) { } } -/* A constraint stops holding. tell_client = 0 when its resource is being destroyed. */ static void constraint_end(struct constraint *k, int tell_client, const char *why) { char name[160]; if (!k->active) return; @@ -2667,7 +2287,6 @@ static void constraint_activate(struct constraint *k) { k->active = 1; g_active_constraint = k; if (!k->is_lock) confine_clamp(k, &g_ptr_x, &g_ptr_y); - /* Pointer focus follows the constraint: the program gets the events itself. */ constraint_origin(k, &ox, &oy); pointer_focus(k->surface->resource, wl_fixed_from_double(g_ptr_x - ox), wl_fixed_from_double(g_ptr_y - oy)); constraint_send_state(k, 1); @@ -2687,7 +2306,6 @@ static void constraint_activate(struct constraint *k) { wl_display_flush_clients(g_display); } -/* Pointer focus entered target: a persistent constraint waiting on it takes hold again. */ static void constraints_focus_entered(struct wl_resource *target, struct wl_client *client) { struct constraint *k; if (g_active_constraint) return; @@ -2719,7 +2337,6 @@ static void constraints_pointer_gone(struct wl_resource *pointer) { } } -/* Double-buffered state (position hint, confine region) applies on the surface's commit. */ static void constraints_surface_commit(struct surface *s) { struct constraint *k; wl_list_for_each(k, &g_constraints, link) { @@ -2764,7 +2381,6 @@ static void locked_pointer_set_cursor_position_hint(struct wl_client *c, struct k->pending_hint_y = wl_fixed_to_double(y); } static void locked_pointer_set_region(struct wl_client *c, struct wl_resource *r, struct wl_resource *region) { - /* A lock's region only says where it may take hold; a lock here takes hold anywhere. */ } static const struct zwp_locked_pointer_v1_interface locked_pointer_impl = { .destroy = constraint_destroy_req, @@ -2842,7 +2458,6 @@ static void bind_pointer_constraints(struct wl_client *c, void *data, uint32_t v wl_resource_set_implementation(r, &pointer_constraints_impl, NULL, NULL); } -/* --- relative pointers --- */ static void relative_pointer_resource_destroy(struct wl_resource *r) { struct relative_pointer *rp = wl_resource_get_user_data(r); @@ -2884,8 +2499,6 @@ static void relative_pointers_pointer_gone(struct wl_resource *pointer) { if (rp->pointer == pointer) rp->pointer = NULL; } -/* relative_motion to every relative pointer of the program owning the focus (winewayland wants - * it whenever it holds one, locked or not). Returns whether any was sent. */ static int send_relative_motion(struct wl_client *client, double dx, double dy) { struct relative_pointer *rp; struct timespec ts; @@ -2904,12 +2517,7 @@ static int send_relative_motion(struct wl_client *client, double dx, double dy) return sent; } -/* ------------------------------------------------------------------ pointer events */ -/* One pointer event: a motion, then an optional button change (button 0 = none). Absolute - * x,y are scene coordinates; with `relative` they are a delta. While a lock holds the pointer - * stays put and the delta (or the change between absolute positions) becomes relative motion - * for the locking program; a confine keeps the pointer inside its area. */ static void pointer_input(double x, double y, int relative, uint32_t button, int pressed) { struct surface *target; struct constraint *k = g_active_constraint; @@ -2968,8 +2576,6 @@ static void pointer_input(double x, double y, int relative, uint32_t button, int if (wl_resource_get_version(sp->ptr) >= WL_POINTER_FRAME_SINCE_VERSION) wl_pointer_send_frame(sp->ptr); } - /* A click lands text input (the soft keyboard's IME text) on the window under the pointer: - * that's where Wine's focus goes, and winewayland routes IME updates per process. */ if (button && pressed && !(k && k->is_lock)) { struct surface *clicked = toplevel_at(g_ptr_x, g_ptr_y); if (clicked) g_ime_click = clicked; @@ -2978,24 +2584,18 @@ static void pointer_input(double x, double y, int relative, uint32_t button, int wl_display_flush_clients(g_display); } -/* Absolute motion to scene x,y, then an optional button change. */ static void pointer_event(double x, double y, uint32_t button, int pressed) { pointer_input(x, y, 0, button, pressed); } -/* A delta from the app's relative input path (Relative Mouse, captured mouse, stick-as-mouse). */ static void pointer_delta(double dx, double dy) { pointer_input(dx, dy, 1, 0, 0); } -/* A button change at the current pointer position. */ static void pointer_button(uint32_t button, int pressed) { pointer_input(0, 0, 1, button, pressed); } -/* Java touch events arrive in INPUT_SPACE over the whole output: action 0=press, 1=move, 2=release. - * Output pixels go through the inverse of the scale mode's mapping (letterbox bars, FILL crop, a - * TOP/BOTTOM half), so the touch lands on the scene pixel that is drawn under the finger. */ static void deliver_pointer(const struct input_msg *m) { int w, h, ow, oh; double x, y; @@ -3003,7 +2603,7 @@ static void deliver_pointer(const struct input_msg *m) { vkp_output_size(&ow, &oh); if (ow <= 0 || oh <= 0 || !vkp_output_to_scene((double)m->p2 * ow / INPUT_SPACE_W, (double)m->p3 * oh / INPUT_SPACE_H, &x, &y)) { - x = (double)m->p2 * w / INPUT_SPACE_W; /* nothing drawn yet: plain stretch */ + x = (double)m->p2 * w / INPUT_SPACE_W; y = (double)m->p3 * h / INPUT_SPACE_H; } if (x < 0) x = 0; @@ -3095,9 +2695,6 @@ static void touch_down(int id, double x, double y) { struct surface *target = touch_target_at(x, y); if (!target) return; struct wl_client *client = wl_resource_get_client(target->resource); - /* A down on an id that is still held means that sequence's up was lost: end it first. The - * fingers still down are counted only after that - counted before, a lost up left any_active - * set, and on the pointer path every later touch was dropped (a tap or swipe did nothing). */ if (g_touch_points[id].surface) touch_cancel_client(client); int any_active = 0; for (int i = 0; i < MAX_TOUCH_POINTS; i++) { @@ -3113,14 +2710,13 @@ static void touch_down(int id, double x, double y) { struct seat_touch *st; int has_touch = 0; for_each_touch_of(client, st) has_touch = 1; - /* One line per gesture (first finger), so a test can see where a touch went. */ if (!any_active) { char name[160]; describe(target, name, sizeof(name)); droiddeck_log("touch", "%s gets %s", name, has_touch ? "touch" : "the pointer (it takes no touch)"); } if (!has_touch) { - if (any_active) return; /* one finger drives the pointer; a second one is ignored */ + if (any_active) return; g_touch_points[id].surface = target; g_touch_points[id].pointer_fallback = 1; pointer_event(x, y, BTN_LEFT, 1); @@ -3201,7 +2797,6 @@ static void deliver_key(const struct input_msg *m) { key_event((uint32_t)m->p1, m->p2); } -/* Vertical wheel steps (negative = up) at the current pointer position. */ static void scroll_event(int steps) { struct surface *target = g_active_constraint ? g_active_constraint->surface : g_desktop ? g_desktop : (g_grab ? g_grab : toplevel_at(g_ptr_x, g_ptr_y)); @@ -3214,7 +2809,6 @@ static void scroll_event(int steps) { uint32_t t = now_ms(); pointer_focus(target->resource, wl_fixed_from_double(g_ptr_x - tx), wl_fixed_from_double(g_ptr_y - ty)); for_each_pointer_of(client, sp) { - /* From version 8 the discrete event is replaced by value120 and must not be sent. */ if (wl_resource_get_version(sp->ptr) >= WL_POINTER_AXIS_VALUE120_SINCE_VERSION) wl_pointer_send_axis_value120(sp->ptr, WL_POINTER_AXIS_VERTICAL_SCROLL, steps * 120); else if (wl_resource_get_version(sp->ptr) >= WL_POINTER_AXIS_DISCRETE_SINCE_VERSION) @@ -3226,16 +2820,15 @@ static void scroll_event(int steps) { wl_display_flush_clients(g_display); } -/* Folds a key into the modifier state; returns whether the state changed. */ static int update_modifiers(uint32_t evdev, int pressed) { static const struct { uint32_t evdev, mask; } mod_keys[] = { - { 42, 0x1 }, { 54, 0x1 }, /* Shift L/R */ - { 29, 0x4 }, { 97, 0x4 }, /* Control L/R */ - { 56, 0x8 }, { 100, 0x8 }, /* Alt L/R, both Mod1 in this keymap */ - { 125, 0x40 }, { 126, 0x40 }, /* Super L/R */ + { 42, 0x1 }, { 54, 0x1 }, + { 29, 0x4 }, { 97, 0x4 }, + { 56, 0x8 }, { 100, 0x8 }, + { 125, 0x40 }, { 126, 0x40 }, }; uint32_t depressed = g_mods_depressed, locked = g_mods_locked; - if (evdev == 58) { /* Caps Lock toggles on press */ + if (evdev == 58) { if (pressed) locked ^= 0x2; } else { int i, n = (int)(sizeof(mod_keys) / sizeof(mod_keys[0])); @@ -3252,13 +2845,7 @@ static int update_modifiers(uint32_t evdev, int pressed) { } static void key_event(uint32_t evdev, int pressed) { - /* Tracked even with nothing to deliver to, so a Shift released unseen is not left held. */ int mods_changed = update_modifiers(evdev, pressed); - /* Keys go to the program window the user last clicked (else the topmost non-shell window), - * never to Wine's desktop surface: winewayland hands each key to the hwnd of the surface that - * holds keyboard focus, and a key handed to explorer's desktop hwnd is queued to explorer's - * thread, not to the foreground program (X11 delivers keys to the focused app window, so - * this mirrors it). The desktop itself is the last resort. */ struct surface *target = NULL; struct wl_resource *ime = droiddeck_ime_target(); if (ime) target = wl_resource_get_user_data(ime); @@ -3283,10 +2870,6 @@ static void key_event(uint32_t evdev, int pressed) { wl_display_flush_clients(g_display); } -/* The output changed size under a running session (a foldable opened or closed, with the - * session following the screen). Every client that has had its first configure is told the new - * size: gamescope's Wayland backend and the desktop's KWin both resize their output to it, and - * the next buffers they commit fill the new panel instead of being letterboxed onto it. */ static void output_resized(void) { struct surface *s; struct wl_resource *r; @@ -3300,17 +2883,16 @@ static void output_resized(void) { wl_display_flush_clients(g_display); } -/* wl event-loop callback: drain queued input events written by the Android UI thread. */ static int on_input_readable(int fd, uint32_t mask, void *data) { struct input_msg m; while (read(fd, &m, sizeof(m)) == (ssize_t)sizeof(m)) { switch (m.type) { case 1: deliver_key(&m); break; - case 2: pointer_event(m.p1, m.p2, 0, 0); break; /* scene motion */ - case 3: pointer_button(m.p1, m.p2); break; /* button at pointer */ + case 2: pointer_event(m.p1, m.p2, 0, 0); break; + case 3: pointer_button(m.p1, m.p2); break; case 4: scroll_event(m.p1); break; case 5: on_vsync(((int64_t)m.p1 << 32) | (uint32_t)m.p2); break; - case 6: pointer_delta(m.p1 / 256.0, m.p2 / 256.0); break; /* relative motion, 1/256 px */ + case 6: pointer_delta(m.p1 / 256.0, m.p2 / 256.0); break; case 7: deliver_touch(&m); break; case 8: output_resized(); break; default: deliver_pointer(&m); break; @@ -3319,8 +2901,6 @@ static int on_input_readable(int fd, uint32_t mask, void *data) { return 0; } -/* Called from JNI (Android UI thread). Queues a pointer event; the compositor thread - * dispatches it. x/y are in INPUT_SPACE (0..1919, 0..1079) over the whole output. */ void droiddeck_wayland_send_pointer(int action, int x, int y) { if (g_input_pipe[1] < 0) return; struct input_msg m = { 0, action, x, y, 0 }; @@ -3335,7 +2915,6 @@ void droiddeck_wayland_send_touch(int action, int pointer_id, int x, int y) { (void)n; } -/* Called from JNI. Queues a key event. evdev = Linux input keycode (e.g. KEY_A=30); state 1=down 0=up. */ void droiddeck_wayland_send_key(int evdev, int state) { if (g_input_pipe[1] < 0) return; struct input_msg m = { 1, evdev, state, 0, 0 }; @@ -3343,7 +2922,6 @@ void droiddeck_wayland_send_key(int evdev, int state) { (void)n; } -/* Called from JNI after g_output_w/h changed: the compositor thread re-sends the size. */ void droiddeck_wayland_output_resized(void) { if (g_input_pipe[1] < 0) return; struct input_msg m = { 8, 0, 0, 0, 0 }; @@ -3351,7 +2929,6 @@ void droiddeck_wayland_output_resized(void) { (void)n; } -/* Called from JNI on every screen refresh (Choreographer). */ void droiddeck_wayland_vsync(int64_t frame_time_ns) { if (g_input_pipe[1] < 0) return; struct input_msg m = { 5, (int)(frame_time_ns >> 32), (int)(uint32_t)frame_time_ns, 0, 0 }; @@ -3359,9 +2936,6 @@ void droiddeck_wayland_vsync(int64_t frame_time_ns) { (void)n; } -/* Called from JNI with the app's X-server input (on-screen controls, mouse): type 2 = motion - * to scene x,y; 3 = evdev button a pressed/released (b); 4 = a wheel steps (negative = up); - * 6 = relative motion by a,b in 1/256 pixel (the app's Relative Mouse / captured-mouse path). */ void droiddeck_wayland_send_scene_input(int type, int a, int b) { if (g_input_pipe[1] < 0) return; struct input_msg m = { type, a, b, 0, 0 }; @@ -3369,15 +2943,11 @@ void droiddeck_wayland_send_scene_input(int type, int a, int b) { (void)n; } -/* ------------------------------------------------------------------ extension module hooks - * What wl_clipboard.c / wl_text_input.c need from the scene (see droiddeck_ext.h). */ const char *droiddeck_client_name(struct wl_client *client) { return client_name(client); } struct wl_display *droiddeck_get_display(void) { return g_display; } void droiddeck_request_redraw(void) { schedule_render(); } -/* The surface text input follows: the last clicked mapped program window, else the topmost - * mapped window not owned by the desktop's process (explorer). NULL when there is none. */ struct wl_resource *droiddeck_ime_target(void) { struct surface *s; if (g_ime_click && g_ime_click->mapped) return g_ime_click->resource; @@ -3395,10 +2965,8 @@ void droiddeck_surface_scene_origin(struct wl_resource *surface, int *x, int *y) void droiddeck_inject_key(uint32_t evdev, int pressed) { key_event(evdev, pressed); } -/* ------------------------------------------------------------------ 10 s summary */ static struct wl_event_source *g_stats_timer; -/* The last window's zero-copy count, kept for the app (WaylandCompositor.nativeZeroCopyFrames). */ volatile unsigned g_zero_copy_last; static int on_stats_timer(void *data) { @@ -3408,24 +2976,16 @@ static int on_stats_timer(void *data) { unsigned zero_copy = ahb_swapchain_stats_take(); unsigned layer_frames = sc_layer_frames_take(); g_zero_copy_last = zero_copy; - /* Interpolated frames the compositor added (framegen_bridge.c). They are on screen, so they - * count there; they are NOT GPU frames from games and never inflate that number. */ unsigned generated = vkp_framegen_stats_take(); if (g_stat_frames || g_stat_dmabuf || g_stat_shm || generated) { char extra[160] = ""; int off = 0; if (g_zero_copy || zero_copy) off += snprintf(extra + off, sizeof(extra) - (size_t)off, " | %u zero-copy frames", zero_copy); - /* Frames the compositor put on a display layer through one of its OWN gralloc buffers (the - * plain layer blit, or the effects chain's result): still hardware-composed, but not - * zero-copy, so they are counted apart from the line above. */ if (layer_frames) off += snprintf(extra + off, sizeof(extra) - (size_t)off, " | %u layer frames", layer_frames); if (generated) snprintf(extra + off, sizeof(extra) - (size_t)off, " | %u generated frames", generated); droiddeck_log("stats", "last 10 s: %u frames on screen (%.1f fps) | %u GPU frames from games | %u window redraws | %d windows open%s", g_stat_frames + generated, (g_stat_frames + generated) / 10.0, g_stat_dmabuf, g_stat_shm, windows, extra); } - /* The perf line, next to the stats line and only when something happened: the compositor thread's - * time per scene and inside the driver (avg/max), what the base surface did, and how long games - * waited for their buffers back. See the audit memory for how to read it. */ { struct vkp_perf vp; vkp_perf_take(&vp); @@ -3448,10 +3008,10 @@ static int on_stats_timer(void *data) { memset(&g_perf, 0, sizeof(g_perf)); } g_stat_frames = g_stat_dmabuf = g_stat_shm = 0; - droiddeck_color_stats_tick(); /* HDR evidence for the same window (nothing when the HDR gate is closed) */ - /* A program that asked for dma-buf feedback (EGL's GPU path does, first thing) but has drawn - * only wl_shm frames since is an OpenGL program whose EGL gave up on the GPU. The software - * path it fell to has no rasteriser in the Wayland layers, so what it commits is black. */ + droiddeck_color_stats_tick(); + + + for (struct client_info *ci = g_clients; ci; ci = ci->next) { if (!ci->asked_feedback || ci->dmabuf_buffers || ci->shm_gl_said || ci->shm_frames < 150) continue; @@ -3465,12 +3025,6 @@ static int on_stats_timer(void *data) { return 0; } -/* ------------------------------------------------------------------ test input - * $XDG_RUNTIME_DIR/test-input is a FIFO for scripted testing from a root shell, in scene - * (desktop) coordinates: "move X Y", "click X Y", "rclick X Y", "dclick X Y", "down X Y", - * "up X Y", "rdown X Y", "rup X Y", "rel DX DY" (relative motion, like the app's Relative - * Mouse path), "key CODE" (evdev), "keydown CODE", "keyup CODE". - * It lives in the app's private directory, so only the app and root can reach it. */ static char g_test_buf[512]; static size_t g_test_len; @@ -3520,7 +3074,7 @@ static int on_test_input(int fd, uint32_t mask, void *data) { } g_test_len = strlen(start); memmove(g_test_buf, start, g_test_len); - if (g_test_len >= sizeof(g_test_buf) - 1) g_test_len = 0; /* overlong line: drop */ + if (g_test_len >= sizeof(g_test_buf) - 1) g_test_len = 0; } return 0; } @@ -3534,14 +3088,10 @@ static void start_test_input(struct wl_event_loop *loop) { snprintf(path, sizeof(path), "%s/test-input", rt); unlink(path); if (mkfifo(path, 0600) != 0) return; - /* O_RDWR keeps a writer open ourselves, so a closing test shell never leaves us at EOF. */ if ((fd = open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC)) < 0) return; wl_event_loop_add_fd(loop, fd, WL_EVENT_READABLE, on_test_input, NULL); } -/* ------------------------------------------------------------------ entry - * Blocks in the wl event loop, so the JNI wrapper runs it on a dedicated thread. - * XDG_RUNTIME_DIR must be set by the caller before this runs. */ static struct wl_listener g_client_created = { .notify = on_client_created }; @@ -3553,8 +3103,6 @@ int droiddeck_wayland_run(void) { wl_list_init(&g_relative_pointers); open_session_log(); - /* Bring the GPU up before any client can connect: loading Turnip the first time - * can take many seconds, and it must not stall a client mid-handshake. */ if (vkp_ready() != 0) WLOGE("renderer unavailable; clients will connect but nothing is drawn"); struct wl_display *display = wl_display_create(); @@ -3563,10 +3111,6 @@ int droiddeck_wayland_run(void) { return 1; } - /* Use a FIXED socket name so it always matches the guest's WAYLAND_DISPLAY=wayland-0 - * (wl_display_add_socket_auto would drift to wayland-1/2/… if a stale socket exists, - * and the guest only ever looks for wayland-0). Unlink any stale socket+lock first so - * a previous run that didn't clean up can't block the bind. */ const char *rt = getenv("XDG_RUNTIME_DIR"); if (rt && *rt) { char p[512]; @@ -3584,33 +3128,30 @@ int droiddeck_wayland_run(void) { wl_global_create(display, &wl_compositor_interface, 6, NULL, bind_compositor); wl_global_create(display, &wl_subcompositor_interface, 1, NULL, bind_subcompositor); wl_global_create(display, &wp_viewporter_interface, 1, NULL, bind_viewporter); - wl_display_init_shm(display); /* wl_shm global + pool/buffer handling */ - /* libdecor binds wl_output at 4; a lower version is a protocol error for the client. */ + wl_display_init_shm(display); + wl_global_create(display, &wl_output_interface, 4, NULL, bind_output); wl_global_create(display, &xdg_wm_base_interface, 1, NULL, bind_xdg_wm_base); wl_global_create(display, &zwp_linux_dmabuf_v1_interface, 4, NULL, bind_dmabuf); - /* gamescope's Wayland backend refuses a seat older than 8. */ wl_global_create(display, &wl_seat_interface, 9, NULL, bind_seat); wl_global_create(display, &banner_desktop_v1_interface, 1, NULL, bind_desktop); wl_global_create(display, &wp_presentation_interface, 2, NULL, bind_presentation); wl_global_create(display, &zwp_pointer_constraints_v1_interface, 1, NULL, bind_pointer_constraints); wl_global_create(display, &zwp_relative_pointer_manager_v1_interface, 1, NULL, bind_relative_pointer_manager); - droiddeck_ext_init(display); /* clipboard, text input, toplevel icons (own files, see droiddeck_ext.h) */ - ahb_swapchain_init(display); /* zero-copy layers: banner_ahb_v1, advertised whenever a display - * layer is possible; the mode event carries the live switch - * (ahb_swapchain.h) */ - droiddeck_color_init(display); /* HDR10 (opt-in): decides the gate from the app's request + the - * display + the layer path above; creates wp_color_manager_v1 - * only when it is open (droiddeck_color.h). Before any client can - * bind zwp_linux_dmabuf_v1, whose table it widens. */ + droiddeck_ext_init(display); + ahb_swapchain_init(display); + + + droiddeck_color_init(display); + + + wl_list_init(&g_pending_releases); g_release_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); if (g_release_timer_fd >= 0) g_release_source = wl_event_loop_add_fd(wl_display_get_event_loop(display), g_release_timer_fd, WL_EVENT_READABLE, on_release_timer, NULL); - /* Input injection: the Android UI thread writes events to g_input_pipe[1]; - * the wl event loop drains them on this thread. */ g_display = display; if (pipe2(g_input_pipe, O_CLOEXEC | O_NONBLOCK) == 0) { wl_event_loop_add_fd(wl_display_get_event_loop(display), g_input_pipe[0], @@ -3622,7 +3163,7 @@ int droiddeck_wayland_run(void) { if ((g_stats_timer = wl_event_loop_add_timer(wl_display_get_event_loop(display), on_stats_timer, NULL))) wl_event_source_timer_update(g_stats_timer, 10000); - wl_display_run(display); /* blocks, dispatches the event loop */ + wl_display_run(display); wl_display_destroy(display); return 0; diff --git a/app/src/main/cpp/waylandcomp/src/compositor_internal.h b/app/src/main/cpp/waylandcomp/src/compositor_internal.h index 0468f007f..0e4229d0a 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor_internal.h +++ b/app/src/main/cpp/waylandcomp/src/compositor_internal.h @@ -1,8 +1,3 @@ -/* - * What compositor.c shares with the protocol modules split out of it (wl_dmabuf.c): the dma-buf - * buffer records, the per-client bookkeeping and the few functions and globals both sides use. - * Everything declared here is hidden: it is internal to libdroiddeckwayland, not part of its ABI. - */ #pragma once #include <stdint.h> #include <sys/types.h> @@ -16,7 +11,6 @@ #define WLOGE(...) __android_log_print(ANDROID_LOG_ERROR, "DroidDeckWayland", __VA_ARGS__) #endif -/* ------------------------------------------------------------------ dmabuf buffers */ #define FOURCC(a, b, c, d) \ ((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24)) @@ -41,39 +35,29 @@ struct dmabuf_buffer { int32_t width, height; uint32_t format; uint64_t modifier; - struct vkp_image *img; /* imported once, reused for every frame */ + struct vkp_image *img; int import_failed; - /* One reference for the wl_buffer resource, one per surface showing the buffer. Mesa destroys - * a swapchain's wl_buffers the moment the game rebuilds its swapchain, i.e. while the last - * committed one is still what is on screen: the import (and the dma-buf memory it pins) stays - * until the surface commits something newer, so the picture never blinks to black. */ int refs; - void *ahb_state; /* zero-copy: ahb_swapchain.c's record (the game's AHardwareBuffer) */ + void *ahb_state; }; -/* Which program each Wayland client is (from /proc/<pid>/cmdline), for the session log. */ struct client_info { struct wl_client *client; struct wl_listener destroy; pid_t pid; char name[64]; - /* An OpenGL program whose EGL gave up on the GPU: it asked for dma-buf feedback (EGL's - * Wayland GPU path always does), never made a dma-buf buffer, and draws wl_shm frames. */ unsigned asked_feedback : 1, shm_gl_said : 1; - /* Clients that describe opaque regions can use alpha outside those regions. Wine windows - * never describe them, and their unused alpha channel must not make them translucent. */ unsigned declares_opaque : 1; unsigned dmabuf_buffers, shm_frames; struct client_info *next; }; -/* ---- compositor.c */ COMPOSITOR_INTERNAL struct client_info *client_info_of(struct wl_client *client); COMPOSITOR_INTERNAL void dmabuf_buffer_unref(struct dmabuf_buffer *b); COMPOSITOR_INTERNAL extern const struct wl_buffer_interface dbuf_buffer_impl; -extern volatile int g_ubwc; /* DROIDDECK_WAYLAND_UBWC (waylandcomp_jni.c sets it) */ -extern volatile int g_no_render_node; /* DROIDDECK_WAYLAND_NO_RENDER_NODE */ +extern volatile int g_ubwc; +extern volatile int g_no_render_node; -/* ---- wl_dmabuf.c */ -COMPOSITOR_INTERNAL extern dev_t g_main_device; /* the render node clients are told to allocate on */ + +COMPOSITOR_INTERNAL extern dev_t g_main_device; COMPOSITOR_INTERNAL void bind_dmabuf(struct wl_client *c, void *data, uint32_t ver, uint32_t id); diff --git a/app/src/main/cpp/waylandcomp/src/droiddeck_color.h b/app/src/main/cpp/waylandcomp/src/droiddeck_color.h index 9591870a1..a578301ae 100644 --- a/app/src/main/cpp/waylandcomp/src/droiddeck_color.h +++ b/app/src/main/cpp/waylandcomp/src/droiddeck_color.h @@ -1,150 +1,63 @@ #ifndef DROIDDECK_COLOR_H #define DROIDDECK_COLOR_H -/* - * HDR10 output on the Wayland backend (opt-in) - the compositor half of HDR_RECON.md Phase A (round 1) - * plus HDR-aware composition (round 2, hdr_compose.h), implemented in wl_color_mgmt.c. The game half - * already ships: Mesa's Wayland WSI in our Turnip is a wp_color_manager_v1 client and exposes - * VK_COLOR_SPACE_HDR10_ST2084_EXT as soon as a compositor advertises BT.2020 + ST 2084; DXVK takes it - * with DXVK_HDR=1 (the app exports it when the setting is on). - * - * THE GATE. Nothing here exists for a session unless all of these hold when the compositor starts: - * - the container's / game's "HDR output" setting is on, or DROIDDECK_WAYLAND_HDR=1 in their - * environment (which overrides the setting either way); - * - the display the game is on lists HDR10 among its supported HDR types (read by the app from - * android.view.Display - a property of the connector, not of the device); - * - display layers with dataspace control (ASurfaceTransaction_setBufferDataSpace, Android 10+) - * and the zero-copy global (banner_ahb_v1): the HDR frame (or the composed HDR picture) is shown - * on the game's own display layer, tagged BT2020_PQ. - * In game, the drawer's HDR output switch (droiddeck_color_set_output) flips between that and the same - * frames tone-mapped to SDR, live; the gate itself never changes during a session. - * A closed gate advertises nothing - no colour-management global, no 10-bit dma-buf formats - so the - * session is byte-for-byte what it was before this file existed, and the session log says why. - * DROIDDECK_WAYLAND_HDR=force skips the display check only (testing the negotiation on an SDR panel; - * SurfaceFlinger then tone-maps the layer, and says so in its composition type). - * - * WHAT AN OPEN GATE OFFERS (exactly the subset Mesa binds, HDR_RECON.md §2.2): wp_color_manager_v1 - * version 1 with the perceptual intent, the parametric creator + mastering-display metadata, - * BT.2020 primaries and the ST 2084 (PQ) transfer function; zwp_linux_dmabuf_v1 adds AB30/XB30 - * (A2B10G10R10, the only 10-bit layout our zero-copy WSI can put in a gralloc buffer). A surface's - * image description is double-buffered on wl_surface.commit; its frames go onto the game's display - * layer tagged BT2020_PQ with the game's SMPTE 2086 / CTA-861.3 metadata (sc_layer.c). - * - * Compositor thread unless noted. - */ #include <stdint.h> #include <wayland-server.h> -/* ADataSpace values (NDK <android/data_space.h>) the layer path emits. */ #define DROIDDECK_ADATASPACE_UNKNOWN 0 -#define DROIDDECK_ADATASPACE_BT2020_PQ 163971072 /* STANDARD_BT2020 | TRANSFER_ST2084 | RANGE_FULL = 0x09c60000 */ -#define DROIDDECK_ADATASPACE_BT2020_HLG 168165376 /* STANDARD_BT2020 | TRANSFER_HLG | RANGE_FULL */ +#define DROIDDECK_ADATASPACE_BT2020_PQ 163971072 +#define DROIDDECK_ADATASPACE_BT2020_HLG 168165376 -/* One image description as the program made it, and what it becomes on a display layer. Immutable: - * a changed description is a new record with a new identity. */ struct droiddeck_color { - uint32_t identity; /* the id the `ready` event carried (never 0) */ - uint32_t primaries, tf; /* wp_color_manager_v1 named values */ - int32_t dataspace; /* ADataSpace on a display layer; 0 = none (treated as sRGB) */ - int has_st2086; /* SMPTE ST 2086: mastering display primaries + luminance */ - float red[2], green[2], blue[2], white[2]; /* CIE 1931 xy */ - float max_lum, min_lum; /* nits */ - int has_cta861; /* CTA-861.3 */ - float max_cll, max_fall; /* nits; 0 = not given */ - char text[256]; /* the session log's one-line summary of it */ + uint32_t identity; + uint32_t primaries, tf; + int32_t dataspace; + int has_st2086; + float red[2], green[2], blue[2], white[2]; + float max_lum, min_lum; + int has_cta861; + float max_cll, max_fall; + char text[256]; }; -/* ---- app -> compositor (JNI, any thread) */ -/* mode 0 = off, 1 = DROIDDECK_WAYLAND_HDR=1, 2 = DROIDDECK_WAYLAND_HDR=force (testing). source names where - * the switch came from; dxvk_hdr = DXVK_HDR=1 is in the game's environment; zero_copy_forced = the - * app turned zero-copy presentation on for this session because HDR needs it. Before the start. */ void droiddeck_color_set_request(int mode, const char *source, int dxvk_hdr, int zero_copy_forced); -/* The display the game is on, as android.view.Display reports it. Before the start (it feeds the - * gate) and again whenever it changes (logged; the gate is decided once per session). */ void droiddeck_color_set_display(int id, const char *name, const char *formats, int hdr10, float max_lum, float max_avg, float min_lum, int ratio_available, float ratio, int api); -/* One reading of Display.getHdrSdrRatio() (API 34+; < 0 = not available). listener = it came from - * the display's ratio listener rather than the app's periodic sampler. */ void droiddeck_color_ratio_sample(float ratio, int listener); -/* Milliseconds since an HDR frame last went onto a display layer; -1 = none this session. */ int droiddeck_color_last_frame_age_ms(void); -/* -1 = the compositor has not decided yet, 0 = closed, 1 = open. */ int droiddeck_color_gate_state(void); -/* The session is ending: write the summary line ("HDR on screen: …"). */ void droiddeck_color_session_end(void); -/* 1 while HDR frames are really on screen: a frame tagged BT2020_PQ reached a display layer or the HDR - * swapchain in the last 1.5 s AND, where the display reports an HDR/SDR ratio, its last reading is - * above 1.01 (the HUD badge). */ int droiddeck_color_hdr_on_screen(void); -/* The HUD / drawer state: 0 = no HDR frames on screen (or not confirmed yet), 1 = HDR frames on screen - * with headroom (as droiddeck_color_hdr_on_screen), 2 = HDR frames on screen but the display has given - * them NO headroom (ratio <= 1.01) for 5 s or more - the brightness slider at maximum, or a screen - * recording (Android turns HDR headroom off while the screen is recorded). */ int droiddeck_color_hdr_state(void); -/* Nits SDR content is placed at inside an HDR picture (default 203, BT.2408); any thread. */ void droiddeck_color_set_sdr_white(float nits); float droiddeck_color_sdr_white(void); -/* The HDR session was asked for (DROIDDECK_WAYLAND_HDR / the setting resolved on or force) - known before - * the compositor starts, so the Vulkan instance can enable VK_EXT_swapchain_colorspace for it. */ int droiddeck_color_requested(void); -/* The drawer's live "HDR output" switch, per session, starting ON. On = HDR frames go to the display as - * HDR; off = the SAME frames are composed tone-mapped to SDR (hdr_compose.h) - the game is told nothing - * and keeps rendering HDR (DXVK_HDR and the colour-manager offer were decided at launch). Compositor - * thread (the app posts it through droiddeck_host_hdr_output); logs the flip and redraws. */ void droiddeck_color_set_output(int on); -/* 1 = HDR output on (any thread). */ int droiddeck_color_output(void); -/* 1 while an HDR game's frames are being shown tone-mapped to SDR (the last one < 1.5 s ago); any thread. */ int droiddeck_color_tonemapped_on_screen(void); -/* Device evidence beside the HDR/SDR headroom (the app, non-root APIs; any thread; HDR sessions only): - * PowerManager thermal status (0..6, -1 unknown) + getThermalHeadroom(10) (< 0 = not available), - * Settings.System SCREEN_BRIGHTNESS (0..255, -1 unknown) + SCREEN_BRIGHTNESS_MODE (1 auto, 0 manual). - * Changes of status and brightness are logged; every no-headroom line, the 10 s line and the verdict - * carry the last values. */ void droiddeck_color_env_sample(int thermal, float headroom, int brightness, int bmode); -/* ---- the explicit HDR headroom request (Android 15+: ASurfaceTransaction_setDesiredHdrHeadroom on the game - * layer, SurfaceView.setDesiredHdrHeadroom on the screen surface). Some phones only boost HDR when asked. */ -/* The ratio to ask for an HDR frame of `c`: content peak (max CLL, else mastering max, else the display's - * peak) / SDR white (203), capped at the display's highest ratio when it reports one; 0 = ask nothing - * (not HDR, or the display reports it cannot boost). `why` gets the one-line reason. Any thread. */ float droiddeck_color_desired_headroom(const struct droiddeck_color *c, char *why, size_t n); -/* What was last asked for (> 0 the ratio, 0 nothing, -1 the API is missing): for the no-headroom lines. */ void droiddeck_color_note_headroom_request(float ratio); -/* Display.getHighestHdrSdrRatio() (Android 16+; <= 0 = not reported). Any thread; logged when it arrives. */ void droiddeck_color_set_highest_ratio(float ratio); -/* The headroom the SCREEN surface should ask for (frames through the HDR10 swapchain in the last 1.5 s), - * 0 = none; `why` gets the reason. The app polls it and applies it. Any thread. */ float droiddeck_color_screen_headroom(char *why, size_t n); -/* ---- compositor.c -> here */ -/* Decide the gate (call after ahb_swapchain_init) and create the global when it is open. */ void droiddeck_color_init(struct wl_display *display); -/* The gate is open: the 10-bit dma-buf formats may be advertised, descriptions may exist. */ int droiddeck_color_hdr_open(void); -/* wl_surface.commit: the pending image description (if any request touched it) becomes current. */ void droiddeck_color_commit(struct wl_resource *surface); -/* The surface's current image description; NULL = none (sRGB, today's default). */ const struct droiddeck_color *droiddeck_color_of(struct wl_resource *surface); -/* A program disconnected: if it presented HDR, its part of the summary is written now. */ void droiddeck_color_client_gone(struct wl_client *client); -/* The 10 s summary tick (one `color` line when anything HDR happened in the window). */ void droiddeck_color_stats_tick(void); -/* A frame of an HDR-described surface went through the compositor's own 8-bit sRGB pass this scene - * (who = the window, reason = why it did not get the display layer). Shown untone-mapped. */ void droiddeck_color_frame_copied(const char *who, const char *reason); -/* ---- sc_layer.c / compositor.c -> here: a NEW frame of an HDR-described surface was shown, and how */ enum droiddeck_hdr_path { - DROIDDECK_HDR_ZERO_COPY = 0, /* the game's own gralloc buffer on its display layer, tagged */ - DROIDDECK_HDR_LAYER_COPY, /* one 8-bit copy of the game's frame on its layer, tagged */ - DROIDDECK_HDR_COMPOSED, /* the whole scene composed into one PQ picture on the game layer (hdr_compose.h) */ - DROIDDECK_HDR_SWAPCHAIN, /* composed into PQ and presented through an HDR10 swapchain (frame generation) */ - DROIDDECK_HDR_TONEMAPPED, /* composed and tone-mapped to SDR (HDR output switched off, or a present that - * cannot carry HDR) - NOT HDR */ + DROIDDECK_HDR_ZERO_COPY = 0, + DROIDDECK_HDR_LAYER_COPY, + DROIDDECK_HDR_COMPOSED, + DROIDDECK_HDR_SWAPCHAIN, + DROIDDECK_HDR_TONEMAPPED, + }; -/* ahb_format = the buffer's AHARDWAREBUFFER_FORMAT_* where one exists (0 otherwise). */ void droiddeck_color_frame_shown(const struct droiddeck_color *c, int path, uint32_t ahb_format); -/* "RGBA1010102 (10-bit)" etc. for an AHARDWAREBUFFER_FORMAT_* value (static buffer). */ const char *droiddeck_ahb_format_name(uint32_t format); #endif diff --git a/app/src/main/cpp/waylandcomp/src/droiddeck_ext.c b/app/src/main/cpp/waylandcomp/src/droiddeck_ext.c index 913f713d3..1172c095b 100644 --- a/app/src/main/cpp/waylandcomp/src/droiddeck_ext.c +++ b/app/src/main/cpp/waylandcomp/src/droiddeck_ext.c @@ -1,11 +1,3 @@ -/* - * Host → compositor message queue plus the init of the extension modules. - * - * The app's threads (UI thread, clipboard listener) hand text to the compositor through - * droiddeck_host_* ; the messages are queued under a mutex and a pipe byte wakes the wl event - * loop, which drains them on the compositor thread - the only thread that may touch - * wl_resources. - */ #define _GNU_SOURCE 1 #include <stdlib.h> #include <string.h> @@ -83,7 +75,7 @@ static int on_wake(int fd, uint32_t mask, void *data) { if (!m) break; switch (m->kind) { case HOST_CLIPBOARD: - clipboard_host_text(m->text, m->len); /* takes the buffer */ + clipboard_host_text(m->text, m->len); m->text = NULL; break; case HOST_TEXT_COMMIT: @@ -114,7 +106,7 @@ void droiddeck_ext_init(struct wl_display *display) { if (pipe2(g_wake, O_CLOEXEC | O_NONBLOCK) == 0) { wl_event_loop_add_fd(loop, g_wake[0], WL_EVENT_READABLE, on_wake, NULL); pthread_mutex_lock(&g_mu); - int queued = g_head != NULL; /* posted before the loop existed (clipboard refresh) */ + int queued = g_head != NULL; pthread_mutex_unlock(&g_mu); if (queued) { char c = 1; ssize_t n = write(g_wake[1], &c, 1); (void)n; } } else diff --git a/app/src/main/cpp/waylandcomp/src/droiddeck_ext.h b/app/src/main/cpp/waylandcomp/src/droiddeck_ext.h index 0dc8ae41a..ca359557e 100644 --- a/app/src/main/cpp/waylandcomp/src/droiddeck_ext.h +++ b/app/src/main/cpp/waylandcomp/src/droiddeck_ext.h @@ -1,53 +1,38 @@ -/* - * Interface between compositor.c and the extension modules that live in their own files: - * wl_clipboard.c wl_data_device_manager + zwlr_data_control_manager_v1 (selection only) - * wl_text_input.c zwp_text_input_manager_v3 (host IME / soft keyboard) - * wl_toplevel_icon.c xdg_toplevel_icon_manager_v1 (accepted, not rendered) - * droiddeck_ext.c the host→compositor message queue shared by the modules - * Everything here runs on the compositor (dispatch) thread unless noted. - */ #pragma once #include <stdint.h> #include <wayland-server.h> void droiddeck_log(const char *tag, const char *fmt, ...); -/* ---- compositor.c → modules */ -void droiddeck_ext_init(struct wl_display *display); /* creates the globals + host queue */ -void droiddeck_clipboard_keyboard_focus(struct wl_client *client); /* keyboard focus moved; NULL = none */ -void droiddeck_text_input_refocus(void); /* the text-input target may have changed */ + +void droiddeck_ext_init(struct wl_display *display); +void droiddeck_clipboard_keyboard_focus(struct wl_client *client); +void droiddeck_text_input_refocus(void); void droiddeck_text_input_surface_gone(struct wl_resource *surface); -/* ---- modules → compositor.c */ const char *droiddeck_client_name(struct wl_client *client); struct wl_display *droiddeck_get_display(void); -/* The surface text input follows (the last clicked window, else the topmost program window). */ struct wl_resource *droiddeck_ime_target(void); -/* Scene (virtual desktop) origin of a toplevel surface; 0,0 when unknown. */ void droiddeck_surface_scene_origin(struct wl_resource *surface, int *x, int *y); -/* Deliver a key press/release through the wl_keyboard path as if typed. */ void droiddeck_inject_key(uint32_t evdev, int pressed); -/* ---- modules → app (JNI upcalls; waylandcomp_jni.c) */ void droiddeck_on_clipboard_text(const char *utf8, int len); void droiddeck_on_text_input(int enabled, const char *program, int x, int y, int w, int h); -/* ---- app → modules (callable from any thread; queued to the compositor thread) */ -void droiddeck_host_clipboard_text(const char *utf8, int len); /* len 0 = clear */ + +void droiddeck_host_clipboard_text(const char *utf8, int len); void droiddeck_host_text_commit(const char *utf8, int len); void droiddeck_host_text_preedit(const char *utf8, int len, int cursor_begin, int cursor_end); void droiddeck_host_text_delete(int before, int after); -void droiddeck_host_zero_copy(int on, int live); /* the drawer's zero-copy switch (ahb_swapchain_set_mode) */ -void droiddeck_host_hdr_output(int on); /* the drawer's HDR output switch (droiddeck_color_set_output) */ +void droiddeck_host_zero_copy(int on, int live); +void droiddeck_host_hdr_output(int on); -/* ---- module internals reached through the queue (droiddeck_ext.c → modules) */ void clipboard_init(struct wl_display *display); -void clipboard_host_text(char *utf8, size_t len); /* takes ownership of utf8 */ +void clipboard_host_text(char *utf8, size_t len); void text_input_init(struct wl_display *display); void text_input_host_commit(const char *utf8, size_t len); void text_input_host_preedit(const char *utf8, size_t len, int cursor_begin, int cursor_end); void text_input_host_delete(int before, int after); void toplevel_icon_init(struct wl_display *display); void single_pixel_init(struct wl_display *display); -/* A wp_single_pixel_buffer_manager_v1 buffer: 1 and its pixel (B, G, R, A, premultiplied), else 0. */ int single_pixel_buffer_get(struct wl_resource *buffer, uint8_t bgra[4]); diff --git a/app/src/main/cpp/waylandcomp/src/effects_chain.c b/app/src/main/cpp/waylandcomp/src/effects_chain.c index 7e446e299..aa5e5cb9d 100644 --- a/app/src/main/cpp/waylandcomp/src/effects_chain.c +++ b/app/src/main/cpp/waylandcomp/src/effects_chain.c @@ -1,16 +1,12 @@ -/* Screen-effect chain - see effects_chain.h. Compositor thread unless noted. */ #define _POSIX_C_SOURCE 200809L #include "effects_chain.h" #include "vk_loader.h" -#include "vk_present.h" /* droiddeck_log */ +#include "vk_present.h" #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -/* The X11 Vulkan renderer's SPIR-V, pre-compiled by app/src/main/cpp/framegen/gen_shaders.sh - * (glslangValidator --vn) and committed next to the GLSL: the NDK build compiles no shaders, so - * there is no build-time tooling for CI to break. Same bytes as the X11 chain runs. */ #include "upscale_vert.h" #include "sgsr_frag.h" #include "sgsr_quality_frag.h" @@ -26,25 +22,24 @@ #include "crt_frag.h" #include "deband_frag.h" -#define FX_FORMAT VK_FORMAT_R8G8B8A8_UNORM /* the default: every SDR frame */ -/* The chain's target format and the scene's view format. R8G8B8A8 always, except while an HDR scene - * runs through the chain (hdr_compose.h): then the scene and the targets are A2B10G10R10, so a PQ - * picture keeps 10 bits through every pass. A change rebuilds the chain's objects once - * (vkp_effects_set_formats). */ +#define FX_FORMAT VK_FORMAT_R8G8B8A8_UNORM + + + + static VkFormat g_fx_fmt = FX_FORMAT, g_scene_fmt = FX_FORMAT; -#define FX_MAX_DIM 4096 /* a FILL-mapped scene can exceed the panel; cap the chain resolution */ +#define FX_MAX_DIM 4096 -/* ------------------------------------------------------------------ settings */ struct fx_settings { - int scaling; /* 0..8 */ - int sharp_pct; /* 0..100, the upscaler's own slider */ + int scaling; + int sharp_pct; int cas_on, cas_pct; int hdr_on; - int deband_on, deband_pct; /* 0..200 */ - int color_on; /* derived: grade not neutral */ - float brightness, contrast, gamma, saturation; /* shader units (already mapped/clamped) */ - float b_raw, c_raw, s_raw; /* slider units, for the log */ + int deband_on, deband_pct; + int color_on; + float brightness, contrast, gamma, saturation; + float b_raw, c_raw, s_raw; int fxaa, toon, crt, ntsc; char look[48]; }; @@ -54,7 +49,7 @@ static struct fx_settings g_set = { .scaling = 0, .sharp_pct = 75, .cas_on = 0, .cas_pct = 60, .deband_pct = 100, .gamma = 1.0f, .saturation = 1.0f, .s_raw = 100.0f, .look = "Off"}; static volatile int g_changed; -static struct fx_settings g_cur; /* the compositor thread's per-frame snapshot */ +static struct fx_settings g_cur; static int g_cur_valid; static void set_begin(void) { pthread_mutex_lock(&g_lock); } @@ -83,7 +78,6 @@ void vkp_effects_set_deband(int enabled, int pct) { static float clampf(float v, float lo, float hi) { return v < lo ? lo : v > hi ? hi : v; } void vkp_effects_set_screen(float brightness, float contrast, float gamma, float saturation, int fxaa, int toon, int crt, int ntsc) { - /* Same mapping as VulkanRendererContext::setColorGrade (GL ColorEffect clamps). */ int on = !(brightness == 0.0f && contrast == 0.0f && gamma == 1.0f && saturation == 100.0f); set_begin(); g_set.color_on = on; @@ -157,18 +151,17 @@ VkFilter vkp_effects_blit_filter(void) { return g_cur.scaling == 2 ? VK_FILTER_NEAREST : VK_FILTER_LINEAR; } -/* ------------------------------------------------------------------ push constants - * Byte-identical to VulkanRendererContext.h: vec4 ndc first (upscale.vert), the rest fragment-only. */ -struct pc_sgsr { float ndc[4]; float vp[4]; float edge; }; /* also NIS: vp + sharpness */ + + +struct pc_sgsr { float ndc[4]; float vp[4]; float edge; }; struct pc_easu { float ndc[4]; uint32_t con0[4], con1[4], con2[4], con3[4]; float outW, outH; }; struct pc_rcas { float ndc[4]; uint32_t con[4]; float outW, outH; }; -struct pc_res { float ndc[4]; float res[2]; float extra; float pad; }; /* fxaa/toon/hdr/crt (res), cas (sharpness), ntsc (frameCount), deband (strength) */ +struct pc_res { float ndc[4]; float res[2]; float extra; float pad; }; struct pc_color { float ndc[4]; float b, c, g, s; }; -#define PC_MAX_SIZE sizeof(struct pc_easu) /* 88 bytes, the largest */ +#define PC_MAX_SIZE sizeof(struct pc_easu) static uint32_t packf(float f) { uint32_t u; memcpy(&u, &f, 4); return u; } -/* Mirrors ffx_fsr1.h FsrEasuCon / FsrRcasCon exactly as the X11 renderer packs them. */ static void easu_con(struct pc_easu *e, float inW, float inH, float outW, float outH) { e->con0[0] = packf(inW / outW); e->con0[1] = packf(inH / outH); e->con0[2] = packf(0.5f * inW / outW - 0.5f); e->con0[3] = packf(0.5f * inH / outH - 0.5f); @@ -183,7 +176,6 @@ static void rcas_con(struct pc_rcas *r, float scale01) { r->con[0] = packf(scale01); r->con[1] = 0; r->con[2] = 0; r->con[3] = 0; } -/* ------------------------------------------------------------------ Vulkan objects */ struct fx_target { VkImage img; VkDeviceMemory mem; VkImageView view; VkFramebuffer fb; VkDescriptorSet ds; @@ -193,16 +185,16 @@ struct fx_target { static VkDevice g_dev; static VkPhysicalDevice g_pd; static VkPhysicalDeviceMemoryProperties g_mp; -static int g_ready; /* 0 = objects not built, 1 = ok, -1 = failed (chain disabled, said once) */ +static int g_ready; static VkRenderPass g_rp; static VkDescriptorSetLayout g_dsl; static VkPipelineLayout g_pl; static VkDescriptorPool g_dpool; -static VkSampler g_sampler; /* linear, clamp-to-edge: what the X11 chain samples its targets with */ +static VkSampler g_sampler; enum { P_SGSR, P_SGSR_HQ, P_NIS, P_EASU, P_RCAS, P_CAS, P_COLOR, P_FXAA, P_TOON, P_HDR, P_NTSC, P_CRT, P_DEBAND, P_COUNT }; static VkPipeline g_pipe[P_COUNT]; -static struct fx_target g_t[2], g_mid; /* ping-pong pair + the EASU output (FSR only) */ -/* The scene image belongs to vk_present; only its view + descriptor live here. */ +static struct fx_target g_t[2], g_mid; + static struct { VkImage img; VkImageView view; VkDescriptorSet ds; int w, h; } g_scene; static uint32_t g_ntsc_frame; @@ -249,15 +241,9 @@ static void target_destroy(struct fx_target *t) { memset(t, 0, sizeof(*t)); } -/* A chain target: rendered into (colour attachment), sampled by the next pass, and blitted to - * the swapchain when it ends up holding the result. Frames are fenced, so a resize never races - * the GPU. 0 = ok. */ static int target_ensure(struct fx_target *t, int w, int h, const char *what) { if (t->img && t->w == w && t->h == h) return 0; target_destroy(t); - /* Unrestricted mutable views keep these render-then-blit targets uncompressed on Turnip. - * UBWC targets corrupt even a constant-colour pass on Adreno 740 (FSR becomes black). - * Only the chain's intermediate images need this; the scene and guest images stay unchanged. */ VkImageCreateInfo ii = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, .imageType = VK_IMAGE_TYPE_2D, .format = g_fx_fmt, .extent = {(uint32_t)w, (uint32_t)h, 1}, .mipLevels = 1, @@ -316,8 +302,6 @@ static VkShaderModule shader(const uint32_t *code, size_t size) { return m; } -/* One full-quad post pipeline: upscale.vert + the effect's fragment shader, opaque, dynamic - * viewport/scissor - the X11 renderer's createPostPipeline. */ static VkPipeline post_pipeline(VkShaderModule vert, const uint32_t *frag_code, size_t frag_size, const char *name) { VkShaderModule frag = shader(frag_code, frag_size); if (!frag) { droiddeck_log("error", "effects: %s: vkCreateShaderModule failed", name); return VK_NULL_HANDLE; } @@ -370,7 +354,6 @@ void vkp_effects_destroy(void) { objects_destroy(); g_dev = VK_NULL_HANDLE; } void vkp_effects_set_formats(VkFormat scene_fmt, VkFormat target_fmt) { if (target_fmt != g_fx_fmt) { - /* The render pass, the pipelines and the targets are built for one format: start over. */ if (g_ready) objects_destroy(); g_fx_fmt = target_fmt; g_scene_fmt = scene_fmt; @@ -387,14 +370,9 @@ void vkp_effects_set_formats(VkFormat scene_fmt, VkFormat target_fmt) { } } -/* Build every device object once, on the first active frame. 0 = ok, -1 = the chain is - * unavailable for this session (logged once; frames fall back to the plain blit). */ static int objects_ensure(void) { if (g_ready) return g_ready == 1 ? 0 : -1; if (!g_dev || !g_vk.CreateGraphicsPipelines) { g_ready = -1; droiddeck_log("error", "effects: no device for the chain"); return -1; } - /* Render pass: one colour target, overwritten whole (DONT_CARE), left SHADER_READ_ONLY for the - * next pass. The dependencies order this pass after whoever read the target last (previous - * pass / previous frame's blit) and before whoever samples or blits it next. */ VkAttachmentDescription att = {.format = g_fx_fmt, .samples = VK_SAMPLE_COUNT_1_BIT, .loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, @@ -465,7 +443,6 @@ fail: return -1; } -/* ------------------------------------------------------------------ recording */ static void barrier(VkCommandBuffer cmd, VkImage img, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags src_stage, VkAccessFlags src_access, @@ -477,7 +454,6 @@ static void barrier(VkCommandBuffer cmd, VkImage img, VkImageLayout from, VkImag g_vk.CmdPipelineBarrier(cmd, src_stage, dst_stage, 0, 0, NULL, 0, NULL, 1, &b); } -/* One pass: sample `src` (a descriptor in SHADER_READ_ONLY_OPTIMAL), draw the full quad into `dst`. */ static void pass(VkCommandBuffer cmd, VkPipeline p, struct fx_target *dst, VkDescriptorSet src, const void *pc, uint32_t pcsz) { VkRenderPassBeginInfo rpi = {.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .renderPass = g_rp, .framebuffer = dst->fb, @@ -504,36 +480,26 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce s = &g_cur; *res_w = scene_w; *res_h = scene_h; - /* The chain's resolution: the scene's mapped size (X11 runs its effects at the output extent, - * with the scene already scaled by the base sampler). Clamped so a FILL-mapped 4K scene does - * not allocate beyond reason; the final blit resizes the remainder. */ int cw = out_w > 0 ? out_w : scene_w, ch = out_h > 0 ? out_h : scene_h; if (cw > FX_MAX_DIM) cw = FX_MAX_DIM; if (ch > FX_MAX_DIM) ch = FX_MAX_DIM; if (cw < 1) cw = 1; if (ch < 1) ch = 1; - /* Spatial upscalers only have work when the scene is shown larger than it is (the X11 - * renderer's "render below display" gate); Sharpen (RCAS) runs at any size. */ const int upscaling = cw > scene_w || ch > scene_h; int spatial = 0; if (spatial_mode(s->scaling) && (s->scaling == 6 || upscaling)) spatial = s->scaling; const int nfx = effect_count(s); const int resize = cw != scene_w || ch != scene_h; - /* Passes that write the ping-pong pair: the scaling/resize stage (if any) + the effects. - * Two targets are needed once any pass reads what an earlier one wrote. */ const int npasses = ((spatial || resize) ? 1 : 0) + nfx; if ((spatial || nfx) && objects_ensure() == 0 && scene_ensure(scene, scene_w, scene_h) == 0 && target_ensure(&g_t[0], cw, ch, "chain") == 0 && (npasses < 2 || target_ensure(&g_t[1], cw, ch, "chain") == 0) && (!(spatial == 4 || spatial == 5) || target_ensure(&g_mid, cw, ch, "EASU") == 0)) { - /* `cur` = the image the next pass samples; `cur_ds` its descriptor. Every pass writes the - * target that is not being read (the pair alternates; the scene is read only first). */ VkDescriptorSet cur_ds; - struct fx_target *cur = NULL; /* NULL = the scene image */ + struct fx_target *cur = NULL; #define FREE_TARGET() ((cur == &g_t[0]) ? &g_t[1] : &g_t[0]) const float sharp01 = (float)s->sharp_pct / 100.0f; - /* ---- stage 1: scaling (scene -> chain res) */ if (spatial) { barrier(cmd, scene, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, @@ -544,20 +510,17 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce SET_NDC(pc); pc.vp[0] = 1.0f / (float)scene_w; pc.vp[1] = 1.0f / (float)scene_h; pc.vp[2] = (float)scene_w; pc.vp[3] = (float)scene_h; - /* SGSR EdgeSharpness 0.5 (slider 0) .. 4.5 (slider 100); NIS takes the slider as is. */ pc.edge = spatial == 7 ? sharp01 : 0.5f + sharp01 * 4.0f; pass(cmd, g_pipe[spatial == 3 ? P_SGSR : spatial == 8 ? P_SGSR_HQ : P_NIS], dst, g_scene.ds, &pc, sizeof(pc)); } else if (spatial == 6) { - /* Sharpen: RCAS fetches the scene 1:1 (outputSize = scene size) while the quad - * covers the chain target = nearest scale + sharpen, as the X11 renderer does. */ struct pc_rcas pc = {0}; SET_NDC(pc); rcas_con(&pc, sharp01); pc.outW = (float)scene_w; pc.outH = (float)scene_h; pass(cmd, g_pipe[P_RCAS], dst, g_scene.ds, &pc, sizeof(pc)); - } else { /* FSR (4) / FSR Fit (5): EASU scene -> mid, RCAS mid -> dst, both at chain res. - * Fill vs fit is the Fullscreen Mode's business here (the mapping), so both - * run the same EASU+RCAS pair. */ + } else { + + struct pc_easu e = {0}; struct pc_rcas r = {0}; SET_NDC(e); @@ -571,8 +534,6 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce } cur = dst; cur_ds = dst->ds; } else if (resize) { - /* No spatial pass: a plain filtered resize to the chain resolution first (Linear or - * Nearest per the scaling mode), so the effects run at output resolution like X11. */ struct fx_target *dst = FREE_TARGET(); barrier(cmd, scene, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, @@ -588,16 +549,12 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); cur = dst; cur_ds = dst->ds; } else { - /* Scene already at output size: the first effect samples it directly. */ barrier(cmd, scene, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); cur = NULL; cur_ds = g_scene.ds; } - /* ---- stage 2: the effects, in the X11 renderer's locked order - * FXAA -> Toon -> Colour -> CAS -> HDR -> NTSC -> CRT -> Debanding (terminal dither). - * Each writes the free ping-pong target; the one just read becomes free. */ enum { FX_FXAA, FX_TOON, FX_COLOR, FX_CAS, FX_HDR, FX_NTSC, FX_CRT, FX_DEBAND }; int order[8], n = 0; if (s->fxaa) order[n++] = FX_FXAA; @@ -626,7 +583,6 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce case FX_CAS: pr.extra = (float)s->cas_pct / 100.0f; pass(cmd, g_pipe[P_CAS], dst, cur_ds, &pr, sizeof(pr)); break; case FX_HDR: pass(cmd, g_pipe[P_HDR], dst, cur_ds, &pr, sizeof(pr)); break; case FX_NTSC: - /* cos/sin(chromaPhase*0.5) has period 4 in the counter: mod 4 keeps the shimmer exact. */ pr.extra = (float)(g_ntsc_frame++ & 3u); pass(cmd, g_pipe[P_NTSC], dst, cur_ds, &pr, sizeof(pr)); break; @@ -638,23 +594,18 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce #undef FREE_TARGET if (cur) { - /* The result: written by the last render pass, now for the output blit. */ barrier(cmd, cur->img, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); *res_w = cur->w; *res_h = cur->h; return cur->img; } - /* Nothing ran after all (cannot happen with nfx > 0): the scene was moved to - * SHADER_READ_ONLY above; bring it to TRANSFER_SRC for the blit. */ barrier(cmd, scene, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); return scene; } - /* Nothing to do (a spatial mode with no room to upscale and no effect), or the chain is - * unavailable: present the scene as composited. */ barrier(cmd, scene, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); diff --git a/app/src/main/cpp/waylandcomp/src/effects_chain.h b/app/src/main/cpp/waylandcomp/src/effects_chain.h index 9b07420df..50e5e3494 100644 --- a/app/src/main/cpp/waylandcomp/src/effects_chain.h +++ b/app/src/main/cpp/waylandcomp/src/effects_chain.h @@ -1,85 +1,33 @@ #ifndef EFFECTS_CHAIN_H #define EFFECTS_CHAIN_H -/* - * Screen-effect chain for the embedded Wayland compositor: the X11 Vulkan renderer's post - * chain (the .frag files in app/src/main/cpp/framegen, VulkanRendererContext::recordUpscalePasses) run - * between the composited scene and the output blit, with the same SPIR-V, push-constant - * layouts, uniform ranges and pass order, so a saved preset looks the same on both backends. - * - * Present-pass hook order (vk_present.c, vkp_render): - * scene (all draws blitted 1:1 into a scene-sized image) - * -> vkp_effects_run() this file: scaling -> effects, in the X11 order - * -> vkp_framegen_run() frame generation (feat/wayland-framegen; inserted after this) - * -> update_map / blit Fullscreen Mode + Screen Alignment, onto the swapchain image - * -> present - * When nothing is on, vkp_effects_active() is 0 and vkp_render keeps its plain path (the draws - * are blitted straight through the mapping; only the blit filter follows Linear/Nearest). - * - * Pass order (LOCKED to the X11 Vulkan renderer): - * scaling (SGSR / SGSR HQ / NIS / FSR EASU+RCAS / Sharpen=RCAS; None/Linear/Nearest = a - * filtered resize) -> FXAA -> Toon -> Colour -> CAS -> HDR -> NTSC -> CRT -> Debanding. - * Scaling resizes the scene to its mapped output size (there is no render scale on Wayland); - * every later pass runs at that resolution and ping-pongs between two targets. - * - * Settings come from any thread (JNI); the compositor thread snapshots them once per frame and - * logs one `effects` line per change. Vulkan objects are created lazily on the first active frame. - */ #define VK_USE_PLATFORM_ANDROID_KHR #include <vulkan/vulkan.h> -/* Compositor thread, from dev_init: the device the chain builds its objects on. */ void vkp_effects_bind_device(VkDevice dev, VkPhysicalDevice pd, const VkPhysicalDeviceMemoryProperties *memprops); -/* Compositor thread: pick up settings changed since the last call. Returns 1 when something - * changed (the caller redraws even a static scene); logs the `effects` line. */ int vkp_effects_sync(void); -/* Compositor thread: 1 when the frame must go through the scene image + chain (any effect on, - * or a spatial scaling mode). Modes None/Linear/Nearest alone are NOT active: the plain blit - * handles them through vkp_effects_blit_filter(). */ int vkp_effects_active(void); -/* The filter for the scene -> output resize: NEAREST for scaling mode 2, else LINEAR. */ VkFilter vkp_effects_blit_filter(void); -/* THE HOOK. Records the chain into `cmd`. `scene` (scene_w x scene_h, R8G8B8A8_UNORM, in - * TRANSFER_DST_OPTIMAL after the composite) -> the image to present, returned in - * TRANSFER_SRC_OPTIMAL with its size in res_w / res_h. out_w/out_h = the scene's mapped size on - * the output (what the scaling modes resize to; the caller maps the result over the whole scene - * afterwards, so Fullscreen Mode / Alignment still apply after the chain). Returns `scene` itself - * (transitioned to TRANSFER_SRC_OPTIMAL) when there is nothing to do or the chain is unavailable. */ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int scene_h, int out_w, int out_h, int *res_w, int *res_h); -/* Compositor thread: drop every Vulkan object (device teardown / device lost). */ void vkp_effects_destroy(void); -/* Compositor thread: release the cached source view/descriptor before its owner destroys img. - * A later image can reuse the same Vulkan handle and dimensions; that is not the same resource. */ void vkp_effects_forget_image(VkImage img); -/* Compositor thread, before vkp_effects_run(): the format of the scene image it will be handed and the - * format its own targets use. R8G8B8A8 / R8G8B8A8 for every SDR frame (the default); A2B10G10R10 / - * A2B10G10R10 while an HDR picture runs through it (hdr_compose.h). A target-format change rebuilds the - * chain's objects on its next run (once per switch). */ void vkp_effects_set_formats(VkFormat scene_fmt, VkFormat target_fmt); -/* ---- settings (any thread; values 1:1 with the X11 Vulkan renderer's setters) ---- */ -/* 0=None 1=Linear 2=Nearest 3=SGSR 4=FSR 5=FSR Fit 6=Sharpen 7=NIS 8=SGSR HQ */ void vkp_effects_set_scaling(int mode); -/* Slider 0..100: RCAS lobe scale (FSR/Sharpen), SGSR EdgeSharpness 0.5..4.5, NIS sharpness. */ void vkp_effects_set_upscale_sharpness(int pct); -/* CAS toggle + slider 0..100 (0 = the pass is off). */ void vkp_effects_set_cas(int enabled, int pct); void vkp_effects_set_hdr(int enabled); -/* Terminal dither: slider 0..200 -> strength/100 LSBs. */ void vkp_effects_set_deband(int enabled, int pct); -/* Colour grade in slider units (brightness/contrast -100..100, gamma 0.5..3, saturation 0..200 - * percent; neutral = 0,0,1,100 turns the pass off) + the four shader toggles. */ void vkp_effects_set_screen(float brightness, float contrast, float gamma, float saturation, int fxaa, int toon, int crt, int ntsc); -/* The Look the current controls match ("Custom" when none), for the log line only. */ void vkp_effects_set_look(const char *name); #endif diff --git a/app/src/main/cpp/waylandcomp/src/framegen_bridge.c b/app/src/main/cpp/waylandcomp/src/framegen_bridge.c index ac2100365..3c311b54f 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_bridge.c +++ b/app/src/main/cpp/waylandcomp/src/framegen_bridge.c @@ -1,4 +1,3 @@ -/* Frame generation on the Wayland backend - see framegen_bridge.h. */ #define _POSIX_C_SOURCE 200809L #include "framegen_bridge.h" #include "framegen_engine.h" @@ -15,20 +14,18 @@ void droiddeck_log(const char *tag, const char *fmt, ...) __attribute__((format( #define FGLOG(...) droiddeck_log("framegen", __VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "DroidDeckWayland", __VA_ARGS__) -/* ---- app controls (written from any thread, read on the compositor thread) ------------- */ static _Atomic int g_kind = VKP_FG_ENGINE_LSFG; static _Atomic int g_armed; static _Atomic int g_mult = 2; -static _Atomic int g_target; /* LSFG adaptive fps, 0 = fixed multiplier */ +static _Atomic int g_target; static _Atomic int g_model = 3; static _Atomic int g_preset = 2; static _Atomic int g_cfg_dirty = 1; -static float g_flow = 0.8f, g_refresh_hz; /* under g_lock */ -static char *g_cache_path; /* under g_lock */ -static _Atomic int g_cache_gen; /* bumps when the path changes */ +static float g_flow = 0.8f, g_refresh_hz; +static char *g_cache_path; +static _Atomic int g_cache_gen; static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; -/* ---- device -------------------------------------------------------------------------- */ static VkDevice g_dev; static VkInstance g_inst; static PFN_vkGetInstanceProcAddr g_gipa; @@ -50,12 +47,10 @@ static struct { PFN_vkDeviceWaitIdle DeviceWaitIdle; } vk; -/* ---- engine + generation ring (compositor thread) ------------------------------------ */ -static int g_engine_kind = -1; /* kind of the running engine, -1 = none */ -static int g_engine_cache_gen = -1; /* the LSFG cache it loaded */ + +static int g_engine_kind = -1; +static int g_engine_cache_gen = -1; static int g_engine_ok; -/* The engine of this kind (with this cache) could not start or build its chain; not retried - * every frame - a new engine selection or a new cache clears it. */ static _Atomic int g_failed_kind = -1; static _Atomic int g_failed_cache_gen = -1; @@ -65,11 +60,10 @@ static uint32_t g_ring_n, g_ring_w, g_ring_h; static VkFormat g_ring_fmt; static uint64_t g_source_frames; -static uint32_t g_planned; /* generations planned for the current source frame */ +static uint32_t g_planned; static int g_was_armed, g_generating_logged; static uint32_t g_built_w, g_built_h; static VkFormat g_built_fmt; -/* Deeper formats (FP16 for HDR frames) the engine failed to build its chain in this session. */ static VkFormat g_refused_fmt[4]; static int g_refused_n; static int format_refused(VkFormat f) { @@ -77,10 +71,10 @@ static int format_refused(VkFormat f) { return 0; } -/* ---- stats ------------------------------------------------------------------------- */ -static _Atomic unsigned g_stat_generated; /* since the last stats_take */ + +static _Atomic unsigned g_stat_generated; static uint64_t g_total_generated; -static float g_presented_rate, g_source_rate; /* smoothed, 0.5 s windows */ +static float g_presented_rate, g_source_rate; static uint32_t g_present_accum, g_source_accum; static int64_t g_window_start_ns; @@ -89,7 +83,6 @@ static int64_t now_ns(void) { return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec; } -/* ====================================================================== app controls */ void vkp_framegen_set_engine(int kind) { if (kind != VKP_FG_ENGINE_LSFG && kind != VKP_FG_ENGINE_WINFG) kind = VKP_FG_ENGINE_LSFG; @@ -100,7 +93,6 @@ void vkp_framegen_set_engine(int kind) { void vkp_framegen_set_armed(int armed, int multiplier, int target_fps) { if (target_fps < 0) target_fps = 0; - /* Adaptive sizes the ring and the swapchain for the most it may ever generate. */ if (target_fps) multiplier = VKP_FG_MAX_GENERATIONS + 1; if (multiplier < 2) multiplier = 2; if (multiplier > VKP_FG_MAX_GENERATIONS + 1) multiplier = VKP_FG_MAX_GENERATIONS + 1; @@ -147,8 +139,6 @@ static void mark_failed(int kind) { atomic_store(&g_failed_kind, kind); } -/* Same codes as VulkanRenderer.getFrameGenProblem(); computed from the live state so an engine - * switch from the app is answered at once. */ int vkp_framegen_problem(void) { if (!g_dev_ready) return -1; int kind = atomic_load(&g_kind); @@ -162,8 +152,6 @@ const char *vkp_framegen_caps_reason(void) { return fge_caps_reason(); } void vkp_framegen_stats(float out[6]) { float accepted = 0.0f, src = 0.0f, thermal = -1.0f; if (g_engine_ok) fge_telemetry(&accepted, &src, &thermal); - /* Planned is what is generated; the governor is off on both paths. Our own measured - * source rate serves both engines (Win-FG measures none). */ out[0] = (float)g_planned; out[1] = (float)g_planned; out[2] = g_source_rate; @@ -174,7 +162,6 @@ void vkp_framegen_stats(float out[6]) { unsigned vkp_framegen_stats_take(void) { return atomic_exchange(&g_stat_generated, 0u); } -/* ====================================================================== device setup */ const void *vkp_framegen_device_features(VkInstance inst, PFN_vkGetInstanceProcAddr gipa, VkPhysicalDevice pd) { @@ -199,7 +186,6 @@ void vkp_framegen_device_ready(VkDevice dev, VkQueue queue, uint32_t qfam, int f void vkp_framegen_device_lost(void) { g_dev_lost = 1; } -/* ====================================================================== ring */ static void destroy_ring(void) { if (!g_ring_n) return; @@ -220,9 +206,6 @@ static int memory_type(uint32_t bits, VkMemoryPropertyFlags want) { return -1; } -/* The generation targets: written by the chain through a storage view, read by the screen - * blit. Kept in GENERAL for their whole life (both uses allow it), so there is no layout dance - * per frame - only access barriers. */ static int ensure_ring(uint32_t w, uint32_t h, VkFormat fmt, uint32_t n) { if (n > VKP_FG_MAX_GENERATIONS) n = VKP_FG_MAX_GENERATIONS; if (g_ring_n == n && g_ring_w == w && g_ring_h == h && g_ring_fmt == fmt) return 1; @@ -257,7 +240,6 @@ static int ensure_ring(uint32_t w, uint32_t h, VkFormat fmt, uint32_t n) { return 1; } -/* ====================================================================== present path */ static int engine_usable(void) { int kind = atomic_load(&g_kind); @@ -270,8 +252,6 @@ int vkp_framegen_format_ok(VkFormat fmt) { return fge_format_ok(fmt); } -/* The engine could not build its chain in a deeper format: refuse the format for the session and - * restart the engine on the next frame (a failed build is sticky inside both engines). */ static void refuse_format(VkFormat fmt, int kind, int w, int h, const char *what) { if (g_refused_n < (int)(sizeof(g_refused_fmt) / sizeof(g_refused_fmt[0]))) g_refused_fmt[g_refused_n++] = fmt; FGLOG("%s could not %s in format %d (the HDR picture's FP16) at %dx%d: it restarts, and HDR frames go through " @@ -321,7 +301,6 @@ static int ensure_engine(int kind) { return 1; } -/* "x3 (2 interpolated frames per game frame)" or "adaptive to 90 fps (up to 3 ...)". */ static const char *describe_mode(char *buf, size_t n, int mult, int target) { if (target) snprintf(buf, n, "adaptive to %d fps (up to %d interpolated frames per game frame)", target, mult - 1); else snprintf(buf, n, "x%d (%d interpolated frame%s per game frame)", mult, mult - 1, mult == 2 ? "" : "s"); @@ -397,9 +376,6 @@ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view, if (n_gen > g_ring_n) n_gen = g_ring_n; g_planned = n_gen; - /* The scene image was just written (blits, and whatever the effects chain did); make that - * visible to the engine's copy (TRANSFER) and shaders (COMPUTE). Fresh ring images get their - * one UNDEFINED -> GENERAL transition here; the others need TRANSFER-read -> COMPUTE-write. */ VkImageMemoryBarrier bars[1 + VKP_FG_MAX_GENERATIONS]; uint32_t nb = 0; bars[nb++] = (VkImageMemoryBarrier){ @@ -425,15 +401,12 @@ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, nb, bars); - /* Frame N in; the shared passes (the whole flow pyramid) run only when generating. */ fge_process(cmd, scene, (uint32_t)w, (uint32_t)h, n_gen); for (uint32_t g = 0; g < n_gen; g++) { fge_generate(cmd, g, n_gen, g_ring[g].img, g_ring[g].view, (uint32_t)w, (uint32_t)h); gens[g] = g_ring[g].img; } if (n_gen) { - /* Compute writes -> the screen blits (this command buffer and the later per-present ones; - * a barrier's first scope covers everything submitted earlier on the queue). */ nb = 0; for (uint32_t g = 0; g < n_gen; g++) bars[nb++] = (VkImageMemoryBarrier){ @@ -466,7 +439,7 @@ void vkp_framegen_presented(int generated) { g_total_generated += (uint64_t)generated; } float elapsed = (float)(now - g_window_start_ns) / 1e9f; - if (elapsed < 0.5f) return; /* half-second window, like the X11 renderer */ + if (elapsed < 0.5f) return; float rate = (float)g_present_accum / elapsed, src = (float)g_source_accum / elapsed; g_presented_rate = g_presented_rate > 0.0f ? g_presented_rate + (rate - g_presented_rate) * 0.25f : rate; g_source_rate = g_source_rate > 0.0f ? g_source_rate + (src - g_source_rate) * 0.25f : src; diff --git a/app/src/main/cpp/waylandcomp/src/framegen_bridge.h b/app/src/main/cpp/waylandcomp/src/framegen_bridge.h index b60229cdb..5e27db40d 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_bridge.h +++ b/app/src/main/cpp/waylandcomp/src/framegen_bridge.h @@ -1,27 +1,5 @@ #ifndef FRAMEGEN_BRIDGE_H #define FRAMEGEN_BRIDGE_H -/* - * Frame generation on the Wayland backend (feat/wayland-framegen). - * - * The two NATIVE engines the X11 renderer runs inside Bannerlator's X11 compositor - LSFG Native - * (the user's Lossless.dll chain, lsfg/) and Win-FG Native (our own FSR3-derived optical-flow - * chain, winfg/) - are hosted here inside the Wayland compositor's Turnip device. Both consume - * consecutive presented frames and emit interpolated frames between them; the guest never - * sees any of it. bionic-fg (the guest-side win-fg Vulkan layer) is X11-only and is not - * ported: its frames are born inside the guest and have nothing to attach to here. - * - * Present order per scene frame (vk_present.c): - * scene -> effects -> FRAME GENERATION -> mapping/blit -> swapchain - * The scene is composed 1:1 into an off-screen image, the effects chain runs on it, then - * vkp_framegen_run() (the one hook) feeds that image to the engine and hands back up to three - * generated images. vk_present.c blits the generated frames to the screen FIRST and the real - * frame LAST (interpolation produces frames that belong between N-1 and N), one FIFO present - * each, so the panel shows them on consecutive vblanks - the same pacing the X11 native path - * is device-proven with. No sleeps, no host-side pacer. - * - * Threads: the set_* controls are called from the app (JNI, any thread) and only store - * values; everything that touches Vulkan runs on the compositor thread inside vkp_render(). - */ #include <stdint.h> #ifndef VK_USE_PLATFORM_ANDROID_KHR #define VK_USE_PLATFORM_ANDROID_KHR @@ -29,63 +7,28 @@ #include <vulkan/vulkan.h> enum { VKP_FG_ENGINE_LSFG = 0, VKP_FG_ENGINE_WINFG = 1 }; -/* Hard ceiling on generated frames per source frame (2x..4x -> 1..3), as on X11. */ #define VKP_FG_MAX_GENERATIONS 3 -/* ---- app controls (any thread) ------------------------------------------------------- */ void vkp_framegen_set_engine(int kind); -/* multiplier 2..4, or target_fps > 0 for LSFG's adaptive mode (up to 4x, as the target needs); - * armed = 0 stops generating (the ring and the engine stay for a re-arm). */ void vkp_framegen_set_armed(int armed, int multiplier, int target_fps); -/* LSFG Native: the SPIR-V cache built from the user's Lossless.dll (Lossless.sync); a new path reloads the engine. */ void vkp_framegen_set_lsfg_cache_path(const char *path); -/* Flow scale 0.25..1.0 and the panel's real refresh rate (the pacer never generates above it). */ void vkp_framegen_set_tuning(float flow_scale, float refresh_hz); -/* Win-FG Native: interpolation model (3/4) and performance preset (0..2). */ void vkp_framegen_set_winfg_tuning(int model, int perf_preset); -/* Same codes as VulkanRenderer.getFrameGenProblem(): -1 not known yet (device not up), 0 fine, - * 1 the driver lacks what the selected engine needs (vkp_framegen_caps_reason says what), - * 2 the engine failed to start / build its chain. */ int vkp_framegen_problem(void); const char *vkp_framegen_caps_reason(void); -/* Same shape as VulkanRenderer.getFrameGenStats(): {generations trusted, generations planned, - * real (source) fps, presented fps incl. generated frames, thermal (-1 = none), ms per - * generated frame (-1 = unknown)}. */ void vkp_framegen_stats(float out[6]); -/* Generated frames presented since the last call (compositor.c, the 10 s stats line). */ unsigned vkp_framegen_stats_take(void); -/* ---- vk_present.c: device setup (compositor thread) ----------------------------------- */ -/* Before vkCreateDevice: probe what the engines need on this physical device and return the - * pNext chain to hang on VkDeviceCreateInfo (NULL when the device cannot run the LSFG chain - - * Win-FG needs no feature). Owned by the bridge, valid until the next call. */ const void *vkp_framegen_device_features(VkInstance inst, PFN_vkGetInstanceProcAddr gipa, VkPhysicalDevice pd); -/* After the device exists (features_enabled = the chain above was accepted). */ void vkp_framegen_device_ready(VkDevice dev, VkQueue queue, uint32_t qfam, int features_enabled); -/* VK_ERROR_DEVICE_LOST: never touch the device again. */ void vkp_framegen_device_lost(void); -/* ---- vk_present.c: present path (compositor thread) ------------------------------------ */ -/* 1 when this frame must go through the compositor pass (armed and the engine can run). Safe - * before the device is up (0). */ int vkp_framegen_active(void); -/* Extra swapchain images wanted so every present of one scene frame is queued without waiting - * for a vblank: multiplier - 1 while armed, 0 otherwise. A change means "rebuild the swapchain". */ int vkp_framegen_extra_images(void); -/* THE hook. Called once per composed scene frame, after the effects hook, with the scene image - * (GENERAL, w x h, fmt, TRANSFER_SRC|SAMPLED). Records the engine's chain into `cmd` and - * returns how many generated frames (0..VKP_FG_MAX_GENERATIONS) it produced, their images in - * gens[] (GENERAL, w x h, already made visible to TRANSFER reads). 0 = present the scene alone. */ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view, int w, int h, VkFormat fmt, VkImage gens[VKP_FG_MAX_GENERATIONS]); -/* After the presents of one scene frame: how many generated frames actually reached the - * swapchain (rate tracking + the stats counters). */ void vkp_framegen_presented(int generated); -/* 1 when vkp_framegen_run can be fed `fmt` (the 8-bit scene always; FP16 for HDR frames where the - * device can generate in it and the engine has not refused it this session). A chain that fails to - * build in a deeper format is not an engine failure: the engine restarts and the format is refused, - * so the caller goes back to 8 bits. */ int vkp_framegen_format_ok(VkFormat fmt); #endif diff --git a/app/src/main/cpp/waylandcomp/src/framegen_engine.cpp b/app/src/main/cpp/waylandcomp/src/framegen_engine.cpp index 12674e533..43edb1240 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_engine.cpp +++ b/app/src/main/cpp/waylandcomp/src/framegen_engine.cpp @@ -1,9 +1,3 @@ -// C++ side of the Wayland frame-generation bridge - see framegen_engine.h. -// -// Hosts the SAME engine objects the X11 renderer drives (lsfg::Engine, winfg::Engine), fed from -// the compositor's own Turnip entry points. Nothing in framegen/lsfg or framegen/winfg is -// modified: the engines take a VkTable (VulkanRendererContext.h) for their dispatch, and one is -// filled here by name from the compositor's vkGetInstanceProcAddr / vkGetDeviceProcAddr. #include "framegen_engine.h" #include <cstdio> @@ -14,7 +8,7 @@ #include <android/log.h> -#include "VulkanRendererContext.h" // VkTable +#include "VulkanRendererContext.h" #include "lsfg/lsfg_engine.h" #include "lsfg/lsfg_probe.h" #include "lsfg/lsfg_vkd.h" @@ -40,13 +34,10 @@ std::unique_ptr<winfg::Engine> g_winfg; int g_kind = -1; char g_build[96] = ""; -// Instance-level entry points the engines and the probe use. #define FGE_INSTANCE_FUNCS(X) \ X(GetPhysicalDeviceProperties) X(GetPhysicalDeviceMemoryProperties) \ X(GetPhysicalDeviceFeatures2) X(GetPhysicalDeviceFormatProperties) \ X(GetPhysicalDeviceQueueFamilyProperties) X(GetDeviceProcAddr) -// Device-level: the LSFG chain's 34 (lsfg_vkd.h), win-fg's extras (winfg_vkd.cpp) and the -// generic set either may reach for. Anything left null in VkTable is never called. #define FGE_DEVICE_FUNCS(X) \ X(AllocateDescriptorSets) X(AllocateMemory) X(BindBufferMemory) X(BindImageMemory) \ X(CmdBindDescriptorSets) X(CmdBindPipeline) X(CmdCopyImage) X(CmdDispatch) \ @@ -78,7 +69,7 @@ void fill_device(VkDevice dev) { #undef X } -} // namespace +} extern "C" { @@ -96,9 +87,9 @@ const void *fge_probe(PFN_vkGetInstanceProcAddr gipa, VkInstance inst, VkPhysica if (n) enumExts(pd, nullptr, &n, exts.data()); } g_caps = lsfg::Caps{}; - // No Vulkan 1.1 compat here: the Wayland compositor only ever runs on Turnip (adrenotools), - // which reports 1.3+; the stock-driver case the compat flag exists for cannot reach it. - g_caps.features = lsfg::queryFeatures(g_table, pd, exts, /*allowVk11=*/false); + + + g_caps.features = lsfg::queryFeatures(g_table, pd, exts, false); g_caps.probedFormat = ring_fmt; g_caps.storageOnSwapchainFormat = g_table.GetPhysicalDeviceFormatProperties ? lsfg::probeStorageFormat(g_table, pd, ring_fmt) : false; @@ -110,7 +101,6 @@ const void *fge_probe(PFN_vkGetInstanceProcAddr gipa, VkInstance inst, VkPhysica ELOG("device gates for the LSFG chain fail: %s", g_caps.reason); return nullptr; } - // Same chain VulkanRendererContext::createLogicalDevice hangs on its device. g_v12 = VkPhysicalDeviceVulkan12Features{}; g_v12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; g_v12.vulkanMemoryModel = VK_TRUE; @@ -207,8 +197,6 @@ void fge_configure(uint32_t multiplier, uint32_t target_fps, float flow_scale, f int fge_prepare(uint32_t w, uint32_t h, VkFormat fmt) { if (g_kind == 1) return g_winfg && g_winfg->prepare(w, h, fmt) ? 1 : 0; if (!g_lsfg) return 0; - // The Wayland scene is the game's own size when it runs fullscreen, so the flow pyramid is - // scaled against that (X11 passes the X screen size here for the same reason). g_lsfg->setGuestExtent(w, h); return g_lsfg->prepare(w, h, fmt) ? 1 : 0; } @@ -247,4 +235,4 @@ void fge_telemetry(float *accepted, float *source_rate, float *thermal) { const char *fge_engine_name(int kind) { return kind == 1 ? "Win-FG Native" : "LSFG Native"; } const char *fge_build_info(void) { return g_build; } -} // extern "C" +} diff --git a/app/src/main/cpp/waylandcomp/src/framegen_engine.h b/app/src/main/cpp/waylandcomp/src/framegen_engine.h index b0deec70b..4257a7592 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_engine.h +++ b/app/src/main/cpp/waylandcomp/src/framegen_engine.h @@ -1,16 +1,5 @@ #ifndef FRAMEGEN_ENGINE_H #define FRAMEGEN_ENGINE_H -/* - * C face of the C++ frame-generation engines for framegen_bridge.c. - * - * framegen_engine.cpp wraps lsfg::Engine (framegen/lsfg) and winfg::Engine (framegen/winfg) - * unmodified - the same objects the X11 renderer (VulkanRendererContext) drives - and fills - * their dispatch tables from the compositor's own Turnip entry points. The standalone - * compositor build (no framegen tree) compiles framegen_engine_stub.c instead, which reports - * "engines not built in" and generates nothing. - * - * Every function runs on the compositor thread. - */ #include <stdint.h> #ifndef VK_USE_PLATFORM_ANDROID_KHR #define VK_USE_PLATFORM_ANDROID_KHR @@ -21,49 +10,27 @@ extern "C" { #endif -/* Probe the physical device and build the VkDeviceCreateInfo pNext chain the LSFG chain needs - * (vulkanMemoryModel + storage-image features; nothing for Win-FG). Returns the chain, or NULL - * when the device fails a gate. `ring_fmt` is the format the engines generate into; it must be - * storage-capable for either engine. */ const void *fge_probe(PFN_vkGetInstanceProcAddr gipa, VkInstance inst, VkPhysicalDevice pd, VkFormat ring_fmt); -/* The device exists; resolve device-level entry points. features_enabled = fge_probe's chain - * was accepted by vkCreateDevice. */ void fge_device_ready(PFN_vkGetInstanceProcAddr gipa, VkInstance inst, VkPhysicalDevice pd, VkDevice dev, VkQueue queue, uint32_t qfam, int features_enabled); -/* 1 when engine `kind` can run on this device (its capability gates pass). */ int fge_caps_ok(int kind); -/* Why the LSFG gates fail ("supported" when they pass); static string. */ const char *fge_caps_reason(void); -/* 1 when the engines can generate in `fmt` on this device (storage + linear sampling), beyond the - * ring format probed at fge_probe - the FP16 scene of HDR frames (vk_present.c). */ int fge_format_ok(VkFormat fmt); -/* Create engine `kind` (LSFG needs the cache path; Win-FG ignores it). 0 = ready, -1 = failed. - * Only one engine holds GPU resources at a time; a different kind replaces it. */ int fge_start(int kind, const char *cache_path); void fge_stop(void); -/* The engine came up but later found it cannot generate (chain build failed). */ int fge_unavailable(void); -/* target_fps > 0: LSFG picks 0..3 generations per frame to reach it (Win-FG ignores it). */ void fge_configure(uint32_t multiplier, uint32_t target_fps, float flow_scale, float refresh_hz, int model, int perf_preset); -/* Build/rebuild the chain for this extent; the guest extent for the flow pyramid is the same - * (the Wayland scene is the game's own size in fullscreen). 1 = chain valid. */ int fge_prepare(uint32_t w, uint32_t h, VkFormat fmt); -/* How many frames to generate for this source frame (0 until the history is valid). */ uint32_t fge_plan(uint32_t capacity, uint64_t source_frames, float presented_rate); -/* Take the scene image (GENERAL) as frame N and run the shared part of the chain. */ void fge_process(VkCommandBuffer cmd, VkImage source, uint32_t w, uint32_t h, uint32_t gens); -/* Synthesise generated frame g of `count` into a storage image (GENERAL). */ void fge_generate(VkCommandBuffer cmd, uint32_t g, uint32_t count, VkImage img, VkImageView view, uint32_t w, uint32_t h); -/* The generation targets were recreated: drop cached descriptors. */ void fge_forget_targets(void); -/* Engine-side telemetry: accepted generations, source rate (0 = engine has none), thermal. */ void fge_telemetry(float *accepted, float *source_rate, float *thermal); const char *fge_engine_name(int kind); -/* "lsfg-native"/"winfg-native" build provenance for the start log line. */ const char *fge_build_info(void); #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c b/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c index dd4144d3e..b9c4dff36 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c +++ b/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c @@ -1,5 +1,3 @@ -/* Stand-in for framegen_engine.cpp when the compositor is built on its own (standalone/, no - * framegen tree, no dxbc). Every engine is reported unavailable; nothing is generated. */ #include "framegen_engine.h" const void *fge_probe(PFN_vkGetInstanceProcAddr gipa, VkInstance inst, VkPhysicalDevice pd, diff --git a/app/src/main/cpp/waylandcomp/src/framegen_jni.c b/app/src/main/cpp/waylandcomp/src/framegen_jni.c index 8d5bad571..ea5ce0396 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_jni.c +++ b/app/src/main/cpp/waylandcomp/src/framegen_jni.c @@ -1,8 +1,3 @@ -/* JNI for frame generation on the Wayland backend (WaylandCompositor.nativeFrameGen* / - * nativeSet*FrameGen* / nativeSetLsfgCachePath / nativeSetWinFgTuning). Every call only stores - * a value in framegen_bridge.c; the compositor thread picks it up on its next frame, so these - * are safe from any thread and before the compositor is up (the app arms the engine from its - * launch code while the SurfaceView is still coming). */ #include <jni.h> #include "framegen_bridge.h" diff --git a/app/src/main/cpp/waylandcomp/src/hdr_compose.c b/app/src/main/cpp/waylandcomp/src/hdr_compose.c index 6f54d4e0c..01d8be6f6 100644 --- a/app/src/main/cpp/waylandcomp/src/hdr_compose.c +++ b/app/src/main/cpp/waylandcomp/src/hdr_compose.c @@ -1,16 +1,10 @@ -/* HDR composition: the encode pass over the mixed scene - see hdr_compose.h. Compositor thread. */ #define _POSIX_C_SOURCE 200809L #include "hdr_compose.h" #include "vk_loader.h" -#include "vk_present.h" /* droiddeck_log, vkp_gpu_name */ +#include "vk_present.h" #include <stdio.h> #include <string.h> -/* SPIR-V, pre-compiled (glslangValidator -V hdr_encode.frag --vn hdr_encode_code) and committed next - * to the GLSL, like every other shader of the compositor: the NDK build compiles no shaders. The quad - * is the effect chain's upscale.vert (same bytes, from app/src/main/cpp/framegen). */ -/* The headers define plain (external) arrays and effects_chain.c includes upscale_vert.h too: this - * translation unit gets its own copy under another name, so the link sees no duplicate symbol. */ #define upscale_vert_code hdrc_upscale_vert_code #include "upscale_vert.h" #undef upscale_vert_code @@ -18,24 +12,20 @@ #define MIXED_FORMAT VK_FORMAT_A2B10G10R10_UNORM_PACK32 -struct pc_encode { float ndc[4]; float params[4]; float rects[HDRC_MAX_RECTS][4]; }; /* 128 bytes */ +struct pc_encode { float ndc[4]; float params[4]; float rects[HDRC_MAX_RECTS][4]; }; static VkDevice g_dev; -static int g_ready; /* 0 = not built, 1 = ok, -1 = unavailable (said once) */ +static int g_ready; static VkSampler g_sampler; static VkDescriptorSetLayout g_dsl; static VkPipelineLayout g_pl; static VkDescriptorPool g_dpool; static VkShaderModule g_vert, g_frag; -/* One render pass + pipeline per output format: the HDR picture is 10-bit, frame generation's scene - * 8-bit - or FP16 where the engine takes it (vk_present.c FG_HDR_FMT). */ #define NFMT 3 static struct { VkFormat fmt; VkRenderPass rp; VkPipeline pipe; } g_out[NFMT] = { {VK_FORMAT_A2B10G10R10_UNORM_PACK32}, {VK_FORMAT_R8G8B8A8_UNORM}, {VK_FORMAT_R16G16B16A16_SFLOAT}}; -/* The mixed image's view + descriptor, and each output image's view + framebuffer: kept while the - * image handle (and size) stays the same, rebuilt when vk_present re-creates the image. */ static struct { VkImage img; int w, h; VkImageView view; VkDescriptorSet ds; } g_src; #define NOUT 3 static struct { VkImage img; VkFormat fmt; int w, h; VkImageView view; VkFramebuffer fb; } g_dst[NOUT]; @@ -92,9 +82,6 @@ static VkShaderModule shader(const uint32_t *code, size_t size) { return m; } -/* The render pass: one colour target written whole (DONT_CARE), left in TRANSFER_DST_OPTIMAL for the - * effects chain / the copies that follow. The dependencies order it after whoever read or wrote the - * target last (last frame's blit or chain) and before whoever samples, blits or copies it next. */ static VkRenderPass make_rp(VkFormat fmt) { VkAttachmentDescription att = {.format = fmt, .samples = VK_SAMPLE_COUNT_1_BIT, .loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, @@ -261,7 +248,6 @@ int hdrc_encode(VkCommandBuffer cmd, VkImage mixed, VkImage out, VkFormat out_fm VkFramebuffer fb = dst_ensure(out, out_fmt, w, h, g_out[slot].rp); if (!fb) return -1; - /* The composite blits wrote the mixed image; the pass samples it. */ VkImageMemoryBarrier b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, diff --git a/app/src/main/cpp/waylandcomp/src/hdr_compose.h b/app/src/main/cpp/waylandcomp/src/hdr_compose.h index cfeef7497..5a37d42ad 100644 --- a/app/src/main/cpp/waylandcomp/src/hdr_compose.h +++ b/app/src/main/cpp/waylandcomp/src/hdr_compose.h @@ -1,22 +1,5 @@ #ifndef HDR_COMPOSE_H #define HDR_COMPOSE_H -/* - * HDR composition (round 2 of HDR_RECON.md's Phase B): when an HDR game cannot be alone on its own - * display layer - screen effects on, frame generation on, a window above it on a display that cannot - * compose a second layer, a windowed game, a frame without a gralloc buffer - the whole scene is put - * into ONE encoding instead of being blitted into an 8-bit sRGB target as it stands (which shows the - * game's PQ frames washed out). - * - * vk_present.c blits every draw 1:1 into a 10-bit "mixed" image (each pixel still in its own - * encoding) and hdrc_encode() runs hdr_encode.frag over it into the output image: - * HDRC_OUT_PQ - PQ BT.2020 (SDR pixels placed at the SDR reference white): goes onto the game's - * display layer tagged BT2020_PQ (sc_layer.c), or into an HDR10 swapchain; - * HDRC_OUT_SDR - sRGB BT.709 with the HDR pixels tone-mapped: for a present that cannot carry HDR. - * Which encoding a pixel is in comes from the draws' rectangles (the topmost draw decides). - * - * Compositor thread. Objects are built lazily on the first use; if the driver refuses them, the - * encode is unavailable for the session (logged once) and the caller falls back. - */ #define VK_USE_PLATFORM_ANDROID_KHR #include <vulkan/vulkan.h> @@ -26,26 +9,17 @@ enum hdrc_mode { HDRC_OUT_PQ = 0, HDRC_OUT_SDR = 1 }; struct hdrc_params { enum hdrc_mode mode; - float sdr_white_nits; /* SDR reference white inside the HDR picture (203) */ - float peak_nits; /* HDR content peak for the tone-map (max CLL / mastering max, else 1000) */ - int count; /* rects, top draw first */ - unsigned hdr_mask; /* bit i: rects[i] is an HDR draw */ - float rects[HDRC_MAX_RECTS][4]; /* scene pixels x0, y0, x1, y1 */ + float sdr_white_nits; + float peak_nits; + int count; + unsigned hdr_mask; + float rects[HDRC_MAX_RECTS][4]; }; -/* Called from dev_init (device up). */ void hdrc_bind_device(VkDevice dev, const VkPhysicalDeviceMemoryProperties *memprops); -/* Device teardown. */ void hdrc_destroy(void); -/* vk_present is about to destroy `img` (a resize of the mixed / scene / picture image): drop the view, - * descriptor and framebuffer cached for it, so a later image that happens to reuse the handle value is - * never drawn through a view of the destroyed one. */ void hdrc_forget_image(VkImage img); -/* Record the encode: `mixed` (w x h, VK_FORMAT_A2B10G10R10_UNORM_PACK32, in TRANSFER_DST_OPTIMAL after - * the composite blits) -> `out` (w x h, out_fmt, created with COLOR_ATTACHMENT + SAMPLED usage), which - * is left in TRANSFER_DST_OPTIMAL - the layout vkp_effects_run() takes its scene in. 0 = recorded, - * -1 = unavailable (nothing recorded; `mixed` untouched). */ int hdrc_encode(VkCommandBuffer cmd, VkImage mixed, VkImage out, VkFormat out_fmt, int w, int h, const struct hdrc_params *p); diff --git a/app/src/main/cpp/waylandcomp/src/hdr_encode.frag b/app/src/main/cpp/waylandcomp/src/hdr_encode.frag index 4b75ed586..db3ea8bca 100644 --- a/app/src/main/cpp/waylandcomp/src/hdr_encode.frag +++ b/app/src/main/cpp/waylandcomp/src/hdr_encode.frag @@ -1,38 +1,12 @@ #version 450 -// ============================================================================= -// HDR composition: one encoding for the whole scene (waylandcomp/src/hdr_compose.c). -// -// The compositor composes with blits, which copy values and know nothing about what they mean. -// When an HDR game (PQ-encoded BT.2020 frames) shares the scene with SDR content (sRGB windows, the -// desktop, a window above the game), every draw is first blitted 1:1 into a 10-bit "mixed" image - -// each pixel still in its own encoding - and this pass turns the mixed image into ONE encoding: -// -// mode 0 (HDR out, PQ BT.2020): HDR pixels pass through; SDR pixels are decoded (sRGB), -// converted BT.709 -> BT.2020 and placed at the SDR reference white (params.x nits, -// 203 = Report ITU-R BT.2408), then PQ-encoded. -// mode 1 (SDR out, sRGB): SDR pixels pass through; HDR pixels are PQ-decoded, converted -// BT.2020 -> BT.709, tone-mapped relative to the SDR reference white (linear below a -// knee, a smooth roll-off above it that puts the content's peak, params.y nits, at 98 %) -// and sRGB-encoded - what a display that cannot take HDR gets instead of washed-out PQ. -// -// Which encoding a pixel is in: the topmost draw covering it decides. params.w packs the number of -// rects (count, 0..6) and a bitmask of which of them are HDR (count + 16 * mask); rects are scene -// pixels (x0, y0, x1, y1), TOP draw first, listed down to the lowest HDR draw (everything below -// that is SDR, and so is any pixel no listed rect covers). -// -// Conventions match upscale.vert / the effect passes: combined-image-sampler at binding 0, the -// push-constant block leads with vec4 ndc (offset 0). The target is exactly scene-sized, so -// gl_FragCoord is the scene pixel and texelFetch reads the mixed image 1:1 (no filtering). -// Float-only mask arithmetic (no bitwise ops). -// ============================================================================= layout(binding = 0) uniform sampler2D Mixed; layout(push_constant) uniform PC { - vec4 ndc; // upscale.vert's quad - vec4 params; // x = SDR white nits, y = HDR content peak nits (mode 1), z = mode, w = count + 16 * mask - vec4 rects[6]; // top-first scene rects + vec4 ndc; + vec4 params; + vec4 rects[6]; } pc; layout(location = 0) in vec2 fragTexCoord; @@ -44,7 +18,6 @@ const float PQ_C1 = 0.8359375; const float PQ_C2 = 18.8515625; const float PQ_C3 = 18.6875; -// Column-major: each group of three is one COLUMN (the coefficients of R, then of G, then of B). const mat3 BT709_TO_BT2020 = mat3(0.6274, 0.0691, 0.0164, 0.3293, 0.9195, 0.0880, 0.0433, 0.0114, 0.8956); @@ -64,25 +37,21 @@ vec3 linearToSrgb(vec3 c) { return mix(lo, hi, step(vec3(0.0031308), c)); } -// PQ code (0..1) -> luminance / 10000 cd/m2 (SMPTE ST 2084 EOTF). vec3 pqToLinear(vec3 e) { vec3 p = pow(clamp(e, 0.0, 1.0), vec3(1.0 / PQ_M2)); return pow(max(p - PQ_C1, 0.0) / max(PQ_C2 - PQ_C3 * p, 1e-6), vec3(1.0 / PQ_M1)); } -// luminance / 10000 cd/m2 -> PQ code (inverse EOTF). vec3 linearToPq(vec3 y) { vec3 p = pow(clamp(y, 0.0, 1.0), vec3(PQ_M1)); return pow((PQ_C1 + PQ_C2 * p) / (1.0 + PQ_C3 * p), vec3(PQ_M2)); } -// Tone curve in units of SDR white: identity up to the knee, then an exponential roll-off towards 1 -// whose rate puts `peak` (also in SDR-white units) at 98 % of the range. float toneCurve(float x, float peak) { const float knee = 0.8; if (x <= knee) return x; float span = max(peak - knee, 0.05); - float rate = max(span / 3.912, 1.0 - knee); // ln(50) = 3.912; never steeper than the identity + float rate = max(span / 3.912, 1.0 - knee); return knee + (1.0 - knee) * (1.0 - exp(-(x - knee) / rate)); } @@ -107,7 +76,6 @@ void main() { float sdrWhite = max(pc.params.x, 1.0); vec3 outc; if (pc.params.z < 0.5) { - // HDR out: PQ BT.2020. if (hdr) { outc = c; } else { @@ -115,7 +83,6 @@ void main() { outc = linearToPq(nits / 10000.0); } } else { - // SDR out: sRGB BT.709, HDR tone-mapped. if (!hdr) { outc = c; } else { diff --git a/app/src/main/cpp/waylandcomp/src/hdr_encode_frag.h b/app/src/main/cpp/waylandcomp/src/hdr_encode_frag.h index eb1536f1f..ba52146fc 100644 --- a/app/src/main/cpp/waylandcomp/src/hdr_encode_frag.h +++ b/app/src/main/cpp/waylandcomp/src/hdr_encode_frag.h @@ -1,4 +1,3 @@ - // 1116.5.0 #pragma once const uint32_t hdr_encode_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000018e,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/waylandcomp/src/headpose_shm.h b/app/src/main/cpp/waylandcomp/src/headpose_shm.h index 13c71a7f8..1bed89e79 100644 --- a/app/src/main/cpp/waylandcomp/src/headpose_shm.h +++ b/app/src/main/cpp/waylandcomp/src/headpose_shm.h @@ -1,27 +1,14 @@ #ifndef HEADPOSE_SHM_H #define HEADPOSE_SHM_H -/* - * The head pose Cardboard mode tracks, shared with the guest: a 128-byte file the compositor maps - * at <files>/.wayland-rt/droiddeck-headpose, which the guest sees as /run/droiddeck/droiddeck-headpose. - * Written by the app (cardboard.c) at the sensor's rate; read by guest VR code (the SteamVR driver in - * tools/vr) without any call into the app. - * - * Seqlock: the writer makes seq odd, writes the rest, then makes it even again. A reader copies the - * struct and keeps the copy only if seq was even and unchanged across the copy. - * - * Axes are OpenVR's (and the cardboard shader's): x right, y up, z back (-z is straight ahead). - * rot maps head directions to the recentred world, row-major. time_ns is CLOCK_MONOTONIC, the same - * clock inside the guest (one kernel). - */ #include <stdint.h> -#define DROIDDECK_HEADPOSE_MAGIC 0x50484444u /* "DDHP" */ +#define DROIDDECK_HEADPOSE_MAGIC 0x50484444u #define DROIDDECK_HEADPOSE_VERSION 1u #define DROIDDECK_HEADPOSE_FILE "droiddeck-headpose" enum { - DROIDDECK_HEADPOSE_LIVE = 1u << 0, /* a rotation sensor is feeding it (else identity) */ - DROIDDECK_HEADPOSE_ENABLED = 1u << 1, /* Cardboard mode is on */ + DROIDDECK_HEADPOSE_LIVE = 1u << 0, + DROIDDECK_HEADPOSE_ENABLED = 1u << 1, }; struct droiddeck_headpose { @@ -32,11 +19,9 @@ struct droiddeck_headpose { float rot[9]; float ipd_m; int64_t time_ns; - /* What the lens pass expects of a side-by-side source, so a VR renderer can match it: one eye's - * half of the frame covers tan_half_fov up and down and eye_aspect times that left and right. */ float tan_half_fov; float eye_aspect; - uint32_t eye_w, eye_h; /* one eye's viewport on the panel, pixels (0 = not drawn yet) */ + uint32_t eye_w, eye_h; uint32_t reserved[12]; }; diff --git a/app/src/main/cpp/waylandcomp/src/sc_layer.c b/app/src/main/cpp/waylandcomp/src/sc_layer.c index 3f44a6be8..c7a00eac9 100644 --- a/app/src/main/cpp/waylandcomp/src/sc_layer.c +++ b/app/src/main/cpp/waylandcomp/src/sc_layer.c @@ -1,7 +1,3 @@ -/* Layer mode: the scene on a small set of ASurfaceControl layers - see sc_layer.h and - * ZERO_COPY_SPIKE.md. The ASurfaceControl/ASurfaceTransaction API is dlsym'd from libandroid.so - * (API 29+), the same way the X11 renderers' scanout code does it, so the library still loads on - * older devices and the prototype stays off every other path. */ #define _GNU_SOURCE #include "sc_layer.h" #include "ahb_swapchain.h" @@ -25,25 +21,21 @@ #define FOURCC(a, b, c, d) \ ((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24)) -#define DRM_ABGR8888 FOURCC('A', 'B', '2', '4') /* R,G,B,A in memory = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM */ -#define DRM_ABGR2101010 FOURCC('A', 'B', '3', '0') /* = AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM (VK A2B10G10R10) */ +#define DRM_ABGR8888 FOURCC('A', 'B', '2', '4') +#define DRM_ABGR2101010 FOURCC('A', 'B', '3', '0') #define AHB_RGBA8 AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM #define AHB_RGB10A2 AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM #define MOD_LINEAR 0ULL -#define MOD_QCOM_COMPRESSED 0x0500000000000001ULL /* DRM_FORMAT_MOD_QCOM_COMPRESSED (UBWC) */ +#define MOD_QCOM_COMPRESSED 0x0500000000000001ULL -/* The gralloc buffer handle behind an AHardwareBuffer (AOSP cutils/native_handle.h, which the NDK - * does not ship; the layout is a stable ABI: fds first, then ints). */ struct droiddeck_native_handle { int version; int numFds; int numInts; int data[]; }; -/* ---- libandroid SurfaceControl API (dlsym) ---------------------------------------------------- */ typedef struct ASurfaceControl ASurfaceControl; typedef struct ASurfaceTransaction ASurfaceTransaction; typedef struct ASurfaceTransactionStats ASurfaceTransactionStats; typedef void (*sc_complete_fn)(void *context, ASurfaceTransactionStats *stats); -/* <android/hdr_metadata.h> (NDK, API 29), declared here like the rest of this table: stable ABI. */ struct droiddeck_color_xy { float x, y; }; struct droiddeck_hdr_smpte2086 { struct droiddeck_color_xy displayPrimaryRed, displayPrimaryGreen, displayPrimaryBlue, whitePoint; @@ -65,22 +57,14 @@ static struct { void (*reparent)(ASurfaceTransaction *, ASurfaceControl *, ASurfaceControl *); void (*setOnComplete)(ASurfaceTransaction *, void *, sc_complete_fn); int (*prevReleaseFence)(ASurfaceTransactionStats *, ASurfaceControl *); - /* Display frame-rate vote for the layer (API 30 / 31). Optional: absent on older Android, where - * the layer simply carries no vote and the app's surface vote is all there is. */ void (*setFrameRate)(ASurfaceTransaction *, ASurfaceControl *, float, int8_t); void (*setFrameRateStrategy)(ASurfaceTransaction *, ASurfaceControl *, float, int8_t, int8_t); - /* Colour (API 29), optional: only an HDR session ever calls them (droiddeck_color.h's gate needs - * setBufferDataSpace; the metadata calls are sent when present). */ void (*setBufferDataSpace)(ASurfaceTransaction *, ASurfaceControl *, int32_t); void (*setHdrMetadata_smpte2086)(ASurfaceTransaction *, ASurfaceControl *, const struct droiddeck_hdr_smpte2086 *); void (*setHdrMetadata_cta861_3)(ASurfaceTransaction *, ASurfaceControl *, const struct droiddeck_hdr_cta861_3 *); - /* Not in the public NDK headers (vndk/hardware_buffer.h) but exported by libnativewindow.so on - * every device; Mesa's Android WSI calls it for every gralloc buffer it imports. */ const void *(*getNativeHandle)(const AHardwareBuffer *); - /* API 35: the HDR headroom this layer asks the display for (0 = no preference). Round 2e asks - * explicitly for every PQ layer - some phones only boost HDR when a layer asks. */ void (*setDesiredHdrHeadroom)(ASurfaceTransaction *, ASurfaceControl *, float); - int state; /* 0 = untried, 1 = loaded, -1 = unavailable */ + int state; } api; #define ASC_VISIBILITY_HIDE 0 @@ -132,83 +116,74 @@ static int load_api(void) { return 0; } -/* ---- display frame-rate vote (VRR / refresh-rate matching) ------------------------------------ */ -/* The app votes a panel cadence with Surface.setFrameRate on the compositor's SurfaceView, but a - * zero-copy game's frames never touch that surface - they go straight onto the GAME layer - so - * SurfaceFlinger needs the same vote there or the layer's cadence is invisible to it. The vote - * belongs to the layer the game is actually presenting on and to no other: the overlay layer - * carries a window that updates on its own (slow) schedule and is explicitly voted 0, so it never - * drags the panel. The rate is set from the app thread and applied on the compositor thread with - * the next transaction, so no transaction is ever created off-thread. 0 = no vote (panel free). */ -#define ASC_FRAME_RATE_COMPAT_DEFAULT 0 /* == ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT */ -#define ASC_CHANGE_FRAME_RATE_ALWAYS 1 /* == ANATIVEWINDOW_CHANGE_FRAME_RATE_ALWAYS */ -static _Atomic float g_fps_want; /* what the app asked for (the game's cadence) */ + + + + + + + +#define ASC_FRAME_RATE_COMPAT_DEFAULT 0 +#define ASC_CHANGE_FRAME_RATE_ALWAYS 1 + +static _Atomic float g_fps_want; void sc_layer_set_frame_rate(float fps) { atomic_store(&g_fps_want, fps > 0.0f ? fps : 0.0f); } -/* ---- the layers and their buffer pools -------------------------------------------------------- */ -/* Five for the game layer: one on screen, one queued, and the ones SurfaceFlinger has not reported - * back yet (its OnComplete for a transaction arrives only after the NEXT frame is presented), with - * room to spare, so a free buffer is nearly always one the display already let go of. With three, - * a late callback left none free ("no free layer buffer … frame dropped"). */ #define POOL_MAX 5 struct slot { AHardwareBuffer *ahb; - struct vkp_image *img; /* the AHB imported into the compositor's Turnip as a blit target */ + struct vkp_image *img; int w, h; - uint32_t fmt; /* AHARDWAREBUFFER_FORMAT_*: RGBA8888, or RGBA1010102 for an HDR picture */ - int release_fd; /* SurfaceFlinger's release fence: signals when it stopped reading; -1 = none */ - int busy; /* set on the layer, or still referenced by SurfaceFlinger */ + uint32_t fmt; + int release_fd; + int busy; }; -/* One Android display layer: its SurfaceControl, its own buffer pool and its own lifetime. The - * z-order is fixed by the id (game below, overlay above); nothing is ever re-ordered at runtime. */ struct layer { const char *name; int32_t z; - int pool_n; /* buffers in this layer's pool (the game cycles more) */ + int pool_n; struct slot slots[POOL_MAX]; ASurfaceControl *sc; - ANativeWindow *win; /* the output window sc was created on */ + ANativeWindow *win; int shown; - int cur_slot; /* pool slot of the buffer on the layer, -1 = none / not ours */ - void *cur_token; /* zero-copy: the game's buffer on the layer (NULL = a pool slot) */ + int cur_slot; + void *cur_token; ARect geo_src, geo_dst; int geo_valid; - int alloc_tier; /* how pool buffers are asked of gralloc (TIER_*), lowered on failure */ + int alloc_tier; uint64_t pool_modifier; int first_logged; int64_t drop_logged_ns; - unsigned drops_unlogged; /* frames dropped since the last "no free layer buffer" line */ - /* The last pool layout written to the log, so a line is written per change, not per buffer. */ + unsigned drops_unlogged; + int logged_w, logged_h, logged_tier; uint32_t logged_fmt; uint64_t logged_mod; - int votes_rate; /* 1: this layer carries the game's cadence (the game layer) */ - float fps_applied; /* the vote the live SurfaceControl already carries (-1 = none yet) */ - int recreate_pending; /* composition recovery: swap this layer's SurfaceControl for a fresh - * one inside the next frame's transaction (see swap_sc_begin) */ - int32_t ds_applied; /* dataspace the live SurfaceControl carries; -1 = never set on it - * (untouched, today's UNKNOWN), see apply_colour */ - uint32_t md_applied; /* identity of the image description whose metadata it carries, 0 = none */ - float hr_applied; /* desired HDR headroom the live SurfaceControl carries; < 0 = never set */ + int votes_rate; + float fps_applied; + int recreate_pending; + + int32_t ds_applied; + + uint32_t md_applied; + float hr_applied; }; static struct layer g_layers[SC_LAYER_COUNT]; static void apply_frame_rate(ASurfaceTransaction *tx, struct layer *l); static void coverage_text(const struct layer *l, char *out, size_t n); static int g_layers_ready; -static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; /* pools + callback state */ -static int g_pending_cb; /* OnComplete callbacks not yet delivered */ -static unsigned g_stat_layer_frames; /* frames put on a layer through our own buffers */ +static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; +static int g_pending_cb; +static unsigned g_stat_layer_frames; static int g_two_logged; -/* A small buffer set on a layer when it is hidden or retired, so SurfaceFlinger replaces (and - * releases, with a fence) the game's buffer instead of holding it while the layer is invisible. */ static AHardwareBuffer *g_blank; static void layers_init(void) { @@ -231,10 +206,10 @@ static struct layer *layer_of(int id) { struct complete_ctx { ASurfaceControl *sc; - int layer; /* which layer's pool prev_slot belongs to */ - int prev_slot; /* pool buffer this transaction replaced: free once its release fence is known */ - void *prev_token; /* or the game's buffer it replaced: ahb_swapchain gets its release fence */ - int retire; /* release sc after this transaction (hide + reparent) */ + int layer; + int prev_slot; + void *prev_token; + int retire; }; static int64_t now_ns(void) { @@ -242,7 +217,6 @@ static int64_t now_ns(void) { return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec; } -/* Binder thread. */ static void on_complete(void *context, ASurfaceTransactionStats *stats) { struct complete_ctx *ctx = context; pthread_mutex_lock(&g_lock); @@ -257,15 +231,12 @@ static void on_complete(void *context, ASurfaceTransactionStats *stats) { pthread_mutex_unlock(&g_lock); if (ctx->prev_token) { int fd = (stats && ctx->sc) ? api.prevReleaseFence(stats, ctx->sc) : -1; - ahb_swapchain_layer_released(ctx->prev_token, fd); /* takes the fd */ + ahb_swapchain_layer_released(ctx->prev_token, fd); } if (ctx->retire && ctx->sc) api.release(ctx->sc); free(ctx); } -/* `sc` is the SurfaceControl the REPLACED buffer sits on, which is not always l->sc: during a - * composition-recovery swap the outgoing buffer belongs to the old SurfaceControl, and both its - * release fence and its release must be taken from that one. */ static int add_complete_on(ASurfaceTransaction *tx, struct layer *l, ASurfaceControl *sc, int prev_slot, void *prev_token, int retire) { struct complete_ctx *ctx = calloc(1, sizeof(*ctx)); @@ -283,7 +254,6 @@ static int add_complete(ASurfaceTransaction *tx, struct layer *l, int prev_slot, return add_complete_on(tx, l, l->sc, prev_slot, prev_token, retire); } -/* The stand-in buffer for a hidden/retired layer (allocated once, black, shared by both layers). */ static AHardwareBuffer *blank_buffer(void) { if (g_blank) return g_blank; AHardwareBuffer_Desc d = { @@ -300,15 +270,11 @@ static AHardwareBuffer *blank_buffer(void) { return g_blank; } -/* Take the current buffer (pool slot or the game's) off the layer by putting the blank one on it, - * so SurfaceFlinger releases it with a fence through this transaction's callback. */ static void replace_with_blank(ASurfaceTransaction *tx, struct layer *l) { AHardwareBuffer *b = blank_buffer(); if (b) api.setBuffer(tx, l->sc, b, -1); } -/* Everything a SurfaceControl needs on its way out, as transaction ops: give the buffer back (so - * SurfaceFlinger releases the game's, with a fence), hide it, unparent it. */ static void retire_ops(ASurfaceTransaction *tx, ASurfaceControl *sc) { AHardwareBuffer *b = blank_buffer(); if (b) api.setBuffer(tx, sc, b, -1); @@ -316,8 +282,6 @@ static void retire_ops(ASurfaceTransaction *tx, ASurfaceControl *sc) { api.reparent(tx, sc, NULL); } -/* Hide + detach one layer; the SurfaceControl is released from the transaction's callback (the - * buffer on it stays referenced by SurfaceFlinger until then). */ static void retire_sc(struct layer *l) { if (!l->sc) return; ASurfaceTransaction *tx = api.txCreate(); @@ -333,13 +297,11 @@ static void retire_sc(struct layer *l) { droiddeck_log("layer", "%s: SurfaceControl retired (window %p)", l->name, (void *)l->win); l->sc = NULL; l->win = NULL; l->shown = 0; l->cur_slot = -1; l->cur_token = NULL; l->geo_valid = 0; - l->fps_applied = -1.0f; /* the next SurfaceControl carries no vote until it is re-applied */ - l->recreate_pending = 0; /* a fresh SurfaceControl is coming anyway */ - l->ds_applied = -1; l->md_applied = 0; l->hr_applied = -1.0f; /* ...and no colour tag either */ + l->fps_applied = -1.0f; + l->recreate_pending = 0; + l->ds_applied = -1; l->md_applied = 0; l->hr_applied = -1.0f; } -/* Wait (bounded) for SurfaceFlinger to finish with every pool buffer of every layer, then free - * the pools. */ static void drain_and_free_pools(void) { int64_t deadline = now_ns() + 300000000LL; for (;;) { @@ -360,14 +322,11 @@ static void drain_and_free_pools(void) { s->release_fd = -1; } g_layers[i].first_logged = 0; - g_layers[i].logged_w = g_layers[i].logged_h = 0; /* the next pool's layout is logged again */ + g_layers[i].logged_w = g_layers[i].logged_h = 0; } pthread_mutex_unlock(&g_lock); } -/* Native-handle sniff, the same one Mesa's u_gralloc fallback uses (u_gralloc_fallback.c): a QTI - * gralloc private_handle_t carries the magic 'gmsm' as its first int and the UBWC flag - * (PRIV_FLAGS_UBWC_ALIGNED, 0x08000000) in the next one. Returns 0 when the layout is unknown. */ static int sniff_modifier(const struct droiddeck_native_handle *h, uint64_t *mod) { const uint32_t gmsm = ('g' << 24) | ('m' << 16) | ('s' << 8) | 'm'; if (!h || h->numFds < 1 || h->numInts < 2) return 0; @@ -376,18 +335,10 @@ static int sniff_modifier(const struct droiddeck_native_handle *h, uint64_t *mod return 1; } -static int g_alloc_failed; /* set by alloc_slot when gralloc or the import refused a buffer */ +static int g_alloc_failed; -/* How a layer's pool buffers are asked of gralloc, best first; a layer only ever moves down. - * TIER_UBWC GPU render target + sampled + composer overlay + AHARDWAREBUFFER_USAGE_VENDOR_0 (bit 28): - * QTI gralloc's GRALLOC_USAGE_PRIVATE_ALLOC_UBWC. Without it QTI gralloc allocates LINEAR even - * for a GPU render target (gr_allocator.cpp IsUBwcEnabled: an explicit UBWC format, the - * private UBWC bit or CLIENT_TARGET, and no CPU bit). Whether gralloc really compressed the - * buffer is read back from its handle (sniff_modifier), never assumed. - * TIER_PLAIN the same without the vendor bit (a gralloc that refuses unknown bits; what we asked before). - * TIER_LINEAR plus a CPU bit, so the layout is known to be linear even when the handle is unreadable. */ enum { TIER_UBWC, TIER_PLAIN, TIER_LINEAR }; -#define DROIDDECK_AHB_USAGE_VENDOR_UBWC (1ULL << 28) /* AHARDWAREBUFFER_USAGE_VENDOR_0 */ +#define DROIDDECK_AHB_USAGE_VENDOR_UBWC (1ULL << 28) static const char *tier_name(int tier) { return tier == TIER_UBWC ? "asked for UBWC" : tier == TIER_PLAIN ? "no UBWC request" : "linear by request (CPU bit)"; @@ -400,8 +351,6 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm AHardwareBuffer_Desc d = { .width = (uint32_t)w, .height = (uint32_t)h, .layers = 1, .format = fmt, - /* GPU render target (the blit writes it) + sampled (SurfaceFlinger's GPU fallback reads it) + - * composer overlay (the display scans it out). See the tiers above for the rest. */ .usage = AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT | AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_COMPOSER_OVERLAY | (tier == TIER_UBWC ? DROIDDECK_AHB_USAGE_VENDOR_UBWC : 0) | @@ -422,8 +371,6 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm const struct droiddeck_native_handle *nh = api.getNativeHandle(ahb); uint64_t mod = MOD_LINEAR; if (!sniff_modifier(nh, &mod)) { - /* Not a handle we can read: the only layout we can assume is linear, and only if the buffer - * was asked for with a CPU bit (gralloc must not have compressed it). */ if (tier < TIER_LINEAR) { AHardwareBuffer_release(ahb); l->alloc_tier = TIER_LINEAR; @@ -434,8 +381,6 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm mod = MOD_LINEAR; } int fd = (nh && nh->numFds > 0) ? nh->data[0] : -1; - /* The test create: vkp_image_import_dmabuf creates the image with gralloc's explicit layout, so a - * layout the compositor's Turnip refuses falls back a tier instead of drawing garbage. */ struct vkp_image *img = fd >= 0 ? vkp_image_import_dmabuf(fd, fmt == AHB_RGB10A2 ? DRM_ABGR2101010 : DRM_ABGR8888, mod, w, h, got.stride * 4, 0, 1) : NULL; if (!img) { @@ -462,7 +407,6 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm return -1; } -/* 1 when a sync_file has signalled (the display has stopped reading that buffer). */ static int fence_signalled(int fd) { struct pollfd p = {.fd = fd, .events = POLLIN}; int r; @@ -470,12 +414,6 @@ static int fence_signalled(int fd) { return r > 0; } -/* A slot SurfaceFlinger has handed back, holding a w x h buffer of AHB format fmt. -1 = none free (or - * the buffer could not be made). A free slot whose release fence has already signalled is preferred; - * otherwise the fence of the first free one is handed back in *wait_fd for the copy to wait on ON THE - * GPU (vkp_blit_image / vkp_pass_copy_to consume it), so the compositor thread no longer blocks here. - * Only a driver without VK_KHR_external_semaphore_fd still waits on the CPU, bounded, as before. - * *wait_fd is -1 whenever nothing needs waiting for (and on every failure). */ static int take_free_slot(struct layer *l, int w, int h, uint32_t fmt, int *wait_fd) { int idx = -1, pending = -1, fd = -1; *wait_fd = -1; @@ -493,8 +431,6 @@ static int take_free_slot(struct layer *l, int w, int h, uint32_t fmt, int *wait if (fd >= 0 && fence_signalled(fd)) { close(fd); fd = -1; } struct slot *s = &l->slots[idx]; if (s->ahb && (s->w != w || s->h != h || s->fmt != fmt)) { - /* Size or format change: this slot is free, so it can be replaced at once (the display keeps its - * own reference to the old buffer for as long as it reads it; the new one needs no wait). */ if (fd >= 0) { close(fd); fd = -1; } vkp_image_destroy(s->img); AHardwareBuffer_release(s->ahb); memset(s, 0, sizeof(*s)); s->release_fd = -1; @@ -504,7 +440,6 @@ static int take_free_slot(struct layer *l, int w, int h, uint32_t fmt, int *wait int r; do { r = poll(&p, 1, 100); } while (r < 0 && errno == EINTR); if (r == 0) { - /* Still read by the display: keep its fence with it (the next use waits again), drop this frame. */ pthread_mutex_lock(&g_lock); if (s->release_fd < 0) s->release_fd = fd; else close(fd); pthread_mutex_unlock(&g_lock); @@ -521,7 +456,6 @@ static int take_free_slot(struct layer *l, int w, int h, uint32_t fmt, int *wait return idx; } -/* ---- public --------------------------------------------------------------------------------- */ int sc_layer_available(void) { return load_api() == 0; } @@ -529,8 +463,8 @@ void sc_layer_probe_dmabuf_fd(int fd) { static int done; if (done || fd < 0) return; done = 1; - struct { uint32_t flags; int32_t fd; } exp = {.flags = 1u /* DMA_BUF_SYNC_READ */, .fd = -1}; - /* DMA_BUF_IOCTL_EXPORT_SYNC_FILE = _IOWR('b', 2, struct dma_buf_export_sync_file) */ + struct { uint32_t flags; int32_t fd; } exp = {.flags = 1u , .fd = -1}; + if (ioctl(fd, _IOWR('b', 2, exp), &exp) == 0) { droiddeck_log("layer", "kernel exports sync_file fences from the game's dma-buf: zero-copy acquire fences can come from the buffer itself"); if (exp.fd >= 0) close(exp.fd); @@ -540,8 +474,6 @@ void sc_layer_probe_dmabuf_fd(int fd) { } } -/* One layer's SurfaceControl on the current output window (created on first use, re-created after - * a window change). -1 = no window / no API. */ static int ensure_sc(struct layer *l) { if (load_api() != 0) return -1; ANativeWindow *win = vkp_window(); @@ -564,16 +496,11 @@ static int ensure_sc(struct layer *l) { return 0; } -/* Geometry of a w x h buffer covering the scene, through the same mapping the blit path and the - * app's touch mapping use. 1 = r filled, 0 = nothing of it is on screen, -1 = no mapping. */ static int layer_geometry(int w, int h, int scene_w, int scene_h, int r[8]) { if (vkp_update_map(scene_w, scene_h) != 0) return -1; return vkp_map_rect(w, h, scene_w, scene_h, r) ? 1 : 0; } -/* The GAME layer's last placement, kept across SurfaceControl retires and swaps (l->geo_valid is - * per-SurfaceControl and is cleared by both). This is the src -> dst the display actually works - * with, and it is what decides whether a second layer is affordable - see overlay_affordable(). */ static ARect g_game_src, g_game_dst; static int g_game_geo_known; @@ -581,7 +508,7 @@ static void apply_geometry(ASurfaceTransaction *tx, struct layer *l, const int r ARect srcR = {r[0], r[1], r[2], r[3]}, dstR = {r[4], r[5], r[6], r[7]}; if (l == &g_layers[SC_LAYER_GAME]) { g_game_src = srcR; g_game_dst = dstR; g_game_geo_known = 1; } if (!l->geo_valid || memcmp(&srcR, &l->geo_src, sizeof(srcR)) || memcmp(&dstR, &l->geo_dst, sizeof(dstR))) { - api.setGeometry(tx, l->sc, &srcR, &dstR, 0 /* no transform: the DPU scales, never rotates */); + api.setGeometry(tx, l->sc, &srcR, &dstR, 0 ); l->geo_src = srcR; l->geo_dst = dstR; l->geo_valid = 1; char cov[96] = ""; if (l->ds_applied > 0) coverage_text(l, cov, sizeof(cov)); @@ -590,16 +517,11 @@ static void apply_geometry(ASurfaceTransaction *tx, struct layer *l, const int r } } -/* Add the vote to tx when it differs from what this layer carries (compositor thread). Only the - * layer the game presents on carries the game's rate; every other layer is voted 0, so a slow - * overlay window can never hold the panel at the game's cadence (or the game's at the overlay's). */ static void apply_frame_rate(ASurfaceTransaction *tx, struct layer *l) { float want = l->votes_rate ? atomic_load(&g_fps_want) : 0.0f; if (want == l->fps_applied) return; if (!api.setFrameRateStrategy && !api.setFrameRate) return; if (api.setFrameRateStrategy) { - /* ALWAYS, like the app's own surface vote: the seamless-only default is ignored by a panel - * sitting at its peak rate, which is exactly the case we need to move. */ api.setFrameRateStrategy(tx, l->sc, want, ASC_FRAME_RATE_COMPAT_DEFAULT, ASC_CHANGE_FRAME_RATE_ALWAYS); } else { api.setFrameRate(tx, l->sc, want, ASC_FRAME_RATE_COMPAT_DEFAULT); @@ -610,13 +532,6 @@ static void apply_frame_rate(ASurfaceTransaction *tx, struct layer *l) { else if (!first) droiddeck_log("layer", "display frame-rate vote on %s cleared (panel runs free)", l->name); } -/* ---- colour (HDR, droiddeck_color.h) ------------------------------------------------------------- - * The layer is told what its buffer's pixels MEAN - the dataspace (BT2020_PQ for an HDR10 frame) and - * the game's mastering / content-light metadata - in the same transaction as the buffer, so the - * display never shows an HDR frame decoded as sRGB or the other way round. A layer that has never been - * tagged is never touched: sessions without an HDR description make no colour call at all. */ -/* How much of the screen the layer's picture covers ("covers 80% of the screen (1920x1080 of 2400x1080)"): - * some phones only switch to HDR for an HDR layer above a minimum area. Empty when unknown. */ static void coverage_text(const struct layer *l, char *out, size_t n) { int ow = 0, oh = 0; vkp_output_size(&ow, &oh); @@ -628,8 +543,6 @@ static void coverage_text(const struct layer *l, char *out, size_t n) { snprintf(out, n, "covers %.0f%% of the screen (%dx%d of %dx%d)", pct > 100.0 ? 100.0 : pct, w, h, ow, oh); } -/* API 35: ask the display for HDR headroom explicitly while the layer carries an HDR frame, and clear the - * request (0 = no preference) when it stops. Only on change; a layer never tagged is never touched. */ static void apply_headroom(ASurfaceTransaction *tx, struct layer *l, const struct droiddeck_color *c) { static int said_missing; if (!api.setDesiredHdrHeadroom) { @@ -642,7 +555,7 @@ static void apply_headroom(ASurfaceTransaction *tx, struct layer *l, const struc return; } const float want = (c && c->dataspace) ? droiddeck_color_desired_headroom(c, NULL, 0) : 0.0f; - if (l->hr_applied < 0.0f && want == 0.0f && !(c && c->dataspace)) return; /* never asked: leave it be */ + if (l->hr_applied < 0.0f && want == 0.0f && !(c && c->dataspace)) return; if (l->hr_applied >= 0.0f && fabsf(want - l->hr_applied) < 0.005f) return; char why[200] = ""; if (c && c->dataspace) droiddeck_color_desired_headroom(c, why, sizeof(why)); @@ -662,7 +575,7 @@ static void apply_colour(ASurfaceTransaction *tx, struct layer *l, const struct int32_t want = c ? c->dataspace : DROIDDECK_ADATASPACE_UNKNOWN; uint32_t md = (c && c->dataspace) ? c->identity : 0; if (!api.setBufferDataSpace) return; - if (l->ds_applied < 0 && want == DROIDDECK_ADATASPACE_UNKNOWN) return; /* never tagged: leave it be */ + if (l->ds_applied < 0 && want == DROIDDECK_ADATASPACE_UNKNOWN) return; apply_headroom(tx, l, want ? c : NULL); if (want == l->ds_applied && md == l->md_applied) return; api.setBufferDataSpace(tx, l->sc, want); @@ -676,7 +589,6 @@ static void apply_colour(ASurfaceTransaction *tx, struct layer *l, const struct .maxLuminance = c->max_lum, .minLuminance = c->min_lum}; } if (cta_on) cta = (struct droiddeck_hdr_cta861_3){.maxContentLightLevel = c->max_cll, .maxFrameAverageLightLevel = c->max_fall}; - /* NULL clears what a previous description set: metadata never outlives the frame it belongs to. */ if (api.setHdrMetadata_smpte2086) api.setHdrMetadata_smpte2086(tx, l->sc, st_on ? &st : NULL); if (api.setHdrMetadata_cta861_3) api.setHdrMetadata_cta861_3(tx, l->sc, cta_on ? &cta : NULL); if (want) { @@ -706,8 +618,6 @@ void sc_layer_hdr_symbols(char *out, size_t size) { api.setDesiredHdrHeadroom ? "yes (asked for per HDR frame: content peak / SDR white)" : "no (Android < 15)"); } -/* Once, when a second layer first goes up: HWC only composes a few layers before SurfaceFlinger - * falls back to GPU client composition, so the count is deliberately capped and said out loud. */ static void log_layer_count(void) { if (g_two_logged) return; g_two_logged = 1; @@ -718,34 +628,12 @@ static void log_layer_count(void) { g_layers[SC_LAYER_OVERLAY].name, (int)g_layers[SC_LAYER_OVERLAY].z, SC_LAYER_COUNT); } -/* ---- composition recovery --------------------------------------------------------------------- - * When the overlay is retired and the scene is one fullscreen window again, the game layer is - * marked for a swap and the swap rides the NEXT frame: a fresh SurfaceControl is created here, the - * frame is put on it, and the old one is hidden and unparented IN THE SAME TRANSACTION. Because - * SurfaceFlinger applies a transaction atomically there is never a composited frame with neither - * layer on it - no black frame, and no dropped frame beyond the one layer creation. The outgoing - * buffer is released through the OLD SurfaceControl's callback, which also releases it. - * - * MEASURED 2026-09-14, and the news is bad: on the Pocket FIT this does NOT bring hardware - * composition back. The swap fires 6 ms after the overlay goes, SurfaceFlinger really does hand out - * a new layer (its id changes), the game never drops a frame - and the composer still reports - * `DEVICE/CLIENT` 24 s later. Neither does dropping the layer path entirely and re-creating the - * SurfaceControl after a gap. The ONE thing that clears it is HOME + resume, which re-creates the - * app's whole window and SurfaceView (`VRI[XServerDisplayActivity]#0` becomes `#4`) - so the sticky - * client-composition state belongs to the PARENT surface (or the display), not to this child layer. - * The phase-4 note that "only re-creating the GAME layer's SurfaceControl clears it" was inferred - * from HOME + resume and is wrong; see sc_layer.h. The swap is kept because it is free and correct - * and the mechanism may differ on hardware that does not rotate every layer - but do not claim it - * restores DEVICE composition, and do not log as if it did. - * - * Returns the old SurfaceControl (the caller must add retire_ops for it to the same transaction and - * name it in add_complete_on), or NULL when no swap is due. Compositor thread. */ static ASurfaceControl *swap_sc_begin(struct layer *l) { if (!l->recreate_pending) return NULL; l->recreate_pending = 0; - if (!l->sc) return NULL; /* nothing to swap; ensure_sc made a fresh one already */ + if (!l->sc) return NULL; ANativeWindow *win = vkp_window(); - if (!win || win != l->win) return NULL; /* the window changed: ensure_sc re-creates it anyway */ + if (!win || win != l->win) return NULL; ASurfaceControl *fresh = api.createFromWindow(win, l->name); if (!fresh) { droiddeck_log("error", "layer: %s: composition recovery could not create a new SurfaceControl", l->name); @@ -753,9 +641,6 @@ static ASurfaceControl *swap_sc_begin(struct layer *l) { } ASurfaceControl *old = l->sc; l->sc = fresh; - /* These describe the SurfaceControl, not the layer: the new one carries none of them yet. - * cur_slot / cur_token are deliberately NOT cleared - they name the buffer still on the OLD - * SurfaceControl, which is what the caller passes to add_complete_on as the one being replaced. */ l->shown = 0; l->geo_valid = 0; l->fps_applied = -1.0f; @@ -765,16 +650,13 @@ static ASurfaceControl *swap_sc_begin(struct layer *l) { return old; } -/* The transaction that puts pool slot `idx` of layer `l` on screen at `r`, as `color` (NULL = no - * description). 0 = applied. */ static int present_slot(struct layer *l, int idx, const int r[8], const struct droiddeck_color *color, int translucent) { struct slot *s = &l->slots[idx]; ASurfaceTransaction *tx = api.txCreate(); if (!tx) return -1; - ASurfaceControl *old = swap_sc_begin(l); /* this frame carries the recovery swap, if one is due */ + ASurfaceControl *old = swap_sc_begin(l); if (old) api.setZOrder(tx, l->sc, l->z); - /* The blit was waited for on the CPU, so no acquire fence is needed (-1). */ api.setBuffer(tx, l->sc, s->ahb, -1); api.setBufferTransparency(tx, l->sc, translucent ? ASC_TRANSPARENCY_TRANSLUCENT : ASC_TRANSPARENCY_OPAQUE); @@ -784,7 +666,7 @@ static int present_slot(struct layer *l, int idx, const int r[8], const struct d if (!l->shown) api.setVisibility(tx, l->sc, ASC_VISIBILITY_SHOW); if (old) retire_ops(tx, old); if (add_complete_on(tx, l, old ? old : l->sc, l->cur_slot, l->cur_token, old ? 1 : 0) != 0 && old) - api.release(old); /* no callback to retire it from (out of memory): let it go here */ + api.release(old); pthread_mutex_lock(&g_lock); s->busy = 1; pthread_mutex_unlock(&g_lock); @@ -797,8 +679,6 @@ static int present_slot(struct layer *l, int idx, const int r[8], const struct d return 0; } -/* No free buffer this frame: the display still holds all of them. Counted for the 10 s perf line, and - * logged at most every 30 s with the number of frames dropped since the last line. */ static unsigned g_pool_drops; static void log_drop(struct layer *l) { g_pool_drops++; @@ -828,8 +708,6 @@ int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, voi if (g < 0) goto unavailable; if (g == 0) { if (acquire_fd >= 0) close(acquire_fd); sc_layer_hide(); return 1; } if (token == l->cur_token && l->shown && !l->recreate_pending) { - /* The same frame again (the scene was redrawn for another reason): the display already - * has it; only the placement may have changed (or, rarely, its description). */ if (acquire_fd >= 0) close(acquire_fd); ASurfaceTransaction *tx = api.txCreate(); if (tx) { @@ -841,22 +719,20 @@ int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, voi } ASurfaceTransaction *tx = api.txCreate(); if (!tx) goto unavailable; - ASurfaceControl *old = swap_sc_begin(l); /* this frame carries the recovery swap, if one is due */ + ASurfaceControl *old = swap_sc_begin(l); if (old) api.setZOrder(tx, l->sc, l->z); - api.setBuffer(tx, l->sc, ahb, acquire_fd); /* the transaction owns the fence */ + api.setBuffer(tx, l->sc, ahb, acquire_fd); api.setBufferTransparency(tx, l->sc, ASC_TRANSPARENCY_OPAQUE); apply_geometry(tx, l, r); apply_frame_rate(tx, l); apply_colour(tx, l, color); if (!l->shown) api.setVisibility(tx, l->sc, ASC_VISIBILITY_SHOW); if (old) retire_ops(tx, old); - /* Same buffer, new SurfaceControl: it is NOT free, so no release is reported for it - the swap - * moved it rather than taking it off screen. */ if (add_complete_on(tx, l, old ? old : l->sc, l->cur_slot, l->cur_token == token ? NULL : l->cur_token, old ? 1 : 0) != 0) { - api.txDelete(tx); /* the fence went with the transaction */ + api.txDelete(tx); if (old) api.release(old); - l->ds_applied = -1; l->md_applied = 0; l->hr_applied = -1.0f; /* the colour tag went with it too: re-send it next time */ + l->ds_applied = -1; l->md_applied = 0; l->hr_applied = -1.0f; return -1; } api.txApply(tx); @@ -864,8 +740,6 @@ int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, voi l->cur_slot = -1; l->cur_token = token; if (!l->shown) { l->shown = 1; droiddeck_log("layer", "%s: layer shown", l->name); } - /* The notification is re-armed for each session; the layer and its logging flag can - * outlive it. Let the notification's own one-shot decide whether to fire. */ if (!l->first_logged) l->first_logged = 1; vkp_signal_first_frame(); if (color && color->dataspace) droiddeck_color_frame_shown(color, DROIDDECK_HDR_ZERO_COPY, ahb_format); @@ -882,7 +756,7 @@ int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const stru int r[8]; int g = layer_geometry(sw, sh, scene_w, scene_h, r); if (g < 0) return -1; - if (g == 0) { sc_layer_hide(); return 0; } /* nothing of it is on screen */ + if (g == 0) { sc_layer_hide(); return 0; } int wait_fd; int idx = take_free_slot(l, sw, sh, AHB_RGBA8, &wait_fd); @@ -905,15 +779,13 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int int rw = 0, rh = 0, r[8]; if (!draws || n <= 0 || ensure_sc(l) != 0) return -1; if (vkp_update_map(scene_w, scene_h) != 0) return -1; - /* Compose + run the effects chain first: the chain's result size (a scaling mode resizes to - * the scene's mapped output size) decides how big the layer buffer has to be. */ if (vkp_pass_begin(scene_w, scene_h, draws, n, &rw, &rh) != 0) return -1; if (!vkp_map_rect(rw, rh, scene_w, scene_h, r)) { vkp_pass_abort(); sc_layer_hide(); return 0; } int wait_fd; int idx = take_free_slot(l, rw, rh, AHB_RGBA8, &wait_fd); if (idx < 0) { vkp_pass_abort(); log_drop(l); return 0; } if (vkp_pass_copy_to(l->slots[idx].img, wait_fd) != 0) return -1; - if (present_slot(l, idx, r, NULL, 0) != 0) return -1; /* the effects chain's result is 8-bit sRGB */ + if (present_slot(l, idx, r, NULL, 0) != 0) return -1; if (!l->first_logged) { l->first_logged = 1; droiddeck_log("layer", "presenting %dx%d frames on their own SurfaceControl layer (%s pool, %d buffers); " @@ -924,14 +796,6 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int return 0; } -/* ---- the HDR picture (hdr_compose.h) ------------------------------------------------------------ - * An HDR game that cannot be alone on its layer: the WHOLE scene, composed into one 10-bit PQ BT.2020 - * picture with the effects applied, goes onto the game layer - one layer, tagged BT2020_PQ with the - * game's metadata, so the display composes it as HDR (and a window above the game never needs the - * second layer that costs this panel its hardware composition). 10-bit buffers; if gralloc or the - * import refuses those, 8-bit ones carry the same tagged picture (colours right, precision less). - * With the drawer's HDR output switch off (hf->tonemap) the same picture is composed tone-mapped to - * sRGB instead and goes on the ordinary 8-bit buffers, UNtagged - an SDR frame like any other. */ static uint32_t g_hdr_pool_fmt = AHB_RGB10A2; int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, @@ -955,7 +819,7 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct } if (idx < 0) { vkp_pass_abort(); log_drop(l); return 0; } if (vkp_pass_copy_to(l->slots[idx].img, wait_fd) != 0) return -1; - if (present_slot(l, idx, r, tm ? NULL : color, 0) != 0) return -1; /* tone-mapped = plain sRGB: no tag */ + if (present_slot(l, idx, r, tm ? NULL : color, 0) != 0) return -1; if (color && color->dataspace) droiddeck_color_frame_shown(color, tm ? DROIDDECK_HDR_TONEMAPPED : DROIDDECK_HDR_COMPOSED, tm ? AHB_RGBA8 : g_hdr_pool_fmt); static int said = -1; @@ -980,40 +844,17 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct return 0; } -/* ---- is a SECOND display layer affordable on this display? ----------------------------------- - * MEASURED on the Pocket FIT (2026-09-14, three times): while a second layer is up, this hardware - * composer hands the WHOLE frame back to the GPU (`DEVICE/CLIENT`), and nothing short of re-creating - * the app's window brings it back - so on this display the overlay layer costs more than it saves, - * every time, for the rest of the session. Where it costs nothing it is still the better path (the - * game keeps copy-free frames with a window on top), so this is a GATE, not a removal. - * - * The gate is read from what the layer path actually knows, never from a device or panel list: - * - rotation: vkp_surface_rotation_degrees(), i.e. VkSurfaceCapabilitiesKHR::currentTransform - - * what the presentation engine says it does to every layer we hand it; - * - scale: the GAME layer's own src -> dst rectangles, the ones passed to setGeometry. - * Rotated AND scaled is the combination that was measured to cost composition (the DPU's rotator - * has to take a scaled source); either alone, or neither, is left as it was. - * - * Why not just measure the composition type after raising the layer and back out if it comes back - * CLIENT - which would beat predicting? Because no app can read it. The composition type lives in - * the Composer HAL; ASurfaceTransactionStats exposes latch time and fences and nothing else, and - * the only place the value is published is `dumpsys android.hardware.graphics.composer3.IComposer`, - * which needs android.permission.DUMP and string-parsing. It would also arrive at least a frame - * late, so backing out would itself be the visible change we are avoiding. Hence the rule. */ static int overlay_affordable(int *deg, int sw[2], int dw[2]) { *deg = vkp_surface_rotation_degrees(); sw[0] = sw[1] = dw[0] = dw[1] = 0; - if (!g_game_geo_known) return 1; /* the game has never been placed: nothing to weigh against */ - if (*deg <= 0) return 1; /* unknown (-1) or upright: leave today's behaviour alone */ + if (!g_game_geo_known) return 1; + if (*deg <= 0) return 1; sw[0] = g_game_src.right - g_game_src.left; sw[1] = g_game_src.bottom - g_game_src.top; dw[0] = g_game_dst.right - g_game_dst.left; dw[1] = g_game_dst.bottom - g_game_dst.top; if (sw[0] <= 0 || sw[1] <= 0) return 1; - return !(dw[0] != sw[0] || dw[1] != sw[1]); /* rotated AND scaled -> not affordable */ + return !(dw[0] != sw[0] || dw[1] != sw[1]); } -/* -1 = not decided yet, 0 = raising the overlay, 1 = declining it. Logged on every change, so a - * tester's log says why they are seeing the copy path instead of two layers - and says it again if - * the placement changes the answer (e.g. a fullscreen mode that stops scaling the game). */ static int g_overlay_declined = -1; int sc_layer_overlay_affordable(void) { @@ -1040,8 +881,6 @@ int sc_layer_overlay_affordable(void) { int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int translucent) { struct layer *l = layer_of(SC_LAYER_OVERLAY); if (!src || !geo) return -1; - /* The caller decides with sc_layer_overlay_affordable() BEFORE it commits the game to a layer; - * this is only a guard so the layer can never go up behind that decision's back. */ if (!sc_layer_overlay_affordable()) return -1; if (ensure_sc(l) != 0) return -1; int sw = vkp_image_width(src), sh = vkp_image_height(src); @@ -1049,8 +888,6 @@ int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int transl int wait_fd; int idx = take_free_slot(l, sw, sh, AHB_RGBA8, &wait_fd); if (idx < 0) { log_drop(l); return 0; } - /* The window is copied into the layer buffer 1:1; `geo` crops it and places it, so the layer - * is exactly the window's rectangle on screen and nothing else is blended anywhere. */ if (vkp_blit_image(src, l->slots[idx].img, wait_fd) != 0) return -1; if (present_slot(l, idx, geo, NULL, translucent) != 0) return -1; if (!l->first_logged) { @@ -1060,8 +897,6 @@ int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int transl return 0; } -/* Hide one layer: the buffer comes off with it (a hidden layer would keep the game's buffer - or - * the pool slot - referenced, and the game needs it back to keep presenting the other way). */ static void hide_layer(struct layer *l) { if (!l->sc || !l->shown || api.state != 1) return; ASurfaceTransaction *tx = api.txCreate(); @@ -1076,10 +911,6 @@ static void hide_layer(struct layer *l) { void sc_layer_hide(void) { layers_init(); int said = g_layers[SC_LAYER_GAME].shown || g_layers[SC_LAYER_OVERLAY].sc; - /* Top down, so nothing of the scene is ever uncovered for a frame. The overlay layer is let go - * of entirely rather than hidden (same reason as sc_layer_hide_overlay: a live second - * SurfaceControl keeps SurfaceFlinger composing on the GPU); the game layer keeps its - * SurfaceControl, since it goes up and down with every effects/frame-generation toggle. */ if (g_layers[SC_LAYER_OVERLAY].sc) sc_layer_hide_overlay(); if (g_layers[SC_LAYER_GAME].shown) hide_layer(&g_layers[SC_LAYER_GAME]); if (said) droiddeck_log("layer", "layers hidden (scene is not a single fullscreen window)"); @@ -1089,18 +920,9 @@ void sc_layer_hide_overlay(void) { layers_init(); struct layer *l = &g_layers[SC_LAYER_OVERLAY]; if (!l->sc) return; - /* RETIRED, not just hidden, so the overlay layer lives exactly as long as the window above the - * game: a live second SurfaceControl is what puts SurfaceFlinger into GPU client composition - * on this hardware (see sc_layer.h), and a hidden one is still live. Measured caveat: letting - * it go does NOT by itself bring hardware composition back - on the Pocket FIT the fallback - * outlives the overlay and only clears when the GAME layer's SurfaceControl is re-created - * (HOME + resume). Retiring is still the right thing; it just is not the whole cure. The pool - * buffers stay allocated for the next window. */ if (l->shown) hide_layer(l); retire_sc(l); droiddeck_log("layer", "%s: gone (nothing is above the game any more)", l->name); - /* ...and that is the half the measurement said is not enough: arm the game layer's - * SurfaceControl swap, which the next frame performs (swap_sc_begin). */ if (g_layers[SC_LAYER_GAME].sc) g_layers[SC_LAYER_GAME].recreate_pending = 1; } diff --git a/app/src/main/cpp/waylandcomp/src/sc_layer.h b/app/src/main/cpp/waylandcomp/src/sc_layer.h index 1d8e6a11e..9df4cd80e 100644 --- a/app/src/main/cpp/waylandcomp/src/sc_layer.h +++ b/app/src/main/cpp/waylandcomp/src/sc_layer.h @@ -1,70 +1,5 @@ #ifndef SC_LAYER_H #define SC_LAYER_H -/* - * Layer mode (DROIDDECK_WAYLAND_ZERO_COPY=1): the scene is handed to SurfaceFlinger as a small, - * deliberately ordered SET of Android display layers - ASurfaceControl children of the - * compositor's SurfaceView - instead of being blitted into the compositor's own swapchain. - * - * app window ......... Compose UI, the in-game drawer, the HUD, the on-screen controls and - * the pointer arrow: ordinary Android views, ALWAYS above everything - * below (the SurfaceView is not Z-on-top, so its whole subtree is under - * the window's own content). None of the layers below take input: - * an ASurfaceControl has no input channel, so touch and mouse keep - * reaching the SurfaceView exactly as they did. - * +-- SurfaceView ..... the compositor's Vulkan swapchain; black while layer mode is up - * +-- z=1 "droiddeck_wayland_game" ...... the one fullscreen game window - * +-- z=2 "droiddeck_wayland_overlay" ... at most one window drawn ABOVE the game - * - * TWO layers is the hard cap (SC_LAYER_COUNT), and deliberately so: HWC composes only a few - * layers before SurfaceFlinger falls back to GPU client composition, which would throw away the - * whole benefit. The count is logged when the second layer first goes up. - * - * MEASURED on the Pocket FIT (Adreno 750, portrait panel + landscape session, so every layer is - * ROT_90 + scaled), `dumpsys android.hardware.graphics.composer3.IComposer/default`: one layer is - * `composition: DEVICE/DEVICE`, effects on the layer keep it DEVICE/DEVICE - but a SECOND layer - * flips the whole frame to `DEVICE/CLIENT`. It does not come back when the overlay goes away. - * The overlay layer is retired rather than hidden for that reason, but the fallback outlives it. - * The likely mechanism is the DPU's rotator budget (one rotated+scaled layer), not the layer count - * as such, so a device or orientation needing no rotation may well take both on the DPU. Even in - * client composition the overlay layer is not a loss - SurfaceFlinger does the one blit the - * compositor would have done - but the hardware-composition win is only real for one layer. - * - * COMPOSITION RECOVERY (sc_layer.c, swap_sc_begin): retiring the overlay arms the game layer, and - * the NEXT frame is presented on a brand-new SurfaceControl while the old one is hidden and - * unparented in the SAME transaction. SurfaceFlinger applies a transaction atomically, so no - * composited frame is ever missing the game - no black frame, no dropped frame beyond the layer - * creation itself. One `layer` line is written when it happens. - * - * It does NOT restore hardware composition on this panel, and that was measured, not assumed - * (2026-09-14, Wizardry on the game layer + Wine's Task Manager on the overlay, reproduced twice): - * - two layers -> DEVICE/CLIENT - * - overlay retired + fresh game SurfaceControl -> still DEVICE/CLIENT, 24 s later too - * - layer path dropped entirely and re-created -> still DEVICE/CLIENT - * - drawer opened and closed, no second window -> DEVICE/DEVICE (control: the drawer is not the cause) - * - HOME + resume -> DEVICE/DEVICE - * HOME + resume re-creates the app's whole window and SurfaceView (`VRI[XServerDisplayActivity]#0` - * becomes `#4`), not just this child layer - so the sticky state belongs to the parent surface or - * the display. Re-creating the parent would cost a real black frame and a swapchain rebuild, which - * is worse than the few percent of GPU that client composition costs. The cure that actually keeps - * the win is PREVENTION, and it IS implemented: sc_layer_overlay_affordable() declines the second - * layer when the game layer is both rotated and scaled, and the window above the game goes down the - * copy path instead (what the pre-phase-4 code did) - one blit SurfaceFlinger would have done - * anyway, and the session keeps DEVICE composition throughout. Where a second layer costs nothing, - * nothing changes. - * - * What can be on the game layer, cheapest first: - * - the game's own gralloc buffer (ahb_swapchain.c, true zero-copy: no copy anywhere); - * - one blit of the game's frame into a compositor-allocated AHardwareBuffer (sc_layer_present); - * - with screen effects on, the compositor pass's result blitted into such a buffer - * (sc_layer_present_pass) - the game STAYS on its layer while a Look is applied, and the - * scene -> output mapping is done by the display (setGeometry) instead of a second GPU blit. - * The compositor never alpha-blends (it composes with blits, which overwrite), so every layer is - * marked OPAQUE and the overlay layer is cropped to the window it carries: the picture is the - * same as the copy path's, pixel for pixel. - * - * Compositor thread only, except the SurfaceFlinger OnComplete callbacks (binder threads), which - * only touch the pools under g_lock. sc_layer_hide*() is safe at any time. - */ #include <stddef.h> #include <stdint.h> #include <android/hardware_buffer.h> @@ -73,103 +8,39 @@ struct vkp_image; struct vkp_draw; struct droiddeck_color; -/* The layers, bottom first. The z-order is the array order (z = id + 1). */ enum sc_layer_id { SC_LAYER_GAME = 0, SC_LAYER_OVERLAY = 1, SC_LAYER_COUNT = 2 }; -/* 1 when this device has the SurfaceControl API the layers need (Android 10+, libnativewindow's - * getNativeHandle); the reason is logged (tag `layer`) the first time it is missing. */ int sc_layer_available(void); -/* One-shot: does the kernel export a sync_file from the game's dma-buf (DMA_BUF_IOCTL_EXPORT_SYNC_FILE)? - * Decides where a zero-copy acquire fence can come from; result goes to the session log. */ void sc_layer_probe_dmabuf_fd(int fd); -/* HDR (droiddeck_color.h): can a display layer be told its buffer's colour encoding here - * (ASurfaceTransaction_setBufferDataSpace, Android 10+)? Part of the HDR gate. */ int sc_layer_can_tag_hdr(void); -/* Which of the colour calls this libandroid has, for the gate's log line. */ void sc_layer_hdr_symbols(char *out, size_t size); -/* COLOUR on the game layer: `color` is the image description of the frame being presented (NULL = - * none, i.e. sRGB). A layer that was never tagged is never touched (no setBufferDataSpace call at all: - * a session without an HDR description behaves exactly as before); once tagged, a later untagged frame - * puts the layer back to UNKNOWN. SMPTE 2086 / CTA-861.3 metadata travel with the dataspace. */ -/* GAME layer: show `src` (the fullscreen window's imported frame, scene-sized) on it, placed - * through the current fullscreen mode / alignment mapping. 0 = shown (or deliberately dropped: no - * free buffer), -1 = the layer path is unavailable this frame and the caller must draw the old way. - * The copy into the layer buffer is 8-bit: an HDR frame keeps its tag (its colours stay right), and - * loses precision. */ int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const struct droiddeck_color *color); -/* GAME layer, screen effects on: run the compositor pass (composite `draws` + the effects chain, - * vkp_pass_begin) and put its result on the layer. Same return values as sc_layer_present. */ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int scene_h); -/* GAME layer, HDR (hdr_compose.h): the whole scene - `draws`, hf->is_hdr marking the HDR ones - composed - * into one 10-bit PQ BT.2020 picture with the effects applied (vkp_pass_begin_hdr), copied into a - * 10-bit layer buffer (8-bit where gralloc refuses those) and shown tagged with `color` (the topmost HDR - * draw's description). The overlay layer is not used: the picture already holds what is above the game. - * Same return values as sc_layer_present. */ struct vkp_hdr_frame; int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int scene_w, int scene_h, const struct droiddeck_color *color); -/* GAME layer, zero-copy (ahb_swapchain.c): show the game's own w x h AHardwareBuffer, gated by - * acquire_fd (a sync_file the layer waits on before reading; owned by the callee, -1 = none). - * token identifies the buffer: once a later transaction replaces it (or the layer is hidden or - * retired), ahb_swapchain_layer_released(token, release_fd) reports SurfaceFlinger's release fence - * for it. Presenting the token already on the layer only updates the placement. 0 = on the layer, - * 1 = nothing of it is on screen (layer hidden, the buffer was not taken), -1 = unavailable this - * frame (the caller draws the old way). `color` / `ahb_format`: the frame's image description (NULL = - * none) and the buffer's AHARDWAREBUFFER_FORMAT_*, for the dataspace and the HDR evidence. */ int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, void *token, int scene_w, int scene_h, const struct droiddeck_color *color, uint32_t ahb_format); -/* Vote a panel refresh rate for the layer the game presents on (VRR / refresh-rate matching), the - * same rate and compatibility the app votes on its own surface with Surface.setFrameRate; 0 = no - * vote. Needed because a zero-copy game's frames go onto the GAME layer and never reach the app's - * surface, so the surface vote alone does not describe the game's cadence to SurfaceFlinger. Only - * the game layer ever carries it: the overlay layer is explicitly voted 0, so a window that redraws - * once a second can never hold (or drop) the panel at the game's cadence. Callable from any thread - * at any time (it only stores the rate); the compositor applies it with the layer's next - * transaction, and re-applies it whenever a SurfaceControl is re-created. A no-op on devices whose - * libandroid has no ASurfaceTransaction_setFrameRate. */ void sc_layer_set_frame_rate(float fps); -/* Is a SECOND display layer worth raising on THIS display? Ask before committing the game to its - * layer, not after: on a display where the answer is no, the whole scene must go down the copy path - * from the start, or the game layer would be shown and hidden again on every frame. - * - * The answer is computed from what the layer path knows - the rotation the presentation engine - * applies to everything we hand it (VkSurfaceCapabilitiesKHR::currentTransform) and the game - * layer's own src -> dst rectangles - and never from a device or panel allowlist. ROTATED AND - * SCALED is the combination measured to cost hardware composition (below); either alone is fine. - * The reason is written to the session log once, and again if the answer changes. */ int sc_layer_overlay_affordable(void); -/* OVERLAY layer: show `src` (one window's imported frame) above the game layer, at the placement - * `geo` = {src x0,y0,x1,y1 in image pixels, dst x0,y0,x1,y1 in output pixels} from vkp_map_draw. - * `translucent` preserves a client's alpha over the game for Steam notifications and overlays. - * 0 = shown or dropped, -1 = unavailable (the caller must fall back to the copy path). */ int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int translucent); -/* The scene is not a single fullscreen window this frame: hide every layer that is up. */ void sc_layer_hide(void); -/* Only the overlay layer: nothing is above the game any more. The game keeps its layer, but its - * SurfaceControl is swapped for a fresh one on the next frame (composition recovery, above - which - * on this panel does not in fact win hardware composition back; see the measurement there). */ void sc_layer_hide_overlay(void); -/* The output window changed or went away (compositor thread, from vkp_apply_window_request). */ void sc_layer_window_gone(void); -/* Frames the compositor put on a layer through one of its own buffers (a blit or the effects - * pass) since the last call - the zero-copy frames ahb_swapchain.c counts are NOT included. - * For the 10 s summary. */ unsigned sc_layer_frames_take(void); -/* Frames dropped since the last call because every layer buffer was still held by the display (the - * 10 s perf line; the "no free layer buffer" log line itself is rate-limited). */ unsigned sc_layer_drops_take(void); #endif diff --git a/app/src/main/cpp/waylandcomp/src/vk_loader.c b/app/src/main/cpp/waylandcomp/src/vk_loader.c index ef939911c..0cda81cd0 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_loader.c +++ b/app/src/main/cpp/waylandcomp/src/vk_loader.c @@ -1,4 +1,3 @@ -/* See vk_loader.h. */ #define _GNU_SOURCE #include "vk_loader.h" #include "../../adrenotools/include/adrenotools/driver.h" diff --git a/app/src/main/cpp/waylandcomp/src/vk_loader.h b/app/src/main/cpp/waylandcomp/src/vk_loader.h index 32a15ae87..e10950d0d 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_loader.h +++ b/app/src/main/cpp/waylandcomp/src/vk_loader.h @@ -1,11 +1,5 @@ #ifndef VK_LOADER_H #define VK_LOADER_H -/* - * Loads Turnip via adrenotools (the same path the app uses for the guest renderer) - * and resolves every Vulkan entry point through its vkGetInstanceProcAddr - NOT the - * process-default system Adreno driver, which lacks VK_EXT_image_drm_format_modifier / - * dmabuf import. All compositor Vulkan calls go through g_vk.* . - */ #define VK_USE_PLATFORM_ANDROID_KHR #include <vulkan/vulkan.h> @@ -29,7 +23,7 @@ X(EndCommandBuffer) X(ResetFences) X(QueueSubmit) X(QueuePresentKHR) X(WaitForFences) \ X(QueueWaitIdle) X(GetMemoryFdPropertiesKHR) \ X(GetImageSubresourceLayout) X(MapMemory) X(UnmapMemory) X(CmdClearColorImage) \ - /* screen-effect chain (effects_chain.c): graphics pipelines over full-screen quads */ \ + \ X(CreateShaderModule) X(DestroyShaderModule) X(CreatePipelineLayout) X(DestroyPipelineLayout) \ X(CreateGraphicsPipelines) X(DestroyPipeline) X(CreateRenderPass) X(DestroyRenderPass) \ X(CreateFramebuffer) X(DestroyFramebuffer) X(CreateImageView) X(DestroyImageView) \ @@ -37,7 +31,7 @@ X(CreateDescriptorPool) X(DestroyDescriptorPool) X(AllocateDescriptorSets) X(FreeDescriptorSets) \ X(UpdateDescriptorSets) X(CmdBeginRenderPass) X(CmdEndRenderPass) X(CmdBindPipeline) \ X(CmdBindDescriptorSets) X(CmdPushConstants) X(CmdDraw) X(CmdSetViewport) X(CmdSetScissor) \ - /* frame generation (framegen_bridge.c / vk_present.c per-present sync slots) */ \ + \ X(DestroySemaphore) struct vk_api { @@ -47,14 +41,10 @@ struct vk_api { }; extern struct vk_api g_vk; -/* Open Turnip via adrenotools (falls back to system libvulkan if any arg is NULL) - * and load global entry points. Returns 0 on success. */ int vk_loader_open(const char *driver_path, const char *library_name, const char *native_lib_dir); void vk_loader_load_instance(VkInstance instance); void vk_loader_load_device(VkDevice device); -/* The driver's vkGetInstanceProcAddr (NULL before vk_loader_open): for modules that resolve their - * own entry points, e.g. the frame-generation engines (framegen_engine.cpp). */ PFN_vkGetInstanceProcAddr vk_loader_gipa(void); #endif diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.c b/app/src/main/cpp/waylandcomp/src/vk_present.c index 1042635f2..29db4f000 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.c +++ b/app/src/main/cpp/waylandcomp/src/vk_present.c @@ -1,5 +1,3 @@ -/* Android-surface render backend - see vk_present.h. Uses Turnip via vk_loader - * (g_vk.*), not the process-default system Adreno driver. */ #define _POSIX_C_SOURCE 200809L #include "vk_present.h" #include "vk_loader.h" @@ -29,78 +27,54 @@ struct vkp_image { VkImage image; VkDeviceMemory mem; int w, h; - int dmabuf; /* imported from a client; owned by the foreign queue family */ - int blit_dst; /* layer mode: a pool buffer this backend blits into (never a source) */ - void *map; /* shm images: persistently mapped linear memory */ + int dmabuf; + int blit_dst; + void *map; VkDeviceSize offset, row_pitch; - int in_general; /* shm images: moved from PREINITIALIZED to GENERAL */ + int in_general; VkFormat fmt; - int sampled; /* created with SAMPLED usage: the alpha pass can read it */ + int sampled; }; -static ANativeWindow *g_window; /* the window frames go to; compositor thread only */ +static ANativeWindow *g_window; static char *g_driver_path, *g_library_name, *g_native_lib_dir; -static int g_dev_state; /* 0 = not yet, 1 = ok, -1 = failed, -2 = lost (VK_ERROR_DEVICE_LOST) */ +static int g_dev_state; static VkInstance g_inst; static VkPhysicalDevice g_pd; static VkDevice g_dev; static VkQueue g_queue; static uint32_t g_qfam; static VkCommandPool g_pool; -/* One command buffer + acquire/render-done semaphore pair per PRESENT. A scene frame is one - * present, or up to 1 + VKP_FG_MAX_GENERATIONS with frame generation (the generated frames go - * out first, the real frame last), each on its own slot so no semaphore ever carries two - * pending signals. Slot 0 is also the layer-mode blit's command buffer. */ #define MAX_PRESENTS (1 + VKP_FG_MAX_GENERATIONS) static VkCommandBuffer g_cmds[MAX_PRESENTS]; static VkSemaphore g_acqs[MAX_PRESENTS], g_rnds[MAX_PRESENTS]; static VkFence g_fence; -/* Extra swapchain images this swapchain was built with (frame generation: one per generated - * frame, so all presents of a scene frame queue without waiting for a vblank). */ static int g_swap_extra; -/* Format of the compositor pass's images (the scene image below, the effects chain's targets and - * the frame-generation ring): a blit converts every client format into it, the chains read RGBA. */ #define SCENE_FMT VK_FORMAT_R8G8B8A8_UNORM static VkPhysicalDeviceMemoryProperties g_memprops; static VkSurfaceKHR g_surface; static VkSwapchainKHR g_swapchain; -/* The display's own rotation of everything we present (VkSurfaceCapabilitiesKHR::currentTransform), - * captured when the swapchain is built. Not a panel or device allowlist: it is what the presentation - * engine says it will do to our layers. */ static VkSurfaceTransformFlagBitsKHR g_surface_transform; static int g_surface_transform_known; static VkImage *g_images; static uint32_t g_nimg; static VkExtent2D g_extent; -static int g_first_frame_done; /* one-shot: fire droiddeck_on_first_frame() on first present */ +static int g_first_frame_done; -/* The base surface under the display layers (vkp_base_black): 1 once a plain black frame has been - * presented on the CURRENT swapchain and nothing else since. Every other present clears it, and so does - * every swapchain teardown, so the next layer frame puts one black frame back first. While it is set, - * a frame that lives entirely on the display layers draws and presents nothing here. */ static int g_base_black; -/* The layer path presents nothing on the base surface while the game keeps its layer, so the swapchain's - * OUT_OF_DATE (a resized surface, a new transform) would never be seen there: check_surface_changed() - * asks the surface directly instead, at most every 100 ms. caps.currentExtent at the last build. */ static VkExtent2D g_caps_extent; static int64_t g_surface_checked_ns; -/* Letterbox bars: vkCmdClearColorImage clears whole images only, so the bars around the picture are - * blitted from this small black image instead of clearing the whole output under a picture that covers - * most of it. Created and cleared once with the device. 1 = ready, 0/-1 = not available (whole clear). */ #define BLACK_DIM 16 static VkImage g_black_img; static VkDeviceMemory g_black_mem; static int g_black_state; -/* The display's release fence of a layer buffer, as a GPU wait (VK_KHR_external_semaphore_fd): the - * pool's next copy into that buffer waits for it on the GPU instead of the compositor thread polling it. */ static PFN_vkImportSemaphoreFdKHR g_import_sem_fd; static VkSemaphore g_wait_sem; -/* The 10 s perf line's counters (vkp_perf_take). Compositor thread only. */ static struct vkp_perf g_perf; static int64_t perf_now(void) { struct timespec ts; @@ -113,7 +87,6 @@ static void perf_add(int64_t *sum, int64_t *max, unsigned *count, int64_t d) { if (d > *max) *max = d; (*count)++; } -/* vkWaitForFences on the compositor's own work, timed for the perf line. */ static VkResult timed_wait(VkFence fence, uint64_t timeout) { int64_t t0 = perf_now(); VkResult r = g_vk.WaitForFences(g_dev, 1, &fence, VK_TRUE, timeout); @@ -121,93 +94,66 @@ static VkResult timed_wait(VkFence fence, uint64_t timeout) { return r; } -/* The scene image (effects path only): every draw composited 1:1 at scene size, the input of the - * screen-effect chain (effects_chain.c) - and of frame generation once that lands. Recreated on a - * scene size change; frames are fenced, so never while the GPU reads it. */ static struct { VkImage img; VkDeviceMemory mem; int w, h; } g_scene; -/* HDR composition (hdr_compose.h): the 10-bit image every draw is blitted into 1:1 in its own encoding - * ("mixed"), and the 10-bit HDR picture the encode pass makes of it for the game's display layer. Only - * ever created while an HDR game shares the scene with something the display layer cannot show alone. */ struct vkp_img_slot { VkImage img; VkDeviceMemory mem; int w, h; VkFormat fmt; }; static struct vkp_img_slot g_mixed, g_hdrscene, g_fgscene; #define HDR_FMT VK_FORMAT_A2B10G10R10_UNORM_PACK32 -/* Frame generation with HDR frames through an HDR10 swapchain: the scene, the effects and the engine's - * frames in FP16 - the format lsfg-vk itself uses for HDR - where the engine can build its chain in it, - * so the PQ picture keeps more than 8 bits through interpolation (framegen_bridge.h). */ #define FG_HDR_FMT VK_FORMAT_R16G16B16A16_SFLOAT -/* HDR10 swapchain (frame generation with an HDR game): VK_EXT_swapchain_colorspace is enabled on the - * instance only when the session asked for HDR; the swapchain is built as A2B10G10R10 + HDR10_ST2084 - * while such frames are being presented and the Android surface lists that pair (its WSI then sets the - * surface dataspace to BT2020_PQ). */ -static int g_colorspace_ext; /* the instance has VK_EXT_swapchain_colorspace */ -static int g_swap_hdr_want; /* the next swapchain should be HDR10 */ -static int g_swap_is_hdr; /* the live swapchain is HDR10 */ -static int g_swap_hdr_unavailable; /* this surface lists no HDR10 format: tone-map instead (said once) */ -static VkFormat g_swap_fmt; /* the live swapchain's format */ -/* VK_EXT_hdr_metadata (HDR sessions, where the device offers it): the HDR10 swapchain carries the game's - * mastering metadata, sent once per swapchain and image description. */ + + + + +static int g_colorspace_ext; +static int g_swap_hdr_want; +static int g_swap_is_hdr; +static int g_swap_hdr_unavailable; +static VkFormat g_swap_fmt; + + static PFN_vkSetHdrMetadataEXT g_set_hdr_metadata; static uint32_t g_swap_md_identity; static int g_surface_formats_said; static const char *vk_result_name(VkResult r); -/* The Android surface is created/destroyed on the app's UI thread while the compositor thread - * may be inside a WSI call (acquire and present can block for a refresh or more). The UI thread - * therefore never touches the swapchain: it leaves the new window here and returns at once; the - * compositor thread picks it up before its next frame (vkp_apply_window_request). g_req_lock is - * only ever held for these few assignments, never across a Vulkan call. */ static pthread_mutex_t g_req_lock = PTHREAD_MUTEX_INITIALIZER; static ANativeWindow *g_req_window; static int g_req_pending; -/* A detach was asked for since the compositor thread last looked. A rebind (null, then the same - * window) to rebuild the swapchain at a new size can land in full before the thread gets to it, - * and would otherwise read as the same window handed over again - nothing rebuilt, and the old - * size's buffers scaled onto the rotated or refolded panel. */ static int g_req_detached; -/* Scale mode + alignment (app values, see vk_present.h); written from any thread, read per frame. */ static volatile int g_mode = VKP_MODE_STRETCH, g_align = VKP_ALIGN_CENTER; -/* The scene -> output mapping of the last frame (compositor thread): scene pixel (x,y) lands at - * output (off_x + x * kx, off_y + y * ky), clipped to the region rectangle. */ static struct { int scene_w, scene_h, out_w, out_h, mode, align; float kx, ky, off_x, off_y; - int rx, ry, rw, rh; /* region the picture may occupy */ + int rx, ry, rw, rh; int valid; } g_map; -/* A failed swapchain creation is retried no sooner than this (the window may be mid-teardown), - * and the failure is logged once per streak instead of every frame. */ static int64_t g_swap_retry_at_ns; static int g_swap_fail_logged; -/* Implemented in waylandcomp_jni.c - notifies Java (dismiss launch overlay). */ extern void droiddeck_on_first_frame(void); static char g_gpu_name[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE]; const char *vkp_gpu_name(void) { return g_gpu_name; } -/* DRM fourccs name the channels of a little-endian 32-bit word: XRGB8888 is B,G,R,X in memory - * (= VK B8G8R8A8) and XBGR8888 is R,G,B,X (= VK R8G8B8A8). Turnip's Wayland WSI sends XB24 for - * R8G8B8A8 swapchains, so reading everything as BGRA swaps red and blue. */ static VkFormat drm_to_vk(uint32_t drm) { switch (drm) { - case 0x34324241: /* AB24 */ - case 0x34324258: /* XB24 */ + case 0x34324241: + case 0x34324258: return VK_FORMAT_R8G8B8A8_UNORM; - case 0x30334241: /* AB30 */ - case 0x30334258: /* XB30 */ + case 0x30334241: + case 0x30334258: return VK_FORMAT_A2B10G10R10_UNORM_PACK32; - case 0x30335241: /* AR30 */ - case 0x30335258: /* XR30 */ + case 0x30335241: + case 0x30335258: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; - case 0x48344241: /* AB4H */ - case 0x48344258: /* XB4H */ + case 0x48344241: + case 0x48344258: return VK_FORMAT_R16G16B16A16_SFLOAT; - default: /* AR24 / XR24 */ + default: return VK_FORMAT_B8G8R8A8_UNORM; } } @@ -220,10 +166,6 @@ void vk_present_set_driver(const char *driver_path, const char *library_name, g_native_lib_dir = native_lib_dir ? strdup(native_lib_dir) : NULL; } -/* Destroy and recreate every acquire/render-done semaphore. Done with the swapchain (after the - * device went idle): a frame that acquired an image and then bailed out leaves its acquire - * semaphore signalled with nothing to wait on it, and reusing it in the next acquire is invalid - * (Adreno answers OUT_OF_DATE, which rebuilds again - a loop). Fresh objects cannot carry that. */ static void reset_sync(void) { VkSemaphoreCreateInfo semci = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; for (int k = 0; k < MAX_PRESENTS; k++) { @@ -235,14 +177,14 @@ static void reset_sync(void) { } static void destroy_swapchain(void) { - g_base_black = 0; /* a new swapchain starts with nothing on it */ + g_base_black = 0; if (g_dev_state != 1) return; g_vk.DeviceWaitIdle(g_dev); reset_sync(); for (uint32_t i = 0; i < g_nimg; i++) blendp_forget_image(g_images[i]); if (g_swapchain) g_vk.DestroySwapchainKHR(g_dev, g_swapchain, NULL); g_swapchain = VK_NULL_HANDLE; - g_swap_md_identity = 0; /* a new swapchain gets the metadata again */ + g_swap_md_identity = 0; if (g_surface) g_vk.DestroySurfaceKHR(g_inst, g_surface, NULL); g_surface = VK_NULL_HANDLE; free(g_images); @@ -253,8 +195,6 @@ static void destroy_swapchain(void) { void vk_present_set_window(ANativeWindow *window) { ANativeWindow *superseded = NULL; pthread_mutex_lock(&g_req_lock); - /* Two requests before the compositor thread got to the first: the first window was never - * used, so its reference is dropped here. */ if (g_req_pending && g_req_window && g_req_window != window) superseded = g_req_window; g_req_window = window; g_req_pending = 1; @@ -274,18 +214,16 @@ int vkp_apply_window_request(void) { g_req_detached = 0; pthread_mutex_unlock(&g_req_lock); if (w == g_window) { - /* The same window handed over again (ANativeWindow_fromSurface adds a reference each time). */ if (w) ANativeWindow_release(w); if (!w || !detached) return 0; - /* ... after a detach: a rebind for a resized surface. Rebuilt at its size on the next frame. */ sc_layer_window_gone(); destroy_swapchain(); droiddeck_log("gpu", "screen surface rebound: rebuilding the swapchain at its new size"); return 1; } - sc_layer_window_gone(); /* the layer (if any) belongs to the old window */ - destroy_swapchain(); /* recreated against the new window on the next frame */ - g_swap_hdr_unavailable = 0; /* a new surface (another display, say) may offer HDR10 */ + sc_layer_window_gone(); + destroy_swapchain(); + g_swap_hdr_unavailable = 0; if (g_window) ANativeWindow_release(g_window); g_window = w; g_swap_retry_at_ns = 0; @@ -329,11 +267,6 @@ static const char *align_name(int a) { } } -/* Mirror of ViewTransformation.update(outer = output, inner = scene, mode, alignment) - keep the - * arithmetic identical: the app maps touch input through that class with the same inputs, so any - * difference here puts the pointer beside what it is pointing at. OFF and FIT are both an - * aspect-preserving letterbox (OFF only differs in the app's fullscreen gates); TOP/BOTTOM confine - * the picture to the top/bottom half of the output (the app's handheld split, #413). */ static void update_map(int scene_w, int scene_h) { int mode = g_mode, align = g_align; int W = (int)g_extent.width, H = (int)g_extent.height; @@ -358,11 +291,10 @@ static void update_map(int scene_w, int scene_h) { if (mode == VKP_MODE_FILL) aspect = sx > sy ? sx : sy; else if (mode == VKP_MODE_INTEGER) { float m = sx < sy ? sx : sy; - aspect = (float)(int)m; /* floor for m >= 1 */ + aspect = (float)(int)m; if (aspect < 1.0f) aspect = 1.0f; } else aspect = sx < sy ? sx : sy; g_map.kx = g_map.ky = aspect; - /* Same integer truncation as ViewTransformation's viewOffsetX/Y. */ g_map.off_x = (float)(g_map.rx + (int)((g_map.rw - scene_w * aspect) * 0.5f)); g_map.off_y = (float)(g_map.ry + (int)((g_map.rh - scene_h * aspect) * 0.5f)); } @@ -393,11 +325,9 @@ static int has_ext(VkExtensionProperties *e, uint32_t n, const char *name) { return 0; } -/* Instance + device + command objects. Doesn't need the window. */ static int dev_init(void) { if (g_dev_state != 0) return g_dev_state == 1 ? 0 : -1; - /* Load Turnip (adrenotools) and its entry points - NOT the system driver. */ if (vk_loader_open(g_driver_path, g_library_name, g_native_lib_dir) != 0) { LOGE("present: vk_loader_open failed"); g_dev_state = -1; return -1; } @@ -405,9 +335,6 @@ static int dev_init(void) { const char *inst_exts[3] = {VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME, NULL}; uint32_t n_inst_exts = 2; - /* HDR sessions only (droiddeck_color.h): the colour-space extension lets the swapchain carry HDR10 for - * frame generation. Asked for only when the loader has it, and only when HDR was asked for, so - * every other session creates exactly the instance it always did. */ if (droiddeck_color_requested() && g_vk.EnumerateInstanceExtensionProperties) { uint32_t ne = 0; g_vk.EnumerateInstanceExtensionProperties(NULL, &ne, NULL); @@ -425,8 +352,6 @@ static int dev_init(void) { "generation %s)", g_colorspace_ext ? "enables" : "has no", g_colorspace_ext ? "is possible where the screen surface offers one" : "is not possible: tone-mapped instead"); } - /* 1.3 like the X11 renderer's instance: the frame-generation probe (framegen_engine.cpp) - * queries VkPhysicalDeviceVulkan12Features, and a 1.1 instance may be answered as 1.1. */ VkApplicationInfo app = {.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "droiddeck-wayland-present", .apiVersion = VK_API_VERSION_1_3}; @@ -464,7 +389,6 @@ static int dev_init(void) { } g_vk.GetPhysicalDeviceMemoryProperties(g_pd, &g_memprops); - /* Verify the dmabuf-import extensions are present, and log any that are missing. */ const char *dev_exts[7] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME, "VK_KHR_external_memory_fd", "VK_EXT_external_memory_dma_buf", "VK_EXT_image_drm_format_modifier", "VK_KHR_image_format_list", NULL, NULL}; @@ -476,14 +400,11 @@ static int dev_init(void) { for (unsigned i = 0; i < 5; i++) if (!has_ext(exts, ne, dev_exts[i])) LOGE("present: driver MISSING %s (dmabuf import will fail)", dev_exts[i]); - /* HDR sessions only: the HDR10 swapchain (frame generation) can carry the game's metadata. */ int want_hdr_md = 0; if (droiddeck_color_requested()) { want_hdr_md = has_ext(exts, ne, VK_EXT_HDR_METADATA_EXTENSION_NAME); if (want_hdr_md) dev_exts[n_dev_exts++] = VK_EXT_HDR_METADATA_EXTENSION_NAME; } - /* The layer pool's release fences as GPU waits (sync_file -> semaphore): optional, the pool falls - * back to waiting for them on the CPU without it. */ const int want_sem_fd = has_ext(exts, ne, VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME); if (want_sem_fd) dev_exts[n_dev_exts++] = VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME; free(exts); @@ -491,10 +412,6 @@ static int dev_init(void) { float prio = 1.0f; VkDeviceQueueCreateInfo qci = {.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, .queueFamilyIndex = g_qfam, .queueCount = 1, .pQueuePriorities = &prio}; - /* Frame generation (framegen_bridge.c): the LSFG chain needs the memory-model and - * storage-image features enabled at device creation; the probe hands back the pNext chain - * for them, or NULL on a device that cannot run it (then Win-FG Native alone is offered). A - * driver that rejects the chain costs nothing: retried once without it, exactly as before. */ const void *fg_features = vkp_framegen_device_features(g_inst, vk_loader_gipa(), g_pd); VkDeviceCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .pNext = fg_features, .queueCreateInfoCount = 1, .pQueueCreateInfos = &qci, @@ -579,7 +496,6 @@ static int swap_init(void) { #define SWLOGE(...) do { if (!g_swap_fail_logged) LOGE(__VA_ARGS__); } while (0) -/* Short names for the session log (the formats and colour spaces an Android surface lists). */ static void vk_format_short(VkFormat f, char *out, size_t n) { const char *s = NULL; switch (f) { @@ -622,8 +538,6 @@ static const char *vk_colorspace_short(VkColorSpaceKHR c) { default: return NULL; } } -/* One line: the total, how many colour spaces each format comes with, and every HDR-capable pair - * (HDR10 / HLG / extended sRGB, any format) - never "the first N pairs". */ static int hdr_capable_space(VkColorSpaceKHR cs) { int c = (int)cs; return c == VK_COLOR_SPACE_HDR10_ST2084_EXT || c == VK_COLOR_SPACE_HDR10_HLG_EXT || @@ -644,7 +558,6 @@ static void log_surface_formats(const VkSurfaceFormatKHR *f, uint32_t n) { pp += (size_t)snprintf(per + pp, sizeof(per) - pp, "%s%s x%u", pp ? ", " : "", nm, cnt); } } - /* The deep formats (10-bit, FP16) first, so a long list never pushes them out of the line. */ for (int pass = 0; pass < 2; pass++) for (uint32_t i = 0; i < n; i++) { if (!hdr_capable_space(f[i].colorSpace) || (vk_format_bits(f[i].format) > 8) != (pass == 0)) continue; @@ -673,17 +586,10 @@ static int swap_init_locked(void) { VkSurfaceCapabilitiesKHR caps; g_vk.GetPhysicalDeviceSurfaceCapabilitiesKHR(g_pd, g_surface, &caps); - /* Remembered for the layer path: currentTransform is the rotation the presentation engine (and - * therefore SurfaceFlinger and the DPU) applies to everything we put on this display. Anything - * but IDENTITY means every display layer we hand over is a ROTATED layer. sc_layer.c needs that - * to decide whether a second layer is affordable - see sc_layer_present_overlay(). */ g_surface_transform = caps.currentTransform; g_surface_transform_known = 1; - g_caps_extent = caps.currentExtent; /* check_surface_changed() compares against this */ + g_caps_extent = caps.currentExtent; g_surface_checked_ns = perf_now(); - /* Every format/colour-space pair the surface lists. With VK_EXT_swapchain_colorspace the Android - * WSI lists each format once per colour space (the Fold: 11 per format), so a fixed-size array - * silently drops the 10-bit and FP16 rows - which is where HDR10 lives. */ uint32_t nfmt = 0; g_vk.GetPhysicalDeviceSurfaceFormatsKHR(g_pd, g_surface, &nfmt, NULL); VkSurfaceFormatKHR *fmts = nfmt ? calloc(nfmt, sizeof(*fmts)) : NULL; @@ -697,10 +603,6 @@ static int swap_init_locked(void) { g_surface_formats_said = 1; log_surface_formats(fmts, nfmt); } - /* HDR10 for frame generation with an HDR game (render_impl sets g_swap_hdr_want): the best format - * the surface lists with VK_COLOR_SPACE_HDR10_ST2084_EXT - 10-bit, then FP16, then 8-bit (still a - * PQ BT.2020 picture, with less precision). None listed -> the frames are tone-mapped into the - * ordinary swapchain, and that is said once. */ g_swap_is_hdr = 0; if (g_swap_hdr_want) { static const VkFormat pref[] = {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32, @@ -736,17 +638,10 @@ static int swap_init_locked(void) { g_extent.width = ANativeWindow_getWidth(g_window); g_extent.height = ANativeWindow_getHeight(g_window); } - /* + one image per generated frame while frame generation is armed: every present of a scene - * frame can then be queued at once and FIFO spaces them onto consecutive vblanks. */ g_swap_extra = vkp_framegen_extra_images(); uint32_t want = caps.minImageCount + 1 + (uint32_t)g_swap_extra; if (caps.maxImageCount && want > caps.maxImageCount) want = caps.maxImageCount; - /* Use IDENTITY preTransform when the surface supports it. Setting preTransform = - * currentTransform tells the presentation engine our content is ALREADY pre-rotated by - * that amount - but our blit doesn't rotate, so on a device whose surface reports a 90° - * currentTransform the display then rotates our upright frame 90° (game shows sideways). - * IDENTITY = "don't rotate what I present", which is what we want. */ VkSurfaceTransformFlagBitsKHR pretrans = (caps.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) ? VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR : caps.currentTransform; @@ -761,8 +656,6 @@ static int swap_init_locked(void) { .presentMode = VK_PRESENT_MODE_FIFO_KHR, .clipped = VK_TRUE}; VkResult cr = g_vk.CreateSwapchainKHR(g_dev, &sci, NULL, &g_swapchain); if (cr != VK_SUCCESS && g_swap_is_hdr) { - /* The surface listed the HDR10 pair but refused the swapchain: say so, never ask again this - * surface, and build the ordinary one now (frames tone-mapped) - never a black screen. */ droiddeck_log("color", "the driver refused the HDR10 swapchain (%s): frames with frame generation are " "tone-mapped to SDR instead", vk_result_name(cr)); g_swap_is_hdr = 0; @@ -794,9 +687,6 @@ static int memory_type(uint32_t bits, VkMemoryPropertyFlags want) { return -1; } -/* The small black image the letterbox bars are blitted from (see g_black_img): created, cleared and left - * in TRANSFER_SRC_OPTIMAL once, with a one-time submit, right after the device comes up. Any failure only - * means the bars are cleared the old way (the whole output). */ static void init_black_image(void) { g_black_state = -1; VkImageCreateInfo ici = { @@ -851,10 +741,6 @@ static void init_black_image(void) { droiddeck_log("gpu", "letterbox black image unavailable: the whole output is cleared every copy-path frame"); } -/* Blit the black image over everything of swapchain image `img` (in TRANSFER_DST_OPTIMAL) OUTSIDE the - * destination rectangle of `covered` - the letterbox bars around a picture that fills that rectangle - * completely. Up to four rectangles, one blit command. 0 = not possible (no black image): the caller - * clears the whole image instead. */ static int record_bars_clear(VkCommandBuffer cmd, VkImage img, const VkImageBlit *covered) { if (g_black_state != 1 || !covered) return 0; const int W = (int)g_extent.width, H = (int)g_extent.height; @@ -868,11 +754,11 @@ static int record_bars_clear(VkCommandBuffer cmd, VkImage img, const VkImageBlit if (y1 > H) y1 = H; if (x1 <= x0 || y1 <= y0) return 0; int rects[4][4], n = 0; - if (y0 > 0) { rects[n][0] = 0; rects[n][1] = 0; rects[n][2] = W; rects[n][3] = y0; n++; } /* top */ - if (y1 < H) { rects[n][0] = 0; rects[n][1] = y1; rects[n][2] = W; rects[n][3] = H; n++; } /* bottom */ - if (x0 > 0) { rects[n][0] = 0; rects[n][1] = y0; rects[n][2] = x0; rects[n][3] = y1; n++; } /* left */ - if (x1 < W) { rects[n][0] = x1; rects[n][1] = y0; rects[n][2] = W; rects[n][3] = y1; n++; } /* right */ - if (!n) return 1; /* the picture covers the whole output: nothing to clear */ + if (y0 > 0) { rects[n][0] = 0; rects[n][1] = 0; rects[n][2] = W; rects[n][3] = y0; n++; } + if (y1 < H) { rects[n][0] = 0; rects[n][1] = y1; rects[n][2] = W; rects[n][3] = H; n++; } + if (x0 > 0) { rects[n][0] = 0; rects[n][1] = y0; rects[n][2] = x0; rects[n][3] = y1; n++; } + if (x1 < W) { rects[n][0] = x1; rects[n][1] = y0; rects[n][2] = W; rects[n][3] = y1; n++; } + if (!n) return 1; VkImageBlit blits[4]; for (int i = 0; i < n; i++) blits[i] = (VkImageBlit){.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, @@ -893,9 +779,8 @@ const char *vkp_modifier_name(uint64_t modifier) { return other; } -/* Can the driver create and import this format/modifier with the requested image usage? */ static int modifier_importable(VkFormat fmt, uint64_t modifier, VkImageUsageFlags usage) { - if (!g_vk.GetPhysicalDeviceImageFormatProperties2) return 1; /* can't ask; the create will tell */ + if (!g_vk.GetPhysicalDeviceImageFormatProperties2) return 1; VkPhysicalDeviceExternalImageFormatInfo ext = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO, .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT}; @@ -1032,7 +917,6 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t return img; } -/* Linear wl_shm images can use the blend pass only if the driver samples them with a linear filter. */ static int shm_sampled(void) { static int known; if (!known) { @@ -1086,8 +970,6 @@ struct vkp_image *vkp_image_create_shm(int w, int h) { return img; } -/* Renders are synchronous (we wait for the frame's fence), so writing between frames - * never races the GPU. */ void vkp_image_upload_shm(struct vkp_image *img, const void *data, int stride) { if (!img || !img->map || !data) return; size_t rowbytes = (size_t)img->w * 4; @@ -1111,9 +993,6 @@ void vkp_image_destroy(struct vkp_image *img) { free(img); } -/* Map a draw into swapchain pixels through the scale mode, clipping the destination to the - * picture's region (the whole output, or its half on TOP/BOTTOM; FILL's overflow is cut here) - * and trimming the source to match. Returns 0 if nothing is left to draw. */ static int map_draw(const struct vkp_draw *d, float kx, float ky, float off_x, float off_y, float L, float T, float R, float B, VkImageBlit *blit) { float x0 = off_x + d->dx * kx, y0 = off_y + d->dy * ky; @@ -1148,12 +1027,10 @@ static int draw_to_blit(const struct vkp_draw *d, VkImageBlit *blit) { return map_draw(d, g_map.kx, g_map.ky, g_map.off_x, g_map.off_y, (float)g_map.rx, (float)g_map.ry, R, B, blit); } -/* The same draw 1:1 into the scene image (no mapping: scene pixels are the destination). */ static int draw_to_scene_blit(const struct vkp_draw *d, int scene_w, int scene_h, VkImageBlit *blit) { return map_draw(d, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, (float)scene_w, (float)scene_h, blit); } -/* A translucent draw needs source-over blending; every other draw keeps the existing blit path. */ static void record_draw(VkCommandBuffer cmd, const struct vkp_draw *d, const VkImageBlit *blit, VkImage target, VkFormat target_fmt, int target_w, int target_h, VkFilter filter) { const struct vkp_image *im = d->img; @@ -1167,15 +1044,13 @@ static void record_draw(VkCommandBuffer cmd, const struct vkp_draw *d, const VkI static void destroy_scene_image(void) { if (g_scene.img) vkp_effects_forget_image(g_scene.img); - if (g_scene.img) hdrc_forget_image(g_scene.img); /* the HDR pass may have drawn into it */ + if (g_scene.img) hdrc_forget_image(g_scene.img); if (g_scene.img) blendp_forget_image(g_scene.img); if (g_scene.img) g_vk.DestroyImage(g_dev, g_scene.img, NULL); if (g_scene.mem) g_vk.FreeMemory(g_dev, g_scene.mem, NULL); memset(&g_scene, 0, sizeof(g_scene)); } -/* The scene image at this size (R8G8B8A8: a blit converts every client format into it, and the - * chain's shaders read it as RGBA). 0 = ready. */ static int ensure_scene_image(int w, int h) { if (g_scene.img && g_scene.w == w && g_scene.h == h) return 0; destroy_scene_image(); @@ -1184,7 +1059,7 @@ static int ensure_scene_image(int w, int h) { .format = VK_FORMAT_R8G8B8A8_UNORM, .extent = {(uint32_t)w, (uint32_t)h, 1}, .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL, .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | - VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, /* the HDR encode pass writes it (frame generation) */ + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED}; VkResult r = g_vk.CreateImage(g_dev, &ici, NULL, &g_scene.img); if (r != VK_SUCCESS) { LOGE("effects: scene image %dx%d: vkCreateImage %s", w, h, vk_result_name(r)); g_scene.img = VK_NULL_HANDLE; return -1; } @@ -1202,7 +1077,6 @@ static int ensure_scene_image(int w, int h) { return 0; } -/* One of the HDR composition's device-local images at this size and format. 0 = ready. */ static int ensure_img(struct vkp_img_slot *s, int w, int h, VkFormat fmt, VkImageUsageFlags usage, const char *what) { if (s->img && s->w == w && s->h == h && s->fmt == fmt) return 0; if (s->img) hdrc_forget_image(s->img); @@ -1239,11 +1113,6 @@ static int ensure_img(struct vkp_img_slot *s, int w, int h, VkFormat fmt, VkImag return 0; } -/* The HDR composition (hdr_compose.h): clear the mixed image, blit every draw into it 1:1 in its own - * encoding, then encode it into `out` (left in TRANSFER_DST_OPTIMAL). The caller has already taken the - * draws' images (queue-family acquire / host-write barriers). Which rectangles are HDR comes from - * hf->is_hdr: listed top draw first, down to the lowest HDR draw (everything under that is SDR). - * 0 = done, -1 = the HDR pass is unavailable (nothing was recorded into `out`). */ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int scene_w, int scene_h, VkImage out, VkFormat out_fmt, enum hdrc_mode mode) { if (!hf || !hf->is_hdr || n <= 0) return -1; @@ -1272,7 +1141,6 @@ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n, for (int i = 0; i < n; i++) { VkImageBlit blit; if (!draws[i].img || !draw_to_scene_blit(&draws[i], scene_w, scene_h, &blit)) continue; - /* A blit converts UNORM to UNORM by value: 8-bit sRGB and 10-bit PQ both land unchanged. */ record_draw(cmd, &draws[i], &blit, g_mixed.img, HDR_FMT, scene_w, scene_h, VK_FILTER_LINEAR); if (hf->is_hdr[i] && lowest_hdr < 0) lowest_hdr = i; } @@ -1296,9 +1164,6 @@ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n, return hdrc_encode(cmd, g_mixed.img, out, out_fmt, scene_w, scene_h, &p); } -/* VK_ERROR_DEVICE_LOST: nothing on this device works any more, and there is no way back short - * of a new session. Say so once and stop touching the swapchain; clients keep being paced by the - * compositor (see pace_without_output) so they don't wedge, they just aren't shown. */ static void device_lost(const char *where) { if (g_dev_state == -2) return; g_dev_state = -2; @@ -1337,17 +1202,10 @@ static const char *vk_result_name(VkResult r) { } } -/* ---------------------------------------------------------------- the compositor pass */ -/* The mapping/blit stage for one present: clear the swapchain image to black (the letterbox), - * blit `src` (src_w x src_h, in GENERAL: the effects chain's result or a generated frame, either - * standing for the whole scene) through the scene -> output mapping, and leave the image ready - * to present. */ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int src_h, int scene_w, int scene_h, uint32_t img, VkFilter filter, int cardboard) { VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - /* Cardboard mode (cardboard.h): the pass turns src into the two eye views over the whole panel, - * and that image is blitted 1:1 instead of src going through the scene mapping. */ int direct = 0; if (cardboard) { VkImageMemoryBarrier rd = { @@ -1371,8 +1229,6 @@ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = g_images[img], .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT}, - /* Whatever wrote src last (blits, the effects chain, a compute dispatch, in this or an - * earlier submission on this queue) must be visible to the blit's read. */ {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_GENERAL, .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, @@ -1380,7 +1236,7 @@ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT}}; g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 2, pre); - struct vkp_image tmp = {.w = src_w, .h = src_h}; /* only its size is looked at */ + struct vkp_image tmp = {.w = src_w, .h = src_h}; struct vkp_draw d = {&tmp, 0, 0, (float)src_w, (float)src_h, 0, 0, scene_w, scene_h}; VkImageBlit blit; const int have_blit = direct ? 1 : draw_to_blit(&d, &blit); @@ -1389,7 +1245,6 @@ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int .srcOffsets = {{0, 0, 0}, {src_w, src_h, 1}}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{0, 0, 0}, {src_w, src_h, 1}}}; - /* The result fills its whole mapped rectangle, so only the letterbox bars around it need black. */ if (!have_blit || !record_bars_clear(cmd, g_images[img], &blit)) { VkClearColorValue black = {.float32 = {0.0f, 0.0f, 0.0f, 1.0f}}; g_vk.CmdClearColorImage(cmd, g_images[img], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); @@ -1411,16 +1266,9 @@ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int 0, 0, NULL, 0, NULL, 1, &b_present); } -/* Acquire a swapchain image for present slot k. On the frame's FIRST acquire the swapchain is - * (re)created when needed and rebuilt once when the surface changed under us (OUT_OF_DATE after a - * resize/rotation, SURFACE_LOST when the window was torn down) - once, so a frame is not lost to - * a recreate that would have succeeded. Later slots have presents in flight on this swapchain, - * so they never rebuild; they fail and the frame ends early. 0 on success. */ static int acquire_image(int k, int first, uint32_t *img, VkResult *ar_out) { for (int attempt = 0; ; attempt++) { if (!g_swapchain && (!first || swap_init() != 0)) return -1; - /* A bounded wait: a surface that went away without telling us must not park the - * compositor thread forever (clients are paced from this thread). */ const int64_t t_acq = perf_now(); VkResult ar = g_vk.AcquireNextImageKHR(g_dev, g_swapchain, 1000000000ULL, g_acqs[k], VK_NULL_HANDLE, img); perf_add(&g_perf.acquire_ns, &g_perf.acquire_max_ns, &g_perf.acquires, perf_now() - t_acq); @@ -1434,14 +1282,11 @@ static int acquire_image(int k, int first, uint32_t *img, VkResult *ar_out) { } droiddeck_log("error", "present: acquire %d failed (%s %d)", k, vk_result_name(ar), (int)ar); if (ar == VK_TIMEOUT || ar == VK_NOT_READY) return -1; - destroy_swapchain(); /* anything else: start over next frame */ + destroy_swapchain(); return -1; } } -/* vkp_render_plain: the base surface's black frame with the compositor pass skipped (HDR game on its - * layer, compositor.c) - neither the effects chain nor frame generation runs, and so neither paces - * this loop with extra presents. */ static int g_plain_frame; int vkp_render_plain(int scene_w, int scene_h) { g_plain_frame = 1; @@ -1450,10 +1295,6 @@ int vkp_render_plain(int scene_w, int scene_h) { return r; } -/* A surface that changed size or transform under a swapchain nobody presents to (the base surface - * while the game is on its display layer) never reports OUT_OF_DATE, so ask it - at most every - * 100 ms, on the compositor thread - and tear the swapchain down when it no longer matches; the next - * frame rebuilds it (and, in layer mode, puts one black frame on it). */ static void check_surface_changed(void) { if (!g_swapchain || !g_surface || g_dev_state != 1) return; const int64_t now = perf_now(); @@ -1475,7 +1316,7 @@ int vkp_base_black(int scene_w, int scene_h) { check_surface_changed(); if (g_base_black && g_swapchain && g_window && g_dev_state == 1) { g_perf.base_kept++; - return 0; /* the black frame from before is still what the base surface shows */ + return 0; } int r = vkp_render_plain(scene_w, scene_h); if (r == 0) g_perf.base_presents++; @@ -1489,10 +1330,6 @@ void vkp_perf_take(struct vkp_perf *out) { int vkp_can_wait_sync_fd(void) { return g_import_sem_fd != NULL && g_wait_sem != VK_NULL_HANDLE; } -/* Hand a sync_file to the next submit as a GPU wait (temporary import into g_wait_sem; the driver - * owns the fd from then on). Returns 1 when the submit must wait on g_wait_sem, 0 when there is nothing - * to wait on - the fd was -1, or it could not be imported and was waited for on the CPU (bounded) and - * closed instead. -1 = the display is still reading the buffer after 100 ms: do not write it. */ static int take_wait_fd(int fd) { if (fd < 0) return 0; if (vkp_can_wait_sync_fd()) { @@ -1516,8 +1353,6 @@ static int take_wait_fd(int fd) { return r == 0 ? -1 : 0; } -/* The HDR10 swapchain's metadata: the game's SMPTE 2086 / CTA-861.3 values (its image description) via - * VK_EXT_hdr_metadata, once per swapchain and description. A game that gave none gets none. */ static void send_hdr_metadata(const struct droiddeck_color *c) { static int said_none, said_nodesc; if (!c) return; @@ -1552,26 +1387,19 @@ static void send_hdr_metadata(const struct droiddeck_color *c) { c->identity, c->text); } -/* The whole present: vkp_render (hf == NULL, exactly as it always was) and vkp_render_hdr (a scene with - * HDR draws: composed into one encoding, hdr_compose.h). */ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int *how) { if (how) *how = 0; vkp_apply_window_request(); - check_surface_changed(); /* a rotation or a fold the rebind did not cover: rebuild at the new size */ + check_surface_changed(); if (g_dev_state == -2) return -1; if (dev_init() != 0 || !g_window || scene_w <= 0 || scene_h <= 0) return -1; - /* Frame generation queues several presents per scene frame and needs swapchain images for - * them: rebuild when that changes (armed, disarmed, another multiplier). */ if (g_swapchain && vkp_framegen_extra_images() != g_swap_extra) { droiddeck_log("gpu", "frame generation now wants %d present%s per frame: rebuilding the swapchain", 1 + vkp_framegen_extra_images(), vkp_framegen_extra_images() ? "s" : ""); destroy_swapchain(); } - /* HDR frames presented here (frame generation with an HDR game) want an HDR10 swapchain - unless the - * drawer's HDR output switch is off (tone-mapped) - nothing else does. A change of that rebuilds the - * swapchain (only in HDR sessions: g_colorspace_ext). */ const int want_hdr = hf && !hf->tonemap && g_colorspace_ext && !g_swap_hdr_unavailable; if (g_swapchain && want_hdr != g_swap_is_hdr) { droiddeck_log("color", want_hdr ? "HDR frames with frame generation: rebuilding the screen swapchain as HDR10" @@ -1585,21 +1413,13 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i VkResult ar; if (acquire_image(0, 1, &img, &ar) != 0) return -1; update_map(scene_w, scene_h); - /* A new HDR10 swapchain, or a new image description on it: hand the game's metadata over. */ if (g_swap_is_hdr && hf && hf->color && hf->color->identity != g_swap_md_identity) { g_swap_md_identity = hf->color->identity; send_hdr_metadata(hf->color); } - /* The compositor pass - scene -> effects -> frame generation -> mapping/blit -> swapchain - - * composes the scene off-screen when either stage needs the whole frame: the screen-effect - * chain (effects_chain.c) or frame generation (framegen_bridge.c). With both off the draws are - * blitted straight through the mapping into the swapchain image, as they always were; only the - * blit filter follows the scaling mode. */ const int fx = !g_plain_frame && vkp_effects_active(); const int fg = !g_plain_frame && vkp_framegen_active(); - /* Cardboard mode needs the whole frame too; it works on the 8-bit picture only (an HDR frame - * is shown flat, said once). */ int cb = !g_plain_frame && vkp_cardboard_active(); if (cb && hf) { static int said; @@ -1607,9 +1427,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i cb = 0; } vkp_cardboard_begin_frame(); - /* HDR frames with frame generation through an HDR10 swapchain: the scene, the effects and the - * engine's frames in FP16 (FG_HDR_FMT) where the engine can take that format - more than 8 bits of - * PQ through interpolation - else the 8-bit scene, as before. */ const int deep = hf && n > 0 && fg && g_swap_is_hdr && !hf->tonemap && vkp_framegen_format_ok(FG_HDR_FMT) && ensure_img(&g_fgscene, scene_w, scene_h, FG_HDR_FMT, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | @@ -1629,7 +1446,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i "into the HDR10 swapchain (%s): banding possible in smooth gradients", sw); } } - /* An HDR scene always takes the pass: its draws are composed into one encoding first. */ const int pass = deep || ((fx || fg || cb || (hf && n > 0)) && ensure_scene_image(scene_w, scene_h) == 0); VkImage scene_img = deep ? g_fgscene.img : g_scene.img; const VkFormat scene_fmt = deep ? FG_HDR_FMT : SCENE_FMT; @@ -1643,11 +1459,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i blendp_begin_frame(); VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - /* Source images: take dmabufs from the client's queue family, move shm images to - * GENERAL once (host writes stay visible across frames: memory is coherent and each - * frame is a new submission). One barrier per distinct image. The destination joins them: - * the swapchain image on the direct path, the scene image on the compositor pass (the - * per-present blits transition the swapchain images themselves there). */ VkImageMemoryBarrier *bars = calloc((size_t)n + 1, sizeof(*bars)); int nb = 0; bars[nb++] = (VkImageMemoryBarrier){ @@ -1682,12 +1493,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, (uint32_t)nb, bars); free(bars); - /* Clear the composition target to black (the letterbox on the direct path, the desktop's - * background on the pass) before the draws land on it. An opaque draw that covers the whole scene - * overwrites every pixel of its rectangle, so then only what lies outside it - * needs black: the letterbox bars on the direct path, nothing at all on the pass (the scene image - * is exactly the scene). Anything else - no such draw, the HDR composition - clears the whole - * target as before. */ VkImage target = pass ? scene_img : g_images[img]; int covered = 0; VkImageBlit cover_blit; @@ -1712,10 +1517,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i 0, 1, &mb, 0, NULL, 0, NULL); } - /* 1. Scene: the draws in order, 1:1 into the scene image (pass) or mapped onto the output. An HDR - * scene is composed into ONE encoding instead (hdr_compose.h): PQ BT.2020 for an HDR10 - * swapchain, tone-mapped sRGB for an ordinary one; the scene image comes back in - * TRANSFER_DST_OPTIMAL like the blits leave it. If that pass is unavailable, the blits below. */ int drawn = 0; int hdr_how = 0; if (hf && pass && n > 0 && @@ -1747,21 +1548,13 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, NULL, 0, NULL, 1, &b_present); } else { - /* 2. Effects (the hook): scaling to the scene's mapped size, then the effects, in the X11 - * order. The result stands for the whole scene (rw x rh) and comes back in - * TRANSFER_SRC_OPTIMAL; with nothing on the scene stays as composited. */ VkImageLayout result_layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; if (fx) { int mapped_w = (int)(scene_w * g_map.kx + 0.5f), mapped_h = (int)(scene_h * g_map.ky + 0.5f); - /* This path's scene is 8-bit. Set only where the chain really runs: a plain frame (the black - * base under an HDR picture) must not flip the chain's format back every frame. */ vkp_effects_set_formats(scene_fmt, scene_fmt); result = vkp_effects_run(cmd, scene_img, scene_w, scene_h, mapped_w, mapped_h, &rw, &rh); result_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; } - /* Hand-off: from here the frame lives in GENERAL - the frame-generation engines copy and - * sample it there, and every screen blit reads it there. Whatever wrote it last (the - * composite blits or the chain's last pass) is made visible to both. */ VkImageMemoryBarrier b_general = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = result_layout, .newLayout = VK_IMAGE_LAYOUT_GENERAL, @@ -1773,23 +1566,16 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, &b_general); - /* 3. Frame generation (the hook): the result is frame N; the engine interpolates between - * N-1 and N and hands back the frames that belong in between. Shown first, N last. */ if (fg) { ngen = vkp_framegen_run(cmd, result, VK_NULL_HANDLE, rw, rh, scene_fmt, gens); if (ngen < 0) ngen = 0; if (ngen > VKP_FG_MAX_GENERATIONS) ngen = VKP_FG_MAX_GENERATIONS; } - /* 4. Mapping/blit: the first present carries generated frame 0, or the result itself. */ record_screen_blit(cmd, ngen ? gens[0] : result, rw, rh, scene_w, scene_h, img, blit_filter, cb); } g_vk.EndCommandBuffer(cmd); - /* One submit + present per slot: generated frames first, the real frame last. The frame - * fence rides on the last submit (a fence signal is ordered after everything submitted - * earlier on the queue, so it still covers all of this frame's work). With FIFO the - * presentation engine shows them on consecutive vblanks: that IS the pacing, no sleeps. */ const int presents = 1 + ngen; int fence_submitted = 0, presented_gen = 0; VkResult pr = VK_SUCCESS; @@ -1818,7 +1604,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i if (qr != VK_SUCCESS) { if (qr == VK_ERROR_DEVICE_LOST) device_lost("submit"); else LOGE("present: submit %d/%d failed (%d)", k + 1, presents, (int)qr); - /* The acquired image is never presented; the swapchain would be stuck with it. */ if (g_dev_state != -2) destroy_swapchain(); return -1; } @@ -1833,8 +1618,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i if (pr == VK_ERROR_OUT_OF_DATE_KHR || pr == VK_ERROR_SURFACE_LOST_KHR || pr == VK_ERROR_DEVICE_LOST) break; } if (!fence_submitted) { - /* Ended early: an empty submit carrying the fence is ordered after everything queued - * above, so the wait below still means "this frame's work is done". */ VkSubmitInfo si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO}; g_vk.QueueSubmit(g_queue, 1, &si, g_fence); } @@ -1842,14 +1625,10 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i if (fg) vkp_framegen_presented(presented_gen); if (fr == VK_ERROR_DEVICE_LOST || pr == VK_ERROR_DEVICE_LOST) { device_lost("present"); return -1; } if (pr == VK_ERROR_OUT_OF_DATE_KHR || pr == VK_ERROR_SURFACE_LOST_KHR) { - /* The surface changed or went away under this frame: rebuild before the next one. */ droiddeck_log("gpu", "screen surface %s on present: rebuilding the swapchain", vk_result_name(pr)); destroy_swapchain(); return -1; } else if (pr == VK_SUBOPTIMAL_KHR || ar == VK_SUBOPTIMAL_KHR) { - /* Expected and harmless here: the swapchain uses IDENTITY preTransform on purpose (see - * swap_init), so a rotated panel reports SUBOPTIMAL on every frame. Rebuilding would change - * nothing (and a rebuild per frame would be far worse), so present as is; say so once. */ static int said; if (!said) { said = 1; droiddeck_log("gpu", "surface reports SUBOPTIMAL (panel rotation); presenting as is"); } } else if (pr != VK_SUCCESS) { @@ -1858,13 +1637,8 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i return -1; } - /* What the base surface shows from now on: black only after a plain frame with nothing drawn - * (vkp_base_black keeps presenting nothing while that holds); anything else it presented is - * content, and the next layer frame must put a black frame back under the layers first. */ g_base_black = (g_plain_frame && n == 0 && !hf) ? 1 : 0; - /* Signal the app once, on the first real client frame reaching the screen, so the - * launch/preloader overlay can dismiss (wayland has no XServer window-content hook). */ if (drawn && !g_first_frame_done) { g_first_frame_done = 1; droiddeck_on_first_frame(); @@ -1882,25 +1656,19 @@ int vkp_render_hdr(int scene_w, int scene_h, const struct vkp_draw *draws, int n return render_impl(scene_w, scene_h, draws, n, hf, how); } -/* ---------------------------------------------------------------- layer mode helpers */ -/* Degrees the display rotates our layers by: 0, 90, 180 or 270; -1 before the swapchain exists. - * Read from the surface, so it follows the panel's install orientation and the session's own - * orientation together, on any device. */ int vkp_surface_rotation_degrees(void) { if (!g_surface_transform_known) return -1; switch (g_surface_transform) { case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: return 90; case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR: return 180; case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: return 270; - default: return 0; /* IDENTITY, and the mirrored forms we never see here */ + default: return 0; } } ANativeWindow *vkp_window(void) { return g_window; } -/* A new session under a compositor that has already presented: arm the one-shot again so the - * next drawn frame reports, or the app would show its loading panel over the new client for good. */ void vkp_reset_first_frame(void) { g_first_frame_done = 0; } @@ -1913,14 +1681,14 @@ void vkp_signal_first_frame(void) { int vkp_update_map(int scene_w, int scene_h) { if (g_dev_state == -2 || dev_init() != 0 || !g_window || scene_w <= 0 || scene_h <= 0) return -1; - check_surface_changed(); /* layer mode may present nothing on the base surface for a long time */ + check_surface_changed(); if (!g_swapchain && swap_init() != 0) return -1; update_map(scene_w, scene_h); return g_map.valid ? 0 : -1; } int vkp_map_rect(int img_w, int img_h, int scene_w, int scene_h, int out[8]) { - struct vkp_image tmp = {.w = img_w, .h = img_h}; /* only its size is looked at */ + struct vkp_image tmp = {.w = img_w, .h = img_h}; struct vkp_draw d = {&tmp, 0, 0, (float)img_w, (float)img_h, 0, 0, scene_w, scene_h}; return vkp_map_draw(&d, out); } @@ -1935,15 +1703,12 @@ int vkp_map_draw(const struct vkp_draw *d, int out[8]) { return 1; } -/* Copy src (a client frame) into dst (a layer pool buffer) 1:1 and wait for it. Both images are - * owned by the "foreign" queue family (the game's driver / the display) between our uses, so each - * use acquires them and the destination is released back for the display to read. */ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) { if (!src || !dst || !dst->blit_dst || g_dev_state == -2 || dev_init() != 0) { if (wait_fd >= 0) close(wait_fd); return -1; } - const int gpu_wait = take_wait_fd(wait_fd); /* the display's release of dst, before the copy writes it */ + const int gpu_wait = take_wait_fd(wait_fd); if (gpu_wait < 0) return -1; VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; @@ -1962,7 +1727,7 @@ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) { .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, .dstQueueFamilyIndex = g_qfam, .image = dst->image, .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT}}; - if (!src->dmabuf) { /* shm image: host-written, GENERAL */ + if (!src->dmabuf) { acq[0].oldLayout = src->in_general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_PREINITIALIZED; acq[0].newLayout = VK_IMAGE_LAYOUT_GENERAL; acq[0].srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; @@ -2004,12 +1769,12 @@ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) { return 0; } -/* A client's GPU cursor read back to the CPU, for the app's pointer overlay (compositor.c, - * wl_pointer.set_cursor). wlroots hands a nested compositor its cursor as a dma-buf from the parent's - * formats - on Adreno usually UBWC, which the CPU cannot read - so it is blitted into a small linear, - * host-visible image first. Rare (a cursor changes shape, it is not redrawn per frame) and waited - * for on the CPU. out: w*h pixels, B8G8R8A8 in memory = 0xAARRGGBB ints. 0 = ok. */ -static struct vkp_image *g_rb; /* the readback image, kept for the next shape */ + + + + + +static struct vkp_image *g_rb; int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) { if (!src || !out || src->w <= 0 || src->h <= 0 || src->w * src->h > max_px || @@ -2076,7 +1841,6 @@ int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) { .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = g_rb->image, .subresourceRange = range, .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .dstAccessMask = VK_ACCESS_HOST_READ_BIT}, - /* the client's buffer goes back to it */ {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = src->dmabuf ? g_qfam : VK_QUEUE_FAMILY_IGNORED, @@ -2102,32 +1866,10 @@ int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) { return 0; } -/* ---------------------------------------------------------------- the compositor pass, off-screen - * - * Layer mode with screen effects on: the game's frame still has to go through the compositor pass - * (scene image -> effects chain), but the RESULT does not have to end up in the app's swapchain - - * it is copied into a gralloc buffer and put on the game's own Android layer, so hardware - * composition stays alive while a Look is applied and the scene -> output mapping is done by the - * display (setGeometry) instead of a second full-screen GPU blit. - * - * The chain runs in its OWN submit, with no swapchain image acquired. That is deliberate and was - * measured: folding it into the submit that owns the acquired image (one command buffer, one - * present, which looks cheaper on paper) puts the whole 13-pass chain behind the acquire - * semaphore, i.e. behind a vblank - 72 fps instead of 111 fps on the Pocket FIT with Retro CRT at - * 1920x1080. The base surface's black frame is presented by the caller AFTER the layer - * transaction, so the layer never waits for a vblank either. - * - * Three steps because the chain's result size is only known once the chain has run, and the layer - * buffer is allocated from it: - * vkp_pass_begin() -> records composite + effects into the frame's command buffer - * vkp_pass_copy_to()-> appends the copy into the layer buffer, submits, waits - * vkp_pass_abort() -> drops it unsubmitted (no free layer buffer this frame) - * Frame generation is NOT run here: its extra frames need a present each, on consecutive vblanks, - * which one buffer per layer transaction cannot pace (see WAYLAND_RUNTIME.md). */ static struct { int active; - VkImage result; /* in TRANSFER_SRC_OPTIMAL */ + VkImage result; int rw, rh; } g_pass; @@ -2144,15 +1886,12 @@ int vkp_pass_begin_hdr(int scene_w, int scene_h, const struct vkp_draw *draws, i return pass_begin_impl(scene_w, scene_h, draws, n, hf, rw, rh); } -/* The off-screen pass for a layer: the SDR one (hf == NULL: composite into the 8-bit scene image, the - * effects in 8-bit - unchanged) or the HDR one (the draws composed into one 10-bit PQ BT.2020 picture, - * the effects run on it in 10-bit). */ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int *rw, int *rh) { if (g_pass.active) vkp_pass_abort(); if (g_dev_state == -2 || dev_init() != 0 || !g_window || scene_w <= 0 || scene_h <= 0) return -1; - check_surface_changed(); /* as in render_impl: the effects pass maps onto the output's size */ - if (!g_swapchain && swap_init() != 0) return -1; /* the mapping is in output pixels */ + check_surface_changed(); + if (!g_swapchain && swap_init() != 0) return -1; update_map(scene_w, scene_h); if (!g_map.valid) return -1; if (hf) { @@ -2171,8 +1910,6 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw blendp_begin_frame(); VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - /* Same acquisition as vkp_render's compositor pass: the sources come from the client's queue - * family (dmabuf) or from a host write (shm), the scene image is the transfer destination. */ VkImageMemoryBarrier *bars = calloc((size_t)n + 1, sizeof(*bars)); if (!bars) { g_vk.EndCommandBuffer(cmd); return -1; } int nb = 0; @@ -2209,11 +1946,9 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw free(bars); if (hf) { - /* HDR: every draw into the mixed image, then ONE encoding into the 10-bit picture: PQ BT.2020, or - * tone-mapped sRGB while the drawer's HDR output switch is off. */ if (compose_hdr(cmd, draws, n, hf, scene_w, scene_h, g_hdrscene.img, HDR_FMT, hf->tonemap ? HDRC_OUT_SDR : HDRC_OUT_PQ) != 0) { - g_vk.EndCommandBuffer(cmd); /* never submitted; reset before its next use */ + g_vk.EndCommandBuffer(cmd); return -1; } if (vkp_effects_active()) vkp_effects_set_formats(HDR_FMT, HDR_FMT); @@ -2253,8 +1988,8 @@ int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd) { g_vk.EndCommandBuffer(cmd); return -1; } - const int gpu_wait = take_wait_fd(wait_fd); /* the display's release of dst, before the copy writes it */ - if (gpu_wait < 0) { g_vk.EndCommandBuffer(cmd); return -1; } /* never submitted; reset before its next use */ + const int gpu_wait = take_wait_fd(wait_fd); + if (gpu_wait < 0) { g_vk.EndCommandBuffer(cmd); return -1; } VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; VkImageMemoryBarrier acq = { @@ -2264,9 +1999,6 @@ int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd) { .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT}; g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 1, &acq); - /* The result stands for the whole scene, and so does the layer buffer (the DPU maps it onto - * the output), so this is 1:1 whenever the buffer was allocated at the size begin() reported; - * a stale-sized buffer is resampled rather than shown wrong. */ VkImageBlit blit = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .srcOffsets = {{0, 0, 0}, {g_pass.rw, g_pass.rh, 1}}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, @@ -2303,5 +2035,5 @@ int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd) { void vkp_pass_abort(void) { if (!g_pass.active) return; g_pass.active = 0; - g_vk.EndCommandBuffer(g_cmds[0]); /* never submitted; the buffer is reset before its next use */ + g_vk.EndCommandBuffer(g_cmds[0]); } diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.h b/app/src/main/cpp/waylandcomp/src/vk_present.h index 2a752c343..5b050d479 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.h +++ b/app/src/main/cpp/waylandcomp/src/vk_present.h @@ -2,70 +2,34 @@ #define VK_PRESENT_H #include <stdint.h> #include <android/native_window.h> -/* - * Android-surface render backend for the embedded Wayland compositor. - * Owns a Turnip VkDevice + a swapchain on the SurfaceView's ANativeWindow. Each - * client buffer becomes an image (a dmabuf from winewayland's Vulkan WSI is - * imported zero-copy; a wl_shm buffer is copied into a host-visible image), and - * every frame blits the whole scene - desktop, windows, subsurfaces - in order. - */ - -// Set the Turnip driver to load (adrenotools). Call before the first frame. -// NULL args -> the backend falls back to the system libvulkan (dmabuf import will -// likely fail - Adreno lacks drm_format_modifier). driver_path ends with '/'. void vk_present_set_driver(const char *driver_path, const char *library_name, const char *native_lib_dir); -// Set/replace the output window (from Surface via ANativeWindow_fromSurface; the backend -// owns the reference from then on). NULL = the surface is gone. Callable from any thread -// and never blocks: the request is applied by the compositor thread (vkp_render / -// vkp_apply_window_request), which tears the old swapchain down and releases the old window. void vk_present_set_window(ANativeWindow *window); -/* Compositor thread: apply a pending window change now. Returns 1 if the window changed. */ int vkp_apply_window_request(void); -/* How the scene is mapped onto the output (the app's Container.FULLSCREEN_* / ALIGN_* values, - * mirrored 1:1 from ViewTransformation.java so touch input, which is mapped by the app with - * that class, lands on the same pixels). Callable from any thread, applies on the next frame. */ enum vkp_scale_mode { VKP_MODE_OFF = 0, VKP_MODE_FIT = 1, VKP_MODE_STRETCH = 2, VKP_MODE_FILL = 3, VKP_MODE_INTEGER = 4 }; enum vkp_align { VKP_ALIGN_CENTER = 0, VKP_ALIGN_TOP = 1, VKP_ALIGN_BOTTOM = 2 }; void vk_present_set_scale_mode(int mode, int alignment); -/* Output pixel (0..output size) -> scene pixel through the current mapping (compositor thread). - * Returns 0 before the first frame has established a mapping (sx/sy untouched). */ int vkp_output_to_scene(double ox, double oy, double *sx, double *sy); -/* Output size in pixels (0x0 before the first swapchain). */ void vkp_output_size(int *w, int *h); struct vkp_image; -/* DRM format modifiers this backend knows the memory layout of (both single-plane on Adreno): - * LINEAR, and QCOM_COMPRESSED = UBWC (drm_fourcc.h: fourcc_mod_code(QCOM = 0x05, 1)). */ #define VKP_MOD_LINEAR 0x0000000000000000ULL #define VKP_MOD_QCOM_COMPRESSED 0x0500000000000001ULL #define VKP_MOD_INVALID 0x00ffffffffffffffULL -/* "linear", "qcom_compressed", or "modifier 0x…" for anything else (static buffer). */ const char *vkp_modifier_name(uint64_t modifier); -/* The modifiers a dma-buf of this DRM fourcc can be imported with as a blit source, asked of the - * renderer's own driver (VkDrmFormatModifierPropertiesListEXT, each confirmed for a dma-buf-backed - * TRANSFER_SRC image with vkGetPhysicalDeviceImageFormatProperties2). Only LINEAR and - * QCOM_COMPRESSED are ever returned (the ones this file can describe a plane layout for); other - * modifiers the driver reports are logged once. Returns the count, 0 when the device is not up - * (LINEAR is then the only safe assumption). Compositor thread. */ int vkp_dmabuf_modifiers(uint32_t drm_format, uint64_t *out, int max); -// Import a dmabuf (single plane). NULL on failure. The image aliases the buffer, so -// later client frames rendered into the same buffer show up without re-importing. struct vkp_image *vkp_image_from_dmabuf(int fd, uint32_t drm_format, uint64_t modifier, int w, int h, uint32_t stride, uint32_t offset); -// Same, choosing the role: as_blit_dst = 0 imports a client frame (blit source), 1 imports a -// buffer this backend blits INTO (layer mode's AHardwareBuffer pool, see sc_layer.h). struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t modifier, int w, int h, uint32_t stride, uint32_t offset, int as_blit_dst); int vkp_image_is_dmabuf(const struct vkp_image *img); -// Create a host-visible image and copy BGRA/XRGB8888 pixels into it. NULL on failure. struct vkp_image *vkp_image_create_shm(int w, int h); void vkp_image_upload_shm(struct vkp_image *img, const void *data, int stride); @@ -73,117 +37,61 @@ int vkp_image_width(const struct vkp_image *img); int vkp_image_height(const struct vkp_image *img); void vkp_image_destroy(struct vkp_image *img); -// One scene draw: the src rectangle of an image (image pixels) scaled into the dst -// rectangle (scene pixels). The scene is mapped onto the output by the scale mode. struct vkp_draw { struct vkp_image *img; float sx, sy, sw, sh; int dx, dy, dw, dh; - int blend; /* translucent: composed over what is under it (blend_pass.h) */ + int blend; }; -// The GPU the renderer runs on ("Adreno (TM) 750"), empty before the device is up. const char *vkp_gpu_name(void); -// 0 if the renderer can create images (device up), -1 otherwise. int vkp_ready(void); -/* Whether an output window is attached (or requested); without one vkp_render() draws nothing. */ int vkp_has_window(void); -/* 1 once the Vulkan device was lost: nothing is presented any more (the session must restart). */ int vkp_device_lost(void); -// Clear to black, blit the draws in order (first = bottom) and present. -// Returns 0 on success, -1 if nothing could be presented (no window yet, etc.). int vkp_render(int scene_w, int scene_h, const struct vkp_draw *draws, int n); -/* A black frame with no compositor pass (no effects, no frame generation), whatever is armed: the - * base surface under an HDR game that keeps its display layer (compositor.c). */ int vkp_render_plain(int scene_w, int scene_h); -/* Layer mode's base surface: make sure it shows a plain black frame under the display layers. A frame - * is presented only when the base is not already black on the current swapchain (entering layer mode, - * after a copy-path / frame-generation frame, after a swapchain rebuild); otherwise nothing is drawn - * or presented at all. 0 = the base is black, -1 = no output. Compositor thread. */ int vkp_base_black(int scene_w, int scene_h); -/* ---- perf counters (the 10 s `perf` line, compositor.c) ---- - * Everything the compositor thread spends in the driver, since the last vkp_perf_take(). Times in ns. */ struct vkp_perf { unsigned acquires, presents, waits; - int64_t acquire_ns, acquire_max_ns; /* vkAcquireNextImageKHR */ - int64_t present_ns, present_max_ns; /* vkQueuePresentKHR */ - int64_t wait_ns, wait_max_ns; /* vkWaitForFences on the compositor's own work */ - unsigned base_presents; /* black frames presented under the display layers */ - unsigned base_kept; /* layer frames that kept the black frame already there */ - unsigned gpu_release_waits; /* layer-buffer release fences waited for on the GPU */ + int64_t acquire_ns, acquire_max_ns; + int64_t present_ns, present_max_ns; + int64_t wait_ns, wait_max_ns; + unsigned base_presents; + unsigned base_kept; + unsigned gpu_release_waits; }; void vkp_perf_take(struct vkp_perf *out); -/* 1 when a release fence (sync_file) can be handed to the GPU as a wait (VK_KHR_external_semaphore_fd): - * the layer pool then reuses a buffer the display is still reading without blocking on the CPU. */ int vkp_can_wait_sync_fd(void); -/* ---- HDR composition (hdr_compose.h) ---- - * An HDR game that cannot be alone on its display layer: the scene is composed into ONE encoding. */ struct droiddeck_color; struct vkp_hdr_frame { - const unsigned char *is_hdr; /* per draw: 1 = its image holds PQ BT.2020 (the surface's description) */ - float peak_nits; /* the HDR content's peak, for a tone-map (max CLL / mastering max; 0 = 1000) */ - int tonemap; /* 1 = the drawer's HDR output switch is off: tone-map to SDR, never PQ */ - const struct droiddeck_color *color; /* the topmost HDR draw's description: an HDR10 swapchain's metadata */ + const unsigned char *is_hdr; + float peak_nits; + int tonemap; + const struct droiddeck_color *color; }; -/* vkp_render for a scene with HDR draws (frame generation): composed into PQ and presented through an - * HDR10 swapchain where the surface offers one (and hf->tonemap is 0), else tone-mapped into the ordinary - * one. *how = 1 HDR10, 2 tone-mapped, 0 the HDR pass was unavailable (shown the old way). Same return as - * vkp_render. */ int vkp_render_hdr(int scene_w, int scene_h, const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int *how); -/* vkp_pass_begin for a scene with HDR draws: composed into a 10-bit picture - PQ BT.2020, or sRGB - * tone-mapped when hf->tonemap - the effects run on it in 10-bit, the result (rw x rh, A2B10G10R10) is - * left for vkp_pass_copy_to() into a layer buffer (a blit: 10-bit or 8-bit). -1 = not possible (nothing - * recorded). */ int vkp_pass_begin_hdr(int scene_w, int scene_h, const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int *rw, int *rh); -/* ---- layer mode helpers (sc_layer.c; compositor thread) ---- */ -/* Whole-image copy of src into dst (a blit-destination dmabuf image), waited for on the CPU. - * wait_fd: a sync_file the copy must wait for before writing dst (the display's release fence of that - * buffer), -1 = none. Always consumed (handed to the GPU, or waited for and closed). */ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd); -/* A client's GPU cursor (an imported dma-buf, often UBWC) read back as w*h 0xAARRGGBB pixels through a - * linear host-visible copy; waited for on the CPU. Compositor thread, between frames. 0 = ok. */ int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px); -/* Refresh the scene -> output mapping for this scene size without presenting (creates the - * swapchain if needed, since the mapping is in output pixels). 0 = mapping valid. */ int vkp_update_map(int scene_w, int scene_h); -/* Map a draw through the current mapping: out = {src x0,y0,x1,y1 (image px), dst x0,y0,x1,y1 - * (output px)}, clipped like the blit path. 0 = nothing of it is visible. */ int vkp_map_draw(const struct vkp_draw *d, int out[8]); -/* The same for a bare w x h buffer shown over the whole scene (no vkp_image: zero-copy layers). */ int vkp_map_rect(int img_w, int img_h, int scene_w, int scene_h, int out[8]); -/* The output window frames go to (NULL = none); compositor thread. */ -/* Degrees the display rotates every layer we present by (0/90/180/270), -1 before the swapchain - * exists. From VkSurfaceCapabilitiesKHR::currentTransform, i.e. what the presentation engine says - * it does - never a device allowlist. The layer path uses it to decide whether a SECOND display - * layer is affordable on this display (sc_layer_present_overlay). */ int vkp_surface_rotation_degrees(void); ANativeWindow *vkp_window(void); -/* Fire the one-shot first-frame notification (layer mode presents outside vkp_render). */ void vkp_signal_first_frame(void); -/* ---- the compositor pass into a layer buffer (sc_layer.c; compositor thread) ---- - * Screen effects on the game's own Android layer: compose `draws` into the scene image and run the - * effects chain WITHOUT presenting, then copy the result into a gralloc layer buffer. Three steps - * because the chain's result size (a scaling mode resizes to the scene's mapped output size) is - * only known once the chain has run, and the layer buffer is allocated from it. - * begin: 0 = a pass is in progress, its result is *rw x *rh; -1 = not possible (draw the old way). - * Exactly one of copy_to (blit into dst, submit, wait; 0 = done) / abort (drop it unsubmitted) - * must follow a successful begin. The chain deliberately runs with NO swapchain image acquired - - * see the comment in vk_present.c. Frame generation is not run here (see WAYLAND_RUNTIME.md). */ int vkp_pass_begin(int scene_w, int scene_h, const struct vkp_draw *draws, int n, int *rw, int *rh); -/* wait_fd: as for vkp_blit_image (consumed in every case). */ int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd); void vkp_pass_abort(void); -// Session log (compositor.c): one line to Download/Wayland-logs and logcat. void droiddeck_log(const char *tag, const char *fmt, ...) __attribute__((format(printf, 2, 3))); #endif diff --git a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c index 4a9dd4d23..b1125ce38 100644 --- a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c +++ b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c @@ -1,9 +1,3 @@ -/* - * JNI entry for the embedded Wayland compositor (experimental parallel runtime). - * Starts the compositor on a dedicated thread (it blocks in the wl event loop). - * The render-to-Surface backend + input are added in the M4 phase; this brings up - * the server so a Wayland client (eventually winewayland.drv) can connect. - */ #include <jni.h> #include <errno.h> #include <stdbool.h> @@ -44,15 +38,15 @@ extern volatile int g_output_w, g_output_h; #define TAG "DroidDeckWayland" static JavaVM *g_jvm; -static volatile pid_t g_comp_tid; /* the compositor thread, for the app's ADPF hint session */ -static jclass g_compositor_cls; /* global ref */ -static jmethodID g_on_first_frame; /* static void onFirstFramePresented() */ -static jmethodID g_on_game_surface; /* static void onGameSurface(String, String) */ -static jmethodID g_on_game_frame; /* static void onGameFrame() */ -static jmethodID g_on_game_program; /* static void onGameProgram(int, String) */ -static jmethodID g_on_pointer_lock; /* static void onPointerLock(boolean, int, int) */ -static jmethodID g_on_clipboard; /* static void onClipboardText(byte[]) */ -static jmethodID g_on_text_input; /* static void onTextInput(boolean, String, int, int, int, int) */ +static volatile pid_t g_comp_tid; +static jclass g_compositor_cls; +static jmethodID g_on_first_frame; +static jmethodID g_on_game_surface; +static jmethodID g_on_game_frame; +static jmethodID g_on_game_program; +static jmethodID g_on_pointer_lock; +static jmethodID g_on_clipboard; +static jmethodID g_on_text_input; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { (void)reserved; @@ -79,9 +73,6 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { return JNI_VERSION_1_6; } -/* Called from vk_present.c on the compositor thread when the first client frame is - * presented. Attaches to the JVM (this thread is a bare pthread) and calls back into - * Java so the launch overlay can dismiss. Fires exactly once. */ void droiddeck_on_first_frame(void) { if (!g_jvm || !g_compositor_cls || !g_on_first_frame) return; JNIEnv *env = NULL; @@ -96,8 +87,6 @@ void droiddeck_on_first_frame(void) { __android_log_print(ANDROID_LOG_INFO, TAG, "first client frame presented -> notified app"); } -/* The compositor thread stays attached once it first calls into Java: the HUD gets an upcall - * for every game frame. */ static __thread JNIEnv *t_env; static __thread int t_attached; @@ -112,7 +101,6 @@ static JNIEnv *thread_env(void) { return t_env = env; } -/* A window started presenting GPU frames (window = its description), or NULL when it closed. */ void droiddeck_on_game_surface(const char *window, const char *gpu) { JNIEnv *env; if (!g_compositor_cls || !g_on_game_surface || !(env = thread_env())) return; @@ -124,8 +112,6 @@ void droiddeck_on_game_surface(const char *window, const char *gpu) { if (jg) (*env)->DeleteLocalRef(env, jg); } -/* The program behind the game window that just started presenting: its Linux pid and executable name - * ("" = unknown). The app arms its launch-time CPU affinity on it. Compositor thread. */ void droiddeck_on_game_program(int pid, const char *program) { JNIEnv *env; if (!g_compositor_cls || !g_on_game_program || !(env = thread_env())) return; @@ -135,7 +121,6 @@ void droiddeck_on_game_program(int pid, const char *program) { if (jp) (*env)->DeleteLocalRef(env, jp); } -/* One GPU frame from that window. */ void droiddeck_on_game_frame(void) { JNIEnv *env; if (!g_compositor_cls || !g_on_game_frame || !(env = thread_env())) return; @@ -143,9 +128,6 @@ void droiddeck_on_game_frame(void) { if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); } -/* A program locked the pointer (locked = 1; the app's input path switches to deltas) or the lock - * ended (locked = 0; x,y = where the pointer is now, in scene coordinates, for the app to re-sync - * its own pointer to). Compositor thread. */ void droiddeck_on_pointer_lock(int locked, int x, int y) { JNIEnv *env; if (!g_compositor_cls || !g_on_pointer_lock || !(env = thread_env())) return; @@ -153,8 +135,6 @@ void droiddeck_on_pointer_lock(int locked, int x, int y) { if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); } -/* A program copied text (UTF-8, not NUL-terminated for the app's sake - a byte[] so emoji and - * NULs survive JNI). Compositor thread. */ void droiddeck_on_clipboard_text(const char *utf8, int len) { JNIEnv *env; if (!g_compositor_cls || !g_on_clipboard || !(env = thread_env())) return; @@ -166,8 +146,6 @@ void droiddeck_on_clipboard_text(const char *utf8, int len) { (*env)->DeleteLocalRef(env, arr); } -/* A program started (enabled) or stopped accepting IME text; x,y,w,h = its caret rectangle in - * scene pixels (all 0 = unknown). Compositor thread. */ void droiddeck_on_text_input(int enabled, const char *program, int x, int y, int w, int h) { JNIEnv *env; if (!g_compositor_cls || !g_on_text_input || !(env = thread_env())) return; @@ -178,16 +156,11 @@ void droiddeck_on_text_input(int enabled, const char *program, int x, int y, int if (jp) (*env)->DeleteLocalRef(env, jp); } -/* android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY: the level the hwui RenderThread runs at. */ #define COMPOSITOR_NICE (-8) static void *comp_thread(void *arg) { (void)arg; __android_log_print(ANDROID_LOG_INFO, TAG, "compositor thread starting"); - /* Every buffer release, frame callback and layer transaction of the session goes through this one - * thread. It is named, so `ps -T`, the logs and the drawer's Thread Priority Boost (PerfPriority - * matches thread names) can find it, and it runs at display priority instead of whatever the - * thread that started it had. Never lowered: a thread that already runs hotter keeps its value. */ pthread_setname_np(pthread_self(), "wl-compositor"); const id_t tid = (id_t)gettid(); g_comp_tid = (pid_t)tid; @@ -225,15 +198,12 @@ static void start_thread(void) { __android_log_print(ANDROID_LOG_ERROR, TAG, "pthread_create failed"); } -/* The compositor thread's tid, or 0 before it starts. The app opens a PerformanceHintManager session - * on it and reports each presented frame's interval, so the governor sees a frame that ran long. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCompositorTid(JNIEnv *env, jclass clazz) { (void)env; (void)clazz; return (jint)g_comp_tid; } -/* Headless start (no output window) - used for bring-up tests. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStart(JNIEnv *env, jclass clazz, jstring xdgRuntimeDir) { @@ -249,8 +219,6 @@ static char *dup_jstr(JNIEnv *env, jstring s) { return out; } -/* Start with a real output Surface + the container's Turnip driver (adrenotools). - * Frames committed by clients are composited to this Surface via Turnip. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStartWithSurface( JNIEnv *env, jclass clazz, jobject surface, jstring xdgRuntimeDir, @@ -263,14 +231,12 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStartWithSurface( free(dp); free(ln); free(nl); if (surface) { ANativeWindow *win = ANativeWindow_fromSurface(env, surface); - vk_present_set_window(win); /* backend acquires; released on nativeSetSurface(null) */ + vk_present_set_window(win); __android_log_print(ANDROID_LOG_INFO, TAG, "output window bound (%p)", (void *)win); } start_thread(); } -/* Inject a pointer event from the Android SurfaceView touch listener (UI thread). - * action: 0=down 1=move 2=up; x/y in output space (0..1919, 0..1079). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendPointer( JNIEnv *env, jclass clazz, jint action, jint x, jint y) { @@ -283,9 +249,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendTouch( droiddeck_wayland_send_touch(action, pointer_id, x, y); } -/* The client's pointer image and whether it wants one at all (wl_pointer.set_cursor). - * out = [serial, hidden, w, h, hotspotX, hotspotY, ARGB pixels...]; returns ints written, 0 if the - * array is too small. The app polls the serial and only rebuilds its overlay when it changes. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSnapshot( JNIEnv *env, jclass clazz, jintArray out) { @@ -298,98 +261,75 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSnapshot( return n; } -/* The serial nativeCursorSnapshot would return, without copying anything. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSerial(JNIEnv *env, jclass clazz) { (void)env; (void)clazz; return droiddeck_cursor_serial(); } -/* Inject a key event. evdev = Linux input keycode (KEY_A=30…); state 1=down 0=up. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendKey( JNIEnv *env, jclass clazz, jint evdev, jint state) { droiddeck_wayland_send_key(evdev, state); } -/* App X-server input in scene (virtual desktop) coordinates; see droiddeck_wayland_send_scene_input. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendSceneInput( JNIEnv *env, jclass clazz, jint type, jint a, jint b) { droiddeck_wayland_send_scene_input(type, a, b); } -/* One screen refresh (Choreographer frame callback, UI thread): the compositor draws once. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeVsync(JNIEnv *env, jclass clazz, jlong frameTimeNanos) { droiddeck_wayland_vsync((int64_t)frameTimeNanos); } -/* Shortcut launches: don't draw explorer's windows (desktop, taskbar), like X11's unviewable classes. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHideShell(JNIEnv *env, jclass clazz, jboolean hide) { g_hide_shell = hide ? 1 : 0; } -/* Zero-copy layer mode (DROIDDECK_WAYLAND_ZERO_COPY=1 at launch, the drawer's switch live): one fullscreen - * window on its own Android layer instead of the swapchain blit (sc_layer.c / ahb_swapchain.c). Any - * thread, any time: before the compositor starts it is the launch default; afterwards the compositor - * thread flips the state, tells the bound games to rebuild their swapchains for it and redraws. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetZeroCopy(JNIEnv *env, jclass clazz, jboolean on) { int live = droiddeck_get_display() != NULL; - if (!live) g_zero_copy = on ? 1 : 0; /* read by ahb_swapchain_init before the queue drains */ + if (!live) g_zero_copy = on ? 1 : 0; droiddeck_host_zero_copy(on ? 1 : 0, live); __android_log_print(ANDROID_LOG_INFO, TAG, "zero-copy layer mode %s%s", on ? "on" : "off", live ? " (live)" : ""); } -/* Milliseconds since the compositor last put a game frame on the layer without a copy; -1 = never - * this session. The drawer's status line ("switching..." until the frames arrive / stop). */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeZeroCopyLastFrameAgeMs(JNIEnv *env, jclass clazz) { return (jint)ahb_swapchain_last_frame_age_ms(); } -/* Zero-copy frames in the last completed 10 s stats window (on_stats_timer), for the drawer's live line. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeZeroCopyFrames(JNIEnv *env, jclass clazz) { return (jint)g_zero_copy_last; } -/* Compressed (UBWC) game buffers: advertise DRM_FORMAT_MOD_QCOM_COMPRESSED on zwp_linux_dmabuf_v1 when - * the renderer's driver imports it (default on; DROIDDECK_WAYLAND_UBWC=0 = off). Set before the compositor - * starts. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetUbwc(JNIEnv *env, jclass clazz, jboolean on) { g_ubwc = on ? 1 : 0; __android_log_print(ANDROID_LOG_INFO, TAG, "compressed (UBWC) game buffers %s", on ? "on" : "off"); } -/* Debug: name no DRM device in the dma-buf feedback (DROIDDECK_WAYLAND_NO_RENDER_NODE=1), the way a - * phone that exposes no /dev/dri node to apps does. Set before the compositor starts. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetNoRenderNode(JNIEnv *env, jclass clazz, jboolean on) { g_no_render_node = on ? 1 : 0; if (on) __android_log_print(ANDROID_LOG_INFO, TAG, "debug: advertising no DRM device (main device 0:0)"); } -/* VRR / refresh-rate matching: the rate the app is voting for the panel, mirrored onto the game's - * own SurfaceControl layer. Under zero-copy the game's frames bypass the app's surface entirely, so - * without this SurfaceFlinger never sees the game's cadence. 0 = clear the vote. Any thread, any - * time; the compositor applies it with its next layer transaction and logs the change. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLayerFrameRate(JNIEnv *env, jclass clazz, jfloat hz) { (void)env; (void)clazz; sc_layer_set_frame_rate((float)hz); } -/* The panel's refresh rate (Hz) for the advertised wl_output mode. Set before the compositor starts. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetOutputRefreshRate(JNIEnv *env, jclass clazz, jfloat hz) { g_output_refresh_mhz = hz > 1.0f ? (int)(hz * 1000.0f + 0.5f) : 0; } -/* The container's screen size for the advertised wl_output mode. Set before the compositor starts. */ extern void vkp_reset_first_frame(void); JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeResetFirstFrame(JNIEnv *env, jclass clazz) { @@ -403,8 +343,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetOutputSize(JNIEnv g_output_h = h > 0 ? h : 0; } -/* The output's size changed while the compositor runs (a foldable opening or closing with the - * session following the screen): store it and have the compositor thread tell the clients. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeResizeOutput(JNIEnv *env, jclass clazz, jint w, jint h) { (void)env; (void)clazz; @@ -414,57 +352,44 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeResizeOutput(JNIEnv droiddeck_wayland_output_resized(); } -/* Fullscreen mode + screen alignment (Container.FULLSCREEN_* / ALIGN_* values): how the scene is - * fitted onto the output. Any thread, any time; the next frame uses it. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetScaleMode(JNIEnv *env, jclass clazz, jint mode, jint alignment) { vk_present_set_scale_mode(mode, alignment); __android_log_print(ANDROID_LOG_INFO, TAG, "scale mode %d alignment %d", mode, alignment); } -/* ---- screen effects (effects_chain.c): the X11 Vulkan renderer's post chain in the compositor. - * Any thread, any time; the compositor thread applies them on its next frame and logs one - * `effects` line per change. Values are 1:1 with VulkanRenderer's setters. */ -/* Scaling mode: 0=None 1=Linear 2=Nearest 3=SGSR 4=FSR 5=FSR Fit 6=Sharpen 7=NIS 8=SGSR HQ. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetUpscaler(JNIEnv *env, jclass clazz, jint mode) { vkp_effects_set_scaling(mode); } -/* The upscaler's sharpness slider 0..100 (RCAS lobe / SGSR edge / NIS sharpness). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetUpscaleSharpness(JNIEnv *env, jclass clazz, jint sharpness) { vkp_effects_set_upscale_sharpness(sharpness); } -/* AMD CAS sharpen toggle + level 0..100 (0 = pass off). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCas(JNIEnv *env, jclass clazz, jboolean enabled, jint sharpness) { vkp_effects_set_cas(enabled ? 1 : 0, sharpness); } -/* Fake-HDR toggle. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdr(JNIEnv *env, jclass clazz, jboolean enabled) { vkp_effects_set_hdr(enabled ? 1 : 0); } -/* Terminal debanding toggle + strength 0..200 (100 = 1 LSB). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetDeband(JNIEnv *env, jclass clazz, jboolean enabled, jint strength) { vkp_effects_set_deband(enabled ? 1 : 0, strength); } -/* Colour grade (slider units: brightness/contrast -100..100, gamma 0.5..3, saturation 0..200 %) and the - * FXAA / Toon / CRT / NTSC toggles - VulkanRenderer.setScreenEffects. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetScreenEffects(JNIEnv *env, jclass clazz, jfloat brightness, jfloat contrast, jfloat gamma, jfloat saturation, jboolean fxaa, jboolean toon, jboolean crt, jboolean ntsc) { vkp_effects_set_screen(brightness, contrast, gamma, saturation, fxaa ? 1 : 0, toon ? 1 : 0, crt ? 1 : 0, ntsc ? 1 : 0); } -/* Cardboard mode - WaylandCompositor.nativeSetCardboard. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboard(JNIEnv *env, jclass clazz, jboolean enabled, jint source, jfloat ipd, jfloat dist, jfloat width, jfloat k1, jfloat k2, jfloat fov, jfloat inset, jfloat chroma) { @@ -474,7 +399,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboard(JNIEnv vkp_cardboard_set(&p); } -/* The head pose (row-major 3x3, head -> world), from the sensor thread. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardPose(JNIEnv *env, jclass clazz, jfloatArray rot, jboolean live) { @@ -484,7 +408,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardPose(JNI vkp_cardboard_set_pose(m, live ? 1 : 0); } -/* Where the head pose is shared with the guest - WaylandCompositor.nativeSetCardboardPoseFile. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardPoseFile(JNIEnv *env, jclass clazz, jstring path) { if (!path) return; @@ -494,9 +417,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardPoseFile (*env)->ReleaseStringUTFChars(env, path, p); } -/* One "display" line in the session log, written from Java: facts that live in the Android - * framework (the panel's HDR capability) rather than in the compositor. droiddeck_log() mirrors to - * logcat and appends to the session file when one is open, so this is safe at any point. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogDisplay(JNIEnv *env, jclass clazz, jstring message) { char *s = dup_jstr(env, message); @@ -504,8 +424,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogDisplay(JNIEnv *e free(s); } -/* The same under the "perf" area: facts the app knows about the session's performance setup (the CPU - * cores the game is pinned to, say). Same safety as nativeLogDisplay. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogPerf(JNIEnv *env, jclass clazz, jstring message) { char *s = dup_jstr(env, message); @@ -513,9 +431,7 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogPerf(JNIEnv *env, free(s); } -/* ---- HDR10 output, round 1 (droiddeck_color.h / wl_color_mgmt.c) ---- */ -/* The opt-in: mode 0 off, 1 DROIDDECK_WAYLAND_HDR=1, 2 =force (testing). Before the compositor starts. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrRequest(JNIEnv *env, jclass clazz, jint mode, jstring source, jboolean dxvkHdr, jboolean zeroCopyForced) { @@ -524,7 +440,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrRequest(JNIEnv free(s); } -/* The game's display as android.view.Display reports it (before the start; again on every change). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrDisplay(JNIEnv *env, jclass clazz, jint id, jstring name, jstring formats, jboolean hdr10, jfloat maxLum, jfloat maxAvg, jfloat minLum, jboolean ratioAvailable, @@ -535,38 +450,32 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrDisplay(JNIEnv free(n); free(f); } -/* One Display.getHdrSdrRatio() reading (listener = from the display's ratio listener). Any thread. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrSdrRatioSample(JNIEnv *env, jclass clazz, jfloat ratio, jboolean listener) { droiddeck_color_ratio_sample((float)ratio, listener ? 1 : 0); } -/* ms since an HDR frame last reached a display layer, -1 = never this session. Any thread. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrLastFrameAgeMs(JNIEnv *env, jclass clazz) { return (jint)droiddeck_color_last_frame_age_ms(); } -/* -1 = not decided yet, 0 = closed, 1 = open. Any thread. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrGateState(JNIEnv *env, jclass clazz) { return (jint)droiddeck_color_gate_state(); } -/* The session is ending: the "HDR on screen: ..." summary line. Any thread, once. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrSessionEnd(JNIEnv *env, jclass clazz) { droiddeck_color_session_end(); } -/* HDR frames really on screen right now (the HUD badge). Any thread. */ JNIEXPORT jboolean JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrOnScreen(JNIEnv *env, jclass clazz) { return droiddeck_color_hdr_on_screen() ? JNI_TRUE : JNI_FALSE; } -/* One "color" line in the session log from Java. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogColor(JNIEnv *env, jclass clazz, jstring message) { char *s = dup_jstr(env, message); @@ -574,8 +483,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogColor(JNIEnv *env free(s); } -/* One line in the session log under an area Java names ("gpu", "nvapi"): launch facts the app decides. - * The area is the log's 9-character column; an empty one reads "app". */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLog(JNIEnv *env, jclass clazz, jstring area, jstring message) { char *a = dup_jstr(env, area), *s = dup_jstr(env, message); @@ -584,18 +491,15 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLog(JNIEnv *env, jcl free(s); } -/* SDR content's level inside an HDR picture, in nits (default 203). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrSdrWhite(JNIEnv *env, jclass clazz, jfloat nits) { droiddeck_color_set_sdr_white((float)nits); } -/* The drawer's live HDR output switch: on = HDR frames as HDR, off = the same frames tone-mapped to SDR. - * Applied on the compositor thread (logged there, with a redraw). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrOutput(JNIEnv *env, jclass clazz, jboolean on) { if (droiddeck_get_display()) droiddeck_host_hdr_output(on ? 1 : 0); - else droiddeck_color_set_output(on ? 1 : 0); /* no compositor thread yet: nothing is drawing either */ + else droiddeck_color_set_output(on ? 1 : 0); } JNIEXPORT jboolean JNICALL @@ -603,7 +507,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrOutput(JNIEnv *en return droiddeck_color_output() ? JNI_TRUE : JNI_FALSE; } -/* Device evidence for the HDR lines: thermal status + headroom, brightness + mode. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrEnvSample(JNIEnv *env, jclass clazz, jint thermal, jfloat headroom, jint brightness, jint mode) { @@ -611,13 +514,11 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrEnvSample(JNIEnv (int)brightness, (int)mode); } -/* Display.getHighestHdrSdrRatio() (Android 16+), <= 0 = not reported. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrHighestRatio(JNIEnv *env, jclass clazz, jfloat ratio) { droiddeck_color_set_highest_ratio((float)ratio == (float)ratio ? (float)ratio : -1.0f); } -/* The HDR headroom the screen surface should ask for (HDR10 swapchain frames in the last 1.5 s), 0 = none. */ JNIEXPORT jfloat JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrScreenHeadroom(JNIEnv *env, jclass clazz) { return (jfloat)droiddeck_color_screen_headroom(NULL, 0); @@ -630,25 +531,21 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrScreenHeadroomWhy return (*env)->NewStringUTF(env, why); } -/* The app's screen-surface request, for the no-headroom lines: > 0 asked, 0 cleared, -1 not possible. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrNoteHeadroomRequest(JNIEnv *env, jclass clazz, jfloat ratio) { droiddeck_color_note_headroom_request((float)ratio); } -/* 0 none, 1 HDR frames on screen with headroom, 2 HDR frames on screen without headroom for 5 s+. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrState(JNIEnv *env, jclass clazz) { return (jint)droiddeck_color_hdr_state(); } -/* An HDR game's frames were shown tone-mapped to SDR in the last 1.5 s (the drawer's status line). */ JNIEXPORT jboolean JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrToneMappedOnScreen(JNIEnv *env, jclass clazz) { return droiddeck_color_tonemapped_on_screen() ? JNI_TRUE : JNI_FALSE; } -/* The Look the controls currently match (null = Custom) - only named in the session log. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLookName(JNIEnv *env, jclass clazz, jstring name) { char *s = dup_jstr(env, name); @@ -656,14 +553,12 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLookName(JNIEnv * free(s); } -/* The in-game FPS limiter: frames per second, 0 = unlimited. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetFpsLimit(JNIEnv *env, jclass clazz, jint fps) { g_fps_limit = fps > 0 ? fps : 0; __android_log_print(ANDROID_LOG_INFO, TAG, "fps limit %d", fps); } -/* Swap/clear the output window (e.g. SurfaceView recreated/destroyed). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetSurface( JNIEnv *env, jclass clazz, jobject surface) { @@ -674,8 +569,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetSurface( } } -/* ---- clipboard + text input (any thread; queued to the compositor thread) ---- - * Text crosses as UTF-8 byte arrays: JNI's modified UTF-8 would mangle emoji. */ static char *dup_bytes(JNIEnv *env, jbyteArray arr, int *len) { *len = 0; @@ -689,7 +582,6 @@ static char *dup_bytes(JNIEnv *env, jbyteArray arr, int *len) { return buf; } -/* Android's clipboard text becomes the guest's selection (empty/null = clear). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetClipboardText(JNIEnv *env, jclass clazz, jbyteArray utf8) { int len; @@ -698,7 +590,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetClipboardText(JNI free(buf); } -/* Soft-keyboard text committed to the program accepting text input. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputCommit(JNIEnv *env, jclass clazz, jbyteArray utf8) { int len; @@ -707,7 +598,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputCommit(JNIE free(buf); } -/* Composing (pre-edit) text; cursorBegin/cursorEnd are character indexes into it, -1 = end. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputPreedit(JNIEnv *env, jclass clazz, jbyteArray utf8, jint cursorBegin, jint cursorEnd) { @@ -717,15 +607,11 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputPreedit(JNI free(buf); } -/* The IME deleted characters around the caret. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputDelete(JNIEnv *env, jclass clazz, jint before, jint after) { droiddeck_host_text_delete(before, after); } -/* KGSL's power control, off (the GPU held at its top clock) or back on. The property is the - * device's, not this process's: it outlives the fd and the app, so the caller clears it at stop - * and again at every app start. No root; the kernel's thermal limits still apply. */ extern void adrenotools_set_turbo(bool turbo); JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTurbo(JNIEnv *env, jclass clazz, jboolean on) { @@ -733,10 +619,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTurbo(JNIEnv * adrenotools_set_turbo(on == JNI_TRUE); } -/* Raises one of our own processes or threads to `nice` with a bare setpriority(), never lowering - * it. android.os.Process.setThreadPriority is not used for this: it also moves the thread between - * scheduling groups by priority, and a guest process must stay where Android put the app. Returns - * the nice value it is left at, or 100 when it could not be read. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeRaisePriority(JNIEnv *env, jclass clazz, jint tid, jint nice) { (void)env; (void)clazz; diff --git a/app/src/main/cpp/waylandcomp/src/wl_clipboard.c b/app/src/main/cpp/waylandcomp/src/wl_clipboard.c index 7668090f7..6547db7db 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_clipboard.c +++ b/app/src/main/cpp/waylandcomp/src/wl_clipboard.c @@ -1,24 +1,3 @@ -/* - * Clipboard: wl_data_device_manager (core, v3) and zwlr_data_control_manager_v1 (v1), selection - * only - drag and drop is refused. Both protocols share one selection: - * - * owner CLIENT a program's wl_data_source / zwlr_data_control_source_v1 (its mime list) - * owner HOST text the app pushed from Android's clipboard (offered under the text mimes) - * owner NONE nothing - * - * Every selection change is announced to every data-control device (they see the selection - * regardless of focus, as the protocol says) and to the wl_data_devices of the client holding - * keyboard focus; a client also gets the current selection when its keyboard focus arrives. - * - * winewayland prefers zwlr_data_control (it then runs the clipboard from the desktop process - * without needing keyboard focus) and tags its own offers with application/x.winewayland.tag - * so it can ignore them when they come back; we pass mime lists through untouched. - * - * Guest → Android: when a program sets a selection with a text mime, the compositor asks for it - * once over a pipe on the event loop (non-blocking, 1 MiB cap, 3 s timeout) and hands the text - * to the app (droiddeck_on_clipboard_text → ClipboardManager). Android → guest: the app's text - * becomes the HOST selection; receive requests are answered with a non-blocking writer. - */ #define _GNU_SOURCE 1 #include <stdlib.h> #include <string.h> @@ -37,17 +16,17 @@ static struct wl_display *g_disp; enum owner { OWNER_NONE, OWNER_CLIENT, OWNER_HOST }; static enum owner g_owner; -static uint32_t g_gen; /* bumps on every selection change */ +static uint32_t g_gen; -struct source { /* a client's data source (either protocol) */ +struct source { struct wl_resource *res; int is_control; char *mimes[MAX_MIMES]; int nmimes; }; -static struct source *g_src; /* current CLIENT selection */ +static struct source *g_src; -static char *g_host_text; /* current HOST selection */ +static char *g_host_text; static size_t g_host_len; static const char *const k_text_mimes[] = { "text/plain;charset=utf-8", "text/plain", "UTF8_STRING", "TEXT", "STRING", NULL @@ -59,18 +38,17 @@ struct device { struct wl_list link; }; static struct wl_list g_devices; -static struct wl_client *g_kb_client; /* keyboard focus holder */ +static struct wl_client *g_kb_client; struct offer { struct wl_resource *res; int is_control; - uint32_t gen; /* the selection this offer describes */ + uint32_t gen; }; -static char *g_last_guest_text; /* echo guard: the last text handed to Android */ +static char *g_last_guest_text; static size_t g_last_guest_len; -/* ------------------------------------------------------------------ helpers */ static int is_text_mime(const char *m) { for (int i = 0; k_text_mimes[i]; i++) if (!strcmp(m, k_text_mimes[i])) return 1; @@ -94,7 +72,6 @@ static void source_send_cancelled(struct source *s) { else wl_data_source_send_cancelled(s->res); } -/* ------------------------------------------------------------------ host → client writer */ struct writer { int fd; @@ -115,9 +92,9 @@ static int writer_pump(struct writer *w) { while (w->off < w->len) { ssize_t n = write(w->fd, w->data + w->off, w->len - w->off); if (n > 0) { w->off += (size_t)n; continue; } - if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0; /* wait */ + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0; if (n < 0 && errno == EINTR) continue; - break; /* gone */ + break; } return 1; } @@ -150,13 +127,12 @@ static void writer_start(int fd, const char *data, size_t len) { if (w->timer) wl_event_source_timer_update(w->timer, CLIP_WRITE_TIMEOUT_MS); } -/* ------------------------------------------------------------------ client → host reader */ struct reader { int fd; char *buf; size_t len; - const char *mime; /* points into the source's list */ + const char *mime; struct wl_event_source *src, *timer; }; static struct reader *g_reader; @@ -202,7 +178,7 @@ static int on_reader_readable(int fd, uint32_t mask, void *data) { r->buf = nb; ssize_t n = read(fd, r->buf + r->len, chunk); if (n > 0) { r->len += (size_t)n; continue; } - if (n == 0) { reader_finish(r); return 0; } /* EOF */ + if (n == 0) { reader_finish(r); return 0; } if (errno == EAGAIN || errno == EWOULDBLOCK) { if (mask & WL_EVENT_HANGUP) { reader_finish(r); return 0; } return 0; @@ -220,14 +196,13 @@ static int on_reader_timeout(void *data) { return 0; } -/* Ask the current client source for its text, once. */ static void read_client_text(void) { const char *mime; int p[2]; if (g_reader) reader_free(g_reader); if (!g_src || !(mime = source_text_mime(g_src))) return; if (pipe2(p, O_CLOEXEC) != 0) return; - fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL) | O_NONBLOCK); /* our end only; the client writes blocking */ + fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL) | O_NONBLOCK); struct reader *r = calloc(1, sizeof(*r)); if (!r) { close(p[0]); close(p[1]); return; } r->fd = p[0]; @@ -242,13 +217,12 @@ static void read_client_text(void) { wl_display_flush_clients(g_disp); } -/* ------------------------------------------------------------------ offers */ static void offer_res_destroy(struct wl_resource *r) { free(wl_resource_get_user_data(r)); } static void offer_receive(struct wl_client *c, struct wl_resource *r, const char *mime, int32_t fd) { struct offer *o = wl_resource_get_user_data(r); - if (!o || o->gen != g_gen) { close(fd); return; } /* stale offer */ + if (!o || o->gen != g_gen) { close(fd); return; } if (g_owner == OWNER_HOST) { if (is_text_mime(mime)) { droiddeck_log("clipboard", "Android clipboard → guest (%zu bytes) for %s", g_host_len, droiddeck_client_name(c)); @@ -257,7 +231,7 @@ static void offer_receive(struct wl_client *c, struct wl_resource *r, const char close(fd); } } else if (g_owner == OWNER_CLIENT && g_src) { - source_send_send(g_src, mime, fd); /* libwayland dups the fd */ + source_send_send(g_src, mime, fd); close(fd); wl_display_flush_clients(g_disp); } else { @@ -277,7 +251,6 @@ static const struct zwlr_data_control_offer_v1_interface control_offer_impl = { .receive = offer_receive, .destroy = offer_destroy, }; -/* Tell one device what the selection is now (a fresh offer, or nothing). */ static void device_send_selection(struct device *d) { struct wl_client *c = wl_resource_get_client(d->res); if (g_owner == OWNER_NONE) { @@ -315,8 +288,6 @@ static void device_send_selection(struct device *d) { else wl_data_device_send_selection(d->res, o->res); } -/* The selection changed: every data-control device hears it; wl_data_devices only if their - * client holds keyboard focus. */ static void broadcast_selection(void) { struct device *d; g_gen++; @@ -336,7 +307,6 @@ void droiddeck_clipboard_keyboard_focus(struct wl_client *client) { if (!d->is_control && wl_resource_get_client(d->res) == client) device_send_selection(d); } -/* ------------------------------------------------------------------ sources */ static void source_res_destroy(struct wl_resource *r) { struct source *s = wl_resource_get_user_data(r); @@ -380,7 +350,6 @@ static void create_source(struct wl_client *c, uint32_t id, int is_control, uint s, source_res_destroy); } -/* A device set (or cleared) the selection. */ static void set_selection(struct wl_client *c, struct wl_resource *source_res) { struct source *s = source_res ? wl_resource_get_user_data(source_res) : NULL; if (s && s == g_src) return; @@ -404,7 +373,6 @@ static void set_selection(struct wl_client *c, struct wl_resource *source_res) { read_client_text(); } -/* ------------------------------------------------------------------ devices */ static void device_res_destroy(struct wl_resource *r) { struct device *d = wl_resource_get_user_data(r); @@ -429,7 +397,6 @@ static void control_device_set_selection(struct wl_client *c, struct wl_resource set_selection(c, source); } static void control_device_set_primary(struct wl_client *c, struct wl_resource *r, struct wl_resource *source) { - /* No primary selection on Android; a source given here just gets cancelled. */ if (source) { struct source *s = wl_resource_get_user_data(source); if (s && s != g_src) source_send_cancelled(s); @@ -455,11 +422,9 @@ static void create_device(struct wl_client *c, uint32_t id, int is_control, uint : (const void *)&data_device_impl, d, device_res_destroy); wl_list_insert(&g_devices, &d->link); - /* Data-control devices see the selection at once; wl_data_devices when focus arrives. */ if (is_control || (g_kb_client && c == g_kb_client)) device_send_selection(d); } -/* ------------------------------------------------------------------ managers */ static void dm_create_source(struct wl_client *c, struct wl_resource *r, uint32_t id) { create_source(c, id, 0, wl_resource_get_version(r)); @@ -493,12 +458,10 @@ static void bind_data_control_manager(struct wl_client *c, void *data, uint32_t wl_resource_set_implementation(r, &cm_impl, NULL, NULL); } -/* ------------------------------------------------------------------ host side */ -/* Android's clipboard text (compositor thread, via the host queue). Takes the buffer. */ void clipboard_host_text(char *utf8, size_t len) { if (len && g_last_guest_text && len == g_last_guest_len && !memcmp(utf8, g_last_guest_text, len)) { - free(utf8); /* our own copy coming back: not a change */ + free(utf8); return; } if (len && g_owner == OWNER_HOST && g_host_len == len && !memcmp(utf8, g_host_text, len)) { diff --git a/app/src/main/cpp/waylandcomp/src/wl_color_mgmt.c b/app/src/main/cpp/waylandcomp/src/wl_color_mgmt.c index f18aab9f4..1ba254c4c 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_color_mgmt.c +++ b/app/src/main/cpp/waylandcomp/src/wl_color_mgmt.c @@ -1,16 +1,3 @@ -/* - * wp_color_manager_v1 (wayland-protocols staging color-management-v1, version 1) - the subset Mesa's - * Wayland WSI binds - plus the HDR gate, the per-surface image descriptions and the session log's - * HDR evidence. See droiddeck_color.h for the design and HDR_RECON.md for why it is shaped this way. - * - * Strictness policy. A protocol error disconnects the client, and the client here is a GAME, so - * errors are only raised where the protocol leaves no choice and a conforming client can never hit - * them: a request gated on a feature we did not advertise, a property set twice, an incomplete - * parameter set, a description that is not ready, an intent we did not advertise. Everything a - * conforming client CAN produce on a bad day is accepted and logged instead: out-of-range HDR - * metadata is dropped (Android gets none rather than nonsense), a second colour-management object - * for the same surface replaces the first, a request on an inert object is ignored. - */ #define _GNU_SOURCE #include "droiddeck_color.h" #include "droiddeck_ext.h" @@ -29,7 +16,6 @@ extern volatile int g_zero_copy; #define TAG "color" -/* AHARDWAREBUFFER_FORMAT_* (android/hardware_buffer.h), for the log only. */ const char *droiddeck_ahb_format_name(uint32_t f) { static char other[32]; switch (f) { @@ -80,24 +66,24 @@ static const char *dataspace_name(int32_t ds) { } } -/* ---------------------------------------------------------------- request (app) + gate */ -static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; /* g_req, g_hdr (app threads + compositor) */ + +static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; static struct { - int mode; /* 0 off, 1 on, 2 forced */ + int mode; char source[64]; int dxvk_hdr, zero_copy_forced; int display_known; int display_id, hdr10, ratio_available, api; char display_name[96], formats[96]; float max_lum, max_avg, min_lum, ratio; - float highest_ratio; /* Display.getHighestHdrSdrRatio() (Android 16+), < 0 = not reported */ + float highest_ratio; } g_req = {.highest_ratio = -1.0f}; -static _Atomic int g_gate = -1; /* -1 undecided, 0 closed, 1 open */ -static char g_gate_why[320]; /* the closed gate's reason (for the summary) */ -static _Atomic int g_output_on = 1; /* the drawer's HDR output switch (per session, starts on) */ +static _Atomic int g_gate = -1; +static char g_gate_why[320]; +static _Atomic int g_output_on = 1; void droiddeck_color_set_request(int mode, const char *source, int dxvk_hdr, int zero_copy_forced) { pthread_mutex_lock(&g_mu); @@ -106,7 +92,7 @@ void droiddeck_color_set_request(int mode, const char *source, int dxvk_hdr, int g_req.dxvk_hdr = dxvk_hdr ? 1 : 0; g_req.zero_copy_forced = zero_copy_forced ? 1 : 0; pthread_mutex_unlock(&g_mu); - atomic_store(&g_output_on, 1); /* the drawer's switch is per session and starts on */ + atomic_store(&g_output_on, 1); } void droiddeck_color_set_display(int id, const char *name, const char *formats, int hdr10, float max_lum, @@ -123,8 +109,6 @@ void droiddeck_color_set_display(int id, const char *name, const char *formats, g_req.ratio = ratio; g_req.api = api; pthread_mutex_unlock(&g_mu); - /* The gate is decided once, when the compositor starts: a display that changes under a running - * session is recorded, and what it means is said out loud. */ if (was_known && atomic_load(&g_gate) >= 0 && (was_hdr10 != (hdr10 ? 1 : 0) || was_id != id)) { if (atomic_load(&g_gate) == 1 && !hdr10) droiddeck_log(TAG, "the game is now on \"%s\" (display %d), which reports no HDR10: HDR frames stay tagged " @@ -139,48 +123,39 @@ void droiddeck_color_set_display(int id, const char *name, const char *formats, int droiddeck_color_gate_state(void) { return atomic_load(&g_gate); } int droiddeck_color_hdr_open(void) { return atomic_load(&g_gate) == 1; } -/* ---------------------------------------------------------------- HDR evidence (summary) */ static struct { - unsigned descs; /* HDR image descriptions made ready */ - unsigned applied; /* commits that made one current on a surface */ - char applied_who[160]; /* the last surface that got one */ - uint64_t layer_frames, layer_10bit, layer_copy8, copy_frames; /* layer_frames = every frame shown AS HDR */ + unsigned descs; + unsigned applied; + char applied_who[160]; + uint64_t layer_frames, layer_10bit, layer_copy8, copy_frames; uint64_t composed, swapchain, tonemapped; - uint64_t tm_off; /* of `tonemapped`: while the drawer's HDR output switch was off */ - unsigned output_offs; /* times the switch was turned off */ + uint64_t tm_off; + unsigned output_offs; unsigned win_layer, win_copy, win_copy8, win_zc, win_composed, win_swapchain, win_tonemapped; char copy_reason[160]; char layer_fmt[48]; int ratio_n, win_ratio_n, ratio_live_n; float ratio_min, ratio_max, ratio_last, win_ratio_min, win_ratio_max, ratio_logged; - float ratio_live_max; /* highest reading taken WHILE HDR frames were on screen (the verdict's) */ + float ratio_live_max; int64_t ratio_logged_ns, ratio_periodic_ns; - /* Live headroom. The display may grant HDR frames no headroom at all while they are on screen - seen - * on the Fold while the screen was being RECORDED (Android turns HDR headroom off for a recording, - * which is SDR), and plausibly with the brightness slider at maximum (SDR white already at the panel's - * limit) - so "the ratio rose once" is not enough for a reader at minute 3: time with HDR frames on - * screen, the part of it with the ratio above 1, and the current no-headroom streak. */ int64_t prev_sample_ns; int prev_live; float prev_ratio; int64_t live_ns, headroom_ns; - int64_t nohead_since_ns; /* HDR frames on screen and ratio <= 1.01 since then (0 = not now) */ - int nohead_said; /* the streak's "no headroom" line is written */ + int64_t nohead_since_ns; + int nohead_said; char verdict[512]; } g_hdr; static _Atomic int64_t g_last_frame_ns; -static _Atomic int64_t g_last_tm_ns; /* the last HDR frame shown tone-mapped to SDR */ +static _Atomic int64_t g_last_tm_ns; -/* What the device says beside the headroom (the app, non-root APIs; droiddeck_color_env_*). The Fold showed - * three ways to lose HDR headroom with HDR frames on screen - a screen recording, heat under load, and - * (plausibly) the brightness slider at maximum - so every no-headroom line carries this. Under g_mu. */ static struct { int known; - int thermal; /* PowerManager THERMAL_STATUS_* (0 none .. 6 shutdown), -1 unknown */ - float headroom; /* PowerManager.getThermalHeadroom(10), < 0 = not available */ - int brightness; /* Settings.System.SCREEN_BRIGHTNESS (0..255), -1 unknown */ - int bmode; /* SCREEN_BRIGHTNESS_MODE: 1 automatic, 0 manual, -1 unknown */ - float requested; /* the last HDR headroom asked for (layer or screen surface): > 0 the ratio, - * 0 none asked, -1 the API is missing on this Android (< 15) */ + int thermal; + float headroom; + int brightness; + int bmode; + float requested; + } g_env = {0, -1, -1.0f, -1, -1, 0.0f}; static const char *thermal_name(int t) { @@ -190,7 +165,6 @@ static const char *thermal_name(int t) { } } -/* "thermal MODERATE (headroom 0.83), brightness 180/255 manual". Caller holds g_mu. */ static void env_text_locked(char *out, size_t n) { char t[48], b[40]; if (g_env.thermal < 0) snprintf(t, sizeof(t), "thermal ?"); @@ -203,9 +177,6 @@ static void env_text_locked(char *out, size_t n) { snprintf(out, n, "%s, %s", t, b); } -/* What the evidence says about headroom lost with HDR frames on screen. A screenshot or a screen recording - * is not detected in this build (that needs extra permissions), so it is named as a possibility when - * nothing the app can measure explains the loss. Caller holds g_mu. */ static void nohead_causes_locked(char *out, size_t n, int brief) { const char *parts[2]; char hot[48]; @@ -214,7 +185,6 @@ static void nohead_causes_locked(char *out, size_t n, int brief) { snprintf(hot, sizeof(hot), "the device is hot (thermal %s)", thermal_name(g_env.thermal)); parts[np++] = hot; } - /* Maximum brightness only counts when it is MANUAL: the Fold kept 3.00 of headroom at 255 auto. */ if (g_env.brightness >= 250 && g_env.bmode == 0) parts[np++] = "brightness at maximum (manual)"; if (np == 1) snprintf(out, n, "likely %s", parts[0]); else if (np == 2) snprintf(out, n, "likely %s and %s", parts[0], parts[1]); @@ -235,11 +205,11 @@ static void nohead_causes_locked(char *out, size_t n, int brief) { snprintf(out, n, "a screenshot or screen recording, heat, or manual brightness at maximum"); } -/* ---- the explicit HDR headroom request (API 35; sc_layer.c for the game layer, the app for the screen - * surface): content peak / SDR white, capped at the display's highest ratio when it reports one. */ -static _Atomic int g_screen_hr_x1000; /* the screen surface's wanted headroom x1000 (HDR10 swapchain) */ -static _Atomic int64_t g_screen_hr_ns; /* the last HDR10-swapchain frame */ -static char g_screen_hr_why[200]; /* under g_mu */ + + +static _Atomic int g_screen_hr_x1000; +static _Atomic int64_t g_screen_hr_ns; +static char g_screen_hr_why[200]; static float peak_of(const struct droiddeck_color *c, float disp_max, const char **src) { if (c->max_cll > 0.0f) { *src = "max CLL"; return c->max_cll; } @@ -308,15 +278,12 @@ int droiddeck_color_last_frame_age_ms(void) { return age > 0x7fffffff ? 0x7fffffff : (int)age; } -/* The one-line answer a tester quotes. Caller holds g_mu. */ static void verdict_locked(char *out, size_t size) { int gate = atomic_load(&g_gate); if (gate < 0) { snprintf(out, size, "not decided yet (the compositor has not started)"); return; } if (gate == 0) { snprintf(out, size, "no, because %s", g_gate_why); return; } if (g_hdr.layer_frames) { const char *who = g_hdr.applied_who[0] ? g_hdr.applied_who : "the game"; - /* Only the paths that carried frames, so the ratio, the share and the device evidence still fit - * the log line. */ char paths[200] = ""; size_t pp = 0; const uint64_t zc_other = g_hdr.layer_frames - g_hdr.layer_10bit - g_hdr.layer_copy8 - g_hdr.composed - @@ -336,12 +303,7 @@ static void verdict_locked(char *out, size_t size) { (unsigned long long)g_hdr.layer_frames, who, paths); char env[128] = "", causes[256] = ""; if (g_env.known) env_text_locked(env, sizeof(env)); - /* Brief: the verdict has to fit one log line with everything else in it. */ if (g_hdr.nohead_said) nohead_causes_locked(causes, sizeof(causes), 1); - /* Only readings taken while HDR frames were on screen count: the ratio says what the display did - * with THEM, not with whatever else was up at another moment. And the share of that time with any - * headroom, plus where it stands now: a ratio that rose once and then fell to 1.00 for minutes is - * not the same answer. */ char share[384] = ""; const long long live_s = (long long)(g_hdr.live_ns / 1000000000LL); if (g_hdr.live_ns >= 1000000000LL) @@ -351,7 +313,6 @@ static void verdict_locked(char *out, size_t size) { (long long)(g_hdr.headroom_ns / 1000000000LL), live_s, g_hdr.ratio_last, g_hdr.nohead_said ? " - no headroom now: " : "", g_hdr.nohead_said ? causes : "", env[0] ? " [" : "", env, env[0] ? "]" : ""); - /* The display's own ceiling (Android 16+) and what was asked for, where known. */ char ceil[80] = "", req[48] = ""; if (g_req.highest_ratio > 0.0f) snprintf(ceil, sizeof(ceil), ", highest possible %.2f", g_req.highest_ratio); if (g_env.requested > 0.0f) snprintf(req, sizeof(req), ", %.1fx requested", g_env.requested); @@ -400,7 +361,6 @@ static void verdict_locked(char *out, size_t size) { "image description (is DXVK_HDR=1 set, and is HDR switched on in the game's own display options?)"); } -/* Log the verdict when it changed (force = log it even if not). */ static void log_verdict(int force) { char v[512]; pthread_mutex_lock(&g_mu); @@ -435,7 +395,7 @@ void droiddeck_color_frame_shown(const struct droiddeck_color *c, int path, uint snprintf(g_hdr.layer_fmt, sizeof(g_hdr.layer_fmt), "HDR10 swapchain"); atomic_store(&g_screen_hr_ns, now_ns()); break; - default: /* DROIDDECK_HDR_TONEMAPPED: shown, but not as HDR */ + default: g_hdr.tonemapped++; g_hdr.win_tonemapped++; if (!atomic_load(&g_output_on)) g_hdr.tm_off++; pthread_mutex_unlock(&g_mu); @@ -447,8 +407,6 @@ void droiddeck_color_frame_shown(const struct droiddeck_color *c, int path, uint pthread_mutex_unlock(&g_mu); atomic_store(&g_last_frame_ns, now_ns()); if (path == DROIDDECK_HDR_SWAPCHAIN) { - /* The screen surface's headroom request (the app applies it): worked out again when the - * description changes, else once a second. */ static uint32_t ident; static int64_t calc_ns; const int64_t t = now_ns(); @@ -494,7 +452,6 @@ void droiddeck_color_ratio_sample(float ratio, int listener) { pthread_mutex_lock(&g_mu); was = g_hdr.ratio_logged; if (ratio > 0.0f) { - /* Time with HDR frames on screen, credited to the reading that stood over it. */ if (g_hdr.prev_sample_ns && g_hdr.prev_live) { int64_t dt = t - g_hdr.prev_sample_ns; if (dt > 0 && dt < 3000000000LL) { @@ -520,8 +477,6 @@ void droiddeck_color_ratio_sample(float ratio, int listener) { if (!g_hdr.ratio_live_n || ratio > g_hdr.ratio_live_max) g_hdr.ratio_live_max = ratio; g_hdr.ratio_live_n++; } - /* A change is logged at once (at most 4 lines a second); while HDR frames are on screen the - * steady value is restated every 5 s, so a tester's log shows it holding, not just moving. */ float moved = ratio - g_hdr.ratio_logged; if (moved < 0.0f) moved = -moved; if ((g_hdr.ratio_n == 1 || moved >= 0.02f) && t - g_hdr.ratio_logged_ns > 250000000LL) @@ -562,7 +517,7 @@ void droiddeck_color_stats_tick(void) { char line[512]; int any; pthread_mutex_lock(&g_mu); - any = g_hdr.win_layer || g_hdr.win_copy || g_hdr.win_tonemapped; /* the ratio alone is logged when it moves */ + any = g_hdr.win_layer || g_hdr.win_copy || g_hdr.win_tonemapped; if (any) { char ratio[256] = "no HDR/SDR ratio reading", env[128] = "", causes[256] = ""; if (g_env.known) env_text_locked(env, sizeof(env)); @@ -591,7 +546,7 @@ int droiddeck_color_hdr_state(void) { int age = droiddeck_color_last_frame_age_ms(); if (age < 0 || age >= 1500) return 0; pthread_mutex_lock(&g_mu); - int confirmed = !g_hdr.ratio_n || g_hdr.ratio_last > 1.01f; /* no ratio on this display: the tag is all there is */ + int confirmed = !g_hdr.ratio_n || g_hdr.ratio_last > 1.01f; int nohead = g_hdr.nohead_said; pthread_mutex_unlock(&g_mu); return confirmed ? 1 : nohead ? 2 : 0; @@ -617,7 +572,7 @@ int droiddeck_color_requested(void) { int droiddeck_color_output(void) { return atomic_load(&g_output_on); } void droiddeck_color_env_sample(int thermal, float headroom, int brightness, int bmode) { - if (atomic_load(&g_gate) != 1) return; /* evidence for the HDR lines only */ + if (atomic_load(&g_gate) != 1) return; char msg[2][200]; int nmsg = 0; pthread_mutex_lock(&g_mu); @@ -656,7 +611,6 @@ int droiddeck_color_tonemapped_on_screen(void) { void droiddeck_color_set_output(int on) { on = on ? 1 : 0; if (atomic_load(&g_gate) != 1) { - /* The drawer only shows the switch in sessions whose gate is open; say so if it ever gets here. */ droiddeck_log(TAG, "HDR output switch (%s) ignored: HDR is not open in this session", on ? "on" : "off"); return; } @@ -674,7 +628,7 @@ void droiddeck_color_set_output(int on) { droiddeck_log(TAG, "HDR output switched OFF in the drawer: HDR frames are tone-mapped to SDR from the next frame " "(%s; %s). The game is not told - it keeps rendering HDR; its own HDR setting and DXVK_HDR are " "untouched", who, age >= 0 && age < 1500 ? "HDR frames were on screen" : "no HDR frames on screen just now"); - droiddeck_request_redraw(); /* a paused game commits nothing: show the change now */ + droiddeck_request_redraw(); } static _Atomic int g_session_ended; @@ -685,19 +639,16 @@ void droiddeck_color_session_end(void) { pthread_mutex_lock(&g_mu); int asked = g_req.mode != 0; pthread_mutex_unlock(&g_mu); - if (asked) log_verdict(1); /* nobody asked for HDR: no summary to write */ + if (asked) log_verdict(1); } -/* ---------------------------------------------------------------- image descriptions */ struct cm_desc { - int refs; /* protocol objects + surfaces (pending / current) */ + int refs; struct droiddeck_color c; - /* Set on the output / preferred descriptions the compositor serves (never on one a game built - * from parameters): get_information is answered rather than refused, with these as the target. */ int informative; - float ref_lum; /* nits: PQ reference white */ - float disp_min_lum, disp_max_lum; /* nits: the display's own volume, from the app */ + float ref_lum; + float disp_min_lum, disp_max_lum; }; static uint32_t g_next_identity = 1; @@ -705,11 +656,10 @@ static uint32_t g_next_identity = 1; static struct cm_desc *desc_ref(struct cm_desc *d) { if (d) d->refs++; return d; } static void desc_unref(struct cm_desc *d) { if (d && --d->refs <= 0) free(d); } -/* The parametric creator's collected properties. 0 / has_* = 0 means unset. */ struct cm_params { uint32_t tf, primaries; int has_mprim; int32_t mprim[8]; - int has_mlum; uint32_t mlum_min, mlum_max; /* min is cd/m² * 10000, max is cd/m² */ + int has_mlum; uint32_t mlum_min, mlum_max; int has_cll; uint32_t cll; int has_fall; uint32_t fall; }; @@ -718,7 +668,6 @@ static void image_description_destroy_req(struct wl_client *c, struct wl_resourc static void image_description_get_information(struct wl_client *c, struct wl_resource *r, uint32_t id) { struct cm_desc *d = wl_resource_get_user_data(r); if (!d || !d->informative) { - /* Built by the game from parameters: the protocol says it knows what it asked for. */ wl_resource_post_error(r, WP_IMAGE_DESCRIPTION_V1_ERROR_NO_INFORMATION, "get_information is not allowed on this image description"); return; @@ -726,7 +675,7 @@ static void image_description_get_information(struct wl_client *c, struct wl_res struct wl_resource *info = wl_resource_create(c, &wp_image_description_info_v1_interface, wl_resource_get_version(r), id); if (!info) { wl_client_post_no_memory(c); return; } - wl_resource_set_implementation(info, NULL, NULL, NULL); /* events only */ + wl_resource_set_implementation(info, NULL, NULL, NULL); const struct droiddeck_color *col = &d->c; int32_t xy[8] = { (int32_t)(col->red[0] * 1e6f + 0.5f), (int32_t)(col->red[1] * 1e6f + 0.5f), (int32_t)(col->green[0] * 1e6f + 0.5f), (int32_t)(col->green[1] * 1e6f + 0.5f), @@ -735,13 +684,11 @@ static void image_description_get_information(struct wl_client *c, struct wl_res wp_image_description_info_v1_send_primaries(info, xy[0], xy[1], xy[2], xy[3], xy[4], xy[5], xy[6], xy[7]); wp_image_description_info_v1_send_primaries_named(info, col->primaries); wp_image_description_info_v1_send_tf_named(info, col->tf); - /* min is cd/m² * 10000, max and reference are cd/m² */ wp_image_description_info_v1_send_luminances(info, (uint32_t)(col->min_lum * 10000.0f + 0.5f), (uint32_t)(col->max_lum + 0.5f), (uint32_t)(d->ref_lum + 0.5f)); wp_image_description_info_v1_send_target_primaries(info, xy[0], xy[1], xy[2], xy[3], xy[4], xy[5], xy[6], xy[7]); wp_image_description_info_v1_send_target_luminance(info, (uint32_t)(d->disp_min_lum * 10000.0f + 0.5f), (uint32_t)(d->disp_max_lum + 0.5f)); - /* done is a destructor event: the client's proxy is gone once it arrives, so the resource goes too. */ wp_image_description_info_v1_send_done(info); wl_resource_destroy(info); } @@ -753,15 +700,6 @@ static void image_description_resource_destroy(struct wl_resource *r) { desc_unref(wl_resource_get_user_data(r)); } -/* What the display layer shows while the gate is open - BT.2020 + PQ over the display's own - * volume - served as the output's image description and as every surface's preferred one. It is - * ready at once and open to get_information, and that is not optional: gamescope asks for the - * preferred description and for its information in the same breath, before any roundtrip - * (WaylandBackend.cpp, UpdateWPPreferredColorManagement). Answering 'failed' and then refusing - * get_information with a protocol error severs its connection mid-roundtrip - the Fold 8 on r5: - * "Broken pipe", signal 6 - while a device with the gate closed never offers colour management - * and never gets here. gamescope reads only the luminances from the answer (reference white vs - * the display's peak) to decide whether it exposes HDR to what it hosts. */ static void make_output_description(struct wl_client *c, struct wl_resource *parent, uint32_t id, const char *what) { struct cm_desc *d = calloc(1, sizeof(*d)); if (!d) { wl_client_post_no_memory(c); return; } @@ -774,9 +712,9 @@ static void make_output_description(struct wl_client *c, struct wl_resource *par col->has_st2086 = 1; col->red[0] = 0.708f; col->red[1] = 0.292f; col->green[0] = 0.170f; col->green[1] = 0.797f; col->blue[0] = 0.131f; col->blue[1] = 0.046f; col->white[0] = 0.3127f; col->white[1] = 0.3290f; - col->min_lum = 0.005f; col->max_lum = 10000.0f; /* the PQ volume */ + col->min_lum = 0.005f; col->max_lum = 10000.0f; d->informative = 1; - d->ref_lum = 203.0f; /* PQ reference white */ + d->ref_lum = 203.0f; pthread_mutex_lock(&g_mu); d->disp_min_lum = g_req.min_lum > 0.0f ? g_req.min_lum : 0.0f; d->disp_max_lum = g_req.max_lum > 0.0f ? g_req.max_lum : 1000.0f; @@ -811,8 +749,6 @@ static void params_create(struct wl_client *client, struct wl_resource *r, uint3 else if (p->primaries == WP_COLOR_MANAGER_V1_PRIMARIES_BT2020 && p->tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG) c->dataspace = DROIDDECK_ADATASPACE_BT2020_HLG; - /* HDR metadata, kept only when it makes sense. The PQ defaults stand in for what was not given - * (0.005 - 10000 cd/m², the target volume = the primary one); CTA-861.3 treats 0 as unknown. */ char dropped[160] = ""; float mlum_min = 0.005f, mlum_max = 10000.0f; if (p->has_mlum) { @@ -827,7 +763,7 @@ static void params_create(struct wl_client *client, struct wl_resource *r, uint3 c->green[0] = p->mprim[2] / 1e6f; c->green[1] = p->mprim[3] / 1e6f; c->blue[0] = p->mprim[4] / 1e6f; c->blue[1] = p->mprim[5] / 1e6f; c->white[0] = p->mprim[6] / 1e6f; c->white[1] = p->mprim[7] / 1e6f; - } else { /* the container's own primaries (BT.2020, D65) */ + } else { c->red[0] = 0.708f; c->red[1] = 0.292f; c->green[0] = 0.170f; c->green[1] = 0.797f; c->blue[0] = 0.131f; c->blue[1] = 0.046f; c->white[0] = 0.3127f; c->white[1] = 0.3290f; } @@ -862,8 +798,6 @@ static void params_create(struct wl_client *client, struct wl_resource *r, uint3 if (!out) { free(d); wl_client_post_no_memory(client); return; } d->refs = 1; wl_resource_set_implementation(out, &image_description_impl, d, image_description_resource_destroy); - /* Sent from inside the request: Mesa blocks its swapchain on this event (it dispatches its queue - * until ready/failed arrives), and the event loop flushes it before it sleeps again. */ wp_image_description_v1_send_ready(out, c->identity); if (c->dataspace) { pthread_mutex_lock(&g_mu); @@ -878,7 +812,7 @@ static void params_create(struct wl_client *client, struct wl_resource *r, uint3 droiddeck_log(TAG, "image description #%u: dropped HDR metadata that breaks the protocol's rules:%s " "(Android gets the rest)", c->identity, dropped); } - wl_resource_destroy(r); /* create is the creator's destructor */ + wl_resource_destroy(r); } #define PARAMS_ONCE(field, what) \ @@ -958,14 +892,11 @@ static const struct wp_image_description_creator_params_v1_interface params_impl }; static void params_resource_destroy(struct wl_resource *r) { free(wl_resource_get_user_data(r)); } -/* ---------------------------------------------------------------- per-surface state */ -/* One per wl_surface that ever had a colour-management object. Lives until the wl_surface goes (a - * swapchain rebuild destroys the object and makes a new one for the same surface). */ struct cm_surf { - struct wl_resource *surface; /* the wl_surface */ + struct wl_resource *surface; struct wl_listener surface_destroy; - struct wl_resource *owner; /* the live wp_color_management_surface_v1, NULL = none */ + struct wl_resource *owner; struct cm_desc *pending, *current; int pending_dirty; }; @@ -989,7 +920,7 @@ static void describe_surface(struct wl_resource *surface, char *out, size_t size static void cm_surface_destroyed(struct wl_listener *l, void *data) { struct cm_surf *cs = wl_container_of(l, cs, surface_destroy); wl_list_remove(&cs->surface_destroy.link); - if (cs->owner) wl_resource_set_user_data(cs->owner, NULL); /* inert from now on */ + if (cs->owner) wl_resource_set_user_data(cs->owner, NULL); desc_unref(cs->pending); desc_unref(cs->current); free(cs); @@ -1035,7 +966,6 @@ static const struct wp_color_management_surface_v1_interface cm_surface_impl = { .unset_image_description = cm_surface_unset_image_description, }; -/* Destroying the object does what unset_image_description does (double-buffered, like it). */ static void cm_surface_resource_destroy(struct wl_resource *r) { struct cm_surf *cs = wl_resource_get_user_data(r); if (!cs || cs->owner != r) return; @@ -1079,7 +1009,6 @@ const struct droiddeck_color *droiddeck_color_of(struct wl_resource *surface) { return cs && cs->current ? &cs->current->c : NULL; } -/* ---------------------------------------------------------------- output + feedback */ static void cm_output_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void cm_output_get_image_description(struct wl_client *c, struct wl_resource *r, uint32_t id) { @@ -1100,7 +1029,6 @@ static const struct wp_color_management_surface_feedback_v1_interface cm_feedbac .get_preferred_parametric = cm_feedback_get_preferred, }; -/* ---------------------------------------------------------------- wp_color_manager_v1 */ static void cm_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } @@ -1124,7 +1052,6 @@ static void cm_get_surface(struct wl_client *c, struct wl_resource *r, uint32_t char who[160]; describe_surface(surface, who, sizeof(who)); if (cs->owner) { - /* The protocol says surface_exists; a game would be disconnected for it. The newer object wins. */ droiddeck_log(TAG, "%s: a second colour-management object for the same surface - the newer one takes over " "(protocol error surface_exists not raised)", who); wl_resource_set_user_data(cs->owner, NULL); @@ -1174,16 +1101,12 @@ static void bind_cm(struct wl_client *c, void *data, uint32_t ver, uint32_t id) struct wl_resource *r = wl_resource_create(c, &wp_color_manager_v1_interface, ver > 1 ? 1 : (int)ver, id); if (!r) { wl_client_post_no_memory(c); return; } wl_resource_set_implementation(r, &cm_impl, NULL, NULL); - /* All at bind, then done: Mesa reads them in the registry roundtrip that follows the bind and - * lists VK_COLOR_SPACE_HDR10_ST2084_EXT from exactly this pair (bt2020 + st2084_pq). Only what - * the display layer can honour is offered. */ wp_color_manager_v1_send_supported_intent(r, WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); wp_color_manager_v1_send_supported_feature(r, WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC); wp_color_manager_v1_send_supported_feature(r, WP_COLOR_MANAGER_V1_FEATURE_SET_MASTERING_DISPLAY_PRIMARIES); wp_color_manager_v1_send_supported_primaries_named(r, WP_COLOR_MANAGER_V1_PRIMARIES_BT2020); wp_color_manager_v1_send_supported_tf_named(r, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ); wp_color_manager_v1_send_done(r); - /* One line per program: Mesa binds again for every surface-format query. */ if (last_named != c) { last_named = c; droiddeck_log(TAG, "%s bound wp_color_manager_v1 version %u (offered: perceptual intent; parametric descriptions " @@ -1194,7 +1117,6 @@ static void bind_cm(struct wl_client *c, void *data, uint32_t ver, uint32_t id) void droiddeck_color_client_gone(struct wl_client *client) { if (atomic_load(&g_gate) != 1 || atomic_load(&g_session_ended)) return; - /* A program that presented HDR has left: say where the session stands now, while it is fresh. */ const char *name = droiddeck_client_name(client); int mine; pthread_mutex_lock(&g_mu); @@ -1203,7 +1125,6 @@ void droiddeck_color_client_gone(struct wl_client *client) { if (mine) log_verdict(1); } -/* ---------------------------------------------------------------- the gate */ void droiddeck_color_init(struct wl_display *display) { char why[320] = ""; @@ -1227,7 +1148,6 @@ void droiddeck_color_init(struct wl_display *display) { else snprintf(disp, sizeof(disp), "the app could not read the display's HDR capability"); - /* What asked for HDR, in the words the user knows it by: the editors' setting, or the env override. */ char asked[128]; if (mode == 2) snprintf(asked, sizeof(asked), "DROIDDECK_WAYLAND_HDR=force (%s)", source); else if (strstr(source, "env")) snprintf(asked, sizeof(asked), "DROIDDECK_WAYLAND_HDR=1 (%s)", source); @@ -1262,8 +1182,6 @@ void droiddeck_color_init(struct wl_display *display) { "(games that check fall back to SDR; some show washed-out colours) - remove it on this display"); log_verdict(1); } else if (dxvk_hdr || (known && hdr10)) { - /* Off, and silent unless it is worth a line: a display that could show HDR10, or a DXVK - * switch that promises games an HDR display they cannot get. */ droiddeck_log(TAG, "HDR output off for this session (%s)%s%s", off_why, (known && hdr10) ? " - this display lists HDR10, so switching HDR output on for this game would " "offer it to games" : "", diff --git a/app/src/main/cpp/waylandcomp/src/wl_dmabuf.c b/app/src/main/cpp/waylandcomp/src/wl_dmabuf.c index 2f8552f1e..cc85bd32e 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_dmabuf.c +++ b/app/src/main/cpp/waylandcomp/src/wl_dmabuf.c @@ -1,8 +1,3 @@ -/* - * zwp_linux_dmabuf_v1: the buffer params a client builds a dma-buf wl_buffer from, the format and - * modifier table the renderer can import, and dma-buf feedback (version 4). Split out of - * compositor.c; the buffer records themselves are in compositor_internal.h. - */ #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -20,7 +15,6 @@ #include "droiddeck_color.h" #include "compositor_internal.h" -/* ------------------------------------------------------------ zwp_linux_dmabuf_v1 */ static void dbuf_buffer_resource_destroy(struct wl_resource *r) { dmabuf_buffer_unref(wl_resource_get_user_data(r)); @@ -44,7 +38,7 @@ static struct wl_resource *params_do_create(struct wl_client *c, struct wl_resou struct dmabuf_buffer *b = calloc(1, sizeof(*b)); if (!b) return NULL; b->n_planes = p->n_planes; - b->refs = 1; /* the wl_buffer resource's */ + b->refs = 1; b->width = w; b->height = h; b->format = format; b->modifier = p->modifier[0]; for (int i = 0; i < MAX_PLANES; i++) b->fd[i] = -1; @@ -52,7 +46,7 @@ static struct wl_resource *params_do_create(struct wl_client *c, struct wl_resou b->fd[i] = p->fd[i]; b->offset[i] = p->offset[i]; b->stride[i] = p->stride[i]; - p->fd[i] = -1; /* ownership moves to the buffer */ + p->fd[i] = -1; } struct wl_resource *buf = wl_resource_create(c, &wl_buffer_interface, 1, id); if (!buf) { @@ -109,14 +103,6 @@ static const struct zwp_linux_dmabuf_v1_interface dmabuf_impl = { .get_default_feedback = dmabuf_get_default_feedback, .get_surface_feedback = dmabuf_get_surface_feedback, }; -/* The advertised format/modifier table, built at the first bind from what the renderer's driver - * can import (vkp_dmabuf_modifiers). INVALID is always offered too (Mesa drops it; other clients - * may use it for the implicit path). Without a renderer the list is LINEAR + INVALID, as before. */ -/* The last two rows are HDR10's (droiddeck_color.h): A2B10G10R10 as AB30 (alpha) + XB30 (opaque) - Mesa - * lists a VkFormat only when both are advertised, and it is the one 10-bit layout our zero-copy WSI - * can put in a gralloc buffer (AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM). They are advertised ONLY - * while the HDR gate is open, and after the 8-bit rows, so every other session - and every client that - * takes the first format it sees - gets exactly the table it always had. */ #define DMABUF_NFMT_MAX 6 #define DMABUF_NFMT_SDR 4 #define DMABUF_NMOD 4 @@ -124,7 +110,7 @@ static const struct zwp_linux_dmabuf_v1_interface dmabuf_impl = { #define DRM_XBGR2101010 FOURCC('X', 'B', '3', '0') static struct { uint32_t fmt; uint64_t mods[DMABUF_NMOD]; int n; } g_dmabuf_fmts[DMABUF_NFMT_MAX] = { {DRM_ARGB8888}, {DRM_XRGB8888}, {DRM_ABGR8888}, {DRM_XBGR8888}, {DRM_ABGR2101010}, {DRM_XBGR2101010}}; -static int g_dmabuf_nfmt = DMABUF_NFMT_SDR; /* rows advertised: the SDR four, + 2 with the HDR gate open */ +static int g_dmabuf_nfmt = DMABUF_NFMT_SDR; static int g_dmabuf_fmts_ready; static void dmabuf_build_feedback(void); @@ -136,8 +122,6 @@ static void dmabuf_build_formats(void) { for (int f = 0; f < g_dmabuf_nfmt; f++) { uint64_t got[DMABUF_NMOD]; int n = vkp_dmabuf_modifiers(g_dmabuf_fmts[f].fmt, got, DMABUF_NMOD), k = 0; - /* LINEAR first: it is the layout every client and the shm fallback agree on, and the one - * we advertised before this table existed. */ g_dmabuf_fmts[f].mods[k++] = MOD_LINEAR; for (int i = 0; i < n && k < DMABUF_NMOD - 1; i++) { if (got[i] == MOD_LINEAR) continue; @@ -161,8 +145,6 @@ static void dmabuf_build_formats(void) { droiddeck_log("dmabuf", "the compositor's driver (%s) reports no importable qcom_compressed layout: " "game swapchains stay linear", vkp_gpu_name()); if (g_dmabuf_nfmt > DMABUF_NFMT_SDR) { - /* The rows above always carry LINEAR; say what the compositor's own driver can really import - * for 10-bit, since a gralloc buffer it cannot import is shown on the display layer only. */ uint64_t got[DMABUF_NMOD]; int n = vkp_dmabuf_modifiers(DRM_XBGR2101010, got, DMABUF_NMOD), ubwc10 = 0; for (int i = 0; i < n; i++) if (got[i] == VKP_MOD_QCOM_COMPRESSED) ubwc10 = 1; @@ -174,19 +156,6 @@ static void dmabuf_build_formats(void) { dmabuf_build_feedback(); } -/* ---- dmabuf feedback (zwp_linux_dmabuf_v1 version 4) - * - * Turnip's Vulkan WSI is happy with the version 3 format/modifier events, so every Vulkan game - * worked while we only advertised 3. Mesa's EGL is not: its Wayland platform only takes the GPU - * (kopper/Zink) path when it can bind this interface with FEEDBACK, and with 3 it silently drops - * to its wl_shm software path. On this Proton layer that path has no software rasteriser to fall - * back to (the gallium build is zink+kopper+swrast, no llvmpipe), so the shm buffer it commits is - * never written: a native OpenGL window came out solid black, ~30 shm commits/s and no GPU frame - * at all (Wizardry: The Labyrinth of Lost Souls). Feedback is what makes that path work. - * - * What a client needs from us is one tranche describing "everything the compositor can import": - * a format table it mmaps read-only, the device to allocate on, and the indices it may use. - * Clients binding versions 1-3 keep getting the old format/modifier events instead. */ #ifndef MFD_CLOEXEC #define MFD_CLOEXEC 0x0001U @@ -202,17 +171,13 @@ static void dmabuf_build_formats(void) { #define F_SEAL_WRITE 0x0008 #endif -struct dmabuf_fmt_entry { uint32_t format; uint32_t pad; uint64_t modifier; }; /* the wire layout */ +struct dmabuf_fmt_entry { uint32_t format; uint32_t pad; uint64_t modifier; }; -static int g_fmt_table_fd = -1; /* sealed read-only memfd of dmabuf_fmt_entry[] */ +static int g_fmt_table_fd = -1; static size_t g_fmt_table_size; -static uint16_t g_fmt_table_n; /* entries, == the indices a tranche may name */ -dev_t g_main_device; /* the render node clients should allocate on */ +static uint16_t g_fmt_table_n; +dev_t g_main_device; -/* The GPU we import through is reached with KGSL, not DRM, so there is no render node of our own - * to name. Clients only use main_device to match "the same device as the compositor" and to pick - * a driver; the one DRM render node this platform has is the right answer, and Zink ignores it - * anyway (it renders on the Vulkan device it already has). 0 if the platform has none. */ static dev_t dmabuf_render_node(void) { static const char *nodes[] = {"/dev/dri/renderD128", "/dev/dri/renderD129", "/dev/dri/card0"}; struct stat st; @@ -229,7 +194,7 @@ static void dmabuf_build_feedback(void) { g_main_device = dmabuf_render_node(); for (int f = 0; f < g_dmabuf_nfmt; f++) for (int m = 0; m < g_dmabuf_fmts[f].n; m++) { - if (g_dmabuf_fmts[f].mods[m] == MOD_INVALID) continue; /* never offer INVALID here */ + if (g_dmabuf_fmts[f].mods[m] == MOD_INVALID) continue; entries[n].format = g_dmabuf_fmts[f].fmt; entries[n].pad = 0; entries[n].modifier = g_dmabuf_fmts[f].mods[m]; @@ -246,15 +211,12 @@ static void dmabuf_build_feedback(void) { close(fd); return; } - /* The client mmaps this read-only and trusts it not to change under it. */ fcntl(fd, F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE); g_fmt_table_fd = fd; g_fmt_table_size = size; g_fmt_table_n = (uint16_t)n; droiddeck_log("dmabuf", "feedback ready: %d format/modifier pairs, main device %u:%u", n, (unsigned)major(g_main_device), (unsigned)minor(g_main_device)); - /* Vulkan games never need the node. Mesa's EGL did until Wayland layer versionCode 9: without - * one it fell back to a software path that draws nothing here (black window, sound plays). */ if (!g_main_device) droiddeck_log("dmabuf", "%s: OpenGL games need Wayland layer versionCode 9 or newer, " "which runs OpenGL on the GPU without a DRM node; older layers show a black window", @@ -262,7 +224,6 @@ static void dmabuf_build_feedback(void) { : "this device gives apps no display (DRM) device (/dev/dri)"); } -/* One tranche: our device, every pair in the table, no scanout flag. */ static void dmabuf_feedback_send(struct wl_resource *fb) { struct wl_array dev, idx; uint16_t *ind; @@ -309,8 +270,6 @@ static void dmabuf_get_default_feedback(struct wl_client *c, struct wl_resource if (ci) ci->asked_feedback = 1; dmabuf_new_feedback(c, r, id); } -/* Per-surface feedback would let us hint a different tranche for a window on its own display - * layer; we have nothing better to say per surface, so it is the default one. */ static void dmabuf_get_surface_feedback(struct wl_client *c, struct wl_resource *r, uint32_t id, struct wl_resource *surface) { dmabuf_new_feedback(c, r, id); @@ -320,8 +279,6 @@ void bind_dmabuf(struct wl_client *c, void *data, uint32_t ver, uint32_t id) { struct wl_resource *r = wl_resource_create(c, &zwp_linux_dmabuf_v1_interface, ver, id); wl_resource_set_implementation(r, &dmabuf_impl, NULL, NULL); if (!g_dmabuf_fmts_ready) dmabuf_build_formats(); - /* From version 4 the format and modifier events are gone: the client asks for feedback - * instead, and sending both would only confuse it about which list is authoritative. */ if (ver >= 4) return; for (int f = 0; f < g_dmabuf_nfmt; f++) { zwp_linux_dmabuf_v1_send_format(r, g_dmabuf_fmts[f].fmt); diff --git a/app/src/main/cpp/waylandcomp/src/wl_single_pixel.c b/app/src/main/cpp/waylandcomp/src/wl_single_pixel.c index 9dfe78233..96c666496 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_single_pixel.c +++ b/app/src/main/cpp/waylandcomp/src/wl_single_pixel.c @@ -1,15 +1,9 @@ -/* - * wp_single_pixel_buffer_manager_v1 (version 1): a wl_buffer of one pixel of one colour, which a - * client stretches over a surface with wp_viewporter. KWin's Wayland backend - the desktop's KWin - * nested in this compositor - refuses to start without it. The surface takes the pixel as a 1x1 - * image, the way it takes a wl_shm buffer (compositor.c, take_pixel). - */ #include <stdlib.h> #include "droiddeck_ext.h" #include "single-pixel-buffer-v1-server-protocol.h" struct single_pixel { - uint8_t bgra[4]; /* premultiplied, as wl_shm's ARGB8888 lies in memory */ + uint8_t bgra[4]; }; static void buffer_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } @@ -30,7 +24,6 @@ static void mgr_create_buffer(struct wl_client *c, struct wl_resource *r, uint32 struct single_pixel *p = calloc(1, sizeof(*p)); struct wl_resource *buffer = p ? wl_resource_create(c, &wl_buffer_interface, 1, id) : NULL; if (!buffer) { free(p); wl_client_post_no_memory(c); return; } - /* The protocol's values span the whole u32 range; the top byte is the 8-bit channel. */ p->bgra[0] = blue >> 24; p->bgra[1] = green >> 24; p->bgra[2] = red >> 24; diff --git a/app/src/main/cpp/waylandcomp/src/wl_text_input.c b/app/src/main/cpp/waylandcomp/src/wl_text_input.c index 631167e91..8b0ade227 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_text_input.c +++ b/app/src/main/cpp/waylandcomp/src/wl_text_input.c @@ -1,25 +1,3 @@ -/* - * Text input: zwp_text_input_manager_v3 (advertised at version 1), the host input method - * bridge winewayland.drv uses for IME text (our soft keyboard). - * - * Focus model. winewayland enables its text input as soon as our `enter` names a surface and - * routes every IME update to that surface's window - inside that window's process (the update - * list is per process). Keyboard focus in this compositor sits on the desktop surface (the - * desktop owner's process), so text input can't follow it; it follows droiddeck_ime_target(): - * the program window last clicked, else the topmost program window. Clicking into notepad - * moves the text-input focus to notepad's process, which is where the edit control lives. - * - * Protocol state. Requests between commits are pending; `commit` bumps the serial and applies - * them (enable resets the state as the protocol says). We answer with `done(serial)` only when - * we sent events (preedit/commit strings), never for every commit. - * - * Host side. droiddeck_on_text_input(enabled, program, cursor rect in scene pixels) tells the app - * when a program starts/stops accepting IME text or moves its caret (Wine reports the rect only - * when an edit control positions its IME composition window, via ImmSetCompositionWindow). - * Typed text arrives through text_input_host_*: preedit_string / commit_string + done. - * delete_surrounding_text is NOT used: winewayland's handler for it is empty, so deletions - * become Backspace/Delete key presses on the wl_keyboard path instead. - */ #define _GNU_SOURCE 1 #include <stdlib.h> #include <string.h> @@ -39,28 +17,25 @@ struct text_input { struct wl_resource *res; struct wl_client *client; struct wl_list link; - struct wl_resource *entered; /* surface we sent enter for, or NULL */ - uint32_t serial; /* commits received */ - /* pending, applied on commit */ + struct wl_resource *entered; + uint32_t serial; + int p_enable, p_disable, p_rect_set, p_content_set; int p_rect[4]; uint32_t p_hint, p_purpose; - /* current */ int enabled; int rect[4]; uint32_t hint, purpose; - int has_preedit; /* we sent a non-empty preedit last */ + int has_preedit; }; static struct wl_list g_inputs; -static struct wl_resource *g_target; /* surface text input is focused on */ +static struct wl_resource *g_target; -/* What the app was last told. */ static int g_told_enabled, g_told_rect[4]; static struct text_input *g_told_ti; static const char *ti_program(struct text_input *ti) { return droiddeck_client_name(ti->client); } -/* The enabled text input focused on the current target, if any. */ static struct text_input *active_input(void) { struct text_input *ti; if (!g_target) return NULL; @@ -92,13 +67,12 @@ static void tell_app(void) { droiddeck_on_text_input(enabled, enabled ? ti_program(ti) : "", rect[0], rect[1], rect[2], rect[3]); } -/* ------------------------------------------------------------------ focus */ static void send_leave(struct text_input *ti) { if (!ti->entered) return; zwp_text_input_v3_send_leave(ti->res, ti->entered); ti->entered = NULL; - ti->enabled = 0; /* the protocol disables on leave */ + ti->enabled = 0; ti->has_preedit = 0; } @@ -129,7 +103,6 @@ void droiddeck_text_input_surface_gone(struct wl_resource *surface) { if (g_told_ti && g_told_ti->entered == NULL && g_told_enabled) tell_app(); } -/* ------------------------------------------------------------------ requests */ static void ti_res_destroy(struct wl_resource *r) { struct text_input *ti = wl_resource_get_user_data(r); @@ -166,7 +139,7 @@ static void ti_commit(struct wl_client *c, struct wl_resource *r) { if (!ti) return; ti->serial++; if (ti->p_enable) { - ti->enabled = ti->entered != NULL; /* enable only counts while focused */ + ti->enabled = ti->entered != NULL; memset(ti->rect, 0, sizeof(ti->rect)); ti->hint = 0; ti->purpose = 0; ti->has_preedit = 0; @@ -212,7 +185,6 @@ static void bind_text_input_manager(struct wl_client *c, void *data, uint32_t ve wl_resource_set_implementation(r, &tim_impl, NULL, NULL); } -/* ------------------------------------------------------------------ host side (compositor thread) */ static size_t utf8_chars(const char *s, size_t len) { size_t n = 0; @@ -220,7 +192,6 @@ static size_t utf8_chars(const char *s, size_t len) { return n; } -/* Byte offset of UTF-8 character index `chars` (negative = end). */ static int32_t utf8_offset(const char *s, size_t len, int chars) { if (chars < 0) return (int32_t)len; size_t i = 0; @@ -259,7 +230,7 @@ void text_input_host_delete(int before, int after) { if (before > 64) before = 64; if (after > 64) after = 64; struct text_input *ti = active_input(); - if (ti && ti->has_preedit) { /* deleting inside a composition: clear it first */ + if (ti && ti->has_preedit) { zwp_text_input_v3_send_preedit_string(ti->res, "", 0, 0); zwp_text_input_v3_send_done(ti->res, ti->serial); ti->has_preedit = 0; diff --git a/app/src/main/cpp/waylandcomp/src/wl_toplevel_icon.c b/app/src/main/cpp/waylandcomp/src/wl_toplevel_icon.c index ce1cd0e5f..667a22534 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_toplevel_icon.c +++ b/app/src/main/cpp/waylandcomp/src/wl_toplevel_icon.c @@ -1,8 +1,3 @@ -/* - * xdg_toplevel_icon_manager_v1 (version 1): window icons are accepted and dropped. The scene - * has no title bars to draw them in; winewayland only stops complaining that the global is - * missing. We send no icon_size (any size is fine) and `done` at bind, as the protocol asks. - */ #include <stdlib.h> #include "droiddeck_ext.h" #include "xdg-toplevel-icon-v1-server-protocol.h" diff --git a/app/src/main/java/com/droiddeck/launcher/App.kt b/app/src/main/java/com/droiddeck/launcher/App.kt index 1b1a2fd82..43d3ffc12 100644 --- a/app/src/main/java/com/droiddeck/launcher/App.kt +++ b/app/src/main/java/com/droiddeck/launcher/App.kt @@ -5,11 +5,6 @@ import com.droiddeck.launcher.runtime.BwrapSpawner import com.droiddeck.launcher.session.CrashHandler import com.droiddeck.launcher.session.GpuClockPin -/** - * Process-wide setup: the crash handler, so a session's folder is finished even when we die, and - * the GPU clock pin cleared in case a killed process left it set, and the socket Flatpak's - * sandboxes are started through (BwrapSpawner). - */ class App : Application() { override fun attachBaseContext(base: android.content.Context) = super.attachBaseContext(com.droiddeck.launcher.core.AppLanguage.wrap(base)) @@ -28,7 +23,6 @@ class App : Application() { GpuClockPin.clearLeftover(this) BwrapSpawner.start(this) com.droiddeck.launcher.frontend.GameFileSync.start(this) - // The game stores the Stores section can talk to; the native engine loads only when one needs it. com.droiddeck.launcher.stores.StoresState.init(this) com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.gog.GogBackend) com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.epic.EpicBackend) diff --git a/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt b/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt index 1fd549f80..cbd760370 100644 --- a/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt @@ -8,10 +8,6 @@ import androidx.compose.runtime.setValue import com.droiddeck.launcher.session.ComponentsManager import com.droiddeck.launcher.session.ProtonDefault -/** - * Component state and work for the launcher and session drawer: the Protons and their - * DXVK/VKD3D sets, the Nightlies catalog, downloads and the swap/restore actions. - */ internal class ComponentsMenu(private val activity: android.app.Activity, private val ui: Handler) { var compSnapshot by mutableStateOf<ComponentsManager.Snapshot?>(null) var compCatalog by mutableStateOf<List<ComponentsManager.CatalogItem>>(emptyList()) @@ -26,7 +22,6 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat val compDownloadRevision: Long get() = downloadRevision private var shownDefault: String? = null - /** Reads the Protons (and on first open saves their originals) off the UI thread; the Nightlies list comes from its cache. */ fun refreshComponents(snapshotFirst: Boolean = false) { Thread({ if (snapshotFirst) runCatching { ComponentsManager.snapshotAll(activity) } @@ -107,8 +102,6 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat private companion object { const val TAG = "ComponentsMenu" - // Both screens use the same package store and .part files. Keep the in-flight registry - // across menu/activity instances so a package cannot be downloaded twice at once. var downloads by mutableStateOf<Map<String, Int>>(emptyMap()) var downloadRevision by mutableStateOf(0L) } diff --git a/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt b/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt index efffe789d..9f8a27f9f 100644 --- a/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt @@ -9,17 +9,12 @@ import com.droiddeck.launcher.runtime.DeckyManager import com.droiddeck.launcher.session.SessionState import java.io.File -/** - * Decky Loader's state in the launcher: what is installed, the releases on offer and an install - * in progress. The launcher screen keeps one and shows it. - */ internal class DeckyMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) { var deckyInstalled by mutableStateOf<String?>(null) var deckyReleases by mutableStateOf<List<DeckyManager.Release>>(emptyList()) var deckyChecking by mutableStateOf(false) var deckyStage by mutableStateOf<String?>(null) var deckyPercent by mutableIntStateOf(-1) - /** True while the stage is the plugin's binary download (DeckyManager.downloadingBinary). */ var deckyDownloadingBinary by mutableStateOf(false) var deckySupervisor by mutableStateOf(false) var deckyReleaseRequest = 0 @@ -32,8 +27,6 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val .getOrElse { DeckyManager.ReleaseChannels(emptyList(), emptyList()) } ui.post { if (request == deckyReleaseRequest) { - // Use the newest compatible stable build, or the newest compatible - // prerelease when the fork has not published a stable ARM64 asset. deckyReleases = channels.stable.ifEmpty { channels.prerelease } deckyChecking = false } diff --git a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt index ababdc230..09c195201 100644 --- a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt +++ b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt @@ -25,35 +25,21 @@ import com.droiddeck.launcher.ui.GpuDriversState import com.droiddeck.launcher.ui.PairRow import com.droiddeck.launcher.wayland.CompositorHost -/** - * The GPU drivers: what this GPU is, the matched driver pairs the release repos offer for it, and - * - in Auto, the default - keeping the recommended pair installed and set. Under Advanced, the - * Linux (runtime) and Android (display) lists as before, each picked on its own - except a - * bundle's two halves (DriverBundle), which are always picked, and deleted, together. The launcher - * screen keeps one and hands it to the Components page's GPU drivers tab. - */ internal class DriverMenus(private val activity: Activity, private val ui: Handler) { var linuxRows by mutableStateOf<List<DriverRow>>(emptyList()) - /** The latest Banners-Turnip release as each driver menu offers it (see [refreshReleaseRows]). */ var linuxDownloads by mutableStateOf<List<com.droiddeck.launcher.ui.DownloadRow>>(emptyList()) var androidDownloads by mutableStateOf<List<com.droiddeck.launcher.ui.DownloadRow>>(emptyList()) - // Empty until the first refresh: the activity has no context yet while its fields are made. var releaseStatus by mutableStateOf("") var releaseChecking by mutableStateOf(false) var canRestoreBundled by mutableStateOf(false) - /** Asset name -> download percent, while it downloads. */ private val releaseProgress = HashMap<String, Int>() var linuxSelected by mutableStateOf("") val gpu: GpuInfo = GpuInfo.detect() - // Read in refreshDrivers/ensureAuto: the activity has no context yet while its fields are made. var mode by mutableStateOf(SessionPrefs.GPU_DRIVERS_AUTO) var pairRows by mutableStateOf<List<PairRow>>(emptyList()) - /** The pair being installed, and how far along. */ var pairBusy by mutableStateOf<String?>(null) var pairPercent by mutableIntStateOf(-1) - /** What Auto last did or found, for the line under the pair in use. */ var autoStatus by mutableStateOf("") - /** The bundle both drivers are set to, as "DD-Turnip 0.1.0", or null when they are not one. */ var activeBundle by mutableStateOf<String?>(null) private var autoCheckedThisProcess = false @@ -107,14 +93,12 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl refreshPairs() } - /** One line for the settings row that opens this: "Auto · WinNative · Balanced". */ fun summary(): String { val active = pairRows.firstOrNull { it.active }?.name ?: activeBundle val label = activity.getString(if (mode == SessionPrefs.GPU_DRIVERS_AUTO) R.string.common_auto else R.string.drivers_manual) return active?.let { activity.getString(R.string.drivers_summary, label, it) } ?: label } - /** The pairs the last check found, with what is installed and in use; this GPU's first. */ fun refreshPairs() { val lm = LinuxVulkanDriverManager(activity) val td = TurnipDriver(activity) @@ -143,12 +127,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl if (auto) ensureAuto(force = false) } - /** - * Auto: make the recommended pair for this GPU the one installed and set. Looks online when - * [force]d (refresh), or when the last check is a day old - Banners-Turnip rebuilds hourly, and - * a new pair a day is plenty - otherwise works from the last check. Once per app start on its - * own; the pair it replaces is removed, anything the user imported or picked is left alone. - */ fun ensureAuto(force: Boolean) { mode = SessionPrefs.gpuDriverMode(activity) if (mode != SessionPrefs.GPU_DRIVERS_AUTO || pairBusy != null || releaseChecking) return @@ -184,18 +162,12 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl }, "gpu-driver-auto").start() } - /** Manual: install (if need be) and set both halves of a pair. */ fun selectPair(key: String) { if (pairBusy != null) return val pair = DriverPairs.from(TurnipReleases.cached(activity)).firstOrNull { it.key == key && it.complete } ?: return installPair(pair, auto = false) } - /** - * Download what is missing of [pair], then set both halves - only once both are in, so a - * failed download never leaves a half-changed pair. Auto's own downloads are recorded, and the - * ones the new pair replaces are removed. - */ private fun installPair(pair: DriverPairs.DriverPair, auto: Boolean) { if (!pair.complete) return pairBusy = pair.key @@ -252,21 +224,15 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl }, "gpu-driver-pair").start() } - /** The bundle a release bundle was downloaded as, while both its halves are installed. */ private fun installedBundle(asset: TurnipReleases.Asset): DriverBundle.Bundle? = TurnipReleases.installedId(activity, asset) { DriverBundle.get(activity, it) != null }?.let { DriverBundle.get(activity, it) } - /** The id a release asset was installed as (a bundle's id for a bundle), or null when it is not installed. */ private fun installedIdOf(asset: TurnipReleases.Asset): String? = when { asset.bundle -> installedBundle(asset)?.id asset.linux -> TurnipReleases.installedId(activity, asset, LinuxVulkanDriverManager(activity)::isInstalled) else -> TurnipReleases.installedId(activity, asset, TurnipDriver(activity)::isInstalled) } - /** - * One release asset, downloaded, checked and installed through the importer; returns its id, - * a bundle's id for a bundle. - */ private fun installAsset(asset: TurnipReleases.Asset, progress: (Int) -> Unit): String { var file: File? = null try { @@ -284,12 +250,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl } } - /** - * Import off the main thread - a driver zip is a few MB and the glibc check reads the whole - * library - then say what happened. A refusal's message is the user-facing reason. A bundle, - * from any import, installs both halves and is set as both drivers at once. [linux] null (the - * tab's own Import .zip) takes a single driver to whichever list its libc belongs in. - */ fun importDriver(uri: Uri, linux: Boolean?) { val name = activity.displayNameOf(uri) Thread({ @@ -328,10 +288,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl }, "import-driver").start() } - /** - * Pick one driver from a list, which is Manual (Auto would put its pair back). A bundle's half - * sets the bundle's other half in the other list as well. - */ fun selectDriver(id: String, linux: Boolean) { val bundle = DriverBundle.containing(activity, id, linux) when { @@ -343,7 +299,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl refreshDrivers() } - /** Set both halves of [bundle] as the drivers in use; returns what to tell the user. */ private fun useBundle(bundle: DriverBundle.Bundle): String { val displayChanged = SessionPrefs.androidDriver(activity) != bundle.androidId DriverBundle.select(activity, bundle) @@ -354,11 +309,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl ) } - /** - * Delete an imported or downloaded driver. A mode still set to it goes back to its default, so a - * session never starts on a driver that is gone; a release download is forgotten, so the menu - * offers it again. - */ fun deleteDriver(id: String, linux: Boolean) { val bundle = DriverBundle.containing(activity, id, linux) if (bundle != null) { @@ -381,7 +331,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl refreshDrivers() } - /** The download entries and the refresh line, from what the last check found. */ fun refreshReleaseRows() { val check = TurnipReleases.cached(activity) fun rows(linux: Boolean) = check?.assets.orEmpty() @@ -413,7 +362,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl } } - /** Only when the user taps refresh: nothing goes online on its own. */ fun checkLatestTurnip() { if (releaseChecking) return releaseChecking = true @@ -431,7 +379,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl }, "turnip-release-check").start() } - /** Download one release driver and import it through the same importer a picked zip uses. */ fun downloadReleaseDriver(assetName: String) { val asset = TurnipReleases.cached(activity)?.assets?.firstOrNull { it.name == assetName } ?: return if (releaseProgress.containsKey(assetName)) return diff --git a/app/src/main/java/com/droiddeck/launcher/HomeActivity.kt b/app/src/main/java/com/droiddeck/launcher/HomeActivity.kt index 7a6c60cbf..54526834b 100644 --- a/app/src/main/java/com/droiddeck/launcher/HomeActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/HomeActivity.kt @@ -5,7 +5,6 @@ import android.content.Intent import android.os.Bundle import com.droiddeck.launcher.session.SessionService -/** Receives the system Home launch and routes it without consuming KEYCODE_HOME. */ class HomeActivity : Activity() { override fun attachBaseContext(newBase: android.content.Context) { super.attachBaseContext(newBase) diff --git a/app/src/main/java/com/droiddeck/launcher/HomeApp.kt b/app/src/main/java/com/droiddeck/launcher/HomeApp.kt index cc1b2afc0..74b49821c 100644 --- a/app/src/main/java/com/droiddeck/launcher/HomeApp.kt +++ b/app/src/main/java/com/droiddeck/launcher/HomeApp.kt @@ -13,7 +13,6 @@ import androidx.core.graphics.drawable.toBitmap import com.droiddeck.launcher.session.SessionService import com.droiddeck.launcher.session.SessionState -/** Android Home role and the launchable Android apps shown by the in-session app drawer. */ object HomeApp { data class LaunchableApp( val label: String, @@ -22,17 +21,10 @@ object HomeApp { val icon: Bitmap?, ) - /** - * Whether the app offers itself as a Home app at all. HomeActivity is declared disabled and - * only enabled when the user turns on "Use as a Home screen": a player who does not want a - * launcher never gets Android's "which Home app?" question, and it is one apk either way. - * The setting is the component's own state, which Android keeps across updates. - */ fun isHomeScreenEnabled(context: Context): Boolean = context.packageManager.getComponentEnabledSetting(ComponentName(context, HomeActivity::class.java)) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED - /** Turned off while DroidDeck is the Home app, the phone goes back to its own launcher. */ fun setHomeScreenEnabled(context: Context, enabled: Boolean) { context.packageManager.setComponentEnabledSetting( ComponentName(context, HomeActivity::class.java), @@ -113,7 +105,6 @@ object HomeApp { try { context.startActivity(settingsIntent()) } catch (_: Exception) { - // A role request remains available on Android 10+ if the settings page is missing. val request = roleRequestIntent(context) if (request != null) context.startActivity(request) } diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index bd7577a78..d72953db8 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -79,11 +79,6 @@ import com.droiddeck.launcher.session.GameStorage import com.droiddeck.launcher.input.SecondScreenDisplay import com.droiddeck.launcher.input.SecondScreenDisplays -/** - * The whole app outside a session: is the runtime installed, is there a newer one, frame - * generation, and one button that starts Steam. Everything a Steam client can do - the library, - * the store, downloads, settings - is the client's own job once [SessionActivity] has it on screen. - */ class MainActivity : ComponentActivity() { override fun attachBaseContext(newBase: android.content.Context) { super.attachBaseContext(newBase) @@ -97,13 +92,10 @@ class MainActivity : ComponentActivity() { private val updates = UpdatesMenu(this, ui) private val protons = ProtonMenu(this, ui) - // The screen's state. Compose redraws whatever reads these when they change. private var installed by mutableStateOf<String?>(null) - /** A session launch waiting on its button to fill the page (LaunchFlood). */ private var flood by mutableStateOf<PendingFlood?>(null) private var floodProgress by androidx.compose.runtime.mutableFloatStateOf(0f) private class PendingFlood(val from: androidx.compose.ui.geometry.Rect, val intent: Intent) - /** A session stopped behind a flood: its blue, drawn back into the button it started from (FloodReturn). */ private var returning by mutableStateOf<ReturningFlood?>(null) private class ReturningFlood(val color: Int, val to: androidx.compose.ui.geometry.Rect?) private var ready by mutableStateOf(false) @@ -123,12 +115,10 @@ class MainActivity : ComponentActivity() { private var steamRepairQueued by mutableStateOf(false) private var mangoapp by mutableStateOf(true) private var steamController by mutableStateOf(SessionPrefs.CONTROLLER_DECK) - // Components page: FEX / DXVK / VKD3D-Proton per Proton (ComponentsManager). private var showComponents by mutableStateOf(false) private var focusComponentsContent by mutableStateOf(true) private var showMapping by mutableStateOf(false) private var saveBusy: String? = null - /** What to do with the zip or folder the file picker hands back after a game page's Manage saves. */ private var onSavePicked: ((File) -> Unit)? = null private var controllerSettings by mutableStateOf<ControllerPrefs.Settings?>(null) private var catalog by mutableStateOf<List<DesktopCatalog.Entry>?>(null) @@ -167,7 +157,6 @@ class MainActivity : ComponentActivity() { private var gameStoresEnabled by mutableStateOf(true) private var gameStoresSpeedTier by mutableStateOf("fast") private var storesOpenTab by mutableStateOf(SessionPrefs.STORES_OPEN_LIBRARY) - /** A page a notification asked for ("stores:downloads", "app:<id>"), until the front end has gone there. */ private var navRequest by mutableStateOf<String?>(null) private var storesShowMature by mutableStateOf(false) private var mic by mutableStateOf(false) @@ -200,8 +189,6 @@ class MainActivity : ComponentActivity() { refreshWifiDiscovery() } - // The app's own picker (files/), once per kind of pick: the two driver lists validate - // differently, and the reason a zip is refused names the list it belongs in. private val pickComponent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r -> if (r.resultCode == RESULT_OK) InAppFilePicker.pickedUri(r.data)?.let { importComponent(it) } } @@ -235,7 +222,6 @@ class MainActivity : ComponentActivity() { wifiDiscoveryAsked = SessionPrefs.wifiDiscoveryAsked(this) wifiDiscoveryBlocked = !wifiDiscoveryPermission && wifiDiscoveryAsked && WifiDiscovery.permissions.none { shouldShowRequestPermissionRationale(it) } - // A revoked grant must not leave an enabled switch behind. if (!wifiDiscoveryPermission && SessionPrefs.wifiDiscoveryEnabled(this)) { SessionPrefs.setWifiDiscoveryEnabled(this, false) } @@ -251,7 +237,6 @@ class MainActivity : ComponentActivity() { } } - /** Takes a new or updated Lossless Scaling from Steam and shows what LSFG can use. */ private fun syncLossless() { Thread({ Lossless.sync(this) @@ -271,7 +256,6 @@ class MainActivity : ComponentActivity() { }, "lossless-import").start() } - /** Import a save zip into [game]: pick it in the app's file picker, then back up and unzip off the main thread. */ private fun importSaves(name: String, game: () -> GameSaves.Game) { if (SessionState.running) { android.widget.Toast.makeText(this, R.string.main_saves_close_session, android.widget.Toast.LENGTH_LONG).show() @@ -290,7 +274,6 @@ class MainActivity : ComponentActivity() { pickSaveZip.launch(InAppFilePicker.buildIntent(this, listOf("zip"), getString(R.string.main_saves_pick_zip, name), GameSaves.savesDir().parentFile?.parentFile?.path)) } - /** Export [game]'s saves in [layout] to a folder picked in the app's file picker. */ private fun exportSaves(name: String, layout: GameSaves.Layout, game: () -> GameSaves.Game) { onSavePicked = { dir -> saveAction(getString(R.string.main_saves_exporting, name)) { @@ -301,7 +284,6 @@ class MainActivity : ComponentActivity() { GameSaves.savesDir().mkdirs() pickSaveDir.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.main_saves_pick_folder, name, layout.label(this)), GameSaves.savesDir().path)) } - /** The Games tab's +: the picked .exe becomes a game (or selects / switches the one it belongs to). */ private fun addGameExe(path: String) { val exe = java.io.File(path) Thread({ @@ -327,7 +309,6 @@ class MainActivity : ComponentActivity() { } private var addedGamesArt by mutableStateOf(true) @Volatile private var artFetchRunning = false - /** The user entered a SteamGridDB API key of their own. */ private var sgdbUserKey by mutableStateOf(false) private val pickRomsDir = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r -> if (r.resultCode == RESULT_OK) InAppFilePicker.pickedPath(r.data)?.let { path -> @@ -335,7 +316,6 @@ class MainActivity : ComponentActivity() { romsDir = path } } - // The mode whose lifted sheet last read its settings, with what it shows; refreshed by loadModeSettings(). private var settingsMode by mutableStateOf<String?>(null) private var resolution by mutableStateOf(com.droiddeck.launcher.session.SessionDisplay.DEFAULT_RESOLUTION) private var fexPreset by mutableStateOf("") @@ -386,7 +366,6 @@ class MainActivity : ComponentActivity() { refreshHomeAppState() } - /** The session surface rises over the front end instead of cutting to it. */ override fun startActivity(intent: Intent?) { if (intent?.component?.className == SessionActivity::class.java.name) { when { @@ -406,8 +385,6 @@ class MainActivity : ComponentActivity() { } if (intent?.component?.className == SessionActivity::class.java.name) { if (flood != null) return - // Started by a blue button: it floods the page first. A running session is re-joined - // as before, rising over the front end. val from = com.droiddeck.launcher.ui.LaunchOrigin.take() if (from != null && !SessionState.running && com.droiddeck.launcher.ui.Motion.scale != 0f) { flood = PendingFlood(from, intent) @@ -419,7 +396,6 @@ class MainActivity : ComponentActivity() { if (intent?.component?.className == SessionActivity::class.java.name) overridePendingTransition(R.anim.session_rise, R.anim.session_hold) } - /** The page is all blue: the session opens on the same blue, with no animation of its own. */ private fun launchFlooded(f: PendingFlood) { if (busy || LinuxRuntimeInstaller.isBusy()) { flood = null; floodProgress = 0f; return } val signal = com.droiddeck.launcher.ui.Themes.byId(theme).signal @@ -453,7 +429,6 @@ class MainActivity : ComponentActivity() { request.getStringExtra(com.droiddeck.launcher.stores.download.StoreDownloadService.EXTRA_NAV)?.let { navRequest = it; request.removeExtra(com.droiddeck.launcher.stores.download.StoreDownloadService.EXTRA_NAV) } shortcutPicker = request.action == Intent.ACTION_CREATE_SHORTCUT if (shortcutPicker) { - // The shortcut picker must only offer games from the scan started for this request. steamGames = emptyList() shortcutLibraryScanning = true } else shortcutLibraryScanning = false @@ -531,7 +506,6 @@ class MainActivity : ComponentActivity() { theme = SessionPrefs.theme(this) frameGenLabel = getString(R.string.frame_gen_off) appScale = com.droiddeck.launcher.core.AppUiPrefs.scale(this) - // Last start's game list, so the Steam wall is up on the first frame; refresh() replaces it. steamGames = if (shortcutPicker) emptyList() else com.droiddeck.launcher.frontend.LibraryCache.load(this) backActionsInverted = SessionPrefs.backActionsInverted(this) orientation = SessionPrefs.orientation(this) @@ -603,18 +577,14 @@ class MainActivity : ComponentActivity() { ), FrontEndActions( onPlay = { startSteamSession() }, - // The client's desktop UI, with gamescope's Deck integration disabled. onPlayDesktopUi = { startSession(Intent(this, SessionActivity::class.java) .putExtra(SessionService.EXTRA_MODE, SessionService.MODE_STEAM) .putExtra(SessionService.EXTRA_STEAM_UI, "desktop"), steamSession = true) }, - // A store game may need something done first (an Epic game its sign-in code). onSteamGame = { g -> if (shortcutPicker) chooseGameShortcut(g) else { - // The launch takes the Windows components already on; the game's list is - // looked at again in the background once the session is over. com.droiddeck.launcher.frontend.AutoComponents.beforeLaunch(this, g) com.droiddeck.launcher.stores.StoreLaunch.prepare(this, g) { launchGame(g) } } @@ -624,7 +594,6 @@ class MainActivity : ComponentActivity() { onSyncGameFiles = { pickGameExport(null) }, onAddGameExe = { path -> addGameExe(path) }, onAddedGamesChanged = { folders -> - // New games want art; an edit does not. val known = steamGames.mapNotNull { it.gameFiles?.path }.toSet() updateAddedGames(folders, then = { if (folders.any { it !in known }) fetchAddedGameArt() }) }, @@ -652,7 +621,6 @@ class MainActivity : ComponentActivity() { .putExtra(SessionService.EXTRA_MODE, SessionService.MODE_DESKTOP)) }, onEmulator = { e -> launchProgram(e.program) }, - // An app added on the Desktop page, full screen under gamescope like an emulator. onUserApp = { app -> app.args.firstOrNull()?.let { Library.flatpakNames[it] = app.name } startActivity(Intent(this, SessionActivity::class.java) @@ -660,7 +628,6 @@ class MainActivity : ComponentActivity() { .putExtra(SessionService.EXTRA_PROGRAM, app.program) .putExtra(SessionService.EXTRA_PROGRAM_ARGS, app.args.toTypedArray())) }, - // A Flatpak app from the store, full screen under gamescope like an emulator. onFlatpakApp = { id, name -> Library.flatpakNames[id] = name startActivity(Intent(this, SessionActivity::class.java) @@ -675,7 +642,6 @@ class MainActivity : ComponentActivity() { .putExtra(SessionService.EXTRA_PROGRAM, e.program) .putExtra(SessionService.EXTRA_PROGRAM_ARGS, Library.launchArgs(e.id, g.guestPath).toTypedArray())) }, - // The activity re-attaches to the session that is running; nothing restarts. onResume = { startActivity(Intent(this, SessionActivity::class.java)) }, onSteamSettings = { loadModeSettings(SessionService.MODE_STEAM) }, onRemoveDesktop = { removeDesktop() }, @@ -694,7 +660,6 @@ class MainActivity : ComponentActivity() { onProtons = { openProtons() }, onComponents = { focusContent -> openComponents(focusContent) }, onComponentsTab = { tab -> openComponents(focusContent = true, tab = tab) }, - // A game page's Manage saves: the game's Proton and saves are read when the work runs, off the main thread. onSaveImport = { sg -> importSaves(sg.name) { GameSaves.game(sg) } }, onSaveExport = { sg, layout -> exportSaves(sg.name, layout) { GameSaves.game(sg) } }, onPerformance = { refreshCores(); showComponents = false; showMapping = false; showPerformance = true }, @@ -742,7 +707,6 @@ class MainActivity : ComponentActivity() { onLanguage = { tag -> if (tag != com.droiddeck.launcher.core.AppLanguage.chosen(this)) { com.droiddeck.launcher.core.AppLanguage.setChosen(this, tag) - // Redrawn in the new language; Setup's page and tab are saved state. recreate() } }, @@ -895,25 +859,17 @@ class MainActivity : ComponentActivity() { if (savedInstanceState == null) ui.post { startSteamAtStartupIfEnabled() } - // The session's logs land in Downloads so a failed run can be handed over as a folder - // rather than dug out of app-private storage. targetSdk 28 means the old permission still - // grants exactly that. val wanted = ArrayList<String>() if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { wanted.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } - // The microphone is off by default. Someone who turned it on before the permission was - // granted is asked once here, with the storage prompt; a refusal is not asked again - the - // toggle asks when used. if (SessionPrefs.micEnabled(this) && !SessionPrefs.micAsked(this) && checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { wanted.add(Manifest.permission.RECORD_AUDIO) SessionPrefs.setMicAsked(this) } if (wanted.isNotEmpty()) requestPermissions(wanted.toTypedArray(), 1) - // A store sign-in an earlier build left in plain text is sealed with the Keystore key. Thread({ com.droiddeck.launcher.stores.StoreAccounts.encryptAll(this) }, "store-credentials").start() - // A session folder left without its ending - the process was killed - gets it now. if (!SessionState.running) Thread({ com.droiddeck.launcher.session.LogMigration.run(this) com.droiddeck.launcher.frontend.GamesFolderMigration.run(this) @@ -969,7 +925,6 @@ class MainActivity : ComponentActivity() { } } - /** On the Components page the pad's LB / RB step through the stack's layers, wrapping around. */ override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (event.keyCode == KeyEvent.KEYCODE_BUTTON_A) com.droiddeck.launcher.ui.HeldKeys.confirm = event.action == KeyEvent.ACTION_DOWN if (showComponents && (event.keyCode == KeyEvent.KEYCODE_BUTTON_L1 || event.keyCode == KeyEvent.KEYCODE_BUTTON_R1)) { @@ -978,8 +933,6 @@ class MainActivity : ComponentActivity() { } return true } - // Compose throws when its focus tree has lost the focused node; one key is dropped - // instead of the app. return try { super.dispatchKeyEvent(event) } catch (e: IllegalStateException) { @@ -994,25 +947,17 @@ class MainActivity : ComponentActivity() { orientation = SessionPrefs.orientation(this) requestedOrientation = orientation refreshWifiDiscovery() - // Back from a store's sign-in page: the Stores chips show the account at once. A store - // install finishing while this screen is up rebuilds the Games list through the listener. com.droiddeck.launcher.stores.StoresState.refresh(this) com.droiddeck.launcher.stores.StoresState.libraryListener = { refreshAddedGames(); refresh() } com.droiddeck.launcher.ui.Motion.refresh(this) - // Back from a session stopped behind a flood: open on its blue, before the first frame. com.droiddeck.launcher.ui.QuitFlood.take()?.let { c -> returning = ReturningFlood(c, com.droiddeck.launcher.ui.LaunchOrigin.takeReturn()) } refreshPhantomStatus() syncLossless() - // Opening the app and coming back from a session both land here. updates.onResume() - // Auto keeps this GPU's recommended driver pair installed and set: once per app start. drivers.ensureAuto(force = false) - // Newer GE-Proton / proton-cachyos / DroidDeck-Proton releases: looked up every few hours at most. protons.checkUpdates() - // Swaps queued while a game ran on that Proton go in once nothing uses it (usually the - // session has just ended). Cheap when nothing is queued. if (!SessionState.running) Thread({ val applied = runCatching { ComponentsManager.applyQueued(this) }.getOrDefault(emptyList()) if (applied.isNotEmpty()) ui.post { @@ -1029,8 +974,6 @@ class MainActivity : ComponentActivity() { decky.deckyInstalled = DeckyManager.installed(this) DeckyManager.syncCefMarker(this) decky.deckySupervisor = DeckyManager.supervisorEnabled(this) - // Added games' art (a store lookup for what the folders lack) starts here, not only when - // the cog opens. refreshAddedGames() if (catalog == null) loadDesktopCatalog() if (!busy) Thread({ checkCatalog() }, "catalog").start() @@ -1057,10 +1000,8 @@ class MainActivity : ComponentActivity() { unregisterReceiver(wifiLocationReceiver) unregisterReceiver(mediaReceiver) ui.removeCallbacks(mediaRefresh) - // An install finishing while this screen is away is picked up by onResume's refresh. com.droiddeck.launcher.stores.StoresState.libraryListener = null displayManager.unregisterDisplayListener(secondScreenDisplayListener) - // The session covers the page by now; coming back finds it as it was. flood = null returning = null floodProgress = 0f @@ -1116,7 +1057,6 @@ class MainActivity : ComponentActivity() { }, "catalog-desktop").start() } - /** Runs a save import or export off the main thread, one at a time, and says how it went. */ private fun saveAction(label: String, work: () -> String) { if (busy || LinuxRuntimeInstaller.isBusy() || saveBusy != null) return saveBusy = label @@ -1129,7 +1069,6 @@ class MainActivity : ComponentActivity() { }, "game-saves-action").start() } - /** The Protons DroidDeck installs are the Components page's Proton layer. */ private fun openProtons() = openComponents(focusContent = true, tab = com.droiddeck.launcher.ui.PROTON_TAB) private fun refreshController() { @@ -1153,7 +1092,6 @@ class MainActivity : ComponentActivity() { showPerformance = false showMapping = false showComponents = true - // The Proton layer's options for every Proton, as they are now. fexPreset = SessionPrefs.fexPreset(this) forceSsbs = SessionPrefs.forceSsbs(this) fastSync = SessionPrefs.fastSync(this) @@ -1196,7 +1134,6 @@ class MainActivity : ComponentActivity() { onDownload = { components.downloadComponent(it) }, onRefresh = { components.refreshComponentCatalog() }, onImport = { pickComponent.launch(InAppFilePicker.buildIntent(this, WCP_EXT, getString(R.string.comp_pick_wcp))) }, - // Reached by a hop (a game's Components card, Play's Graphics drivers): back the way it came. onBack = { if (!com.droiddeck.launcher.ui.Hops.back { showComponents = false }) showComponents = false }, games = steamGames, generalFexPreset = fexPreset, @@ -1206,7 +1143,6 @@ class MainActivity : ComponentActivity() { onAuto = { on -> drivers.setMode(on) }, onPair = { key -> drivers.selectPair(key) }, onRefresh = { if (drivers.mode == SessionPrefs.GPU_DRIVERS_AUTO) drivers.ensureAuto(force = true) else drivers.checkLatestTurnip() }, - // Picking one driver on its own is Manual: Auto would put its pair back. onSelectLinux = { id -> drivers.selectDriver(id, linux = true) }, onSelectAndroid = { id -> drivers.selectDriver(id, linux = false) }, onRemoveLinux = { id -> drivers.deleteDriver(id, linux = true) }, @@ -1241,9 +1177,7 @@ class MainActivity : ComponentActivity() { ) }, ) - // A download can finish in the session drawer while this page is visible. androidx.compose.runtime.LaunchedEffect(components.compDownloadRevision) { components.refreshComponents() } - // A Proton installed or removed changes the stack's list of Protons. androidx.compose.runtime.LaunchedEffect(protons.protonBusyId) { if (protons.protonBusyId == null) components.refreshComponents() } } @@ -1292,7 +1226,6 @@ class MainActivity : ComponentActivity() { @Composable private fun ModeSheetHost(mode: String, tab: String, extras: com.droiddeck.launcher.ui.SheetExtras) { - // Read for the other mode last (or not yet): what this one shows is read as it opens. if (settingsMode != mode) return com.droiddeck.launcher.ui.ModeSheetRows( ModeSettings( @@ -1377,8 +1310,6 @@ class MainActivity : ComponentActivity() { onMic = { on -> SessionPrefs.setMicEnabled(this, on) mic = on - // The session checks the grant itself at start; asking here means the - // answer is in before the first session that wants it. if (on && checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), 2) } @@ -1461,17 +1392,12 @@ class MainActivity : ComponentActivity() { ) } - /** - * The Games tab after these added games were added or edited: each read alone and put in - * place of its old entry (or added), no library-wide scan. [then] runs after, on the main thread. - */ private fun updateAddedGames(folders: List<String>, then: () -> Unit = {}) { if (folders.isEmpty()) return Thread({ val fresh = folders.mapNotNull { f -> com.droiddeck.launcher.frontend.AddedGames.single(this, f)?.let { com.droiddeck.launcher.frontend.Library.addedGame(this, it) } } - // An added or edited game's Windows components, from its source's list. fresh.forEach { com.droiddeck.launcher.frontend.AutoComponents.refreshLater(this, it) } ui.post { val paths = folders.toSet() @@ -1481,19 +1407,16 @@ class MainActivity : ComponentActivity() { }, "added-games-update").start() } - /** An added game removed: off the Games tab now; the rest (cache, Steam, listing) off the main thread. */ private fun removeAddedGame(folder: String, appId: Long?, name: String) { steamGames = steamGames.filterNot { it.library == Library.ADDED && it.gameFiles?.path == folder } Thread({ com.droiddeck.launcher.frontend.AddedGames.remove(this, java.io.File(folder), appId, name) }, "added-game-remove").start() } - /** After the added games changed: art that is missing is fetched (off the main thread). */ private fun refreshAddedGames() { sgdbKeyKnown() fetchAddedGameArt() } - /** Whether the user has a SteamGridDB key of their own, for Setup; read off the main thread. */ private fun sgdbKeyKnown() { Thread({ val set = com.droiddeck.launcher.frontend.SteamGridDb.userKey(this).isNotEmpty() @@ -1502,8 +1425,6 @@ class MainActivity : ComponentActivity() { } private fun fetchAddedGameArt() { - // Art the games do not have yet, from Steam's store, off the main thread; the rail - // redraws when something arrives. if (SessionPrefs.addedGamesArt(this) && !artFetchRunning) { artFetchRunning = true Thread({ @@ -1550,8 +1471,6 @@ class MainActivity : ComponentActivity() { gameStorage = SessionPrefs.gameStorage(this) storageDiagnostics = SessionPrefs.storageDiagnosticsEnabled(this) settingsMode = mode - // The sheet lifts at once, on what was last read; the slow part (driver files, a walk of the - // added-games folders, the storage volumes) lands while it rises. Thread({ val zram = com.droiddeck.launcher.core.ZramSupport.supported() val zramOn = SessionPrefs.zramCompression(this) @@ -1572,7 +1491,6 @@ class MainActivity : ComponentActivity() { }, "mode-settings").start() } - /** A second Steam library, proven writable first; "" = internal only. */ private fun setGameStorage(path: String, label: String) { if (path.isNotEmpty() && path != SessionPrefs.GAME_STORAGE_OFF) { val problem = GameStorage.prepare(this, path) @@ -1585,7 +1503,6 @@ class MainActivity : ComponentActivity() { gameStorage = path } - /** The two masks as the dialog shows them; an empty stored list shows as every core ticked. */ private fun refreshCores() { clientOverride = SessionPrefs.clientCpusOverride(this) glThread = SessionPrefs.glThread(this) @@ -1626,17 +1543,11 @@ class MainActivity : ComponentActivity() { ?: SessionState.program?.substringAfterLast('/')?.substringBefore('.') ?: getString(R.string.main_running_program) else -> "Steam" } else null - // The libraries, off the main thread: manifests and a folder scan. val scanGeneration = ++libraryScanGeneration if (shortcutPicker) shortcutLibraryScanning = true Thread({ - // The emulators first: they need no walk of the games, and Desktop's dock is made of them. val emus = Library.emulators(this) { id -> DesktopCatalog.installed(this, id) != null } ui.post { if (scanGeneration == libraryScanGeneration) emulatorList = emus } - // Steam's own games next, from their manifests alone (quick), beside the added games - // already listed: a game Steam installed during the session shows as the session ends, - // without waiting for the added games' walk below. Its Windows components' list is - // queued then too, on the low-priority worker, so its page is usually ready. if (ready) { val quick = runCatching { Library.steamGames(this) }.getOrNull() if (quick != null) ui.post { @@ -1650,15 +1561,9 @@ class MainActivity : ComponentActivity() { } } } - // Then one update with the whole list: the wall places games by their position in it, - // so a partial list first would shuffle every capsule when the rest arrived. - // LibraryCache covers the wait. val games = if (ready) Library.launchableGames(this) else emptyList() val all = games.distinctBy { it.gameId } if (ready) com.droiddeck.launcher.frontend.LibraryCache.save(this, all) - // Once per app start: the Steam games' Windows components from Steam's lists. - // Queued only, after the launcher has been up a little; the worker takes them one at a time. - // And again after each session (a game installed in it, files changed). val endedSession = SessionState.sessionId.takeIf { !SessionState.running && it != null && it != componentSweepSession } if (ready && (!componentSweepDone || endedSession != null)) { componentSweepDone = true @@ -1672,8 +1577,6 @@ class MainActivity : ComponentActivity() { handleGameLink(all) } } - // Box art for the games that have none, fetched after the list is up; the list is - // rebuilt once if any was found. if (!OfflineMode.enabled(this) && CoverArt.fetchMissing(this, emus.flatMap { it.games })) { val refreshed = Library.emulators(this) { id -> DesktopCatalog.installed(this, id) != null } ui.post { emulatorList = refreshed } @@ -1685,7 +1588,6 @@ class MainActivity : ComponentActivity() { if (runtimeChangesBlocked()) return val release = available if (removalPending || installed != null && release?.version == installed) { - // Nothing to install: offer the one destructive thing this screen can do. showRemove = true return } @@ -1693,14 +1595,10 @@ class MainActivity : ComponentActivity() { Thread({ checkCatalog() }, "catalog").start() return } - // The runtime draws with Turnip, an Adreno driver: on Mali, Xclipse or PowerVR the - // compositor gets no usable Vulkan device and a session is sound over a black screen. - // Said before the download, not after it; the user may still go ahead. if (installed == null && !com.droiddeck.launcher.core.DeviceSupport.adreno()) { showNonAdreno = release; return } install(release) } - /** Starts a session; with no runtime on a non-Adreno, the same warning Setup gives comes first, before any download. */ private fun startSession(intent: Intent, steamSession: Boolean = false): Boolean { if (busy || LinuxRuntimeInstaller.isBusy()) return false refreshPhantomStatus() @@ -1759,7 +1657,6 @@ class MainActivity : ComponentActivity() { } } - /** Setup's Reset all settings; the activity is recreated so every screen reads the defaults (and the language) again. */ private fun resetSettings(gameProfiles: Boolean) { if (SessionState.running) return val scopes = steamGames.map { it.profileKey } @@ -1784,16 +1681,10 @@ class MainActivity : ComponentActivity() { } private fun install(release: LinuxRuntimeInstaller.Release) { - // The service keeps the process alive if the user switches away; this screen joins the - // same install (or starts it, if it gets there first) to show the progress. com.droiddeck.launcher.runtime.RuntimeInstallService.start(this, release) followRuntimeOperation { listener -> LinuxRuntimeInstaller.install(this, release, listener) } } - /** - * Shows a runtime operation's progress until it ends. [run] either starts one or joins the one already - * running (null: nothing was), which is how a launcher rebuilt mid-install picks it back up. - */ private fun followRuntimeOperation(run: (LinuxRuntimeInstaller.ProgressListener) -> Boolean?) { busy = true failed = false @@ -1823,7 +1714,6 @@ class MainActivity : ComponentActivity() { companion object { private const val TAG = "MainActivity" private const val MEDIA_SETTLE_MS = 1500L - /** What the picker offers for a driver zip; some file apps label a zip as a plain stream. */ private val ZIP_EXT = listOf("zip") private val WCP_EXT = listOf("wcp") } diff --git a/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt b/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt index e73babbc1..5bd651cf5 100644 --- a/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt @@ -9,11 +9,6 @@ import com.droiddeck.launcher.session.ProtonExtras import com.droiddeck.launcher.ui.ProtonRow import com.droiddeck.launcher.session.SessionState -/** - * The optional ARM64 Protons (GE-Proton, proton-cachyos, DroidDeck-Proton): the rows the page lists, - * an install or removal in progress, and whether a newer release is out. The launcher screen keeps - * one and shows it. - */ internal class ProtonMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) { var protonRows by mutableStateOf<List<ProtonRow>>(emptyList()) var protonBusyId by mutableStateOf<String?>(null) @@ -42,7 +37,6 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va }, "install-proton-$id").start() } - /** Removes the build in [dir], or with [older] every build of it but the newest. */ fun removeProton(id: String, dir: String? = null, older: Boolean = false) { val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return if (protonBusyId != null || SessionState.running) return @@ -66,7 +60,6 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va } } - /** Looks for newer releases, at most every few hours (see ProtonExtras.checkLatest); quiet offline. */ fun checkUpdates(force: Boolean = false) { if (!checking.compareAndSet(false, true)) return Thread({ diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index f584f69ba..b414cd672 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -110,17 +110,6 @@ import com.droiddeck.launcher.ui.sessionDrawerPages import com.droiddeck.launcher.ui.step import kotlin.math.abs -/** - * The session's screen: our Wayland compositor presenting onto this activity's Surface, and the - * input that reaches it. The session itself - gamescope, the Steam client, audio - belongs to - * [SessionService] and keeps running when this activity does not exist, which is what lets the - * user leave Big Picture for another app and come back to it still signed in and still - * downloading. - * - * Three layers: the SurfaceView the compositor draws into, the on-screen pad (a canvas View, - * since it is input rather than a menu), and one Compose layer on top for everything else - - * the HUD line, the loading overlay, the drawer and its dialogs. - */ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun attachBaseContext(newBase: android.content.Context) { super.attachBaseContext(newBase) @@ -130,7 +119,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private lateinit var pip: com.droiddeck.launcher.session.SessionPipController private var pipUi by mutableStateOf(false) private var pipAutoEnter by mutableStateOf(false) - /** Unfolded controls: the setting, and the Deck controls under the game while they show. */ private var unfoldedControls by mutableStateOf(false) private var unfoldedPanel: com.droiddeck.launcher.ui.DeckControlsPanel? = null private lateinit var sessionRoot: FrameLayout @@ -140,20 +128,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private lateinit var loading: LoadingState private lateinit var hud: PerfHud private var padBridge: PadBridge? = null - /** The handheld's gyro and accelerometer, fed to the Deck controller while the session shows. */ private var padMotion: com.droiddeck.launcher.input.PadMotion? = null - /** Between onResume and onPause. */ private var resumed = false private val deckPadListener: () -> Unit = { uiHandler.post { updatePadMotion(); openDeckRing("deck pad changed"); applyUnfoldedControls() } } - /** - * A Deck's controller is there from the moment it boots, and the client looks for it once, as - * it starts. The ring behind the Deck (libfakeinput's /dev/hidraw16) opens on the pad's first - * input otherwise - on a phone with nothing attached, the first touch of the on-screen - * controls, seconds after the client had looked, found no controller and never set the touch - * mode, so its interface took a finger as a mouse. Opened as soon as the session settles on a - * Deck instead. - */ private fun openDeckRing(why: String) { val deck = SessionState.deckPad val opened = if (deck) padBridge?.start() else null @@ -164,23 +142,16 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }) } - /** Motion is read while the session shows and only if the pad is a Deck controller - which - * the service can decide after this activity has resumed (SessionState.deckPadListener). */ private fun updatePadMotion() { - // A Steam Controller brings its own gyro (SteamControllerBle); the phone's would fight it. if (resumed && !pipUi && SessionState.deckPad && !SteamControllerBle.connected) padMotion?.start() else padMotion?.stop() } private var steamController: SteamControllerBle? = null private var onScreenControls: OnScreenControls? = null - /** Steam's touch controls (SteamTouchControls), made once the session offers the device. */ private var steamTouchControls: com.droiddeck.launcher.input.SteamTouchControls? = null private var steamTouchAvailable by mutableStateOf(false) private val steamTouchRetry = Runnable { updateOnScreenControls() } - /** While Steam's touch controls are up: how the game's touch config says touches outside them - * act (Steam Link's input and mouse modes), in place of the drawer's touch mode. */ private var steamTouchOptions: com.droiddeck.launcher.input.SteamTouchConfig.Options? = null - /** The touch mode touches are routed by: the drawer's, or the game's while Steam's controls are up. */ private var routedTouchMode = SessionPrefs.TOUCH_AUTO private var keyboard: KeyboardHost? = null private var controllerSettings by mutableStateOf<ControllerPrefs.Settings?>(null) @@ -194,22 +165,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private var touchMode by mutableStateOf(SessionPrefs.TOUCH_AUTO) private var cursorPos by mutableStateOf(androidx.compose.ui.geometry.Offset(-100f, -100f)) private var cursorVisible by mutableStateOf(false) - /** The blue the front end flooded to on the way in; the loading screen opens on it (LaunchFlood). */ private var floodColor by mutableStateOf<Int?>(null) private val cursorHide = Runnable { cursorVisible = false } - /** When the pad or the on-screen controls were last used (uptimeMillis); see [showCursor]. */ private var lastPadInputMs = 0L - // The program's own pointer, from wl_pointer.set_cursor (see syncClientCursor): its image in - // session pixels, hotspot, and whether it asked for no pointer at all. private val cursorBuf = IntArray(WaylandCompositor.CURSOR_BUF_INTS) private var cursorSerial = 0 private var clientHidesCursor = false - /** - * A program holds a pointer lock (zwp_locked_pointer_v1: a mouse-look game, through gamescope). - * While it does, a physical mouse is captured and its raw deltas go to the program as relative - * motion. Without capture the deltas are worked out from Android's pointer position, which stops - * at the screen edge, and so did the camera (#153). - */ private var pointerLocked = false private val pointerLockListener = WaylandCompositor.PointerLockListener { locked, _, _ -> uiHandler.post { @@ -223,7 +184,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private var cursorHotX by mutableStateOf(0) private var cursorHotY by mutableStateOf(0) private var cursorImageScale by mutableStateOf(1f) - /** labwc changes the shape a moment after the move that reached an edge: look again then. */ private val cursorResync = Runnable { if (syncClientCursor() && clientHidesCursor) cursorVisible = false } private val uiHandler = Handler(Looper.getMainLooper()) private var pendingBackAction: Runnable? = null @@ -254,17 +214,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } private val resumeKeysDown = mutableSetOf<Pair<Int, Int>>() - // Compose reads these; the activity writes them. private var drawerOpen by mutableStateOf(false) - /** A stop growing out of the dialog's Stop (overlay px); the session ends once it covers the screen. */ private var quitFrom by mutableStateOf<androidx.compose.ui.geometry.Rect?>(null) - /** Left behind that flood: the front end opens on its blue, so no sink of our own. */ private var quitFlooded = false - /** Leaving behind a flood spreading out of the throbber (FloodSpread); see finish(). */ private var leaving by mutableStateOf(false) - /** Close without the leaving flood: a retry opens a fresh session at once. */ private var closeAtOnce = false - /** The drawer's Components tab: the Protons as last read (ComponentsManager). */ private var drawerComponents by mutableStateOf<ComponentsManager.Snapshot?>(null) private val drawerComponentMenu by lazy { ComponentsMenu(this, uiHandler) } private var drawerGameProfileId by mutableStateOf<Long?>(null) @@ -289,24 +243,18 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private var drawerPage by mutableStateOf(SessionDrawerPage.CONTROLLER) private var drawerControllerActive by mutableStateOf(false) private var backActionsInverted by mutableStateOf(false) - /** What the pad is to the Steam client (SessionPrefs.CONTROLLER_*), switched from the drawer. */ private var padType by mutableStateOf(SessionPrefs.CONTROLLER_DECK) - /** The on-screen PC keyboard (ui/PcKeyboard): real key presses, Esc and F1 included. */ private var pcKeyboardOpen by mutableStateOf(false) - /** The game in front, when its source has achievements. */ private var achievementsInFront by mutableStateOf<AchievementsInFront?>(null) private var achievementsOpen by mutableStateOf(false) private var hudOn by mutableStateOf(true) private var upscaler by mutableStateOf(0) private var upscaleSharpness by mutableStateOf(75) private var effects by mutableStateOf(ScreenEffects.OFF) - /** Cardboard mode (gpu/Cardboard) and the sensor that turns the virtual screen with the head. */ private var cardboard by mutableStateOf(Cardboard.OFF) private var headTracker: HeadTracker? = null private var steamVrStatus by mutableStateOf<SteamVr.Status?>(null) - /** The picture was switched to side-by-side for SteamVR and goes back when it stops. */ private var steamVrSwitchedPicture by mutableStateOf(false) - /** SteamVR has been seen running since the switch: only then does "not running" mean it stopped. */ private var steamVrSeenRunning = false private val hasRotationSensor by lazy { (getSystemService(SENSOR_SERVICE) as android.hardware.SensorManager).let { @@ -314,8 +262,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { ?: it.getDefaultSensor(android.hardware.Sensor.TYPE_ROTATION_VECTOR) } != null } - // Held in a field: SharedPreferences keeps its listeners weakly. A change from anywhere (the - // drawer, tools/droiddeckctl prefs set) applies live. private val cardboardPrefListener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key -> if (key == SessionPrefs.CARDBOARD_KEY) uiHandler.post { applyCardboard(SessionPrefs.cardboard(this)) } } @@ -336,7 +282,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private var secondScreenMode by mutableStateOf(SessionState.secondScreenMode) private var secondScreenDisplays by mutableStateOf<List<SecondScreenDisplay>>(emptyList()) private var selectedSecondScreenDisplay by mutableStateOf(SessionState.secondScreenDisplay) - /** Between onStart and onStop: while stopped the second screen stays closed, its choice kept. */ private var started = false private lateinit var displayManager: DisplayManager private var secondScreenPresentation: SecondScreenPresentation? = null @@ -349,12 +294,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** - * The panel's rate, followed for the rest of the session. It is read once at start, and a - * panel that drops from 120 to 60 Hz later (battery saver, heat, a vendor overriding the mode - * asked for) left the compositor telling gamescope every frame took 8.3 ms, so the client and - * its games paced against a vblank that no longer came. (WinNative 53836ca9.) - */ private fun followPanelRefresh(displayId: Int) { if (!CompositorHost.isStarted) return val display = (if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay) ?: return @@ -365,11 +304,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private var isHomeApp by mutableStateOf(false) private var androidApps by mutableStateOf<List<HomeApp.LaunchableApp>>(emptyList()) - /** - * Shows the on-screen pad when nothing is plugged in and takes it away the moment something - * is - a user with a controller in their hands should not be looking at buttons they cannot - * press, and a user without one must not be left with no way to answer Big Picture. - */ private val deviceListener = object : InputManager.InputDeviceListener { override fun onInputDeviceAdded(deviceId: Int) { logInputDevice("connected", deviceId) @@ -399,7 +333,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } if (!SessionState.running && SessionState.phase in setOf(SessionPhase.IDLE, SessionPhase.FAILED)) { val mode = intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM - // A Flatpak app or AppImage is named by its arguments, which the name lookup reads here. SessionState.programArgs = intent.getStringArrayExtra(SessionService.EXTRA_PROGRAM_ARGS)?.toList().orEmpty() SessionEvents.begin(this, mode, SessionPaths.label(mode, intent.getStringExtra(SessionService.EXTRA_PROGRAM))) } @@ -413,9 +346,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { SessionEvents.record("agent.start_requested", mapOf("mode" to SessionState.mode)) } window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - // Game-tier power policy for the whole session: the panel's fastest mode (one the frame cap - // divides, when there is a cap), and the OS told it is in gameplay. Logged so a slow device - // says why. The cap is the session's: an activity re-created mid-session keeps it. if (!SessionState.running) { SessionState.fpsLimit = SessionPrefs.fpsLimit( this, SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM), @@ -425,18 +355,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (!SessionState.running) SessionState.followScreen = followChoice() applyFollowOrientation(resources.configuration) goFullscreen() - // The device's volume keys change the stream the session plays on (the relay and - // PulseAudio are media playback); they are never forwarded to the guest. volumeControlStream = android.media.AudioManager.STREAM_MUSIC displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager refreshSecondScreenDisplays() - // No runtime is not a reason to leave: the loading screen installs it (installThenStart, - // below) and the session starts when it is in. if (!LinuxRuntime.isInstalled(this)) Log.i(TAG, "the Linux runtime is not installed; the loading screen installs it") - // Captured mouse events go down the focus chain, and focus sits somewhere in the overlay: - // the root sees every one of them whichever view that is. val root = object : FrameLayout(this) { override fun dispatchCapturedPointerEvent(event: MotionEvent): Boolean = onCapturedPointer(event) || super.dispatchCapturedPointerEvent(event) @@ -449,7 +373,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { surfaceView = SurfaceView(this) surfaceView.holder.addCallback(this) root.addView(surfaceView) - // The window's size is only known once laid out, and changes as the phone folds or turns. root.addOnLayoutChangeListener { _, l, t, r, b, ol, ot, or, ob -> if (r - l != or - ol || b - t != ob - ot) uiHandler.post { applyUnfoldedControls() } } @@ -458,7 +381,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { bridge.setSelectSteam(ControllerPrefs.read(this).selectSteam) padBridge = bridge SessionState.padBridge = bridge - // A paired Steam Controller is read over Bluetooth LE, out of lizard mode, as Steam Link reads it. SteamControllerBle.listener = { uiHandler.post { updatePadMotion(); updateOnScreenControls() } } steamController = SteamControllerBle(this).also { it.start() } padMotion = com.droiddeck.launcher.input.PadMotion(this) { @@ -467,8 +389,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } SessionState.deckPadListener = deckPadListener openDeckRing("activity created") - // A player on the pad or the on-screen controls has no use for the mouse arrow; the next - // touchpad or mouse move brings it back (showCursor), once the pad has been quiet a moment. bridge.setOnPlayerInput { lastPadInputMs = android.os.SystemClock.uptimeMillis() uiHandler.removeCallbacks(cursorHide) @@ -477,8 +397,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { onScreenControls = OnScreenControls(this, bridge, onKeyboard = ::togglePcKeyboard).also { root.addView(it) } keyboard = KeyboardHost(this).also { root.addView(it) } touchpad = TouchpadGestures(PointerGestures.slop(this), pointerListener) - // One arrow, ours: Android draws a system pointer for a mouse over any window, and the - // session already draws the pointer it is sent. val noCursor = android.view.PointerIcon.getSystemIcon(this, android.view.PointerIcon.TYPE_NULL) root.pointerIcon = noCursor surfaceView.pointerIcon = noCursor @@ -496,7 +414,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { hud.onPresentingWindowChanged = { if (FrameGen.engine(this) != FrameGen.ENGINE_OFF) CompositorHost.rearmFrameGen { applyFrameGen() } } - // A session that has already drawn is past its milestones; do not cover its picture. if (SessionState.running && SessionState.firstFrameSeen) { loading.visible = false hud.start() @@ -520,12 +437,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { WaylandCompositor.setPointerLockListener(pointerLockListener) SessionEvents.markReadyIfPossible() SessionState.endListener = endListener - // A single Back opens the session menu; two quick presses/swipes send the Steam QAM chord. - // The single action waits out the double-press window so the two actions stay distinct. onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { if (pipUi) return - // The PC keyboard closes first (B on a controller), then Back is the drawer again. when { ::loading.isInitialized && loading.ended -> finish() drawerOpen -> drawerOpen = false @@ -538,18 +452,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { watchSession() } - /** - * One Compose layer for every menu and overlay. Touches nothing in it consumes fall through to - * the pad and the game underneath. - */ private fun createSessionOverlay(): ComposeView = ComposeView(this).apply { isFocusable = true isFocusableInTouchMode = true - // Compose draws each control's focus ring. Android's default highlight would cover - // this full-screen host and remain as a white veil after the drawer closes. defaultFocusHighlightEnabled = false setContent { - // The drawer, the PC keyboard and picture in picture take the mouse back. androidx.compose.runtime.LaunchedEffect(drawerOpen, pcKeyboardOpen, achievementsOpen, pipUi) { updatePointerCapture() } DroidDeckTheme { if (pipUi) { @@ -569,7 +476,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { loading.step, loading.percent, loading.elapsed, loading.hint, loading.ended, topic = loading.topic, readable = loading.readable, title = loadingTitle(), steam = loadingMode() == SessionService.MODE_STEAM, - // As the drawer's Stop does: the service stops, and a start still installing is cancelled. onCancel = { SessionService.stop(this@SessionActivity); finish() }, endedDetail = loading.endedDetail, onRetry = { retrySession() }, @@ -581,7 +487,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { window.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(android.graphics.Color.BLACK)) }, ) - // Opening the drawer takes the controller away from the game: release its pad. androidx.compose.runtime.LaunchedEffect(drawerOpen) { if (drawerOpen) { syncLossless() @@ -593,9 +498,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { releaseDrawerDirection() } } - // SteamVR's state for the Cardboard group, refreshed while the drawer shows it. - // Watched for as long as Cardboard mode is on (or the picture is switched for it), drawer open or - // not: however SteamVR is started or quit, the picture follows it. LaunchedEffect(cardboard.enabled, steamVrSwitchedPicture) { while (cardboard.enabled || steamVrSwitchedPicture) { val now = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { @@ -614,7 +516,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { achievementsInFront = found if (found == null) achievementsOpen = false } - // The PC keyboard takes it too, for moving over the keys; the drawer opens over it. androidx.compose.runtime.LaunchedEffect(pcKeyboardOpen) { if (pcKeyboardOpen) padBridge?.releaseAll() } if (pcKeyboardOpen) com.droiddeck.launcher.ui.PcKeyboard( sendKey = { code, down -> if (CompositorHost.isStarted) sendGuestKey(code, down) }, @@ -651,7 +552,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { onUpscaler = { m -> SessionPrefs.setUpscaler(this@SessionActivity, m); upscaler = m WaylandCompositor.nativeSetUpscaler(m) - // Two Looks carry a scaling mode of their own, so the mode decides which one the log names. WaylandCompositor.nativeSetLookName(ScreenEffectLooks.match(effects, m)?.name) }, onUpscaleSharpness = { pct -> @@ -700,7 +600,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { onQam = if (SessionState.mode == SessionService.MODE_STEAM) ({ triggerSteamQam() }) else null, onAchievements = achievementsInFront?.let { { drawerOpen = false; achievementsOpen = true } }, achievementsProgress = achievementsInFront?.progress, - // Only where the session set the pad up to be switched (SessionService: deckPad). padType = if (SessionState.mode == SessionService.MODE_STEAM && SessionState.deckPad) padType else null, onPadType = { id -> SessionPrefs.setSteamController(this@SessionActivity, id) @@ -806,7 +705,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { onComponentSwap = { pid, comp, value -> swapDrawerComponent(pid, comp, value) }, onGameComponent = ::setDrawerGameComponent, )) - // The panel takes the pad, as the PC keyboard does. LaunchedEffect(achievementsOpen) { if (achievementsOpen) { padBridge?.releaseAll() @@ -860,7 +758,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private fun closeAchievements() { achievementsOpen = false } - /** A live game window only; Steam's overlay may be in front of that window. */ private fun resolveAchievementsInFront(): AchievementsInFront? { if (!SessionState.running || SessionState.mode != SessionService.MODE_STEAM) return null val snapshot = AgentGuest.focus(this) ?: return null @@ -905,7 +802,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { performBackAction(double = false) } pendingBackAction = pending - // Wait 500 ms for a second Back press before performing the single-press action. uiHandler.postDelayed(pending, 500L) } @@ -923,7 +819,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } - /** Re-reads the Protons for the drawer's Components tab, first running swaps a closed game was holding back. */ private fun refreshDrawerComponents() { Thread({ val applied = runCatching { ComponentsManager.applyQueued(this) }.getOrDefault(emptyList()) @@ -1045,7 +940,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val build = snap.protons.firstOrNull { it.proton.id == protonId }?.proton?.version?.let(ComponentsManager::safeName) val current = active ?: "orig:$build" if (queued != null && value == current) { - // Picking what is already in place again cancels the swap waiting for the game. ComponentsManager.cancelQueued(this, protonId, comp); getString(R.string.comp_swap_cancelled) } else if (value.startsWith("orig:")) ComponentsManager.restore(this, protonId, comp, value.removePrefix("orig:")) else ComponentsManager.swap(this, protonId, value) @@ -1114,7 +1008,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, "drawer-game-no-steam").start() } - /** The session this screen is bringing up: the live one when re-attached, else the intent's. */ private fun loadingMode(): String = if (SessionState.running) SessionState.mode else intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM @@ -1127,8 +1020,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } private fun loadingTitle(): String = when (loadingMode()) { - // Also when the desktop was asked for with Steam (Big Picture's Switch to Desktop): the client - // does not open on it. SessionService.MODE_DESKTOP -> getString(R.string.session_starting_desktop) SessionService.MODE_RUN -> { val program = if (SessionState.running) SessionState.program else intent.getStringExtra(SessionService.EXTRA_PROGRAM) @@ -1139,8 +1030,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } private fun shareCurrentSessionLogs() { - // Once a session has ended its folder has been handed on (SessionPaths.take), and the - // ended screen's Share logs is for that one. val folder = SessionPaths.current() ?: SessionPaths.lastEnded() if (folder == null || !folder.isDirectory) { Toast.makeText(this, R.string.session_no_logs, Toast.LENGTH_LONG).show() @@ -1167,8 +1056,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { upscaleSharpness = SessionPrefs.upscaleSharpness(this) effects = SessionPrefs.screenEffects(this) applyCardboard(SessionPrefs.cardboard(this)) - // A switch left from before this activity (or this resume): SteamVR was running then, so the next - // "not running" hands the picture back. if (SessionPrefs.cardboardSourceBeforeSteamVr(this) >= 0 && !steamVrSwitchedPicture) { steamVrSwitchedPicture = true steamVrSeenRunning = true @@ -1197,21 +1084,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { padType = SessionPrefs.steamController(this) } - // ── Compositor ────────────────────────────────────────────────────────────────────────── - /** True while the loading screen is installing the Linux runtime or the desktop; the session waits for it. */ @Volatile private var installingRuntime = false - /** - * First Play (or Desktop) on a fresh install: the runtime, and for the desktop its package, - * are downloaded and unpacked here, on the loading screen's own line and bar, and the session - * starts when they are in. Nothing else changes. - * - * The first Steam session also gets Valve's ARM64 Proton here, before the client has ever - * started, so the compatibility tool is installed and the default at sign-in with no install - * dialog, download or client restart. Not being able to fetch it stops nothing: the session - * then asks the client for it after sign-in, as it always has. - */ private fun installThenStart(runtime: Boolean, desktop: Boolean, proton: Boolean = false) { if (SessionState.stopRequested) return installingRuntime = true @@ -1264,7 +1139,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } loading.percent = -1 loading.say(getString(R.string.session_starting_session), LoadingState.Topic.SESSION, readable = false) - // The surface may have come and gone while the download ran; start on the live one. if (surfaceView.holder.surface?.isValid == true) surfaceCreated(surfaceView.holder) } }, "runtime-install").start() @@ -1279,16 +1153,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, "session-failure-artifacts").start() } - /** - * The loading screen's lines for one package: downloading it (with and without its size), checking it, - * unpacking it; the checklist [topic] they belong to, and whether the checking line is shown as it is. - */ private class PackageLines( val downloading: Int, val downloadingMb: Int, val checking: Int, val unpacking: Int, val topic: LoadingState.Topic, val checkingReadable: Boolean, ) - /** Reports one package's download on the loading screen: "<what> · 332 of 791 MB", checking, unpacking. */ private fun progressFor(what: PackageLines, mb: Long) = object : com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.ProgressListener { override fun onProgress(stage: String, percent: Int) = onProgress(com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.Step.OTHER, stage, percent) @@ -1309,7 +1178,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** Null when the runtime is in, else the loading screen's closing line. */ private fun installRuntime(): String? { val release = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.fetchRelease() ?: return getString(R.string.session_runtime_catalog_unreachable) @@ -1318,7 +1186,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return if (ok) null else getString(R.string.session_runtime_install_failed) } - /** Null when the desktop package is in, else the loading screen's closing line. */ private fun installDesktop(): String? { uiHandler.post { loading.percent = -1; loading.say(getString(DESKTOP_LINES.downloading), DESKTOP_LINES.topic, readable = true) } val entry = com.droiddeck.launcher.runtime.DesktopCatalog.desktopEntry(this) @@ -1327,7 +1194,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return problem?.let { getString(R.string.session_desktop_install_failed, it) } } - /** Null when the ARM64 Proton is in, else why not; the session goes on either way. */ private fun installProton(): String? { uiHandler.post { loading.percent = -1; loading.say(getString(PROTON_LINES.downloading), PROTON_LINES.topic, readable = true) } val catalog = com.droiddeck.launcher.runtime.DesktopCatalog @@ -1342,22 +1208,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { SessionEvents.transition(SessionPhase.STARTING_COMPOSITOR, "compositor.starting") } val runtimeDir = File(filesDir, ".wayland-rt").apply { mkdirs() } - // The compositor hands this keymap to wl_keyboard clients, which is how the guest reads - // the evdev codes we inject. FileUtils.copyAsset(this, "wayland/keymap.xkb", File(runtimeDir, "keymap.xkb")) - // Turnip, not the system Adreno driver: importing the dma-bufs gamescope commits needs - // VK_EXT_image_drm_format_modifier, which the system driver does not implement. val turnip = TurnipDriver(this) com.droiddeck.launcher.gpu.DriverSetupLog.write(this, "surface.driver-setup") val driverId = if (CompositorHost.isStarted) null else turnip.install() - // The output size belongs to the session, not to the Surface: gamescope's display is - // sized once when the session starts and cannot change. A foldable recreates the Surface - // on the other panel, and recomputing the size there told the compositor a 21:9 buffer - // was 16:9 - the picture came back squashed sideways. While a session runs, keep its size. - // Follow screen is the exception: there the compositor tells gamescope the new size, so the - // session takes the shape of the Surface it now has. val previous = SessionState.outputSize val live = if (SessionState.followScreen && !isInPictureInPictureMode) com.droiddeck.launcher.session.SessionDisplay.followSize(holder.surfaceFrame.width(), holder.surfaceFrame.height()) @@ -1365,25 +1221,18 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val size = live ?: if (SessionState.running) previous else outputSize() SessionState.outputSize = size if (!SessionState.running) SessionState.refreshHz = refreshHz() - // How far the panel enlarges the session, for texture sharpness "Auto" (TextureFiltering): - // the game environment is published again so the next launch derives it from this session. val panel = panelBounds() SessionState.upscaleRatio = maxOf(panel.width(), panel.height()).toFloat() / maxOf(size.first, size.second).coerceAtLeast(1) Thread({ runCatching { GameEnvironmentStore.publish(this) } .onFailure { Log.e(TAG, "Could not update game environment", it) } }, "game-env-publish").start() - // Letterbox, never stretch or crop: the output can be a different shape from the panel, - // and a game's picture must keep its proportions with bars, not lose its edges. WaylandCompositor.nativeSetScaleMode(SCALE_FIT, ALIGN_CENTER) WaylandCompositor.nativeSetUpscaler(SessionPrefs.upscaler(this)) WaylandCompositor.nativeSetUpscaleSharpness(SessionPrefs.upscaleSharpness(this)) SessionPrefs.screenEffects(this).push(SessionPrefs.upscaler(this)) panelSizeMm().let { (w, h) -> SessionPrefs.cardboard(this).push(w, h) } WaylandCompositor.nativeSetCardboardPoseFile(File(filesDir, ".wayland-rt/droiddeck-headpose").path) - // The session's folder, claimed here because the compositor starts before the service and - // opens its log once. The compositor reads the path from its environment; setting it after - // it has started changes nothing, which is why the service copies the file in at teardown. if (!SessionState.running) { val waylandLog = File(SessionPaths.beginOrCurrent(this), "wayland.log") WaylandCompositor.setSessionLogFile(waylandLog) @@ -1393,9 +1242,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { Log.w(TAG, "could not point the compositor's log at $waylandLog", e) } } - // HDR10: the gate is decided once, when the compositor starts (it lives for the whole app - // process), from the panel's own word and the mode's setting. Zero-copy presentation is - // what puts an HDR frame on a display layer tagged BT2020_PQ, so it is turned on with it. if (!CompositorHost.isStarted) { val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM) val probe = HdrSupport.probe(this) @@ -1429,10 +1275,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { WaylandCompositor.nativeResizeOutput(size.first, size.second) } if (!SessionState.running) SessionEvents.record("compositor.started") - // The service owns everything below the compositor. It is started whenever no session is - // running - NOT only when the compositor was just started: the compositor lives for the - // whole process, so the second Play after a session ended used to re-attach the Surface, - // start nothing, and leave the loading panel counting up over a dead session. if (!SessionState.running) { SessionEvents.transition(SessionPhase.STARTING_GUEST, "guest.starting") SessionService.start( @@ -1460,12 +1302,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** - * Follow screen: the session's display takes the window's new size (a Fold opening onto its - * inner panel, or closing onto the cover), so the picture fills the panel instead of being - * letterboxed onto it. Not in picture-in-picture: that window is a preview of the session, - * and resizing the guest to it would leave Steam laid out for a thumbnail. - */ private fun followWindow(width: Int, height: Int) { if (!SessionState.followScreen || isInPictureInPictureMode || !CompositorHost.isStarted) return val size = com.droiddeck.launcher.session.SessionDisplay.followSize(width, height) ?: return @@ -1477,18 +1313,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { WaylandCompositor.nativeResizeOutput(size.first, size.second) } - /** Whether this session's resolution is Follow screen; decided once, when it starts. */ private fun followChoice(): Boolean { val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM) return SessionPrefs.resolutionChoice(this, mode, com.droiddeck.launcher.session.SessionDisplay.panelSize(this)) == com.droiddeck.launcher.session.SessionDisplay.FOLLOW_SCREEN } - /** - * Follow screen on a large panel (a Fold's inner screen) may turn with the device: a window - * locked to landscape on a squarer panel is letterboxed by the system itself, which no output - * size can undo. Every other case keeps the landscape orientation picked in Setup. - */ private fun applyFollowOrientation(config: android.content.res.Configuration) { val wanted = if (SessionState.followScreen && config.smallestScreenWidthDp >= 600) { android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER @@ -1510,11 +1340,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { CompositorHost.detach(holder.surface) } - /** - * The size the session renders at. gamescope is told this and scales its output onto whatever - * the panel is, so a 1440p phone can run the client at 1080p without the client knowing. - */ - /** The panel, not the window: resources.displayMetrics is what is left after the system bars and the cutout are taken out. */ private fun panelBounds(): android.graphics.Rect = if (Build.VERSION.SDK_INT >= 30) { windowManager.maximumWindowMetrics.bounds } else { @@ -1528,7 +1353,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM) val panel = com.droiddeck.launcher.session.SessionDisplay.panelSize(this) val program = intent.getStringExtra(SessionService.EXTRA_PROGRAM) - // Lightweight emulators keep their panel-sized default until a resolution is chosen. val choice = if (intent.getStringExtra(SessionService.EXTRA_MODE) == SessionService.MODE_RUN && Library.drawsAtPanel(program) && !SessionPrefs.resolutionChosen(this, mode)) com.droiddeck.launcher.session.SessionDisplay.MATCH_SCREEN @@ -1536,13 +1360,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return com.droiddeck.launcher.session.SessionDisplay.resolveChoice(panel, choice) } - /** - * The rate of the mode PerfMode asked for, not the one the panel is still in: the switch lands - * some frames after onCreate, and read too early it gave gamescope `-r 60` on a 120 Hz panel - - * the client and every game then paced themselves to 60. (WinNative's requestedPanelRefreshRate.) - * Never above the user's peak refresh rate, which Android holds the panel to whatever mode is - * active; it is the rate gamescope gives Steam, and the QAM's Frame Limit divides it. - */ private fun refreshHz(): Float { val display = if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay val wanted = window.attributes.preferredDisplayModeId @@ -1552,10 +1369,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return if (capped > 1f) capped else 60f } - /** - * The saved frame-generation setting, pushed to the compositor. Off the main thread: LSFG's - * first use translates the shader chain out of Lossless.dll, which takes seconds. - */ private fun applyFrameGen() { val hz = refreshHz() Thread({ @@ -1568,7 +1381,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, "frame-gen").start() } - /** A Lossless Scaling installed or updated in Steam during the session is taken when the drawer opens. */ private fun syncLossless() { Thread({ Lossless.sync(this) @@ -1589,9 +1401,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, "lossless-import").start() } - // ── Session state ─────────────────────────────────────────────────────────────────────── - /** Keeps the loading overlay current from the session log, and its clock moving. */ private fun watchSession() { val handler = Handler(Looper.getMainLooper()) var ticks = 0 @@ -1625,10 +1435,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (status == 0) { runOnUiThread { if (isFinishing || isDestroyed) return@runOnUiThread - // A session asked for in this one's place (the desktop's Steam launchers or - // Steam's Switch to Desktop) starts here: this activity restarts on its intent, as an agent - // start does. startActivity(next) could not do it - the activity is singleTop, so - // the request landed in this instance's onNewIntent and went with its finish(). SessionState.relaunch?.let { next -> SessionState.relaunch = null next.putExtra(EXTRA_RETURN_HOME, intent.getBooleanExtra(EXTRA_RETURN_HOME, false)) @@ -1640,7 +1446,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } return } - // What the log says about why, read off the main thread (notifyEnded arrives on it). Thread({ val hint = sessionEndHint(SessionState.logFile) runOnUiThread { @@ -1655,7 +1460,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { finish() return } - // It stays until the user leaves: a failure read against a timer is a failure not read. val stopped = when (SessionState.mode) { SessionService.MODE_RUN -> com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?.let { getString(R.string.session_ended_named, it) } ?: getString(R.string.session_ended_program) @@ -1669,14 +1473,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { focusEndedScreen() } - /** The ended screen's buttons take the pad: it is no longer driving a game. */ private fun focusEndedScreen() { drawerOpen = false padBridge?.releaseAll() uiHandler.post { sessionOverlay.requestFocus() } } - /** The same start again, in a fresh activity: the ended one keeps no session to reuse. */ private fun retrySession() { val again = Intent(intent) closeAtOnce = true @@ -1684,13 +1486,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { startActivity(again) } - /** - * One line of advice for a failure the log identifies, or null. The one this recognises: the - * ENOSYS storm - dozens of `socket(): Function not implemented` / `shared memfd open() failed` - * lines - which is proot's seccomp acceleration failing an x86 helper (Steam's xalia under - * FEX) on some devices (a Fold 5, twice). The switch that answers it is in Performance, and a - * user who never opens the log would not know. - */ private class EndHint(val text: String, val evenOnSuccess: Boolean = false) private fun sessionEndHint(log: File?): EndHint? { @@ -1698,7 +1493,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return try { var enosys = 0 var fexMissing = false - // The tail is where a dying session says why; 512 KB covers the storm without reading a 1 GB log. val size = log.length() java.io.RandomAccessFile(log, "r").use { f -> val start = maxOf(0L, size - 512L * 1024) @@ -1722,11 +1516,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - // ── Input ─────────────────────────────────────────────────────────────────────────────── override fun dispatchKeyEvent(event: KeyEvent): Boolean { - // The device's own volume keys belong to Android: forwarded to the guest as keys they - // changed nothing anyone could hear (a Pocket FIT report, on the desktop). when (event.keyCode) { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE -> return super.dispatchKeyEvent(event) @@ -1737,7 +1528,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (drawerOpen && !drawerControllerActive) sessionOverlay.requestFocus() drawerControllerActive = true } - // While the session starts, B is the loading screen's Cancel, as its corner says. if (fromController && event.keyCode == KeyEvent.KEYCODE_BUTTON_B && ::loading.isInitialized && loading.visible && !loading.ended && !drawerOpen) { if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) { SessionService.stop(this) @@ -1745,7 +1535,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } return true } - // The ended screen is a page of buttons: the pad drives them, and B leaves. if (::loading.isInitialized && loading.ended) { if (event.keyCode == KeyEvent.KEYCODE_BUTTON_B || event.keyCode == KeyEvent.KEYCODE_BACK) { if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) finish() @@ -1786,27 +1575,16 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } if ((pcKeyboardOpen || achievementsOpen) && fromController) return super.dispatchKeyEvent(event) if (event.keyCode != KeyEvent.KEYCODE_BACK && padBridge?.onKeyEvent(event) == true) return true - // A hardware keyboard, forwarded to the compositor's wl_keyboard. Back is left to the - // activity, which opens the drawer. if (CompositorHost.isStarted && event.keyCode != KeyEvent.KEYCODE_BACK && !fromController) { val down = event.action == KeyEvent.ACTION_DOWN if (down || event.action == KeyEvent.ACTION_UP) { var evdev = EvdevKeys.fromKeyCode(event.keyCode) - // What the key stands for, and the key it would sit on without Shift - non-zero - // only for a character Shift puts there: every capital and the symbol row. val ch = event.unicodeChar val plain = if (ch > 0) EvdevKeys.unshiftedChar(ch) else 0 - // A soft keyboard's symbol keys are in neither the table nor a scan code, so they - // reached the session as nothing at all - a sign-in took an address without its @. - // Work back from the character instead: which key carries it. if (evdev <= 0 && ch > 0) { val code = EvdevKeys.fromKeyCode(EvdevKeys.keycodeForChar(if (plain != 0) plain else ch)) if (code > 0) evdev = code } - // The modifier has to be made here. A soft keyboard reports Shift in the event's - // meta state and sends no Shift key of its own, so a capital arrives as a key this - // side already knows and came out lowercase. A hardware keyboard sends its own - // Shift, and a second one would release the modifier while the key is still held. val shiftEvdev = if (evdev > 0 && plain != 0 && event.deviceId <= 0) 42 else 0 if (evdev <= 0 && event.scanCode > 0) evdev = event.scanCode if (evdev > 0) { @@ -1843,7 +1621,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return super.dispatchGenericMotionEvent(event) } - /** With no view focused, captured mouse events skip the root and arrive here as trackball events. */ override fun dispatchTrackballEvent(event: MotionEvent): Boolean = onCapturedPointer(event) || super.dispatchTrackballEvent(event) @@ -1895,7 +1672,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { uiHandler.postDelayed(drawerDirectionRepeat, drawerFirstRepeatDelayMs) } - /** Use Android's per-device flat region, then rescale the remaining stick travel to 0..1. */ private fun centeredStickAxis(event: MotionEvent, axis: Int): Float { val value = event.getAxisValue(axis) val flat = (event.device?.getMotionRange(axis, event.source)?.flat ?: 0f) @@ -1948,17 +1724,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { showCursor(cx, cy) } - /** - * The arrow stays on a desktop; in a Steam session it shows for a moment after each move. While - * a controller is driving (input in the last [CURSOR_PAD_HOLD_MS]) it stays hidden, so a touch - * during play does not flash it up - Bannerlator's rule for its Wayland pointer. - */ private fun showCursor(x: Float, y: Float) { cursorPos = androidx.compose.ui.geometry.Offset(x, y) syncClientCursor() uiHandler.removeCallbacks(cursorResync) uiHandler.postDelayed(cursorResync, 60) - // A program that asked for no pointer (a mouse-look game) wins over everything below. if (clientHidesCursor) { cursorVisible = false; return } if (android.os.SystemClock.uptimeMillis() - lastPadInputMs < CURSOR_PAD_HOLD_MS) return cursorVisible = true @@ -1966,14 +1736,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (SessionState.mode == SessionService.MODE_STEAM && SessionState.steamUi != "desktop") uiHandler.postDelayed(cursorHide, 2500) } - /** - * Pulls the program's pointer (wl_pointer.set_cursor) if it changed: labwc's resize arrows at a - * window's edges and corners, a text field's I-beam, or a request for no pointer. Serial 0 = no - * program has set one yet, and the built-in arrow stays. Returns true when something changed. - */ private fun syncClientCursor(): Boolean { if (!CompositorHost.isStarted) return false - // Asked on every pointer move; the image is copied only when the client has changed it. if (WaylandCompositor.nativeCursorSerial() == cursorSerial) return false val n = WaylandCompositor.nativeCursorSnapshot(cursorBuf) if (n < 6) return false @@ -1994,7 +1758,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { cursorImage = bitmap.asImageBitmap() cursorHotX = cursorBuf[4] cursorHotY = cursorBuf[5] - // Session pixels -> screen pixels, as the session's picture is scaled (movePointer). val width = surfaceView.width.takeIf { it > 0 } val height = surfaceView.height.takeIf { it > 0 } val out = SessionState.outputSize @@ -2016,7 +1779,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } if (selectedSecondScreenDisplay !in newIds) selectedSecondScreenDisplay = candidates.firstOrNull()?.id ?: -1 if (secondScreenMode != SecondScreenMode.NONE && candidates.isEmpty()) closeSecondScreen(reset = true) - // A choice kept while its display was away (asleep, waking): back as soon as it is. else if (started && secondScreenMode != SecondScreenMode.NONE && secondScreenPresentation == null) showSecondScreen(secondScreenMode) } @@ -2046,8 +1808,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (pipUi || !started) return val target = displayManager.getDisplay(selectedSecondScreenDisplay) if (target == null || !target.isValid || (target.flags and Display.FLAG_PRESENTATION) == 0) { - // Not there right now - a panel still waking, say. The choice is kept; the display - // listener shows it again when the display comes back (refreshSecondScreenDisplays). closeSecondScreen(reset = false) return } @@ -2098,13 +1858,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { applyUnfoldedControls() } - /** - * Unfolded controls: on a foldable's inner screen, the game above and the Steam Deck controller's - * trackpads and back grips below ([com.droiddeck.launcher.ui.DeckControlsPanel], as a second - * screen shows them, plus a whole gamepad). The on-screen pad is hidden meanwhile. The surface shrinks to the top part, so a Follow screen session resizes - * to it and any other is fitted into it. Folded onto the cover screen - a phone-sized panel - - * or in picture-in-picture, the game is full screen again. - */ private fun applyUnfoldedControls() { if (!::sessionRoot.isInitialized) return val width = sessionRoot.width @@ -2129,7 +1882,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (panel == null) { panel = com.droiddeck.launcher.ui.DeckControlsPanel(this, onClose = { chooseUnfoldedControls(false) }, onSteamMenu = ::tapSteamButton, onQam = ::tapQam, pad = padBridge) - // Just above the surface: the on-screen pad, keyboard and drawer still draw over it. sessionRoot.addView(panel, sessionRoot.indexOfChild(surfaceView) + 1) unfoldedPanel = panel updateOnScreenControls() @@ -2157,7 +1909,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** The touch mode in force: the game's own while Steam's touch controls are shown. */ private fun effectiveTouchMode(): String { val options = steamTouchOptions if (options == null || steamTouchControls?.visibility != View.VISIBLE) return SessionPrefs.touchMode(this) @@ -2181,14 +1932,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { routedTouchMode = next } - /** Touchpad on the desktop, direct in Steam, unless the drawer says otherwise. */ private fun usingTouchpad(): Boolean = when (routedTouchMode) { SessionPrefs.TOUCH_PAD -> true SessionPrefs.TOUCH_DIRECT -> false else -> SessionState.mode != SessionService.MODE_STEAM || SessionState.steamUi == "desktop" } - /** The picture's rectangle inside the view: where the pointer may go. */ private fun drawnRect(): android.graphics.RectF? { val width = surfaceView.width.takeIf { it > 0 } ?: return null val height = surfaceView.height.takeIf { it > 0 } ?: return null @@ -2203,7 +1952,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun onTouchEvent(event: MotionEvent): Boolean { if (event.isFromSource(android.view.InputDevice.SOURCE_MOUSE)) return onMouse(event) - // Child views get the event first, so menus, keyboards and the on-screen pad still work. if (routedTouchMode == SessionPrefs.TOUCH_OFF) return true if (usingTouchpad()) { val rect = drawnRect() ?: return false @@ -2222,8 +1970,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { InputSpace.x(x, surfaceView.width), InputSpace.y(y, surfaceView.height)) } when (event.actionMasked) { - // A first finger starts a new gesture, and Android has ended every earlier one: whatever - // the compositor still holds (an up lost to the drawer opening mid-touch) goes first. MotionEvent.ACTION_DOWN -> { WaylandCompositor.nativeSendTouch(3, -1, 0, 0) listOf(PointerGestures.BTN_LEFT, PointerGestures.BTN_RIGHT, PointerGestures.BTN_MIDDLE).forEach { @@ -2240,8 +1986,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return true } - /** A mouse: hover moves, buttons press, the wheel scrolls. Android sends buttons as touch - * actions and hover as generic motion, so both paths land here. */ private fun onMouse(event: MotionEvent): Boolean { when (event.actionMasked) { MotionEvent.ACTION_HOVER_MOVE, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_HOVER_ENTER -> @@ -2259,19 +2003,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val v = event.getAxisValue(MotionEvent.AXIS_VSCROLL) if (v != 0f) WaylandCompositor.nativeSendSceneInput(4, -Math.round(v), 0) } - MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP -> {} // the BUTTON_* events carry these + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP -> {} else -> return false } return true } - /** A physical mouse, not a touchscreen, stylus or the system's own virtual devices. */ private fun mouseAttached(): Boolean = InputDevice.getDeviceIds().any { id -> val d = InputDevice.getDevice(id) d != null && !d.isVirtual && d.supportsSource(InputDevice.SOURCE_MOUSE) } - /** Capture the mouse exactly while a program holds a lock and the session has the mouse. */ private fun updatePointerCapture() { if (!::surfaceView.isInitialized) return val view = window.decorView @@ -2280,10 +2022,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (want) view.requestPointerCapture() else view.releasePointerCapture() } - /** - * A captured mouse: raw motion as relative pointer deltas, buttons and the wheel as on the - * uncaptured path. Anything else (a captured touchpad's absolute fingers) is left alone. - */ private fun onCapturedPointer(event: MotionEvent): Boolean { if (!pointerLocked || !event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE)) return false when (event.actionMasked) { @@ -2308,14 +2046,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val v = event.getAxisValue(MotionEvent.AXIS_VSCROLL) if (v != 0f) WaylandCompositor.nativeSendSceneInput(4, -Math.round(v), 0) } - MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP -> {} // the BUTTON_* events carry these + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP -> {} else -> return false } return true } - /** The mode decides which controls appear; `droiddeck-osc` in Downloads still overrides. */ - /** Which controller the session has, so a pad report says what was plugged in and when. */ private fun logInputDevice(what: String, deviceId: Int) { val d = InputDevice.getDevice(deviceId) ?: return if (!PadBridge.isFromController(d)) return @@ -2323,12 +2059,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { what, d.name, d.vendorId, d.productId, d.id, d.sources)) } - /** Steam's touch controls, once the session has made the device (SessionService). */ private fun steamTouch(): com.droiddeck.launcher.input.SteamTouchControls? { val device = com.droiddeck.launcher.input.SteamTouchDevice.current ?: return null steamTouchControls?.let { view -> if (view.device === device) return view - // A new session made a new device: the view of the old one goes. view.releaseAll() (view.parent as? android.view.ViewGroup)?.removeView(view) steamTouchControls = null @@ -2341,13 +2075,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { steamTouchOptions = options applyTouchRouting() } - // Paste: the clipboard is shared with the session's X servers; Ctrl+V types it. view.onPaste = { - sendGuestKey(29, true); sendGuestKey(47, true) // KEY_LEFTCTRL, KEY_V + sendGuestKey(29, true); sendGuestKey(47, true) sendGuestKey(47, false); sendGuestKey(29, false) } - // The client reads a controller's config when it connects: after a save, plug the device - // out and back in so it takes the new one. view.onSaved = { device.plugged = false uiHandler.postDelayed({ updateOnScreenControls() }, 700) @@ -2364,8 +2095,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { ?.let { FileUtils.readString(it)?.trim()?.lowercase() } ?: SessionPrefs.oscMode(this) val controls = onScreenControls ?: return - // In picture-in-picture, or with the unfolded screen's gamepad below the game, the on-screen - // pad would only cover the picture or that panel. if (pipUi || unfoldedPanel != null) { onScreenButtonsVisible = false controls.releaseAll() @@ -2375,12 +2104,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return } val buttonsOnly = forced == SessionPrefs.OSC_STEAM_QAM - // Steam's touch controller stands in for the app's pad, and is plugged into the client only - // while it is shown - as Steam Link withdraws its controls when a physical pad is in use. val wantsSteam = forced == SessionPrefs.OSC_STEAM_TOUCH && SessionState.mode == SessionService.MODE_STEAM val steam = if (wantsSteam) steamTouch() else null - // The service makes the device while the session starts, after this activity: the app's - // own pad until then, and another look in a moment. if (wantsSteam && steam == null && !isFinishing && !isDestroyed) { uiHandler.removeCallbacks(steamTouchRetry) uiHandler.postDelayed(steamTouchRetry, 1000) @@ -2394,13 +2119,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { SessionPrefs.OSC_ALWAYS -> true SessionPrefs.OSC_STEAM_QAM -> true SessionPrefs.OSC_NEVER -> false - // Shown throughout a Steam session: a handheld's built-in pad would keep Auto off for good. - // Outside Steam it is Auto. SessionPrefs.OSC_STEAM_TOUCH -> SessionState.mode == SessionService.MODE_STEAM || (!PadBridge.anyControllerConnected() && SessionState.mode != SessionService.MODE_DESKTOP) - // Auto: the touch pad when there is no controller - except on the desktop, where the - // screen is a touchpad for the pointer and a pad over it would be in the way. A game - // started from the rail, or Steam, gets it; the drawer turns it on anywhere. else -> !PadBridge.anyControllerConnected() && !SteamControllerBle.connected && SessionState.mode != SessionService.MODE_DESKTOP } @@ -2486,7 +2206,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun onPictureInPictureModeChanged(inPip: Boolean, newConfig: android.content.res.Configuration) { super.onPictureInPictureModeChanged(inPip, newConfig) if (::pip.isInitialized) pip.changed(inPip) - // The panel may have changed while the session was a thumbnail (folded or unfolded in PiP). if (!inPip && surfaceW > 0) followWindow(surfaceW, surfaceH) applyUnfoldedControls() } @@ -2498,7 +2217,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { super.startActivityForResult(intent, requestCode, options) } - // ── Lifecycle ─────────────────────────────────────────────────────────────────────────── override fun onStart() { super.onStart() @@ -2514,8 +2232,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (runtimeRemovalBlocked) { super.onStop(); return } started = false displayManager.unregisterDisplayListener(displayListener) - // Closed while the session is out of sight (sleep, a closed lid, another app), and kept: - // onStart puts it back. closeSecondScreen(reset = false) SessionService.setActivityVisible(this, false) super.onStop() @@ -2532,7 +2248,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { recreate() return } - // Home/notification resumes must retain the external frontend's return destination. if (this.intent.getBooleanExtra(EXTRA_RETURN_HOME, false)) intent.putExtra(EXTRA_RETURN_HOME, true) setIntent(intent) if (intent.action == SessionService.ACTION_HOME_GUIDE) { @@ -2572,14 +2287,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun onPause() { if (runtimeRemovalBlocked) { super.onPause(); return } sessionClipboard.stop() - // Do not carry transient session UI across an app/display transition. In particular, the - // drawer's dim layer can otherwise remain over Steam when this activity returns. drawerOpen = false pcKeyboardOpen = false achievementsOpen = false (getSystemService(INPUT_SERVICE) as? InputManager)?.unregisterInputDeviceListener(deviceListener) releaseDrawerDirection() - // A button held when the app goes away would stay held in the ring for the whole session. onScreenControls?.releaseAll() resumed = false updatePointerCapture() @@ -2590,8 +2302,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { super.onPause() } - /** SteamVR for Cardboard mode: install, start or stop it in the guest. Starting switches the - * picture to side-by-side, which is what SteamVR draws. */ private fun steamVrAction(action: String) { if (action == "start") { switchPictureForSteamVr() @@ -2603,7 +2313,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val ok = SteamVr.run(this, action) val now = SteamVr.status(this) uiHandler.post { - // A start that failed leaves nothing side-by-side to show: the picture goes back. if (!ok && action == "start" && now != SteamVr.Status.RUNNING) restoreSteamVrPicture() onSteamVrStatus(now) if (!ok) Toast.makeText(this, getString(R.string.cardboard_steamvr_failed), Toast.LENGTH_LONG).show() @@ -2611,7 +2320,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, "steamvr-$action").start() } - /** SteamVR draws a side-by-side pair: the picture switches to that, remembering what it was. */ private fun switchPictureForSteamVr() { if (cardboard.source != Cardboard.SOURCE_SBS) { SessionPrefs.setCardboardSourceBeforeSteamVr(this, cardboard.source) @@ -2624,7 +2332,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private fun onSteamVrStatus(now: SteamVr.Status) { steamVrStatus = now - // Started some other way than the drawer (a script, a VR game): the picture follows it all the same. if (now == SteamVr.Status.RUNNING && cardboard.enabled && !steamVrSwitchedPicture && cardboard.source != Cardboard.SOURCE_SBS) { switchPictureForSteamVr() @@ -2635,7 +2342,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { else if (steamVrSeenRunning && now != SteamVr.Status.UNAVAILABLE) restoreSteamVrPicture() } - /** SteamVR stopped: the Cardboard picture goes back to what it was before Start. */ private fun restoreSteamVrPicture() { val before = SessionPrefs.cardboardSourceBeforeSteamVr(this) SessionPrefs.setCardboardSourceBeforeSteamVr(this, -1) @@ -2647,7 +2353,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { applyCardboard(c) } - /** Cardboard mode: hand the settings to the compositor and run the head tracker while it needs one. */ private fun applyCardboard(c: Cardboard) { val was = cardboard cardboard = c @@ -2657,8 +2362,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } private fun updateHeadTracker() { - // Side-by-side sources track too: the compositor leaves their picture alone, but whoever renders - // it (the SteamVR driver, tools/vr) reads the pose from the shared file. val want = resumed && cardboard.enabled && cardboard.headTracking if (want) { val t = headTracker ?: HeadTracker(this) { @@ -2671,7 +2374,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** The panel's physical size in millimetres, long side first, for lining the eyes up with the lenses. */ private fun panelSizeMm(): Pair<Float, Float> { val dm = resources.displayMetrics val landscape = dm.widthPixels >= dm.heightPixels @@ -2697,11 +2399,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { goFullscreen() refreshClipboard() } - // Android drops a capture with the focus; a lock still held takes it again on return. updatePointerCapture() } - /** The keyboard/trackpad presentation can own focus instead of the main display. */ fun refreshClipboard() { if (resumed) sessionClipboard.refresh() } @@ -2714,8 +2414,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) - // ALWAYS from Android 11: SHORT_EDGES left a black strip along a camera on the long edge, - // which is where a Fold's inner-screen camera sits with the device held in landscape. if (Build.VERSION.SDK_INT >= 28) { window.attributes = window.attributes.apply { layoutInDisplayCutoutMode = if (Build.VERSION.SDK_INT >= 30) @@ -2725,7 +2423,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** Ours, so a finishing activity never unhooks the one that replaced it. */ private val endListener: (Int) -> Unit = { status -> onSessionEnded(status) } private val firstFrameListener = Runnable { SessionEvents.firstFrame() @@ -2739,8 +2436,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun onDestroy() { uiHandler.removeCallbacks(steamTouchRetry) if (runtimeRemovalBlocked) { super.onDestroy(); return } - // Deliberately does NOT end the session: this activity can be destroyed while the user is - // in another app, and the whole point of the service is that Steam survives that. watching = false releaseDrawerDirection() pendingBackAction?.let(uiHandler::removeCallbacks) @@ -2759,7 +2454,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { super.onDestroy() } - /** The screen is all blue: the session stops and the front end opens on the same blue (FloodReturn). */ private fun finishFlooded() { if (isFinishing) return val signal = com.droiddeck.launcher.ui.Themes.byId(SessionPrefs.theme(this)).signal @@ -2770,20 +2464,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { finish() } - /** - * Every way out on screen (backing out of the loading screen, closing the ended screen, the - * session ending) leaves as a session arrives, in reverse: the blue spreads out of the - * throbber's ball, or the middle of the screen once a game is up, and the front end opens on it - * and draws it back into the button the session came from. Off screen, or with animations - * off, it just closes. - */ override fun finish() { if (isFinishing) return if (!quitFlooded && !closeAtOnce && com.droiddeck.launcher.ui.Motion.scale != 0f && lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.RESUMED)) { if (!leaving) { leaving = true - // Should the flood never get its frames, the session still closes. uiHandler.postDelayed({ completeLeave() }, LEAVE_TIMEOUT_MS) } return @@ -2799,7 +2485,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { else overridePendingTransition(R.anim.session_hold, R.anim.session_sink) } - /** The screen is all blue: close onto the front end, which opens on the same blue. */ private fun completeLeave() { if (isFinishing) return val signal = com.droiddeck.launcher.ui.Themes.byId(SessionPrefs.theme(this)).signal @@ -2811,7 +2496,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { companion object { const val EXTRA_RETURN_HOME = "returnHome" - /** How long the leaving flood may take before the session closes without it. */ private const val LEAVE_TIMEOUT_MS = 1_500L private const val TAG = "SessionActivity" private const val FEX_MISSING = "droiddeck-fex: x86 Linux programs need" @@ -2825,7 +2509,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private const val DRAWER_SLOW_REPEAT_FLOOR_MS = 240L private const val DRAWER_FAST_REPEAT_INTERVAL_MS = 90L private const val DRAWER_REPEAT_ACCELERATION_MS = 1_400L - /** Compositor scale modes (Container.FULLSCREEN_* values): 1 = fit with bars, centred. */ private const val SCALE_FIT = 1 private const val ALIGN_CENTER = 0 private val RUNTIME_LINES = PackageLines( @@ -2846,5 +2529,4 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } -/** [progress]: (unlocked, total) from the cached list. */ private class AchievementsInFront(val game: StoreAchievements.Game, val title: String, val progress: Pair<Int, Int>?) diff --git a/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt b/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt index a55ac1930..8459bc7d9 100644 --- a/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt @@ -18,13 +18,7 @@ import com.droiddeck.launcher.ui.UpdatesState import com.droiddeck.launcher.update.AppUpdates import com.droiddeck.launcher.update.SelfInstaller -/** - * The Updates page's state: the channels' latest builds, the one followed, and an update being - * downloaded or installed. Checked when the launcher comes to the front - opening the app, and - * coming back from a session - at most once a minute. - */ internal class UpdatesMenu(private val activity: Activity, private val ui: Handler) { - // Read in start(): the activity has no context yet while its fields are made. var catalog by mutableStateOf<AppUpdates.Catalog?>(null) var follow by mutableStateOf(AppUpdates.Follow(AppUpdates.Channel.STABLE)) var checking by mutableStateOf(false) @@ -32,7 +26,6 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl var stage by mutableStateOf<String?>(null) var percent by mutableIntStateOf(-1) var askPermission by mutableStateOf(false) - // An install waiting for the user to allow "Install unknown apps"; it goes on when they come back. private var pending: AppUpdates.Release? = null private var lastCheck = 0L private var request = 0 @@ -53,7 +46,6 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl val hasUpdate: Boolean get() = AppUpdates.hasUpdate(catalog, follow) - /** On every return to the launcher: go on with an install the user just allowed, and check if due. */ fun onResume() { pending?.let { r -> if (SelfInstaller.canInstall(activity)) { pending = null; install(r) } @@ -107,7 +99,6 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl }, "app-update-install").start() } - /** PackageInstaller's answer: Android's confirm prompt to show, or why it failed. Success restarts the app. */ private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) { @@ -127,7 +118,6 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl } } - /** From onCreate: the last check, the channel followed, and PackageInstaller's answers. */ fun start() { catalog = AppUpdates.cached(activity) follow = AppUpdates.follow(activity, catalog) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt index b6828c742..b755ca646 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt @@ -2,26 +2,16 @@ package com.droiddeck.launcher.agent import android.content.Context -/** - * Who may run code through the agent bridge. Reading state, starting and stopping sessions, - * launching and quitting games and sending input only need the shell (the provider's DUMP - * permission). Running commands in the guest, evaluating JavaScript in the Steam client, session - * environment, binary overrides and preference writes reach inside the app's sandbox - the guest - * holds the Steam login - which the shell of a stock device otherwise cannot. They require - * Setup > Diagnostics > Debugging tools to be on, in every build. The bridge itself cannot turn it on. - */ object AgentAccess { const val MAX_REQUEST_CHARS = 256 * 1024 private const val PREFS = "agent" private const val KEY_COMMANDS = "commands" - /** The preferences file the bridge must never write: set-pref refuses it. */ const val PROTECTED_PREFS = PREFS fun commandsAllowed(context: Context): Boolean = prefs(context).getBoolean(KEY_COMMANDS, false) - /** The Setup toggle's own state. */ fun commandsEnabled(context: Context): Boolean = prefs(context).getBoolean(KEY_COMMANDS, false) fun setCommandsEnabled(context: Context, enabled: Boolean) { @@ -44,5 +34,4 @@ object AgentAccess { private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) } -/** A refusal with a stable code, answered as {"ok": false, "error": {code, message}}. */ class AgentException(val code: String, message: String) : RuntimeException(message) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentBridgeProvider.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentBridgeProvider.kt index 2b8ba775b..4294e5ad9 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentBridgeProvider.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentBridgeProvider.kt @@ -27,14 +27,6 @@ import java.io.File import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -/** - * The shell's control surface (docs/agent-control.md), in every build: the provider and its start - * Activity need android.permission.DUMP, held by the shell and privileged or explicitly granted callers. Commands that - * reach into the app's sandbox are further gated by [AgentAccess]. - * - * A method's request is JSON, base64-encoded in the "request" extra; every answer is JSON in the - * "json" key of the returned Bundle, {"ok": true, ...} or {"ok": false, "error": {code, message}}. - */ class AgentBridgeProvider : ContentProvider() { override fun onCreate(): Boolean { val appContext = context?.applicationContext ?: return false @@ -58,7 +50,6 @@ class AgentBridgeProvider : ContentProvider() { val response = try { val request = decode(extras?.getString(EXTRA_REQUEST)) - // Binder identity is the shell's; everything below acts as the app. val token = Binder.clearCallingIdentity() try { dispatch(context, method, request) @@ -136,8 +127,6 @@ class AgentBridgeProvider : ContentProvider() { .put("status", SessionState.failureStatus ?: JSONObject.NULL) session.put("failure", failure) - // gamescope's focus as last published by the guest agent, without the window lists - // (the focus command returns those, fresh). val focus = AgentGuest.focus(context)?.let { snap -> JSONObject() .put("focusedApp", snap.opt("focusedApp") ?: JSONObject.NULL) @@ -225,11 +214,9 @@ class AgentBridgeProvider : ContentProvider() { return result.put("ok", true).put("command", "prefs") } - /** A guest agent request; its own answer already carries ok or error. */ private fun guest(context: Context, request: JSONObject, timeoutMs: Long): JSONObject = AgentGuest.call(context, request, timeoutMs) - /** The guest's own timeout plus slack for the round trip. */ private fun timeoutMs(request: JSONObject, defaultSeconds: Double, slackMs: Long): Long { val seconds = if (request.has("timeout")) request.opt("timeout")?.toString()?.toDoubleOrNull() ?: throw AgentException("INVALID_REQUEST", "timeout must be a number") else defaultSeconds @@ -262,7 +249,6 @@ class AgentBridgeProvider : ContentProvider() { companion object { const val RESULT_JSON = "json" const val EXTRA_REQUEST = "request" - /** 2 added agent and session.focus; everything in 1 is unchanged. */ const val SCHEMA = 2 private val recoveryComplete = CountDownLatch(1) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt index 83ea0ccd1..5f7f09262 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt @@ -10,18 +10,6 @@ import java.nio.file.Files import java.nio.file.LinkOption.NOFOLLOW_LINKS import java.nio.file.StandardOpenOption.READ -/** - * Experiment switches an agent sets without touching Download/droiddeck-env or rebuilding the apk: - * - * - environment lines (KEY=VALUE) for the session, applied after the user's own droiddeck-env, so - * they win. "next" lines are taken by the next session that starts and dropped when it ends; - * "persistent" ones stay until cleared. - * - binary overrides: a file pushed to the app's external files (agent-inbox/<name>) is installed - * as /opt/droiddeck-agent/bin/<name> in the rootfs and put first on the next session's PATH - - * a gamescope build under test, say. Removed when that session ends. - * - * Kept in the app's own files (agent-env.json), never in a place the shell can write. - */ object AgentEnv { private const val TAG = "AgentEnv" private const val FILE = "agent-env.json" @@ -38,7 +26,6 @@ object AgentEnv { load(context) } - /** Add [lines] for the next session ([persistent] = every session until cleared). */ fun set(context: Context, lines: List<String>, persistent: Boolean): JSONObject = synchronized(lock) { AgentAccess.requireCommands(context) if (lines.any { line -> KEY.matchEntire(line.substringBefore('=', "")) == null || !line.contains('=') || line.any { it == '\n' || it == '\r' || it == '\u0000' } }) { @@ -52,7 +39,6 @@ object AgentEnv { state } - /** Drop lines: [scope] is "next", "persistent" or "all". */ fun clear(context: Context, scope: String): JSONObject = synchronized(lock) { AgentAccess.requireCommands(context) val state = load(context) @@ -66,7 +52,6 @@ object AgentEnv { state } - /** Install agent-inbox/[name] as an override for the next session. */ fun override(context: Context, name: String): JSONObject = synchronized(lock) { AgentAccess.requireCommands(context) if (NAME.matchEntire(name) == null || name in setOf(".", "..")) throw AgentException("INVALID_NAME", "Override names are letters, digits, '.', '_' and '-'") @@ -93,10 +78,6 @@ object AgentEnv { state.put("installed", "$GUEST_BIN/$name") } - /** - * At session start: the lines this session gets, after the user's droiddeck-env. Pending lines - * and overrides become this session's own, to be dropped by [endSession]. - */ fun beginSession(context: Context): List<String> = synchronized(lock) { if (!AgentAccess.commandsAllowed(context)) { revoke(context) @@ -108,7 +89,6 @@ object AgentEnv { state.put("active", JSONArray(active)).put("pending", JSONArray()) state.put("activeOverrides", JSONArray(overrides)).put("pendingOverrides", JSONArray()) save(context, state) - // Anything else there was left by a session that never reached endSession (the app died). bin(context).listFiles() ?.filter { it.name !in overrides }?.forEach { it.delete() } val lines = strings(state.optJSONArray("persistent")) + active @@ -124,7 +104,6 @@ object AgentEnv { bin(context).listFiles()?.forEach { it.delete() } } - /** At session end: this session's lines and override binaries go. */ fun endSession(context: Context) = synchronized(lock) { val state = load(context) val overrides = strings(state.optJSONArray("activeOverrides")) @@ -135,7 +114,6 @@ object AgentEnv { save(context, state) } - /** A PATH= line among [lines] (the user's or an agent's) is extended rather than replaced. */ private fun pathOf(lines: List<String>) = lines.lastOrNull { it.startsWith("PATH=") }?.substringAfter('=')?.takeIf { it.isNotBlank() } ?: BASE_PATH @@ -178,7 +156,6 @@ object AgentEnv { } } -/** Typed writes to the app's SharedPreferences, for driving a settings matrix without the UI. */ object AgentPrefs { fun set(context: Context, file: String, key: String, type: String, value: String?): JSONObject { AgentAccess.requireCommands(context) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt index c572e943d..3b265ebb9 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt @@ -15,11 +15,6 @@ import org.json.JSONObject import java.io.File import java.util.UUID -/** - * The app's side of the guest agent (tools/linuxfs/overlay/usr/local/bin/droiddeck-agent): requests - * go in as files under the session root's agent directory and answers come back the same way, so - * whatever runs does so inside the live session with its own environment. - */ object AgentGuest { private val main by lazy { Handler(Looper.getMainLooper()) } var focusRevision by mutableIntStateOf(0) @@ -32,11 +27,6 @@ object AgentGuest { fun dir(context: Context) = File(LinuxRuntime.sessionRoot(context), "agent") - /** - * Before each session: nothing of the last one's daemon, answers or focus may be read as - * current. Then watch for the new daemon's hello and focus snapshots, which go into the - * session's events.jsonl. - */ fun reset(context: Context) { synchronized(lock) { observer?.stopWatching() @@ -59,7 +49,6 @@ object AgentGuest { } } - /** At the end of a session: no more events for a folder that is being closed. */ fun stop() { synchronized(lock) { observer?.stopWatching() @@ -82,7 +71,6 @@ object AgentGuest { SessionEvents.record("focus.changed", fields + ("seq" to snapshot.optInt("seq"))) } - /** The daemon's hello, when it is running in the current session. */ fun hello(context: Context): JSONObject? { if (!SessionState.running) return null val hello = readJson(File(dir(context), "hello.json")) ?: return null @@ -90,18 +78,15 @@ object AgentGuest { return hello.takeIf { pid > 1 && File("/proc/$pid").exists() } } - /** The newest focus snapshot the daemon published, when a session is running. */ fun focus(context: Context): JSONObject? = if (SessionState.running) readJson(File(dir(context), "focus.json")) else null - /** The Steam library game whose tile currently has controller or keyboard focus. */ fun selectedSteamAppId(context: Context): Long? { val result = call(context, JSONObject().put("kind", "steam-selection"), 6_000) if (!result.optBoolean("ok") || result.isNull("appId")) return null return result.optLong("appId").takeIf { it in 1..4_294_967_295L } } - /** The installed game Gamescope is presenting, including while Steam's overlay has focus. */ fun activeSteamAppId(context: Context, installedAppIds: Set<Long>): Long? { val snapshot = focus(context) ?: return null val focusedAppId = snapshot.optLong("focusedApp").takeIf { snapshot.has("focusedApp") } @@ -151,7 +136,6 @@ object AgentGuest { return null } - /** Send one request and wait for its answer. Throws [AgentException] with a stable code. */ fun call(context: Context, request: JSONObject, timeoutMs: Long): JSONObject { if (request.optString("kind") in setOf("exec", "cdp")) AgentAccess.requireCommands(context) if (!SessionState.running) throw AgentException("NO_ACTIVE_SESSION", "There is no running session") diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentInput.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentInput.kt index e2c8674b4..94cf53388 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentInput.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentInput.kt @@ -5,13 +5,7 @@ import com.droiddeck.launcher.session.SessionState import com.droiddeck.launcher.wayland.WaylandCompositor import org.json.JSONObject -/** - * Input into the running session the way a player gives it: keys and touches through the - * compositor (what the session screen sends), and the virtual pad through the same PadBridge the - * on-screen controls drive, so games that ignore touch still see a controller. - */ object AgentInput { - /** Out of the way of real fingers, which Android numbers from 0. */ private const val TOUCH_ID = 9 private const val TOUCH_SCALE_X = 1919f private const val TOUCH_SCALE_Y = 1079f @@ -56,7 +50,6 @@ object AgentInput { return JSONObject().put("code", code) } - /** Typed as key presses (US layout), which reaches X programs that have no text-input support. */ private fun text(request: JSONObject): JSONObject { val text = request.optString("text") if (request.optBoolean("commit")) { @@ -99,11 +92,6 @@ object AgentInput { return JSONObject().put("from", org.json.JSONArray().put(x1).put(y1)).put("to", org.json.JSONArray().put(x2).put(y2)) } - /** - * A point in the compositor's touch space (0..1919 x 0..1079 whatever the session's size), from - * the session's output pixels (space "output", the default: what the guest draws at) or 0..1 - * fractions (space "fraction"). - */ private fun point(request: JSONObject, xKey: String, yKey: String): Pair<Int, Int> { if (!request.has(xKey) || !request.has(yKey)) throw AgentException("INVALID_INPUT", "$xKey and $yKey are required") val x = request.getDouble(xKey) @@ -183,7 +171,6 @@ object AgentInput { "guide" to PadState.GUIDE, "steam" to PadState.GUIDE, ) - /** Linux evdev codes (input-event-codes.h) by name. */ private val KEYS: Map<String, Int> = buildMap { "1234567890".forEachIndexed { i, c -> put(c.toString(), 2 + i) } "QWERTYUIOP".forEachIndexed { i, c -> put(c.toString(), 16 + i) } @@ -202,7 +189,6 @@ object AgentInput { )) } - /** US-layout characters as (evdev code, shift). */ private val TYPED: Map<Char, Pair<Int, Boolean>> = buildMap { for (c in 'a'..'z') put(c, KEYS.getValue(c.uppercase()) to false) for (c in 'A'..'Z') put(c, KEYS.getValue(c.toString()) to true) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentStartActivity.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentStartActivity.kt index edf38dde6..9b9d85bbd 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentStartActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentStartActivity.kt @@ -10,7 +10,6 @@ import com.droiddeck.launcher.session.SessionService import org.json.JSONArray import org.json.JSONObject -/** Shell-launched trampoline: Android permits the shell to start this protected debug Activity. */ class AgentStartActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentUi.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentUi.kt index 6153da0f3..b6179e07b 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentUi.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentUi.kt @@ -19,16 +19,8 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference -/** - * The app's own UI as an agent sees it: every Compose node in every window of the process (popups - * and menus included), read from Compose's semantics rather than through UI Automator, which on a - * dual-screen device dumps whichever window has focus - often not this app's. Nodes carry their - * test tag (ui/TestTags.kt), text, content description, role, state and on-screen bounds; click - * and set-text act on a node by tag or text, so a test needs no coordinates. - */ object AgentUi { private const val TIMEOUT_MS = 5_000L - /** Setup's Debugging tools toggle: the bridge never acts on its own gate (AgentAccess). */ private const val GATE_TAG = "setting-agent-commands" fun handle(request: JSONObject): JSONObject = when (val op = request.optString("op", "dump")) { @@ -106,10 +98,6 @@ object AgentUi { return json } - /** - * The node a request names by "tag" (exact) or "match" (text or description containing it, - * ignoring case); "index" picks among several, in tree order. - */ private fun act(request: JSONObject, action: (SemanticsNode) -> Unit): JSONObject { val tag = request.optString("tag").takeIf { it.isNotEmpty() } val text = if (tag == null) request.optString("match").takeIf { it.isNotEmpty() } else null diff --git a/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java b/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java index ad5278117..15ffd7bdb 100644 --- a/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java +++ b/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java @@ -9,34 +9,12 @@ import com.droiddeck.launcher.core.HostProcess; import java.io.File; import java.util.ArrayList; -/** - * The DirectAudio relay helper, which owns Android's speaker and microphone on behalf of a Linux - * session. - * - * <p>Nothing inside the session can open Android's audio itself. A game is a glibc process in a - * rootfs that binds no {@code /system}, and a glibc process cannot call libaaudio at all; the - * session is also never given a way to spawn a bionic helper of its own. So the helper runs out - * here, under the app's own uid, and the driver inside Wine talks to it over a unix socket. The uid - * matters for more than tidiness: Android checks the microphone permission against whoever opens - * the stream, so a helper started any other way could play but never record. - * - * <p>It is executed from the app's native library directory because Android will not execute a - * binary from anywhere else - the same reason the PulseAudio daemon ships as {@code - * libpulseaudio.so} despite being a program rather than a library. - * - * <p>Started before the client, because the driver asks once whether a helper is there, when Wine - * picks an audio driver, and reports itself unavailable if not. Ported from Bannerlator (GPL-3.0); - * the helper binary is the one built by The412Banner/directaudio's relay workflow. - */ public class DirectAudioRelayComponent extends SessionPart { private static final String TAG = "DirectAudioRelay"; - /** The helper, named as a library so Android will run it. */ public static final String BINARY = "libdirectaudiorelay.so"; private final File socketPath; - /** Shared with PulseAudioComponent: one audio.log per session, appended by both. */ private File logFile; - /** Null when the microphone was not asked for; the helper then opens no input stream. */ private final File micFifoPath; private volatile int pid = -1; @@ -45,7 +23,6 @@ public class DirectAudioRelayComponent extends SessionPart { this.micFifoPath = micFifoPath; } - /** Send the helper's output to this file as well as logcat. Set before {@link #start()}. */ public void setLogFile(File file) { this.logFile = file; } @@ -60,21 +37,10 @@ public class DirectAudioRelayComponent extends SessionPart { } File parent = socketPath.getParentFile(); if (parent != null) { - //noinspection ResultOfMethodCallIgnored parent.mkdirs(); } - // A socket left by a session that did not shut down cleanly is not a listener, and the - // helper would fail to bind onto it. - //noinspection ResultOfMethodCallIgnored socketPath.delete(); - // PulseAudio's pipe module insists on creating the pipe itself and fails if one is already - // there. The helper creates one too when the path is missing, and it starts a few - // milliseconds after the daemon is exec'd - well before the daemon has read its config. So - // left to itself the helper won its own race, the daemon's mkfifo failed with EEXIST, and - // the microphone was lost while everything else came up. Wait for the daemon to make the - // pipe before the helper runs; it appears within a second, and if it never does the helper - // starts anyway and creates it, which is the right thing when the daemon is not involved. if (micFifoPath != null) { long deadline = System.currentTimeMillis() + 3000L; while (!micFifoPath.exists() && System.currentTimeMillis() < deadline) { @@ -91,12 +57,8 @@ public class DirectAudioRelayComponent extends SessionPart { StringBuilder command = new StringBuilder(binary.getAbsolutePath()); command.append(" --socket ").append(socketPath.getAbsolutePath()); - // Diagnostics into the session's audio.log: every 1000 callbacks the device buffer, - // Android's own xrun count and the ring's fill, plus every buffer change. command.append(" --log"); if (micFifoPath != null) { - // The helper creates the pipe if it is not there. PulseAudio is told the same path and - // reads from the other end of it. command.append(" --mic-fifo ").append(micFifoPath.getAbsolutePath()); } ArrayList<String> env = new ArrayList<>(); @@ -126,8 +88,6 @@ public class DirectAudioRelayComponent extends SessionPart { @Override public void stop() { if (pid != -1) { - // The helper exits on a signal and tears down its streams; there is nothing below it to - // orphan, unlike the proot tree. Process.killProcess(pid); pid = -1; } diff --git a/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java b/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java index c4e1db8c7..849455b7a 100644 --- a/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java +++ b/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java @@ -17,30 +17,15 @@ import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.concurrent.TimeUnit; -/** - * PulseAudio 13.0 running in the app process, with Android's AAudio as its sink, so the guest's - * libpulse clients (Steam and everything it launches) have a server to talk to. The socket lives - * in the app's files directory and is bound into the session at its own path, so PULSE_SERVER - * needs no translating. - */ public class PulseAudioComponent extends SessionPart { private static final String TAG = "PulseAudio"; - /** Where the guest reaches the daemon; the session exports PULSE_SERVER=unix:<this>. */ public static final String SOCKET_NAME = "PS0"; - /** Identifies the bundled pulseaudio.tzst; a change here re-unpacks it over what a device has. */ private static final String BUNDLE_STAMP = "2026-10-08-pa13-directaudio-native"; private final File workingDir; - /** Where the daemon's own output is kept for this session, or null for logcat only. */ private File logFile; - /** - * A named pipe carrying microphone audio, or null for no microphone. The bundle ships - * module-aaudio-sink but no matching source, and the daemon runs where Android permits - * recording - so pointing module-pipe-source at a pipe the DirectAudio relay helper writes - * turns that one stream into a source the client can see, named DirectAudioMic. - */ private final String micFifoPath; private volatile int pid = -1; @@ -48,13 +33,11 @@ public class PulseAudioComponent extends SessionPart { this(context, null); } - /** As above, with a microphone fed from {@code micFifoPath}; null for output only. */ public PulseAudioComponent(Context context, String micFifoPath) { this.workingDir = new File(context.getFilesDir(), "pulseaudio"); this.micFifoPath = micFifoPath; } - /** CRC-32 of the bundled pulseaudio.tzst, or "?" if it cannot be read. About 600 KB; cheap. */ private String bundleChecksum() { java.util.zip.CRC32 crc = new java.util.zip.CRC32(); byte[] buf = new byte[64 * 1024]; @@ -67,7 +50,6 @@ public class PulseAudioComponent extends SessionPart { return Long.toHexString(crc.getValue()); } - /** Send the daemon's output to this file as well as logcat. Set before {@link #start()}. */ public void setLogFile(File file) { this.logFile = file; } @@ -80,18 +62,9 @@ public class PulseAudioComponent extends SessionPart { public void start() { stop(); if (!workingDir.isDirectory()) { - //noinspection ResultOfMethodCallIgnored workingDir.mkdirs(); FileUtils.chmod(workingDir, 0771); } - // The loadable modules (module-aaudio-sink - the app's own, from tools/aaudio-sink - the - // native protocol, the pipe modules) ride in the apk; the daemon and its libraries come from the native library directory, the one - // place an app may execute a file from. The bundle is unpacked once per BUNDLE_STAMP, not - // once ever: an installed app kept the modules it unpacked on its first run, so a bundle - // fixed in a later build never reached the device - which is how a 17.0 glibc build of - // module-pipe-source sat beside the 13.0 daemon, failed to dlopen, and the microphone - // never appeared. The stamp carries a checksum of the bundle itself, so any change to it - - // a rebuilt sink module included - re-unpacks, whether or not BUNDLE_STAMP was bumped. File modulesDir = new File(workingDir, "modules"); File stamp = new File(modulesDir, ".bundle"); String have = FileUtils.readString(stamp); @@ -112,38 +85,18 @@ public class PulseAudioComponent extends SessionPart { if (pactl.isFile()) FileUtils.chmod(pactl, 0771); copyFromLibraryDir(); - //noinspection ResultOfMethodCallIgnored socket().delete(); - // module-pipe-source creates the pipe with mkfifo and fails outright if one is already - // there - EEXIST, reported as "Unknown error 17" - and the module then does not load at - // all, so the source never appears and the client reports no microphone. Ours lives in the - // app's files directory and survives a session, so after the very first run the path would - // always be occupied. Removed here, before the daemon reads this config: the daemon makes - // it, and the relay helper starts afterwards and is content to find one already made. if (micFifoPath != null && !micFifoPath.isEmpty()) { - //noinspection ResultOfMethodCallIgnored new File(micFifoPath).delete(); } ArrayList<String> config = new ArrayList<>(); config.add("load-module module-native-protocol-unix auth-anonymous=1 auth-cookie-enabled=0 socket=\"" + socket().getAbsolutePath() + "\""); - // volume=1.0 is not optional: with no volume argument module-aaudio-sink defaults - // the sink to 0% and the session plays silence. - // The sink's name is what the client's Audio settings show as the output device, so it - // says which road the sound takes. - // The client's sound: DirectAudio's own engine, in this process, one step from Android. - // decay=0: shrinking the device buffer after a quiet spell probes below what works and that - // probe is a click; the client plays menus. Between .nofail and .fail a module that cannot - // open its stream does not take the daemon down; ensureClientSink() then loads the plain - // AAudio sink in its place. config.add(".nofail"); config.add("load-module module-directaudio-native-sink sink_name=DirectAudio performance_mode=1 adaptive=1 decay=0 limiter=1 watchdog=1 volume=1.0"); config.add("set-default-sink DirectAudio"); config.add(".fail"); if (micFifoPath != null && !micFifoPath.isEmpty()) { - // The format is the helper's, fixed at s16le/48000/mono: it resamples when the device - // grants another input rate, so the daemon is never told a rate the bytes are not. - // A pipe has no clock, so nothing here corrects drift - acceptable for voice. config.add("load-module module-pipe-source source_name=DirectAudioMic file=\"" + micFifoPath + "\" format=s16le rate=48000 channels=1"); config.add("set-default-source DirectAudioMic"); @@ -159,12 +112,7 @@ public class PulseAudioComponent extends SessionPart { String command = workingDir.getAbsolutePath() + "/libpulseaudio.so" + " --system=false --disable-shm=true --fail=false" + " -n --file=default.pa --daemonize=false --use-pid-file=false --exit-idle-time=-1" - // Info level: the classic sink reports the stream Android granted (burst, capacity, - // starting buffer) only there, and that is what a stutter report needs. + " --log-level=info"; - // A module that refuses to load, a pipe that could not be made, the daemon exiting at - // startup: all of it used to reach logcat and nothing else, so a user's folder said nothing - // at all about sound. Four separate faults hid behind "no input device" in one night. final java.io.PrintWriter out = openLog(); pid = HostProcess.start(command, env.toArray(new String[0]), workingDir, null, line -> { @@ -176,11 +124,6 @@ public class PulseAudioComponent extends SessionPart { guard.start(); } - /** - * The fallback: once the daemon answers, if the DirectAudio sink is not - * there (the module failed to load or to open its stream), load the plain AAudio sink so the - * client is never left without an output. Says so in the audio log either way. - */ private void ensureClientSink(java.io.PrintWriter out) { try { long deadline = System.currentTimeMillis() + 15000L; @@ -208,7 +151,6 @@ public class PulseAudioComponent extends SessionPart { } } - /** Runs pactl against this daemon; its stdout, or null when it failed or timed out. */ private String pactl(String... args) { File pactl = new File(workingDir, "pactl"); if (!pactl.isFile() || pid <= 1) return null; @@ -298,7 +240,6 @@ public class PulseAudioComponent extends SessionPart { } } - /** The session's audio log, appended to by both the daemon and the relay helper. */ private java.io.PrintWriter openLog() { if (logFile == null) return null; try { diff --git a/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt b/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt index 46a164f1a..3ea69d565 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt @@ -6,22 +6,9 @@ import android.content.res.Configuration import android.os.LocaleList import java.util.Locale -/** - * The language the app shows itself in. - * - * Nothing chosen (the default) means the system's: Android's own language, or the per-app language - * Android 13+ lets a user set for DroidDeck in its settings, exactly as Android resolves it. Picking - * one in Setup overrides that until the user picks "System default" again. Each activity and service - * applies the choice to its own context ([wrap], [applyTo]), so it works on every Android version - * the app runs on rather than only where the platform's per-app language API exists. - * - * Steam follows the same choice: [SteamLanguage] turns [effective] into the client's language. - */ object AppLanguage { - /** The stored value meaning "follow the system". */ const val SYSTEM = "" - /** The languages the app ships, as BCP 47 tags, in the order the picker lists them. */ val supported: List<String> = listOf( "en", "id", "ms", "cs", "da", "de", "es", "es-419", "fr", "it", "hu", "nl", "nb", "pl", "pt-BR", "pt-PT", "ro", "fi", "sv", "vi", "tr", "el", "bg", "ru", "uk", "ar", "hi", "th", @@ -31,10 +18,8 @@ object AppLanguage { private const val PREFS = "language" private const val KEY = "app_language" - /** The locale Android gave the process before any choice here was applied. */ @Volatile private var systemLocale: Locale? = null - /** The stored choice: one of [supported], or [SYSTEM]. */ fun chosen(context: Context): String = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY, SYSTEM) ?.takeIf { it in supported } ?: SYSTEM @@ -44,7 +29,6 @@ object AppLanguage { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY, value).commit() val locale = value.takeIf { it.isNotEmpty() }?.let(Locale::forLanguageTag) ?: system(context) Locale.setDefault(locale) - // Code that reads strings through the application context sees the new language too. val app = context.applicationContext @Suppress("DEPRECATION") app.resources.updateConfiguration( @@ -53,24 +37,16 @@ object AppLanguage { ) } - /** The locale Android chose for the app, ignoring the choice made here. */ fun system(context: Context): Locale = systemLocale ?: context.resources.configuration.locales[0] ?: Locale.getDefault() - /** The locale the app is shown in: the chosen language, or else the system's. */ fun effective(context: Context): Locale = chosen(context).takeIf { it.isNotEmpty() }?.let(Locale::forLanguageTag) ?: system(context) - /** Remembers the system's locale; call with the untouched base context, before [wrap]. */ fun noteSystem(base: Context) { if (systemLocale == null) systemLocale = base.resources.configuration.locales[0] } - /** - * [base], showing the chosen language: for an Application's or a Service's attachBaseContext. - * Only the locale is overridden, so screen size, density and night mode keep following the - * device as they change. - */ fun wrap(base: Context): Context { noteSystem(base) val tag = chosen(base).ifEmpty { return base } @@ -79,11 +55,6 @@ object AppLanguage { return base.createConfigurationContext(override(locale)) } - /** - * Applies the chosen language to an activity; call from its attachBaseContext, after super. - * applyOverrideConfiguration keeps the locale through rotation and window resizes, which an - * activity here handles itself rather than being recreated. - */ fun applyTo(activity: Activity, base: Context) { noteSystem(base) val tag = chosen(base).ifEmpty { return } @@ -92,13 +63,10 @@ object AppLanguage { activity.applyOverrideConfiguration(override(locale)) } - /** Follows a change of the system's language while the process lives (Application.onConfigurationChanged). */ fun systemChanged(config: Configuration) { config.locales[0]?.let { systemLocale = it } } - // A new Configuration's fontScale is 1, which as an override would undo the user's font size; - // 0 leaves it undefined, so only the locale is changed. private fun override(locale: Locale) = Configuration().apply { fontScale = 0f; setLocales(LocaleList(locale)) } private val latinAmerica = setOf( @@ -106,7 +74,6 @@ object AppLanguage { "PE", "PR", "PY", "SV", "US", "UY", "VE", ) - /** Which of the app's languages [locale] reads as, or null when the app does not ship it. */ fun supportedTag(locale: Locale): String? = when (locale.language) { "en", "fr", "ru", "ja", "ko", "ms", "cs", "da", "de", "it", "hu", "nl", "nb", "pl", "ro", "fi", "sv", "vi", "tr", "el", "bg", "uk", "ar", "hi", "th" -> locale.language @@ -122,11 +89,9 @@ object AppLanguage { else -> null } - /** [locale]'s language named in itself, without its region unless the app tells them apart. */ fun displayName(locale: Locale): String = supportedTag(locale)?.let(::nativeName) ?: locale.getDisplayLanguage(locale).replaceFirstChar { it.titlecase(locale) } - /** A language's name in that language, for the picker: what a reader of it looks for. */ fun nativeName(tag: String): String = when (tag) { "en" -> "English" "id" -> "Bahasa Indonesia" diff --git a/app/src/main/java/com/droiddeck/launcher/core/ArchivePaths.java b/app/src/main/java/com/droiddeck/launcher/core/ArchivePaths.java index 3f9eaee44..8f6a8cb6e 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/ArchivePaths.java +++ b/app/src/main/java/com/droiddeck/launcher/core/ArchivePaths.java @@ -3,18 +3,9 @@ package com.droiddeck.launcher.core; import java.io.File; import java.io.IOException; -/** - * Where an archive entry may land. An entry name ("../x", "/etc/x") or a symlink already written - * by an earlier entry can point outside the directory being filled; the canonical path follows - * both, so anything that does not resolve inside it is refused. - */ public final class ArchivePaths { private ArchivePaths() {} - /** - * The file for {@code name} under {@code destination}, or null when it would resolve outside - * it. The destination itself counts as inside (a "./" entry). - */ public static File inside(File destination, String name) throws IOException { String base = destination.getCanonicalPath(); File file = new File(destination, name); diff --git a/app/src/main/java/com/droiddeck/launcher/core/CpuAllocationDefaults.kt b/app/src/main/java/com/droiddeck/launcher/core/CpuAllocationDefaults.kt index 08b0c59dd..adaa106a0 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/CpuAllocationDefaults.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/CpuAllocationDefaults.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.core import com.droiddeck.launcher.gpu.GpuInfo import com.droiddeck.launcher.gpu.SocNames -/** Device defaults only: affinity does not reserve cores or lock their clocks. */ object CpuAllocationDefaults { data class Allocation(val client: String, val game: String) @@ -12,7 +11,6 @@ object CpuAllocationDefaults { CpuCores.all.associateWith(CpuCores::maxGhz)) } - /** Select by topology, including faster SM8550 variants; never guess core indices. */ fun forDevice(model: String, platform: String, frequencies: Map<Int, Double?>): Allocation? { val chip = model.trim().uppercase().substringBefore('-') val gen2 = chip in setOf("SM8550", "QCS8550") || diff --git a/app/src/main/java/com/droiddeck/launcher/core/CpuCores.kt b/app/src/main/java/com/droiddeck/launcher/core/CpuCores.kt index cf39812cf..b3dca67cc 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/CpuCores.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/CpuCores.kt @@ -2,17 +2,9 @@ package com.droiddeck.launcher.core import java.io.File -/** - * The device's cores as taskset names them, for the two masks a Steam session can carry. - * - * A list is stored the way taskset's `-c` takes it - comma-separated core numbers - so what the - * dialog produces is what the session script and the Proton wrappers consume unchanged. - */ object CpuCores { - /** Core numbers present on this device, in order. */ val all: List<Int> by lazy { (0 until Runtime.getRuntime().availableProcessors()).toList() } - /** The core's ceiling in GHz for the dialog's labels, or null where the kernel hides it. */ fun maxGhz(core: Int): Double? = FileUtils.readString(File("/sys/devices/system/cpu/cpu$core/cpufreq/cpuinfo_max_freq")) ?.trim()?.toLongOrNull()?.let { it / 1_000_000.0 } @@ -22,22 +14,14 @@ object CpuCores { fun format(cores: Set<Int>): String = cores.sorted().joinToString(",") - /** The stored list, or every core when nothing (or nothing valid) is stored. */ fun listOrAll(list: String): String { val cores = parse(list) return format(if (cores.isEmpty()) all.toSet() else cores) } - /** An all-core game must undo the affinity inherited from a pinned Steam client. */ fun gameLaunchList(list: String, clientPinned: Boolean): String = restrictionOrEmpty(list).ifEmpty { if (clientPinned) listOrAll("") else "" } - /** - * The list only when it is a real restriction, else "". Empty, absent, or naming every core - * all mean "no preference": pinning a process to all cores is what the scheduler does - * unaided, and passing it would only put a meaningless line in the session log. Bannerlator's - * rule, kept for the game mask. - */ fun restrictionOrEmpty(list: String): String { val cores = parse(list) return if (cores.isEmpty() || cores.size >= all.size) "" else format(cores) diff --git a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt index 77ff35fb6..7d23f91fb 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt @@ -21,15 +21,6 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * `device.txt`: what this device is and what the session was told to do, written the moment a - * session starts so a log can be read months later without asking the reporter a single question. - * - * **Nothing identifying goes in here.** No serial number, no Android ID, no IMEI, no account name, - * no network names - only what the hardware is, what the ROM is, and which of our own settings - * were in effect. The one thing that comes close is the build fingerprint, which names the ROM and - * its build date and is the same string on every unit of that model. - */ object DeviceReport { private const val TAG = "DeviceReport" diff --git a/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt b/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt index 1af4095be..325ed7d43 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt @@ -5,26 +5,14 @@ import android.os.Build import com.droiddeck.launcher.R import java.io.File -/** - * Whether this device can draw a Linux session at all. - * - * The runtime draws with Turnip, an Adreno driver. On Mali, Xclipse and PowerVR the compositor - * gets no usable Vulkan device and a session comes up as sound over a black screen - a failure - * with nothing in it to read. The app cannot fix that, so it says so before the download rather - * than after it. Adreno is recognised by what only Qualcomm's stack has: the KGSL node, or the - * vendor's own Vulkan driver at its usual path. - */ object DeviceSupport { fun adreno(): Boolean = File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists() - /** The chip as the device names it, for the card that explains the refusal. */ fun gpuName(context: Context): String { val soc = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it.isNotBlank() && it != Build.UNKNOWN } else null return soc?.let { "$it (${Build.HARDWARE})" } ?: Build.HARDWARE.ifBlank { context.getString(R.string.gpu_this_gpu) } } - /** False on the Odin 3 (CQ8725S): a low-latency Wi-Fi lock makes its WLAN firmware hang, and - * that firmware reboots the whole device instead of restarting Wi-Fi. */ fun lowLatencyWifiSafe(): Boolean = Build.VERSION.SDK_INT < 31 || Build.SOC_MODEL != "CQ8725S" } diff --git a/app/src/main/java/com/droiddeck/launcher/core/Downloader.java b/app/src/main/java/com/droiddeck/launcher/core/Downloader.java index 254341221..1e63ef982 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/Downloader.java +++ b/app/src/main/java/com/droiddeck/launcher/core/Downloader.java @@ -12,11 +12,6 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; -/** - * The two fetches this app makes: the runtime catalog (a few hundred bytes of JSON) and the - * runtime tarball itself (~790 MB). Plain HttpURLConnection - nothing here needs a client library, - * and one fewer dependency is one fewer thing that can fail a release build. - */ public final class Downloader { private static final String TAG = "Downloader"; private static final int TIMEOUT_MS = 30_000; @@ -44,11 +39,6 @@ public final class Downloader { } } - /** - * Downloads to {@code destination}, reporting a 0..1 fraction (or -1 when the server sends no - * length). {@code resume} continues a partial file with a Range request, which is what makes a - * 790 MB download survive a dropped connection instead of starting over. - */ public static boolean downloadFile(String url, File destination, boolean resume, java.util.function.Consumer<Float> progress) { HttpURLConnection connection = null; @@ -59,10 +49,8 @@ public final class Downloader { int code = connection.getResponseCode(); boolean appending = code == HttpURLConnection.HTTP_PARTIAL; if (code / 100 != 2) { - // The server ignored or refused the range: start the file again. if (have > 0) { connection.disconnect(); - //noinspection ResultOfMethodCallIgnored destination.delete(); return downloadFile(url, destination, false, progress); } @@ -73,7 +61,7 @@ public final class Downloader { long length = connection.getContentLengthLong(); long total = length < 0 ? -1 : length + have; File parent = destination.getParentFile(); - if (parent != null && !parent.isDirectory()) //noinspection ResultOfMethodCallIgnored + if (parent != null && !parent.isDirectory()) parent.mkdirs(); long written = have; try (InputStream in = new BufferedInputStream(connection.getInputStream(), 1 << 16); @@ -83,8 +71,6 @@ public final class Downloader { for (int read = in.read(buffer); read > 0; read = in.read(buffer)) { out.write(buffer, 0, read); written += read; - // A callback per 64 KB would be tens of thousands of UI hops; once per MB is - // enough to move a progress bar smoothly. if (progress != null && written - lastReport > (1 << 20)) { lastReport = written; progress.accept(total > 0 ? written / (float) total : -1f); diff --git a/app/src/main/java/com/droiddeck/launcher/core/FexPreset.kt b/app/src/main/java/com/droiddeck/launcher/core/FexPreset.kt index 832c9d039..6914bffa8 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/FexPreset.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/FexPreset.kt @@ -28,7 +28,6 @@ object FexPreset { fun byId(id: String): Preset = all.firstOrNull { it.id == id } ?: all[0] - /** Game-launch variables in KEY=VALUE form. */ fun env(id: String): List<String> = byId(id).env val environmentKeys: Set<String> by lazy { diff --git a/app/src/main/java/com/droiddeck/launcher/core/FileUtils.java b/app/src/main/java/com/droiddeck/launcher/core/FileUtils.java index 8190e2877..4cbba0897 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/FileUtils.java +++ b/app/src/main/java/com/droiddeck/launcher/core/FileUtils.java @@ -18,14 +18,12 @@ public final class FileUtils { private FileUtils() {} - /** Recursive delete that does not follow symlinks - a rootfs is full of them. */ public static void delete(File file) { if (file == null || !file.exists() && !isSymlink(file)) return; if (file.isDirectory() && !isSymlink(file)) { File[] children = file.listFiles(); if (children != null) for (File child : children) delete(child); } - //noinspection ResultOfMethodCallIgnored file.delete(); } @@ -37,7 +35,6 @@ public final class FileUtils { } } - /** Empties a directory without removing it. */ public static void clear(File dir) { if (dir == null || !dir.isDirectory()) return; File[] children = dir.listFiles(); @@ -47,7 +44,7 @@ public final class FileUtils { public static boolean writeString(File file, String content) { try { File parent = file.getParentFile(); - if (parent != null && !parent.isDirectory()) //noinspection ResultOfMethodCallIgnored + if (parent != null && !parent.isDirectory()) parent.mkdirs(); Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8)); return true; @@ -81,10 +78,9 @@ public final class FileUtils { } } - /** Copies an asset out of the apk, creating the parent directory. */ public static boolean copyAsset(Context context, String assetPath, File destination) { File parent = destination.getParentFile(); - if (parent != null && !parent.isDirectory()) //noinspection ResultOfMethodCallIgnored + if (parent != null && !parent.isDirectory()) parent.mkdirs(); try (InputStream in = context.getAssets().open(assetPath); OutputStream out = new FileOutputStream(destination)) { diff --git a/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt b/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt index dc6483118..045b52ed4 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt @@ -29,25 +29,15 @@ object GameEnvironment { "MESA_SHADER_CACHE_DISABLE" to "false", "VKD3D_FEATURE_LEVEL" to "12_2", "VKD3D_SHADER_MODEL" to "6_9", - // .NET 7 and later reserve 256 GB of address space for the GC heap at startup, more than an - // Android process has free, and the runtime fails to start. 32 GB is plenty for a game. - // Older NativeAOT runtimes require bare hex digits. CoreCLR accepts this form too. "DOTNET_GCRegionRange" to "800000000", - // .NET's W^X writes JIT code through a second mapping that FEX does not watch, so FEX keeps - // running the stale translation and the game hangs at startup. "DOTNET_EnableWriteXorExecute" to "0", ).apply { FexPreset.env(preset).forEach { put(it.substringBefore('='), it.substringAfter('=')) } - // Steam settings' Force SSBS: droiddeck-game-env preloads libssbs.so into the game's Wine. if (forceSsbs) put(FORCE_SSBS, "1") } const val FORCE_SSBS = "DROIDDECK_FORCE_SSBS" - /** - * A game's Start without Steam: droiddeck-game-env launches it without Proton's steam.exe (umu's - * launch where the Proton has it, else runinprefix), for games that close right away. - */ const val NO_STEAM = "DROIDDECK_NO_STEAM" fun gameNoSteam(config: Config, scope: String): Boolean = diff --git a/app/src/main/java/com/droiddeck/launcher/core/Hashes.kt b/app/src/main/java/com/droiddeck/launcher/core/Hashes.kt index 072874c87..0e4dce500 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/Hashes.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/Hashes.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.core import java.io.File import java.security.MessageDigest -/** File checksums as lowercase hex, and GitHub's release-asset digests, in one place. */ object Hashes { private val GITHUB_SHA256 = Regex("(?i)sha256:[0-9a-f]{64}") @@ -13,11 +12,9 @@ object Hashes { @JvmStatic fun sha512(file: File): String = hex(file, "SHA-512") - /** True when [digest] is GitHub's asset digest form, "sha256:" and 64 hex digits. */ @JvmStatic fun isGithubSha256(digest: String?): Boolean = digest != null && digest.matches(GITHUB_SHA256) - /** The hex of a GitHub asset digest ("sha256:<hex>"), or null when [digest] is not one. */ @JvmStatic fun githubSha256(digest: String?): String? = digest?.takeIf { isGithubSha256(it) }?.substringAfter(':') diff --git a/app/src/main/java/com/droiddeck/launcher/core/HostEnvironment.kt b/app/src/main/java/com/droiddeck/launcher/core/HostEnvironment.kt index f40c93f82..e9b06990e 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/HostEnvironment.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/HostEnvironment.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.core -/** The variables a host process is started with, in the order they were set. */ class HostEnvironment { private val entries = LinkedHashMap<String, String>() @@ -8,6 +7,5 @@ class HostEnvironment { entries[name] = value } - /** `NAME=VALUE` lines, the shape ProcessBuilder and proot take. */ fun asArray(): Array<String> = entries.map { (name, value) -> "$name=$value" }.toTypedArray() } diff --git a/app/src/main/java/com/droiddeck/launcher/core/HostProcess.kt b/app/src/main/java/com/droiddeck/launcher/core/HostProcess.kt index 550d55a52..4a8b12934 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/HostProcess.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/HostProcess.kt @@ -4,27 +4,9 @@ import android.util.Log import java.io.File import java.util.function.Consumer -/** - * Starts a process on the Android side - proot, the audio daemon, logcat - and hands back its pid. - * - * The pid matters: stopping a session means killing that exact process (proot runs with - * --kill-on-exit, so the guest tree goes with it), and java.lang.Process does not expose it before - * API 33, so it is read from the implementation's private field. Output can be drained line by - * line to a callback; without one it goes to /dev/null, never to a pipe nobody reads, which would - * block the child the moment the pipe filled. - */ object HostProcess { private const val TAG = "HostProcess" - /** - * @param command the program and its arguments as one line; a backslash escapes a space, since - * paths under /data are full of them. - * @param env `NAME=VALUE` lines added to the app's own environment (proot needs nothing from - * the inherited set, and the guest starts with `env -i` anyway), or null. - * @param onExit told the exit status from a waiting thread, or null. - * @param onLine told each line of stdout and stderr, or null to discard both. - * @return the pid, or -1 when the process could not be started. - */ @JvmStatic fun start(command: String, env: Array<String>?, dir: File?, onExit: Consumer<Int>?, onLine: Consumer<String>?): Int { val argv = split(command) @@ -46,14 +28,13 @@ object HostProcess { try { process.inputStream.bufferedReader().useLines { lines -> lines.forEach { onLine.accept(it) } } } catch (e: Exception) { - // The process ended or the pipe closed; nothing more to read. } }, "proc-out-$pid").apply { priority = Thread.NORM_PRIORITY - 1; isDaemon = true; start() } } if (onExit != null) { Thread({ var status = -1 - try { status = process.waitFor() } catch (e: InterruptedException) { /* reported as -1 */ } + try { status = process.waitFor() } catch (e: InterruptedException) { } onExit.accept(status) }, "proc-wait-$pid").apply { priority = Thread.NORM_PRIORITY - 1; start() } } @@ -75,7 +56,6 @@ object HostProcess { -1 } - /** Splits a command line on spaces; a backslash keeps the space that follows it. */ @JvmStatic fun split(command: String): List<String> { val parts = ArrayList<String>() diff --git a/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt b/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt index 4406a7028..969442631 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt @@ -1,64 +1,28 @@ package com.droiddeck.launcher.core -/** - * Scrubs credentials out of a log line before it is written somewhere a user will share. - * - * The Steam client's own logs are the reason this exists: they carry the account's session token - * (logged as a bare number after "Using JWT"), machine-auth GUIDs, `token=`/`sessionid=` pairs, - * Steam Guard codes, 32-hex WebAPI keys and the account's SteamID. A session folder from this app - * is meant to be attached to a bug report as it is, so none of that may be in it. - * - * Ported from Bannerlator's {@code SteamLogRedactor} (GPL-3.0), which was written against real - * client logs; the account-name registration is left out because this app never learns it. - * - * What is deliberately KEPT, because it is what makes a log worth reading: EResult codes, CM host - * names and Valve's server addresses, timings, token expiry dates, app IDs, pids, connection-state - * changes and file paths. A SteamID is masked rather than deleted - first four and last four digits - * survive - so a reader can still tell two accounts' lines apart without the value identifying either. - * - * The device's OWN addresses are not kept: the client's IPv6 check logs "external address <ours>" - * about twenty times a session, and a public IPv6 address is the user's. [learnOwnAddresses] reads - * the link the session was given (etc/droiddeck-net) and every address on the same /64 - privacy - * addresses rotate inside it - or an own public IPv4 is replaced wherever it appears. Likewise every - * Steam account and persona name on the device ([learnAccounts]), for users who sign in with an - * account name rather than an email. - */ object LogRedactor { private val EMAIL = Regex("[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}") - /** "Using JWT 25484942796017334" - the client logs its session token as digits, not base64. */ private val JWT_LABELLED = Regex("(?i)(\\bJWT[\\s=:]+)(\\d{8,})") - /** A 3-part base64url JWT logged verbatim (a refresh or access token). */ private val JWT_BASE64 = Regex("ey[A-Za-z0-9_\\-]{6,}\\.[A-Za-z0-9_\\-]{6,}\\.[A-Za-z0-9_\\-]{6,}") - /** Machine-auth and other GUIDs. */ private val GUID = Regex("\\b[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\\b") - /** - * key=value secrets. A bare `key` is excluded on purpose so "key=english" survives; a real - * WebAPI key is caught by [WEBAPI_KEY]. The value class excludes `<` and `>` so running this - * twice over an already-scrubbed line changes nothing. - */ private val SECRET_KV = Regex( "(?i)\\b(access[_-]?token|refresh[_-]?token|auth[_-]?token|authtoken|token|authcode|" + "auth[_-]?ticket|ticket|sessionid|steamloginsecure|webapikey|api[_-]?key|" + "machine[_-]?auth(?:[_-]?token)?|machineauth|password|passwd|pwd|secret)" + "(\\s*[=:]\\s*|=)([^\\s\"'<>&;,]{4,})" ) - /** A Steam Guard code, only where the text around it says that is what it is. */ private val GUARD_CODE = Regex( "(?i)((?:steam\\s*)?guard\\s*code[\\s:=]*|two[\\s-]?factor[\\s:=]*|2fa[\\s:=]*)([A-Za-z0-9]{5})" ) - /** Exactly 32 hex - a WebAPI key. Bounded so a 40-hex depot chunk id is left alone. */ private val WEBAPI_KEY = Regex("\\b[0-9A-Fa-f]{32}\\b") - /** A long opaque run after a sensitive word, for anything the rules above missed. */ private val RESIDUAL = Regex( "(?i)\\b(jwt|token|ticket|sessionid|steamloginsecure|machineauth)\\b[\\s=:]*([A-Za-z0-9+/=_\\-]{12,})" ) private val LONG_TOKEN = Regex("[A-Za-z0-9_\\-]{88,}") private val STEAMID64 = Regex("\\b(76561)(\\d{8})(\\d{4})\\b") private val STEAMID3 = Regex("\\[U:1:(\\d+)]") - /** "external address 2607:..." / "external IP: 203.0.113.9" - the client stating ours. */ private val EXTERNAL_ADDR = Regex("(?i)(external\\s+(?:ip\\s+)?address\\s*[:=]?\\s*|external\\s+ip\\s*[:=]?\\s*)([0-9A-Fa-f:.]{7,})") private val IPV4 = Regex("(?<![0-9.])(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})(?![0-9.])") - /** An IPv6 literal: "::" somewhere, or all eight groups (a clock's 10:03:05 is neither). */ private val IPV6 = Regex("(?<![0-9A-Za-z:])[0-9A-Fa-f]{0,4}(?::[0-9A-Fa-f]{0,4}){2,7}(?:%[A-Za-z0-9_.]+)?(?![0-9A-Za-z:])") private val MAC = Regex("(?i)(?<![0-9a-z:-])[0-9a-f]{2}([:-])(?:[0-9a-f]{2}\\1){4}[0-9a-f]{2}(?![0-9a-z:-])") private val MAC_FIELD = Regex( @@ -70,20 +34,9 @@ object LogRedactor { "[\"']?\\s*[:=]\\s*)(\"[^\"\\r\\n<>]+\"|'[^'\\r\\n<>]+'|[^\\s\"'<>&;,\\[\\]{}]+)" ) - /** - * The account a Steam UI login line names ("Login: OnLoginStateChange <account> 2 1 0 0"): a - * user who signs in with a plain account name, not an email, had it in every log. The single - * space then a non-space keeps "OnLoginStateChange 0 1 0 0" (no account yet) as it is. - */ private val LOGIN_STATE = Regex("(?i)(OnLoginStateChange:? )([^\\s<]\\S*)") private val LOGIN_USERS = Regex("(OnLoginUsersChanged )(\\S.*)$") - /** - * An account name as a field: `AccountName`, `account_name`, `username` (`"AccountName" - * "someone"` in a VDF, `account_name=...`, `"username": "..."`) and `login` - the last only - * as `login=`, a quoted key or a VDF pair, so "Login: OnLoginStateChange ..." keeps its label. - * A quoted value is blanked whole; one already blanked is left as it is. - */ private const val ACCOUNT_VALUE = "(\"[^\"<\\r\\n]+\"|'[^'<\\r\\n]+'|[^\\s\"'<>&;,}]+)" private val ACCOUNT_KV = Regex( "(?i)(?<![A-Za-z0-9_])(account[_-]?name|user[_-]?name|epicusername)" + @@ -93,18 +46,11 @@ object LogRedactor { "(?i)(?<![A-Za-z0-9_])(login)(\\s*=\\s*|[\"']\\s*[=:]\\s*|[\"']\\s+)" + ACCOUNT_VALUE ) - /** A learned pattern and the text (lowercase) a line must hold for it to match; null when none can be said. */ private class Learned(val regex: Regex, val needs: String?) - /** The device's Steam accounts and persona names as patterns (see [learnAccounts]). */ @Volatile private var accounts: List<Learned> = emptyList() - /** - * Learns every Steam account on the device from the client's loginusers.vdf - its AccountName - * and PersonaName - so they are replaced wherever a log mentions them, whoever the user is. - * Names under three characters are skipped: blanking every "a" would ruin a log. - */ fun learnAccounts(loginUsers: java.io.File) { val found = ArrayList<Learned>() try { @@ -121,21 +67,14 @@ object LogRedactor { accounts = found } - /** Everything the session's runtime can tell about whose logs these are: addresses and accounts. */ fun learnFromRuntime(root: java.io.File) { learnOwnAddresses(java.io.File(root, "etc/droiddeck-net")) learnAccounts(java.io.File(root, "root/.local/share/Steam/config/loginusers.vdf")) } - /** This device's public addresses as patterns (see [learnOwnAddresses]); empty until learned. */ @Volatile private var own: List<Learned> = emptyList() - /** - * Learns the device's addresses from the link file the app writes for the session - * (`addr <address> <prefix>` lines). Global IPv6 become their /64, public IPv4 stay exact; - * private, link-local and loopback addresses identify nobody and are left out. - */ fun learnOwnAddresses(linkFile: java.io.File) { val found = ArrayList<Learned>() try { @@ -144,7 +83,6 @@ object LogRedactor { when (kind(addr)) { "public IPv6" -> { val groups = expand6(addr)?.take(4) ?: return@forEachLine - // Any spelling of an address in this /64: leading zeros dropped, :: anywhere after. val prefix = groups.joinToString(":") { g -> "0{0,3}" + Regex.escape(g.trimStart('0').ifEmpty { "0" }) } found += Learned( Regex("(?i)(?<![0-9A-Fa-f:])$prefix(?::[0-9A-Fa-f]{0,4}){1,4}(?:%[A-Za-z0-9_.]+)?"), @@ -160,7 +98,6 @@ object LogRedactor { own = found } - /** "public IPv6", "private IPv4", "link-local IPv6", ... for an address literal; null if it is not one. */ fun kind(address: String): String? { val a = address.substringBefore('%') IPV4.matchEntire(a)?.let { m -> @@ -184,7 +121,6 @@ object LogRedactor { } } - /** The eight groups of an IPv6 literal, or null. */ private fun expand6(address: String): List<String>? { val a = address.substringBefore('%') if (!a.contains(':') || a.count { it == ':' } > 7 || a.contains(":::")) return null @@ -200,17 +136,12 @@ object LogRedactor { return parts.map { it.lowercase() } } - /** - * Every IP address in [text] replaced by what kind it is ("<public IPv6>"), for reports whose - * point is the shape of the network, not its numbers. - */ fun describeAddresses(text: String): String { var out = IPV6.replace(text) { m -> kind(m.value)?.let { "<$it>" } ?: m.value } out = IPV4.replace(out) { m -> kind(m.value)?.let { "<$it>" } ?: m.value } return out } - /** A file worth scrubbing: text (no NUL in its first 4 KB) and not huge. */ fun isText(file: java.io.File): Boolean = try { file.isFile && file.length() < 64L * 1024 * 1024 && file.inputStream().use { input -> val buf = ByteArray(4096) @@ -221,34 +152,16 @@ object LogRedactor { false } - /** - * Bump whenever a pattern is added or tightened. A session folder records the version it was - * scrubbed under (SessionArtifacts.SCRUBBED_TREE_MARKER), and a share copies a file as it is - * only when that matches, so a folder scrubbed under older rules is scrubbed again on the way - * out. - */ const val RULES_VERSION = 3 - /** - * A private IPv4 address (10/8, 172.16/12, 192.168/16, link-local 169.254/16): the home - * network's layout, e.g. its router as the resolver. Blanked in a shared zip only; public - * server addresses stay. Not part of a longer dotted number (a version like 10.0.19041.1). - */ private val LAN_IPV4 = Regex( "(?<![\\d.])(?:10\\.\\d{1,3}|192\\.168|172\\.(?:1[6-9]|2\\d|3[01])|169\\.254)\\.\\d{1,3}\\.\\d{1,3}(?!\\.?\\d)" ) - /** [src]'s lines, scrubbed, to [out]. */ fun scrubTo(src: java.io.File, out: java.io.Writer) { src.forEachLine { line -> out.write(redact(line)); out.write("\n") } } - /** - * The last pass for a file going into a shared zip: [redact], then the rules the stores' log - * uses too ([SecretScrub]) - URLs without query, fragment and user:password and with any - * token-bearing path segment blanked, token values and Authorization / Cookie headers blanked - * - and private IPv4 addresses as `<lan-address>`. A line already clean comes out unchanged. - */ fun redactForShare(line: String): String { if (line.isEmpty()) return line return try { @@ -259,23 +172,15 @@ object LogRedactor { } } - /** [src]'s lines through [redactForShare], to [out]: the pass every text file in a shared zip gets. */ fun scrubForShare(src: java.io.File, out: java.io.Writer) { src.forEachLine { line -> out.write(redactForShare(line)); out.write("\n") } } - /** `<redacted:account>`, inside [value]'s quotes when it had them. */ private fun quoted(value: String): String { val q = value.first().takeIf { it == '"' || it == '\'' }?.toString().orEmpty() return "$q<redacted:account>$q" } - /** - * A line on its way through [redact]. Each rule runs only on a line holding what it cannot match - * without - a word, enough separators, a long enough run: most of a Steam log is lines no rule - * touches, and a session's logs went through every rule at about a megabyte a second. A line with anything but ASCII in it goes through - * every case-insensitive rule, since Android's regex folds case more widely than lowercase(). - */ private class Line(var text: String) { private val unfolded = text.any { it.code >= 0x80 } private var lower: String? = null @@ -308,7 +213,6 @@ object LogRedactor { private fun isHex(c: Char) = c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F' - /** [line] with every credential shape replaced. Null- and exception-safe by construction. */ fun redact(line: String): String { if (line.isEmpty()) return line return try { @@ -328,7 +232,6 @@ object LogRedactor { } if (out.has("guard", "factor", "2fa")) out.set(GUARD_CODE.replace(out.text) { "${it.groupValues[1]}<redacted:code>" }) if (out.hasRun(32, ::isHex)) out.set(WEBAPI_KEY.replace(out.text, "<redacted:key>")) - // Mask, not delete: the last four digits let a reader correlate lines to one account. if ("76561" in out.text) out.set(STEAMID64.replace(out.text) { "${it.groupValues[1]}********${it.groupValues[3]}" }) if ("[U:1:" in out.text) out.set(STEAMID3.replace(out.text) { m -> val id = m.groupValues[1] @@ -352,8 +255,6 @@ object LogRedactor { } out.text } catch (t: Throwable) { - // A log line is never worth crashing a session over, but an unscrubbed one must not - // reach the file either. "<redaction failed; line withheld>" } } diff --git a/app/src/main/java/com/droiddeck/launcher/core/NetworkReport.kt b/app/src/main/java/com/droiddeck/launcher/core/NetworkReport.kt index b8a0e5dae..d06b5c593 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/NetworkReport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/NetworkReport.kt @@ -10,24 +10,11 @@ import java.io.File import java.net.Inet4Address import java.net.Inet6Address -/** - * `network.txt`: what the session was given to work with, for the reports that say "the client - * thinks WiFi is off" or "Firefox cannot resolve anything". - * - * The runtime's NetworkManager bridge reports Android's active link. Include whether the phone - * had a validated link and which DNS servers the app wrote into the runtime's `resolv.conf`. - * - * **No network names.** The SSID is identifying and is deliberately not collected; the transport - * type, the addresses' families and the DNS servers are what a diagnosis needs. - */ object NetworkReport { private const val TAG = "NetworkReport" fun write(context: Context, target: File) { try { - // Addresses as their kind ("<public IPv6>"), never the numbers: a public address is the - // user's, and this file goes into shared session logs. The session's made-up hardware - // address is stable per device (seeded from ANDROID_ID), so it is masked too. val text = LogRedactor.describeAddresses(build(context)) .replace(Regex("(?m)^(\\s*mac )\\S+"), "$1<masked>") target.writeText(text) @@ -82,8 +69,6 @@ object NetworkReport { append("\nWhat the runtime was given\n") append("--------------------------\n") - // The app writes this file from the live link at every change; it is what everything inside - // the session resolves with, so a mismatch with the system's list above is the bug itself. val resolv = File(LinuxRuntime.rootDir(context), "etc/resolv.conf") if (resolv.isFile) { append("etc/resolv.conf:\n") diff --git a/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt b/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt index 6a39746b7..cbad4349d 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt @@ -5,7 +5,6 @@ import android.os.Build import android.provider.Settings import com.droiddeck.launcher.R -/** Android's phantom process monitor can kill the many child processes used by a Steam session. */ enum class PhantomProcessStatus { NOT_APPLICABLE, DISABLED, @@ -68,12 +67,6 @@ object PhantomProcessLimit { return status(global, systemProperty(OVERRIDE_PROPERTY)) } - /** - * The value Android itself acts on, in FeatureFlagUtils.isEnabled's order: the global setting - * when it is set, otherwise the feature-flag override property. Developer options' "Disable - * child process restrictions" writes only the property, so a device switched off there has no - * global setting at all and must not be reported as unset. - */ fun status(global: String?, override: String?): PhantomProcessStatus { val value = global?.trim()?.lowercase()?.takeIf { it.isNotEmpty() } ?: override?.trim()?.lowercase()?.takeIf { it.isNotEmpty() } @@ -122,7 +115,6 @@ object PhantomProcessLimit { fun fixSentence(context: Context, sdk: Int = Build.VERSION.SDK_INT): String = context.getString(if (hasDeveloperToggle(sdk)) R.string.phantom_fix_toggle else R.string.phantom_fix_no_toggle) - /** English: for the device report. */ fun reportValue(status: PhantomProcessStatus): String = when (status) { PhantomProcessStatus.DISABLED -> "disabled (good - the OS will not kill the session's children)" PhantomProcessStatus.ENABLED -> "ENABLED (the OS may kill the session with no log; turn off Restrict child processes)" diff --git a/app/src/main/java/com/droiddeck/launcher/core/SecretScrub.kt b/app/src/main/java/com/droiddeck/launcher/core/SecretScrub.kt index c91ca60ef..7ac3f1801 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/SecretScrub.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/SecretScrub.kt @@ -1,21 +1,5 @@ package com.droiddeck.launcher.core -/** - * The one set of rules that keeps URLs and tokens out of anything the app writes or shares: the - * stores' log ([com.droiddeck.launcher.stores.StoreLog.redactLine]) and the last pass over every - * text file in a shared log zip ([LogRedactor.redactForShare]). - * - * - Every URL loses its query, fragment and `user:password@`. [Urls.SHORT] then keeps only the host - * and a plain first path segment (the stores' lines, where a GOG secure link carries its token in - * the path); [Urls.KEEP_PATH] keeps the path but blanks any segment that carries a token. - * - Outside URLs, the values of token-like keys (`token`, `__token__`, `f_token`, `hdnts`, access / - * refresh / id tokens; exchange and authorization codes of four characters or more, and a bare - * `code=` where it is one: in a query string or on a line with OAuth keys such as `client_id`, - * `redirect_uri` or `state`, never an 8-hex exception code like Proton's `code=c0000005`) and of - * Authorization / Cookie headers are blanked with [mark]. - * - * A line already scrubbed comes out unchanged. - */ object SecretScrub { enum class Urls { SHORT, KEEP_PATH } @@ -23,12 +7,9 @@ object SecretScrub { private val SAFE_SEGMENT = Regex("""[A-Za-z0-9._\-]{1,40}""") private val TOKEN_SEGMENT = Regex("""(?i)(token|hdnts|hmac|signature|sig|exp)=""") private val TOKEN = Regex("""(?i)(?<![A-Za-z0-9_])(__token__|f_token|hdnts|access_token|refresh_token|id_token|token)(["']?\s*[=:]\s*["']?)[^\s&;,"'<>}]+""") - /** Codes that are only ever OAuth grants. */ private val GRANT_CODE = Regex("""(?i)(?<![A-Za-z0-9_])(exchange_code|authorization_?code)(["']?\s*=\s*["']?)[^\s&;,"'<>}]{4,}""") - /** A bare `code=`: a grant only right after `?` / `&` (a query string) or beside OAuth keys ([OAUTH_CONTEXT]). */ private val CODE = Regex("""(?i)(?<![A-Za-z0-9_])(code)(["']?\s*=\s*["']?)([^\s&;,"'<>}]{4,})""") private val OAUTH_CONTEXT = Regex("""(?i)(?<![A-Za-z0-9_])(client_id|client_secret|redirect_uri|state|grant_type|response_type|access_token|refresh_token|id_token)["']?\s*[=:]""") - /** A Windows exception or status code (`code=c0000005`, `code=0x80000003`): never a grant. */ private val STATUS_CODE = Regex("""(?i)(0x)?[0-9a-f]{8}""") private val HEADER = Regex("""(?i)\b(authorization|cookie|set-cookie)(\s*[:=]\s*).+""") diff --git a/app/src/main/java/com/droiddeck/launcher/core/SessionLogCapture.kt b/app/src/main/java/com/droiddeck/launcher/core/SessionLogCapture.kt index 67b924ec2..965a4950d 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/SessionLogCapture.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/SessionLogCapture.kt @@ -5,24 +5,6 @@ import android.util.Log import java.io.BufferedWriter import java.io.File -/** - * `app.log`: what the app itself said while the session ran, and `crash.log`: what Android's crash - * buffer holds if something died hard. - * - * Everything this app decides is reported through `Log` - which driver was chosen and why, the - * audio line, a rival client being stopped, the session's exit status, a helper that was missing. - * None of it reaches a user's folder, so a report used to arrive with the settings visible in - * `device.txt` and no record of what actually happened. An app may always read back its own log - * entries, so the simplest complete answer is to run `logcat` filtered to our own pid and keep - * what it says. - * - * `logcat -b crash` earns its own file: proot once died before `main` over a missing library, and - * that appeared in the crash buffer and *nowhere else* - not in the session log, not in the main - * buffer. It is the first thing to read when a session "exits instantly". - * - * Both are best-effort. Where a ROM refuses to hand an app its own entries the files are written - * with a line saying so, which is better than their absence looking like silence. - */ object SessionLogCapture { private const val TAG = "SessionLogCapture" @@ -30,7 +12,6 @@ object SessionLogCapture { private var writer: BufferedWriter? = null private var target: File? = null - /** Start mirroring this process's log lines into [target]. Safe to call twice. */ @Synchronized fun start(target: File) { stop() @@ -41,8 +22,6 @@ object SessionLogCapture { out.flush() writer = out this.target = target - // -T 1 starts at the newest line rather than replaying the whole buffer; -v threadtime - // keeps the timestamps and thread ids that make two logs line up. pid = HostProcess.start( "/system/bin/logcat -v threadtime -T 1 --pid=" + Process.myPid(), null, target.parentFile, null, @@ -51,7 +30,6 @@ object SessionLogCapture { try { writer?.apply { write(line); newLine(); flush() } } catch (e: Exception) { - // A full or unmounted card must not take the session with it. } } } @@ -65,7 +43,6 @@ object SessionLogCapture { } } - /** Stops the capture only if it is the one writing into [dir]: a newer session's is left alone. */ @Synchronized fun stopFor(dir: File) { val t = target ?: return @@ -86,23 +63,14 @@ object SessionLogCapture { try { writer?.close() } catch (e: Exception) { - // nothing useful to do with a failure to close a log } writer = null } - /** - * Dump Android's crash buffer into [target]. Called at teardown: a session that was killed is - * exactly when this matters, and the buffer keeps its entries after the process is gone. - */ fun dumpCrashBuffer(target: File) { try { val lines = StringBuilder() val done = java.util.concurrent.CountDownLatch(1) - // Wait on the process ENDING, not on the first line arriving: the lines come through a - // callback on another thread, so a poll for "is it empty yet" declared an empty buffer - // every time and still cost the wait. exec() returns a pid, never an exit status - the - // first version printed that pid as "rc", which was nonsense in the file. val pid = HostProcess.start( "/system/bin/logcat -b crash -d -v threadtime -t 400", null, target.parentFile, { done.countDown() }, diff --git a/app/src/main/java/com/droiddeck/launcher/core/SessionPart.java b/app/src/main/java/com/droiddeck/launcher/core/SessionPart.java index 86f4380aa..bb5d13214 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/SessionPart.java +++ b/app/src/main/java/com/droiddeck/launcher/core/SessionPart.java @@ -2,12 +2,6 @@ package com.droiddeck.launcher.core; import android.content.Context; -/** - * One thing a running session depends on beside the guest process itself: the audio daemon, the - * rumble listener, the battery the client reads, the network-link file. The session starts its - * parts in order before the guest and stops them in reverse after it. A part is handed the - * application context, never an activity's, so nothing here can keep a screen alive. - */ public abstract class SessionPart { private Context app; @@ -15,7 +9,6 @@ public abstract class SessionPart { this.app = context.getApplicationContext(); } - /** The application context, valid after {@link #attach}. */ protected final Context app() { return app; } diff --git a/app/src/main/java/com/droiddeck/launcher/core/TarZst.java b/app/src/main/java/com/droiddeck/launcher/core/TarZst.java index 0a1110f23..c00030c01 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/TarZst.java +++ b/app/src/main/java/com/droiddeck/launcher/core/TarZst.java @@ -13,11 +13,6 @@ import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; -/** - * The small zstd tarballs that ship inside the apk (the Turnip driver, the PulseAudio modules). - * The runtime rootfs is not one of these - it needs hard links and symlinks preserved, which - * {@link com.droiddeck.launcher.runtime.LinuxRuntimeInstaller} handles itself. - */ public final class TarZst { private static final String TAG = "TarZst"; @@ -33,7 +28,6 @@ public final class TarZst { } public static boolean extract(InputStream source, File destination) { - //noinspection ResultOfMethodCallIgnored destination.mkdirs(); try (InputStream in = new ZstdCompressorInputStream(new BufferedInputStream(source, 1 << 16)); TarArchiveInputStream tar = new TarArchiveInputStream(in)) { @@ -42,15 +36,13 @@ public final class TarZst { File file = ArchivePaths.inside(destination, entry.getName()); if (file == null) continue; if (entry.isDirectory()) { - //noinspection ResultOfMethodCallIgnored file.mkdirs(); continue; } File parent = file.getParentFile(); - if (parent != null && !parent.isDirectory()) //noinspection ResultOfMethodCallIgnored + if (parent != null && !parent.isDirectory()) parent.mkdirs(); if (entry.isSymbolicLink()) { - //noinspection ResultOfMethodCallIgnored file.delete(); FileUtils.symlink(entry.getLinkName(), file.getAbsolutePath()); continue; @@ -59,7 +51,7 @@ public final class TarZst { try (OutputStream out = new FileOutputStream(file)) { FileUtils.copy(tar, out); } - if ((entry.getMode() & 0111) != 0) //noinspection ResultOfMethodCallIgnored + if ((entry.getMode() & 0111) != 0) file.setExecutable(true, false); } return true; diff --git a/app/src/main/java/com/droiddeck/launcher/core/TextureFiltering.kt b/app/src/main/java/com/droiddeck/launcher/core/TextureFiltering.kt index 97c07289f..66be28cb4 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/TextureFiltering.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/TextureFiltering.kt @@ -5,29 +5,15 @@ import kotlin.math.ln import kotlin.math.max import kotlin.math.min -/** - * Texture filtering for DirectX 9-11 games, as DXVK configuration options - * (d3d9/d3d11.samplerAnisotropy and samplerLodBias). VKD3D-Proton and WineD3D do not read them. - * Published into the game environment and read by DXVK when a game starts, so a change lands on - * the next launch, not the running game. - */ object TextureFiltering { - /** 0 = the game's own filtering; otherwise that anisotropy on every linearly filtered sampler. */ val ANISOTROPY = listOf(0, 2, 4, 8, 16) - /** Negative mip LOD bias added to the game's own: sharper textures at the cost of some shimmer. */ const val LOD_BIAS_OFF = "0" - /** Derived from how far the session is enlarged to the panel (see [autoLodBias]). */ const val LOD_BIAS_AUTO = "auto" val LOD_BIAS = listOf(LOD_BIAS_OFF, LOD_BIAS_AUTO, "-0.25", "-0.5", "-0.75", "-1.0") private const val LOD_BIAS_MIN = -2f - /** - * The mip LOD bias for a picture enlarged by [scale] (panel size over session size, long side - * to long side): -log2(scale), AMD's FSR 1 guidance (a 1.5x enlargement - 720p on a 1080p - * panel - gives -0.58). 0 when the session is not enlarged or the scale is unknown. - */ fun autoLodBias(scale: Float): Float { if (scale.isNaN() || scale <= 1f) return 0f return max((-ln(scale.toDouble()) / ln(2.0)).toFloat(), LOD_BIAS_MIN) @@ -39,7 +25,6 @@ object TextureFiltering { else -> choice.toFloatOrNull()?.let { max(min(it, 0f), LOD_BIAS_MIN) } ?: 0f } - /** The DXVK_CONFIG options for the two settings, "" when both are left to the game. */ fun dxvkOptions(anisotropy: Int, lodBiasChoice: String, autoBias: Float): String { val parts = ArrayList<String>(4) val aniso = if (anisotropy in ANISOTROPY) anisotropy else 0 @@ -49,7 +34,6 @@ object TextureFiltering { } val bias = lodBias(lodBiasChoice, autoBias) if (bias < 0f) { - // Locale.US: DXVK parses "-0.58", never "-0,58". val b = String.format(Locale.US, "%.2f", bias) parts += "d3d9.samplerLodBias = $b" parts += "d3d11.samplerLodBias = $b" diff --git a/app/src/main/java/com/droiddeck/launcher/core/WifiDiscovery.kt b/app/src/main/java/com/droiddeck/launcher/core/WifiDiscovery.kt index 40a6721dc..66cc77f91 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/WifiDiscovery.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/WifiDiscovery.kt @@ -7,11 +7,9 @@ import android.location.LocationManager import android.os.Build import com.droiddeck.launcher.session.SessionPrefs -/** Location is used only for Android's Wi-Fi names and scan results, never coordinates. */ object WifiDiscovery { val permissions = arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) - // targetSdk 28 accepts either grant for Wi-Fi scanning. fun permissionGranted(context: Context): Boolean = permissions.any { context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED } diff --git a/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbFix.kt b/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbFix.kt index 573aea35a..e3f432e18 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbFix.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbFix.kt @@ -16,7 +16,6 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.ArrayDeque -/** A narrowly scoped Wireless debugging client for the child-process setting repair. */ object WirelessAdbFix { private const val CERT_FILE = "wireless-adb-cert.pem" private const val KEY_FILE = "wireless-adb-key.pem" @@ -66,7 +65,6 @@ object WirelessAdbFix { return SavedConnection(host, port) } - /** A saved pairing skips the PIN; an unknown connection port can be filled in from Settings. */ fun savedConnectionAddress(context: Context): String? = savedConnection(context)?.let { saved -> val host = if (':' in saved.host) "[${saved.host}]" else saved.host "$host:${saved.port ?: ""}" @@ -80,7 +78,6 @@ object WirelessAdbFix { ) } - /** Finds this paired device's separate TLS connection port from Android's ADB mDNS record. */ fun isLocalAddress(address: InetAddress): Boolean = runCatching { if (address.isLoopbackAddress) return true NetworkInterface.getNetworkInterfaces()?.toList().orEmpty().any { network -> @@ -184,7 +181,6 @@ object WirelessAdbFix { saveConnection(context, host, port) } - /** Tries the app's saved pairing without asking for a new pairing code. */ fun setUsingSavedPairing(context: Context, enabled: Boolean) { val appContext = context.applicationContext val saved = savedConnection(appContext) diff --git a/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt b/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt index e81ed9681..51c3b2494 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt @@ -5,7 +5,6 @@ import android.content.Context import android.os.Build import java.io.File -/** Whether the client's memory can be paged out to compressed swap: MADV_PAGEOUT (Linux 5.4) and a zram or other swap device. */ object ZramSupport { private const val GIB = 1L shl 30 diff --git a/app/src/main/java/com/droiddeck/launcher/files/AppFilesProvider.kt b/app/src/main/java/com/droiddeck/launcher/files/AppFilesProvider.kt index c5bbef88e..0f794ce0d 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/AppFilesProvider.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/AppFilesProvider.kt @@ -23,15 +23,6 @@ import java.nio.file.Path import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes -/** - * The app's own data folder (the Linux runtime, settings, logs) in Android's file picker, under the - * app's name beside the phone's storage - what MT Manager's "documents provider" injection does, as - * part of the app. Only the system's documents UI can bind it (MANAGE_DOCUMENTS); another app sees - * a file only when the user picks it there. - * - * A document id is the file's absolute path, and nothing outside the data folder is ever handed out - * or accepted: every id is resolved and checked to lie inside it. - */ class AppFilesProvider : DocumentsProvider() { private lateinit var base: File private lateinit var baseCanonical: String @@ -65,8 +56,6 @@ class AppFilesProvider : DocumentsProvider() { override fun queryChildDocuments(parentDocumentId: String, projection: Array<String>?, sortOrder: String?): Cursor { val result = MatrixCursor(projection ?: DOCUMENT_COLUMNS) - // A link in the runtime that leads out of the data folder (/etc/localtime and the like) is - // left out rather than listed as something that cannot be opened. fileFor(parentDocumentId).listFiles()?.filter { inside(it) }?.forEach { addRow(result, it) } return result } @@ -104,10 +93,6 @@ class AppFilesProvider : DocumentsProvider() { } } - /** - * Deletes a file or a folder and what is in it without ever following a link: the runtime holds - * links to places outside it, and deleting a folder must remove the link, not what it points at. - */ private fun deleteTree(root: Path) { if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) { Files.delete(root) @@ -158,7 +143,6 @@ class AppFilesProvider : DocumentsProvider() { val needle = query.lowercase() val pending = ArrayDeque(listOf(base)) var visited = 0 - // The runtime is tens of thousands of files: a bounded walk, not the whole tree. while (pending.isNotEmpty() && result.count < 50 && visited++ < 20000) { val f = pending.removeFirst() if (!inside(f)) continue @@ -168,7 +152,6 @@ class AppFilesProvider : DocumentsProvider() { return result } - /** The file an id names, when it exists and lies inside the data folder (links followed). */ private fun fileFor(documentId: String): File { val f = File(documentId) if (!f.isAbsolute || !inside(f)) throw FileNotFoundException("$documentId is outside the app's files") @@ -177,7 +160,6 @@ class AppFilesProvider : DocumentsProvider() { } private fun inside(f: File): Boolean { - // The folder's own entries are listed as they are; a link is judged by where it leads. val abs = f.absoluteFile.normalize().path if (abs != base.absolutePath && !abs.startsWith(base.absolutePath + "/")) return false val real = runCatching { f.canonicalPath }.getOrNull() ?: return false @@ -225,11 +207,6 @@ class AppFilesProvider : DocumentsProvider() { Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, ) - /** - * The name in the picker. The same app ships under other package names (for the performance - * modes some phones give only named apps); each says which it is, so four installs are not - * four identical "DroidDeck" entries. - */ fun titleFor(context: Context): String { val name = context.getString(R.string.app_name) return when (context.packageName) { diff --git a/app/src/main/java/com/droiddeck/launcher/files/CollapsibleRail.kt b/app/src/main/java/com/droiddeck/launcher/files/CollapsibleRail.kt index 30ae1537b..665d063fd 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/CollapsibleRail.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/CollapsibleRail.kt @@ -49,38 +49,26 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -/** One tappable entry in a [CollapsibleRail] (a container tab, a file-manager location, …). */ data class RailItem( val label: String, val icon: ImageVector, val selected: Boolean, - /** >0 shows a small accent count badge on the item's icon (expanded and collapsed). */ val badge: Int = 0, - /** The under-icon label when the rail is collapsed, for names too long to fit; null derives one. */ val shortLabel: String? = null, - // onClick stays LAST so existing call sites can pass it as a trailing lambda; badge is an - // optional named param before it (default 0). val onClick: () -> Unit, ) -/** A group of [RailItem]s under an optional small section header (STORAGE / QUICK / FAVORITES …). */ data class RailSection(val header: String?, val items: List<RailItem>) -/** - * The tiny under-icon label for a COLLAPSED rail item. The rail width never changes, so labels must - * fit the narrow (~58dp) column: multi-word names wrap to a second line; single words too long to - * fit are abbreviated; short ones pass through. (Explicit abbreviations first, then a length guard.) - */ private fun collapsedLabel(label: String): String { when (label) { - "ENVIROMENT" -> return "ENVIRON" // the app's existing (mis)spelling, kept uppercase + "ENVIROMENT" -> return "ENVIRON" "WIN COMPONENTS" -> return "WIN COMP" } - if (label.contains(' ')) return label // multi-word → wraps to 2 lines + if (label.contains(' ')) return label return if (label.length > 9) label.take(8) + "…" else label } -/** A rail item's 20dp icon, with a small accent count badge on its corner when [badge] > 0. */ @Composable private fun RailItemIcon(item: RailItem, tint: androidx.compose.ui.graphics.Color) { if (item.badge > 0) { @@ -99,21 +87,10 @@ private fun RailItemIcon(item: RailItem, tint: androidx.compose.ui.graphics.Colo } } -/** A secondary link shown under the header (e.g. "What is all this?", "Reset to app defaults"). */ data class RailLink(val label: String, val icon: ImageVector, val onClick: () -> Unit) -/** - * State for a [CollapsibleRail], with the app-wide unified rule: - * - PORTRAIT → always collapsed (icon-only), the toggle is HIDDEN and it can never expand. - * - LANDSCAPE → expanded by default; the user's collapse choice is remembered PER-SCREEN - * ([screenKey]) and is NOT overridden by rotation. portrait→landscape restores that choice. - * - * Backed by rememberSaveable (survives process death) and by the fact the app UI activity keeps - * orientation in configChanges (no recreate) so the selected item / fields / scroll are preserved. - */ class RailState internal constructor( val collapsed: Boolean, - /** True in portrait: the rail is locked collapsed and must not show an expand toggle. */ val portraitLocked: Boolean, val onToggle: () -> Unit, ) @@ -121,15 +98,12 @@ class RailState internal constructor( @Composable fun rememberRailState(screenKey: String): RailState { val context = LocalContext.current - // The same file the File Manager keeps its own browse settings in (no androidx.preference here). val prefs = remember { context.getSharedPreferences("file_manager", android.content.Context.MODE_PRIVATE) } val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE val choseKey = "rail_${screenKey}_userChose" val collapsedKey = "rail_${screenKey}_collapsed" - // Keep the landscape choice in saveable state regardless of orientation so portrait→landscape - // restores it. In portrait we simply report collapsed+locked and ignore it. var userChose by rememberSaveable(screenKey) { mutableStateOf(prefs.getBoolean(choseKey, false)) } var landscapeCollapsed by rememberSaveable(screenKey) { mutableStateOf(if (prefs.getBoolean(choseKey, false)) prefs.getBoolean(collapsedKey, false) else false) @@ -152,16 +126,6 @@ fun rememberRailState(screenKey: String): RailState { ) } -/** - * The shared collapsible left rail (mockup "Option 3"): app glyph + title, optional links, and the - * navigation items, replacing a top tab bar so content runs full height beside it. Width animates - * 190dp ↔ 58dp. When collapsed it is icon-only (the caller surfaces the active item's name over the - * content). Used by the container editors, File Manager and Save Manager. - * - * [footer] is an optional slot pinned to the bottom of the rail (a weighted spacer pushes it down), - * for a persistent per-screen summary (e.g. the Save Manager's "N games need syncing" line). The - * caller renders whatever it wants there and can key it off [RailState.collapsed] to go icon-only. - */ @Composable fun CollapsibleRail( state: RailState, @@ -170,12 +134,7 @@ fun CollapsibleRail( modifier: Modifier = Modifier, links: List<RailLink> = emptyList(), footer: (@Composable ColumnScope.() -> Unit)? = null, - // Optional header glyph (a themed drawable). When null the rail shows its plain solid square. - // Used by the container editors to show the wine-glass container icon; other rails leave it null. @DrawableRes headerIcon: Int? = null, - // When true each item gets a rounded outlined-button look (the selected one takes the accent - // border + accent-dim fill), matching the File Manager's "New Folder" button. Off by default so - // the container/save rails keep their lighter flat rows (5 stacked outlined tabs read too heavy). outlinedItems: Boolean = false, ) { val collapsed = state.collapsed @@ -187,7 +146,6 @@ fun CollapsibleRail( .fillMaxHeight() .background(MaterialTheme.colorScheme.surfaceContainerHigh) ) { - // ── Header: app glyph + title + collapse handle (handle hidden when portrait-locked) ── Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = if (collapsed) Arrangement.Center else Arrangement.Start, @@ -228,14 +186,12 @@ fun CollapsibleRail( } } } - // Collapsed (landscape only) → an expand handle; portrait is locked with no handle. if (collapsed && !state.portraitLocked) { IconButton(onClick = state.onToggle, modifier = Modifier.fillMaxWidth().height(24.dp)) { Text("››", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp) } } - // ── Links ── if (links.isNotEmpty()) { if (collapsed) { Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { @@ -258,8 +214,6 @@ fun CollapsibleRail( HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f)) - // ── Sections + items (SCROLLABLE so a tall rail - e.g. many File Manager favourites - is - // fully reachable in both orientations; the header above and footer below stay pinned) ── Column( modifier = Modifier .weight(1f) @@ -283,8 +237,6 @@ fun CollapsibleRail( } section.items.forEach { item -> val active = item.selected - // Outlined variant: each item is a rounded outlined button; selected = accent border + - // accent-dim fill. Flat variant (default): a simple accent-tinted highlight for selected. val shape = RoundedCornerShape(9.dp) val base = Modifier.fillMaxWidth() val styled = if (outlinedItems) { @@ -308,9 +260,6 @@ fun CollapsibleRail( val iconTint = if (active) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant val labelColor = if (active) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant if (collapsed) { - // Icon-only width is unchanged - a TINY label goes UNDER the icon (2 lines max, - // centre-aligned, wrapping/abbreviated) so every button stays identifiable without - // widening the rail. Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = styled, @@ -350,9 +299,8 @@ fun CollapsibleRail( } } - } // end scrollable section list (its weight fills the gap so the footer stays pinned) + } - // ── Optional pinned footer (e.g. the sync summary) ── if (footer != null) { HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)) footer() @@ -360,16 +308,6 @@ fun CollapsibleRail( } } -/** - * Portrait counterpart to [CollapsibleRail]: the same navigation [items] laid out as a horizontal - * icon-over-label bar pinned across the top, with an accent underline under the selected one. Mirrors - * the container editor's top tab bar so every editor reads the same - pin this in portrait wherever a - * [CollapsibleRail] is used in landscape. Optional [links] (Help / Reset …) are appended after a - * faint separator, exactly like the rail's links. - * - * (The container editor still carries its own private copy of this bar; new screens should prefer - * this shared one, and the container editor can migrate to it later.) - */ @Composable fun RailTopTabs( items: List<RailItem>, @@ -391,7 +329,6 @@ fun RailTopTabs( ) } if (links.isNotEmpty()) { - // Faint separator: everything past here is an action button, not a tab. Box( modifier = Modifier .padding(horizontal = 2.dp) @@ -408,8 +345,6 @@ fun RailTopTabs( } } -/** One cell of [RailTopTabs] - icon over a small label, with an accent underline (zero-width when - * unselected, so every cell keeps the same height). Mirrors the container editor's TopCell. */ @Composable private fun TopTabCell( icon: ImageVector, diff --git a/app/src/main/java/com/droiddeck/launcher/files/FavoritesStore.kt b/app/src/main/java/com/droiddeck/launcher/files/FavoritesStore.kt index de38b7a4f..8255b561b 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FavoritesStore.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FavoritesStore.kt @@ -3,15 +3,6 @@ package com.droiddeck.launcher.files import android.content.Context import org.json.JSONArray -/** - * Persists the File Manager's favourite / bookmarked directories. - * - * Stores an ordered (insertion-order) list of absolute path strings as a JSON array - * under "file_manager_favorites" in SharedPreferences "file_manager_prefs". Only the - * absolute path is kept - everything shown on a favourite card (drive label, container - * name, display path) is derived live, so a renamed or deleted container resolves - * correctly for free. - */ object FavoritesStore { private const val PREFS_NAME = "file_manager_prefs" @@ -20,7 +11,6 @@ object FavoritesStore { private fun prefs(context: Context) = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - /** Ordered list of favourited absolute paths (insertion order). */ fun list(context: Context): List<String> { val raw = prefs(context).getString(KEY, null) ?: return emptyList() return runCatching { @@ -31,7 +21,6 @@ object FavoritesStore { fun isFavorite(context: Context, path: String): Boolean = list(context).contains(path) - /** Add [path] if not already present (deduped); keeps insertion order. */ fun add(context: Context, path: String) { val current = list(context) if (current.contains(path)) return @@ -44,7 +33,6 @@ object FavoritesStore { save(context, current.filterNot { it == path }) } - /** Toggle [path]; returns true if it is now a favourite, false if it was removed. */ fun toggle(context: Context, path: String): Boolean = if (isFavorite(context, path)) { remove(context, path); false diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileAttributes.kt b/app/src/main/java/com/droiddeck/launcher/files/FileAttributes.kt index 60c359bf4..43fc55d71 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileAttributes.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileAttributes.kt @@ -4,30 +4,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import java.io.File -// Reading and setting a file's read-only and hidden attributes (the DOS attribute xattr). -// ── DOS file attributes (Read-only / Hidden) ── -// The in-container file manager (wfm.exe) exposes these in its Properties dialog; this mirrors the -// same two toggles for files browsed from the app side. No root - we only touch permission bits the -// app owns (container files) and the Wine DOS-attribute xattr. -// FILE_ATTRIBUTE_HIDDEN, as Wine encodes it in the user.DOSATTRIB extended attribute. private const val FILE_ATTRIBUTE_HIDDEN = 0x2 private const val DOSATTRIB_XATTR = "user.DOSATTRIB" -// Snapshot of a file's two toggleable attributes. hiddenSupported is false when the underlying -// filesystem can't store the DOSATTRIB xattr (the FUSE /storage volumes) - the Hidden toggle is then -// disabled while Read-only keeps working. internal data class FileAttrState( val readOnly: Boolean, val hidden: Boolean, val hiddenSupported: Boolean, ) -// Wine (and Samba) store user.DOSATTRIB as an ASCII hex string ("0x22"), sometimes with trailing -// Samba fields after a separator. We only need the leading DOS-attribute hex, so read from the "0x" -// prefix and stop at the first non-hex character. private fun parseDosAttrib(raw: ByteArray): Int { val s = String(raw, Charsets.US_ASCII).trim() val hex = if (s.startsWith("0x") || s.startsWith("0X")) s.substring(2) else s @@ -35,11 +23,8 @@ private fun parseDosAttrib(raw: ByteArray): Int { return digits.toIntOrNull(16) ?: 0 } -// Read the current read-only + Wine-hidden state. Never throws: an xattr-unsupported filesystem -// comes back hiddenSupported=false (Hidden toggle disabled), Read-only always resolves. internal fun readFileAttrs(file: File): FileAttrState { val path = file.absolutePath - // Read-only reflects the OWNER write bit - that's what Wine maps to FILE_ATTRIBUTE_READONLY. val readOnly = runCatching { (android.system.Os.stat(path).st_mode and android.system.OsConstants.S_IWUSR) == 0 }.getOrDefault(!file.canWrite()) @@ -49,8 +34,6 @@ internal fun readFileAttrs(file: File): FileAttrState { try { hidden = (parseDosAttrib(android.system.Os.getxattr(path, DOSATTRIB_XATTR)) and FILE_ATTRIBUTE_HIDDEN) != 0 } catch (e: android.system.ErrnoException) { - // ENODATA: xattr namespace works, the attribute just isn't set yet -> still toggleable. - // ENOTSUP/EOPNOTSUPP/anything else: the fs can't store it -> disable the Hidden toggle. supported = e.errno == android.system.OsConstants.ENODATA } catch (e: Exception) { supported = false @@ -58,10 +41,6 @@ internal fun readFileAttrs(file: File): FileAttrState { return FileAttrState(readOnly, hidden, supported) } -// Toggle read-only by flipping the write bits, preserving every other permission bit. Setting -// read-only clears owner/group/other write (so Wine sees FILE_ATTRIBUTE_READONLY regardless of which -// write bit it checks); clearing it restores owner write. Falls back to File.setWritable if chmod is -// somehow refused. Returns true on success. internal fun setReadOnly(file: File, readOnly: Boolean): Boolean = runCatching { val path = file.absolutePath val mode = android.system.Os.stat(path).st_mode @@ -73,10 +52,6 @@ internal fun setReadOnly(file: File, readOnly: Boolean): Boolean = runCatching { true }.getOrElse { file.setWritable(!readOnly, true) } -// Flip Wine's DOS hidden bit in user.DOSATTRIB, preserving the other DOS-attribute bits -// (archive/system/read-only) already encoded there, and synthesizing a minimal value when absent. -// Writes Wine's own "0x%x" format, which Wine reads back natively. Returns false when the fs can't -// store the xattr, so the caller can disable just the Hidden toggle. internal fun setHidden(file: File, hidden: Boolean): Boolean = try { val path = file.absolutePath var attr = try { diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileManagerActivity.kt b/app/src/main/java/com/droiddeck/launcher/files/FileManagerActivity.kt index 2395d1c80..27cb9dbce 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileManagerActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileManagerActivity.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier import com.droiddeck.launcher.ui.DroidDeckTheme import java.io.File -/** The File Manager as a screen of its own, from the Files tile on the main screen. */ class FileManagerActivity : ComponentActivity() { override fun attachBaseContext(newBase: android.content.Context) { super.attachBaseContext(newBase) diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileManagerItems.kt b/app/src/main/java/com/droiddeck/launcher/files/FileManagerItems.kt index 8efe2c6cd..45c8a92c8 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileManagerItems.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileManagerItems.kt @@ -77,23 +77,16 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -// The File Manager's rows, tiles, favourites and context menu. -// Semantic identity colours for the favourite-card drive badge. Intentionally NOT theme -// accent colours - they identify the storage source at a glance. Returns (background, foreground). private fun badgeColors(loc: FavLocation): Pair<Color, Color> { val white = Color(0xFFFFFFFF) return when { - loc.storage == FavStorage.INTERNAL -> Color(0xFF2E5FB0) to white // blue - loc.storage == FavStorage.SD -> Color(0xFF2E7D32) to white // green - else -> Color(0xFF555555) to white // grey + loc.storage == FavStorage.INTERNAL -> Color(0xFF2E5FB0) to white + loc.storage == FavStorage.SD -> Color(0xFF2E7D32) to white + else -> Color(0xFF555555) to white } } -// The context-menu item list shared by the list rows (FileItemRow) and the grid tiles -// (FileGridTile), so both open the identical menu. Gating (isDir/canRun/isInno/looksLikeArchive) is -// recomputed from [file] here - one source of truth for what each item shows. Every item dismisses -// the menu first, then runs its action. @Composable private fun FileContextMenuItems( file: File, @@ -114,15 +107,12 @@ private fun FileContextMenuItems( onClick = { onDismissMenu(); onSelect() }, ) MenuItemDivider() - // Properties: basic info + Read-only / Hidden toggles, for ANY file or folder (handy for config - // files like .txt/.cfg/.ini). Kept near the top since it's a common reason to open this menu. DropdownMenuItem( text = { Text(stringResource(R.string.fm_properties)) }, leadingIcon = { Icon(Icons.Filled.Info, null, tint = MaterialTheme.colorScheme.primary) }, onClick = { onDismissMenu(); onProperties() }, ) MenuItemDivider() - // Favorites are directories - only folders get the pin toggle. if (isDir) { DropdownMenuItem( text = { Text(if (isFavorite) stringResource(R.string.fm_favorite_remove) else stringResource(R.string.fm_favorite_add)) }, @@ -189,11 +179,8 @@ internal fun FileItemRow( val context = LocalContext.current val isDir = file.isDirectory val isExe = !isDir && file.name.lowercase().let { it.endsWith(".exe") || it.endsWith(".bat") || it.endsWith(".msi") || it.endsWith(".sh") } - // Image files show a real thumbnail instead of the generic file icon (handy when picking a - // wallpaper/icon). Coil sizes the decode to the 36dp slot and caches it, so scrolling stays smooth. val isImage = !isDir && file.extension.lowercase() in IMAGE_THUMB_EXTS - // For real PE executables, try to pull out the embedded application icon (async, off the main thread). var exeIcon by remember(file.absolutePath) { mutableStateOf<ImageBitmap?>(null) } if (!isDir && file.name.lowercase().endsWith(".exe")) { LaunchedEffect(file.absolutePath) { @@ -207,8 +194,6 @@ internal fun FileItemRow( .fillMaxWidth() .padding(horizontal = 12.dp, vertical = 3.dp) .combinedClickable( - // In selection mode a tap toggles instead of opening, so you can rattle through a - // folder without long-pressing every single row. onClick = { if (selectionMode) onToggleSelect() else onTap() }, onLongClick = onLongPress, ), @@ -231,7 +216,6 @@ internal fun FileItemRow( Spacer(Modifier.width(4.dp)) } when { - // Show the executable's own embedded icon when we managed to extract one. exeIcon != null -> Image( bitmap = exeIcon!!, contentDescription = null, @@ -249,7 +233,6 @@ internal fun FileItemRow( tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(if (compact) 24.dp else 36.dp), ) - // Real image preview. Falls back to the generic file icon while loading or on decode failure. isImage -> AsyncImage( model = file, contentDescription = null, @@ -313,8 +296,6 @@ internal fun FileItemRow( } } -// The ".." row at the top of a folder: one level up, styled like a folder row so it reads as part -// of the list and takes controller focus the same way. @Composable internal fun ParentFolderRow(compact: Boolean = false, onTap: () -> Unit) { Card( @@ -354,8 +335,6 @@ internal fun ParentFolderRow(compact: Boolean = false, onTap: () -> Unit) { } } -// Dedicated Favorites list that replaces the file list while the star toggle is on. -// Reads the store keyed on [favTick] so it recomposes after any pin/unpin. @Composable internal fun FavoritesList( currentDir: File, @@ -431,9 +410,6 @@ internal fun FavoritesList( } } -// A single favourite - matches the FileItemRow card style (surfaceContainer + outline + -// RoundedCornerShape(10.dp)). Shows the folder name, a coloured drive badge + origin text, -// and the full display path; tapping jumps into it, the filled star unpins. @Composable private fun FavoriteCard( file: File, @@ -472,7 +448,6 @@ private fun FavoriteCard( overflow = TextOverflow.Ellipsis, ) Spacer(Modifier.height(2.dp)) - // Origin line: coloured drive badge + source description. Row(verticalAlignment = Alignment.CenterVertically) { Text( text = stringResource(loc.driveLabel), @@ -517,13 +492,6 @@ private fun FavoriteCard( } } -/** - * One entry in the File Manager's grid view: a big thumbnail with the name under it. - * - * Deliberately drops size and date - at this width they truncate to noise. The grid is for - * recognising things by sight (screenshots, covers, game folders); the list stays the view for - * reading details. - */ @OptIn(ExperimentalFoundationApi::class) @Composable internal fun FileGridTile( @@ -554,7 +522,6 @@ internal fun FileGridTile( if (bmp != null) exeIcon = bmp.asImageBitmap() } } - // The tile has no ⋮ button - long-press opens this menu, anchored to the Box around the Card. Box { Card( modifier = Modifier @@ -608,8 +575,6 @@ internal fun FileGridTile( } } Spacer(Modifier.height(4.dp)) - // Always reserve two lines (issue #475): a one-line name used to make its card shorter than - // its neighbours, so grid rows had ragged heights. Fixed min/max keeps every tile the same size. Text( file.name, color = MaterialTheme.colorScheme.onSurface, diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileManagerModel.kt b/app/src/main/java/com/droiddeck/launcher/files/FileManagerModel.kt index eac7dc463..c8bcf1ee7 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileManagerModel.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileManagerModel.kt @@ -12,33 +12,8 @@ import androidx.annotation.StringRes import com.droiddeck.launcher.R import java.io.File -// The File Manager's non-UI helpers: sorting, paths, conflict choices and favourite locations. - -/* - * Bannerlator's File Manager (ui/screens/FileManagerScreen.kt), carried over whole: the same - * browsing, grid and list, sorting, search, multi-select, copy/cut/paste with conflicts and - * progress, rename, delete, new folder, properties, favourites and the locations rail, and the - * same pick modes. What this app has no use for is not here: running a file in a Wine container, - * adding it to a container's shortcuts, the Drive C: / Drive Z: locations and the archive - * unpacker that installs into a prefix. Everything else is line for line the same so a fix in - * either app carries to the other. - */ - -/** - * What to do when a pasted item already exists at the destination. - * - * OVERWRITE and MERGE resolve to the same call - `copyWithProgress` recurses into an existing - * directory and truncates existing files - but they mean different things to the user, so both are - * offered and the wording is chosen per item type (files overwrite, folders merge). - */ enum class ConflictChoice { OVERWRITE, MERGE, KEEP_BOTH, SKIP } -/** - * Shortens a path from the LEFT, keeping whole segments. - * - * Compose's TextOverflow can only ellipsise the tail, which for a path throws away the part that - * matters - `/storage/emulated/0/Games/Racing/Dir…` tells you nothing about where you are. - */ internal fun elidePathStart(path: String, max: Int): String { if (path.length <= max) return path val parts = path.split('/').filter { it.isNotEmpty() } @@ -50,12 +25,9 @@ internal fun elidePathStart(path: String, max: Int): String { return if (out.isEmpty()) "…" + path.takeLast(max - 1) else "…$out" } -/** Folder-first ordering stays fixed across both sort directions. */ internal fun comparatorFor(sortBy: String, desc: Boolean): Comparator<File> { val inner: Comparator<File> = when (sortBy) { "date" -> compareBy { it.lastModified() } - // Directory length() is meaningless, so folders sort by name within the size ordering - // instead of pretending to have one. "size" -> compareBy { if (it.isDirectory) -1L else it.length() } else -> compareBy { it.name.lowercase() } } @@ -63,27 +35,23 @@ internal fun comparatorFor(sortBy: String, desc: Boolean): Comparator<File> { return compareBy<File> { if (it.isDirectory) 0 else 1 }.then(directed) } -// Image extensions that get a real thumbnail (via Coil) instead of the generic file icon. internal val IMAGE_THUMB_EXTS = setOf("jpg", "jpeg", "png", "webp", "bmp", "gif") -// True when [child] is [ancestor] itself or lives anywhere inside it. internal fun isWithin(child: File, ancestor: File): Boolean { val c = runCatching { child.canonicalPath }.getOrDefault(child.absolutePath) val a = runCatching { ancestor.canonicalPath }.getOrDefault(ancestor.absolutePath) return c == a || c.startsWith(a + File.separator) } -// ── Favorites: origin resolution ── enum class FavStorage { INTERNAL, SD, OTHER } data class FavLocation( val storage: FavStorage, - @StringRes val driveLabel: Int, // Internal, SD card, or Storage - val displayPath: String // the unix absolute path + @StringRes val driveLabel: Int, + val displayPath: String ) -// Resolve where [file] lives (storage source + a friendly label) by prefix-matching its path. fun describeLocation(file: File): FavLocation { val abs = file.absolutePath @@ -99,7 +67,6 @@ fun describeLocation(file: File): FavLocation { } } - // The Linux system's root (the app's files/linuxfs, the sessions' /) and anything under it. if (Regex("/files/linuxfs(/|$)").containsMatchIn(abs)) { return FavLocation(FavStorage.OTHER, R.string.fm_drive_linux, abs) } diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileManagerScreen.kt b/app/src/main/java/com/droiddeck/launcher/files/FileManagerScreen.kt index 46f2f708b..9fbd251ff 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileManagerScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileManagerScreen.kt @@ -98,12 +98,7 @@ import java.util.Date @OptIn(ExperimentalMaterial3Api::class) @Composable fun FileManagerScreen( - // Pick mode (issue #73): reuse this File Manager as a themed file picker. When on, editing/run - // features are gated off and tapping a matching file returns it via [onPick]. Defaults keep the - // full-featured File Manager nav destination unchanged. pickMode: Boolean = false, - // Directory-pick mode (issue #70): only folders are listed, files are hidden, and a - // "Select this folder" action returns the current directory via [onPick]. Implies pickMode. pickDirMode: Boolean = false, pickExtensions: List<String> = emptyList(), initialDir: File? = null, @@ -113,7 +108,6 @@ fun FileManagerScreen( val context = LocalContext.current val scope = rememberCoroutineScope() - // Only matching files are shown in pick mode (directories are always shown). Empty = all files. val lowerExts = remember(pickExtensions) { pickExtensions.map { it.lowercase() } } fun matchesPickExt(file: File): Boolean { if (lowerExts.isEmpty()) return true @@ -124,18 +118,10 @@ fun FileManagerScreen( val pickPrefs = remember { context.getSharedPreferences("file_manager", android.content.Context.MODE_PRIVATE) } val browsePrefs = pickPrefs val rootDir = remember { - // Both modes: honour an explicit caller-supplied start dir (e.g. Log Manager's game-log - // folder), else open at the INTERNAL STORAGE ROOT. Selection screens (drive-folder pick, - // local component pick, imports) previously defaulted to Download which - combined with the - // currentRoot floor below - trapped users in Download with no way up (reported bug). initialDir?.takeIf { it.isDirectory } ?: File("/storage/emulated/0") } var currentDir by remember { mutableStateOf(rootDir) } - // The up/back FLOOR - back + the up-arrow are disabled while currentDir == currentRoot. It MUST be - // the VOLUME ROOT of the start dir (internal /storage/emulated/0, or an SD card /storage/XXXX-XXXX), - // NOT the start dir itself: otherwise opening at any subfolder disables up/back and traps the user - // there. (Mirrors the volume-root logic in favLocationOf above.) var currentRoot by remember { val abs = rootDir.absolutePath val internal = "/storage/emulated/0" @@ -145,8 +131,6 @@ fun FileManagerScreen( val name = abs.removePrefix("/storage/").substringBefore('/') if (name.isNotEmpty() && name != "emulated" && name != "self") File("/storage/$name") else rootDir } - // A game's own folder or Proton prefix lives in the app's private storage, which has no - // volume above it: floor at the app's files dir so up/back still reach the library roots. else -> { val appFiles = runCatching { context.filesDir.canonicalPath }.getOrNull() generateSequence(rootDir) { it.parentFile } @@ -159,40 +143,26 @@ fun FileManagerScreen( var entries by remember { mutableStateOf(listOf<File>()) } var selectedEntry by remember { mutableStateOf<File?>(null) } var showMenuFor by remember { mutableStateOf<File?>(null) } - // Clipboard holds a LIST so one paste can carry a whole selection. Cut/copy semantics are a - // flag on the batch rather than per item - mixing the two in one clipboard has no sane meaning. var clipboardFiles by remember { mutableStateOf<List<File>>(emptyList()) } var isCutOperation by remember { mutableStateOf(false) } - // Multi-select. Keyed by absolute path rather than File so a directory reload (which builds - // fresh File objects) doesn't silently drop the selection. var selectionMode by remember { mutableStateOf(false) } var selectedPaths by remember { mutableStateOf<Set<String>>(emptySet()) } - // Paste conflict resolution, surfaced from the IO coroutine and answered by the dialog. var pendingConflict by remember { mutableStateOf<File?>(null) } var conflictChoice by remember { mutableStateOf<ConflictChoice?>(null) } var conflictApplyToAll by remember { mutableStateOf(false) } - // Set while a copy/move runs so the progress UI can offer a cancel. var operationJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) } var pendingBulkDelete by remember { mutableStateOf<List<File>>(emptyList()) } - // Browse controls. Persisted so the list doesn't reset its order every time you open a folder. var searchQuery by remember { mutableStateOf("") } var showSearch by remember { mutableStateOf(false) } var sortBy by remember { mutableStateOf(browsePrefs.getString("fmSortBy", "name") ?: "name") } var sortDesc by remember { mutableStateOf(browsePrefs.getBoolean("fmSortDesc", false)) } var showHidden by remember { mutableStateOf(browsePrefs.getBoolean("fmShowHidden", true)) } var showSortMenu by remember { mutableStateOf(false) } - // View mode: list of cards (default) or a thumbnail grid. Density applies to the list only - - // a grid tile has no second line to compact. - // The grid/list toggle is the SOURCE OF TRUTH in BOTH orientations (it drives the view and its - // choice persists across rotation). Grid is the default - most useful in landscape, and in - // portrait GridCells.Adaptive naturally renders fewer columns (~2). Do NOT force portrait to list: - // that broke the toggle on-device (tapping it did nothing in portrait). var gridView by remember { mutableStateOf(browsePrefs.getBoolean("fmGridView", true)) } val showGrid = gridView var compactRows by remember { mutableStateOf(browsePrefs.getBoolean("fmCompactRows", false)) } var showNewFolderDialog by remember { mutableStateOf(false) } var renameTarget by remember { mutableStateOf<File?>(null) } - // Properties sheet (basic info + Read-only / Hidden toggles) target; null when closed. var propertiesTarget by remember { mutableStateOf<File?>(null) } var isOperationRunning by remember { mutableStateOf(false) } var operationLabel by remember { mutableStateOf("") } @@ -201,24 +171,16 @@ fun FileManagerScreen( val listState = rememberLazyListState() val pullState = rememberPullToRefreshState() - // Favorites view: when on, a dedicated bookmarks list replaces the file list. - // favTick is bumped on any add/remove/toggle so the favorites view + per-row star recompute. var showFavorites by remember { mutableStateOf(false) } var favTick by remember { mutableIntStateOf(0) } - // resetScroll: jump to the top of the list (true for navigation; false for in-place reloads - // after delete/paste/rename/refresh so the user keeps their scroll position). fun loadDirectory(dir: File, resetScroll: Boolean = true) { currentDir = dir - // Remember the browsed directory so the next pick resumes here. if (pickMode) pickPrefs.edit().putString("lastFilePickerDir", dir.absolutePath).apply() scope.launch { val list = withContext(Dispatchers.IO) { dir.listFiles()?.toList() - // Dir-pick mode: folders only. File-pick: folders + matching files. Else: all. ?.filter { if (pickDirMode) it.isDirectory else !pickMode || it.isDirectory || matchesPickExt(it) } - // Dotfiles are noise in a storage root (.aya, .$recycle_bin$) but occasionally - // the thing you came for, so it's a toggle rather than a permanent filter. ?.filter { showHidden || !it.name.startsWith(".") } ?.sortedWith(comparatorFor(sortBy, sortDesc)) ?: emptyList() } @@ -227,7 +189,6 @@ fun FileManagerScreen( } } - // Pull-to-refresh: re-list the current directory, keeping scroll position. if (pullState.isRefreshing) { LaunchedEffect(true) { loadDirectory(currentDir, resetScroll = false) @@ -235,19 +196,13 @@ fun FileManagerScreen( } } - // Jump to a drive's root; pins the Back boundary so we don't climb above it. fun openDrive(dir: File) { currentRoot = dir loadDirectory(dir) } - // Open the start dir without openDrive(): that pins the floor to the start dir itself, which - // greyed out up/back whenever the manager was opened at a game's folder. LaunchedEffect(Unit) { loadDirectory(rootDir) } - // System/gesture Back: while the Favorites view is open it closes that first; otherwise - // it goes up one directory. Only at the current drive's root with Favorites closed is it - // disabled, letting Back propagate to close the File Manager. BackHandler(enabled = showFavorites || currentDir != currentRoot) { if (showFavorites) { showFavorites = false @@ -257,15 +212,12 @@ fun FileManagerScreen( if (parent != null && parent.exists()) loadDirectory(parent) } - // One level up, never above the current drive's root: shared by the toolbar arrow and the - // ".." row at the top of the list. val canGoUp = currentDir != currentRoot && currentDir.parentFile?.exists() == true fun goUp() { val parent = currentDir.parentFile if (currentDir != currentRoot && parent != null && parent.exists()) loadDirectory(parent) } - // Resolve a non-colliding destination in [dir] for [name] (foo.txt -> "foo (1).txt"). fun uniqueDestination(dir: File, name: String): File { var candidate = File(dir, name) if (!candidate.exists()) return candidate @@ -291,12 +243,9 @@ fun FileManagerScreen( } } - /** Waits for the user to answer the conflict dialog for [file]; null if they cancelled it. */ suspend fun askConflict(file: File): ConflictChoice? { pendingConflict = file conflictChoice = null - // Poll rather than plumb a CompletableDeferred through Compose state - the dialog answers - // by setting conflictChoice, and this coroutine is already off the critical path. while (pendingConflict != null && conflictChoice == null) kotlinx.coroutines.delay(50) return conflictChoice } @@ -319,12 +268,10 @@ fun FileManagerScreen( var done = 0 for (src in sources) { - // Pasting a folder into itself or its own subtree would recurse forever. if (src.isDirectory && isWithin(dstDir, src)) { failed++ continue } - // Moving into the folder it already sits in is a no-op. if (cut && src.parentFile?.absolutePath == dstDir.absolutePath) { skipped++ continue @@ -336,16 +283,12 @@ fun FileManagerScreen( if (conflictApplyToAll) applyToAll = it } ?: run { skipped++; null } ?: continue when (choice) { - // Overwrite and Merge both paste onto the real destination: copyWithProgress - // recurses into an existing directory and truncates existing files, so the - // two differ only in what the user expects, not in what we call. ConflictChoice.OVERWRITE, ConflictChoice.MERGE -> Unit ConflictChoice.KEEP_BOTH -> dst = uniqueDestination(dstDir, src.name) ConflictChoice.SKIP -> { skipped++; continue } } } - // Progress is per item; with a batch the label carries the overall position. operationLabel = if (sources.size > 1) { context.getString(if (cut) R.string.fm_op_moving_batch else R.string.fm_op_copying_batch, done + 1, sources.size, src.name) } else { @@ -419,8 +362,6 @@ fun FileManagerScreen( } var showDriveMenu by remember { mutableStateOf(false) } - // Re-enumerated whenever we come back to the screen: returning from a container can leave this - // process on a stale storage view, and the volume set has to be re-read rather than cached. var storageTick by remember { mutableIntStateOf(0) } val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(lifecycleOwner) { @@ -435,7 +376,6 @@ fun FileManagerScreen( com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context).takeIf { File(it, "usr").isDirectory } } - // ── Dialogs ── if (showNewFolderDialog) { var folderName by remember { mutableStateOf("") } @@ -488,8 +428,6 @@ fun FileManagerScreen( FilePropertiesDialog( file = file, onDismiss = { propertiesTarget = null }, - // Attribute changes affect Hidden/read-only which the listing filters/sorts on, so refresh - // in place (keeping scroll) after any toggle applies. onChanged = { loadDirectory(currentDir, resetScroll = false) }, ) } @@ -519,7 +457,6 @@ fun FileManagerScreen( Column { Text(stringResource(R.string.fm_cannot_undo)) Spacer(Modifier.height(6.dp)) - // Name a few so an accidental Select-All is obvious before it's too late. victims.take(5).forEach { Text("• ${it.name}", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) } @@ -557,7 +494,6 @@ fun FileManagerScreen( ) } - // Paste conflict - one per colliding item, with "apply to all" for a long batch. pendingConflict?.let { conflict -> val isDir = conflict.isDirectory OutlinedAlertDialog( @@ -605,7 +541,6 @@ fun FileManagerScreen( } Column(modifier = Modifier.fillMaxSize()) { - // ── Pick-mode title ── if (pickMode && !pickerTitle.isNullOrEmpty()) { Text( text = pickerTitle, @@ -618,7 +553,6 @@ fun FileManagerScreen( .padding(horizontal = 16.dp, vertical = 10.dp), ) } - // ── Dir-pick action bar: confirm the currently-browsed folder ── if (pickDirMode) { Button( onClick = { onPick?.invoke(currentDir) }, @@ -631,7 +565,6 @@ fun FileManagerScreen( Text(stringResource(R.string.fm_select_this_folder)) } } - // ── Path bar ── Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -644,12 +577,8 @@ fun FileManagerScreen( } val currentDriveLabel = stringResource(describeLocation(currentDir).driveLabel) - // Dim the drive chip while the Favorites list is open (it's not the active context). val driveChipAlpha = if (showFavorites) 0.45f else 1f Box { - // The drive/location selector opens the drive dropdown, so give it the same outlined - // look as the "New Folder" button + the rail location items - it reads as a button, not - // plain text. Border uses the theme accent token; behaviour unchanged. val driveChipShape = RoundedCornerShape(8.dp) Text( text = " $currentDriveLabel ▾", @@ -720,13 +649,8 @@ fun FileManagerScreen( modifier = Modifier.weight(1f), ) } else if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { - // PORTRAIT: hide the current-folder name - it's redundant with the path bar directly - // below (which shows the full path). The spacer keeps the action icons right-aligned. Spacer(Modifier.weight(1f)) } else { - // LANDSCAPE: the CURRENT FOLDER, not the full path. A path ellipsised on the right - // hides its tail - the only part that says where you are ("…/Games/Racing/Dir…"). The - // full path moves to the line below, where it has room. Text( text = currentDir.name.ifBlank { currentDir.absolutePath }, color = MaterialTheme.colorScheme.onSurface, @@ -739,7 +663,6 @@ fun FileManagerScreen( } if (!showFavorites) { - // Keep New Folder in the toolbar so the file list uses the full height. if (!pickMode) { OutlinedButton( onClick = { showNewFolderDialog = true }, @@ -777,8 +700,6 @@ fun FileManagerScreen( Text(if (sortBy == key) "$label ${if (sortDesc) "↓" else "↑"}" else label) }, onClick = { - // Tapping the active field flips direction; a different - // field switches to it ascending. if (sortBy == key) sortDesc = !sortDesc else { sortBy = key; sortDesc = false } browsePrefs.edit().putString("fmSortBy", sortBy) .putBoolean("fmSortDesc", sortDesc).apply() @@ -809,7 +730,6 @@ fun FileManagerScreen( } } - // Star toggle: open/close the dedicated Favorites list. IconButton(onClick = { showFavorites = !showFavorites }) { if (showFavorites) { Icon(Icons.Filled.Star, stringResource(R.string.fm_hide_favorites), tint = MaterialTheme.colorScheme.primary) @@ -819,7 +739,6 @@ fun FileManagerScreen( } } - // ── Search field ── filters the current folder only; it is not a recursive search. if (showSearch && !showFavorites) { OutlinedTextField( value = searchQuery, @@ -830,7 +749,6 @@ fun FileManagerScreen( ) } - // Free space on the volume being browsed - worth knowing before starting a 60 GB copy. val freeSpace = remember(currentDir.absolutePath, entries) { runCatching { currentDir.usableSpace }.getOrDefault(0L) } @@ -840,14 +758,10 @@ fun FileManagerScreen( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 2.dp), ) { Text( - // Elided from the LEFT: the deepest part of a path is the informative part, so - // when it doesn't fit we drop the /storage/emulated/0 prefix, not the tail. text = elidePathStart(currentDir.absolutePath, 52), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, maxLines = 1, - // fill = true: the path takes all remaining width, so the free-space figure - // is pinned to the right edge instead of sliding around with the path length. modifier = Modifier.weight(1f), ) if (freeSpace > 0) { @@ -863,7 +777,6 @@ fun FileManagerScreen( HorizontalDivider(color = MaterialTheme.colorScheme.outline) - // ── Selection bar ── replaces the paste banner while picking items. if (selectionMode) { Row( verticalAlignment = Alignment.CenterVertically, @@ -878,7 +791,6 @@ fun FileManagerScreen( fontSize = 13.sp, modifier = Modifier.weight(1f), ) - // Compact outlined buttons so all five fit one row alongside the count. val selBarPadding = PaddingValues(horizontal = 10.dp, vertical = 2.dp) OutlinedButton( onClick = { @@ -924,7 +836,6 @@ fun FileManagerScreen( } } - // ── Paste banner ── if (clipboardFiles.isNotEmpty()) { Row( verticalAlignment = Alignment.CenterVertically, @@ -948,7 +859,6 @@ fun FileManagerScreen( } } - // ── Progress overlay ── if (isOperationRunning) { Column( modifier = Modifier @@ -966,8 +876,6 @@ fun FileManagerScreen( overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), ) - // A multi-gigabyte copy onto a slow card is exactly when you discover you - // picked the wrong folder; without this the only way out was killing the app. if (operationJob != null) { TextButton(onClick = { operationJob?.cancel() @@ -996,10 +904,6 @@ fun FileManagerScreen( } } - // ── Left locations rail (mockup Option 2) + content ── - // Shared collapsible rail: landscape expanded by default, portrait collapsed icon-only. Not - // shown in pick mode (the themed picker keeps its slim layout). Built each recompose (cheap) - // so it tracks the current drive/favourites without stale click lambdas. val fmRailState = rememberRailState("filemanager") fun locItem(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector, dir: File, shortLabel: String? = null) = RailItem(label, icon, !showFavorites && currentRoot.absolutePath == dir.absolutePath, shortLabel = shortLabel) { @@ -1012,15 +916,12 @@ fun FileManagerScreen( showFavorites = false; if (d.readable) openDrive(d.dir) }) } - // The Linux system the sessions run in: its own / (the desktop's root), once installed. linuxRoot?.let { add(locItem(stringResource(R.string.fm_drive_linux), Icons.Filled.Terminal, it)) } } val quickItems = buildList { File("/storage/emulated/0/Download").takeIf { it.isDirectory }?.let { add(locItem(stringResource(R.string.fm_rail_downloads), Icons.Filled.Download, it, stringResource(R.string.fm_rail_downloads_short))) } - // The ROMs folder chosen on the main screen: what the session shows as /root/ROMs. SessionPrefs.romsDir(context).takeIf { it.isNotEmpty() }?.let(::File)?.takeIf { it.isDirectory } ?.let { add(locItem(stringResource(R.string.fm_rail_roms), Icons.Filled.SportsEsports, it)) } - // The app's own logs folder: private, which this file manager (the app itself) can read. com.droiddeck.launcher.runtime.LinuxRuntime.logDir(context).takeIf { it.isDirectory }?.let { add(locItem(stringResource(R.string.fm_rail_session_logs), Icons.Filled.Description, it)) } File("/storage/emulated/0/Pictures").takeIf { it.isDirectory }?.let { add(locItem(stringResource(R.string.fm_rail_pictures), Icons.Filled.Image, it)) } } @@ -1033,12 +934,10 @@ fun FileManagerScreen( } Row(modifier = Modifier.weight(1f).fillMaxWidth()) { - // The slim picker keeps its layout: no rail. if (!pickMode) { CollapsibleRail(state = fmRailState, title = stringResource(R.string.fm_rail_title), sections = locationSections, outlinedItems = true) } Box(modifier = Modifier.weight(1f).fillMaxSize()) { - // ── Favorites list OR file list ── if (showFavorites) { FavoritesList( currentDir = currentDir, @@ -1060,7 +959,6 @@ fun FileManagerScreen( modifier = Modifier.fillMaxSize(), ) } else { - // ── File list (pull down to refresh) ── Box( modifier = Modifier .fillMaxSize() @@ -1088,8 +986,6 @@ fun FileManagerScreen( selected = file.absolutePath in selectedPaths, onLongPress = { if (!pickMode) { - // In selection mode a long-press toggles; otherwise it opens the - // same context menu the list rows show (the tile has no ⋮ button). if (selectionMode) { selectedPaths = if (file.absolutePath in selectedPaths) selectedPaths - file.absolutePath @@ -1168,8 +1064,6 @@ fun FileManagerScreen( selectionMode = selectionMode, selected = file.absolutePath in selectedPaths, onLongPress = { - // In selection mode a long-press toggles; otherwise it opens the - // per-item context menu (matching the grid tiles). if (!pickMode) { if (selectionMode) { selectedPaths = if (file.absolutePath in selectedPaths) @@ -1223,8 +1117,6 @@ fun FileManagerScreen( } } } - // material3 1.2.0's PullToRefreshContainer draws its indicator even at rest; - // only show it while the user is actively pulling or a refresh is running. if (pullState.verticalOffset > 0.5f || pullState.isRefreshing) { PullToRefreshContainer( state = pullState, @@ -1233,7 +1125,7 @@ fun FileManagerScreen( } } } - } // end content Box (beside the rail) - } // end rail + content Row + } + } } } diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileOps.kt b/app/src/main/java/com/droiddeck/launcher/files/FileOps.kt index e363a3acc..04b83abaa 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileOps.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileOps.kt @@ -10,15 +10,9 @@ import java.io.IOException import java.nio.file.Files import java.util.Locale -/** - * The file operations the File Manager needs, as Bannerlator's FileUtils has them: a delete that - * says whether it worked, a copy and a move that report bytes as they go, and the size format - * the rows use. Kept apart from core/FileUtils, whose callers want a quiet delete. - */ object FileOps { private const val TAG = "FileOps" - /** Byte-accurate copy progress (copied/total bytes), reported as the copy proceeds. */ fun interface ProgressCallback { fun onProgress(copiedBytes: Long, totalBytes: Long) } fun formatBytes(context: Context, bytes: Long): String { @@ -30,7 +24,6 @@ object FileOps { fun isSymlink(file: File): Boolean = try { Files.isSymbolicLink(file.toPath()) } catch (e: Exception) { false } - /** Recursive delete that never follows a symlink; true when everything went. */ fun delete(target: File?): Boolean { if (target == null) return false if (target.isDirectory && !isSymlink(target)) { @@ -46,20 +39,12 @@ object FileOps { return ok } - /** Recursive total size in bytes of files under [f] (skips symlinks). Used to size a progress bar. */ fun totalSize(f: File?): Long { if (f == null || !f.exists() || isSymlink(f)) return 0 if (f.isDirectory) return f.listFiles()?.sumOf { totalSize(it) } ?: 0 return f.length() } - /** - * Moves [src] to [dst], relinking instead of copying whenever possible. A rename is instant - * regardless of size but only works within one filesystem; renameTo returning false is the - * signal that a boundary was crossed, so it doubles as the check, and the fallback is - * copy-then-delete. Only attempted when the destination does not exist: renaming onto an - * existing directory does not merge. - */ fun moveWithProgress(src: File, dst: File, progress: ProgressCallback?): Boolean { if (sameFile(src, dst)) return true if (!dst.exists() && src.renameTo(dst)) { @@ -70,7 +55,6 @@ object FileOps { return delete(src) } - /** A copy that reports byte progress, smooth even within one large file. */ fun copyWithProgress(src: File, dst: File, progress: ProgressCallback?): Boolean { val total = maxOf(totalSize(src), 1L) return copyWithProgress(src, dst, longArrayOf(0), total, progress) @@ -96,7 +80,7 @@ object FileOps { } val parent = dst.parentFile if (!src.exists() || (parent != null && !parent.exists() && !parent.mkdirs())) return false - val chunk = 4L * 1024 * 1024 // report progress every 4 MiB + val chunk = 4L * 1024 * 1024 try { FileInputStream(src).channel.use { input -> FileOutputStream(dst).channel.use { output -> @@ -123,7 +107,6 @@ object FileOps { private fun sameFile(a: File, b: File): Boolean = try { a.canonicalPath == b.canonicalPath } catch (e: IOException) { a.absolutePath == b.absolutePath } - /** True when [child] is [ancestor] itself or lives anywhere inside it. */ private fun isWithin(child: File, ancestor: File): Boolean { val c = try { child.canonicalPath } catch (e: IOException) { child.absolutePath } val a = try { ancestor.canonicalPath } catch (e: IOException) { ancestor.absolutePath } diff --git a/app/src/main/java/com/droiddeck/launcher/files/FilePickerActivity.kt b/app/src/main/java/com/droiddeck/launcher/files/FilePickerActivity.kt index 8f8782719..6fd89fa99 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FilePickerActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FilePickerActivity.kt @@ -16,16 +16,6 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.ui.DroidDeckTheme import java.io.File -/** - * Themed host for [FileManagerScreen] in pick mode. Launched through [InAppFilePicker] and answers with the - * picked path in [EXTRA_SELECTED_FILE] on RESULT_OK. - * - * Extras: - * - [EXTRA_EXTENSIONS] allowed lowercase extensions, no dot; absent or empty = all files - * - [EXTRA_PICK_DIRECTORY] true to pick a folder instead of a file - * - [EXTRA_INITIAL_DIRECTORY] where to open; absent = the last folder picked from, then internal storage - * - [EXTRA_PICKER_TITLE] the header - */ class FilePickerActivity : ComponentActivity() { override fun attachBaseContext(newBase: android.content.Context) { super.attachBaseContext(newBase) @@ -36,7 +26,6 @@ class FilePickerActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requestedOrientation = com.droiddeck.launcher.session.SessionPrefs.orientation(this) - // targetSdk 28: the old storage permission is exactly what lets us list the card. if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 1) } @@ -44,7 +33,6 @@ class FilePickerActivity : ComponentActivity() { val extensions = intent.getStringArrayExtra(EXTRA_EXTENSIONS)?.toList() ?: emptyList() val pickDir = intent.getBooleanExtra(EXTRA_PICK_DIRECTORY, false) val title = intent.getStringExtra(EXTRA_PICKER_TITLE) - // The screen remembers the folder it last picked from; an explicit start wins over it. val prefs = getSharedPreferences("file_manager", MODE_PRIVATE) val initialDir = (intent.getStringExtra(EXTRA_INITIAL_DIRECTORY) ?: prefs.getString("lastFilePickerDir", null)) ?.let { File(it) }?.takeIf { it.isDirectory } @@ -70,7 +58,6 @@ class FilePickerActivity : ComponentActivity() { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) - // The screen re-lists on its own when the grant lands; nothing to relay. if (requestCode == 1 && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) recreate() } diff --git a/app/src/main/java/com/droiddeck/launcher/files/FilePropertiesDialog.kt b/app/src/main/java/com/droiddeck/launcher/files/FilePropertiesDialog.kt index fff674dc9..f4f8d80fb 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FilePropertiesDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FilePropertiesDialog.kt @@ -39,17 +39,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -// The File Manager's properties dialog. -/** - * Properties sheet for a single file or folder: basic info plus the two Windows/Wine file attributes - * the in-container file manager (wfm.exe) also exposes - Read-only and Hidden. Each toggle applies - * immediately (off the main thread) and refreshes the listing via [onChanged]. - * - * State is keyed on the file so it always reflects the entry it was opened for. On a filesystem that - * can't store Wine's DOSATTRIB xattr (the FUSE /storage volumes) only the Hidden toggle is disabled; - * Read-only keeps working everywhere. - */ @Composable internal fun FilePropertiesDialog( file: File, @@ -60,7 +50,6 @@ internal fun FilePropertiesDialog( val scope = rememberCoroutineScope() val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) } var attrs by remember(file.absolutePath) { mutableStateOf<FileAttrState?>(null) } - // Guards against a second toggle landing while the first is still being applied off-thread. var busy by remember(file.absolutePath) { mutableStateOf(false) } LaunchedEffect(file.absolutePath) { attrs = withContext(Dispatchers.IO) { readFileAttrs(file) } @@ -71,7 +60,6 @@ internal fun FilePropertiesDialog( title = { Text(stringResource(R.string.fm_properties)) }, text = { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - // ── Basic info ── Text( text = file.name, color = MaterialTheme.colorScheme.onBackground, @@ -95,7 +83,6 @@ internal fun FilePropertiesDialog( Spacer(Modifier.height(6.dp)) val state = attrs - // ── Read-only ── checked when the owner can't write (Wine's FILE_ATTRIBUTE_READONLY). AttributeToggleRow( label = stringResource(R.string.fm_read_only), description = stringResource(R.string.fm_read_only_hint), @@ -116,7 +103,6 @@ internal fun FilePropertiesDialog( }, ) Spacer(Modifier.height(2.dp)) - // ── Hidden ── Wine's DOS hidden bit in the user.DOSATTRIB xattr. val hiddenSupported = state?.hiddenSupported == true AttributeToggleRow( label = stringResource(R.string.fm_hidden), @@ -134,7 +120,6 @@ internal fun FilePropertiesDialog( attrs = attrs?.copy(hidden = want) onChanged() } else { - // The write failed after all - disable the toggle rather than lie. attrs = attrs?.copy(hiddenSupported = false) Toast.makeText(context, R.string.fm_hidden_failed, Toast.LENGTH_SHORT).show() } @@ -147,7 +132,6 @@ internal fun FilePropertiesDialog( ) } -// One "label: value" line in the Properties info block. @Composable private fun PropertyLine(label: String, value: String) { Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { @@ -168,7 +152,6 @@ private fun PropertyLine(label: String, value: String) { } } -// A labelled attribute switch with a one-line description, used by the Properties sheet. @Composable private fun AttributeToggleRow( label: String, diff --git a/app/src/main/java/com/droiddeck/launcher/files/InAppFilePicker.kt b/app/src/main/java/com/droiddeck/launcher/files/InAppFilePicker.kt index f53ffd26f..1ce9aca1f 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/InAppFilePicker.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/InAppFilePicker.kt @@ -5,14 +5,7 @@ import android.content.Intent import android.net.Uri import java.io.File -/** - * The app's own file picker, the way Bannerlator's File Manager offers its pick mode: one intent - * builder per kind of pick, and the picked path read back from the result. Every import on the - * main screen goes through here rather than Android's document picker, which cannot show an SD - * card as a card, remembers nothing, and hands back a content URI the runtime cannot bind. - */ object InAppFilePicker { - /** Pick one file. [extensions] are lowercase, without the dot; empty = any file. */ fun buildIntent( context: Context, extensions: List<String> = emptyList(), @@ -24,7 +17,6 @@ object InAppFilePicker { initialDir?.let { putExtra(FilePickerActivity.EXTRA_INITIAL_DIRECTORY, it) } } - /** Pick a folder: only folders are listed, and "Use this folder" returns the one open. */ fun buildDirIntent( context: Context, title: String? = null, @@ -39,6 +31,5 @@ object InAppFilePicker { fun pickedFile(data: Intent?): File? = pickedPath(data)?.let { File(it) } - /** The picked file as the `file:` URI the import code already accepts. */ fun pickedUri(data: Intent?): Uri? = pickedFile(data)?.let { Uri.fromFile(it) } } diff --git a/app/src/main/java/com/droiddeck/launcher/files/MenuStyle.kt b/app/src/main/java/com/droiddeck/launcher/files/MenuStyle.kt index 4435c3795..81f138b20 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/MenuStyle.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/MenuStyle.kt @@ -10,9 +10,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp -// Shared outlined-card look for the app's popup menus, matching the FileManager / CommunityCard -// idiom (surfaceContainer fill, 1dp outline, rounded 10dp). Applied to a DropdownMenu's modifier so -// the popup reads as an outlined card. Keep all three menu call-sites on this so they stay identical. @Composable internal fun Modifier.outlinedMenuCard(): Modifier { val cs = MaterialTheme.colorScheme @@ -23,7 +20,6 @@ internal fun Modifier.outlinedMenuCard(): Modifier { .border(1.dp, cs.outline, shape) } -// The thin low-alpha separator that sits between items inside an outlined menu card. @Composable internal fun MenuItemDivider() { HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)) diff --git a/app/src/main/java/com/droiddeck/launcher/files/OutlinedAlertDialog.kt b/app/src/main/java/com/droiddeck/launcher/files/OutlinedAlertDialog.kt index 3159d361c..3919e89b0 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/OutlinedAlertDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/OutlinedAlertDialog.kt @@ -12,12 +12,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -// Single source of truth for the app's AlertDialogs: a drop-in wrapper around the Material3 -// slot-based AlertDialog that adds a 1dp outline around the whole popup box - matching the -// outline that Modifier.outlinedMenuCard() (MenuStyle.kt) puts on the app's menus. Signature -// mirrors androidx.compose.material3.AlertDialog exactly (compose-bom 2024.02.00 → material3 -// 1.2.0), so every call site can swap AlertDialog( → OutlinedAlertDialog( unchanged. The -// border shares the dialog's shape so its corner radius follows the rounded dialog. @Composable internal fun OutlinedAlertDialog( onDismissRequest: () -> Unit, diff --git a/app/src/main/java/com/droiddeck/launcher/files/PeIconExtractor.kt b/app/src/main/java/com/droiddeck/launcher/files/PeIconExtractor.kt index dc65b5e0c..621a06118 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/PeIconExtractor.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/PeIconExtractor.kt @@ -6,21 +6,11 @@ import java.io.File import java.io.RandomAccessFile import kotlin.math.max -/** - * Extracts the embedded application icon from a Windows PE executable (.exe). - * - * Walks the PE resource directory to the first RT_GROUP_ICON, picks the largest / - * deepest-colour entry it references, then decodes that RT_ICON image. Icon images - * are either PNG (Vista+) - decoded directly - or a classic DIB (BITMAPINFOHEADER + - * XOR pixels + 1-bpp AND mask) - decoded by hand here. - * - * Everything is best-effort: any malformed/oversized/unsupported input returns null. - */ object PeIconExtractor { private const val RT_ICON = 3 private const val RT_GROUP_ICON = 14 - private const val MAX_RES_SECTION = 64 * 1024 * 1024 // don't slurp absurd resource sections + private const val MAX_RES_SECTION = 64 * 1024 * 1024 private const val MAX_ICON_DIM = 1024 fun extract(file: File): Bitmap? = try { @@ -31,11 +21,11 @@ object PeIconExtractor { private fun parse(raf: RandomAccessFile): Bitmap? { val dos = read(raf, 0, 0x40) ?: return null - if (u8(dos, 0) != 0x4D || u8(dos, 1) != 0x5A) return null // "MZ" + if (u8(dos, 0) != 0x4D || u8(dos, 1) != 0x5A) return null val peOff = u32(dos, 0x3C).toInt() val coff = read(raf, peOff.toLong(), 24) ?: return null - if (u8(coff, 0) != 0x50 || u8(coff, 1) != 0x45 || u8(coff, 2) != 0 || u8(coff, 3) != 0) return null // "PE\0\0" + if (u8(coff, 0) != 0x50 || u8(coff, 1) != 0x45 || u8(coff, 2) != 0 || u8(coff, 3) != 0) return null val numSections = u16(coff, 6) val sizeOpt = u16(coff, 20) if (numSections <= 0 || numSections > 96 || sizeOpt <= 0) return null @@ -43,8 +33,8 @@ object PeIconExtractor { val optOff = peOff + 24L val opt = read(raf, optOff, sizeOpt) ?: return null val magic = u16(opt, 0) - val ddOff = if (magic == 0x20B) 112 else 96 // PE32+ vs PE32 data-directory offset - val resDirEntry = ddOff + 2 * 8 // data directory index 2 = resource table + val ddOff = if (magic == 0x20B) 112 else 96 + val resDirEntry = ddOff + 2 * 8 if (resDirEntry + 8 > opt.size) return null val resRVA = u32(opt, resDirEntry).toInt() val resSize = u32(opt, resDirEntry + 4).toInt() @@ -75,7 +65,6 @@ object PeIconExtractor { val resDirStart = resRVA - secVA if (resDirStart < 0 || resDirStart + 16 > res.size) return null - // RT_GROUP_ICON -> first group's data entry -> GRPICONDIR val grpTypeDir = findIdSubdir(res, resDirStart, RT_GROUP_ICON, resDirStart) ?: return null val grpData = firstDataEntry(res, grpTypeDir, resDirStart) ?: return null val grp = dataEntryBytes(res, grpData, secVA) ?: return null @@ -97,7 +86,6 @@ object PeIconExtractor { } if (bestId < 0) return null - // RT_ICON with the chosen id -> image bytes val iconTypeDir = findIdSubdir(res, resDirStart, RT_ICON, resDirStart) ?: return null val iconIdDir = findIdSubdir(res, iconTypeDir, bestId, resDirStart) ?: return null val iconData = firstDataEntry(res, iconIdDir, resDirStart) ?: return null @@ -106,9 +94,7 @@ object PeIconExtractor { return decodeIcon(img) } - // ── resource-directory navigation ── - /** Resolve the sub-directory under [dirIdx] whose id matches [wantId]; null if absent or not a directory. */ private fun findIdSubdir(res: ByteArray, dirIdx: Int, wantId: Int, resDirStart: Int): Int? { if (dirIdx + 16 > res.size) return null val total = u16(res, dirIdx + 12) + u16(res, dirIdx + 14) @@ -126,7 +112,6 @@ object PeIconExtractor { return null } - /** Descend the first entry of each level until an IMAGE_RESOURCE_DATA_ENTRY; returns its index. */ private fun firstDataEntry(res: ByteArray, dirIdx: Int, resDirStart: Int): Int? { if (dirIdx + 16 > res.size) return null if (u16(res, dirIdx + 12) + u16(res, dirIdx + 14) == 0) return null @@ -140,7 +125,6 @@ object PeIconExtractor { } } - /** Read the bytes an IMAGE_RESOURCE_DATA_ENTRY points at (its OffsetToData is an RVA). */ private fun dataEntryBytes(res: ByteArray, entryIdx: Int, secVA: Int): ByteArray? { if (entryIdx < 0 || entryIdx + 8 > res.size) return null val rva = u32(res, entryIdx).toInt() @@ -150,7 +134,6 @@ object PeIconExtractor { return res.copyOfRange(start, start + size) } - // ── icon image decoding ── private fun decodeIcon(b: ByteArray): Bitmap? { if (b.size >= 8 && u8(b, 0) == 0x89 && u8(b, 1) == 0x50 && u8(b, 2) == 0x4E && u8(b, 3) == 0x47) { @@ -164,7 +147,7 @@ object PeIconExtractor { val headerSize = u32(b, 0).toInt() val width = s32(b, 4) val heightFull = s32(b, 8) - val height = heightFull / 2 // DIB icon height counts XOR + AND mask + val height = heightFull / 2 val bitCount = u16(b, 14) if (width <= 0 || height <= 0 || width > MAX_ICON_DIM || height > MAX_ICON_DIM) return null @@ -183,7 +166,6 @@ object PeIconExtractor { val andRowSize = ((width + 31) / 32) * 4 val andStart = xorStart + rowSize * height - // 32-bit icons sometimes ship an all-zero alpha channel; fall back to the AND mask then. var hasAlpha = false if (bitCount == 32) { var y = 0 @@ -245,7 +227,6 @@ object PeIconExtractor { return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888) } - /** AND-mask alpha for a pixel: returns 0xFF000000 (opaque) or 0 (transparent), shifted into place. */ private fun maskAlpha(b: ByteArray, andStart: Int, andRowSize: Int, srcRow: Int, x: Int): Int { val o = andStart + srcRow * andRowSize + x / 8 if (o >= b.size) return 0xFF shl 24 @@ -253,7 +234,6 @@ object PeIconExtractor { return if (bit == 1) 0 else (0xFF shl 24) } - // ── little-endian readers ── private fun u8(b: ByteArray, off: Int): Int = b[off].toInt() and 0xFF private fun u16(b: ByteArray, off: Int): Int = u8(b, off) or (u8(b, off + 1) shl 8) diff --git a/app/src/main/java/com/droiddeck/launcher/files/StorageRoots.kt b/app/src/main/java/com/droiddeck/launcher/files/StorageRoots.kt index a53e792ce..99e940157 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/StorageRoots.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/StorageRoots.kt @@ -8,52 +8,24 @@ import android.os.storage.StorageVolume import com.droiddeck.launcher.R import java.io.File -/** - * A storage volume offered in the file manager's drive menu. - * - * [dir] is the best path we found for the volume: its root when that is listable, otherwise the - * deepest directory we can actually read on the way down to the app-specific directory. A volume - * always gets exactly one entry, even if several paths lead to it. - */ data class StorageRoot( val label: String, val dir: File, val removable: Boolean, - /** False when no path on the volume could be listed; the entry is still shown. */ val readable: Boolean, ) -/** - * Enumerates mounted storage volumes. - * - * Listing `/storage` is not enough on its own: a volume can be absent from this process's mount - * view (notably after a container exits and the app process is restarted onto a stale storage - * sandbox) even though it is mounted and healthy. The framework knows about it regardless, so the - * volume set is built from several independent sources: - * - * 1. [StorageManager.getStorageVolumes] - the authoritative list, read over Binder. - * 2. [Context.getExternalFilesDirs] - per-app directories, granted separately from shared storage. - * 3. `/storage` and `/mnt/media_rw` directory listings - the filesystem view, as a backstop. - * - * Those sources overlap heavily: one SD card is reachable as `/storage/<uuid>`, as - * `/mnt/media_rw/<uuid>`, and via its app-specific directory, and typically only some of them are - * readable. So paths are grouped by the *volume* they belong to rather than by path, and each - * volume contributes a single entry pointing at the best path available for it. - */ object StorageRoots { private const val EMULATED_PREFIX = "/storage/emulated" private const val INTERNAL_PATH = "/storage/emulated/0" - /** Identifies the physical volume a path belongs to; paths sharing a key are the same card. */ private const val PRIMARY_KEY = "primary" private class Volume(val key: String) { var label: String? = null var removable: Boolean = key != PRIMARY_KEY - /** Candidate directories, best-guess first. */ val candidates = LinkedHashSet<File>() - /** App-specific directories, used to find a readable path inside an unreadable root. */ val seeds = LinkedHashSet<File>() } @@ -62,7 +34,6 @@ object StorageRoots { fun volumeOf(key: String) = volumes.getOrPut(key) { Volume(key) } - // Internal storage is never removable and never absent; seed it first so it heads the menu. volumeOf(PRIMARY_KEY).apply { label = context.getString(R.string.fm_drive_internal) removable = false @@ -72,7 +43,6 @@ object StorageRoots { val appDirs = appSpecificDirs(context) val storageManager = context.getSystemService(StorageManager::class.java) - // 1 + 2 - framework-reported volumes. Authoritative, and unaffected by our mount view. storageManager?.storageVolumes.orEmpty() .filter { isMounted(it.state) } .forEach { volume -> @@ -86,14 +56,11 @@ object StorageRoots { } if (uuid != null) { entry.candidates += File("/storage/$uuid") - // Last resort only: the raw mount is normally unreadable to an app, so it must - // never outrank /storage/<uuid> and must never become an entry of its own. entry.candidates += File("/mnt/media_rw/$uuid") } appDirs.filter { belongsTo(storageManager!!, it, volume) }.forEach { entry.seeds += it } } - // 3 - filesystem backstop, for any volume the framework did not report. File("/storage").listFiles().orEmpty().forEach { child -> if (!child.isDirectory || child.name == "self" || child.name == "emulated") return@forEach volumeOf(child.name).candidates += child @@ -102,7 +69,6 @@ object StorageRoots { if (child.isDirectory) volumeOf(child.name).candidates += child } - // App-specific directories, which stay reachable when shared storage is not. appDirs.forEach { dir -> val root = volumeRootOf(dir) ?: return@forEach val entry = volumeOf(if (isEmulated(root)) PRIMARY_KEY else root.name) @@ -117,11 +83,9 @@ object StorageRoots { } private fun Volume.toRoot(context: Context): StorageRoot { - // Prefer a path we can actually list; a readable child beats an unreadable root. val dir = candidates.firstOrNull(::canBrowse) ?: candidates.firstNotNullOfOrNull { root -> seeds.firstNotNullOfOrNull { deepestReadable(root, it) } } ?: seeds.firstOrNull(::canBrowse) - // Report the volume anyway; an entry that lists empty beats one that silently vanished. ?: candidates.firstOrNull() ?: File("/storage/$key") @@ -135,7 +99,6 @@ object StorageRoots { private fun Volume.defaultLabel(context: Context): String = if (removable) context.getString(R.string.fm_drive_sd) else key - /** Appends the volume id to any label used by more than one volume, so entries stay tellable apart. */ private fun disambiguate(roots: List<StorageRoot>): List<StorageRoot> { val counts = roots.groupingBy { it.label }.eachCount() return roots.map { root -> @@ -143,7 +106,6 @@ object StorageRoots { } } - /** Walks up from [seed] towards [root] and returns the highest directory we can list. */ private fun deepestReadable(root: File, seed: File): File? { var current: File? = seed.absoluteFile var best: File? = null @@ -154,7 +116,6 @@ object StorageRoots { return best } - /** `/storage/ABCD-1234/Android/data/<pkg>/files` -> `/storage/ABCD-1234`. */ private fun volumeRootOf(appDir: File): File? = generateSequence(appDir.absoluteFile) { it.parentFile } .firstOrNull { it.name.equals("Android", ignoreCase = true) } @@ -178,11 +139,6 @@ object StorageRoots { } } - /** - * Names a volume the way the rest of the file manager does. The framework description is only - * used when it says something more useful than "SD card" - on some devices it is a bare disk - * name like "android", which tells the user nothing about which card they are looking at. - */ private fun labelFor(context: Context, volume: StorageVolume, uuid: String?): String { if (volume.isPrimary) return context.getString(R.string.fm_drive_internal) if (volume.isRemovable) return context.getString(R.string.fm_drive_sd) diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedExes.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedExes.kt index 62afb0b8a..a9f18ac06 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedExes.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedExes.kt @@ -5,15 +5,7 @@ import org.json.JSONArray import org.json.JSONObject import java.io.File -/** - * Games added from the Games tab's + rather than a Games folder: one picked .exe, or each game - * "Add all games in this folder" found. Kept by the game's folder in `files/added-exes.json`, - * which [AddedGames.scan] merges in. The exe chosen later, the shortcut appid, the edits and the - * removal live where every added game keeps them ([com.droiddeck.launcher.session.SessionPrefs]); - * this holds which folders are games and the exe they came with. - */ object AddedExes { - /** [picked]: the user chose [exe]; false = [GameExePicker]'s best guess, which a scan ranks again. */ class Entry(val folder: String, val exe: String, val picked: Boolean = true) private fun file(context: Context) = File(context.filesDir, "added-exes.json") @@ -30,7 +22,6 @@ object AddedExes { emptyList() } - /** Adds [entry], replacing one for the same folder. */ @Synchronized fun put(context: Context, entry: Entry) = write(context, list(context).filter { it.folder != entry.folder } + entry) diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt index 1fb5bb272..0dd76260b 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt @@ -11,14 +11,6 @@ import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder -/** - * Artwork for an added game: what the user put in the game's folder first, else what Steam has - * for it - by the Steam appid its files name, else by a store search on its title - else what - * SteamGridDB has ([SteamGridDb]), fetched once into the app's cache. Four pieces, named the way - * the client names them in its grid folder: the portrait capsule, the wide header, the hero - * background and the logo; plus an icon pulled out of the game's own .exe. The session hands - * the paths to the runtime's shortcuts writer, which copies them into the client's grid. - */ object AddedGameArt { private const val TAG = "AddedGameArt" private const val STORE_SEARCH = "https://store.steampowered.com/api/storesearch/?l=english&cc=US&term=" @@ -29,15 +21,10 @@ object AddedGameArt { val any get() = portrait != null || header != null || hero != null || logo != null } - /** - * The pieces a user can choose, as the client takes them: [assetType] is its custom-artwork - * number (null: the icon, set by path); [cacheName] the automatic file in the game's cache. - */ enum class Slot(val key: String, val cacheName: String, val assetType: Int?) { COVER("p", "p.jpg", 0), BACKGROUND("hero", "hero.jpg", 1), LOGO("logo", "logo.png", 2), ICON("icon", "icon.png", null), } - /** Where a piece came from; [AUTO] = nothing found yet. */ const val FOLDER = "folder" const val STEAM = "steam" const val SGDB = "sgdb" @@ -45,10 +32,8 @@ object AddedGameArt { const val EXE = "exe" const val AUTO = "" - /** A slot as it stands: the file shown and where it came from (one of [FOLDER].. [AUTO]); [chosen] = the user picked it. */ class SlotState(val slot: Slot, val file: File?, val source: String, val chosen: Boolean) - /** One image the chooser offers: its group ([FOLDER], [STEAM], [SGDB]), a thumbnail and the full image (URL or path). */ class Option(val group: String, val thumb: String, val full: String) private val CHOSEN = Regex("""chosen-(\w+)\.(\w+)""") @@ -64,7 +49,6 @@ object AddedGameArt { internal fun cacheDir(context: Context, game: AddedGames.Game) = File(context.filesDir, "added-art/${game.appId}") - /** An image in the folder (or its `art` subfolder) under one of the given names, any image extension. */ private fun find(folder: File, names: List<String>): File? { val dirs = listOf(folder, File(folder, "art"), File(folder, ".art")) for (dir in dirs) for (name in names) for (ext in IMAGE) { @@ -74,7 +58,6 @@ object AddedGameArt { return null } - /** What the folder itself offers; the file named after the folder counts as the portrait. */ private fun local(game: AddedGames.Game): Art { val folder = game.folder return Art( @@ -86,7 +69,6 @@ object AddedGameArt { ) } - /** The user's choice for each piece, else the folder's art, else the cached art; no network. */ fun resolve(context: Context, game: AddedGames.Game): Art { val own = local(game) val cache = cacheDir(context, game) @@ -100,7 +82,6 @@ object AddedGameArt { ) } - /** Each choosable piece with its file and where it came from, for the game's editor; no network. */ fun describe(context: Context, game: AddedGames.Game): List<SlotState> { val own = local(game) val cache = cacheDir(context, game) @@ -122,17 +103,11 @@ object AddedGameArt { private val CHOOSABLE = setOf("png", "jpg", "jpeg", "webp") - /** Images in the game's folder (and its art folders) that could stand for a piece. */ internal fun folderImages(folder: File): List<File> = listOf(folder, File(folder, "art"), File(folder, ".art")).flatMap { dir -> dir.listFiles { f -> f.isFile && f.extension.lowercase() in CHOOSABLE }?.sortedBy { it.name.lowercase() }.orEmpty() }.take(12) - /** - * What the chooser offers for [slot]: the game folder's images, Steam's official piece (when the - * game has a Steam appid and Steam has the piece), and SteamGridDB's best few (with a key). - * Blocking: it asks the network. - */ fun options(context: Context, game: AddedGames.Game, slot: Slot): List<Option> { val out = ArrayList<Option>() folderImages(game.folder).forEach { out.add(Option(FOLDER, it.path, it.path)) } @@ -155,7 +130,6 @@ object AddedGameArt { return out } - /** Sets [slot] to [option] (a file is copied, a URL fetched) and records where it came from. False when it could not be had. Blocking. */ fun choose(context: Context, game: AddedGames.Game, slot: Slot, option: Option): Boolean { val bytes = if (option.full.startsWith("https://")) get(option.full) else runCatching { File(option.full).readBytes() }.getOrNull() bytes ?: return false @@ -170,13 +144,11 @@ object AddedGameArt { return true } - /** Where the user's choice for [slot] came from and which image it was ("" both when automatic). */ fun chosenSource(context: Context, game: AddedGames.Game, slot: Slot): Pair<String, String> { val value = SessionPrefs.addedGameArtSource(context, game.folder.path, slot.key) return value.substringBefore('|') to value.substringAfter('|', "") } - /** [slot] back to what is found by itself. */ fun reset(context: Context, game: AddedGames.Game, slot: Slot) { chosen(cacheDir(context, game), slot)?.delete() SessionPrefs.setAddedGameArtSource(context, game.folder.path, slot.key, "") @@ -191,7 +163,6 @@ object AddedGameArt { try { c.responseCode == 200 } finally { c.disconnect() } }.getOrDefault(false) - /** The first icon inside the game's .exe, kept as a PNG beside the cached art. */ private fun exeIcon(context: Context, game: AddedGames.Game): File? { val cache = cacheDir(context, game) val png = File(cache, "icon.png") @@ -206,12 +177,6 @@ object AddedGameArt { return if (tmp.renameTo(png)) png else null } - /** - * Fetches the art that is missing: Steam first (the appid the game's files name, else a store - * search on its title whose result carries the same name), then SteamGridDB when a key is - * set. Each lookup runs once per game; a miss is retried after a week. Blocking: run it off - * the main thread. Returns true when anything new arrived. - */ fun fetchMissing(context: Context, games: List<AddedGames.Game>): Boolean { if (!SessionPrefs.addedGamesArt(context)) return false val sgdbKey = SteamGridDb.key(context) @@ -236,17 +201,12 @@ object AddedGameArt { return changed } - /** - * The game's Steam app, the one record its Windows components use too ([SteamMatch]): what its - * files name, else a store search on its title ([search]) whose result has the same name. - */ internal fun steamMatch(context: Context, game: AddedGames.Game, search: (String) -> Int?): SteamMatch.Match { val match = SteamMatch.resolve(context, game.folder.path, game.exe, game.name, legacy = File(cacheDir(context, game), "steam-appid"), search = search) match.appId?.let { Log.i(TAG, "${game.name}: Steam app $it (${match.certainty.name.lowercase()})") } return match } - /** What [marker] remembers (an id, or "none" for a week), else [find]'s answer written to it. */ private fun remembered(marker: File, find: () -> String?): String? { if (marker.isFile) { val value = marker.readText().trim() @@ -262,11 +222,6 @@ object AddedGameArt { private val EDITION = Regex("(?i)\\b(complete|definitive|legendary|goty|game of the year|deluxe|ultimate|remastered)( edition)?\\b") - /** - * How well a result's name [got] matches the game's [name]: 3 the same, 2 the same once - * edition words are dropped, 1 one starts with the other; 0 no match. Nothing is taken for - * being the first result. - */ internal fun nameMatch(name: String, got: String): Int { val want = normalize(name) val wantShort = normalize(name.replace(EDITION, " ").trim().ifEmpty { name }) @@ -280,13 +235,11 @@ object AddedGameArt { } } - /** The id of the store result whose name matches [name] best, or null when none does. */ internal fun pickStoreResult(name: String, items: List<Pair<String, String>>): String? = items.map { (id, got) -> id to nameMatch(name, got) } .filter { it.second > 0 && it.first.isNotEmpty() } .maxByOrNull { it.second }?.first - /** The Steam appid whose store name matches [name], or null for no match. */ private fun search(name: String): String? { val term = name.replace(EDITION, " ").trim().ifEmpty { name } val json = get(STORE_SEARCH + URLEncoder.encode(term, "UTF-8"))?.toString(Charsets.UTF_8) ?: return null @@ -309,10 +262,6 @@ object AddedGameArt { return got } - /** - * Newer Steam games keep their art under hashed paths (store_item_assets/steam/apps/<id>/<hash>/…), - * where the fixed CDN names above 404. The store's item API names those paths; once per appid. - */ private fun steamAssetUrl(id: String, remote: String): String? { val assets = steamAssets.getOrPut(id) { runCatching { @@ -336,11 +285,6 @@ object AddedGameArt { private val steamAssets = java.util.concurrent.ConcurrentHashMap<String, Map<String, String>>() - /** - * When nothing else gave a cover (or header), one made from the exe's icon: the icon centred on - * its own colour, darkened toward the bottom, so the game still has a face in the list and in - * Steam. Made once per game; real art found later takes over because it is looked at first. - */ private fun exeArt(context: Context, game: AddedGames.Game, cover: Boolean): File? { val cache = cacheDir(context, game) val out = File(cache, if (cover) "p-exe.png" else "header-exe.png") diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameEdits.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameEdits.kt index df3a22f9b..5690d00fe 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameEdits.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameEdits.kt @@ -5,27 +5,16 @@ import com.droiddeck.launcher.session.SessionPrefs import com.droiddeck.launcher.stores.SteamLiveShortcuts import java.io.File -/** - * The game editor's writes for an added game, Steam's non-Steam Properties in the app: name, - * target, Start in, launch options and art. Each goes through the same shortcut - the listing the - * shortcuts writer reads, and the running client - on the appid the game already has, so an edit - * never makes a second shortcut. Each call is quick (the game alone, [AddedGames.single]); the - * client and the listing follow on [AddedGamesPublisher], in order. Call off the main thread. - */ object AddedGameEdits { - /** The added game in [folder] as it stands now, or null when it is not listed. */ fun game(context: Context, folder: String): AddedGames.Game? = AddedGames.single(context, folder) - /** The client told ([live]), then the listing rewritten, on [AddedGamesPublisher]. */ private fun publish(context: Context, live: () -> Unit) = AddedGamesPublisher.later(context, live) - /** [name] as the game's name; "" goes back to the name found by itself. */ fun setName(context: Context, folder: String, name: String): AddedGames.Game? { SessionPrefs.setAddedGameName(context, folder, name) return apply(context, folder) } - /** [exe] as the game's target; false when the session cannot see it. */ fun setExe(context: Context, folder: String, exe: File): Boolean { if (!exe.isFile || AddedGames.guestPath(context, exe) == null) return false SessionPrefs.setAddedGameExe(context, folder, exe.path) @@ -33,7 +22,6 @@ object AddedGameEdits { return true } - /** [dir] (a host folder) as the game's Start in; "" = the exe's own folder. False when the session cannot see it. */ fun setStartIn(context: Context, folder: String, dir: String): Boolean { val guest = if (dir.isBlank()) "" else AddedGames.guestPath(context, File(dir.trim())) ?: return false SessionPrefs.setAddedGameStartIn(context, folder, guest) @@ -46,7 +34,6 @@ object AddedGameEdits { return apply(context, folder) } - /** [slot] set to [option], then handed to the shortcut. False when the image could not be had. */ fun setArt(context: Context, folder: String, slot: AddedGameArt.Slot, option: AddedGameArt.Option): Boolean { val game = game(context, folder) ?: return false if (!AddedGameArt.choose(context, game, slot, option)) return false @@ -54,7 +41,6 @@ object AddedGameEdits { return true } - /** [slot] back to automatic, then handed to the shortcut. */ fun resetArt(context: Context, folder: String, slot: AddedGameArt.Slot) { val game = game(context, folder) ?: return AddedGameArt.reset(context, game, slot) diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt index a4ce68acb..a4a2e80ab 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt @@ -11,7 +11,6 @@ import com.droiddeck.launcher.stores.StoreInstallRoot import java.io.File import java.util.zip.CRC32 -/** Windows game folders shared with the Steam session. */ object AddedGames { private const val TAG = "AddedGames" private const val LIBRARY = "/mnt/droiddeck-sd" @@ -19,38 +18,23 @@ object AddedGames { class Game( val folder: File, val name: String, - /** The game's own .exe: what the icon is read from and what the settings page lists. */ val exe: File, - /** The guest path the shortcut runs: [exe], or a store install's launcher .bat in front of it. */ val guestExe: String, val guestDir: String, - /** The client's 32-bit appid for this shortcut, as an unsigned value. */ val appId: Long, - /** What steam://rungameid/ takes for a shortcut. */ val gameId: Long, val candidates: List<File>, val steamAppId: Int? = null, - /** Where the game came from: a store's id, or [Library.ADDED] for a folder the user added. */ val source: String = Library.ADDED, - /** The store's own id for the game, from its sidecar; null for a plain added folder. */ val storeId: String? = null, - /** The exe is the picker's guess and nothing clearly won ([GameExePicker.Ranking.uncertain]). */ val exeUncertain: Boolean = false, - /** Steam's launch options for the shortcut; "" for none. */ val launchOptions: String = "", - /** Added from the Games tab (an entry of its own), not found in a Games folder. */ val single: Boolean = false, ) { fun folderName(): String = folder.name } - /** A Games folder an earlier build had, and where the session sees it. */ class Root(val host: File, val guest: String) - /** - * The Games folders earlier builds had, bound into the session at the guest paths they had - * then (/root/Games/<name>), so the games in them (entries now, [GamesFolderMigration]) keep - * their paths and shortcuts. Nothing adds one any more. - */ fun roots(context: Context): List<Root> { GamesFolderMigration.run(context) return SessionPrefs.gameFolderBinds(context).map { (host, guest) -> Root(File(host), guest) } @@ -61,14 +45,10 @@ object AddedGames { "|.*installer.*|.*uninstall.*|.*updater?.*|.*config(ur.*)?|.*settings.*|.*editor.*|.*server.*|.*benchmark.*|.*helper.*|.*eac.*|.*easyanticheat.*|.*battleye.*)\\.exe$", ) - /** Under here the chosen Games folders are bound inside the session, one each. */ const val GUEST_DIR = "/root/Games" - /** Where a host path appears inside the session, or null when the session cannot see it. */ fun guestPath(context: Context, host: File): String? { val path = host.absolutePath - // The Games folders are bound on their own, so a folder anywhere - an SD card, a USB - // drive - works without being inside one of the other binds. for (root in roots(context)) { val dir = root.host.absolutePath if (path == dir) return root.guest @@ -77,26 +57,19 @@ object AddedGames { SessionPrefs.romsDir(context).takeIf { it.isNotEmpty() }?.let { roms -> if (path.startsWith("$roms/")) return "/root/ROMs/" + path.removePrefix("$roms/") } - // A store root on a card has a bind of its own, before the library rule: the same folder - // keeps the same guest path (and appid) whether or not that card is also the Steam library. StoreInstallRoot.externalGuestPath(context, host)?.let { return it } GameStorage.effective(context)?.let { lib -> if (path.startsWith("${lib.path}/")) return "$LIBRARY/" + path.removePrefix("${lib.path}/") } - // Inside the runtime's own tree (store installs on internal storage live at - // root/Games/Stores): the tree is the session's / and needs no bind. val runtime = LinuxRuntime.rootDir(context).absolutePath if (path.startsWith("$runtime/")) return "/" + path.removePrefix("$runtime/") - // Internal storage, bound at /root/Storage in every session: an exe picked anywhere on it. val storage = android.os.Environment.getExternalStorageDirectory().absolutePath if (path.startsWith("$storage/")) return "/root/Storage/" + path.removePrefix("$storage/") return null } - /** The .exe files a game folder offers, best first ([GameExePicker]); declared Store exes lead. */ fun candidates(folder: File): List<File> = rank(folder).exes - /** [GameExePicker]'s ranking, with a Store-declared executable taking precedence when present. */ fun rank(folder: File): GameExePicker.Ranking { val key = "${folder.path}|${folder.lastModified()}" val now = System.currentTimeMillis() @@ -115,20 +88,11 @@ object AddedGames { return result } - /** A folder's ranking for a minute, so one add (rank, then the game built from it) walks it once. */ private val ranked = java.util.concurrent.ConcurrentHashMap<String, Pair<Long, GameExePicker.Ranking>>() private const val RANK_FRESH_MS = 60_000L - /** - * Names identified from an exe (Steam manifest, GOG info, version resource), by the exe's path, - * size and time and its game folder's time (a GOG info file dropped in changes the latter). - */ private val identified = java.util.concurrent.ConcurrentHashMap<String, String>() - /** - * The game's own title for [exe] when the disk names one ([GameIdentifier]: a Steam manifest, - * a GOG info file, the exe's version resource), else null (the folder name stands). - */ internal fun identifiedName(folder: File, exe: File): String? { val key = "${exe.path}|${exe.length()}|${exe.lastModified()}|${folder.lastModified()}" identified[key]?.let { return it.ifEmpty { null } } @@ -141,13 +105,6 @@ object AddedGames { return name } - /** - * The exe's version resource often spells the title the way a programmer typed it ("Insane2") - * while the folder has the real spacing ("Insane 2"). When both spell the same letters and - * digits, the one with more word breaks wins; a tie keeps mixed case over all one case - * ("DiRT 3" over "dirt 3"), else the exe's spelling. Different titles keep the exe's. - */ - /** "Watch_Dogs" → "Watch Dogs": an underscore in a version-resource title is a programmer's space. */ internal fun underscoresToSpaces(name: String): String = name.replace('_', ' ').replace(Regex("\\s+"), " ").trim().ifEmpty { name } @@ -163,7 +120,6 @@ object AddedGames { else -> peName } } - /** The executables MicrosoftGame.config or appxmanifest.xml declares that exist inside [folder]. */ internal fun declaredExes(folder: File): List<File> { val names = DECLARED.flatMap { (file, pattern) -> runCatching { pattern.findAll(File(folder, file).readText()).map { it.groupValues[1] }.toList() }.getOrDefault(emptyList()) @@ -195,30 +151,20 @@ object AddedGames { val steamInstalls = steamInstallDirs(dir) for (folder in dir.listFiles { f -> f.isDirectory }?.sortedBy { it.name.lowercase() } ?: emptyList()) { if (folder.name.lowercase() in steamInstalls || folder.name.equals("steamapps", ignoreCase = true)) continue - // Removed from the list by the user (the folder stays on disk); restored from the settings page. if (folder.path in removed) continue - // The store install root sits inside the Games storage; its games are scanned below, per store. if (folder.name == "Games" && StoreInstallRoot.roots(context).any { it.absolutePath == folder.absolutePath }) continue scanGame(context, folder, out) } } - // Games added as one picked exe (the Games tab's +), by that exe's folder. for (entry in AddedExes.list(context)) { val folder = File(entry.folder) if (folder.path in removed || !folder.isDirectory) continue scanGame(context, folder, out, single = entry) } - // Games the Stores section installed: one folder each under <root>/<Store>/, with a sidecar. - // An unfinished one (sidecar still `installing`, or none at all) is neither a game nor Custom. for (folder in StoreInstallRoot.gameFolders(context)) if (StoreInstallRoot.isFinished(folder)) scanGame(context, folder, out) return out.distinctBy { it.folder.canonicalPath } } - /** - * The game in [folderPath] alone, built by the same rules as [scan] (store sidecar, an entry - * from the Games tab, the chosen exe, the edits) without walking every other game: what the - * editor needs for one game. Null when that folder is not a game, or was removed. - */ fun single(context: Context, folderPath: String): Game? { val folder = File(folderPath) if (!folder.isDirectory || folderPath in SessionPrefs.removedAddedGames(context)) return null @@ -227,12 +173,6 @@ object AddedGames { return out.firstOrNull() } - /** - * Stops listing the added game in [folder] at once: marked removed and dropped from the - * library cache; the running client loses its shortcut ([appId], else the one stored for the - * folder) and the listing is rewritten afterwards ([AddedGamesPublisher]). Nothing on disk is - * deleted. Quick; call off the main thread (preferences and a small file). - */ fun remove(context: Context, folder: File, appId: Long? = null, name: String = folder.name) { val app = context.applicationContext val started = android.os.SystemClock.elapsedRealtime() @@ -243,13 +183,10 @@ object AddedGames { AddedGamesPublisher.later(app) { if (id != null) com.droiddeck.launcher.stores.SteamLiveShortcuts.remove(app, id) } } - /** What [addExe] did with a picked exe. */ sealed class AddResult { - /** A new game, or the existing one the exe already is, or the listed game whose exe it now is. */ class Added(val game: Game) : AddResult() class Existing(val game: Game) : AddResult() class Switched(val game: Game) : AddResult() - /** The session cannot see the exe's path. */ object Unreachable : AddResult() object NotFound : AddResult() } @@ -258,11 +195,6 @@ object AddedGames { private fun inside(path: String, folder: String) = path.startsWith(folder.trimEnd('/') + "/") - /** - * The game folder [exe] belongs to, without a scan: an entry from the Games tab (Games folders - * of earlier builds included), a removed game, a store install, or the first-level folder under - * the Steam library that holds it. Null when it is in none. - */ private fun ownerFolder(context: Context, exe: File): File? { GamesFolderMigration.run(context) val path = canonical(exe) @@ -270,7 +202,6 @@ object AddedGames { AddedExes.list(context).map { File(it.folder) }.firstOrNull(::holds)?.let { return it } SessionPrefs.removedAddedGames(context).map { File(it) }.firstOrNull(::holds)?.let { return it } StoreInstallRoot.gameFolders(context).firstOrNull(::holds)?.let { return it } - // Games folders of earlier builds are entries now, found above. val dirs = listOfNotNull( GameStorage.effective(context)?.let { File(it.path, "steamapps/common") }, GameStorage.effective(context)?.let { File(it.path) }, @@ -284,12 +215,6 @@ object AddedGames { return null } - /** - * Adds the game [exe] is: the game itself when it is listed already, that game's exe switched - * when [exe] sits in a listed (or removed) game's folder, else a new game for [exe]'s folder. - * Only that one game is read; Steam (the running client, then the listing) follows on - * [AddedGamesPublisher]. Nothing is added when the session cannot see the path. - */ fun addExe(context: Context, exe: File): AddResult { val app = context.applicationContext if (!exe.isFile) return AddResult.NotFound @@ -309,15 +234,8 @@ object AddedGames { return if (owner == null) AddResult.Added(game) else AddResult.Switched(game) } - /** What [addFolder] did: the games it added, and how many of the folder's games were listed already. */ class FolderResult(val added: List<Game>, val already: Int) - /** - * "Add all games in this folder": every subfolder of [dir] with an .exe becomes a game with - * [GameExePicker]'s best one (marked uncertain where nothing clearly won), as an entry of its - * own, not a Games folder. A subfolder that is, holds or sits in a listed game counts as - * already there; a removed one comes back. Steam follows once, on [AddedGamesPublisher]. Blocking. - */ fun addFolder(context: Context, dir: File, onGame: (Game) -> Unit = {}, onChecking: (String?) -> Unit = {}): FolderResult { val app = context.applicationContext val removed = SessionPrefs.removedAddedGames(app).toSet() @@ -343,11 +261,6 @@ object AddedGames { return FolderResult(added, already) } - /** - * The game folders already listed, without building any game: the first-level folders of the - * Steam library (as [scan] walks them), the Games tab's entries (Games folders of earlier builds - * included) and the store installs. Canonical paths; removed games are left out. - */ private fun listedFolders(context: Context, removed: Set<String>): List<String> { val out = ArrayList<String>() GamesFolderMigration.run(context) @@ -367,11 +280,6 @@ object AddedGames { return out } - /** - * The folders under a library's steamapps/common that a manifest beside it already claims. - * Steam lists those itself, so a shortcut would only add a non-Steam copy of the game. Steam - * may lowercase installdir on Android's case-insensitive storage, so names compare lowercased. - */ internal fun steamInstallDirs(dir: File): Set<String> { val steamapps = dir.parentFile?.takeIf { dir.name == "common" && it.name == "steamapps" } ?: return emptySet() return steamapps.listFiles { f -> f.isFile && f.name.startsWith("appmanifest_") && f.name.endsWith(".acf") } @@ -385,33 +293,24 @@ object AddedGames { private val INSTALL_DIR = Regex("\"installdir\"\\s*\"([^\"]*)\"", RegexOption.IGNORE_CASE) - /** [single]: the entry a game added from the Games tab has; its exe stands in when no other is chosen and the user picked it. */ private fun scanGame(context: Context, folder: File, out: MutableList<Game>, single: AddedExes.Entry? = null) { run { - // A store install carries a sidecar; every one of them is a shortcut. val sidecar = StoreGameSidecar.read(folder) val ranking = rank(folder) val candidates = ranking.exes val picked = SessionPrefs.addedGameExe(context, folder.path) val chosen = picked.takeIf { it.isNotEmpty() }?.let { File(it) }?.takeIf { it.isFile } ?: single?.takeIf { it.picked }?.let { File(it.exe) }?.takeIf { it.isFile } - // A store install says which exe it is; the folder's guess only when that file is gone. val found = chosen ?: sidecar?.exeFile(folder)?.takeIf { it.isFile } ?: candidates.firstOrNull() ?: return - // The store's launcher .bat stands in front of the exe when the game needs arguments or - // environment; it is what the shortcut runs. Dropped when the user picked another exe. val launch = if (chosen == null) sidecar?.launcherFile(folder)?.takeIf { it.isFile } ?: found else found val foundGuest = guestPath(context, launch) if (foundGuest == null) { Log.w(TAG, "${folder.name}: the session cannot see ${launch.path}"); return } - // A store install's title; else the title the disk gives (the shortcut's appid is kept - // per folder from its first scan, so a name found later does not move it); else the folder. val autoName = sidecar?.title ?: identifiedName(folder, found) ?: folder.name - // Keyed by the pre-rename path so shortcut ids, and the prefixes and saves under them, stay put. val crc = CRC32().apply { update(("\"${foundGuest.replaceFirst(Regex("^$LIBRARY/"), "$LEGACY_LIBRARY/")}\"" + autoName).toByteArray()) }.value val appId = SessionPrefs.addedGameAppId(context, folder.path, crc or 0x80000000L) val config = accountConfig(File(LinuxRuntime.rootDir(context), "root/.local/share/Steam")) val record = config?.let { shortcutRecord(it, appId) } val rev = record?.optInt("rev", 0) ?: 0 - // Name, Start in and launch options edited in Steam's own Properties become the app's. if (sidecar == null && record != null) adoptSteamEdits(context, folder, record) val name = SessionPrefs.addedGameName(context, folder.path).takeIf { sidecar == null && it.isNotEmpty() } ?: autoName val adopted = guestPath(context, folder) @@ -460,12 +359,6 @@ object AddedGames { private fun unquote(value: String?): String = value.orEmpty().trim().trim('"') - /** - * What the user changed in Steam's Properties since the app last wrote the shortcut ([record]: - * what the app asked for, and what Steam had): a new name, launch options, or a Start in other - * than the exe's folder, taken over as the app's own. Each Steam value is taken once, so an app - * edit made after it is not undone before the shortcut is written again. - */ internal fun adoptSteamEdits(context: Context, folder: File, record: JSONObject) { val app = record.optJSONObject("app") ?: return val steam = record.optJSONObject("steam") ?: return @@ -489,7 +382,6 @@ object AddedGames { } } - /** The list the session hands the runtime's shortcuts writer; one file per session start. */ fun writeListing(context: Context, games: List<Game>): File { val file = File(context.filesDir, "session/added-games.json").apply { parentFile?.mkdirs() } val json = StringBuilder("[") @@ -500,8 +392,6 @@ object AddedGames { .append(",\"dir\":").append(quote(g.guestDir)).append(",\"appid\":").append(g.appId) .append(",\"seen\":").append(SessionPrefs.addedGameExeSeen(context, g.folder.path)) if (g.launchOptions.isNotEmpty()) json.append(",\"launch\":").append(quote(g.launchOptions)) - // The art, as the session sees it: the app's cache is bound at its own path, a file in - // the game's folder at the folder's guest path. val art = AddedGameArt.resolve(context, g) val pieces = listOf("p" to art.portrait, "header" to art.header, "hero" to art.hero, "logo" to art.logo, "icon" to art.icon) .mapNotNull { (k, f) -> f?.let { file -> artGuestPath(context, file)?.let { k to it } } } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGamesPublisher.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGamesPublisher.kt index 19243b8e8..7fc51558f 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGamesPublisher.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGamesPublisher.kt @@ -5,23 +5,15 @@ import android.util.Log import androidx.annotation.VisibleForTesting import java.util.concurrent.Executors -/** - * The slow part of an added-game change, off the caller's path and in order: the running client - * told ([later]'s work), then the listing the shortcuts writer reads at the next session start and - * the library cache, both from one scan ([republish]). An add, an edit or a removal shows at once; - * this follows. - */ object AddedGamesPublisher { private const val TAG = "AddedGamesPublisher" - /** Tests publish at once, to read the listing right after a change. */ @VisibleForTesting @Volatile internal var now = false private val worker = Executors.newSingleThreadExecutor { r -> Thread(r, "added-games-publish") } - /** [live] (a DevTools call or nothing), then [republish], on the worker. */ fun later(context: Context, live: () -> Unit = {}) { val app = context.applicationContext val work = Runnable { @@ -31,7 +23,6 @@ object AddedGamesPublisher { if (now) work.run() else worker.execute(work) } - /** The listing and the library cache from one scan of the added games. Blocking. */ fun republish(context: Context) { val games = AddedGames.scan(context) AddedGames.writeListing(context, games) diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt index b71c8c1a4..ced1abfbe 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt @@ -16,29 +16,12 @@ import org.json.JSONObject import java.io.File import java.util.concurrent.PriorityBlockingQueue -/** - * Turns on, per game, the Windows components its source's list names ([WinCompSources]). Only the - * findings marked auto are turned on, only the ones that install here, and never one the user - * switched off: the user's switches are kept apart ([WinComponents.Selection.user]) and win. - * - * All the work is done by one background thread (low priority, so are the runtime helpers it - * starts), one job at a time from one queue: working out a game's list (kept, so a page opens on - * it) and downloading a component (each once, whichever games want it). The queue waits while a - * session runs; jobs nobody asked for (the start-up sweep, a game being launched) also wait on - * battery saver, a warm device or a metered network. Nothing here runs on the caller's thread. - */ object AutoComponents { private const val TAG = "AutoComponents" - /** The key a game's picks are kept under: the appid its prefix is named after. */ fun appKey(game: Library.SteamGame): String? = if (game.library == Library.ADDED) Integer.toUnsignedString(game.appId) else game.appId.takeIf { it > 0 }?.toString() - /** - * What turns on by itself for [findings], given [catalog] and what is [installed]: the catalog - * name each auto finding installs as, with its reason. A component that needs a Windows - * installer, or is not in the catalog, stays out (the editor lists it as a suggestion). - */ fun autoPicks(findings: List<WinCompSources.Finding>, catalog: Map<String, WinComponents.Component>): Map<String, WinComponents.AutoReason> { val out = LinkedHashMap<String, WinComponents.AutoReason>() for (f in findings) { @@ -51,30 +34,22 @@ object AutoComponents { return out } - // ---- seams for tests ---- @VisibleForTesting internal var catalogOf: (Context, Boolean) -> List<WinComponents.Component>? = { c, network -> if (network) WinComponents.fetch(c) else WinComponents.cached(c) } @VisibleForTesting internal var installedOf: (Context) -> Set<String> = { WinComponents.installedIds(it).toSet() } @VisibleForTesting internal var installer: (Context, WinComponents.Component, Map<String, WinComponents.Component>, (WinComponents.Progress) -> Unit) -> String? = { c, comp, all, progress -> WinComponents.install(c, comp, all, progress) } @VisibleForTesting internal var sessionRunning: () -> Boolean = { SessionState.running } - /** Battery saver, a warm device or a metered network: no work nobody asked for. */ @VisibleForTesting internal var unfavourable: (Context) -> Boolean = { c -> constrained(c) } - /** A usable network: a download waits for one. */ @VisibleForTesting internal var online: (Context) -> Boolean = { c -> isOnline(c) } - /** Tests run the queue on the caller's thread ([drain]) instead of the worker. */ @VisibleForTesting @Volatile internal var runNow = false - /** Bumped whenever a game's list or automatic components change, so an open page reads them again. */ val changes = MutableStateFlow(0) - /** The components downloading for an automatic pick right now, with their progress. */ val downloading = MutableStateFlow<Map<String, WinComponents.Progress>>(emptyMap()) - /** Games (by [appKey]) whose list is queued or being worked out: a page waits on it. */ val pendingGames = MutableStateFlow<Set<String>>(emptySet()) - /** Components queued for an automatic pick, not yet downloaded. */ val pendingComponents = MutableStateFlow<Set<String>>(emptySet()) private fun mark(job: Job, on: Boolean) { @@ -84,10 +59,8 @@ object AutoComponents { flow.value = if (on) flow.value + id else flow.value - id } - /** One piece of work: a game's list ([game]) or one component's download ([component]). [asked]: the user is waiting on it. */ internal class Job( val game: Library.SteamGame?, val component: String?, val asked: Boolean, val seq: Long, val pics: List<Int>? = null, - /** For a game Steam installed during the running session: may run in it, unless a game is running. */ val inSession: Boolean = false, ) { val key get() = when { @@ -95,24 +68,17 @@ object AutoComponents { component != null -> "c:$component" else -> "g:" + (game?.let { appKey(it) } ?: "") } - /** Needs the network: a download, or Steam asked anonymously. */ val online get() = component != null || pics != null } private val seq = java.util.concurrent.atomic.AtomicLong() - /** What the user asked for first, then in the order queued. */ private val jobs = PriorityBlockingQueue<Job>(16, compareByDescending<Job> { it.asked }.thenBy { it.seq }) @Volatile private var draining = false private val queuedKeys = java.util.Collections.synchronizedSet(HashSet<String>()) - /** The games waiting on a component's download, to turn it on for when it lands. */ private val waiting = HashMap<String, MutableList<Library.SteamGame>>() @Volatile private var worker: Thread? = null private const val PAUSE_MS = 15_000L - /** - * [game]'s list worked out again on the worker, and its components queued: [asked] when the - * user is waiting (its page is open, or they installed or added it). - */ fun queueGame(context: Context, game: Library.SteamGame, asked: Boolean) { if (appKey(game) == null) return enqueue(context, Job(game, null, asked, seq.incrementAndGet())) @@ -121,10 +87,8 @@ object AutoComponents { private fun queueDownload(context: Context, id: String, asked: Boolean, inSession: Boolean = false) = enqueue(context, Job(null, id, asked, seq.incrementAndGet(), inSession = inSession)) - /** Adds [job] unless the same one is queued (a game or a component, once each). */ private fun enqueue(context: Context, job: Job) { if (!queuedKeys.add(job.key)) { - // Asked for now: ahead of what nobody asked for, so the open page does not wait behind the sweep. if (job.asked && jobs.removeIf { it.key == job.key && !it.asked }) jobs.add(job) if (runNow && !draining) drain(context) return @@ -135,12 +99,10 @@ object AutoComponents { if (runNow) { if (!draining) drain(context) } else ensureWorker(context.applicationContext) } - /** Whether [job] has to wait: always while a session runs; one nobody asked for also when it is unfavourable. */ internal fun mustWait(context: Context, job: Job): Boolean = if (job.inSession && sessionRunning()) gameRunning() || (job.online && !online(context)) else sessionRunning() || (job.online && !online(context)) || (!job.asked && unfavourable(context)) - /** A game is running in the session (Steam's reaper, or Proton waiting on its game). */ @VisibleForTesting internal var gameRunning: () -> Boolean = { gameProcessRunning() } private fun gameProcessRunning(): Boolean = File("/proc").listFiles()?.any { p -> @@ -148,17 +110,11 @@ object AutoComponents { ?.let { it.contains("SteamLaunch AppId=") || it.contains("proton waitforexitandrun") } == true } == true - /** - * A game Steam installed during the running session: its list worked out and its components - * readied in the session (one job, low priority), so a launch right after finds them; it - * waits while a game runs. All other work stays paused until the session ends. - */ fun queueInSession(context: Context, game: Library.SteamGame) { if (appKey(game) == null) return enqueue(context, Job(game, null, true, seq.incrementAndGet(), inSession = true)) } - /** Runs the queued jobs on this thread until the queue is empty or the next one has to wait. Tests. */ @VisibleForTesting internal fun drain(context: Context) { draining = true @@ -173,7 +129,6 @@ object AutoComponents { } } - /** The first job in order that may run now, taken off the queue; null when none may. */ private fun next(context: Context): Job? = synchronized(jobs) { val job = jobs.sortedWith(order).firstOrNull { !mustWait(context, it) } ?: return null jobs.remove(job) @@ -190,8 +145,6 @@ object AutoComponents { worker = Thread({ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND) while (true) { - // Something queued, then the first that may run (one for a game installed in the - // session goes ahead of paused work); else a look again in a while. try { jobs.put(jobs.take()) } catch (e: InterruptedException) { return@Thread } val job = next(app) if (job == null) { @@ -219,21 +172,14 @@ object AutoComponents { } } - /** - * [game]'s list (worked out again only when what it is read from changed), its automatic - * components that are downloaded turned on, and the rest queued for download. - */ private fun apply(context: Context, game: Library.SteamGame, asked: Boolean, inSession: Boolean = false) { val key = appKey(game) ?: return val folder = game.gameFiles if (game.library == Library.ADDED && game.source == Library.ADDED && folder != null && SteamMatch.get(context, folder.path) == null) { AddedGames.single(context, folder.path)?.let { SteamMatch.resolve(context, folder.path, it.exe, it.name) } } - // A GOG or Epic game installed before its sidecar kept the store's list: read it once from the store. val store = com.droiddeck.launcher.stores.Store.byId(game.source) if (folder != null && (store == com.droiddeck.launcher.stores.Store.GOG || store == com.droiddeck.launcher.stores.Store.EPIC)) StoreListBackfill.fill(context, folder) - // Its Steam app, when the client never saw it: Steam asked anonymously now if the user is - // waiting on this game (a pass over every game asks for all of them in one go, [sweep]). if (asked && online(context)) steamAppOf(context, game)?.let { SteamAppInfo.lookup(context, listOf(it)) } val findings = findings(context, game) val catalog = (catalogOf(context, false) ?: catalogOf(context, true))?.associateBy { it.name } ?: return @@ -249,7 +195,6 @@ object AutoComponents { queueDownload(context, id, asked, inSession) missing.add(id) } - // In a session the launch waits a little for these (droiddeck-wincomponents reads the list). if (inSession) Pending.set(context, key, missing) val before = WinComponents.selection(context, key).auto if (before.keys != kept.keys) { @@ -259,7 +204,6 @@ object AutoComponents { changes.value = changes.value + 1 } - /** One component downloaded and installed, then turned on for every game waiting on it. */ private fun download(context: Context, id: String, asked: Boolean, inSession: Boolean = false) { val catalog = catalogOf(context, true)?.associateBy { it.name } ?: run { Pending.drop(context, id); return } if (id !in installedOf(context)) { @@ -279,10 +223,6 @@ object AutoComponents { Pending.drop(context, id) } - /** - * The components still being readied for games installed in the session, where the launch - * reads them (~/.config/droiddeck/wincomponents-pending.json in the runtime). - */ internal object Pending { private fun file(context: Context) = File(com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context), "root/.config/droiddeck/wincomponents-pending.json") @@ -315,11 +255,9 @@ object AutoComponents { } } - // ---- the list, kept per game ---- private const val FINDINGS = "wincomp-findings.json" - /** What the list is read from, by modification time: a change means it is worked out again. */ internal fun signature(context: Context, game: Library.SteamGame): String { val folder = game.gameFiles val parts = listOfNotNull( @@ -334,7 +272,6 @@ object AutoComponents { return parts.joinToString("|") } - /** [game]'s list: the one kept when nothing it is read from changed, else worked out again and kept. Worker only. */ internal fun findings(context: Context, game: Library.SteamGame): List<WinCompSources.Finding> { val key = appKey(game) ?: return emptyList() val sig = signature(context, game) @@ -345,7 +282,6 @@ object AutoComponents { return fresh } - /** The list kept for [appKey], without working anything out; null before the first time. */ fun cachedFindings(context: Context, appKey: String): List<WinCompSources.Finding>? = readFindings(context).optJSONObject(appKey)?.let { decode(it.optJSONArray("findings")) } @@ -374,34 +310,21 @@ object AutoComponents { WinCompSources.Finding(o.optString("component").takeIf { it.isNotEmpty() }, o.optString("original"), origin, o.optBoolean("auto"), args) } - // ---- when ---- - /** Before a launch from DroidDeck: nothing but a queued look (it waits for the session to end). */ fun beforeLaunch(context: Context, game: Library.SteamGame) = queueGame(context, game, asked = false) - /** After an install or an add: the user did it, so it does not wait on a metered network. */ fun refreshLater(context: Context, game: Library.SteamGame) = queueGame(context, game, asked = true) - /** - * Every game, whatever its source, at app start and after each session (Steam installs its - * games without a word to DroidDeck; older GOG and Epic installs get their list read once): - * only queued, one game at a time, behind anything asked for, under the same conditions. A - * game whose files did not change costs a look at their times. - */ fun sweep(context: Context, games: List<Library.SteamGame>) { - // First every Steam app these games are that the client never saw, asked of Steam in one - // connection; then each game's list. val apps = games.mapNotNull { steamAppOf(context, it) }.distinct() if (apps.isNotEmpty() && SteamAppInfo.needLookup(context, apps).isNotEmpty()) enqueue(context, Job(null, null, false, seq.incrementAndGet(), pics = apps)) games.forEach { queueGame(context, it, asked = false) } } - /** The Steam app whose list a game takes: a Steam game's own, or an added game's match. */ private fun steamAppOf(context: Context, game: Library.SteamGame): Int? = if (game.library != Library.ADDED) game.appId.takeIf { it > 0 } else game.gameFiles?.let { SteamMatch.get(context, it.path) }?.appId - /** The default network is there and validated (reaches the internet). */ fun isOnline(context: Context): Boolean { val cm = context.getSystemService(ConnectivityManager::class.java) ?: return false val caps = cm.getNetworkCapabilities(cm.activeNetwork ?: return false) ?: return false @@ -409,7 +332,6 @@ object AutoComponents { caps.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED) } - /** Battery saver, thermal status moderate or worse, or a metered network. */ private fun constrained(context: Context): Boolean { val power = context.getSystemService(PowerManager::class.java) if (power?.isPowerSaveMode == true) return true diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/CoverArt.kt b/app/src/main/java/com/droiddeck/launcher/frontend/CoverArt.kt index cf7f151aa..8bb83d15a 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/CoverArt.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/CoverArt.kt @@ -11,27 +11,12 @@ import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder -/** - * Box art for the emulators' games, found and kept on the device: the front end shows a disc image - * with nothing but its emulator's logo otherwise. - * - * In order: the name of the file against RetroArch's thumbnail collection for the system - * (libretro-thumbnails, named after the standard disc-dump names - "Need for Speed - Underground 2 - * (USA)"), first exactly and then loosely (tags dropped, the same region preferred) against the - * collection's list of names; then the disc's own ID, read from the image - PS2 serials against - * xlenore/ps2-covers (what PCSX2 and ARMSX2 download from), GameCube/Wii IDs against GameTDB (what - * Dolphin uses). A PS3 disc image gives its own PS3_GAME/ICON0.PNG first: the libretro PS3 - * collection is small. A cover is downloaded once into files/covers; a game none of them has is not - * asked about again for a few days. - */ object CoverArt { private const val TAG = "CoverArt" private const val MISS_RETRY_MS = 3L * 24 * 3600 * 1000 private const val INDEX_TTL_MS = 14L * 24 * 3600 * 1000 - /** Downloads in one pass, so a first visit to a big library does not hold the network for long. */ private const val MAX_PER_PASS = 40 - /** The emulator's systems in libretro-thumbnails, in the order they are tried. */ private val collections = mapOf( "rpcs3" to listOf("Sony_-_PlayStation_3"), "armsx2" to listOf("Sony_-_PlayStation_2"), @@ -44,14 +29,12 @@ object CoverArt { private fun root(context: Context) = File(context.filesDir, "covers") - /** The file name, as the cover is kept under: a game keeps its art when its folder moves. */ private fun keyOf(rom: Library.Rom): String = rom.hostPath.name.replace(Regex("[^A-Za-z0-9._() -]"), "_").take(150) private fun coverFile(context: Context, rom: Library.Rom) = File(root(context), "${rom.emulatorId}/${keyOf(rom)}.img") private fun missFile(context: Context, rom: Library.Rom) = File(root(context), "${rom.emulatorId}/${keyOf(rom)}.miss") - /** A cover already on the device for this game, or null. */ fun cached(context: Context, rom: Library.Rom): File? = coverFile(context, rom).takeIf { it.isFile && it.length() > 0 } private fun wanted(context: Context, rom: Library.Rom): Boolean { @@ -60,10 +43,6 @@ object CoverArt { return !(miss.isFile && System.currentTimeMillis() - miss.lastModified() < MISS_RETRY_MS) } - /** - * Looks for covers for the games that have none, on the calling (background) thread. True when - * at least one was found, so the caller rebuilds the list to show it. - */ fun fetchMissing(context: Context, roms: List<Library.Rom>): Boolean { var found = false var tries = 0 @@ -71,7 +50,6 @@ object CoverArt { if (!wanted(context, rom)) continue if (++tries > MAX_PER_PASS) break val target = coverFile(context, rom) - // A PS3 disc carries its own cover (PS3_GAME/ICON0.PNG, not encrypted): no download. if (rom.emulatorId == "rpcs3" && try { copyPs3Icon(rom.hostPath, target) } catch (e: Exception) { false }) { Log.i(TAG, "${rom.name}: ICON0.PNG from the disc") missFile(context, rom).delete() @@ -93,13 +71,10 @@ object CoverArt { private fun findUrl(context: Context, rom: Library.Rom): String? { val name = rom.hostPath.nameWithoutExtension.removeSuffix(".dec") for (collection in collections.getValue(rom.emulatorId)) { - // Exactly the dump name first: no listing needed. val exact = thumbnailUrl(collection, name) if (exists(exact)) return exact - // Then loosely, against the collection's names. nearest(name, names(context, collection))?.let { return thumbnailUrl(collection, it) } } - // The disc's own ID. return when (rom.emulatorId) { "armsx2" -> ps2Serial(rom.hostPath)?.let { "https://raw.githubusercontent.com/xlenore/ps2-covers/main/covers/default/$it.jpg" } "dolphin" -> nintendoDiscId(rom.hostPath)?.let { id -> @@ -110,18 +85,15 @@ object CoverArt { }?.takeIf { exists(it) } } - /** libretro-thumbnails replaces these in a title's file name. */ private fun thumbName(title: String) = title.replace(Regex("[&*/:`<>?\\\\|\"]"), "_") private fun thumbnailUrl(collection: String, title: String) = "https://raw.githubusercontent.com/libretro-thumbnails/$collection/master/Named_Boxarts/" + URLEncoder.encode("${thumbName(title)}.png", "UTF-8").replace("+", "%20") - // ---- loose matching ------------------------------------------------------------------------ private fun tags(name: String): List<String> = Regex("[(\\[]([^)\\]]*)[)\\]]").findAll(name).map { it.groupValues[1].lowercase() }.toList() - /** A title without its tags, articles and punctuation: "Legend of Zelda, The (USA)" = "legend of zelda". */ private fun base(name: String): String = name .replace(Regex("\\s*[(\\[][^)\\]]*[)\\]]"), " ") .lowercase() @@ -139,7 +111,6 @@ object CoverArt { return same.firstOrNull { region in tags(it) } ?: same.firstOrNull { "usa" in tags(it) } ?: same.first() } - /** The titles in a collection's Named_Boxarts (no extension), kept for a fortnight. */ private fun names(context: Context, collection: String): List<String> { val cache = File(root(context), "index-$collection.txt") if (cache.isFile && System.currentTimeMillis() - cache.lastModified() < INDEX_TTL_MS) return cache.readLines() @@ -161,12 +132,7 @@ object CoverArt { private fun cachedOr(cache: File): List<String> = if (cache.isFile) cache.readLines() else emptyList() - // ---- disc IDs ------------------------------------------------------------------------------ - /** - * A file's bytes from an ISO 9660 image by path ("PS3_GAME/ICON0.PNG"), or null. Primary volume - * names only (upper case, ";1" suffix), which is what PS2 and PS3 discs use. - */ private fun isoFile(file: File, path: String, maxSize: Int): ByteArray? { if (!file.name.endsWith(".iso", ignoreCase = true)) return null RandomAccessFile(file, "r").use { f -> @@ -208,14 +174,12 @@ object CoverArt { return if (bounds.outWidth > 0 && tmp.renameTo(target)) true else { tmp.delete(); false } } - /** "SLUS-21065" from a PS2 .iso's SYSTEM.CNF (BOOT2 = cdrom0:\SLUS_210.65;1), or null. */ private fun ps2Serial(file: File): String? { val cnf = String(isoFile(file, "SYSTEM.CNF", 4096) ?: return null, Charsets.US_ASCII) val m = Regex("BOOT2\\s*=\\s*cdrom0:\\\\?([A-Z]{4})_(\\d{3})\\.(\\d{2})").find(cnf) ?: return null return "${m.groupValues[1]}-${m.groupValues[2]}${m.groupValues[3]}" } - /** The six-character ID of a GameCube/Wii disc image (.iso/.gcm, or .wbfs), or null. */ private fun nintendoDiscId(file: File): String? { val offset = when (file.extension.lowercase()) { "iso", "gcm" -> 0L; "wbfs" -> 0x200L; else -> return null } RandomAccessFile(file, "r").use { f -> @@ -225,7 +189,6 @@ object CoverArt { } } - // ---- network ------------------------------------------------------------------------------- private fun open(url: String, method: String = "GET"): HttpURLConnection = (URL(url).openConnection() as HttpURLConnection).apply { @@ -245,7 +208,6 @@ object CoverArt { if (c.responseCode != 200) { c.disconnect(); null } else c.inputStream.bufferedReader().use { it.readText() } } catch (e: Exception) { Log.w(TAG, "$url: $e"); null } - /** Saves the image at [url] to [target] if it really is an image. */ private fun download(url: String, target: File): Boolean = try { val c = open(url) if (c.responseCode != 200) { c.disconnect(); false } else { diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/DependencyDetector.kt b/app/src/main/java/com/droiddeck/launcher/frontend/DependencyDetector.kt index 3b977bc4b..0acb02d37 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/DependencyDetector.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/DependencyDetector.kt @@ -3,49 +3,25 @@ package com.droiddeck.launcher.frontend import java.io.File import java.util.Locale -/** - * Best-effort detection of the Windows redistributables a game bundles, by scanning its install - * folder for the redist installers Steam/Epic/GOG ship (`_CommonRedist/`, `_Redist/`, - * `Redistributables/`, loose `vcredist_x64.exe`, …) and mapping them to entries in our component - * catalog (WinComponents, Bannerlator's components.json `name` keys). The result drives the - * Recommended part of a game's Windows components; the user still switches each one on. - * Bannerlator's detector, unchanged but for this note and its package. - * - * Wine-aware: keeps the redists that actually matter under Wine/DXVK (VC++ runtimes, .NET, PhysX, - * OpenAL, XACT, XNA, GFWL) and SKIPS the ones DXVK/VKD3D already provide (DX12 Agility SDK) or that - * aren't our components (anti-cheat, Rockstar/Social-Club/launcher setups). Never throws; returns an - * empty list on anything unexpected. Pure filesystem — no network, no Android deps (unit-testable). - */ object DependencyDetector { - /** How a recommendation was found — drives the UI's "recommended vs optional" split. */ enum class Kind { - /** The game bundles the redist installer (in _CommonRedist etc.) — meant to be installed. */ BUNDLED, - /** A runtime DLL the game ships loose — usually already works from the game folder; install - * only if the game misbehaves. */ SHIPPED, - /** Steam installs it with the game from Steamworks Shared ([SteamRedists]). */ STEAM, } - /** A recommended component: the catalog [componentName] to install, a display [label], the on-disk - * redist that triggered it ([reason]), and [kind] (bundled installer vs shipped DLL). */ data class Recommendation(val componentName: String, val label: String, val reason: String, val kind: Kind = Kind.BUNDLED) private const val MAX_DEPTH = 5 - private const val MAX_ENTRIES = 8000 // cost guard against giant game trees + private const val MAX_ENTRIES = 8000 - /** A loose VC++ installer with no year in its path — resolved to "latest" only if no specific - * vcredistYYYY was found anywhere (else it's just a duplicate of the versioned one). */ private const val VCREDIST_LOOSE = "__vcredist_loose" - /** Directory names that are redist containers worth descending into. */ private val REDIST_DIR_RE = Regex( """(?i)^(_?commonredist|_?redist(ributables)?|__?installer|vcredist|vc_redist|directx|direct_x|physx|openal|dotnet(fx)?|\.net|xna|xact|redist|support)$""" ) - /** Skip whole subtrees that are never our components. */ private val SKIP_DIR_RE = Regex("""(?i)^(d3d12.*|.*agility.*|dxvk|vkd3d|battleye|easyanticheat|eac|anticheat)$""") private val YEAR_TO_VCREDIST = mapOf( @@ -70,7 +46,6 @@ object DependencyDetector { "dotnetcoredesktop8" to ".NET Desktop 8", ) - /** Preferred install order (runtimes others may depend on first). */ private val INSTALL_ORDER = listOf( "vcredist2005", "vcredist2008", "vcredist2010", "vcredist2012", "vcredist2013", "vcredist2015", "vcredist2019", "vcredist2022", @@ -79,13 +54,6 @@ object DependencyDetector { "d3dx9", "d3dx11", "xact", "xact_x64", "xna31", "xna40", "physx", "oalinst", "XLiveRedist", ) - /** - * Detect from the .exe the user picked, resolving the game root first: walk up from the exe's - * folder (bounded) to the nearest ancestor that holds a redist container (`_CommonRedist`, …) at - * its top level, and scan from there — so a nested exe (`…/Game/bin/x.exe`) still finds a root-level - * `_CommonRedist`, without over-reaching into a shared `steamapps/common` (which has no redist - * container of its own). Falls back to scanning the exe's own folder. - */ fun detectForExe(exeFile: File): List<Recommendation> { val start = exeFile.absoluteFile.parentFile ?: return emptyList() var d: File? = start @@ -101,19 +69,14 @@ object DependencyDetector { return detect(start) } - /** - * Scan [gameDir] and return the recommended components, de-duplicated (first reason wins), in a - * stable install-friendly order. Empty when nothing is detected or [gameDir] isn't a directory. - */ fun detect(gameDir: File): List<Recommendation> { if (!gameDir.isDirectory) return emptyList() - val hits = LinkedHashMap<String, Recommendation>() // componentName -> first recommendation + val hits = LinkedHashMap<String, Recommendation>() var visited = 0 fun consider(f: File, isDir: Boolean, insideRedist: Boolean) { val name = f.name.lowercase(Locale.US) val path = f.path.replace('\\', '/').lowercase(Locale.US) - // Bundled = in a redist container or an installer (exe/msi/cab); shipped = a loose runtime DLL. val kind = if (insideRedist || isDir || name.endsWith(".exe") || name.endsWith(".msi") || name.endsWith(".cab")) Kind.BUNDLED else Kind.SHIPPED for (comp in classify(path, name, isDir)) { @@ -121,7 +84,7 @@ object DependencyDetector { if (existing == null) { hits[comp] = Recommendation(comp, LABELS[comp] ?: comp, relativeReason(gameDir, f), kind) } else if (existing.kind == Kind.SHIPPED && kind == Kind.BUNDLED) { - hits[comp] = existing.copy(kind = Kind.BUNDLED) // a bundled installer outranks a shipped DLL + hits[comp] = existing.copy(kind = Kind.BUNDLED) } } } @@ -134,17 +97,11 @@ object DependencyDetector { val lower = f.name.lowercase(Locale.US) if (f.isDirectory) { if (SKIP_DIR_RE.matches(lower)) continue - // Unity's <Game>_Data/Managed holds the game's own Mono assemblies, not a .NET - // Framework to install. if (lower == "managed" && dir.name.endsWith("_Data", ignoreCase = true)) continue val isRedistDir = insideRedist || REDIST_DIR_RE.matches(lower) if (isRedistDir) consider(f, isDir = true, insideRedist = true) - // Aggressive coverage: descend redist containers fully, and otherwise sweep the game - // tree a few levels deep so runtime libraries the game ships (in bin/, etc.) are seen. if (isRedistDir || depth < 3) walk(f, depth + 1, isRedistDir) } else { - // Classify installers + runtime libraries (.dll/.exe/.msi) anywhere in the scanned - // tree; other files are ignored. Recommendations are advisory, so cast a wide net. val ext = lower.substringAfterLast('.', "") if (insideRedist || ext == "dll" || ext == "exe" || ext == "msi") consider(f, isDir = false, insideRedist = insideRedist) } @@ -160,20 +117,13 @@ object DependencyDetector { } } - /** - * The catalog components one installer stands for, by its path ("_CommonRedist/vcredist/2015/ - * vc_redist.x64.exe", "DXSETUP.exe"): what a store's install list names. A VC++ installer - * with no year is the latest; empty for one that is none of ours. - */ fun componentsFor(path: String): List<String> { val p = path.replace('\\', '/').lowercase(Locale.US) return classify(p, p.substringAfterLast('/'), isDir = false).map { if (it == VCREDIST_LOOSE) "vcredist2022" else it } } - /** The name the catalog component [name] goes by, for a reason line. */ fun label(name: String): String = LABELS[name] ?: name - /** A loose VC++ installer resolves to the latest 14.x only if no specific vcredistYYYY was found. */ private fun resolveLooseVcredist(hits: LinkedHashMap<String, Recommendation>) { val loose = hits.remove(VCREDIST_LOOSE) ?: return val hasSpecific = hits.keys.any { it in YEAR_TO_VCREDIST.values } @@ -186,52 +136,38 @@ object DependencyDetector { return if (p.startsWith("$g/")) p.substring(g.length + 1) else f.name } - /** - * Map a redist file/dir (its full lowercased [path], leaf [name], and whether it [isDir]) to - * catalog component name(s). Empty for things we deliberately skip. - */ private fun classify(path: String, name: String, isDir: Boolean): List<String> { - // Hard skips first — DX12 is provided by DXVK/VKD3D; these aren't our components. if (path.contains("d3d12") || path.contains("agility") || path.contains("battleye") || path.contains("anticheat") || path.contains("easyanticheat") || name.contains("socialclub") || name.contains("social-club") || name.contains("rockstar") || name == "setup.exe" ) return emptyList() - // Visual C++ runtimes — most important. Year comes from the path context. if (path.contains("vcredist") || path.contains("vc_redist") || name.startsWith("vcredist") || name.startsWith("vc_redist")) { val year = YEAR_TO_VCREDIST.keys.firstOrNull { path.contains(it) } return when { year != null -> listOf(YEAR_TO_VCREDIST.getValue(year)) - isDir -> emptyList() // bare "vcredist" container — wait for the year subfolder / installer - else -> listOf(VCREDIST_LOOSE) // loose installer, no year → resolved post-walk + isDir -> emptyList() + else -> listOf(VCREDIST_LOOSE) } } - // VC++ runtime DLL the game ships (msvcp140.dll → 2015-2022, msvcp120.dll → 2013, …). VCRUNTIME_DLL.entries.firstOrNull { name.startsWith(it.key) }?.let { return listOf(it.value) } if (path.contains("openal") || name.startsWith("oalinst") || name == "openal32.dll" || name == "wrap_oal.dll") return listOf("oalinst") if (path.contains("physx") || name.startsWith("physx")) return listOf("physx") if (path.contains("xact") || name.startsWith("xactengine") || name.startsWith("x3daudio")) return listOf("xact", "xact_x64") - // Games for Windows Live — the redist folder, OR a GFWL library the game ships (xlive.dll, - // Codemasters' dbxLive32/64, gfwl*): GFWL titles need XLiveRedist to run even when they don't - // bundle the redist itself. if (path.contains("xliveredist") || name.contains("xlive") || name.startsWith("gfwl") || name.startsWith("dbxlive")) { return listOf("XLiveRedist") } if (path.contains("xna")) return listOf(if (path.contains("3.1") || path.contains("3_1") || path.contains("v31")) "xna31" else "xna40") - // .NET — Framework and Core/Desktop runtimes. if (path.contains("dotnet") || path.contains(".net") || name.startsWith("ndp") || name.startsWith("dotnetfx") || name.contains("windowsdesktop-runtime")) { val comp = dotnetComponent("$path $name") return when { comp != null -> listOf(comp) - isDir -> emptyList() // bare ".NET" container — wait for the versioned installer - else -> listOf("dotnet48") // unlabeled .NET Framework installer file → sensible default + isDir -> emptyList() + else -> listOf("dotnet48") } } - // DirectX 11 helper DLLs the game ships (d3dx11_43.dll). if (D3DX11_RE.matches(name)) return listOf("d3dx11") - // DirectX 9 helper DLLs / d3dcompiler / June-2010 DX redist (DXVK provides core d3d9 but NOT - // these helper DLLs). if (name.startsWith("dxsetup") || name.startsWith("dxwebsetup") || D3DX9_RE.matches(name) || D3DCOMPILER_RE.matches(name) || (path.contains("directx") && name.endsWith(".exe")) @@ -239,7 +175,6 @@ object DependencyDetector { return emptyList() } - /** Runtime DLL prefix → the VC++ redistributable that provides it (checked with startsWith). */ private val VCRUNTIME_DLL = linkedMapOf( "vcruntime140" to "vcredist2022", "msvcp140" to "vcredist2022", "msvcr140" to "vcredist2022", "concrt140" to "vcredist2022", "msvcp120" to "vcredist2013", "msvcr120" to "vcredist2013", @@ -252,7 +187,6 @@ object DependencyDetector { private val D3DX11_RE = Regex("""d3dx11_\d+\.dll""") private val D3DCOMPILER_RE = Regex("""d3dcompiler_\d+\.dll""") - /** Resolve a .NET component from installer/path text, or null when no version is discernible. */ private fun dotnetComponent(s: String): String? = when { s.contains("desktop-runtime-8") || Regex("""\b8\.\d""").containsMatchIn(s) -> "dotnetcoredesktop8" s.contains("desktop-runtime-7") || Regex("""\b7\.\d""").containsMatchIn(s) -> "dotnetcoredesktop7" @@ -264,6 +198,6 @@ object DependencyDetector { s.contains("dotnetfx45") || s.contains("ndp45") || s.contains("4.5") -> "dotnet45" s.contains("dotnetfx40") || s.contains("ndp40") || s.contains("4.0") -> "dotnet40" s.contains("dotnetfx35") || s.contains("3.5") -> "dotnet35" - else -> null // no version signal (e.g. a bare "DotNet" container dir) + else -> null } } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameExePicker.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameExePicker.kt index 1ceadbfa4..f692c7523 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameExePicker.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameExePicker.kt @@ -2,26 +2,15 @@ package com.droiddeck.launcher.frontend import java.io.File -/** - * Which `.exe` in a game folder is the game. A real install holds installers, crash handlers, - * anti-cheat stubs and launchers beside the game, and the game itself may sit several levels down - * (`Game/Binaries/Win64/Game-Win64-Shipping.exe`), so candidates are collected with a bounded walk - * and ranked rather than guessed by position. - */ object GameExePicker { - /** How far below a game folder to look: Unreal puts the game three down, repacks deeper. */ const val MAX_DEPTH = 5 - /** Folders and .exe files looked at per game before the walk stops, so a huge game stays quick. */ const val WALK_BUDGET = 400 - /** Below this the pick is marked uncertain rather than trusted silently. */ const val CONFIDENT_SCORE = 60 - /** The best and the runner-up this close together: nothing clearly won. */ const val AMBIGUOUS_MARGIN = 15 - /** Folders that never hold the game itself. */ private val SKIP_DIRS = setOf( "_commonredist", "commonredist", "redist", "redistributable", "redistributables", "directx", "dotnet", "dotnetfx", "vcredist", "vc_redist", "prerequisites", "prereq", @@ -32,7 +21,6 @@ object GameExePicker { private val SKIP_DIR_PREFIX = listOf("dotnet", "vcredist", "$") private val SKIP_PATHS = listOf("engine/extras", "engine/binaries/thirdparty") - /** Exe names that are never the game. */ private val JUNK_EXE_RE = Regex( """(?i)^(unins\w*|setup|install\w*|vc_?redist.*|vcredist.*|dxsetup|dxwebsetup|directx.*|""" + """oalinst|openal.*|dotnetfx.*|ndp\d.*|unitycrashhandler\d*|crashreport\w*|crashpad\w*|""" + @@ -41,10 +29,8 @@ object GameExePicker { """activation\w*|register\w*|readme|report\w*|.*_debug|.*-debug)$""", ) - /** A folder's exes best first, with their scores, and whether the first clearly won. */ class Ranking(val exes: List<File>, val scores: List<Int>, val uncertain: Boolean) - /** Ranks the .exe files under [folder]; [keep] drops names the caller never offers. */ fun rank(folder: File, keep: (File) -> Boolean = { true }): Ranking { val scored = collect(folder).filter { keep(it.first) }.map { (exe, depth) -> exe to score(exe, folder, depth) } .sortedWith(compareByDescending<Pair<File, Int>> { it.second }.thenBy { it.first.path.lowercase() }) @@ -54,11 +40,6 @@ object GameExePicker { return Ranking(scored.map { it.first }, scored.map { it.second }, uncertain) } - /** - * Every plausible exe under [folder] with its depth (0 = in the folder itself): up to - * [MAX_DEPTH] folders down, nearest levels first, at most [WALK_BUDGET] entries, never through a - * symlink, past folders that never hold the game. - */ private fun collect(folder: File): List<Pair<File, Int>> { val found = ArrayList<Pair<File, Int>>() var visited = 0 @@ -86,31 +67,23 @@ object GameExePicker { return found } - /** - * How likely [exe] is to be the game in [folder]. Relative only: the numbers rank candidates - * against each other and decide whether anything won clearly enough to trust. - */ internal fun score(exe: File, folder: File, depth: Int): Int { var score = 0 val base = exe.nameWithoutExtension val exeKey = key(base) val folderKey = key(folder.name) - // The exe named after its folder is the single strongest signal. if (exeKey.isNotEmpty() && folderKey.isNotEmpty()) { when { exeKey == folderKey -> score += 100 folderKey.contains(exeKey) || exeKey.contains(folderKey) -> score += 60 } } - // Unreal ships as <Game>-Win64-Shipping.exe under Binaries/Win64. if (base.endsWith("-Win64-Shipping", true) || base.endsWith("-Win32-Shipping", true)) score += 70 val path = exe.absolutePath.lowercase() if (path.contains("/binaries/win64") || path.contains("/binaries/win32")) score += 30 if (path.contains("/bin/") || path.contains("/bin64/")) score += 10 - // A launcher is a real entry point, so it stays a candidate, just not the preferred one. if (GameIdentifier.isLauncherExeName(base)) score -= 40 if (GameIdentifier.isJunkPeName(base)) score -= 30 - // Shallower is more likely the entry point; big binaries beat small helpers. score -= depth * 8 score += (exe.length() / (8L * 1024 * 1024)).toInt().coerceAtMost(30) return score @@ -125,6 +98,5 @@ object GameExePicker { private fun isLink(f: File): Boolean = runCatching { java.nio.file.Files.isSymbolicLink(f.toPath()) }.getOrDefault(false) - /** Comparison key: letters and digits only, so "GTA IV" and "gta-iv" match. */ private fun key(s: String) = s.lowercase().filter { it.isLetterOrDigit() } } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameFileSync.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameFileSync.kt index 3bef04f5a..0196797cf 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameFileSync.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameFileSync.kt @@ -10,7 +10,6 @@ import java.io.File import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -/** Runs while DroidDeck's process is alive, including downloads in a background Steam session. */ object GameFileSync { private val worker = Executors.newSingleThreadScheduledExecutor { task -> Thread(task, "game-file-sync").apply { isDaemon = true } } private fun prefs(context: Context) = context.getSharedPreferences("game_files", Context.MODE_PRIVATE) @@ -39,7 +38,6 @@ object GameFileSync { @Synchronized private fun sync(context: Context, folder: File) { check(LinuxRuntime.isInstalled(context)) { context.getString(R.string.game_sync_err_runtime) } - // A disconnected library must not be interpreted as a mass uninstall. check(File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps").isDirectory) { context.getString(R.string.game_sync_err_steam_library) } val storage = GameStorage.effective(context) val storageSetting = SessionPrefs.gameStorage(context) diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameFiles.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameFiles.kt index 6b39093c0..8e0b55def 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameFiles.kt @@ -11,14 +11,12 @@ import org.json.JSONObject import java.io.File import java.io.InputStream -/** Small, portable launch files for frontends that scan ROM folders. No guest paths or commands. */ object GameFiles { const val EXTENSION = "steam" private const val MAX_BYTES = 128 private const val INVENTORY = ".droiddeck-sync.json" private const val FORMAT = "droiddeck-game-files-v1" - /** Called on a worker: content providers and shared storage may block. */ fun readIntent(context: Context, request: Intent): String? = runCatching { if (request.action != Intent.ACTION_VIEW) return null val uri = request.data ?: return null @@ -36,7 +34,6 @@ object GameFiles { }.getOrNull() fun read(stream: InputStream): String? { - // An untrusted URI must not make us buffer an arbitrary ROM or parse a command. val bytes = ByteArray(MAX_BYTES + 1) var count = 0 while (count < bytes.size) { @@ -84,7 +81,6 @@ object GameFiles { } else target } - /** Cocoon matches icons in its data folder's images directory. Never replace custom art. */ private fun exportArt(context: Context, folder: File, game: Library.SteamGame) { val art = listOfNotNull(game.icon, game.art).firstOrNull { it.isFile && it.extension.lowercase() in listOf("jpg", "jpeg", "png", "webp") @@ -100,7 +96,6 @@ object GameFiles { if (!target.exists()) AtomicFile(target).writeBytes(art.readBytes()) } - /** Only remove files in our inventory whose content is still exactly what we wrote. */ @Synchronized fun sync(context: Context, folder: File, games: List<Library.SteamGame>) { require(folder.isDirectory) { context.getString(R.string.gfiles_folder_unavailable) } val inventory = child(context, folder, INVENTORY) @@ -119,21 +114,17 @@ object GameFiles { else exportTarget(context, folder, game, name) val text = contents(game.gameIdString) val existed = target.exists() - // Keep an edited export without letting it block the rest of the library sync. val owned = inventoryOwns(context, folder, previous, target, game.gameIdString) val file = if (owned && target.exists() && !matches(target, text)) target else exportNamed(context, folder, game, target.name) if (!existed || owned) next.put(file.name, game.gameIdString) file } - // Write new exports before pruning; a failed write must not remove old launch files. for (name in previous.keys()) { val id = previous.getString(name) if (!next.has(name) && name.endsWith(".$EXTENSION") && GameLaunchLink.validId(id)) { val file = child(context, folder, name) if (matches(file, contents(id))) { - // Android shared storage is often case insensitive. A case-only rename may - // still resolve to the live export, which must not be pruned as an old file. val retained = exports.any { java.nio.file.Files.isSameFile(it.toPath(), file.toPath()) } if (!retained) check(file.delete()) { context.getString(R.string.gfiles_remove_failed, name) } } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameIdentifier.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameIdentifier.kt index d3b88b2a9..4b385d896 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameIdentifier.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameIdentifier.kt @@ -2,65 +2,35 @@ package com.droiddeck.launcher.frontend import java.io.File -/** - * Best-effort identification of a game from its on-disk footprint, given its .exe: a Steam appId - * and/or a display name, so an added game is not just named after its folder or exe. Pure - * filesystem and PE parsing, no network, never throws; it reports only what the disk tells. - * - * Signal chain (appId preferred; best name chosen across all hits): - * 1. steam_appid.txt → appId (Goldberg-style installs) - * 2. ColdClientLoader / emu ini → appId (AppId= key) - * 3. appmanifest_<id>.acf → appId + name (real Steam install under steamapps/) - * 4. goggame-<id>.info → name (+ gogId) (GOG installs) - * 5. PE VS_VERSIONINFO → name (ProductName / FileDescription) - * 6. cleaned folder / exe name → name (weak fallback) - */ object GameIdentifier { - /** How far up the directory tree to look for marker files (games nest deep, e.g. Game/Binaries/Win64/x.exe). */ private const val MAX_PARENT_DEPTH = 6 enum class Source { STEAM_APPID_TXT, STEAM_MANIFEST_ACF, STEAM_EMU_INI, GOG_INFO, PE_VERSION, FILENAME, NONE } enum class Confidence { HIGH, MEDIUM, LOW } - /** - * @param appId Steam appId if one was found on disk, else null. - * @param gogId GOG product id if a goggame info file was found, else null. - * @param name Best display name found locally; may be null when only an appId was - * found (caller resolves the name from the appId over the network). - * @param source Which signal produced the primary identity. - */ data class GameIdentity( val appId: Int? = null, val gogId: String? = null, val name: String? = null, val source: Source = Source.NONE, val confidence: Confidence = Confidence.LOW, - /** Which signal gave [name]: [Source.FILENAME] means only a cleaned folder or exe name. */ val nameSource: Source = Source.NONE, ) { val hasStrongId: Boolean get() = appId != null } - /** Identify the game owning [exeFile]. Never throws; returns an empty identity if nothing matched. */ fun identify(exeFile: File): GameIdentity { val dirs = ancestorDirs(exeFile) - // Steam appId from a bare steam_appid.txt (in the exe dir, a steam_settings/ subdir, or an ancestor). val steamAppId = dirs.firstNotNullOfOrNull { readSteamAppIdTxt(it) } - // AppId= out of a ColdClientLoader.ini / steam_emu.ini next to the game. val emuAppId = dirs.firstNotNullOfOrNull { readEmuIniAppId(it) } - // Real Steam install: appmanifest_<id>.acf up under steamapps/ — carries both appId and name. val acf = resolveSteamManifest(exeFile, dirs) - // GOG: goggame-<id>.info JSON with the real title. val gog = dirs.firstNotNullOfOrNull { readGogInfo(it) } - // PE ProductName / FileDescription — but not when it merely names a launcher/loader - // ("Rockstar Games Launcher Redirector", "GSE", …), which must not become the game name. val peName = PeVersionInfo.bestName(PeVersionInfo.read(exeFile))?.takeUnless { isJunkPeName(it) } val appId = acf?.appId ?: steamAppId ?: emuAppId - // Best name, most-trustworthy first. val nameHit: Pair<String, Source>? = when { acf?.name != null -> acf.name to Source.STEAM_MANIFEST_ACF gog?.name != null -> gog.name to Source.GOG_INFO @@ -68,7 +38,6 @@ object GameIdentifier { else -> cleanedFallbackName(exeFile)?.let { it to Source.FILENAME } } - // Primary source = whatever gave us the appId (strongest), else the name source. val primary: Source = when { acf?.appId != null -> Source.STEAM_MANIFEST_ACF steamAppId != null -> Source.STEAM_APPID_TXT @@ -93,27 +62,19 @@ object GameIdentifier { ) } - /** - * Make an identified title readable and filesystem-safe (a Steam shortcut's name also names - * files): "DARK SOULS: REMASTERED" becomes "DARK SOULS - REMASTERED". Strips trademark marks, - * turns title punctuation into safe equivalents, and collapses whitespace. - */ fun normalizeName(raw: String): String { var s = raw s = s.replace("™", "").replace("®", "").replace("©", "") - s = s.replace(Regex("""(?i)\(\s*(tm|r|c)\s*\)"""), "") // "(TM)" / "(R)" / "(C)" - s = s.replace(":", " - ") // colon → dash separator (safe + readable) - s = s.replace(Regex("""[\\/]"""), " ") // path separators → space - s = s.replace(Regex("""["*?<>|]"""), "") // other illegal chars → drop - // Drop trailing generic descriptors a PE FileDescription tacks on ("DiRT 3 Executable" → "DiRT 3"). + s = s.replace(Regex("""(?i)\(\s*(tm|r|c)\s*\)"""), "") + s = s.replace(":", " - ") + s = s.replace(Regex("""[\\/]"""), " ") + s = s.replace(Regex("""["*?<>|]"""), "") s = s.replace(Regex("""(?i)[ \-]+(executable|application|launcher|redistributable|installer|setup)$"""), "") - s = s.replace(Regex("""\s+"""), " ").trim() // collapse whitespace - return s.trim(' ', '-').trim() // tidy stray leading/trailing dashes + s = s.replace(Regex("""\s+"""), " ").trim() + return s.trim(' ', '-').trim() } - // ── directory walk ── - /** The exe's directory plus its ancestors, nearest-first, bounded by [MAX_PARENT_DEPTH]. */ private fun ancestorDirs(exeFile: File): List<File> { val out = ArrayList<File>(MAX_PARENT_DEPTH + 1) var d: File? = exeFile.absoluteFile.parentFile @@ -126,12 +87,9 @@ object GameIdentifier { return out } - // ── signal readers (all best-effort; return null on anything unexpected) ── - /** steam_appid.txt directly in [dir] or in a steam_settings/ subdir. */ private fun readSteamAppIdTxt(dir: File): Int? { for (candidate in arrayOf(File(dir, "steam_appid.txt"), File(dir, "steam_settings/steam_appid.txt"))) { - // Strip a leading UTF-8 BOM (trim() doesn't) before reading the digits. val id = readSmallTextFile(candidate)?.removePrefix("\uFEFF")?.trim() ?.takeWhile { it.isDigit() }?.toIntOrNull() if (id != null && id > 0) return id @@ -139,12 +97,6 @@ object GameIdentifier { return null } - /** - * First `AppId=<n>` in a crack/emu config .ini next to the game (ColdClientLoader, steam_emu, - * flt, hlm, …). Known names are tried first, then any other `.ini` in the dir — only crack - * configs carry an `AppId=` key, game settings .ini files (settings.ini, graphics.ini) don't, - * so the broad scan is safe. - */ private fun readEmuIniAppId(dir: File): Int? { val re = Regex("""(?im)^\s*appid\s*=\s*(\d+)""") val named = arrayOf( @@ -166,13 +118,6 @@ object GameIdentifier { private data class Acf(val appId: Int?, val name: String?) - /** - * Resolve the Steam appmanifest for this game: find the `common` ancestor on the exe's - * path, read the sibling `steamapps/appmanifest_*.acf`, and match by installdir == the - * game folder under common. No match, no manifest: a folder in steamapps/common that no - * manifest claims is exactly the kind added games list, and the library's one other game is - * not its name. - */ private fun resolveSteamManifest(exeFile: File, dirs: List<File>): Acf? { val commonIdx = dirs.indexOfFirst { it.name.equals("common", ignoreCase = true) } if (commonIdx < 0) return null @@ -183,7 +128,6 @@ object GameIdentifier { } ?: return null if (manifests.isEmpty()) return null - // The game's install folder is the child of `common` on our descent path. val gameFolder = if (commonIdx > 0) dirs[commonIdx - 1].name else null val match = manifests.firstOrNull { m -> @@ -200,7 +144,6 @@ object GameIdentifier { private data class Gog(val id: String?, val name: String?) - /** goggame-<id>.info JSON: pull `name` (and the id from the filename). */ private fun readGogInfo(dir: File): Gog? { val files = dir.listFiles { f -> f.isFile && f.name.startsWith("goggame-") && f.name.endsWith(".info") @@ -214,75 +157,42 @@ object GameIdentifier { return null } - // ── filename fallback ── private val REPACK_TOKENS = setOf( "fitgirl", "dodi", "codex", "plaza", "skidrow", "rune", "empress", "flt", "tenoke", "reloaded", "elamigos", "razor1911", "hoodlum", "gog", "repack", "multi", "goldberg", - // repack/site tags that show up in folder names "ankergames", "steamrip", "onlinefix", "online-fix", "gload", "fitgirlrepacks", ) private const val EXE_NOISE_WORDS = """win64|win32|shipping|wingdk|client|launcher|game|x64|x86|64bit|32bit""" - /** - * Does this exe basename look like engine/arch boilerplate rather than a title? Deliberately - * UNANCHORED — "GameConTest" is boilerplate-ish wherever the giveaway sits, and the answer - * decides whether we trust the folder name instead of the exe name. - */ private val EXE_NOISE_RE = Regex("""(?i)[-_. ]*(?:$EXE_NOISE_WORDS)""") - /** - * Trailing noise to strip off a title. ANCHORED to the end and allowed to repeat, because these - * words are only junk as a suffix: "Witcher3-Win64-Shipping" -> "Witcher3", while - * "Game of Thrones" and "Client Simulator" must survive intact. - * - * Kept separate from [EXE_NOISE_RE] on purpose. One regex used to serve both jobs, so - * anchoring it to fix stripping ALSO narrowed the boilerplate test — and a game in - * "Game Controller Test/GameConTest.exe" stopped preferring its folder and became "GameConTest". - */ private val EXE_SUFFIX_RE = Regex("""(?i)(?:[-_. ]*(?:$EXE_NOISE_WORDS))+$""") private val VERSION_TOKEN_RE = Regex("""(?i)\b(v?\d+([._]\d+)+|build[-_ ]?\d+|update[-_ ]?\d+)\b""") - /** - * True when an exe basename is a launcher/loader rather than the game — its parent folder is the - * better title. Catches "PlayGTAV"/"Play - GTA IV"/"LaunchGame"/"MyLauncher"/"GSE" but NOT a real - * title that merely starts with those letters ("PlayerUnknown"). A "play|launch|start|run" prefix - * counts only when it's a whole token — followed by a separator or a camelCase uppercase boundary, - * not another lowercase letter (checked case-sensitively, so it survives the case-insensitive JUNK - * matching used elsewhere). - */ - /** True when an exe basename looks like a launcher/loader rather than the game itself. */ fun isLauncherExeName(base: String): Boolean { val lower = base.lowercase() if (lower == "gse" || lower.endsWith("launcher") || lower.endsWith("redirector")) return true for (p in arrayOf("play", "launch", "start", "run")) { if (!lower.startsWith(p)) continue val rest = base.substring(p.length) - if (rest.isEmpty()) return true // exactly "Play" + if (rest.isEmpty()) return true val c = rest[0] - if (!c.isLetterOrDigit() || c.isUpperCase()) return true // "Play - GTA IV" / camelCase "PlayGTAV" - // lowercase continuation ("Player…") → part of a longer word, not a launcher + if (!c.isLetterOrDigit() || c.isUpperCase()) return true } return false } - /** A PE ProductName/FileDescription that describes a launcher/loader, not the game — reject it so it - * doesn't become the shortcut name (e.g. "Rockstar Games Launcher Redirector", "GSE", "Steam"). */ private val JUNK_NAME_RE = Regex( """(?i)^(gse|steam|steam client|goldberg.*|launcher|game|play|start|setup|.* launcher.*|.*launcher redirector|.* redirector|rockstar games launcher.*|epic games launcher|smartsteamemu|coldclient.*|steamemu.*)$""" ) - /** True when a PE name is a launcher/loader label rather than a real game title. */ fun isJunkPeName(name: String): Boolean { val n = name.trim() return n.isEmpty() || JUNK_NAME_RE.matches(n) } - /** - * Prefer the game's install-folder name (usually the real title) over the exe basename when the - * exe is a UE-style shipping binary OR a launcher/loader; then strip repack/suffix noise. - */ private fun cleanedFallbackName(exeFile: File): String? { val exeBase = exeFile.nameWithoutExtension val preferFolder = EXE_NOISE_RE.containsMatchIn(exeBase) || @@ -293,14 +203,12 @@ object GameIdentifier { return cleanName(raw) } - /** Strip bracketed tags, repack-group names, engine/arch suffixes and version tokens; tidy separators. */ private fun cleanName(raw: String): String? { var s = raw - s = s.replace(Regex("""[\[(][^\])]*[\])]"""), " ") // (…) […] + s = s.replace(Regex("""[\[(][^\])]*[\])]"""), " ") s = VERSION_TOKEN_RE.replace(s, " ") s = EXE_SUFFIX_RE.replace(s, " ") s = s.replace(Regex("""[._]+"""), " ") - // Drop standalone repack-group words. s = s.split(Regex("""[\s-]+""")) .filter { it.isNotBlank() && it.lowercase() !in REPACK_TOKENS } .joinToString(" ") @@ -308,20 +216,16 @@ object GameIdentifier { return s.ifBlank { null } } - // ── tiny text/format helpers (regex-based → no Android/org.json dependency, unit-testable) ── - /** Read a small text file (guards against accidentally slurping a huge file). */ private fun readSmallTextFile(f: File, maxBytes: Long = 1L * 1024 * 1024): String? = try { if (f.isFile && f.length() in 1..maxBytes) f.readText(Charsets.UTF_8) else null } catch (_: Throwable) { null } - /** Value of a Valve KeyValues (.acf/.vdf) `"key" "value"` pair, first match, case-insensitive key. */ private fun vdfValue(text: String, key: String): String? = Regex(""""${Regex.escape(key)}"\s*"([^"]*)"""", RegexOption.IGNORE_CASE).find(text)?.groupValues?.getOrNull(1)?.trim() - /** Value of a JSON `"key": "value"` pair, first match. Sufficient for the flat goggame .info files. */ private fun jsonStringValue(text: String, key: String): String? = Regex(""""${Regex.escape(key)}"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"""").find(text) ?.groupValues?.getOrNull(1)?.replace("\\\"", "\"")?.replace("\\\\", "\\")?.trim() diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameLaunchLink.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameLaunchLink.kt index c19709250..27374023c 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameLaunchLink.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameLaunchLink.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.frontend -/** External callers can select a game, never a guest path or arbitrary Steam command. */ object GameLaunchLink { private val idPattern = Regex("[1-9][0-9]{0,19}") private val linkPattern = Regex("droiddeck://game/([1-9][0-9]{0,19})") diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt index 5cd221658..f6ea660da 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt @@ -42,7 +42,6 @@ object GameShortcuts { if (!requested) Toast.makeText(activity, R.string.game_shortcut_unsupported, Toast.LENGTH_LONG).show() } - /** Standard shortcut picker result, understood by launchers that import Android shortcuts. */ fun result(activity: Activity, game: Library.SteamGame): Intent = activity.getSystemService(ShortcutManager::class.java).createShortcutResultIntent(info(activity, game)) } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GamesFolderMigration.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GamesFolderMigration.kt index 56846c807..faf79c153 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GamesFolderMigration.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GamesFolderMigration.kt @@ -8,15 +8,6 @@ import com.droiddeck.launcher.stores.StoreInstallRoot import java.io.File import java.util.zip.CRC32 -/** - * Games folders (the roots earlier builds let the user add in Steam settings) become what the - * Games tab's "Add all games in this folder" makes: one entry per game ([AddedExes]). Everything - * a game had is keyed by its own folder and stays as it was - the chosen exe, the shortcut appid, - * the name, Start in, launch options, art choices, a removal. The roots stay bound into the - * session at the same guest paths ([SessionPrefs.gameFolderBinds]), so every exe keeps its path - * inside the session and every Steam shortcut its target; they are no longer Steam libraries. - * Runs whenever saved roots remain (so twice is the same as once) and clears them after. - */ object GamesFolderMigration { private const val TAG = "GamesFolderMigration" @@ -26,7 +17,6 @@ object GamesFolderMigration { if (dirs.isEmpty()) return val roots = legacyRoots(SessionPrefs.gameFolderBinds(context).map { it.first } + dirs) .filter { root -> root.host.path in dirs } - // The binds first: a game's guest path (and with it the shortcut's appid) depends on them. SessionPrefs.setGameFolderBinds(context, SessionPrefs.gameFolderBinds(context) + roots.map { it.host.path to it.guest }) val removed = SessionPrefs.removedAddedGames(context).toSet() val entries = AddedExes.list(context).map { it.folder }.toMutableSet() @@ -50,10 +40,6 @@ object GamesFolderMigration { SessionPrefs.setGamesFoldersMigrated(context) } - /** - * The guest paths earlier builds gave Games folders: /root/Games/<folder name>, with a short - * hash of the host path when two share a name. Kept exactly, so nothing moves in the session. - */ internal fun legacyRoots(paths: List<String>): List<AddedGames.Root> { val hosts = paths.distinct().map { File(it) } val names = hosts.groupingBy { it.name.lowercase() }.eachCount() diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt b/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt index f985b2e5b..1fd87f97d 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt @@ -8,42 +8,24 @@ import com.droiddeck.launcher.session.SessionPrefs import com.droiddeck.launcher.session.SecondaryLibrary import java.io.File -/** - * What the front end lists: the Steam client's installed games (from its own appmanifests, both - * libraries) and each emulator's games (files in the ROMs folder, by extension, with a system - * folder as a hint). Read on a worker thread; nothing here is cached beyond one screen refresh. - */ object Library { - /** [gameId] is what steam://rungameid/ takes: the appid for a Steam title, the shortcut id for an added game. */ - /** - * [art] is the portrait capsule; [hero] the wide banner Steam shows above a game's page, when - * the client has cached one. [lastPlayed] is Steam's own "LastPlayed" (unix seconds), 0 = never. - */ class SteamGame( val appId: Int, val name: String, val art: File?, val library: String, val gameId: Long = appId.toLong(), val hero: File? = null, val lastPlayed: Long = 0L, val gameFiles: File? = null, val protonPrefix: File? = null, val icon: File? = null, - /** Where the game came from: [SOURCE_STEAM], a store's id (gog, epic, amazon) or [ADDED] for a folder the user added. */ val source: String = SOURCE_STEAM, - /** The store's own id for a store install; null otherwise. */ val storeId: String? = null, ) { - /** Decimal form used by Steam links and Android shortcuts, including unsigned shortcut ids. */ val gameIdString: String get() = java.lang.Long.toUnsignedString(gameId) - /** Key used by per-game environment and Windows-component profiles. */ val profileKey: String get() = if (library == ADDED) java.lang.Integer.toUnsignedString(appId) else appId.toString() } - /** The [SteamGame.library] of a game added to the library rather than installed by Steam, and the [SteamGame.source] of a plain added folder. */ const val ADDED = "added" - /** The [SteamGame.source] of a title Steam itself installed. */ const val SOURCE_STEAM = "steam" class Rom(val name: String, val hostPath: File, val guestPath: String, val emulatorId: String, val art: File? = null) class Emulator(val id: String, val name: String, val system: String, val program: String, val installed: Boolean, val games: List<Rom>) { - /** The shown name of [system] when it is words rather than a platform name, else 0. */ val systemRes: Int get() = if (id == "retroarch") R.string.content_system_many else 0 - /** The emulator's own icon, bundled (the runtime keeps them as theme SVGs the app cannot draw). */ val iconRes: Int get() = when (id) { "rpcs3" -> R.drawable.emu_rpcs3; "armsx2" -> R.drawable.emu_pcsx2; "dolphin" -> R.drawable.emu_dolphin "duckstation" -> R.drawable.emu_duckstation; "melonds" -> R.drawable.emu_melonds; "cemu" -> R.drawable.emu_cemu @@ -51,34 +33,33 @@ object Library { } } - /** The client's own tools and runtimes live in steamapps beside the games; they are not titles. */ private val NOT_GAMES = setOf( - 858280, // Proton 3.7 - 961940, // Proton 3.16 - 993090, // Lossless Scaling - 1054830, // Proton 4.2 - 1070560, // Steam Linux Runtime 1.0 - 1113280, // Proton 4.11 - 1245040, // Proton 5.0 - 1391110, // Steam Linux Runtime 2.0 - 1420170, // Proton 5.13 - 1493710, // Proton Experimental - 1580130, // Proton 6.3 - 1628350, // Steam Linux Runtime 3.0 - 1887720, // Proton 7 - 2180100, // Proton Hotfix - 228980, // Steamworks Common Redistributables - 2348590, // Proton 8 - 2805730, // Proton 9 - 3029110, // Lepton - 3127680, // FEX - 3658110, // Proton 10 - 4183110, // Steam Linux Runtime 4.0 - 4185400, // Steam Linux Runtime 4.0 for ARM64 - 4427310, // Proton Experimental for ARM64 - 4628710, // Proton 11 / Proton Next - 4628740, // Proton 11 for ARM64 - 4690330, // Legacy Steam Runtime + 858280, + 961940, + 993090, + 1054830, + 1070560, + 1113280, + 1245040, + 1391110, + 1420170, + 1493710, + 1580130, + 1628350, + 1887720, + 2180100, + 228980, + 2348590, + 2805730, + 3029110, + 3127680, + 3658110, + 4183110, + 4185400, + 4427310, + 4628710, + 4628740, + 4690330, ) private val STEAM_CAPSULES = listOf("library_capsule.jpg", "library_600x900.jpg") private val STEAM_HEROES = listOf("library_hero.jpg") @@ -88,7 +69,6 @@ object Library { private val LAST_PLAYED = Regex("^\\s*\"LastPlayed\"\\s*\"(\\d+)\"", RegexOption.MULTILINE) private val INSTALL_DIR = Regex("^\\s*\"installdir\"\\s*\"([^\"]*)\"", RegexOption.MULTILINE) - /** Steam's library roots visible to this launcher: its private default plus the selected library. */ private fun steamLibraries(context: Context): List<Pair<File, String>> { val root = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam") return listOfNotNull( @@ -97,7 +77,6 @@ object Library { ) + GameStorage.gamesFolderLibraries(context).map { it.host to it.label } } - /** Proton keeps each game's prefix below compatdata/<appid>/pfx in a Steam library. */ fun protonPrefix( context: Context, appId: Long, preferredLibrary: File? = null, libraries: List<File> = steamLibraries(context).map { it.first }, @@ -130,14 +109,10 @@ object Library { val text = try { manifest.readText() } catch (e: Exception) { if (strictRead) throw e; return@forEach } val name = NAME.find(text)?.groupValues?.get(1)?.trim().orEmpty() val flags = STATE.find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0 - // StateFlags 4 = fully installed; anything else is downloading, updating or broken. if (name.isEmpty() || flags and 4 == 0) return@forEach val art = steamCacheImage(cache, appId, STEAM_CAPSULES) val hero = steamCacheImage(cache, appId, STEAM_HEROES) val lastPlayed = LAST_PLAYED.find(text)?.groupValues?.get(1)?.toLongOrNull() ?: 0L - // App manifests are Valve KeyValues (VDF) files. installdir is one folder - // below steamapps/common; only expose it when the directory exists and the - // manifest value cannot escape that directory. val installDir = INSTALL_DIR.find(text)?.groupValues?.get(1)?.trim() ?.takeIf { it.isNotEmpty() && it != "." && it != ".." && '/' !in it && '\\' !in it } val gameFiles = installDir?.let { File(steamapps, "common/$it").takeIf(File::isDirectory) } @@ -153,14 +128,11 @@ object Library { return out.values.toList() } - /** The same installed-game inventory used for links, shortcuts and file exports. */ - /** [added]: the added games when the caller has just scanned them, so they are not scanned again. */ fun launchableGames(context: Context, strictRead: Boolean = false, added: List<AddedGames.Game>? = null): List<SteamGame> { val roots = steamLibraries(context).map { it.first } return (steamGames(context, strictRead) + (added ?: AddedGames.scan(context)).map { g -> addedGame(context, g, roots) }).distinctBy { it.gameId } } - /** One added game as the Games tab lists it; [roots] the Steam libraries, for its Proton prefix. */ fun addedGame(context: Context, g: AddedGames.Game, roots: List<File> = steamLibraries(context).map { it.first }): SteamGame = AddedGameArt.resolve(context, g).let { art -> SteamGame( @@ -172,7 +144,6 @@ object Library { ) } - /** Steam stores current library art inside hash-named folders under the app's cache dir. */ private fun steamCacheImage(cache: File, appId: Int, names: List<String>): File? { val appDir = File(cache, appId.toString()) val dirs = listOf(appDir) + appDir.listFiles() @@ -182,12 +153,9 @@ object Library { .firstOrNull { it.isFile && it.length() > 0L } } - /** [atPanel]: frames cheap enough to draw at the panel's own size (see [drawsAtPanel]). */ private class Spec(val id: String, val name: String, val system: String, val program: String, val folders: List<String>, val exts: Set<String>, val atPanel: Boolean = false) private val specs = listOf( Spec("rpcs3", "RPCS3", "PS3", "/opt/appimages/rpcs3.AppImage", listOf("ps3"), setOf("iso")), - // PS2: ARMSX2, the PCSX2 fork with ARM64 recompilers. Upstream PCSX2 interprets the PS2's - // CPUs on ARM64 (NFS Underground 2: 17 fps against ARMSX2's full 60), so it is not offered. Spec("armsx2", "ARMSX2", "PS2", "/opt/appimages/armsx2.AppImage", listOf("ps2"), setOf("iso", "chd", "cso", "gz")), Spec("dolphin", "Dolphin", "GameCube / Wii", "/opt/appimages/dolphin.AppImage", listOf("gc", "gamecube", "wii"), setOf("iso", "rvz", "gcz", "wbfs", "ciso")), Spec("duckstation", "DuckStation", "PS1", "/opt/appimages/duckstation.AppImage", listOf("ps1", "psx"), setOf("cue", "chd", "pbp", "iso", "bin", "img", "ecm", "m3u")), @@ -196,73 +164,46 @@ object Library { Spec("ppsspp", "PPSSPP", "PSP", "/usr/bin/PPSSPPSDL", listOf("psp"), setOf("iso", "cso", "pbp", "chd")), Spec("retroarch", "RetroArch", "many systems", "/usr/bin/retroarch", emptyList(), emptySet()), ) - /** - * A dump's file name as a title: its tags - (USA), (En,Fr,Es,Pt), [!], (v1.01) - identify the - * file and are dropped for reading ("Tomb Raider (USA) (En,Fr,Es,Pt)" -> "Tomb Raider"). The - * file keeps them, and the cover lookup still uses them. - */ private fun displayTitle(name: String): String = name.replace(Regex("\\s*[(\\[][^)\\]]*[)\\]]"), "").trim().ifEmpty { name } - /** Every system folder name the specs claim, so a loose-file scan of the root skips them. */ private val systemFolders: Set<String> by lazy { specs.flatMap { it.folders }.toSet() } private val installedIds = mapOf( "rpcs3" to "rpcs3", "armsx2" to "armsx2", "dolphin" to "dolphin", "duckstation" to "duckstation", "melonds" to "melonds", "cemu" to "cemu", "ppsspp" to "emulators", "retroarch" to "emulators", ) - /** The emulator's name for a program path from the rail ("ARMSX2"), or null. */ fun nameForProgram(program: String?): String? = if (program == com.droiddeck.launcher.runtime.FlatpakManager.LAUNCHER || program == com.droiddeck.launcher.runtime.AppImageManager.LAUNCHER || program == com.droiddeck.launcher.runtime.UserApps.SCRIPT_LAUNCHER) { com.droiddeck.launcher.session.SessionState.programArgs.firstOrNull()?.let { flatpakNames[it] ?: it.substringAfterLast('.') } } else specs.firstOrNull { it.program == program }?.name - /** Added apps' names by Flatpak id or directory, as launched: the session only knows that. */ val flatpakNames = java.util.concurrent.ConcurrentHashMap<String, String>() - /** - * Whether a program from the rail runs at the panel's own resolution rather than the session's - * 720p default. melonDS draws two 256x192 screens on the CPU: the panel's size costs it - * nothing, and 720p scaled up to the panel blurs the sharp pixels its screen layout is set - * up for (droiddeck-pad-defaults). - */ fun drawsAtPanel(program: String?): Boolean = specs.any { it.program == program && it.atPanel } - /** Desktop catalog package that supplies this emulator. */ fun packageId(emulatorId: String): String? = installedIds[emulatorId] - /** Every emulator the app knows, installed or not, with the games its system folder holds. */ fun emulators(context: Context, installedPackage: (String) -> Boolean): List<Emulator> { val romsRoot = SessionPrefs.romsDir(context).takeIf { it.isNotEmpty() }?.let(::File)?.takeIf { it.isDirectory } return specs.map { spec -> val games = ArrayList<Rom>() if (romsRoot != null && spec.exts.isNotEmpty()) { - // The system's folder(s), matched without regard to case, and up to three folders - // inside them - a dump usually comes as a folder named for the game with the image - // inside it, and people sort those into folders of their own (ps2/games/<game>/). - // The root itself only one deep, for a file left loose there. val systemDirs = romsRoot.listFiles { f -> f.isDirectory && f.name.lowercase() in spec.folders }.orEmpty().toList() val dirs = LinkedHashSet<File>() - // A BIOS folder holds the console's firmware, not games (psx/bios/SCPH1001.BIN). for (top in systemDirs) top.walkTopDown().maxDepth(3).onEnter { it.name.lowercase() !in firmwareFolders } .filter { it.isDirectory }.forEach { dirs.add(it) } dirs.add(romsRoot) - // ...and not into another system's folder: a PS3 .iso in ps3/ is not a PS2 game. romsRoot.listFiles { f -> f.isDirectory && f.name.lowercase() !in systemFolders }?.forEach { dirs.add(it) } for (dir in dirs) { - // A PS3 disc dump is a folder with PS3_GAME in it; RPCS3 boots the folder. if (spec.id == "rpcs3" && File(dir, "PS3_GAME").isDirectory) { val rel = dir.relativeTo(romsRoot).path games.add(Rom(displayTitle(dir.name), dir, "/root/ROMs/$rel", spec.id, - // The dump carries its own art, as an installed package does. art = File(dir, "PS3_GAME/ICON0.PNG").takeIf { it.isFile })) continue } val files = dir.listFiles()?.sortedBy { it.name.lowercase() }.orEmpty() - // A disc sheet (.cue, .gdi) names its track files and a playlist (.m3u) its - // discs: the sheet is the game, what it names is part of it. A .bin beside a - // .cue of the same name is a track even when the sheet can't be read. val parts = files.filter { it.isFile && it.extension.lowercase() in sheetExts }.flatMap(::sheetParts).toSet() files.forEach { f -> val ext = f.extension.lowercase() @@ -275,7 +216,6 @@ object Library { } } if (spec.id == "rpcs3") games.addAll(rpcs3Installed(context)) - // A cover found for the game before (CoverArt) where it has no art of its own. val withArt = games.map { g -> if (g.art != null) g else CoverArt.cached(context, g)?.let { Rom(g.name, g.hostPath, g.guestPath, g.emulatorId, it) } ?: g } Emulator(spec.id, spec.name, spec.system, spec.program, installedPackage(installedIds.getValue(spec.id)), withArt) } @@ -284,27 +224,19 @@ object Library { private val firmwareFolders = setOf("bios", "firmware") private val sheetExts = setOf("cue", "gdi", "m3u") - /** The file names (lower case) a .cue, .gdi or .m3u refers to, from its own folder. */ private fun sheetParts(sheet: File): List<String> = runCatching { if (sheet.length() > 64 * 1024) return emptyList() sheet.readLines().mapNotNull { line -> val t = line.trim() when (sheet.extension.lowercase()) { - // FILE "Tekken 3 (USA) (Track 1).bin" BINARY "cue" -> Regex("^FILE\\s+\"([^\"]+)\"", RegexOption.IGNORE_CASE).find(t)?.groupValues?.get(1) ?: Regex("^FILE\\s+(\\S+)", RegexOption.IGNORE_CASE).find(t)?.groupValues?.get(1) - // 1 0 4 2352 "track01.bin" 0 / 1 0 4 2352 track01.bin 0 "gdi" -> Regex("\"([^\"]+)\"").find(t)?.groupValues?.get(1) ?: t.split(Regex("\\s+")).getOrNull(4) else -> t.takeIf { it.isNotEmpty() && !it.startsWith("#") } } }.map { File(it.replace('\\', '/')).name.lowercase() } }.getOrDefault(emptyList()) - /** - * What RPCS3 has installed on its own HDD - packages (PSN games) land in dev_hdd0/game/<ID> - * with a PARAM.SFO for the title and an EBOOT to boot - as RPCS3's own file list would show - * them. Not in the ROMs folder, so listed from RPCS3's home in the runtime. - */ private fun rpcs3Installed(context: Context): List<Rom> { val hdd = File(LinuxRuntime.rootDir(context), "root/.config/rpcs3/dev_hdd0/game") return hdd.listFiles { f -> f.isDirectory }?.sortedBy { it.name }?.mapNotNull { dir -> @@ -312,7 +244,6 @@ object Library { val sfo = File(dir, "PARAM.SFO") if (!eboot.isFile || !sfo.isFile) return@mapNotNull null val fields = readSfo(sfo) - // Only games: patches, DLC and save data live here too, with their own categories. if (fields["CATEGORY"]?.let { it == "HG" || it == "DG" || it == "GD" } == false) return@mapNotNull null val title = fields["TITLE"]?.trim()?.takeIf { it.isNotEmpty() } ?: dir.name Rom(title, eboot, "/root/.config/rpcs3/dev_hdd0/game/${dir.name}/USRDIR/EBOOT.BIN", "rpcs3", @@ -320,7 +251,6 @@ object Library { } ?: emptyList() } - /** The string fields of a PARAM.SFO (the PSP/PS3 metadata file): a small binary table. */ private fun readSfo(file: File): Map<String, String> { val out = HashMap<String, String>() try { @@ -337,7 +267,7 @@ object Library { var keyEnd = keyStart while (keyEnd < b.size && b[keyEnd] != 0.toByte()) keyEnd++ val key = String(b, keyStart, keyEnd - keyStart, Charsets.US_ASCII) - if (fmt == 0x0204 || fmt == 0x0004) { // utf8 string (null-terminated or not) + if (fmt == 0x0204 || fmt == 0x0004) { val start = data + dataOff val end = minOf(b.size, start + len) var stop = start @@ -346,38 +276,21 @@ object Library { } } } catch (e: Exception) { - // unreadable metadata: the folder name stands in } return out } - /** How the emulator is told which game to boot, on its command line. */ fun launchArgs(emulatorId: String, guestPath: String): List<String> = when (emulatorId) { "rpcs3" -> listOf("--no-gui", guestPath) - // Straight into the game in its controller-driven full-screen UI (first-time setup there - // too), and gone when the game is quit from its pause menu (guide button): -batch. "armsx2" -> listOf("-batch", "-bigpicture", "-fullscreen", "--", guestPath) - // DuckStation shares PCSX2's flags (src/duckstation-qt/qthost.cpp): the same launch and exit. "duckstation" -> listOf("-batch", "-bigpicture", "-fullscreen", "--", guestPath) - // Dolphin: full screen, drawn inside its own main window, without its "stop the emulation?" - // question; no warning boxes either, which wait for a click a controller cannot give - // (they still go to Dolphin's log). The guide button is Dolphin's Toggle Fullscreen hotkey - // (droiddeck-pad-defaults), which shows Dolphin's window and its settings - so no -b, which - // hides that window; closing Dolphin ends the session. Under gamescope Dolphin does not - // always see its window as focused, and by default both its hotkeys and the game's - // controller then stop: HotkeysRequireFocus off, BackgroundInput on. -C sets a Dolphin.ini - // value for this run only, so Dolphin started from the desktop keeps its own settings. "dolphin" -> listOf( "-C", "Dolphin.Display.Fullscreen=True", "-C", "Dolphin.Display.RenderToMain=True", "-C", "Dolphin.Interface.ConfirmStop=False", "-C", "Dolphin.Interface.UsePanicHandlers=False", "-C", "Dolphin.General.HotkeysRequireFocus=False", "-C", "Dolphin.Input.BackgroundInput=True", "-e", guestPath, ) - // Full screen (LaunchSettings.cpp -f); the settings seed keeps the Getting Started - // dialog away (droiddeck-pad-defaults). "cemu" -> listOf("-f", "-g", guestPath) - // Full screen (CLI.cpp --fullscreen); the guide button leaves it for melonDS's menus and - // comes back (HK_FullscreenToggle, droiddeck-pad-defaults). "melonds" -> listOf("-f", guestPath) else -> listOf(guestPath) } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/LibraryCache.kt b/app/src/main/java/com/droiddeck/launcher/frontend/LibraryCache.kt index cd946724a..c8e1cbabf 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/LibraryCache.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/LibraryCache.kt @@ -6,11 +6,6 @@ import org.json.JSONArray import org.json.JSONObject import java.io.File -/** - * The last game list the front end showed, kept in the app's files so the next start draws the - * Steam wall on its first frame. Building the list reads every manifest and walks the added-games - * folders, which can take a second or more; the fresh list replaces this one when it is ready. - */ object LibraryCache { private const val TAG = "LibraryCache" private fun file(context: Context) = File(context.filesDir, "library-cache.json") diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/PeVersionInfo.kt b/app/src/main/java/com/droiddeck/launcher/frontend/PeVersionInfo.kt index 77b61deca..99ead7d6f 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/PeVersionInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/PeVersionInfo.kt @@ -3,27 +3,11 @@ package com.droiddeck.launcher.frontend import java.io.File import java.io.RandomAccessFile -/** - * Reads the StringFileInfo strings (ProductName, FileDescription, …) out of the - * VS_VERSION_INFO resource embedded in a Windows PE executable (.exe). - * - * Walks the PE resource directory to the first RT_VERSION resource — the same - * navigation [com.droiddeck.launcher.files.PeIconExtractor] uses for icons — then parses the alignment-heavy - * VS_VERSIONINFO → StringFileInfo → StringTable → String tree by hand. - * - * Everything is best-effort: any malformed / oversized / unsupported input returns - * null (or an empty map). No allocation beyond the single resource blob. - */ object PeVersionInfo { private const val RT_VERSION = 16 - private const val MAX_RES_SECTION = 64 * 1024 * 1024 // don't slurp absurd resource sections + private const val MAX_RES_SECTION = 64 * 1024 * 1024 - /** - * Returns the StringFileInfo key→value map (e.g. `"ProductName" -> "Palworld"`), - * or null if the file has no readable version resource. Keys are the raw - * VS_VERSIONINFO names; look up [productName]/[fileDescription] for the common ones. - */ fun read(file: File): Map<String, String>? = try { RandomAccessFile(file, "r").use { raf -> val blob = versionResourceBytes(raf) ?: return@use null @@ -33,13 +17,6 @@ object PeVersionInfo { null } - /** - * Best display name from a version map. ProductName is usually the cleanest title, but is - * sometimes an internal abbreviation (e.g. God of War ships ProductName "GoW" while - * FileDescription is "God of War"). When ProductName is a single spaceless token and - * FileDescription is a normal multi-word title, prefer FileDescription. Trimmed; null if - * neither is meaningful. - */ fun bestName(map: Map<String, String>?): String? { if (map == null) return null val product = map["ProductName"]?.trim()?.takeIf { it.isNotEmpty() } @@ -52,15 +29,14 @@ object PeVersionInfo { return product ?: desc } - // ── locate the RT_VERSION resource blob (mirrors PeIconExtractor's navigation) ── private fun versionResourceBytes(raf: RandomAccessFile): ByteArray? { val dos = read(raf, 0, 0x40) ?: return null - if (u8(dos, 0) != 0x4D || u8(dos, 1) != 0x5A) return null // "MZ" + if (u8(dos, 0) != 0x4D || u8(dos, 1) != 0x5A) return null val peOff = u32(dos, 0x3C).toInt() val coff = read(raf, peOff.toLong(), 24) ?: return null - if (u8(coff, 0) != 0x50 || u8(coff, 1) != 0x45 || u8(coff, 2) != 0 || u8(coff, 3) != 0) return null // "PE\0\0" + if (u8(coff, 0) != 0x50 || u8(coff, 1) != 0x45 || u8(coff, 2) != 0 || u8(coff, 3) != 0) return null val numSections = u16(coff, 6) val sizeOpt = u16(coff, 20) if (numSections <= 0 || numSections > 96 || sizeOpt <= 0) return null @@ -68,8 +44,8 @@ object PeVersionInfo { val optOff = peOff + 24L val opt = read(raf, optOff, sizeOpt) ?: return null val magic = u16(opt, 0) - val ddOff = if (magic == 0x20B) 112 else 96 // PE32+ vs PE32 data-directory offset - val resDirEntry = ddOff + 2 * 8 // data directory index 2 = resource table + val ddOff = if (magic == 0x20B) 112 else 96 + val resDirEntry = ddOff + 2 * 8 if (resDirEntry + 8 > opt.size) return null val resRVA = u32(opt, resDirEntry).toInt() val resSize = u32(opt, resDirEntry + 4).toInt() @@ -105,7 +81,6 @@ object PeVersionInfo { return dataEntryBytes(res, verData, secVA) } - /** Resolve the sub-directory under [dirIdx] whose id matches [wantId]; null if absent or not a directory. */ private fun findIdSubdir(res: ByteArray, dirIdx: Int, wantId: Int, resDirStart: Int): Int? { if (dirIdx + 16 > res.size) return null val total = u16(res, dirIdx + 12) + u16(res, dirIdx + 14) @@ -123,7 +98,6 @@ object PeVersionInfo { return null } - /** Descend the first entry of each level until an IMAGE_RESOURCE_DATA_ENTRY; returns its index. */ private fun firstDataEntry(res: ByteArray, dirIdx: Int, resDirStart: Int): Int? { if (dirIdx + 16 > res.size) return null if (u16(res, dirIdx + 12) + u16(res, dirIdx + 14) == 0) return null @@ -137,7 +111,6 @@ object PeVersionInfo { } } - /** Read the bytes an IMAGE_RESOURCE_DATA_ENTRY points at (its OffsetToData is an RVA). */ private fun dataEntryBytes(res: ByteArray, entryIdx: Int, secVA: Int): ByteArray? { if (entryIdx < 0 || entryIdx + 8 > res.size) return null val rva = u32(res, entryIdx).toInt() @@ -147,17 +120,11 @@ object PeVersionInfo { return res.copyOfRange(start, start + size) } - // ── VS_VERSIONINFO tree parse ── - // - // Every node is: WORD wLength, WORD wValueLength, WORD wType, WCHAR szKey[], - // then (32-bit aligned) an optional Value, then (32-bit aligned) child nodes. - // We only care about: VS_VERSIONINFO → StringFileInfo → StringTable → String. private fun parseStringFileInfo(b: ByteArray): Map<String, String> { val out = LinkedHashMap<String, String>() if (b.size < 6) return out val rootLen = minOf(u16(b, 0), b.size) - // VS_VERSION_INFO header: skip szKey "VS_VERSION_INFO" then the FIXEDFILEINFO value. val rootValLen = u16(b, 2) var pos = align4(6 + (utf16zLen(b, 6) + 1) * 2) pos = align4(pos + rootValLen) @@ -173,7 +140,6 @@ object PeVersionInfo { return out } - /** Iterate the StringTable children inside a StringFileInfo block [start, end). */ private fun parseTables(b: ByteArray, start: Int, end: Int, out: MutableMap<String, String>) { var p = align4(start + 6 + (utf16zLen(b, start + 6) + 1) * 2) while (p + 6 <= end) { @@ -185,7 +151,6 @@ object PeVersionInfo { } } - /** Iterate the String children inside a StringTable block [start, end). */ private fun parseStrings(b: ByteArray, start: Int, end: Int, out: MutableMap<String, String>) { var q = align4(start + 6 + (utf16zLen(b, start + 6) + 1) * 2) while (q + 6 <= end) { @@ -202,11 +167,9 @@ object PeVersionInfo { } } - // ── little-endian / UTF-16 readers ── private fun align4(x: Int): Int = (x + 3) and 3.inv() - /** Number of UTF-16 code units before the null terminator starting at [off]. */ private fun utf16zLen(b: ByteArray, off: Int): Int { var i = off while (i + 1 < b.size) { @@ -216,7 +179,6 @@ object PeVersionInfo { return (i - off) / 2 } - /** Read a null-terminated UTF-16LE string starting at [off], not reading past [limit]. */ private fun utf16z(b: ByteArray, off: Int, limit: Int = b.size): String { val sb = StringBuilder() var i = off diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/PrefixInstalledDetector.kt b/app/src/main/java/com/droiddeck/launcher/frontend/PrefixInstalledDetector.kt index 7f5ca5b0b..91625338c 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/PrefixInstalledDetector.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/PrefixInstalledDetector.kt @@ -3,18 +3,9 @@ package com.droiddeck.launcher.frontend import org.json.JSONObject import java.io.File -/** - * Which catalog components a game's prefix already has from elsewhere - a real installer, the game, - * the user - so the list can say so instead of recommending them again. Bannerlator's detector, - * on DroidDeck's compatdata/<id>: only markers absent from a fresh Proton prefix count (OpenAL32, - * Mono, PhysX, Games for Windows Live); msvcp140, d3dx9 and the like ship with Proton and cannot - * tell. Files DroidDeck put there itself (droiddeck-wincomponents' state) are not counted. - * Pure filesystem reads; never throws. - */ object PrefixInstalledDetector { private const val STATE = ".droiddeck-wincomponents.json" - /** Detector names (oalinst, mono, physx, XLiveRedist) present in [compat]/pfx. */ fun detect(compat: File?): Set<String> = try { val pfx = compat?.let { File(it, "pfx") } if (pfx == null || !pfx.isDirectory) emptySet() else { @@ -38,7 +29,6 @@ object PrefixInstalledDetector { emptySet() } - /** Windows names ignore case; the prefix's folder may spell them either way. */ private fun fileIgnoringCase(pfx: File, rel: String): Boolean { val target = File(pfx, rel) return target.parentFile?.listFiles()?.any { it.isFile && it.name.equals(target.name, ignoreCase = true) } == true diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SessionInstalls.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SessionInstalls.kt index 5291f80fc..3c32bcb06 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SessionInstalls.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SessionInstalls.kt @@ -7,17 +7,10 @@ import com.droiddeck.launcher.session.GameStorage import com.droiddeck.launcher.session.SessionState import java.io.File -/** - * A Steam game whose download finishes during a session, noticed by its appmanifest (a new one, - * or StateFlags turning to 4, fully installed) with a light look every five seconds while the - * session runs, and handed to [AutoComponents] as a single job that may run in the session, so its - * Windows components are in place when the user plays it right after the download. - */ object SessionInstalls { private const val TAG = "SessionInstalls" private const val POLL_MS = 5_000L - /** One appmanifest as read: the app, its name and folder, whether it is fully installed. */ class Manifest(val appId: Int, val name: String, val installDir: String, val installed: Boolean, val steamapps: File) private val APPID = Regex("\"appid\"\\s*\"(\\d+)\"", RegexOption.IGNORE_CASE) @@ -32,16 +25,11 @@ object SessionInstalls { STATE.find(text)?.groupValues?.get(1)?.toIntOrNull() == 4, file.parentFile!!) }.getOrNull() - /** The libraries the session's client installs to, as host folders. */ internal fun steamappsDirs(context: Context): List<File> = listOfNotNull( File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps"), GameStorage.effective(context)?.let { File(it.path, "steamapps") }, ).filter { it.isDirectory } - /** - * Keeps what the libraries hold between looks: a manifest re-read only when its time changed. - * [poll] returns the apps that became fully installed since the last look (none on the first). - */ internal class Watch(private val dirs: () -> List<File>) { private val seen = HashMap<String, Pair<Long, Manifest?>>() private var first = true @@ -60,22 +48,16 @@ object SessionInstalls { } } - /** A finished install as the Games tab would list it. */ internal fun gameOf(m: Manifest): Library.SteamGame = Library.SteamGame(m.appId, m.name.ifEmpty { m.appId.toString() }, null, m.steamapps.parentFile?.path ?: "steam", gameFiles = File(m.steamapps, "common/${m.installDir}"), source = Library.SOURCE_STEAM) - /** - * Looks every five seconds while the session runs, on a thread of its own at background - * priority; each finished install becomes one job for its Windows components. - */ fun watch(context: Context) { val app = context.applicationContext Thread({ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND) val watch = Watch { steamappsDirs(app) } try { - // The session is still starting when this is called: up to two minutes for it. var waited = 0L while (!SessionState.running && waited < 120_000L) { Thread.sleep(POLL_MS); waited += POLL_MS } watch.poll() diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamAppInfo.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamAppInfo.kt index 16a37fc0a..e847d8060 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamAppInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamAppInfo.kt @@ -12,25 +12,16 @@ import java.io.RandomAccessFile import java.nio.ByteBuffer import java.nio.ByteOrder -/** - * Steam's own data for an app, from the client in the runtime: its app-info cache - * (Steam/appcache/appinfo.vdf, the PICS data the client keeps for every app it has seen). What is - * read is which Steamworks Shared depots (the VC++, DirectX... redistributables of app 228980) the - * app's depots point at, and its name. Kept per appid in the app's files, so it works offline; an - * app the client has not seen is looked for again after a week. - */ object SteamAppInfo { private const val TAG = "SteamAppInfo" private const val CACHE = "steam-appinfo.json" private const val RETRY_AFTER_MS = 7L * 24 * 3600 * 1000 - /** The Steamworks Shared app the redistributable depots belong to. */ const val SHARED_APP = "228980" class Info(val name: String, val sharedDepots: List<String>) fun appinfoFile(context: Context) = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/appcache/appinfo.vdf") - /** The shared depots for [appId]: from the cache, else the client's app-info file; null when unknown. */ @Synchronized fun info(context: Context, appId: Int, appinfo: File = appinfoFile(context)): Info? { val cacheFile = AtomicFile(File(context.filesDir, CACHE)) @@ -40,13 +31,10 @@ object SteamAppInfo { val known = cache.optJSONObject(appId.toString()) known?.let { o -> if (!o.optBoolean("miss")) return Info(o.optString("name"), o.optJSONArray("depots").strings()) - // A miss is read again when the client has written its cache since (the user may have - // looked at the app in Steam), else a week later. if (now - o.optLong("at") < RETRY_AFTER_MS && o.optLong("mtime") == mtime) return null } val read = runCatching { read(appinfo, appId) }.onFailure { Log.w(TAG, "appinfo: ${it.javaClass.simpleName}") }.getOrNull() val entry = JSONObject().put("at", now).put("mtime", mtime) - // When Steam was last asked anonymously ([lookup]), kept across re-reads of the file. known?.optLong("picsAt")?.takeIf { it > 0 }?.let { entry.put("picsAt", it) } if (read == null) entry.put("miss", true) else entry.put("name", read.name).put("depots", JSONArray(read.sharedDepots)) cache.put(appId.toString(), entry) @@ -55,14 +43,9 @@ object SteamAppInfo { return read } - /** Steam asked anonymously ([SteamPics]); a test stands in for it. */ @androidx.annotation.VisibleForTesting internal var pics: (List<Int>) -> Map<Int, Info?>? = { SteamPics.lookup(it) } - /** - * Of [appIds], those neither the client's cache (appinfo.vdf, read first: free) nor an earlier - * answer has, and that Steam was not asked about anonymously in the past week. - */ fun needLookup(context: Context, appIds: List<Int>): List<Int> { val wanted = appIds.distinct().filter { it > 0 && info(context, it) == null } if (wanted.isEmpty()) return emptyList() @@ -71,11 +54,6 @@ object SteamAppInfo { return wanted.filter { now - (cache.optJSONObject(it.toString())?.optLong("picsAt") ?: 0L) >= RETRY_AFTER_MS } } - /** - * [appIds] the client never saw, asked of Steam anonymously in one connection ([SteamPics], in - * batches) and every answer kept like the client's: an app's depots and name, or a miss tried - * again in a week. False when nothing needed asking or Steam could not be reached. Blocking. - */ @Synchronized fun lookup(context: Context, appIds: List<Int>): Boolean { val need = needLookup(context, appIds) @@ -96,7 +74,6 @@ object SteamAppInfo { return true } - /** What is kept for [appId]: "hit", "miss" or "none" (part of a game's list fingerprint). */ fun state(context: Context, appId: Int): String = cache(context).optJSONObject(appId.toString())?.let { if (it.optBoolean("miss")) "miss" else "hit" } ?: "none" private fun cache(context: Context): JSONObject = @@ -104,10 +81,6 @@ object SteamAppInfo { private fun JSONArray?.strings(): List<String> = if (this == null) emptyList() else (0 until length()).map { optString(it) } - /** - * [appId]'s entry in an appinfo.vdf (versions 27, 28 and 29, the last with its keys in a string - * table), or null when the file does not have it. - */ internal fun read(file: File, appId: Int): Info? { if (!file.isFile) return null RandomAccessFile(file, "r").use { raf -> @@ -181,7 +154,6 @@ object SteamAppInfo { return String(bytes, Charsets.UTF_8) } - /** One binary key-value map, to its end marker; keys by name, or by index into [strings]. */ private fun readMap(b: ByteBuffer, strings: List<String>?): Map<String, Any> { val out = LinkedHashMap<String, Any>() while (b.hasRemaining()) { diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamGridDb.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamGridDb.kt index 630fb64cd..dc35dafb2 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamGridDb.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamGridDb.kt @@ -11,15 +11,6 @@ import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder -/** - * SteamGridDB, the art source after Steam's store: a game by its Steam appid, else by a search - * whose result must carry the same name, then its best-rated grids, heroes, logos and icons. - * - * The API key: the user's own (Steam settings › Games), sealed with [CredentialCipher] in the - * app's files, else the one the build carries (`BuildConfig.SGDB_API_KEY`, from a CI secret), else - * none, and then this step is skipped. The key travels only in the Authorization header and is - * never logged. - */ object SteamGridDb { private const val TAG = "SteamGridDb" private const val API = "https://www.steamgriddb.com/api/v2" @@ -31,17 +22,14 @@ object SteamGridDb { private fun keyFile(context: Context) = File(context.filesDir, "sgdb/key.json") - /** The key the build carries, or "" (a build without the secret, a fork's PR build). */ fun builtInKey(): String = runCatching { Class.forName("com.droiddeck.launcher.BuildConfig").getField("SGDB_API_KEY").get(null) as? String }.getOrNull().orEmpty() - /** The user's key wins, then the build's; "" when there is neither. */ fun pick(user: String, builtIn: String): String = user.trim().ifEmpty { builtIn.trim() } fun key(context: Context): String = pick(userKey(context), builtInKey()) - /** The key the user entered, or "". */ @Synchronized fun userKey(context: Context): String { val f = keyFile(context) @@ -56,7 +44,6 @@ object SteamGridDb { } } - /** Saves the user's key sealed (plain only where the Keystore does not work); "" clears it. */ @Synchronized fun setUserKey(context: Context, key: String) { val f = keyFile(context) @@ -75,7 +62,6 @@ object SteamGridDb { if (!tmp.renameTo(f)) { f.delete(); tmp.renameTo(f) } } - /** The SteamGridDB game id for [steamAppId], else for a search on [name] that returns the same name; null when neither. */ fun gameId(key: String, steamAppId: Int?, name: String): Int? { if (steamAppId != null) { json(key, "$API/games/steam/$steamAppId")?.optJSONObject("data")?.optInt("id")?.takeIf { it > 0 }?.let { return it } @@ -85,16 +71,11 @@ object SteamGridDb { return match(name, (0 until results.length()).map { results.getJSONObject(it) }.map { it.optInt("id") to it.optString("name") }) } - /** The id of the first result whose name matches [name] ([AddedGameArt.nameMatch]); no blind first pick. */ internal fun match(name: String, results: List<Pair<Int, String>>): Int? = results.map { (id, got) -> id to AddedGameArt.nameMatch(name, got) } .filter { it.second > 0 && it.first > 0 } .maxByOrNull { it.second }?.first - /** - * Fetches what is missing into [cache] under the names Steam's art uses there (p.jpg, - * header.jpg, hero.jpg, logo.png) and the icon as icon-sgdb.png. True when anything arrived. - */ fun download(key: String, id: Int, cache: File): Boolean { var got = false for ((path, local) in listOf( @@ -116,7 +97,6 @@ object SteamGridDb { return got } - /** The best-scored [limit] images for [slot] of game [id], as (thumbnail, full image) URLs. */ fun images(key: String, id: Int, slot: AddedGameArt.Slot, limit: Int): List<Pair<String, String>> { val path = when (slot) { AddedGameArt.Slot.COVER -> "grids/game/$id?dimensions=600x900&$QUERY" @@ -128,20 +108,17 @@ object SteamGridDb { return ranked(items, pngOnly = slot == AddedGameArt.Slot.LOGO || slot == AddedGameArt.Slot.ICON).take(limit) } - /** [items] best-scored first, as (thumbnail, full image) URLs; for a .png slot only PNGs. */ internal fun ranked(items: JSONArray, pngOnly: Boolean): List<Pair<String, String>> = (0 until items.length()).map { items.getJSONObject(it) } .filter { it.optString("url").startsWith("https://") && (!pngOnly || it.optString("mime") == "image/png" || it.optString("url").endsWith(".png", true)) } .sortedByDescending { it.optInt("score", 0) } .map { (it.optString("thumb").takeIf { t -> t.startsWith("https://") } ?: it.optString("url")) to it.optString("url") } - /** The highest-scored image's URL; for a .png slot only a PNG. */ internal fun best(items: JSONArray, pngOnly: Boolean): String? = (0 until items.length()).map { items.getJSONObject(it) } .filter { it.optString("url").startsWith("https://") && (!pngOnly || it.optString("mime") == "image/png" || it.optString("url").endsWith(".png", true)) } .maxByOrNull { it.optInt("score", 0) }?.optString("url") - /** A GET with the key in its Authorization header; the body as JSON, or null. Logs the path only. */ private fun json(key: String, url: String): JSONObject? { val c = URL(url).openConnection() as HttpURLConnection c.connectTimeout = 10_000 diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamMatch.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamMatch.kt index 177888a46..c0c83676d 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamMatch.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamMatch.kt @@ -5,20 +5,9 @@ import android.util.AtomicFile import org.json.JSONObject import java.io.File -/** - * Which Steam app a game added in DroidDeck is, kept once per game (by its folder) and used by - * everything that asks: its art ([AddedGameArt]) and its Windows components ([WinCompSources]), - * so the two never disagree. - * - * [Certainty.FILES]: the game's own files name the appid (steam_appid.txt, also under - * steam_settings/ or a parent folder; an emulator ini; Steam's appmanifest) - certain. - * [Certainty.NAME]: Steam's store search returned a game of the same name - a guess. - * [Certainty.NONE]: neither; a miss is tried again after a week. - */ object SteamMatch { enum class Certainty { FILES, NAME, NONE } - /** [source]: what gave the appid (GameIdentifier's source, or "store search"). */ class Match(val appId: Int?, val certainty: Certainty, val source: String, val checkedAt: Long) private const val FILE = "steam-matches.json" @@ -50,12 +39,6 @@ object SteamMatch { try { out.write(json.toString().toByteArray()); atomic.finishWrite(out) } catch (e: Exception) { atomic.failWrite(out) } } - /** - * The match for the game in [key] (its folder), [exe] its exe and [name] its title: its files - * first, always read again; else the one kept (a name match stays, a miss for a week); else - * [legacy], an appid an earlier build's art lookup kept; else [search] on the name, when given. - * Blocking ([search] asks the network). - */ fun resolve(context: Context, key: String, exe: File, name: String, legacy: File? = null, search: ((String) -> Int?)? = null): Match { val now = System.currentTimeMillis() val identity = runCatching { GameIdentifier.identify(exe) }.getOrNull() diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamPics.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamPics.kt index ae0bf12f2..2dd132c51 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamPics.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamPics.kt @@ -18,18 +18,9 @@ import java.util.zip.GZIPInputStream import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory -/** - * Steam's own app info (PICS) for apps the client in the runtime has never seen, asked of Steam - * anonymously: a connection to one of Steam's connection managers (the list from Steam's public - * ISteamDirectory/GetCMListForConnect, the WebSocket transport Steam's web clients use), an - * anonymous logon - no account, nothing of the user's Steam sign-in - a request for the apps' - * access tokens and then their product info, the app KeyValues read for their Steamworks Shared - * depots and name, and the connection closed. What steamcmd's anonymous `app_info_print` sees. - */ object SteamPics { private const val TAG = "SteamPics" private const val CM_LIST = "https://api.steampowered.com/ISteamDirectory/GetCMListForConnect/v1/?cellid=0&format=json" - /** At most this many apps per request. */ const val BATCH = 25 private const val PROTO = 0x80000000.toInt() private const val EMSG_MULTI = 1 @@ -41,13 +32,8 @@ object SteamPics { private const val EMSG_INFO_REQUEST = 8903 private const val EMSG_INFO_RESPONSE = 8904 private const val PROTOCOL_VERSION = 65580 - /** An anonymous user's SteamID: universe Public, account type AnonUser, instance 0. */ private const val ANON_STEAMID = (1L shl 56) or (10L shl 52) - /** - * [appIds]' info, one connection for all of them in batches of [BATCH]: each known app's - * info, null for an app Steam does not know. Null when Steam could not be reached. Blocking. - */ fun lookup(appIds: List<Int>): Map<Int, SteamAppInfo.Info?>? { if (appIds.isEmpty()) return emptyMap() val host = runCatching { cmHost() }.getOrNull() ?: return null @@ -56,7 +42,6 @@ object SteamPics { ws.send(message(EMSG_LOGON, header(ANON_STEAMID, 0), Proto().varint(1, PROTOCOL_VERSION.toLong()).varint(7, 16).string(6, "english").bytes())) var steamId = 0L var session = 0 - // The logon's answer (other messages before it are passed over). loop@ while (true) for ((emsg, hdr, body) in messages(ws.receive() ?: return null)) if (emsg == EMSG_LOGON_RESPONSE) { val h = fields(hdr); val b = fields(body) if ((b[1]?.firstOrNull() as? Long ?: 0L) != 1L) { Log.w(TAG, "anonymous logon refused: ${b[1]}"); return null } @@ -99,10 +84,8 @@ object SteamPics { } } - /** [appIds] in requests of at most [BATCH], each once. */ fun batches(appIds: List<Int>): List<List<Int>> = appIds.distinct().chunked(BATCH) - /** A connection manager for the WebSocket transport, least loaded first as Steam lists them. */ private fun cmHost(): String? { val c = URL(CM_LIST).openConnection() as HttpURLConnection c.connectTimeout = 10_000; c.readTimeout = 10_000 @@ -112,11 +95,9 @@ object SteamPics { .firstOrNull { it.optString("type") == "websockets" }?.optString("endpoint")?.substringBefore(':') } - // ---- the product info answer ---- class InfoResponse(val apps: Map<Int, SteamAppInfo.Info>, val unknown: List<Int>, val pending: Boolean) - /** A CMsgClientPICSProductInfoResponse: each app's KeyValues read, the unknown ones, whether more follows. */ @VisibleForTesting internal fun parseInfoResponse(body: ByteArray): InfoResponse { val f = fields(body) @@ -132,17 +113,14 @@ object SteamPics { return InfoResponse(apps, unknown, (f[6]?.firstOrNull() as? Long ?: 0L) != 0L) } - /** An app's KeyValues text ("appinfo" { "common" {...} "depots" {...} }) to its name and shared depots. */ @VisibleForTesting internal fun infoFromKeyValues(text: String): SteamAppInfo.Info? { - // The buffer ends in a NUL, kept or not by whoever handed it over. val tree = runCatching { KeyValues(text.trimEnd('\u0000', ' ', '\n', '\r', '\t')).read() }.getOrNull() ?: return null @Suppress("UNCHECKED_CAST") val app = tree["appinfo"] as? Map<String, Any> ?: return null return SteamAppInfo.infoOf(app) } - /** Valve's text KeyValues: quoted keys and values, nested braces. */ private class KeyValues(private val s: String) { private var i = 0 fun read(): Map<String, Any> = map(top = true) @@ -182,7 +160,6 @@ object SteamPics { } } - // ---- messages ---- private fun header(steamId: Long, session: Int) = Proto().fixed64(1, steamId).varint(2, session.toLong()).bytes() @@ -190,7 +167,6 @@ object SteamPics { ByteBuffer.allocate(8 + header.size + body.size).order(ByteOrder.LITTLE_ENDIAN) .putInt(emsg or PROTO).putInt(header.size).put(header).put(body).array() - /** One message off the wire as (emsg, header, body); a Multi unpacked (gunzipped when it says so) into the ones inside. */ @VisibleForTesting internal fun messages(data: ByteArray): List<Triple<Int, ByteArray, ByteArray>> { if (data.size < 8) return emptyList() @@ -217,9 +193,7 @@ object SteamPics { return out } - // ---- protobuf, the little needed ---- - /** Writes protobuf fields. */ @VisibleForTesting internal class Proto { private val out = ByteArrayOutputStream() @@ -231,7 +205,6 @@ object SteamPics { fun bytes(): ByteArray = out.toByteArray() } - /** Reads protobuf fields: number to values (Long for varint and fixed, ByteArray for length-delimited). */ @VisibleForTesting internal fun fields(b: ByteArray): Map<Int, List<Any>> { val out = HashMap<Int, MutableList<Any>>() @@ -258,16 +231,13 @@ object SteamPics { return out } - // ---- WebSocket over TLS, binary frames ---- private class Wss(host: String, timeoutMs: Int) : AutoCloseable { - // Made with the host name, so it goes out as SNI. private val socket: SSLSocket = (SSLSocketFactory.getDefault().createSocket(host, 443) as SSLSocket).apply { soTimeout = timeoutMs } private val out: OutputStream private val input: DataInputStream init { - // Host name checked against the certificate, as HTTPS does. socket.sslParameters = socket.sslParameters.apply { endpointIdentificationAlgorithm = "HTTPS" } socket.startHandshake() out = socket.outputStream @@ -305,7 +275,6 @@ object SteamPics { out.write(frame.toByteArray()); out.flush() } - /** The next binary message (fragments joined); null on close. */ fun receive(): ByteArray? { val message = ByteArrayOutputStream() while (true) { diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt index f656df784..f41b8265f 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt @@ -2,14 +2,7 @@ package com.droiddeck.launcher.frontend import java.io.File -/** - * What Steam itself installs alongside a game: the Steamworks Shared redistributables (app 228980) - * listed under "SharedDepots" in the game's appmanifest. Those live in Steamworks Shared rather than - * the game's folder, so the folder scan ([DependencyDetector]) cannot see them; this reads Steam's - * list instead. Depots are matched to Bannerlator's catalog names; unknown ones are left out. - */ object SteamRedists { - /** Steamworks Shared depot -> (catalog component, what Steam calls it). 2015-2019 runtimes are one family. */ val DEPOTS = linkedMapOf( "228981" to ("vcredist2005_dll" to "VC++ 2005"), "228982" to ("vcredist2008_dll" to "VC++ 2008"), @@ -25,7 +18,6 @@ object SteamRedists { private val BLOCK = Regex("\"SharedDepots\"\\s*\\{([^}]*)\\}", RegexOption.IGNORE_CASE) private val ENTRY = Regex("\"(\\d+)\"\\s+\"\\d+\"") - /** The game's appmanifest beside its install folder (steamapps/common/<dir> -> steamapps/). */ fun manifest(gameDir: File?, appId: Int): File? { val steamapps = gameDir?.parentFile?.parentFile ?: return null return File(steamapps, "appmanifest_$appId.acf").takeIf { it.isFile } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/StoreListBackfill.kt b/app/src/main/java/com/droiddeck/launcher/frontend/StoreListBackfill.kt index 0ac8cf34d..63315ac7b 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/StoreListBackfill.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/StoreListBackfill.kt @@ -14,47 +14,28 @@ import com.droiddeck.launcher.stores.gog.GogDownloadManager import org.json.JSONObject import java.io.File -/** - * For a GOG or Epic game installed before its sidecar kept the store's list (GOG's build - * `dependencies`, Epic's manifest prerequisite): the list read once from the store's build - * manifest with the saved sign-in - the manifest alone, never a game file - and kept in the sidecar - * the way an install keeps it. Runs on the components' worker ([AutoComponents]). Without a - * sign-in or a network it is skipped quietly and tried again a week later. - */ object StoreListBackfill { private const val TAG = "StoreListBackfill" private const val FILE = "store-list-backfill.json" private const val RETRY_AFTER_MS = 7L * 24 * 3600 * 1000 - /** The store's list for the game in a sidecar: GOG's dependency ids, or Epic's prerequisite {name, path}; null when it could not be had. */ fun interface Fetcher { fun fetch(context: Context, sidecar: StoreGameSidecar): Map<String, String>? } - /** The store itself, with the saved sign-in. */ internal val storeFetcher = Fetcher { context, sidecar -> fromStore(context, sidecar) } @VisibleForTesting internal var fetcher: Fetcher = storeFetcher - /** Whether [sidecar] was written before the list was kept. */ - /** - * Whether [sidecar] has no list read whole: written before the list was kept, or by a build - * that could have written an empty one for a manifest it did not read right. - */ fun needs(sidecar: StoreGameSidecar): Boolean = (sidecar.store == Store.GOG || sidecar.store == Store.EPIC) && WinCompSources.LIST_READ !in sidecar.extra - /** - * The list for the game in [folder], fetched and kept when its sidecar lacks one and it was not - * tried in the past week. True when the sidecar changed. Blocking (network). - */ @Synchronized fun fill(context: Context, folder: File): Boolean { val sidecar = StoreGameSidecar.read(folder)?.takeIf { it.isInstalled && needs(it) } ?: return false val tries = runCatching { JSONObject(AtomicFile(File(context.filesDir, FILE)).readFully().toString(Charsets.UTF_8)) }.getOrDefault(JSONObject()) val now = System.currentTimeMillis() if (now - tries.optLong(folder.path, 0L) < RETRY_AFTER_MS) return false - // Offline is no try: it waits for a network rather than a week. if (!AutoComponents.online(context)) return false val list = runCatching { fetcher.fetch(context, sidecar) }.getOrNull() if (list == null) { @@ -65,7 +46,6 @@ object StoreListBackfill { Log.i(TAG, "${sidecar.title}: the ${sidecar.store.label} list could not be had; again in a week") return false } - // Read fresh and written whole, as an install writes it: only the list is added. val current = StoreGameSidecar.read(folder) ?: return false current.copy(extra = current.extra + list + (WinCompSources.LIST_READ to "1")).write(folder) Log.i(TAG, "backfill: ${sidecar.title} " + when (sidecar.store) { diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/WinCompSources.kt b/app/src/main/java/com/droiddeck/launcher/frontend/WinCompSources.kt index b96d23bd6..7562cc1ee 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/WinCompSources.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/WinCompSources.kt @@ -7,35 +7,11 @@ import org.json.JSONObject import java.io.File import java.util.Locale -/** - * Which Windows components a game needs, from the list its source keeps - the source badge decides - * which list is trusted: - * - * - **Steam**: the Steamworks Shared redistributables Steam installs with the game (its - * appmanifest's SharedDepots, else the client's app info) - turned on by themselves. - * - **GOG**: the build manifest's `dependencies`, kept in the sidecar at install - on by themselves. - * - **Epic Games**: the manifest's prerequisite (usually UE4/UE5's prerequisite setup: VC++ and the - * DirectX June 2010 runtime), kept in the sidecar at install - on by themselves. - * - **Amazon Games**: fuel.json's PostInstall installers - on by themselves. - * - **Custom** whose files name a Steam appid ([SteamMatch], certain): Steam's list for that app - - * on by themselves. Matched by name only: the same list as Recommended. No match: the folder scan - * ([DependencyDetector]) as Recommended. - * - * Every store entry is mapped to catalog names here, in one place; one that maps to nothing is a - * suggestion under its own name. The folder scan is offered as Recommended for every game. - */ object WinCompSources { - /** Whose list a finding came from; the reason line says it. */ enum class Origin { STEAM, GOG, EPIC, AMAZON, STEAM_NAME, FOLDER } - /** - * One thing a list names: [component] the catalog name it maps to (null: none, a suggestion), - * [original] what the list called it, [auto] whether it is turned on by itself, [args] what - * the reason line names (Steam: the label, the game, the appid). - */ data class Finding(val component: String?, val original: String, val origin: Origin, val auto: Boolean, val args: List<String> = emptyList()) - // ---- mapping tables: a store's names -> catalog components ---- private val VC_YEAR = mapOf( "2005" to "vcredist2005", "2008" to "vcredist2008", "2010" to "vcredist2010", "2012" to "vcredist2012", @@ -47,10 +23,6 @@ object WinCompSources { "472" to "dotnet472", "48" to "dotnet48", "481" to "dotnet48", ) - /** - * A GOG dependency id ("MSVC2017_x64", "DirectX", "DOTNET48", "PhysX", "OpenAL", "XNA4") to its - * components; empty for one that is none of ours (ISI scripts, GOG's own helpers). - */ fun gog(id: String): List<String> { val key = id.uppercase(Locale.US).replace(Regex("[^A-Z0-9]"), "") Regex("""^(?:MSVC|VCREDIST|VC)(\d{4})""").find(key)?.let { m -> return listOfNotNull(VC_YEAR[m.groupValues[1]]) } @@ -67,40 +39,30 @@ object WinCompSources { } } - /** Unreal's prerequisite setup (UE4PrereqSetup_x64.exe, UEPrereqSetup_x64.exe): VC++ 2015-2022 and DirectX June 2010. */ private val UE_PREREQ = Regex("""(?i)ue\d?prereqsetup""") - /** An Epic prerequisite ([path] the installer it runs, [name] what it is called) to its components. */ fun epic(path: String, name: String): List<String> = when { UE_PREREQ.containsMatchIn(path) || name.contains("UE4 Prerequisites", true) || name.contains("UE5 Prerequisites", true) || name.contains("Unreal Engine Prerequisites", true) -> listOf("vcredist2022", "d3dx9") else -> DependencyDetector.componentsFor(path).ifEmpty { if (path.isEmpty()) gog(name) else emptyList() } } - /** An installer a store runs after an install (Amazon's PostInstall "Command") to its components. */ fun installer(path: String): List<String> = if (UE_PREREQ.containsMatchIn(path)) listOf("vcredist2022", "d3dx9") else DependencyDetector.componentsFor(path) - /** Steamworks Shared depot ids to (component, Steam's label). */ fun steamDepots(ids: List<String>): List<Pair<String, String>> = ids.mapNotNull { SteamRedists.DEPOTS[it] }.distinctBy { it.first } - // ---- what each store left at install ---- - /** Sidecar keys the store installs fill. */ const val GOG_DEPENDENCIES = "dependencies" const val EPIC_PREREQ_PATH = "prereqPath" const val EPIC_PREREQ_NAME = "prereqName" - /** Set beside the list when it was read whole (an empty list then means the store lists none). */ const val LIST_READ = "listRead" - /** fuel.json's PostInstall commands. */ fun amazonPostInstall(folder: File): List<String> = runCatching { val arr = JSONObject(File(folder, "fuel.json").readText()).optJSONArray("PostInstall") ?: return emptyList() (0 until arr.length()).mapNotNull { arr.optJSONObject(it)?.optString("Command")?.takeIf { c -> c.isNotEmpty() } } }.getOrDefault(emptyList()) - // ---- per game ---- - /** Steam's list for [appId]: the local appmanifest's SharedDepots, else the client's app info. */ fun steamList(context: Context, gameDir: File?, appId: Int, gameName: String, auto: Boolean): List<Finding> { val local = SteamRedists.detect(gameDir, appId) val pairs = if (local.isNotEmpty()) local.map { it.componentName to it.label } @@ -110,7 +72,6 @@ object WinCompSources { } } - /** Everything known for [game], blocking (files; the client's app info; no network). */ fun forGame(context: Context, game: Library.SteamGame): List<Finding> { val folder = game.gameFiles val out = ArrayList<Finding>() @@ -141,8 +102,6 @@ object WinCompSources { } else -> {} } - // A game added in DroidDeck (Custom, or a store's) that is a Steam app: Steam's list for it, - // on by itself when its files say so, else only recommended. if (game.library == Library.ADDED && folder != null) SteamMatch.get(context, folder.path)?.let { match -> val id = match.appId ?: return@let when (match.certainty) { @@ -151,7 +110,6 @@ object WinCompSources { else -> {} } } - // The folder scan, for every game: what it bundles, as Recommended. folder?.let { DependencyDetector.detect(it) }?.forEach { rec -> out += Finding(rec.componentName, rec.reason, Origin.FOLDER, false, listOf(rec.reason, rec.kind.name)) } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/Cardboard.kt b/app/src/main/java/com/droiddeck/launcher/gpu/Cardboard.kt index b5ec1b544..6c2522576 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/Cardboard.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/Cardboard.kt @@ -4,20 +4,6 @@ import com.droiddeck.launcher.wayland.WaylandCompositor import org.json.JSONObject import kotlin.math.sqrt -/** - * Cardboard mode: the compositor draws the session as two lens-distorted eye views side by side - * (waylandcomp/src/cardboard.c), for a phone or handheld dropped into a Google Cardboard-style - * viewer. Units are the menu's own: - * - * - [ipdMm] distance between the eyes, millimetres - * - [screenWidthDm] the virtual screen's width, decimetres (40 = 4 m) - * - [screenDistanceDm] how far away it hangs, decimetres (30 = 3 m) - * - [viewer] a [Viewer] id: the lens it is drawn for - * - * [source] SCREEN shows the frame as a flat virtual screen, world-locked by the rotation sensor - * when [headTracking] is on (3DoF), with real stereo depth from the IPD. SBS takes a frame that is - * already a left/right pair (SteamVR, SBS 3D games and video) and only adds the lens. - */ data class Cardboard( val enabled: Boolean = false, val source: Int = SOURCE_SCREEN, @@ -29,12 +15,6 @@ data class Cardboard( ) { val lens: Viewer get() = Viewer.byId(viewer) - /** - * Hands the settings to the compositor. [panelWidthMm] x [panelHeightMm] is the physical size of - * the panel the eyes are drawn on (0 = unknown). The width decides how far each lens centre sits - * from its viewport's centre, so the two pictures line up with the viewer's lenses; the height, - * seen through the lens from its focal distance, is the field of view the distortion works in. - */ fun push(panelWidthMm: Float, panelHeightMm: Float) { val l = lens WaylandCompositor.nativeSetCardboard(enabled, source, ipdMm / 1000f, screenDistanceDm / 10f, @@ -45,18 +25,10 @@ data class Cardboard( .put("enabled", enabled).put("source", source).put("viewer", viewer).put("ipdMm", ipdMm) .put("screenWidthDm", screenWidthDm).put("screenDistanceDm", screenDistanceDm).put("headTracking", headTracking) - /** - * A lens the eyes can be drawn for: its distortion (Cardboard viewer profile coefficients, in - * tangent units at the panel), how far it sits from the panel, and how far apart the two are. - * [fallbackFovDeg] is the eye's field of view when the panel's size is unknown, or with no lens. - */ enum class Viewer(val id: String, val k1: Float, val k2: Float, val screenToLensMm: Float, val lensSeparationMm: Float, val chroma: Float, val fallbackFovDeg: Float) { - /** Google Cardboard (2015): the common plastic and cardboard viewers. */ CARDBOARD_V2("cardboard2", 0.34f, 0.55f, 39.3f, 63.9f, 0.006f, 90f), - /** The original 2014 Cardboard: narrower, gentler lenses. */ CARDBOARD_V1("cardboard1", 0.441f, 0.156f, 42f, 60f, 0.004f, 80f), - /** No lens: side-by-side 3D glasses, a 3D TV, or just looking at the stereo pair. */ NONE("none", 0f, 0f, 0f, 0f, 0f, 60f); companion object { @@ -80,21 +52,12 @@ data class Cardboard( headTracking = json.optBoolean("headTracking", true), ) - /** - * One eye's vertical field of view on the panel side of the lens: half the panel's height - * seen from the lens's focal distance. The lens then spreads it wider (the distortion). - */ fun fovDeg(viewer: Viewer, panelHeightMm: Float): Float { if (panelHeightMm <= 0f || viewer.screenToLensMm <= 0f) return viewer.fallbackFovDeg val half = Math.toDegrees(kotlin.math.atan((panelHeightMm / 2f / viewer.screenToLensMm).toDouble())).toFloat() return (2f * half).coerceIn(30f, 140f) } - /** - * The lens centre's inset towards the nose, as a fraction of one eye viewport's width. Each - * eye gets half the panel, so its viewport centre is a quarter of the panel from the middle; - * the lens centre is half the lens separation from it. 0 when either is unknown. - */ fun lensInset(panelWidthMm: Float, lensSeparationMm: Float): Float { if (panelWidthMm <= 0f || lensSeparationMm <= 0f) return 0f val inset = (panelWidthMm / 4f - lensSeparationMm / 2f) / (panelWidthMm / 2f) @@ -103,15 +66,9 @@ data class Cardboard( } } -/** - * Head pose maths for Cardboard mode, kept free of Android types so it is unit-tested on the JVM. - * Matrices are row-major 3x3 FloatArrays. "World" is Android's sensor world (x east, y north, - * z up); "screen" axes are x right, y up, z out of the panel towards the viewer's eyes. - */ object HeadPose { val IDENTITY = floatArrayOf(1f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f) - /** Unit quaternion (x, y, z, w) -> rotation matrix: what SensorManager.getRotationMatrixFromVector gives. */ fun fromQuaternion(x: Float, y: Float, z: Float, w: Float): FloatArray { val n = sqrt(x * x + y * y + z * z + w * w).takeIf { it > 1e-6f } ?: 1f val qx = x / n; val qy = y / n; val qz = z / n; val qw = w / n @@ -122,57 +79,33 @@ object HeadPose { ) } - /** The rotation vector sensor's values -> (x, y, z, w), computing w when the sensor leaves it out. */ fun quaternionFromRotationVector(v: FloatArray): FloatArray { val x = v[0]; val y = v[1]; val z = v[2] val w = if (v.size >= 4) v[3] else sqrt((1f - x * x - y * y - z * z).coerceAtLeast(0f)) return floatArrayOf(x, y, z, w) } - /** - * Device -> screen axes for a Display rotation (Surface.ROTATION_* as 0..3): the returned matrix - * maps screen to world, given the device's own device -> world matrix. The same transform as - * input/PadMotion's and SensorManager.remapCoordinateSystem's (ROTATION_90 = AXIS_Y, AXIS_MINUS_X: - * the device's x is the screen's y, its y the screen's -x). - */ fun toScreen(deviceToWorld: FloatArray, rotation: Int): FloatArray { - // Columns of A: where each screen axis points in device coordinates. val a = when (rotation and 3) { - 1 -> floatArrayOf(0f, 1f, 0f, -1f, 0f, 0f, 0f, 0f, 1f) // screen x = -device y, y = device x - 2 -> floatArrayOf(-1f, 0f, 0f, 0f, -1f, 0f, 0f, 0f, 1f) // screen x = -device x, y = -device y - 3 -> floatArrayOf(0f, -1f, 0f, 1f, 0f, 0f, 0f, 0f, 1f) // screen x = device y, y = -device x + 1 -> floatArrayOf(0f, 1f, 0f, -1f, 0f, 0f, 0f, 0f, 1f) + 2 -> floatArrayOf(-1f, 0f, 0f, 0f, -1f, 0f, 0f, 0f, 1f) + 3 -> floatArrayOf(0f, -1f, 0f, 1f, 0f, 0f, 0f, 0f, 1f) else -> IDENTITY } return multiply(deviceToWorld, a) } - /** sin(60 degrees): a view further from level than this is "held flat" for recentring. */ private const val FLAT_SIN = 0.866f - /** - * What a recentre locks to. The pose the compositor gets is basis^T * screenToWorld * tilt: - * - [basis]: a level frame (columns right, up, back in world) facing the recentred heading. Every - * rotation is measured in it, so turning about the real vertical always pans the view; - * - [tilt]: the device's own tilt at the recentre, taken out so that pose starts straight ahead. - * Identity when the device was held upright: the horizon then stays the true one. - */ class Reference(val basis: FloatArray, val tilt: FloatArray) - /** - * Recentre on the current orientation. Held upright (the screen within 60 degrees of vertical, as - * in a viewer), only the heading resets: forward is where the screen faces. Held flat (lying back, - * or the device on a table or in the hands like a tray), forward is where the top of the screen - * points and the tilt resets too; turning at the hips still pans, tipping the device still tilts. - */ fun recenter(screenToWorld: FloatArray): Reference { val fz = -screenToWorld[8] val flat = kotlin.math.abs(fz) > FLAT_SIN - // Upright: the screen's -z. Flat: the screen's +y (its top edge), which is level then. var fx = if (flat) screenToWorld[1] else -screenToWorld[2] var fy = if (flat) screenToWorld[4] else -screenToWorld[5] val len = sqrt(fx * fx + fy * fy).takeIf { it > 1e-4f } ?: run { fx = 0f; fy = 1f; 1f } fx /= len; fy /= len - // right = forward x up(0,0,1) = (fy, -fx, 0); up = (0,0,1); back = -forward. val basis = floatArrayOf( fy, 0f, -fx, -fx, 0f, -fy, @@ -182,7 +115,6 @@ object HeadPose { return Reference(basis, tilt) } - /** head -> recentred world: what the compositor takes. */ fun relative(ref: Reference, screenToWorld: FloatArray): FloatArray = multiply(multiply(transpose(ref.basis), screenToWorld), ref.tilt) @@ -193,7 +125,6 @@ object HeadPose { fun transpose(m: FloatArray): FloatArray = floatArrayOf(m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]) - /** m * v for a 3-vector. */ fun apply(m: FloatArray, v: FloatArray): FloatArray = floatArrayOf( m[0] * v[0] + m[1] * v[1] + m[2] * v[2], m[3] * v[0] + m[4] * v[1] + m[5] * v[2], diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt index 5e30ebb4a..48c58d01b 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt @@ -9,25 +9,17 @@ import com.droiddeck.launcher.session.SessionPrefs import java.io.File import java.util.Locale -/** - * Everything the device info window shows about the phone's chip: SoC, GPU, CPU, memory, system, - * the phone's own Vulkan driver and the drivers DroidDeck uses. Reads files and asks Vulkan once - * ([VulkanInfo]), so [collect] belongs off the main thread. Values the phone does not give are - * left out rather than guessed. - */ object DeviceInfo { class Section(val title: String, val rows: List<Pair<String, String>>) fun collect(context: Context): List<Section> { val vk = VulkanInfo.query() - // After Vulkan has been asked, detection can use its name too. val gpu = GpuInfo.detect() return listOf( chip(), gpu(gpu), cpu(), memory(context), system(), vulkan(context, vk), drivers(context, gpu), ).filter { it.rows.isNotEmpty() } } - /** The whole window as plain text, for a bug report. */ fun text(sections: List<Section>): String = buildString { append("DroidDeck device info\n") for (s in sections) { @@ -78,7 +70,6 @@ object DeviceInfo { private fun cpu() = Section("CPU", buildList<Pair<String, String>> { add("Cores", Runtime.getRuntime().availableProcessors().toString()) - // Each cpufreq policy is one cluster: the cores it covers and their speed range. val policies = File("/sys/devices/system/cpu/cpufreq").listFiles { f -> f.name.matches(Regex("policy\\d+")) } ?.sortedBy { it.name.removePrefix("policy").toInt() }.orEmpty() for (p in policies) { @@ -94,7 +85,6 @@ object DeviceInfo { private fun ghz(khz: Long) = String.format(Locale.US, "%.2f GHz", khz / 1e6) - /** The GPU's slowest and fastest step: KGSL's list, else a devfreq GPU node (Mali and others). */ private fun gpuRange(): String? { fun mhz(hz: Long) = "${hz / 1_000_000} MHz" val kgsl = read("/sys/class/kgsl/kgsl-3d0/gpu_available_frequencies") @@ -134,7 +124,6 @@ object DeviceInfo { add("API version", vk["api"]) add("Loader version", vk["loader"]) add("Driver", listOfNotNull(vk["driver_name"], vk["driver_version"]).joinToString(" ")) - // Qualcomm's build string starts with its own label ("Driver Build: 43c70540de, ..."). add("Driver build", vk["driver_info"]?.replace(Regex("(?i)^driver build:\\s*"), "")) add("Conformance", vk["conformance"]) add("Vendor / device id", listOfNotNull(vk["vendor_id"], vk["device_id"]).joinToString(" / ")) @@ -144,7 +133,6 @@ object DeviceInfo { val features = listOf("geometry_shader" to "Geometry shaders", "tessellation_shader" to "Tessellation", "texture_bc" to "BC textures") for ((key, label) in features) add(label, vk[key]) for ((key, value) in vk) if (key.startsWith("ext.")) add(key.removePrefix("ext."), value) - // What Android certifies the phone for. val pm = context.packageManager val hw = pm.systemAvailableFeatures.associate { it.name to it.version } hw[PackageManager.FEATURE_VULKAN_HARDWARE_VERSION]?.let { diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt index 2814e4bfb..db8dc86c4 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt @@ -11,37 +11,18 @@ import java.io.File import java.io.IOException import java.util.zip.ZipInputStream -/** - * An all-in-one driver package: one zip carrying both halves of a matched pair, the display - * (Android, bionic) driver in one directory and the runtime (Linux, glibc) driver in another, - * indexed by a root `manifest.json` - DroidDeck's own DD-Turnip releases (Droid-Deck/Drivers): - * - * ``` - * manifest.json {"package", "version", "drivers": {"android": {"path"}, "linux": {"path"}}} - * android/ meta.json (AdrenoTools) + libvulkan_freedreno.so - * linux/ meta.json + libvulkan_freedreno.so - * ``` - * - * Installing one puts each half through its own importer, with that importer's checks, and - * remembers the two as one bundle: picking either half sets both, and deleting either deletes both, - * so a bundle is always used whole. Zips of the older kind, one driver each, never reach this. - */ object DriverBundle { private const val TAG = "DriverBundle" private const val PREFS = "driver_bundles" private const val MANIFEST = "manifest.json" - /** Refuses a zip that unpacks to more than this: both halves together are about 30 MB. */ private const val MAX_BYTES = 512L shl 20 class Manifest(val name: String, val version: String, val tag: String, val androidPath: String, val linuxPath: String) - /** An installed bundle: [androidId] in TurnipDriver's list, [linuxId] in LinuxVulkanDriverManager's. */ class Bundle(val id: String, val name: String, val version: String, val androidId: String, val linuxId: String) { - /** "DD-Turnip 0.1.0". */ val label: String get() = if (version.isEmpty()) name else "$name $version" } - /** The manifest of a bundle, or null when [json] is not one (both halves, in two directories). */ fun parseManifest(json: JSONObject): Manifest? { val drivers = json.optJSONObject("drivers") ?: return null val android = cleanPath(drivers.optJSONObject("android")?.optString("path").orEmpty()) ?: return null @@ -58,7 +39,6 @@ object DriverBundle { return p.takeIf { it.isNotEmpty() && it.split('/').none { part -> part.isEmpty() || part == "." || part == ".." } } } - /** True when the zip at [uri] is a bundle rather than a single driver. */ fun isBundle(context: Context, uri: Uri): Boolean = try { context.contentResolver.openInputStream(uri)?.use { input -> ZipInputStream(input).use { zip -> @@ -71,11 +51,6 @@ object DriverBundle { false } - /** - * Install both halves of the bundle at [uri]. Both or neither: a half that is refused removes - * the one already in. Throws [IllegalArgumentException] with a user-facing reason when the zip - * is not a usable bundle, [IOException] on read failures. - */ fun install(context: Context, uri: Uri): Bundle { val work = File(context.cacheDir, "bundle-${System.nanoTime()}") val td = TurnipDriver(context) @@ -153,7 +128,6 @@ object DriverBundle { } ?: throw IOException("cannot open $uri") } - /** Every installed bundle whose two halves are both still there. */ fun all(context: Context): List<Bundle> { val td = TurnipDriver(context) val lm = LinuxVulkanDriverManager(context) @@ -167,24 +141,20 @@ object DriverBundle { fun get(context: Context, id: String): Bundle? = all(context).firstOrNull { it.id == id } - /** The bundle [driverId] is a half of, in the runtime list when [linux], else the display list. */ fun containing(context: Context, driverId: String, linux: Boolean): Bundle? = all(context).firstOrNull { (if (linux) it.linuxId else it.androidId) == driverId } - /** The bundle both drivers are set to right now, if they are one. */ fun active(context: Context): Bundle? { val android = SessionPrefs.androidDriver(context) val linux = SessionPrefs.linuxDriver(context) return all(context).firstOrNull { it.androidId == android && it.linuxId == linux } } - /** Set both halves as the drivers in use. */ fun select(context: Context, bundle: Bundle) { SessionPrefs.setAndroidDriver(context, bundle.androidId) SessionPrefs.setLinuxDriver(context, bundle.linuxId) } - /** Delete both halves; a driver still set to either goes back to its default. */ fun remove(context: Context, bundle: Bundle) { TurnipDriver(context).remove(bundle.androidId) LinuxVulkanDriverManager(context).removeDriver(bundle.linuxId) diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt index f3b28fe38..f737b4d4c 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt @@ -2,20 +2,6 @@ package com.droiddeck.launcher.gpu import com.droiddeck.launcher.gpu.GpuInfo.Family -/** - * The release drivers as matched pairs: a display build (the app's compositor, bionic) and a Linux - * build (the runtime, glibc) that were made to run together. Mixing a runtime driver carrying a - * patch with a display driver that lacks it is where the hard-to-read bugs come from, so the app - * installs and selects both halves of one pair at once. - * - * - Banners-Turnip publishes each variant as a set in every release (`Turnip-<tag><variant>.zip` - * and `...-Linux.zip`), rebuilt from upstream Mesa as it moves: the pair is one variant of the - * newest release that carries both halves. - * - WinNative ships its display (`v1.x`) and Linux (`linux-v0.x`) builds as two release lines with - * their own numbers: the pair is the newest of each, of one flavour (Balanced or Performance). - * - DroidDeck's DD-Turnip ships both halves in one zip per release (DriverBundle): the pair is the - * newest bundle, and it can never be mixed. - */ object DriverPairs { const val BANNER = "banner" const val BANNER_A8XX = "banner-a8xx" @@ -24,24 +10,18 @@ object DriverPairs { const val WN_BALANCED = "wn-b" const val WN_PERFORMANCE = "wn-p" const val DD_TURNIP = "dd-turnip" - /** Each DD-Turnip release is its own pair: "dd-turnip:DD-Turnip-v0.1.0". */ fun ddTurnip(tag: String) = "$DD_TURNIP:$tag" class DriverPair( val key: String, - /** "Banners-Turnip · Adreno 6xx/7xx", "WinNative · Balanced". */ val name: String, - /** The families the pair is built for; the list only offers it on those unless asked. */ val families: Set<Family>, val display: TurnipReleases.Asset?, val linux: TurnipReleases.Asset?, - /** Both halves in one zip, instead of [display] and [linux]. */ val bundle: TurnipReleases.Asset? = null, ) { val complete: Boolean get() = bundle != null || (display != null && linux != null) - /** The zips to download for the pair: the bundle, or the two halves. */ val assets: List<TurnipReleases.Asset> get() = listOfNotNull(bundle ?: display, if (bundle == null) linux else null) - /** "r4 · 1.19 + 0.1.2": the build of each half. */ val version: String get() = listOfNotNull(bundle?.tag, display?.tag, linux?.tag).distinct().joinToString(" + ") { it.removePrefix("linux-").removePrefix("DD-Turnip-") } fun suits(gpu: GpuInfo) = gpu.family in families || @@ -65,7 +45,6 @@ object DriverPairs { else -> key to ALL_ADRENO } - /** Every pair the last release check found, in the order the list shows them. */ fun from(check: TurnipReleases.Check?): List<DriverPair> { val assets = check?.assets.orEmpty().filter { it.pair.isNotEmpty() } val order = listOf(DD_TURNIP, BANNER, BANNER_A8XX, BANNER_ONEUI, BANNER_710, WN_BALANCED, WN_PERFORMANCE) @@ -75,9 +54,6 @@ object DriverPairs { val (name, families) = describe(key) val halves = assets.filter { it.pair == key } halves.firstOrNull { it.bundle }?.let { return@map DriverPair(key, name, families, null, null, it) } - // The list keeps the newest release of each half; for Banners-Turnip the two halves - // must come from the same release, so a set missing one half in its newest release - // is offered incomplete rather than mixed with an older one. val display = halves.firstOrNull { !it.linux } val linux = halves.firstOrNull { it.linux } val sameRelease = !key.startsWith(BANNER) || display?.tag == linux?.tag @@ -85,24 +61,15 @@ object DriverPairs { } } - /** - * The pair Auto sets for [gpu], among the [pairs] the last release check found: the newest - * official DD-Turnip release on the Adreno 840 (840 and 840v2), WinNative Balanced on the other - * 8xx (Banners' A8xx builds still break on some first boots) and on an 840 while no DD-Turnip - * release is listed, the OneUI build for an 8 Gen 2 under One UI, the patched test builds for - * the 710/720/722, and upstream Banners-Turnip for the rest. Null when nothing fits (not Adreno). - */ fun recommendedKey(gpu: GpuInfo, pairs: List<DriverPair>): String? = when (gpu.family) { Family.NOT_ADRENO -> null Family.A8XX -> if (gpu.model == 840) newestDdTurnip(pairs) ?: WN_BALANCED else WN_BALANCED Family.A7XX_LOW -> BANNER_710 Family.A7XX -> if (gpu.oneUi8Gen2) BANNER_ONEUI else BANNER Family.A6XX -> BANNER - // KGSL gave no model: Android 16 arrived with the 8xx, so a device that new is taken as one. Family.ADRENO_UNKNOWN -> if (android.os.Build.VERSION.SDK_INT >= 36) WN_BALANCED else BANNER } - /** The complete DD-Turnip pair with the highest version, or null when none is listed. */ fun newestDdTurnip(pairs: List<DriverPair>): String? = pairs .filter { it.key.startsWith("$DD_TURNIP:") && it.complete } .maxByOrNull { p -> diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java b/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java index 00aca26a5..0fb72fbac 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java @@ -5,7 +5,6 @@ import android.util.Log; import java.io.File; import java.io.FileWriter; -/** Setup breadcrumbs survive failures before a session log exists. No account data. */ public final class DriverSetupLog { private DriverSetupLog() {} @@ -13,7 +12,7 @@ public final class DriverSetupLog { Log.i("DriverSetup", stage); append(context.getFilesDir(), stage); try { append(context.getExternalFilesDir(null), stage); } - catch (Exception ignored) { /* External storage is optional. */ } + catch (Exception ignored) { } } private static void append(File directory, String stage) { @@ -21,10 +20,9 @@ public final class DriverSetupLog { try { directory.mkdirs(); File file = new File(directory, "driver-setup.txt"); - // Keep the latest run's breadcrumbs bounded across repeated attempts. try (FileWriter out = new FileWriter(file, file.length() < 131072)) { out.write(System.currentTimeMillis() + " " + stage + "\n"); } - } catch (Exception ignored) { /* Diagnostics must never prevent startup. */ } + } catch (Exception ignored) { } } } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/FrameGen.kt b/app/src/main/java/com/droiddeck/launcher/gpu/FrameGen.kt index b864e837b..952ada616 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/FrameGen.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/FrameGen.kt @@ -5,34 +5,20 @@ import android.util.Log import com.droiddeck.launcher.R import com.droiddeck.launcher.wayland.WaylandCompositor -/** - * The frame-generation setting and how it reaches the compositor. - * - * Both engines generate on gamescope's final output inside our compositor, so they work for any - * game with nothing changed in the runtime. Win-FG is our own and needs nothing; LSFG needs the - * shader chain from the user's copy of Lossless Scaling (see [Lossless]). Every compositor - * setter is a value store on the native side, so this can be applied before the compositor is - * up and changed live from the main screen while a session runs. - */ object FrameGen { const val ENGINE_OFF = "off" const val ENGINE_WINFG = "winfg" const val ENGINE_LSFG = "lsfg" - /** Win-FG's Performance preset, pinned: changing it live rebuilds the whole chain. */ private const val WINFG_PERF_PRESET = 2 - /** Bidirectional flow with the occlusion gate - the chain's own default. */ private const val WINFG_MODEL = 4 - /** Optical-flow resolution as a fraction of the output; Bannerlator's per-engine defaults. */ private const val FLOW_SCALE_WINFG = 0.60f private const val FLOW_SCALE_LSFG = 0.80f - /** LSFG's adaptive targets: it generates as many frames as it takes to reach one. */ val ADAPTIVE_TARGETS = listOf(120, 90, 60) private const val PREFS = "frame_gen" - /** A fixed [multiplier] (2..4), or for LSFG a [target] frame rate (0 = fixed). */ data class Mode(val engine: String, val multiplier: Int = 2, val target: Int = 0) { companion object { val OFF = Mode(ENGINE_OFF) @@ -54,7 +40,6 @@ object FrameGen { .putString("engine", mode.engine).putInt("multiplier", mode.multiplier).putInt("target", mode.target).apply() } - /** "Off", "Win-FG 2×", "LSFG 3×", "LSFG Adaptive 90". */ fun label(context: Context): String = label(context, mode(context)) fun label(context: Context, mode: Mode): String = when { @@ -64,11 +49,6 @@ object FrameGen { else -> context.getString(R.string.frame_gen_off) } - /** - * Pushes the saved setting into the compositor. Returns a message for the user when the - * engine cannot run (LSFG without Lossless Scaling), or null. Blocks while a new or updated - * Lossless.dll is prepared. - */ fun apply(context: Context, refreshHz: Float): String? { val mode = mode(context) when (mode.engine) { diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt index 87bf01d4d..d14fe8417 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt @@ -6,38 +6,23 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.core.FileUtils import java.io.File -/** - * What GPU this is, in the terms the driver lists are sorted by. KGSL names the model - * ("Adreno740v2", "Adreno825"); the family decides which Turnip builds run on it at all, and the - * support level is what the app has actually been tested on (Adreno 650, 725 and newer) - an - * Adreno 610 may start, but it is not "supported" just because it is a Qualcomm chip. - */ data class GpuInfo( - /** "Adreno 740", or the vendor's own name when this is not an Adreno. */ val name: String, - /** The three-digit Adreno model (740, 825), or 0 when KGSL does not say. */ val model: Int, val family: Family, - /** The SoC as a person would say it ("Snapdragon 8 Gen 2 (QCS8550)"), or "" when the phone does not say. */ val soc: String, - /** Samsung's One UI on an 8 Gen 2: its Turnip needs the OneUI build, or frames tear and flicker. */ val oneUi8Gen2: Boolean, - /** KGSL's own name for the GPU, as the kernel spells it ("Adreno740v2", "Adreno33v2"). */ val kgslName: String = "", - /** Where [model] came from: "kernel", "vulkan" or "platform", "" when unknown. */ val modelSource: String = "", ) { enum class Family(val label: String) { A8XX("Adreno 8xx"), - /** Adreno 710/720/722: gen 7 cores cut down enough to need their own patches. */ A7XX_LOW("Adreno 710/720/722"), A7XX("Adreno 7xx"), A6XX("Adreno 6xx"), - /** An Adreno whose model KGSL does not give: treated as the newest family it could be. */ ADRENO_UNKNOWN("Adreno"), NOT_ADRENO("Not an Adreno GPU"); - /** [label] in the app's language: the Adreno families are names, the rest is words. */ fun label(context: Context): String = if (this == NOT_ADRENO) context.getString(R.string.gpuinfo_not_adreno) else label } @@ -52,7 +37,6 @@ data class GpuInfo( else -> Support.UNTESTED } - /** One line for the device card and the system check, in English for the device report. */ val supportText: String get() = when (support) { Support.TESTED -> "Supported" @@ -61,30 +45,23 @@ data class GpuInfo( Support.UNSUPPORTED -> "Not supported: DroidDeck needs an Adreno (Snapdragon) GPU" } - /** [supportText] in the app's language. */ fun supportText(context: Context): String = context.getString(when (support) { Support.TESTED -> R.string.gpuinfo_supported Support.UNTESTED -> if (family == Family.A7XX_LOW) R.string.gpuinfo_experimental else R.string.gpuinfo_below_tested Support.UNSUPPORTED -> R.string.gpuinfo_unsupported }) - /** [name] for the screen: the stand-in for a GPU the device does not name is in the app's language. */ fun displayName(context: Context): String = if (name == UNNAMED) context.getString(R.string.gpu_this_gpu) else name companion object { - /** [name] when the device names no GPU; English, as the device report shows it. */ private const val UNNAMED = "this GPU" fun detect(): GpuInfo { val adreno = File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists() val raw = listOf("/sys/class/kgsl/kgsl-3d0/gpu_model", "/sys/class/kgsl/kgsl-3d0/gpu_chipid") .firstNotNullOfOrNull { FileUtils.readString(File(it))?.trim()?.takeIf(String::isNotEmpty) } - // Where vendors put the chip's model, named when it is known: "Snapdragon 8 Gen 2 (QCS8550)". val soc = if (adreno) SocNames.label() else "" val fromKernel = raw?.let { threeDigits(it) } ?: 0 - // Some kernels name the GPU without its model (AYANEO's Pocket FIT: "Adreno33v2"). The - // Vulkan driver's own name, when this process has asked it, then the platform's code - // name stand in - the GPU is the same on every phone of a platform. val fromVulkan = if (fromKernel > 0 || !adreno) 0 else VulkanInfo.cachedOrNull()?.get("device")?.let { threeDigits(it) } ?: 0 val fromPlatform = if (fromKernel > 0 || fromVulkan > 0 || !adreno) 0 else platformModel() val model = maxOf(fromKernel, fromVulkan, fromPlatform) @@ -106,7 +83,6 @@ data class GpuInfo( private fun threeDigits(text: String): Int = Regex("""(\d{3})""").find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0 - /** Qualcomm platform code names whose GPU is known for certain. */ private val PLATFORMS = mapOf( "msmnile" to 640, "kona" to 650, "lahaina" to 660, "taro" to 730, "cape" to 730, "kalama" to 740, "pineapple" to 750, "sun" to 830, @@ -115,7 +91,6 @@ data class GpuInfo( internal fun platformModel(platform: String = systemProperty("ro.board.platform")): Int = PLATFORMS[platform.lowercase()] ?: PLATFORMS[systemProperty("ro.vendor.qti.soc_name").lowercase()] ?: 0 - /** android.os.SystemProperties.get, which apps may read but not call directly. */ fun systemProperty(name: String): String = try { Class.forName("android.os.SystemProperties").getMethod("get", String::class.java).invoke(null, name) as? String ?: "" } catch (_: Exception) { diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/HeadTracker.kt b/app/src/main/java/com/droiddeck/launcher/gpu/HeadTracker.kt index 3841854e2..849c7870e 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/HeadTracker.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/HeadTracker.kt @@ -10,15 +10,6 @@ import android.os.HandlerThread import android.util.Log import com.droiddeck.launcher.wayland.WaylandCompositor -/** - * 3DoF head tracking for Cardboard mode: the rotation sensor -> the compositor's head pose - * ([HeadPose] for the maths). Game rotation vector first (gyro + accelerometer: no compass jumps - * indoors, and only the heading drifts, which a recentre fixes); the plain rotation vector where a - * device has no game one. Events arrive on a thread of their own and go straight to native code, - * so the session's UI thread never sits between the sensor and the screen. - * - * [rotation] is the Display rotation (Surface.ROTATION_*) the session is drawn in. - */ class HeadTracker(context: Context, private val rotation: () -> Int) : SensorEventListener { private val sensors = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager private val sensor: Sensor? = sensors.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR) @@ -28,7 +19,6 @@ class HeadTracker(context: Context, private val rotation: () -> Int) : SensorEve @Volatile private var recenterPending = true @Volatile private var latest: FloatArray? = null - /** Whether this device has a rotation sensor at all (no sensor = a fixed, head-locked screen). */ val available: Boolean get() = sensor != null val sensorName: String get() = sensor?.let { "${it.name} (${if (it.type == Sensor.TYPE_GAME_ROTATION_VECTOR) "game rotation vector" else "rotation vector"})" } ?: "none" @@ -54,7 +44,6 @@ class HeadTracker(context: Context, private val rotation: () -> Int) : SensorEve WaylandCompositor.nativeSetCardboardPose(HeadPose.IDENTITY, false) } - /** Make where the viewer looks now straight ahead (see HeadPose.recenter). */ fun recenter() { recenterPending = true latest?.let { apply(it) } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java index 4662f894f..d74b5a3f1 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java @@ -6,28 +6,8 @@ import android.util.Log; import java.util.ArrayList; import java.util.List; -/** - * The Vulkan driver a session draws with inside the runtime: in Steam mode the native client's - * own UI (OpenGL through the runtime's Zink), every game it launches (D3D through Proton's - * DXVK/VKD3D) and gamescope's compositing; in desktop mode every program on the desktop. One - * choice per mode covers all of it. - * - * <p>Distinct from the Android driver the app's compositor loads to put the finished frame on the - * screen ({@link TurnipDriver}), which every session ends in - and NOT interchangeable with it: - * these are glibc processes, that one is bionic. - * - * <p>"" (the default) means the driver the runtime was built with, at - * {@code usr/lib/libvulkan_freedreno.so} inside it - nothing is set and nothing is touched. - * Otherwise the value is an imported driver's id ({@link LinuxVulkanDriverManager}), and the - * session is handed that driver's ICD manifest in {@code BL_VK_DRIVER}: the session script points - * the Vulkan loader at it with {@code VK_DRIVER_FILES} after checking both the manifest and the - * library are readable from inside, so the runtime itself is never modified and the choice is - * reversible. Ported from Bannerlator's {@code LinuxVulkanDriver} (GPL-3.0); the choice lives in - * {@code SessionPrefs} per mode here rather than on a shortcut. - */ public final class LinuxVulkanDriver { private static final String TAG = "LinuxVulkanDriver"; - /** Env var the session reads; its value is an absolute ICD manifest path. */ public static final String ENV = "BL_VK_DRIVER"; public static final String HELP_TEXT = @@ -38,7 +18,6 @@ public final class LinuxVulkanDriver { private LinuxVulkanDriver() {} - /** Stored values in editor order: the runtime's own driver, then each imported one. */ public static List<String> optionValues(Context context) { ArrayList<String> values = new ArrayList<>(); values.add(""); @@ -46,10 +25,6 @@ public final class LinuxVulkanDriver { return values; } - /** - * Label for a stored value. An id that is no longer installed says so - the launch path falls - * back to the runtime's own driver for it. - */ public static String optionLabel(Context context, String value) { if (value == null || value.isEmpty()) return "Runtime default (built into the runtime)"; LinuxVulkanDriverManager m = new LinuxVulkanDriverManager(context); @@ -58,10 +33,6 @@ public final class LinuxVulkanDriver { return m.getDriverName(value) + (ver.isEmpty() ? "" : " " + ver) + " (imported)"; } - /** - * The ICD manifest path to hand the session, or null to leave the runtime's own driver alone. - * An id whose import is gone resolves to null rather than failing the launch. - */ public static String resolveIcdPath(Context context, String value) { if (value == null || value.isEmpty()) return null; LinuxVulkanDriverManager m = new LinuxVulkanDriverManager(context); diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java index e5c3d585c..3973297cd 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java @@ -22,30 +22,6 @@ import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -/** - * Imported LINUX Vulkan drivers: glibc Turnip ICDs ({@code Turnip-<tag>[-variant]-Linux.zip} from - * Banners-Turnip) for the runtime - the gamescope session that runs Valve's native ARM64 Steam - * client, and the KDE Plasma desktop beside it. This is the driver that DRAWS there: the client's own UI - * (OpenGL through the runtime's Zink), every game the client launches (D3D through Proton's - * DXVK/VKD3D) and everything on the desktop. Putting the frame on the screen stays the Android - * driver's job, in the app's compositor ({@link TurnipDriver}). - * - * <p>Not interchangeable with that one: the client and its games are glibc processes and cannot - * load a bionic object at all, and this one cannot be loaded by the app. The check that separates - * them is the libc the driver links against - {@code libc.so.6} here, {@code libc.so} there. - * - * <p>Layout (under the app's files dir, which the session sees by its full host path): - * <pre> - * files/linux_vulkan_drivers/<id>/ - * libvulkan_freedreno.so the driver - * icd.json generated Vulkan ICD manifest; library_path = the .so's ABSOLUTE path - * meta.json name / driverVersion / minGlibc, our own schema - * </pre> - * The session gets {@code icd.json}'s path in {@code BL_VK_DRIVER} and exports it as - * {@code VK_DRIVER_FILES}, so nothing inside the runtime is modified and the choice is reversible. - * - * <p>Ported from Bannerlator's {@code LinuxVulkanDriverManager} (GPL-3.0), unchanged in behaviour. - */ public class LinuxVulkanDriverManager { private static final String TAG = "LinuxVulkanDriver"; public static final String DIR_NAME = "linux_vulkan_drivers"; @@ -66,14 +42,12 @@ public class LinuxVulkanDriverManager { return new File(rootDir, id); } - /** Installed = has the .so AND the generated manifest (a half-extracted dir is never offered). */ public boolean isInstalled(String id) { if (id == null || id.isEmpty() || id.contains("/") || id.contains("..")) return false; File dir = getDriverDir(id); return new File(dir, LIB_NAME).isFile() && new File(dir, ICD_NAME).isFile(); } - /** Absolute path of the driver's ICD manifest, or null when the id isn't installed. */ public String getIcdPath(String id) { return isInstalled(id) ? new File(getDriverDir(id), ICD_NAME).getAbsolutePath() : null; } @@ -108,7 +82,6 @@ public class LinuxVulkanDriverManager { return m != null ? m.optString("driverVersion", "") : ""; } - /** The glibc the driver asks for, as its zip recorded it ("" when the zip did not say). */ public String getMinGlibc(String id) { JSONObject m = readMeta(id); return m != null ? m.optString("minGlibc", "") : ""; @@ -118,14 +91,8 @@ public class LinuxVulkanDriverManager { if (id == null || id.isEmpty() || id.contains("/") || id.contains("..")) return; Log.d(TAG, "removing imported Linux Vulkan driver " + id); FileUtils.delete(getDriverDir(id)); - // A mode still set to this id falls back to the runtime's own driver at launch (logged there). } - /** - * Import a {@code -Linux.zip}. Returns the new driver id. - * @throws IllegalArgumentException with a user-facing reason when the zip is not an importable - * Linux driver; {@link IOException} on read/extract failures. - */ public String installDriver(Uri zipUri, String displayName) throws IOException { File tmpDir = newStagingDir(); boolean keep = false; @@ -138,7 +105,6 @@ public class LinuxVulkanDriverManager { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { if (entry.isDirectory()) continue; - // Flatten: only the base name matters, and it also defeats zip-slip paths. String base = new File(entry.getName()).getName(); if (base.isEmpty()) continue; if (base.startsWith("libvulkan_freedreno") && base.endsWith(".so")) { @@ -154,8 +120,6 @@ public class LinuxVulkanDriverManager { Log.w(TAG, "meta.json unreadable, ignoring: " + e.getMessage()); } } - // The zip's own freedreno_icd.aarch64.json is dropped on purpose: its - // library_path is relative to itself, and adopt() writes an absolute one. } } String id = adopt(tmpDir, soName, zipMeta, displayName); @@ -166,7 +130,6 @@ public class LinuxVulkanDriverManager { } } - /** An empty directory beside the installed drivers, for a driver's files before {@link #adopt}. */ File newStagingDir() throws IOException { File tmpDir = new File(rootDir, ".tmp-" + System.nanoTime()); FileUtils.delete(tmpDir); @@ -174,11 +137,6 @@ public class LinuxVulkanDriverManager { return tmpDir; } - /** - * Check a staged driver ({@link #LIB_NAME} in {@code tmpDir}, copied from {@code soName} in - * the zip), write its manifest and meta.json, and move it in. Returns the id; the caller - * deletes {@code tmpDir} when this throws. - */ String adopt(File tmpDir, String soName, JSONObject zipMeta, String displayName) throws IOException { try { if (soName == null) { @@ -188,9 +146,6 @@ public class LinuxVulkanDriverManager { if (!isAarch64Elf(so)) { throw new IllegalArgumentException(context.getString(R.string.lvd_not_aarch64, soName)); } - // The one check that actually separates this from the bionic builds. Both sonames - // are in the driver's .dynstr, so a byte scan is enough and needs no ELF parsing: - // glibc's is versioned ("libc.so.6"), bionic's is not ("libc.so"). if (!containsAscii(so, "libc.so.6")) { throw new IllegalArgumentException(context.getString(R.string.lvd_not_glibc, soName)); } @@ -209,8 +164,6 @@ public class LinuxVulkanDriverManager { String id = uniqueId(sanitizeId(name)); File dir = getDriverDir(id); - // ICD manifest: absolute library_path, so the loader never depends on cwd and the guest - // reads it at the same path the app wrote (the files dir is bound into the session). JSONObject icd = new JSONObject(); icd.put("file_format_version", "1.0.0"); JSONObject icdBody = new JSONObject(); @@ -251,7 +204,6 @@ public class LinuxVulkanDriverManager { return s.isEmpty() ? "linux-driver" : s; } - /** ELF magic, EI_CLASS = 64-bit, EI_DATA = little-endian, e_machine = 0xB7 (AArch64). */ static boolean isAarch64Elf(File f) { try (FileInputStream in = new FileInputStream(f)) { byte[] h = new byte[20]; @@ -292,7 +244,6 @@ public class LinuxVulkanDriverManager { while (j < nb.length && buf[i + j] == nb[j]) j++; if (j == nb.length) return true; } - // Keep the last needle-1 bytes so a match across a buffer boundary is still found. carry = Math.min(nb.length - 1, len); System.arraycopy(buf, len - carry, buf, 0, carry); } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/Lossless.kt b/app/src/main/java/com/droiddeck/launcher/gpu/Lossless.kt index b07e5d4bb..375a60a57 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/Lossless.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/Lossless.kt @@ -10,16 +10,6 @@ import java.io.File import java.security.DigestOutputStream import java.security.MessageDigest -/** - * LSFG's shader chain, from the user's own Lossless Scaling (Steam app 993090). - * - * The copy the Steam client installs, in any library it installs to, is taken by - * itself and taken again whenever the client updates it. Once the session has seen the account - * own the app, a Lossless.dll can also be imported by hand; it stays until the Steam copy changes - * or another is imported. The DLL is copied in and its shaders are built into a cache named after - * its contents, so a different DLL hands the compositor a different path and its engine reloads. - * Everything here blocks: call it off the main thread. - */ object Lossless { enum class Source { NONE, STEAM, IMPORT } @@ -46,7 +36,6 @@ object Lossless { return State(owned(context), cacheFile(context)?.isFile == true, source) } - /** Recorded once: by the session's ownership check (a flag in the guest's home) or a Steam copy. */ fun owned(context: Context): Boolean { if (prefs(context).getBoolean(KEY_OWNED, false)) return true if (!ownedFlag(context).exists()) return false @@ -56,7 +45,6 @@ object Lossless { fun cacheFile(context: Context): File? = prefs(context).getString(KEY_ID, null)?.let { cacheFor(context, it) } - /** Takes a new or updated Steam copy and rebuilds a missing or outdated cache. A [LsfgNative] status. */ @Synchronized fun sync(context: Context): Int { val steam = steamDll(context) @@ -163,7 +151,6 @@ object Lossless { ?.forEach { it.delete() } } - /** The newest copy in any library the client installs to: its own, the card's, the Games folders. */ private fun steamDll(context: Context): File? { val libraries = listOfNotNull( File(LinuxRuntime.rootDir(context), "root/.local/share/Steam"), diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/LsfgNative.java b/app/src/main/java/com/droiddeck/launcher/gpu/LsfgNative.java index 6eb3c3441..d16a8c7a4 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/LsfgNative.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/LsfgNative.java @@ -1,16 +1,10 @@ package com.droiddeck.launcher.gpu; -/** - * The native half of LSFG's shader cache (lsfg_jni.cpp, compiled into the compositor library): - * {@code Lossless.dll} is mapped read-only and parsed as data, never loaded or executed, and its - * DXBC chain translated to SPIR-V. {@link Lossless} decides when; these calls block. - */ final class LsfgNative { static { System.loadLibrary("droiddeckwayland"); } private LsfgNative() {} - // Mirrors lsfg::DllStatus. static final int STATUS_OK = 0; static final int STATUS_NOT_INSTALLED = 1; static final int STATUS_UNREADABLE_FILE = 2; @@ -19,7 +13,6 @@ final class LsfgNative { static final int STATUS_TRANSLATION_FAILED = 5; static final int STATUS_CACHE_UNUSABLE = 6; - // Mirrors lsfg::Variant. static final int VARIANT_NONE = 0; static native int nativeValidateDll(String dllPath); diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt b/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt index 36d233f49..a7c7175c7 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt @@ -6,20 +6,6 @@ import com.droiddeck.launcher.wayland.WaylandCompositor import org.json.JSONObject import kotlin.math.abs -/** - * The compositor's post chain (waylandcomp/src/effects_chain.c) as the session menu drives it: - * the colour grade, the sharpening, the shader toggles and terminal debanding. Units are the - * menu's own, so a value goes straight from a row into the setter it drives: - * - * - [brightness] / [contrast] -100..100, 0 = neutral - * - [gamma] 0.5..3.0, 1.0 = neutral - * - [saturation] 0..200 percent, 100 = neutral - * - [casLevel] 0..100; the pass runs only while [cas] is on - * - [debandStrength] 0..200, 100 = one LSB - * - * Applies live: the compositor picks the change up on its next frame. While any pass is on, a - * zero-copy game goes through the compositor pass instead, which costs a little GPU time. - */ data class ScreenEffects( val brightness: Int = 0, val contrast: Int = 0, @@ -35,14 +21,11 @@ data class ScreenEffects( val crt: Boolean = false, val ntsc: Boolean = false, ) { - /** The sharpening level the picture gets: 0 while the pass is off, whatever its level is parked at. */ val effectiveCas: Int get() = if (cas) casLevel else 0 - /** Whether any pass runs, so the menu can say when the picture is untouched. */ val any: Boolean get() = brightness != 0 || contrast != 0 || abs(gamma - 1f) > 0.005f || saturation != 100 || effectiveCas > 0 || hdr || deband || fxaa || toon || crt || ntsc - /** Hands every pass to the compositor, with the Look's name for its log line. */ fun push(upscaler: Int) { WaylandCompositor.nativeSetCas(cas, casLevel) WaylandCompositor.nativeSetHdr(hdr) @@ -77,30 +60,19 @@ data class ScreenEffects( } } -/** - * One Look: a named set of values for the rows that already exist on the Display page. Picking - * a Look moves those rows; each stays adjustable afterwards, and the moment one moves the Look - * row reads Custom. [scalingMode] null leaves the scaling mode alone; otherwise it is a - * SessionPrefs.upscalerChoices mode the Look switches to. - */ data class Look( - /** The Look's id: what the row picks by and the compositor logs. */ val name: String, val effects: ScreenEffects, val scalingMode: Int? = null, - /** One line under the row while this Look is picked. */ val desc: String, - /** [name] and [desc] in the app's language, for the menu. */ @StringRes val label: Int, @StringRes val descRes: Int, ) object ScreenEffectLooks { - // Scaling modes as the compositor numbers them (WaylandCompositor.nativeSetUpscaler). private const val SCALING_NEAREST = 2 private const val SCALING_FSR = 4 - /** Index 0 is the neutral state a fresh session matches, so the row always names something. */ val LOOKS: List<Look> = listOf( Look("Off", ScreenEffects.OFF, desc = "Nothing applied.", label = R.string.frame_gen_off, descRes = R.string.look_off_desc), Look("Game Clarity", ScreenEffects(brightness = 2, contrast = 12, saturation = 108, cas = true, casLevel = 55), @@ -127,11 +99,6 @@ object ScreenEffectLooks { desc = "Toon outlines plus sharpening.", label = R.string.look_anime_edge, descRes = R.string.look_anime_edge_desc), ) - /** - * The Look the live values are, or null for Custom. A Look's sharpening is compared as the - * effective level, so a pass switched off matches a Look without one whatever its level is - * parked at; a Look with no scaling mode of its own matches under any. - */ fun match(e: ScreenEffects, upscaler: Int): Look? = LOOKS.firstOrNull { l -> val t = l.effects t.brightness == e.brightness && t.contrast == e.contrast && abs(t.gamma - e.gamma) < 0.005f && diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt b/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt index 76adb4bf2..39ad3f2ab 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt @@ -2,14 +2,7 @@ package com.droiddeck.launcher.gpu import android.os.Build -/** - * Which chip this is, as a person would say it. Handhelds often leave Android's SoC model blank - * (the AYANEO Pocket FIT does), so the model is looked for where vendors put it, the same order - * ClusterTune uses; then it is named from a table of models we are sure of, or from the platform - * code name as a family ("Snapdragon 8 Gen 3 family") when the model itself is a vendor variant. - */ object SocNames { - /** Model codes whose product name is certain. */ private val MODELS = mapOf( "SM8150" to "Snapdragon 855", "SM8250" to "Snapdragon 865", "SM8250-AC" to "Snapdragon 870", "SM8350" to "Snapdragon 888", "SM8450" to "Snapdragon 8 Gen 1", "SM8475" to "Snapdragon 8+ Gen 1", @@ -19,27 +12,22 @@ object SocNames { "SM6375" to "Snapdragon 695", ) - /** Platform code names: every chip of one platform shares its CPU and GPU design. */ private val PLATFORMS = mapOf( "msmnile" to "Snapdragon 855/860", "kona" to "Snapdragon 865/870", "lahaina" to "Snapdragon 888", "taro" to "Snapdragon 8 Gen 1", "cape" to "Snapdragon 8+ Gen 1", "kalama" to "Snapdragon 8 Gen 2", "pineapple" to "Snapdragon 8 Gen 3", "sun" to "Snapdragon 8 Elite", ) - /** The chip's model code ("QCS8550", "SG8350P"), or "" when the phone does not say. */ fun model(prop: (String) -> String = GpuInfo::systemProperty): String { val sdk = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it != Build.UNKNOWN }.orEmpty() else "" return listOf(prop("ro.soc.model"), prop("ro.vendor.qti.soc_model"), sdk, prop("ro.fota.platform")) .map { it.trim() } - // ro.fota.platform is often a branch name ("MSM_13.0"), not a chip. .firstOrNull { it.isNotEmpty() && !Regex("(?i)^msm_\\d").containsMatchIn(it) }.orEmpty() } - /** "Snapdragon 8 Gen 2", "Snapdragon 8 Gen 3 family", or null when neither the model nor the platform is known. */ fun name(model: String, platform: String = GpuInfo.systemProperty("ro.board.platform")): String? = MODELS[model.uppercase()] ?: PLATFORMS[platform.lowercase()]?.let { "$it family" } - /** For the GPU card: "Snapdragon 8 Gen 2 (QCS8550)", the bare model, or "". */ fun label(model: String = model(), platform: String = GpuInfo.systemProperty("ro.board.platform")): String { val name = name(model, platform) return when { diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/SteamVr.kt b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVr.kt index c592b2604..93d71262c 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/SteamVr.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVr.kt @@ -4,18 +4,10 @@ import android.content.Context import com.droiddeck.launcher.agent.AgentGuest import org.json.JSONObject -/** - * Cardboard mode's SteamVR (docs/cardboard-vr.md): Valve's ARM64 SteamVR in the guest, rendering - * through the DroidDeck driver as a side-by-side pair that the Cardboard pass puts the lens on. Driven - * through the guest agent's fixed "vr" request (droiddeck-steamvr), so it needs no agent commands. - * Every call blocks on the guest: run them off the main thread. - */ object SteamVr { enum class Status { - /** No session, no agent, or a runtime without the helper or the driver. */ UNAVAILABLE, NOT_INSTALLED, - /** Downloading or unpacking (571 MB). */ INSTALLING, READY, RUNNING, @@ -25,24 +17,17 @@ object SteamVr { parse(AgentGuest.call(context, JSONObject().put("kind", "vr").put("action", "status"), STATUS_TIMEOUT_MS)) }.getOrDefault(Status.UNAVAILABLE) - /** install, start or stop; true when the guest did it. */ fun run(context: Context, action: String): Boolean = runCatching { require(action in setOf("install", "start", "stop")) succeeded(action, AgentGuest.call(context, JSONObject().put("kind", "vr").put("action", action), ACTION_TIMEOUT_MS)) }.getOrDefault(false) - /** - * The agent answers ok whenever it ran the request, even when droiddeck-steamvr then failed; the - * helper's own exit code says whether start or stop worked. install runs detached and only reports - * that it started (its progress shows in status). - */ fun succeeded(action: String, answer: JSONObject): Boolean = when { !answer.optBoolean("ok", false) -> false action == "install" -> answer.optBoolean("started", false) else -> answer.optInt("exitCode", -1) == 0 } - /** The agent's status answer -> what the menu shows. */ fun parse(answer: JSONObject): Status = when { !answer.optBoolean("ok", false) -> Status.UNAVAILABLE answer.optInt("exitCode", 0) != 0 -> Status.UNAVAILABLE diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java index c125b531c..700ec81c3 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java @@ -24,43 +24,17 @@ import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -/** - * The Vulkan driver the in-app compositor runs on. It has to be Turnip: the system Adreno driver - * does not implement VK_EXT_image_drm_format_modifier, so importing the dma-bufs gamescope hands - * over fails and the session renders nothing. Two builds ship in the apk, one per Adreno - * generation, and the GPU decides which is unpacked - unless the user has imported an AdrenoTools - * zip of their own and chosen it, which then wins. - * - * <p>The guest's own Turnip is a different copy entirely - a glibc build inside the rootfs, or an - * imported one ({@link LinuxVulkanDriverManager}). This one is the bionic build the app process - * loads through adrenotools, and it is what every session ends in: whatever draws inside the - * runtime, this is the driver that puts the frame on the panel. - * - * <p>Every driver, bundled or imported, lives under {@code files/graphics_driver/<id>/} with a - * {@code meta.json} whose {@code libraryName} names the .so beside it - the AdrenoTools layout. - * The import path is ported from Bannerlator's {@code AdrenotoolsManager} (GPL-3.0), including - * the refusal of zips that belong in the Linux runtime list: a "-Linux" (glibc) Turnip names no - * library on purpose, and installed here it would put the compositor on the system Vulkan - a - * black screen wearing a driver's name. - */ public final class TurnipDriver { private static final String TAG = "TurnipDriver"; - /** Pinned A6xx/A7xx upstream build; used as the A6xx offline fallback. */ private static final String DRIVER_A6XX = "turnip26.3.0-a6xx"; - /** Adreno 7xx (8 Gen 2/3, e.g. the Pocket FIT's A750). */ private static final String DRIVER_A7XX = "turnip25.1.0"; - /** Adreno 8xx (8 Elite, e.g. a Fold 8's A830). */ private static final String DRIVER_A8XX = "turnip-sdk36"; - /** The two the apk carries; never removable, unpacked on demand. */ public static final List<String> BUNDLED = Collections.unmodifiableList(Arrays.asList( DRIVER_A7XX, DRIVER_A8XX, com.droiddeck.launcher.session.QuestExperimental.DRIVER, DRIVER_A6XX, com.droiddeck.launcher.session.QuestExperimental.DRIVER_A6XX)); - /** Overrides the pick, for a device we cannot reach: a7xx, a8xx or system. */ private static final String OVERRIDE_FILE = "Download/droiddeck-driver"; - /** Bundled drivers the user deleted: hidden from the list, their unpacked copy removed. */ private static final String PREFS = "graphics_driver"; private static final String KEY_HIDDEN = "hiddenBundled"; - /** Stored choice meaning "pick by GPU" - the default, and what a removed import falls back to. */ public static final String AUTO = ""; public static final String HELP_TEXT = @@ -78,7 +52,6 @@ public final class TurnipDriver { this.contentDir = new File(context.getFilesDir(), "graphics_driver"); } - /** Absolute directory holding the driver, with a trailing slash - adrenotools wants both. */ public String driverPath(String driverId) { return new File(contentDir, driverId).getAbsolutePath() + "/"; } @@ -88,7 +61,6 @@ public final class TurnipDriver { return meta != null ? meta.optString("libraryName", null) : null; } - /** meta.json's "name", or the id when it has none. */ public String displayName(String driverId) { JSONObject meta = readMeta(driverId); String name = meta != null ? meta.optString("name", "") : ""; @@ -113,17 +85,11 @@ public final class TurnipDriver { } } - /** An imported driver is installed when its meta.json names a library that is there. */ public boolean isInstalled(String driverId) { String library = libraryName(driverId); return library != null && !library.isEmpty() && new File(new File(contentDir, driverId), library).isFile(); } - /** - * The bundled drivers the list shows: {@link #BUNDLED} minus the ones the user deleted. A deleted - * one still lives in the apk and Auto still unpacks it on a GPU that needs it, so deleting can - * never leave a device without a driver; it only leaves the list. - */ public List<String> visibleBundled() { java.util.Set<String> hidden = hiddenBundled(); ArrayList<String> out = new ArrayList<>(); @@ -136,7 +102,6 @@ public final class TurnipDriver { .getStringSet(KEY_HIDDEN, java.util.Collections.<String>emptySet())); } - /** "Delete" for a bundled driver: remove its unpacked copy and hide it from the list. */ public void hideBundled(String driverId) { if (!BUNDLED.contains(driverId)) return; FileUtils.delete(new File(contentDir, driverId)); @@ -150,7 +115,6 @@ public final class TurnipDriver { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().remove(KEY_HIDDEN).apply(); } - /** Imported drivers only - the bundled two are listed by {@link #BUNDLED}. */ public List<String> enumerateImported() { ArrayList<String> ids = new ArrayList<>(); File[] dirs = contentDir.listFiles(); @@ -163,7 +127,6 @@ public final class TurnipDriver { return ids; } - /** The bundled build the GPU would pick on its own, for the "Auto" row's label. */ public String autoId() { String id = chooseByDevice(); return id == null ? "system" : id; @@ -177,12 +140,6 @@ public final class TurnipDriver { if (driverId.equals(SessionPrefs.androidDriver(context))) SessionPrefs.setAndroidDriver(context, AUTO); } - /** - * Import an AdrenoTools zip. Returns the new driver id. - * @throws IllegalArgumentException with a user-facing reason when the zip is not an AdrenoTools - * driver - including when it is a Linux runtime driver, which says which list it belongs in; - * {@link IOException} on read/extract failures. - */ public String installFromZip(Uri zipUri, String displayName) throws IOException { File tmpDir = newStagingDir(); boolean keep = false; @@ -193,8 +150,6 @@ public final class TurnipDriver { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { if (entry.isDirectory()) continue; - // Flatten: adrenotools reads the .so beside meta.json, and a base name also - // defeats zip-slip paths. String base = new File(entry.getName()).getName(); if (base.isEmpty()) continue; Files.copy(zis, new File(tmpDir, base).toPath(), StandardCopyOption.REPLACE_EXISTING); @@ -208,7 +163,6 @@ public final class TurnipDriver { } } - /** An empty directory beside the installed drivers, for a driver's files before {@link #adopt}. */ File newStagingDir() throws IOException { if (!contentDir.exists()) contentDir.mkdirs(); File tmpDir = new File(contentDir, ".tmp-" + System.nanoTime()); @@ -217,10 +171,6 @@ public final class TurnipDriver { return tmpDir; } - /** - * Check a staged driver (meta.json and its library, flat) and move it in as a new driver. - * Returns the id; the caller deletes {@code tmpDir} when this throws. - */ String adopt(File tmpDir, String displayName) throws IOException { String rejected = rejectionReason(tmpDir); if (rejected != null) throw new IllegalArgumentException(context.getString(R.string.turnip_not_adrenotools, rejected)); @@ -246,10 +196,6 @@ public final class TurnipDriver { } } - /** - * Why a just-extracted zip is not something adrenotools can be pointed at, or null when it is. - * The kinds refused by name are the other driver lists' zips, which carry a meta.json too. - */ private String rejectionReason(File dir) { File metaFile = new File(dir, "meta.json"); if (!metaFile.isFile()) return context.getString(R.string.turnip_reject_no_meta); @@ -272,7 +218,6 @@ public final class TurnipDriver { return context.getString(R.string.turnip_reject_library_missing, libraryName); if (!LinuxVulkanDriverManager.isAarch64Elf(library)) return context.getString(R.string.turnip_reject_not_aarch64, libraryName); - // A glibc build that happens to carry a libraryName would load nothing in this process. if (LinuxVulkanDriverManager.containsAscii(library, "libc.so.6")) return context.getString(R.string.turnip_reject_glibc, libraryName); return null; @@ -285,11 +230,6 @@ public final class TurnipDriver { return id; } - /** - * Unpacks the driver this device needs and returns its id, or null to fall back to the system - * Vulkan loader (which means a black session on Adreno, but is better than refusing to start - * on a GPU neither build covers). - */ public String install() { String id = choose(); DriverSetupLog.write(context, "install.begin driver=" + id); @@ -314,11 +254,6 @@ public final class TurnipDriver { return id; } - /** - * The user's choice first - an imported driver, or one of the bundled two pinned by hand - - * then the Downloads override, then the GPU. A chosen import that has since been removed logs - * and falls through, so a stale preference never blocks a session. - */ private String choose() { String chosen = SessionPrefs.androidDriver(context); if (chosen != null && !chosen.isEmpty()) { @@ -349,7 +284,6 @@ public final class TurnipDriver { String model = gpuModel(); Log.i(TAG, "gpu model: " + (model == null ? "unknown" : model)); if (model != null) { - // "Adreno750", "adreno_830" - the generation is the first digit of the three. java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d)\\d\\d").matcher(model); if (m.find()) { char generation = m.group(1).charAt(0); @@ -358,11 +292,9 @@ public final class TurnipDriver { if (generation == '6') return DRIVER_A6XX; } } - // Nothing to go on: Android 16 shipped with the 8 Elite, so treat a new device as 8xx. return android.os.Build.VERSION.SDK_INT >= 36 ? DRIVER_A8XX : DRIVER_A7XX; } - /** KGSL names the GPU here, and this file is world-readable where /dev/kgsl-3d0 is not. */ private static String gpuModel() { for (String path : new String[]{ "/sys/class/kgsl/kgsl-3d0/gpu_model", diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt index acc941928..2523afb20 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt @@ -12,41 +12,20 @@ import java.io.IOException import java.net.HttpURLConnection import java.net.URL -/** - * Driver downloads from the Turnip release repos, offered in both driver menus. Checked only when - * the user taps refresh - the result is remembered, so the menu shows the last check without going - * online. A download goes through the same importer as a zip picked by hand, so it gets the same - * checks and lands as an ordinary imported driver. - * - * Each repo's recent releases are read newest first and every driver variant is offered from the - * newest release that carries it: a repo that stops publishing one variant (Banners-Turnip's A8xx - * Linux build after v26.3.0-20260923) still offers its last one, and a repo with two release lines - * (WinNative's Android `v1.x` and Linux `linux-v0.x`) offers the newest of each. - */ object TurnipReleases { private const val PREFS = "turnip_releases" private const val KEY_RELEASE = "latest" private const val KEY_DOWNLOADS = "downloads" - /** [label] names the variant for the menu: the GPUs it is for, and a build flavour. */ - /** [sha256] is the release asset's digest from GitHub; empty for a list cached before it was kept. */ - /** - * [pair] names the matched set an asset belongs to (DriverPairs): a display build and a Linux - * build of the same pair are made to run together. "" for a list cached before pairs were kept. - */ - /** [bundle]: one zip holding both halves of the pair (DriverBundle); [linux] is false for it. */ class Asset(val source: String, val tag: String, val name: String, val url: String, val size: Long, val linux: Boolean, val label: String, val sha256: String = "", val pair: String = "", val bundle: Boolean = false) - /** [latest] = each source's newest release tag, for the refresh line. */ class Check(val assets: List<Asset>, val latest: List<Pair<String, String>>, val failed: List<String>, val checkedAt: Long) - /** What an asset is: a Linux build or not, the menu label, and the pair it belongs to. */ private class Kind(val linux: Boolean, val label: String, val pair: String, val bundle: Boolean = false) private class Source(val label: String, val repo: String, val classify: (name: String, tag: String) -> Kind?) private val SOURCES = listOf( - // Turnip-<tag>[-variant][-Linux|-Wayland].zip; -Wayland is for a path this app does not have. Source("Banners-Turnip", "The412Banner/Banners-Turnip") { name, tag -> val prefix = "Turnip-$tag" if (!name.startsWith(prefix) || !name.endsWith(".zip")) return@Source null @@ -63,32 +42,23 @@ object TurnipReleases { } Kind(linux, label, "banner" + variant.lowercase()) }, - // WN-Turnip-<ver>-<b|p>_Axxx.zip (Android) and WN-Linux-Turnip-<ver>-<b|p>_Axxx.zip (Linux): - // b = Balanced, p = Performance, Axxx = every Adreno. Source("WinNative", "WinNative-Emu/Drivers") { name, _ -> val m = Regex("""^WN-(Linux-)?Turnip-[^-]+-([a-z]+)_(\w+)\.zip$""").find(name) ?: return@Source null val flavour = when (m.groupValues[2]) { "b" -> "Balanced"; "p" -> "Performance"; else -> m.groupValues[2] } val gpus = if (m.groupValues[3] == "Axxx") "all Adreno" else m.groupValues[3] Kind(m.groupValues[1].isNotEmpty(), "$gpus · $flavour", "wn-" + m.groupValues[2]) }, - // DD-Turnip-v<ver>.zip: DroidDeck's own all-in-one build, both halves in one zip, published - // to Drivers-CI (releases only). Every release is offered, not just the newest, each its own pair. Source("DroidDeck", "Droid-Deck/Drivers-CI") { name, tag -> if (!Regex("""^DD-Turnip-v\d+\.\d+\.\d+\.zip$""").matches(name)) return@Source null Kind(false, "Android + Linux · all Adreno", DriverPairs.ddTurnip(tag), bundle = true) }, ) - /** The result of the last check, or null when the user has never checked. */ fun cached(context: Context): Check? { val raw = prefs(context).getString(KEY_RELEASE, null) ?: return null return runCatching { parse(JSONObject(raw)) }.getOrNull() } - /** - * Read every source now and remember the result. A source that fails is named in [Check.failed] - * and the others still count; throws only when every source failed. - */ fun refresh(context: Context): Check { val assets = JSONArray() val latest = JSONArray() @@ -109,13 +79,10 @@ object TurnipReleases { val a = list.getJSONObject(j) val name = a.getString("name") val kind = src.classify(name, tag) ?: continue - // A bundle is only offered from an official release, never a pre-release. if (kind.bundle && r.optBoolean("prerelease")) continue val linux = kind.linux val label = kind.label - // Only assets GitHub has a sha256 for are offered: the download is checked against it. val sha = Hashes.githubSha256(a.optString("digest")) ?: continue - // Newest first: the first release carrying a variant is the one offered. if (!seen.add(if (kind.bundle) kind.pair else "$linux|$label")) continue assets.put(JSONObject().put("source", src.label).put("tag", tag).put("name", name) .put("url", a.getString("browser_download_url")).put("size", a.optLong("size")) @@ -170,11 +137,9 @@ object TurnipReleases { return Check(out, latest, failed, json.optLong("checkedAt")) } - /** The id an asset was installed as, when it was downloaded before and is still installed. */ fun installedId(context: Context, asset: Asset, isInstalled: (String) -> Boolean): String? = downloads(context).optString(asset.name, "").takeIf { it.isNotEmpty() && isInstalled(it) } - /** True when [id] came from a release download rather than a zip picked by hand. */ fun isDownloaded(context: Context, id: String): Boolean { val d = downloads(context) return d.keys().asSequence().any { d.optString(it) == id } @@ -184,7 +149,6 @@ object TurnipReleases { prefs(context).edit().putString(KEY_DOWNLOADS, downloads(context).put(asset.name, id).toString()).apply() } - /** Forget a deleted driver, so its release offers the download again. */ fun forget(context: Context, id: String) { val d = downloads(context) val keys = d.keys().asSequence().filter { d.optString(it) == id }.toList() @@ -193,7 +157,6 @@ object TurnipReleases { prefs(context).edit().putString(KEY_DOWNLOADS, d.toString()).apply() } - /** Download an asset into the cache; the caller imports it and deletes the file. */ fun download(context: Context, asset: Asset, progress: (Int) -> Unit): File { if (asset.sha256.isEmpty()) throw IOException(context.getString(R.string.turnip_list_predates_checksums)) val target = File(context.cacheDir, asset.name) diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt index ee4ca1a69..d03e6a5c6 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt @@ -2,17 +2,11 @@ package com.droiddeck.launcher.gpu import android.util.Log -/** - * The phone's own Vulkan driver, as it describes itself (native/deviceinfo): device name, API and - * driver versions, a few limits and the extensions DXVK/VKD3D lean on. Asked once per process and - * kept; the first [query] creates a Vulkan instance, so it belongs off the main thread. - */ object VulkanInfo { @Volatile private var cached: Map<String, String>? = null @JvmStatic private external fun nativeQuery(): String - /** Blocking on first use. Empty-ish map with an "error" key when Vulkan cannot be asked. */ @Synchronized fun query(): Map<String, String> { cached?.let { return it } @@ -31,6 +25,5 @@ object VulkanInfo { return map } - /** What [query] found, if it has run in this process; never blocks. */ fun cachedOrNull(): Map<String, String>? = cached } diff --git a/app/src/main/java/com/droiddeck/launcher/input/ControllerPrefs.kt b/app/src/main/java/com/droiddeck/launcher/input/ControllerPrefs.kt index 739f83d3f..09b282d8a 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/ControllerPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/ControllerPrefs.kt @@ -123,7 +123,6 @@ object ControllerPrefs { prefs(context).edit().putBoolean("keyboardButton", on).apply() } - /** The physical Select button opens the Steam menu instead of View/Select. */ fun setSelectSteam(context: Context, on: Boolean) { prefs(context).edit().putBoolean("selectSteam", on).apply() } @@ -151,7 +150,6 @@ object ControllerPrefs { } fun resetAll(context: Context) { - // On older Android versions clear() does not notify listeners; restore rumble explicitly. prefs(context).edit().clear().putBoolean("rumble", true).apply() } diff --git a/app/src/main/java/com/droiddeck/launcher/input/DeckControls.kt b/app/src/main/java/com/droiddeck/launcher/input/DeckControls.kt index 23146be37..a8c499793 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/DeckControls.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/DeckControls.kt @@ -1,15 +1,6 @@ package com.droiddeck.launcher.input -/** - * The Steam Deck controller's back grips and trackpads, which a handheld's own pad does not have: - * the second screen's Deck controls (DeckControlsPanel) drive them, and each change goes into the - * pad's ring ([FakeInputWriter.writeDeckControls]) for libfakeinput's Deck report. - * - * Trackpad positions are -1..1 each way with y up, as on a Deck; a pad touched reports its - * position, a pad clicked also reports full pressure. - */ object DeckControls { - /** Bits of the ring's controls word - MUST match DECK_EXTRA_* in fakeinput_steam.cpp. */ const val L4 = 1 const val R4 = 2 const val L5 = 4 @@ -19,7 +10,6 @@ object DeckControls { private const val LEFT_CLICK = 64 private const val RIGHT_CLICK = 128 - /** The pad the Deck controller is made from (PadBridge's slot). */ private const val SLOT = 0 private var controls = 0 @@ -32,7 +22,6 @@ object DeckControls { publish() } - /** A finger on (or off) one trackpad at x, y in -1..1, y up. */ @Synchronized fun setPad(right: Boolean, touching: Boolean, x: Float = 0f, y: Float = 0f) { val at = if (right) 2 else 0 @@ -49,7 +38,6 @@ object DeckControls { publish() } - /** Everything let go - for when the panel goes away with a finger still on it. */ @Synchronized fun releaseAll() { if (controls == 0 && pads.all { it.toInt() == 0 } && pressure.all { it.toInt() == 0 }) return diff --git a/app/src/main/java/com/droiddeck/launcher/input/EvdevKeys.java b/app/src/main/java/com/droiddeck/launcher/input/EvdevKeys.java index 23aae64be..48f9ea30d 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/EvdevKeys.java +++ b/app/src/main/java/com/droiddeck/launcher/input/EvdevKeys.java @@ -2,21 +2,11 @@ package com.droiddeck.launcher.input; import android.view.KeyEvent; -/** - * Android key codes to Linux evdev codes, for the keys we inject into the compositor's - * wl_keyboard. A hardware keyboard is how an account name and password are typed on the client's - * first run, so this matters before any game does. - * - * <p>evdev's order is not alphabetical: the numbers are the physical positions of a PC keyboard, - * which is what the compositor's xkb keymap is written against. Table taken from Bannerlator's - * Wayland path unchanged. - */ public final class EvdevKeys { private EvdevKeys() {} public static int fromKeyCode(int kc) { switch (kc) { - // Letters (evdev order is NOT alphabetical) case KeyEvent.KEYCODE_A: return 30; case KeyEvent.KEYCODE_B: return 48; case KeyEvent.KEYCODE_C: return 46; case KeyEvent.KEYCODE_D: return 32; case KeyEvent.KEYCODE_E: return 18; case KeyEvent.KEYCODE_F: return 33; @@ -30,36 +20,30 @@ public final class EvdevKeys { case KeyEvent.KEYCODE_U: return 22; case KeyEvent.KEYCODE_V: return 47; case KeyEvent.KEYCODE_W: return 17; case KeyEvent.KEYCODE_X: return 45; case KeyEvent.KEYCODE_Y: return 21; case KeyEvent.KEYCODE_Z: return 44; - // Digit row case KeyEvent.KEYCODE_1: return 2; case KeyEvent.KEYCODE_2: return 3; case KeyEvent.KEYCODE_3: return 4; case KeyEvent.KEYCODE_4: return 5; case KeyEvent.KEYCODE_5: return 6; case KeyEvent.KEYCODE_6: return 7; case KeyEvent.KEYCODE_7: return 8; case KeyEvent.KEYCODE_8: return 9; case KeyEvent.KEYCODE_9: return 10; case KeyEvent.KEYCODE_0: return 11; - // Whitespace / edit case KeyEvent.KEYCODE_ENTER: return 28; case KeyEvent.KEYCODE_NUMPAD_ENTER: return 28; case KeyEvent.KEYCODE_SPACE: return 57; case KeyEvent.KEYCODE_TAB: return 15; - case KeyEvent.KEYCODE_DEL: return 14; /* backspace */ + case KeyEvent.KEYCODE_DEL: return 14; case KeyEvent.KEYCODE_FORWARD_DEL: return 111; case KeyEvent.KEYCODE_ESCAPE: return 1; - // Modifiers case KeyEvent.KEYCODE_SHIFT_LEFT: return 42; case KeyEvent.KEYCODE_SHIFT_RIGHT: return 54; case KeyEvent.KEYCODE_CTRL_LEFT: return 29; case KeyEvent.KEYCODE_CTRL_RIGHT: return 97; case KeyEvent.KEYCODE_ALT_LEFT: return 56; case KeyEvent.KEYCODE_ALT_RIGHT: return 100; case KeyEvent.KEYCODE_CAPS_LOCK: return 58; - // Arrows / nav case KeyEvent.KEYCODE_DPAD_UP: return 103; case KeyEvent.KEYCODE_DPAD_DOWN: return 108; case KeyEvent.KEYCODE_DPAD_LEFT: return 105; case KeyEvent.KEYCODE_DPAD_RIGHT: return 106; case KeyEvent.KEYCODE_MOVE_HOME: return 102; case KeyEvent.KEYCODE_MOVE_END: return 107; case KeyEvent.KEYCODE_PAGE_UP: return 104; case KeyEvent.KEYCODE_PAGE_DOWN: return 109; case KeyEvent.KEYCODE_INSERT: return 110; - // Punctuation case KeyEvent.KEYCODE_GRAVE: return 41; case KeyEvent.KEYCODE_MINUS: return 12; case KeyEvent.KEYCODE_EQUALS: return 13; case KeyEvent.KEYCODE_LEFT_BRACKET: return 26; case KeyEvent.KEYCODE_RIGHT_BRACKET: return 27; case KeyEvent.KEYCODE_BACKSLASH: return 43; case KeyEvent.KEYCODE_SEMICOLON: return 39; case KeyEvent.KEYCODE_APOSTROPHE: return 40; case KeyEvent.KEYCODE_SLASH: return 53; case KeyEvent.KEYCODE_COMMA: return 51; case KeyEvent.KEYCODE_PERIOD: return 52; - // Function row case KeyEvent.KEYCODE_F1: return 59; case KeyEvent.KEYCODE_F2: return 60; case KeyEvent.KEYCODE_F3: return 61; case KeyEvent.KEYCODE_F4: return 62; case KeyEvent.KEYCODE_F5: return 63; case KeyEvent.KEYCODE_F6: return 64; @@ -70,23 +54,15 @@ public final class EvdevKeys { } } - /** - * The character a key carries when Shift is not held, for a character that needs it - so '@' - * gives '2' and 'A' gives 'a'. Returns 0 when the character is typed without Shift, which is - * also the answer for anything this layout does not place on a key. US layout, which is what - * the session's keymap is. - */ public static int unshiftedChar(int ch) { if (ch >= 'A' && ch <= 'Z') return Character.toLowerCase(ch); int at = SHIFTED_CHARS.indexOf(ch); return at >= 0 ? PLAIN_CHARS.charAt(at) : 0; } - // Index-aligned: the symbol, and the key it shares with Shift held. private static final String SHIFTED_CHARS = "!@#$%^&*()_+{}|:\"<>?~"; private static final String PLAIN_CHARS = "1234567890-=[]\\;',./`"; - /** The Android key code that carries an unshifted character, or 0 when nothing does. */ public static int keycodeForChar(int ch) { if (ch >= 'a' && ch <= 'z') return KeyEvent.KEYCODE_A + (ch - 'a'); if (ch >= '0' && ch <= '9') return KeyEvent.KEYCODE_0 + (ch - '0'); diff --git a/app/src/main/java/com/droiddeck/launcher/input/FakeInputRingPaths.java b/app/src/main/java/com/droiddeck/launcher/input/FakeInputRingPaths.java index 05e5fc306..d16aa0b53 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/FakeInputRingPaths.java +++ b/app/src/main/java/com/droiddeck/launcher/input/FakeInputRingPaths.java @@ -2,16 +2,11 @@ package com.droiddeck.launcher.input; import java.io.File; -/** - * Where the fake input rings live and how the guest is told about them. Kept apart from - * {@link FakeInputWriter}, which loads native code, so it can be tested on the JVM. - */ final class FakeInputRingPaths { static final String RING_DIR_NAME = "fakeinput-rings"; private FakeInputRingPaths() {} - /** The rings sit beside the fake evdev nodes: session/dev/input -> session/dev/fakeinput-rings. */ static File ringDir(File fakeInputDir) { if (fakeInputDir == null) { return null; @@ -26,13 +21,6 @@ final class FakeInputRingPaths { return ringDir != null ? new File(ringDir, "ring" + slot) : null; } - /** - * The ring's path as the guest opens it. The session tree is bound into the guest at its own - * host path, spelled the way {@code Context.getFilesDir()} spells it (LinuxRuntime.binds), so - * the export has to use that same spelling. A canonical path is not that: where - * /data/user/0 is a link, or a separate mount, onto /data/data (ColorOS 16, #166) it resolves - * to a prefix nothing is bound at, and every pad, physical and on-screen, goes dead. - */ static String exportPath(File ringFile) { return ringFile.getAbsolutePath(); } diff --git a/app/src/main/java/com/droiddeck/launcher/input/FakeInputWriter.java b/app/src/main/java/com/droiddeck/launcher/input/FakeInputWriter.java index ed4a88d27..f698f481d 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/FakeInputWriter.java +++ b/app/src/main/java/com/droiddeck/launcher/input/FakeInputWriter.java @@ -9,33 +9,17 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; -/** - * Fake evdev transport for the emulated Xbox 360 pads. - * - * <p>Replaces the old append-file transport (which desynced after ~15 min when an - * unlooped {@code channel.write} left a partial event in the file and the - * IOException was swallowed) with a fixed-size mmap RING BUFFER shared with the - * native reader in {@code fakeinput.cpp}. Delta events are published into the - * ring under a monotonic {@code write_seq}; an authoritative absolute-state - * snapshot is published alongside under a seqlock so the native reader can replay - * a full keyframe to heal any desync (open, ring overflow) without duplicate - * input. Ring layout (magic/version/offsets) is a paired ABI with the native - * {@code FakeInputRingHeader} - keep the two in lockstep. - */ public class FakeInputWriter { private static final String TAG = "FakeInputWriter"; - // Event types public static final short EV_SYN = 0x00; public static final short EV_KEY = 0x01; public static final short EV_ABS = 0x03; public static final short EV_MSC = 0x04; - // Event codes public static final short MSC_SCAN = 0x04; public static final short SYN_REPORT = 0x00; - // Xbox 360 controller button codes public static final short BTN_A = 0x130; public static final short BTN_B = 0x131; public static final short BTN_X = 0x133; @@ -48,7 +32,6 @@ public class FakeInputWriter { public static final short BTN_MODE = 0x13C; public static final short BTN_THUMBR = 0x13E; - // Absolute axis codes public static final short ABS_X = 0x00; public static final short ABS_Y = 0x01; public static final short ABS_RX = 0x03; @@ -58,17 +41,12 @@ public class FakeInputWriter { public static final short ABS_HAT0X = 0x10; public static final short ABS_HAT0Y = 0x11; - // Button mapping (bit i of the snapshot buttons word) private static final short[] BUTTON_MAP = { BTN_A, BTN_B, BTN_X, BTN_Y, BTN_TL, BTN_TR, BTN_SELECT, BTN_START, BTN_THUMBL, BTN_THUMBR, BTN_MODE }; - /** Bit 10 of the snapshot word. The first ten are PadState's own bit order; the Steam - * button is not, because PadState keeps 10 and 11 for the triggers. */ private static final int SNAPSHOT_IDX_MODE = 10; - /** Bit 11: the Quick Access button. Snapshot only - an Xbox pad's evdev node has no such key; - * libfakeinput's Deck controller reads it from here. */ private static final int SNAPSHOT_IDX_QAM = 11; private static final int EVENT_SIZE = 24; @@ -76,11 +54,10 @@ public class FakeInputWriter { private static final int BUFFER_SIZE = EVENT_SIZE * MAX_EVENTS_PER_UPDATE; private static final int MAX_FAKE_INPUT_SLOTS = 4; - // Ring layout - MUST match FakeInputRingHeader in fakeinput.cpp. private static final int RING_CAPACITY_EVENTS = 4096; private static final int RING_HEADER_SIZE = 64; private static final int RING_SIZE = RING_HEADER_SIZE + (RING_CAPACITY_EVENTS * EVENT_SIZE); - private static final int RING_MAGIC = 0x46494252; // FIBR + private static final int RING_MAGIC = 0x46494252; private static final int RING_VERSION = 2; private static final int RING_MAGIC_OFFSET = 0; private static final int RING_VERSION_OFFSET = 4; @@ -88,26 +65,18 @@ public class FakeInputWriter { private static final int RING_CAPACITY_OFFSET = 12; private static final int RING_WRITE_SEQ_OFFSET = 16; private static final int RING_GENERATION_OFFSET = 24; - // Authoritative absolute-state snapshot the native reader replays as a full - // keyframe to heal any delta-stream desync. Written under a seqlock - // (RING_SNAPSHOT_SEQ_OFFSET: odd = write in progress). private static final int RING_SNAPSHOT_SEQ_OFFSET = 32; private static final int RING_SNAPSHOT_BUTTONS_OFFSET = 40; - private static final int RING_SNAPSHOT_AXES_OFFSET = 44; // short[8] - // After the events - MUST match the blocks in fakeinput_steam.cpp. DeckImu carries the - // Deck-only motion, grips and trackpads. TritonShared carries the paired Steam Controller's - // native BLE reports and output reports, so the Steam client can see it as its own device. + private static final int RING_SNAPSHOT_AXES_OFFSET = 44; private static final int IMU_OFFSET = RING_SIZE; - private static final int IMU_MAGIC = 0x31554D49; // IMU1 - // Written by libfakeinput, not here: the client's SETTING_IMU_MODE | 0x80000000 (readImuMode). + private static final int IMU_MAGIC = 0x31554D49; private static final int IMU_MODE_OFFSET = IMU_OFFSET + 4; private static final int IMU_SEQ_OFFSET = IMU_OFFSET + 8; - private static final int IMU_ACCEL_OFFSET = IMU_OFFSET + 16; // short[3] - private static final int IMU_GYRO_OFFSET = IMU_OFFSET + 22; // short[3] - private static final int DECK_PADS_OFFSET = IMU_OFFSET + 28; // short[4]: left X, Y, right X, Y - private static final int DECK_PRESSURE_OFFSET = IMU_OFFSET + 36; // short[2]: left, right - private static final int DECK_CONTROLS_OFFSET = IMU_OFFSET + 40; // int: DeckControls bits - // Slot 0 only: what the pad is to the Steam client (libfakeinput's PAD_TYPE_*), | PAD_TYPE_SET. + private static final int IMU_ACCEL_OFFSET = IMU_OFFSET + 16; + private static final int IMU_GYRO_OFFSET = IMU_OFFSET + 22; + private static final int DECK_PADS_OFFSET = IMU_OFFSET + 28; + private static final int DECK_PRESSURE_OFFSET = IMU_OFFSET + 36; + private static final int DECK_CONTROLS_OFFSET = IMU_OFFSET + 40; private static final int PAD_TYPE_OFFSET = IMU_OFFSET + 44; private static final int PAD_TYPE_SET = 0x80000000; public static final int PAD_TYPE_DECK = 0; @@ -115,13 +84,10 @@ public class FakeInputWriter { public static final int PAD_TYPE_XBOX = 2; public static final int PAD_TYPE_DS4 = 3; public static final int PAD_TYPE_DUALSENSE = 4; - /** The type chosen this process, written into a ring made after the choice too; -1 for none. */ private static volatile int padType = -1; - /** The paired controller's state, likewise written into a ring made after it connected. */ private static volatile boolean tritonConnected = false; private static final int TRITON_OFFSET = RING_SIZE + 64; - private static final int TRITON_MAGIC = 0x4e525454; // TTRN - // Whether the paired controller is connected: libfakeinput offers its device only then. + private static final int TRITON_MAGIC = 0x4e525454; private static final int TRITON_CONNECTED_OFFSET = TRITON_OFFSET + 4; private static final int TRITON_INPUT_SEQ_OFFSET = TRITON_OFFSET + 8; private static final int TRITON_REPORT_ID_OFFSET = TRITON_OFFSET + 16; @@ -131,7 +97,6 @@ public class FakeInputWriter { private static final int TRITON_OUTPUT_FEATURE_OFFSET = TRITON_OFFSET + 96; private static final int TRITON_OUTPUT_LENGTH_OFFSET = TRITON_OFFSET + 100; private static final int TRITON_OUTPUT_OFFSET = TRITON_OFFSET + 104; - // A feature read: libfakeinput bumps the request, the reply names the request it answers. private static final int TRITON_GET_REQUEST_OFFSET = TRITON_OFFSET + 168; private static final int TRITON_GET_REPLY_OFFSET = TRITON_OFFSET + 176; private static final int TRITON_GET_REPORT_ID_OFFSET = TRITON_OFFSET + 184; @@ -163,9 +128,6 @@ public class FakeInputWriter { private int prevTriggerL, prevTriggerR; private int prevHatX, prevHatY; private boolean hasChanges = false; - // If a publish fails after prev* state has already advanced, the next frame - // must re-emit the whole state once; otherwise an unchanged control would stay - // silent and the ring snapshot would never catch up. private boolean pendingFullResend = false; private boolean forceResend = false; @@ -186,7 +148,6 @@ public class FakeInputWriter { boolean everActivated; } - // ---- Static ring management (shared by all writers + the env exporter) ---- public static void prepareRingSlots(File fakeInputDir, int slotCount) { int boundedSlotCount = Math.max(0, Math.min(slotCount, MAX_FAKE_INPUT_SLOTS)); @@ -197,10 +158,6 @@ public class FakeInputWriter { } } - /** - * Prepares every slot ring and returns the {@code slot=path;...} spec the native - * reader consumes via FAKE_EVDEV_MEMFD_PATHS. - */ public static String getRingEnv(File fakeInputDir) { synchronized (RING_LOCK) { prepareRingSlotsLocked(fakeInputDir, MAX_FAKE_INPUT_SLOTS); @@ -269,8 +226,6 @@ public class FakeInputWriter { data.putLong(TRITON_GET_REPLY_OFFSET, 0L); } - // Lock order: RING_LOCK, then the slot. Every writer holds the slot while it touches data, and - // re-checks it there: a writer that looked the slot up just before this ran finds it gone. private static void releaseRingSlotLocked(int slot) { RingSlot ringSlot = RING_SLOTS[slot]; if (ringSlot == null) { @@ -433,8 +388,6 @@ public class FakeInputWriter { return false; } - // Raw byte copy: endianness is irrelevant since we never interpret - // multi-byte fields out of the source here. ByteBuffer source = this.buffer.duplicate(); synchronized (ringSlot) { ByteBuffer ring = ringSlot.data; @@ -446,19 +399,12 @@ public class FakeInputWriter { while (source.remaining() >= EVENT_SIZE) { int eventIndex = (int) (writeSeq % RING_CAPACITY_EVENTS); int targetOffset = RING_HEADER_SIZE + (eventIndex * EVENT_SIZE); - // Bulk-copy one event (EVENT_SIZE bytes) instead of byte-by-byte. - // Bound the source to a single event, position the ring at the slot, - // then let put(ByteBuffer) move the whole block. Mutating the ring's - // Java position is harmless: the native reader uses a raw pointer, not - // this position. API-26 safe (no JDK 13+ absolute bulk put). source.limit(source.position() + EVENT_SIZE); ring.position(targetOffset); ring.put(source); source.limit(sourceLimit); writeSeq++; } - // Publish the resulting absolute state. prev* now hold the post-update - // values, i.e. exactly the state the events just written transition to. writeSnapshotLocked(ring); nativeStoreFence(); ring.putLong(RING_WRITE_SEQ_OFFSET, writeSeq); @@ -466,9 +412,6 @@ public class FakeInputWriter { return true; } - // Publishes the full absolute controller state for the native reader to replay - // as a keyframe. seqlock: bump to odd, write fields, bump to even, with - // store-store fences so the reader's acquire loads see consistent payloads. private void writeSnapshotLocked(ByteBuffer ring) { int buttons = 0; for (int i = 0; i < BUTTON_MAP.length; i++) { @@ -480,11 +423,9 @@ public class FakeInputWriter { buttons |= (1 << SNAPSHOT_IDX_QAM); } long seq = ring.getLong(RING_SNAPSHOT_SEQ_OFFSET); - ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 1); // odd: write in progress + ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 1); nativeStoreFence(); ring.putInt(RING_SNAPSHOT_BUTTONS_OFFSET, buttons); - // Axis order must match the native kSnapshotAxisCodes: - // X, Y, RX, RY, GAS(=triggerR), BRAKE(=triggerL), HAT0X, HAT0Y. ring.putShort(RING_SNAPSHOT_AXES_OFFSET, clampShort(this.prevThumbLX)); ring.putShort(RING_SNAPSHOT_AXES_OFFSET + 2, clampShort(this.prevThumbLY)); ring.putShort(RING_SNAPSHOT_AXES_OFFSET + 4, clampShort(this.prevThumbRX)); @@ -494,13 +435,9 @@ public class FakeInputWriter { ring.putShort(RING_SNAPSHOT_AXES_OFFSET + 12, clampShort(this.prevHatX)); ring.putShort(RING_SNAPSHOT_AXES_OFFSET + 14, clampShort(this.prevHatY)); nativeStoreFence(); - ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 2); // even: write complete + ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 2); } - /** - * Publishes the pad's motion for slot {@code slot}: accelerometer and gyro, already in the Deck - * controller's axes and units (see PadMotion). Safe from any thread. - */ public static void writeMotion(int slot, short[] accel, short[] gyro) { RingSlot ringSlot; synchronized (RING_LOCK) { @@ -511,7 +448,7 @@ public class FakeInputWriter { ByteBuffer ring = ringSlot.data; if (ring == null) return; long seq = ring.getLong(IMU_SEQ_OFFSET); - ring.putLong(IMU_SEQ_OFFSET, seq + 1); // odd: write in progress + ring.putLong(IMU_SEQ_OFFSET, seq + 1); nativeStoreFence(); for (int i = 0; i < 3; i++) { ring.putShort(IMU_ACCEL_OFFSET + i * 2, accel[i]); @@ -523,10 +460,6 @@ public class FakeInputWriter { } } - /** - * The motion the Steam client asked the Deck controller of slot {@code slot} for: its - * SETTING_IMU_MODE (0 = off), or -1 until it has said. Safe from any thread. - */ public static int readImuMode(int slot) { RingSlot ringSlot; synchronized (RING_LOCK) { @@ -540,11 +473,6 @@ public class FakeInputWriter { } } - /** - * Publishes the Deck's back grips and trackpads for slot {@code slot} (see DeckControls): - * {@code controls} is the DeckControls bit set, {@code pads} left X, Y, right X, Y and - * {@code pressure} left, right, already in the Deck's units. Safe from any thread. - */ public static void writeDeckControls(int slot, int controls, short[] pads, short[] pressure) { RingSlot ringSlot; synchronized (RING_LOCK) { @@ -566,20 +494,11 @@ public class FakeInputWriter { } } - /** - * Whether the pad, as the Steam client now sees it, has a Quick Access button: a Deck's or a - * Steam Controller's does, an Xbox or PlayStation pad's does not (libfakeinput drops the bit). - * Unset is the Deck, libfakeinput's default. - */ public static boolean padHasQam() { int type = padType; return type < 0 || type == PAD_TYPE_DECK || type == PAD_TYPE_STEAM; } - /** - * Switches what the pad is to the Steam client while the session runs ([PAD_TYPE_DECK], - * [PAD_TYPE_STEAM] or [PAD_TYPE_XBOX]); libfakeinput unplugs the old device and plugs in the new. - */ public static void setPadType(int type) { padType = type; RingSlot ringSlot; @@ -595,7 +514,6 @@ public class FakeInputWriter { } } - /** The paired Steam Controller came or went (SteamControllerBle): its device is plugged in or out. */ public static void setTritonConnected(int slot, boolean connected) { tritonConnected = connected; RingSlot ringSlot = tritonSlot(slot); @@ -608,7 +526,6 @@ public class FakeInputWriter { } } - /** Publishes a native Triton input report (without the HID report ID byte). */ public static void writeTritonState(int slot, int reportId, byte[] report) { if (report == null || report.length == 0 || report.length > TRITON_REPORT_MAX) return; RingSlot ringSlot; @@ -633,7 +550,6 @@ public class FakeInputWriter { } } - /** Returns the newest Steam-client output report after {@code lastSequence}, or null. */ public static byte[] readTritonOutput(int slot, long lastSequence) { RingSlot ringSlot; synchronized (RING_LOCK) { @@ -657,7 +573,6 @@ public class FakeInputWriter { } } - /** The Steam client's newest unanswered feature read, or 0: its request number. */ public static long readTritonFeatureRequest(int slot) { RingSlot ringSlot = tritonSlot(slot); if (ringSlot == null) return 0L; @@ -670,7 +585,6 @@ public class FakeInputWriter { } } - /** Answers feature read {@code request} with the controller's report, or a failure when null. */ public static void writeTritonFeatureReply(int slot, long request, byte[] report) { RingSlot ringSlot = tritonSlot(slot); if (ringSlot == null) return; @@ -748,11 +662,6 @@ public class FakeInputWriter { isOpen = false; } - /** - * Reset all input state to neutral (all buttons released, axes zeroed). - * This keeps the ring active so games that cache the file descriptor can still - * reconnect. - */ public synchronized void reset() { if (!isOpen && !open()) return; @@ -760,7 +669,6 @@ public class FakeInputWriter { buffer.clear(); hasChanges = false; - // Release all buttons for (int i = 0; i < BUTTON_MAP.length; i++) { if (prevButtonStates[i]) { prevButtonStates[i] = false; @@ -774,7 +682,6 @@ public class FakeInputWriter { hasChanges = true; } - // Zero all axes if (prevThumbLX != 0) { prevThumbLX = 0; writeEvent(EV_ABS, ABS_X, 0); @@ -823,9 +730,6 @@ public class FakeInputWriter { Log.i(TAG, "Soft released fake input: " + eventFile.getAbsolutePath()); } - /** - * Full destroy - reset, tear the ring generation down, and delete the discovery node. - */ public synchronized void destroy() { destroyed = true; reset(); @@ -861,9 +765,6 @@ public class FakeInputWriter { if (!isOpen && !open()) return; - // Keep the ring delta-first for latency. Full event frames are only used as - // a one-shot repair after a failed publish; normal open/overflow recovery is - // handled by the native snapshot keyframe. forceResend = pendingFullResend; pendingFullResend = false; if (forceResend) { @@ -873,13 +774,9 @@ public class FakeInputWriter { buffer.clear(); hasChanges = false; - // Buttons for (int i = 0; i < 10; i++) { writeButton(i, state.isDown(i)); } - // The Steam button. In a Linux session this is what opens the client's own in-game menu, - // and the client looks for it where an Xbox pad keeps it: evdev BTN_MODE, which SDL then - // reports as button 8 - exactly the "guide:b8" in the mapping Steam writes for this pad. writeButton(SNAPSHOT_IDX_MODE, state.isDown(PadState.GUIDE)); boolean qam = state.isDown(PadState.QAM); @@ -888,14 +785,11 @@ public class FakeInputWriter { hasChanges = true; } - // Sticks int lx = (int) (state.leftX * 32767); int ly = (int) (state.leftY * 32767); int rx = (int) (state.rightX * 32767); int ry = (int) (state.rightY * 32767); - // The fake evdev ring is event-queue semantics, so unchanged axes normally - // stay silent; forceResend overrides that to emit a complete keyframe. if (forceResend || lx != prevThumbLX) { prevThumbLX = lx; writeEvent(EV_ABS, ABS_X, lx); @@ -913,7 +807,6 @@ public class FakeInputWriter { writeEvent(EV_ABS, ABS_RY, ry); } - // L2 and R2 (Triggers) int tl = (int) (state.leftTrigger * 255); int tr = (int) (state.rightTrigger * 255); if (forceResend || tl != prevTriggerL) { @@ -925,7 +818,6 @@ public class FakeInputWriter { writeEvent(EV_ABS, ABS_GAS, tr); } - // D-pad int hatX = state.hatX(); int hatY = state.hatY(); if (forceResend || hatX != prevHatX) { @@ -937,13 +829,11 @@ public class FakeInputWriter { writeEvent(EV_ABS, ABS_HAT0Y, hatY); } - // Detect change else no need to write if (hasChanges) { writeEvent(EV_SYN, SYN_REPORT, 0); buffer.flip(); if (!flushBuffer()) { Log.e(TAG, "Gamepad write error: fake input mmap ring unavailable"); - // Couldn't publish; re-assert the whole state on the next frame. pendingFullResend = true; } } diff --git a/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt b/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt index b18279647..494533f43 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt @@ -13,17 +13,6 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import android.view.inputmethod.InputMethodManager -/** - * A place for the soft keyboard to land. Steam's UI and the games under it are X11 clients of - * gamescope and know nothing of Wayland text input, so everything the keyboard produces has to - * become key events: what the IME commits as text is turned into the key presses that would have - * typed it (Shift included, from the key character map), and handed to the activity, whose key - * path turns them into evdev codes for the compositor. Backspace, Enter and the keys an IME sends - * directly go the same way. - * - * Invisible and unfocusable until asked for: a focused View would take D-pad and stick motion - * away from the pad. - */ class KeyboardHost( context: Context, private val sendKeyEvent: (KeyEvent) -> Boolean, @@ -38,7 +27,7 @@ class KeyboardHost( init { isFocusable = false isFocusableInTouchMode = false - visibility = VISIBLE // present in the tree, draws nothing + visibility = VISIBLE layoutParams = android.widget.FrameLayout.LayoutParams(1, 1) } @@ -46,8 +35,6 @@ class KeyboardHost( override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? { if (!shown) return null - // No suggestions and no autocorrect: the keyboard sends what was pressed, key by key, which - // is what a password field on the other side needs. outAttrs.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE or EditorInfo.IME_FLAG_NO_FULLSCREEN or @@ -80,9 +67,6 @@ class KeyboardHost( private inner class KeyConnection : BaseInputConnection(this, false) { override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean { if (text.isNullOrEmpty()) return true - // The key events that type this text on a US layout - a capital arrives as Shift - // down, the letter, Shift up. Characters the map cannot place are sent as a bare - // key event carrying the character, which the activity's key path works back from. val events = map.getEvents(text.toString().toCharArray()) if (events != null) { for (event in events) sendKeyEvent(event) @@ -125,7 +109,6 @@ class KeyboardHost( override fun getSelectedText(flags: Int): CharSequence? = null } - /** A key event that carries a character the key map could not place. */ private class CharKeyEvent(origin: KeyEvent, private val ch: Char) : KeyEvent(origin) { override fun getUnicodeChar(): Int = ch.code override fun getUnicodeChar(metaState: Int): Int = ch.code diff --git a/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt b/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt index 8c4de8858..d7ac2259f 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt @@ -183,7 +183,6 @@ class OnScreenControls( relayout() } - /** Hide the full touch pad for an unobstructed screen while keeping its quick toggle available. */ fun setQuickHidden(hidden: Boolean) { if (buttonsOnly || editing || quickHidden == hidden) return if (hidden) releaseAll() diff --git a/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java b/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java index 78822422e..6348f58de 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java +++ b/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java @@ -13,27 +13,8 @@ import java.io.File; import java.util.Arrays; import java.util.concurrent.atomic.AtomicIntegerArray; -/** - * A physical controller, republished as a synthetic evdev device the Steam client can see. - * - * <p>Android hands a gamepad to the foreground activity as key and motion events; the Steam client - * inside the runtime never sees that. What it does scan is {@code /dev/input}, which the session's - * interposer (libfakeinput.so) serves out of the shared-memory rings written here. So the path is: - * Android event → {@link PadState} → {@link FakeInputWriter} ring → interposer → evdev node → - * SDL → Big Picture. - * - * <p>The identity the interposer reports is an Xbox 360 pad, which is what makes SDL apply a known - * mapping without the user configuring anything. In a Steam session it is a Steam Deck controller - * to the client instead ({@link com.droiddeck.launcher.session.SteamDeckPad}), which has a Quick - * Access button of its own; an Xbox pad has none, so there the client's menu is reached with the - * Guide-then-A chord the client itself answers. - * - * <p>Each further controller is another player: slots 1 to 3, an Xbox 360 pad each, which in a - * Steam session only the client reads (libfakeinput) and hands games through Steam Input. - */ public final class PadBridge { private static final String TAG = "PadBridge"; - /** Player slots: the session prepares a ring for each (SessionService). */ public static final int SLOTS = 4; private static final float DEAD_ZONE = 0.12f; private static final int[] JOYSTICK_AXES = { @@ -48,18 +29,11 @@ public final class PadBridge { KeyEvent.KEYCODE_BUTTON_THUMBL, KeyEvent.KEYCODE_BUTTON_THUMBR, KeyEvent.KEYCODE_BUTTON_MODE, KeyEvent.KEYCODE_BUTTON_L2, KeyEvent.KEYCODE_BUTTON_R2 }; - // The client takes A as part of the chord only once it has had Guide held for a while, and a - // client starved of CPU needs longer. Too short, and it acts on A as well, selecting whatever it - // had focused before opening QAM: with 80 ms of lead that was 3 times in 20 at rest, and 250 ms - // none in 20; with the session down to 1 fps a fixed 400 ms still let 3 in 15 through, where - // 1000 ms let none. The lead keeps 80 ms when frames are quick, for a QAM that feels immediate, - // and stretches to eight frames when they are slow. private static final long QAM_GUIDE_LEAD_MIN_MS = 80; private static final long QAM_GUIDE_LEAD_MAX_MS = 1500; private static final int QAM_GUIDE_LEAD_FRAMES = 8; private static final long QAM_A_HOLD_MS = 200; private static final long QAM_GUIDE_TAIL_MS = 200; - // Off the main thread, so a busy UI cannot shorten or stretch the steps. private static final Handler chordHandler; static { android.os.HandlerThread thread = new android.os.HandlerThread("qam-chord", android.os.Process.THREAD_PRIORITY_DISPLAY); @@ -70,7 +44,6 @@ public final class PadBridge { private final File fakeInputDir; private final FakeInputWriter writer; private final PadState state = new PadState(); - /** The input device id holding each slot, or {@link #NO_CONTROLLER}. */ private final int[] owners = new int[SLOTS]; private final FakeInputWriter[] playerWriters = new FakeInputWriter[SLOTS]; private final PadState[] playerStates = new PadState[SLOTS]; @@ -82,16 +55,11 @@ public final class PadBridge { private boolean systemQamPressed; private boolean qamChordActive; private boolean qamSyntheticAPressed; - /** The Deck controller's Quick Access button, held for one tap. */ private boolean qamTapPressed; private int qamChordGeneration; - /** Told on the main thread when a player uses the pad or the on-screen controls; see [setOnPlayerInput]. */ private volatile Runnable onPlayerInput; private final java.util.concurrent.atomic.AtomicBoolean playerInputPosted = new java.util.concurrent.atomic.AtomicBoolean(); - // What reached the pad over the last stats window, so a report of "the pad does nothing" or - // "touch and the pad don't work together" shows in the app log whether input arrived, from - // which device, and whether it got to the ring. Logged only for a window that had input. private static final long STATS_WINDOW_MS = 10_000; private int statButtons, statAxes, statOnScreen, statDropped; private String statDevice; @@ -99,14 +67,8 @@ public final class PadBridge { private final java.util.Set<Integer> seenDevices = new java.util.HashSet<>(); private int lastDeviceId = Integer.MIN_VALUE; - /** No physical controller is driving the pad: nothing yet, or the on-screen controls were last. */ public static final int NO_CONTROLLER = -1; - /** - * The Android input device id of the physical controller that last drove any slot, or - * {@link #NO_CONTROLLER}: where rumble goes when it names no slot of ours (RumbleComponent). - */ private static volatile int activeControllerId = NO_CONTROLLER; - /** Per slot, the controller its rumble belongs to; slot 0's is none while the on-screen controls lead. */ private static final AtomicIntegerArray slotControllers = new AtomicIntegerArray(SLOTS); static { for (int slot = 0; slot < SLOTS; slot++) slotControllers.set(slot, NO_CONTROLLER); @@ -114,7 +76,6 @@ public final class PadBridge { public static int activeControllerId() { return activeControllerId; } - /** The controller playing in this ring slot, or the last one used for any other slot. */ public static int controllerForSlot(int slot) { return slot >= 0 && slot < SLOTS ? slotControllers.get(slot) : activeControllerId; } @@ -126,12 +87,10 @@ public final class PadBridge { for (int slot = 1; slot < SLOTS; slot++) playerStates[slot] = new PadState(); } - /** The physical Select button opens the Steam menu instead of View/Select (Setup → Controller). */ public void setSelectSteam(boolean on) { selectSteam = on; } - /** Opens the ring; safe to call more than once. */ public synchronized boolean start() { if (!open) { open = writer.open(); @@ -165,19 +124,13 @@ public final class PadBridge { } } - /** True when the device this event came from is a gamepad or joystick, not the touchscreen. */ public static boolean isFromController(InputDevice device) { if (device == null) return false; - // A virtual device is the system's own synthetic input, not a pad somebody is holding - - // counting it would hide the on-screen controls with nothing to replace them. if (device.isVirtual()) return false; int sources = device.getSources(); if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) return true; if ((sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) return false; if (device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC) return true; - // Some composite keyboards advertise joystick input without any sticks or pad buttons. - // A keyboard's mouse ranges do not establish a joystick; a real hybrid's joystick ranges - // or physical controller buttons do. GAMEPAD above remains authoritative even with keys. for (int axis : JOYSTICK_AXES) { InputDevice.MotionRange range = device.getMotionRange(axis, InputDevice.SOURCE_JOYSTICK); if (range != null && range.getMax() > range.getMin()) return true; @@ -188,11 +141,9 @@ public final class PadBridge { return false; } - /** Whether this key belongs to the pad, rather than a keyboard on the same input device. */ public static boolean isControllerKeyEvent(KeyEvent event) { InputDevice device = event.getDevice(); if (device == null || device.isVirtual()) return false; - // An explicit pad button is unambiguous even when a hybrid omits its capabilities. if (KeyEvent.isGamepadButton(event.getKeyCode())) { return device.supportsSource(InputDevice.SOURCE_GAMEPAD) || device.supportsSource(InputDevice.SOURCE_JOYSTICK); @@ -206,9 +157,6 @@ public final class PadBridge { case KeyEvent.KEYCODE_MENU: case KeyEvent.KEYCODE_HOME: if (!isFromController(device)) return false; - // Without GAMEPAD, an alphabetic hybrid's keyboard arrows remain keyboard keys. - // Its ambiguous navigation keys need explicit pad/joystick event provenance; - // DPAD alone also describes ordinary keyboard arrows. return device.supportsSource(InputDevice.SOURCE_GAMEPAD) || device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC || event.isFromSource(InputDevice.SOURCE_GAMEPAD) @@ -217,17 +165,13 @@ public final class PadBridge { } } - /** Mouse motion on a composite controller still belongs to the pointer, not its sticks. */ public static boolean isControllerMotionEvent(MotionEvent event) { InputDevice device = event.getDevice(); - // An actual joystick event, like a pad button, survives a hybrid's missing capabilities. return event.isFromSource(InputDevice.SOURCE_JOYSTICK) && device != null && !device.isVirtual() && (device.supportsSource(InputDevice.SOURCE_GAMEPAD) || device.supportsSource(InputDevice.SOURCE_JOYSTICK)); } - /** A Bluetooth keyboard can share a device with a joystick. Classify the key, not just - * the device, so its letters and modifiers still reach the desktop keyboard. */ public static boolean isControllerKey(KeyEvent event) { return isFromController(event.getDevice()) && isPadKey(event.getKeyCode(), event.getDevice().getKeyboardType()); @@ -249,7 +193,6 @@ public final class PadBridge { } } - /** Whether any real controller is attached right now. */ public static boolean anyControllerConnected() { for (int id : InputDevice.getDeviceIds()) { if (isFromController(InputDevice.getDevice(id))) return true; @@ -257,7 +200,6 @@ public final class PadBridge { return false; } - /** Publishes every currently connected controller so the client can enumerate all players. */ public synchronized void syncConnectedControllers() { for (int deviceId : InputDevice.getDeviceIds()) { InputDevice device = InputDevice.getDevice(deviceId); @@ -265,11 +207,6 @@ public final class PadBridge { } } - /** - * Called (main thread) when a player presses a button, pushes a stick, trigger or d-pad past - * halfway, or uses the on-screen controls - the session hides its mouse cursor then. Resting - * sticks and trigger noise do not count. - */ public void setOnPlayerInput(Runnable listener) { onPlayerInput = listener; } @@ -283,7 +220,6 @@ public final class PadBridge { }); } - /** @return true when the event was a pad button and has been consumed. */ public synchronized boolean onKeyEvent(KeyEvent event) { if (!isControllerKeyEvent(event)) return false; int slot = slotFor(event.getDevice()); @@ -297,8 +233,6 @@ public final class PadBridge { case KeyEvent.KEYCODE_BUTTON_L1: pad.press(4, pressed); break; case KeyEvent.KEYCODE_BUTTON_R1: pad.press(5, pressed); break; case KeyEvent.KEYCODE_BUTTON_SELECT: - // Optionally (Setup → Controller) the physical Select button opens the Steam menu - // instead of View/Select: on some handhelds it sits next to the Steam button. if (selectSteam) pad.press(PadState.GUIDE, pressed); else pad.press(6, pressed); break; @@ -307,8 +241,6 @@ public final class PadBridge { case KeyEvent.KEYCODE_MENU: pad.press(7, pressed); break; case KeyEvent.KEYCODE_BUTTON_THUMBL: pad.press(8, pressed); break; case KeyEvent.KEYCODE_BUTTON_THUMBR: pad.press(9, pressed); break; - // The client's own in-game menu is opened by this one; the interposer publishes it as - // BTN_MODE, which SDL reports as the "guide" button. case KeyEvent.KEYCODE_BUTTON_MODE: case KeyEvent.KEYCODE_HOME: pad.press(PadState.GUIDE, pressed); break; case KeyEvent.KEYCODE_BUTTON_L2: pad.leftTrigger = pressed ? 1f : 0f; break; @@ -327,7 +259,6 @@ public final class PadBridge { return true; } - /** @return true when the event was a pad's sticks/triggers and has been consumed. */ public synchronized boolean onMotionEvent(MotionEvent event) { if (!isControllerMotionEvent(event)) return false; if (event.getAction() != MotionEvent.ACTION_MOVE) return false; @@ -337,14 +268,11 @@ public final class PadBridge { statAxes++; scheduleStats(); pad.leftX = axis(event, MotionEvent.AXIS_X); - // Android's Y axis grows downwards and evdev's ABS_Y does too, so no flip here: what the - // pad reports as "down" is what the client is told. pad.leftY = axis(event, MotionEvent.AXIS_Y); pad.rightX = axis(event, MotionEvent.AXIS_Z); pad.rightY = axis(event, MotionEvent.AXIS_RZ); float lt = event.getAxisValue(MotionEvent.AXIS_LTRIGGER); float rt = event.getAxisValue(MotionEvent.AXIS_RTRIGGER); - // Some pads only report the triggers on BRAKE/GAS. if (lt == 0f) lt = event.getAxisValue(MotionEvent.AXIS_BRAKE); if (rt == 0f) rt = event.getAxisValue(MotionEvent.AXIS_GAS); pad.leftTrigger = lt; @@ -363,10 +291,6 @@ public final class PadBridge { return true; } - /** - * The on-screen controls' way in: they mutate the same state a physical pad writes, so the - * client only ever sees one device and a user can use both at once without them fighting. - */ public synchronized void applyTouch(java.util.function.Consumer<PadState> mutation) { mutation.accept(state); activeControllerId = NO_CONTROLLER; @@ -377,12 +301,6 @@ public final class PadBridge { publish(); } - /** - * A controller the app reads itself rather than through Android's input (the Steam Controller - * over Bluetooth, SteamControllerBle): its whole state, merged into the one exported slot as a - * physical pad's events are. Android has no device for it, so a game's rumble cannot reach it. - * It streams its state whether or not anything moved; {@code changed} is whether this one did. - */ public synchronized void applyExternal(java.util.function.Consumer<PadState> mutation, boolean changed) { mutation.accept(state); if (changed) { @@ -394,21 +312,15 @@ public final class PadBridge { publish(); } - /** Touch-only Steam and QAM buttons, merged with physical input without changing its state. */ public synchronized void setSystemButtons(boolean guidePressed, boolean qamPressed) { if (systemGuidePressed == guidePressed && systemQamPressed == qamPressed) return; boolean qamStarted = qamPressed && !systemQamPressed; systemGuidePressed = guidePressed; systemQamPressed = qamPressed; - // A Deck or Steam Controller's QAM button is held for as long as the touch one is. if (qamStarted && !nativeQam()) startQamChord(); else publish(); } - /** - * Everything released and centred - for when the controller stops feeding the game (the - * session drawer opened), so a button or stick held at that moment is not left down in it. - */ public synchronized void releaseAll() { state.clear(); publish(); @@ -419,23 +331,16 @@ public final class PadBridge { } } - /** A controller went away: its player's buttons are let go, and a further player's pad is unplugged. */ public synchronized void onDeviceRemoved(int deviceId) { for (int slot = 0; slot < SLOTS; slot++) { if (owners[slot] == deviceId) free(slot); } } - /** - * Whether the client's pad has its own QAM button to press. The Deck's sysfs being up is not - * enough: the drawer can make the pad an Xbox or PlayStation one, which has none, and a press - * of it never reaches the client. - */ private static boolean nativeQam() { return SessionState.getDeckPad() && FakeInputWriter.padHasQam(); } - /** Opens the client's Quick Access menu: a tap of the pad's button, or the Guide-then-A chord. */ public synchronized void triggerQam() { if (!nativeQam()) { startQamChord(); @@ -488,7 +393,6 @@ public final class PadBridge { publish(); } - /** The slot this controller plays in: the one it has, else the first free one, else player 1's. */ private int slotFor(InputDevice device) { int id = device.getId(); if (owners[0] == id) return 0; @@ -527,7 +431,6 @@ public final class PadBridge { } } - /** Per event, so the common case - the same pad as last time - is a single compare. */ private void noteDevice(InputDevice device, int slot) { activeControllerId = device.getId(); slotControllers.set(slot, device.getId()); diff --git a/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt b/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt index 7058bf300..bc640507a 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt @@ -11,31 +11,14 @@ import android.util.Log import android.view.Surface import kotlin.math.roundToInt -/** - * The handheld's own gyro and accelerometer, as the Steam Deck controller's (SteamDeckPad). - * - * The phone is the controller's body, so its motion sensors are the ones a Deck's would be. Android - * reports them in the device's natural orientation; they are turned to the screen as it is shown - * (x right, y up, z towards the player - SDL's frame for a gamepad), then to the Deck's own axes - * and units as SDL's Deck driver undoes them (SDL_hidapi_steamdeck.c): X right, Y away from the - * player, Z up; 1 g = 16384, 2000 °/s = 32768. The Steam client calibrates the gyro itself. - * - * Sensors run only while the session is on screen with the pad a Deck controller, and the client - * wants motion (see [follow]), sampled as often as the Deck reports (4 ms); each reading goes - * straight into the pad's ring ([FakeInputWriter.writeMotion]), where libfakeinput's 4 ms report - * picks up the latest. - */ class PadMotion(private val context: Context, private val rotation: () -> Int) : SensorEventListener { private val sensors = context.getSystemService(SensorManager::class.java) private var thread: HandlerThread? = null - /** Readings are taken only while this is set; a callback already queued when [stop] ran drops. */ @Volatile private var active = false - // Touched only on the worker thread. private val accel = ShortArray(3) private val gyro = ShortArray(3) private var reading = false private var idlePolls = 0 - // Asking the display costs more than the rest of a reading, which arrives every 2 ms. private var turn = Surface.ROTATION_0 private var turnAt = 0L @@ -65,8 +48,6 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : val worker = thread ?: return thread = null active = false - // On the worker, after any poll or reading already queued there, so nothing registers again - // or overwrites the still controller. Handler(worker.looper).post { sensors?.unregisterListener(this) reading = false @@ -76,12 +57,6 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : worker.quitSafely() } - /** - * Reads the sensors while the client wants motion: the Deck's IMU mode (unknown counts as - * wanted), or Steam's touch controls asking for gyro. A game whose layout has no gyro turns the - * mode off; Steam's own menus keep it on. The client also turns it off and on again for a few - * ms whenever it re-applies a layout, so the sensors stop only after two polls without it. - */ private fun follow(handler: Handler, gyroscope: Sensor?, accelerometer: Sensor?) { val touch = SteamTouchDevice.current val wanted = FakeInputWriter.readImuMode(SLOT) != 0 || @@ -101,7 +76,6 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : } } - /** A still controller, not one frozen mid-turn. */ private fun still() { gyro.fill(0) FakeInputWriter.writeMotion(SLOT, accel, gyro) @@ -115,7 +89,6 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : turn = rotation() turnAt = event.timestamp } - // Natural orientation to the screen's: Android's display-rotation transform. val x: Float val y: Float when (turn) { @@ -132,7 +105,6 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : Sensor.TYPE_ACCELEROMETER -> { target = accel; scale = ACCEL_COUNTS_PER_M_S2 } else -> return } - // Screen frame (right, up, towards the player) to the Deck's (right, away, up). target[0] = counts(x * scale) target[1] = counts(-z * scale) target[2] = counts(y * scale) @@ -146,11 +118,8 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : companion object { private const val TAG = "PadMotion" - /** The pad the Deck controller is made from (PadBridge's slot). */ private const val SLOT = 0 - /** The Deck controller's report interval (libfakeinput: DECK_REPORT_INTERVAL_US). */ private const val SAMPLING_US = 4000 - /** How often [follow] checks whether the client wants motion. */ private const val POLL_MS = 500L private const val ROTATION_REFRESH_NS = 250_000_000L private const val ACCEL_COUNTS_PER_M_S2 = 16384f / SensorManager.GRAVITY_EARTH diff --git a/app/src/main/java/com/droiddeck/launcher/input/PadState.kt b/app/src/main/java/com/droiddeck/launcher/input/PadState.kt index c798b682f..d337a2a32 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/PadState.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/PadState.kt @@ -1,11 +1,5 @@ package com.droiddeck.launcher.input -/** - * What one Xbox-shaped pad is doing right now: two sticks and two triggers as -1..1 / 0..1, the - * four d-pad directions, and a bitmask of the buttons numbered the way the fake evdev node - * reports them. Written by whatever drives the pad (a physical controller, the on-screen one, or - * both at once) and read by [FakeInputWriter] into the guest's input ring. - */ class PadState { @JvmField var leftX = 0f @JvmField var leftY = 0f @@ -25,7 +19,6 @@ class PadState { fun isDown(button: Int): Boolean = buttons and (1 shl button) != 0 - /** The d-pad as a hat: -1 left, 1 right; -1 up, 1 down. Opposites cancel to 0. */ fun hatX(): Int = if (right && !left) 1 else if (left && !right) -1 else 0 fun hatY(): Int = if (down && !up) 1 else if (up && !down) -1 else 0 @@ -36,7 +29,6 @@ class PadState { buttons = other.buttons } - /** Everything released and centred. */ fun clear() { leftX = 0f; leftY = 0f; rightX = 0f; rightY = 0f leftTrigger = 0f; rightTrigger = 0f @@ -55,9 +47,7 @@ class PadState { const val START = 7 const val L3 = 8 const val R3 = 9 - /** The Steam/Guide button, which the fake evdev node reports as BTN_MODE. */ const val GUIDE = 12 - /** The Quick Access button: a Deck controller's (SteamDeckPad) only, since an Xbox pad has none. */ const val QAM = 13 } } diff --git a/app/src/main/java/com/droiddeck/launcher/input/PointerGestures.kt b/app/src/main/java/com/droiddeck/launcher/input/PointerGestures.kt index 9183578d1..dab9ff652 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/PointerGestures.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/PointerGestures.kt @@ -7,25 +7,12 @@ import android.view.ViewConfiguration import kotlin.math.abs import kotlin.math.roundToInt -/** - * Touch as a mouse, the way a remote desktop does it: where you touch is where the pointer goes. - * - * - tap ............................ left click - * - drag ........................... moves the pointer, no button (hover) - * - hold, then lift ................ right click - * - hold, then drag ................ left-button drag (a window by its title bar, a selection) - * - two fingers up / down .......... scroll wheel - * - * The listener gets pointer positions in the view's pixels; the activity maps them onto the - * compositor. Mouse events are not handled here - a mouse has real buttons. - */ class PointerGestures( private val slopPx: Float, private val listener: Listener, ) { interface Listener { fun onMove(x: Float, y: Float) - /** [button] is an evdev code: BTN_LEFT, BTN_RIGHT. */ fun onButton(button: Int, pressed: Boolean, x: Float, y: Float) fun onWheel(steps: Int) fun onLongPress() @@ -36,8 +23,8 @@ class PointerGestures( private var downY = 0f private var downAt = 0L private var moved = false - private var held = false // the long press fired - private var dragging = false // left button down after a hold + private var held = false + private var dragging = false private var scrolling = false private var scrollAnchorY = 0f private val longPress = Runnable { @@ -55,7 +42,6 @@ class PointerGestures( return true } MotionEvent.ACTION_POINTER_DOWN -> { - // A second finger: scrolling, and whatever the first one was doing is off. handler.removeCallbacks(longPress) if (dragging) { listener.onButton(BTN_LEFT, false, event.x, event.y); dragging = false } scrolling = true @@ -67,7 +53,6 @@ class PointerGestures( val dy = event.getY(0) - scrollAnchorY val steps = (dy / SCROLL_STEP_PX).toInt() if (steps != 0) { - // Finger down = content up = wheel "up", which the compositor takes as negative. listener.onWheel(-steps) scrollAnchorY += steps * SCROLL_STEP_PX } @@ -77,7 +62,6 @@ class PointerGestures( moved = true handler.removeCallbacks(longPress) if (held) { - // Held, then moved: a drag with the left button, from where the hold was. dragging = true listener.onButton(BTN_LEFT, true, downX, downY) } diff --git a/app/src/main/java/com/droiddeck/launcher/input/SecondScreenMode.kt b/app/src/main/java/com/droiddeck/launcher/input/SecondScreenMode.kt index de5f70d94..6789654f7 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SecondScreenMode.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SecondScreenMode.kt @@ -5,18 +5,15 @@ import android.view.Display import androidx.annotation.StringRes import com.droiddeck.launcher.R -/** The control surface shown on a secondary Android display during a Linux session. */ enum class SecondScreenMode(val id: String, @StringRes val label: Int) { NONE("none", R.string.second_mode_none), KEYBOARD_TRACKPAD("keyboard-trackpad", R.string.second_mode_keyboard_trackpad), TERMINAL("terminal", R.string.second_mode_terminal), - /** The Steam Deck controller's back grips and trackpads; offered while the pad is one. */ DECK_CONTROLS("deck-controls", R.string.second_mode_deck_controls), } data class SecondScreenDisplay(val id: Int, val label: String) -/** The presentation-capable displays shared by the session controls and Android app launcher. */ object SecondScreenDisplays { fun available(displayManager: DisplayManager, primaryDisplayId: Int = Display.DEFAULT_DISPLAY): List<SecondScreenDisplay> = displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION) diff --git a/app/src/main/java/com/droiddeck/launcher/input/SessionClipboard.kt b/app/src/main/java/com/droiddeck/launcher/input/SessionClipboard.kt index faa6e1ace..4ec08bcb4 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SessionClipboard.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SessionClipboard.kt @@ -11,7 +11,6 @@ import com.droiddeck.launcher.wayland.WaylandCompositor import java.io.File import java.io.IOException -/** Text selections shared by Android and the guest while the session is visible. */ class SessionClipboard(context: Context, private val hasFocus: () -> Boolean) { private val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager private val handler = Handler(Looper.getMainLooper()) @@ -20,8 +19,6 @@ class SessionClipboard(context: Context, private val hasFocus: () -> Boolean) { private val androidListener = ClipboardManager.OnPrimaryClipChangedListener { refresh() } private val guestListener = object : WaylandCompositor.ClipboardListener { override fun onGuestClipboardText(text: String) { - // Native callbacks arrive on the compositor thread. Do not publish a delayed callback - // after this activity has paused or been replaced. handler.post { if (active && hasFocus() && WaylandCompositor.isClipboardListener(this)) { val current = clipboard.primaryClip?.takeIf { it.itemCount > 0 }?.getItemAt(0)?.text @@ -49,11 +46,9 @@ class SessionClipboard(context: Context, private val hasFocus: () -> Boolean) { handler.removeCallbacksAndMessages(null) } - /** Android 10+ permits clipboard reads only with window focus. Resume can precede focus. */ fun refresh() { if (!active || !hasFocus()) return val clip = clipboard.primaryClip - // Only share literal text, never resolve content URIs or load copied files. if (clip == null) { publish(null) } else if (clip.itemCount > 0) { @@ -63,8 +58,6 @@ class SessionClipboard(context: Context, private val hasFocus: () -> Boolean) { private fun publish(text: String?) { WaylandCompositor.setClipboardText(text) - // Gamescope needs the same selection in its own Xwayland. Rename complete UTF-8 text - // into place so its helper never sees half a launch command. Kept in private app data. val pending = File(hostFile.parentFile, "android-clipboard.pending") try { hostFile.parentFile?.mkdirs() diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamControllerBle.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamControllerBle.kt index 34357e48a..ee84cffe4 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamControllerBle.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamControllerBle.kt @@ -18,20 +18,11 @@ import java.nio.ByteOrder import java.util.ArrayDeque import java.util.UUID -/** - * Reads the Steam Controller (2025, Triton) through its Valve BLE service. State reports remain - * native Triton packets and are published to the session's separate 28DE:1303 HID device. Steam's - * output reports travel back to their matching BLE characteristics, preserving grip capacitance, - * trackpads, sensors, and controller haptics without folding the controller into the Deck target. - * - * Lizard mode is refreshed while connected; when a session ends, the controller restores it itself. - */ -@SuppressLint("MissingPermission") // BLUETOOTH is granted at install below targetSdk 31 +@SuppressLint("MissingPermission") class SteamControllerBle(private val context: Context) { private val thread = HandlerThread("steam-controller-ble").apply { start() } private val handler = Handler(thread.looper) private val outputReports = mutableMapOf<Int, UUID>() - /** GATT allows one operation at a time: writes carry a value, a feature read none. */ private val queuedWrites = ArrayDeque<Pair<UUID, ByteArray?>>() private var gatt: BluetoothGatt? = null private var running = false @@ -42,10 +33,8 @@ class SteamControllerBle(private val context: Context) { private var discoveries = 0 private var outputSequence = 0L private var outputWritePending = false - /** The Steam client's feature read being answered, so the poll queues each one once. */ private var featureRead = 0L - /** Looks for a paired Steam Controller and keeps a connection to it until [stop]. */ fun start() = handler.post { if (running) return@post running = true @@ -53,7 +42,6 @@ class SteamControllerBle(private val context: Context) { connect() } - /** Lets the controller go (it returns to lizard mode by itself); this object is done after. */ fun stop() = handler.post { running = false handler.removeCallbacksAndMessages(null) @@ -118,7 +106,6 @@ class SteamControllerBle(private val context: Context) { if (running) handler.postDelayed(::connect, RETRY_MS) return } - // The Triton needs data length extensions, which Android enables for a large MTU. if (!g.requestMtu(517)) subscribe(g) } @@ -160,7 +147,6 @@ class SteamControllerBle(private val context: Context) { if (g === gatt) onFeatureRead(characteristic.value.takeIf { status == BluetoothGatt.GATT_SUCCESS }) } - // Android 13 hands the value over with the call; older ones only through the characteristic. override fun onCharacteristicChanged(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) { if (g === gatt && characteristic.uuid == input) onState(value) } @@ -177,8 +163,6 @@ class SteamControllerBle(private val context: Context) { if (service == null) { Log.w(TAG, "no Valve service on \"${g.device.name}\" (attempt ${discoveries + 1}); services: " + g.services.joinToString { it.uuid.toString().take(8) }) - // Android may answer from the service list its HID host cached, which can lack the - // vendor service: ask the controller again before giving up on it. if (++discoveries < 3) { refreshCache(g) handler.postDelayed({ if (g === gatt) g.discoverServices() }, 500) @@ -213,13 +197,11 @@ class SteamControllerBle(private val context: Context) { g.writeDescriptor(cccd) } - /** BluetoothGatt.refresh(): hidden, but used to drop stale service caches. */ private fun refreshCache(g: BluetoothGatt) { runCatching { g.javaClass.getMethod("refresh").invoke(g) } .onFailure { Log.w(TAG, "could not refresh the service cache", it) } } - /** SDL's DisableSteamTritonLizardMode: ID_SET_SETTINGS_VALUES (0x87), setting 9 = off. */ private fun refreshLizardMode() { if (!connected) return val msg = ByteArray(FEATURE_BYTES) @@ -228,18 +210,15 @@ class SteamControllerBle(private val context: Context) { handler.postDelayed(::refreshLizardMode, LIZARD_REFRESH_MS) } - /** Preserve the full 0x45 or 0x47 packet so Triton-only bits reach Steam's native driver. */ private fun onState(data: ByteArray) { if (data.size < STATE_BYTES) return FakeInputWriter.writeTritonState(SLOT, reportId, data) } - /** A neutral report prevents a link lost mid-press from leaving controls held. */ private fun release() { FakeInputWriter.writeTritonState(SLOT, if (reportId == 0x47) 0x47 else 0x45, ByteArray(STATE_BYTES)) } - /** Drain Steam HID output reports and write each to Triton's matching BLE characteristic. */ private fun pollOutput() { if (!running || !connected || gatt == null) return FakeInputWriter.readTritonOutput(SLOT, outputSequence)?.let { output -> @@ -249,14 +228,12 @@ class SteamControllerBle(private val context: Context) { val request = FakeInputWriter.readTritonFeatureRequest(SLOT) if (request != 0L && request != featureRead) { featureRead = request - // Behind any queued write, so a read answers the feature report written before it. queuedWrites.addLast(REPORT to null) writeNextOutput() } handler.postDelayed(::pollOutput, OUTPUT_POLL_MS) } - /** The controller's answer to a feature read (its info, for one), or null when the read failed. */ private fun onFeatureRead(value: ByteArray?) { outputWritePending = false if (featureRead != 0L) { @@ -277,7 +254,6 @@ class SteamControllerBle(private val context: Context) { return } } - // SDL's BLE driver removes the HID report ID and trailing padding byte before transmission. enqueueOutput(characteristic, report.copyOfRange(1, report.size - 1)) } @@ -319,13 +295,11 @@ class SteamControllerBle(private val context: Context) { companion object { private const val TAG = "SteamControllerBle" - /** The paired controller's native Triton reports live in slot 0's extended ring. */ private const val SLOT = 0 private const val RETRY_MS = 5_000L private const val LIZARD_REFRESH_MS = 3_000L private const val OUTPUT_POLL_MS = 4L private const val OUTPUT_RETRY_MS = 10L - /** SDL's 64-byte feature report less the report ID and the trailing byte. */ private const val FEATURE_BYTES = 62 private const val STATE_BYTES = 45 private const val OUTPUT_UUID_PREFIX = "100f6c" @@ -341,7 +315,6 @@ class SteamControllerBle(private val context: Context) { device.type and BluetoothDevice.DEVICE_TYPE_LE != 0 && (device.name ?: "").let { it.startsWith("Steam Ctrl") || it.startsWith("SteamController") } - /** Whether a session should expose a Triton HID node for a paired controller. */ @JvmStatic fun hasPairedController(context: Context): Boolean { val adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter ?: return false @@ -350,12 +323,10 @@ class SteamControllerBle(private val context: Context) { .getOrDefault(false) } - /** A Steam Controller is connected and feeding its native HID reports. */ @Volatile @JvmStatic var connected = false private set - /** Notifies the activity when [connected] changes. */ @Volatile @JvmStatic var listener: (() -> Unit)? = null } diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchBindings.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchBindings.kt index f5f46d88c..4d19cb87d 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchBindings.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchBindings.kt @@ -6,21 +6,8 @@ import android.graphics.BitmapFactory import android.util.LruCache import java.io.File -/** - * What each touch control is bound to, and the icon it shows - Steam's, as its configurator keeps - * them. - * - * A Steam Input binding is one string: `<action>, <label>, <icon>, <#foreground #background>`, e.g. - * `xinput_button A, Jump, ghost_045_move_0400.png, #232323 #E4E4E4`. The icon is a file name looked - * up as Steam does (steamui.so): the game's own `TouchMenuIcons` folder, then the binding icon - * library `tenfoot/resource/images/library/controller/binding_icons`. Steam draws it filled with - * the foreground colour through its own alpha (mask-image) on a button of the background colour. - * Steam Link is sent these per control by the client it streams from; here they are read from the - * game's touch config, and editing one writes the binding string back, as the configurator does. - */ object SteamTouchBindings { - /** Steam's binding icon colours (the configurator's palette, steamui sp.js), and its defaults. */ val PALETTE = listOf( "#FFFFFF", "#E4E4E4", "#AAAAAA", "#787878", "#434343", "#222222", "#0000AD", "#0045AD", "#0074AD", "#00ADAD", "#33AD69", "#00AD3D", "#00AD00", "#48B119", "#74AD00", "#96AD00", "#ADA200", "#AD5D00", @@ -29,7 +16,6 @@ object SteamTouchBindings { const val DEFAULT_FOREGROUND = "#232323" const val DEFAULT_BACKGROUND = "#E4E4E4" - /** A parsed binding string. Empty strings for absent fields. */ data class Binding(val action: String, val label: String, val icon: String, val foreground: String, val background: String) { fun compose(): String = if (icon.isEmpty() && foreground.isEmpty() && background.isEmpty()) "$action, $label, " @@ -45,10 +31,8 @@ object SteamTouchBindings { colors.getOrNull(0).orEmpty(), colors.getOrNull(1).orEmpty()) } - /** Where a control's binding lives: its input in the group the preset puts on its source. */ data class Ref(val groupId: String, val input: String) - /** The source and input names a control reads (D-pad: its four directions). */ private fun inputsOf(type: Int): List<Pair<String, String>> = when (type) { SteamTouchConfig.A -> listOf("button_diamond" to "button_a") SteamTouchConfig.B -> listOf("button_diamond" to "button_b") @@ -69,13 +53,8 @@ object SteamTouchBindings { else -> emptyList() } - /** D-pad directions, in the order [refsFor] returns them: up, down, left, right. */ val DPAD_INPUTS = listOf("dpad_north", "dpad_south", "dpad_west", "dpad_east") - /** - * Where control [type]'s bindings are for preset [preset] (the action set less one) and its - * active [layers]: a layer's group wins over the base's for the same source. - */ fun refsFor(mappings: KeyValues.Node, type: Int, preset: Int, layers: List<Int> = emptyList()): List<Ref?> { val presets = mappings.children("preset").associateBy { it.string("id")?.toIntOrNull() ?: -1 } val stack = layers.reversed().mapNotNull { presets[it - 1] } + listOfNotNull(presets[preset] ?: presets.values.firstOrNull()) @@ -90,7 +69,6 @@ object SteamTouchBindings { parts.getOrNull(0) == source && parts.getOrNull(1) == "active" && !parts.contains("modeshift") }?.first - /** The binding at [ref]: its first activator's first binding (Full_Press when there is one). */ fun bindingAt(mappings: KeyValues.Node, ref: Ref): Binding? { val group = mappings.children("group").firstOrNull { it.string("id") == ref.groupId } ?: return null val activators = group.child("inputs")?.child(ref.input)?.child("activators") ?: return null @@ -98,7 +76,6 @@ object SteamTouchBindings { return activator?.child("bindings")?.string("binding")?.let(::parse) } - /** A short glyph for a binding with no icon or label: what it presses. */ fun glyph(binding: Binding?): String? { val words = binding?.action?.split(' ') ?: return null val arg = words.getOrNull(1)?.uppercase() ?: return null @@ -123,12 +100,7 @@ object SteamTouchBindings { "LEFT_ARROW" to "←", "RIGHT_ARROW" to "→", ) - // ---- Writing a binding back ---- - /** - * Rewrites every binding of [ref]'s input in config [text] (VDF) with [change], keeping the rest - * of the file byte for byte. Returns the new text, or null when the input is not there. - */ fun rewrite(text: String, ref: Ref, change: (Binding) -> Binding): String? { val tokens = Vdf.tokens(text) val group = Vdf.blocks(tokens, 0, tokens.size, "group").firstOrNull { (s, e) -> @@ -145,7 +117,6 @@ object SteamTouchBindings { return out.toString() } - /** A VDF tokenizer that keeps where each token sits, for edits that leave the rest untouched. */ private object Vdf { class Token(val text: String, val quoted: Boolean, val start: Int, val end: Int) @@ -175,8 +146,6 @@ object SteamTouchBindings { return out } - /** The bodies (token index ranges inside the braces) of direct children named [key] of the - * block spanning [from, to). */ fun blocks(tokens: List<Token>, from: Int, to: Int, key: String): List<Pair<Int, Int>> { val found = mutableListOf<Pair<Int, Int>>() var i = from @@ -200,7 +169,6 @@ object SteamTouchBindings { } i++ } - // The whole file is one top-level block ("controller_mappings"): look inside it too. if (found.isEmpty() && from == 0) { val top = tokens.indexOfFirst { it.text == "{" && !it.quoted } if (top > 0) { @@ -217,7 +185,6 @@ object SteamTouchBindings { return found } - /** A direct key-value of the block spanning [from, to). */ fun value(tokens: List<Token>, from: Int, to: Int, key: String): String? { var depth = 0 var i = from @@ -232,7 +199,6 @@ object SteamTouchBindings { } } - // ---- Icons ---- private val cache = object : LruCache<String, Bitmap>(24 * 1024 * 1024) { override fun sizeOf(key: String, value: Bitmap) = value.byteCount @@ -241,7 +207,6 @@ object SteamTouchBindings { fun libraryDir(context: Context) = File(SteamTouchConfig.steamRoot(context), "tenfoot/resource/images/library/controller/binding_icons") - /** The game's own icons folder (`TouchMenuIcons` in its install directory), when it has one. */ fun gameIconsDir(context: Context, appId: Int): File? = try { val manifest = File(SteamTouchConfig.steamRoot(context), "steamapps/appmanifest_$appId.acf") val installDir = KeyValues.parse(manifest.readText()).let { it.child("AppState") ?: it }.string("installdir") @@ -257,7 +222,6 @@ object SteamTouchBindings { return File(SteamTouchConfig.steamRoot(context), "steamui/images/controller/$name").takeIf { it.isFile } } - /** An icon, decoded at about [size] px. Blocking (reads a file); cached. */ fun icon(context: Context, appId: Int, name: String, size: Int = 128): Bitmap? { val key = "$appId/$name/$size" cache.get(key)?.let { return it } @@ -276,7 +240,6 @@ object SteamTouchBindings { fun cachedIcon(appId: Int, name: String, size: Int = 128): Bitmap? = cache.get("$appId/$name/$size") - /** Icon names for the picker: the game's own first, then Steam's library by category. */ fun iconNames(context: Context, appId: Int): List<String> { val own = gameIconsDir(context, appId)?.list()?.filter { it.endsWith(".png", true) }?.sorted().orEmpty() val library = libraryDir(context).list()?.filter { it.endsWith(".png", true) && it != "special_blank.png" }?.sorted().orEmpty() diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchConfig.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchConfig.kt index 844fe147e..882a3a3dd 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchConfig.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchConfig.kt @@ -7,25 +7,9 @@ import java.io.ByteArrayOutputStream import java.io.File import java.io.RandomAccessFile -/** - * Steam's touch configs, read and written where Steam keeps them. - * - * A touch config is a Steam Input config (`controller_mobile_touch`) like any other - bindings per - * action set, synced to the account, shared as community configs - with one extra key, - * `touch_layout`: where each on-screen control sits, per action set, as a hex-encoded - * `CVirtualControllerLayouts` protobuf (steammessages_virtualcontroller.proto). Steam Link is - * handed both by the client it streams from; here the client tells the device only the app and - * action set it is on (SteamTouchDevice), so the config is found on disk: the file the client's - * console log says it loaded for the touch controller, else the app's entry in - * `configset_controller_mobile_touch.vdf`. With no layout in it, controls go where Steam Link puts - * them by default. Saving a layout writes `touch_layout` into the app's autosave config, as the - * client does when Steam Link saves one; Steam Cloud picks that file up like any autosave. - * (docs/development/steam-touch-controller.md.) - */ object SteamTouchConfig { private const val TAG = "SteamTouch" - // EControllerElementType. const val THUMB = 0 const val STEAM = 1 const val JOYSTICK_LEFT = 2 @@ -43,7 +27,7 @@ object SteamTouchConfig { const val TRIGGER_RIGHT = 14 const val BUMPER_LEFT = 15 const val BUMPER_RIGHT = 16 - const val MACRO_0 = 17 // 17..24 = macros 0-7 + const val MACRO_0 = 17 const val TRACKPAD_CENTER = 25 const val TRACKPAD_LEFT = 26 const val TRACKPAD_RIGHT = 27 @@ -53,27 +37,19 @@ object SteamTouchConfig { const val MAGNIFYING_GLASS = 29 const val PASTE = 34 - /** Controls a layout may place though no binding asks for them (Steam Link's tray). */ val OPTIONAL = setOf(PASTE) - /** A control's place: centre as a fraction of the screen, and its scale. */ data class Element(val type: Int, val visible: Boolean, val x: Float, val y: Float, val xScale: Float = 1f, val yScale: Float = 1f) data class Layout(val actionSet: Int, val elements: List<Element>, val color: FloatArray?, val version: Int?) - /** A decoded `touch_layout`; [rest] is every other top-level field, written back untouched. */ class Layouts(val layouts: List<Layout>, val rest: ByteArray) { fun forActionSet(id: Int): Layout? = layouts.firstOrNull { it.actionSet == id } - /** The per-game options Steam Link keeps beside the layouts (CVirtualControllerLayouts - * fields 2-4): input mode, mouse mode, trackpad sensitivity. */ val options: Options get() = Options.decode(rest) } - /** EInputMode: 1 mouse, 2 controller, 3 both. EMouseMode: 2 absolute cursor (direct touch), - * 3 touch, 4 relative (trackpad); 0 = not set. */ data class Options(val inputMode: Int = INPUT_CONTROLLER, val mouseMode: Int = 0, val trackpadSensitivity: Float = 1f) { - /** [rest] with these options in place of its own fields 2-4, the rest kept as it was. */ fun encodeInto(rest: ByteArray): ByteArray { val out = ByteArrayOutputStream() val p = Proto(rest) @@ -116,7 +92,6 @@ object SteamTouchConfig { const val MOUSE_TOUCH = 3 const val MOUSE_RELATIVE = 4 - /** The config's action sets in preset order: id as the device is told (preset id + 1) and title. */ fun actionSets(config: Config): List<Pair<Int, String>> { val mappings = config.mappings ?: return listOf(1 to "Default") val titles = mappings.child("actions")?.entries?.mapNotNull { (key, v) -> @@ -130,40 +105,30 @@ object SteamTouchConfig { return sets.ifEmpty { listOf(1 to "Default") } } - /** A config as the overlay needs it. */ class Config( val file: File?, val text: String?, val layouts: Layouts?, - /** Bound controls per preset (action set id - 1). */ val available: Map<Int, Set<Int>>, - /** The parsed config, for its bindings (SteamTouchBindings). */ val mappings: KeyValues.Node? = null, ) { - /** The config's own title, as Steam Link's tray shows it ("Default", "Gamepad"…). */ val title: String? get() = mappings?.string("title")?.let { t -> if (t.startsWith("#")) mappings.child("localization")?.child("english")?.string("title") ?: "Default" else t } - /** The controls to show for [actionSet] (as the device is told it) and [layers]. */ fun availableFor(actionSet: Int, layers: List<Int>): Set<Int> { val base = available[presetOf(actionSet)] ?: available.values.firstOrNull() ?: DEFAULT_SET return layers.fold(base) { set, layer -> set + (available[presetOf(layer)] ?: emptySet()) } + ALWAYS } } - /** Big Picture's own config uses Steam's basic UI bindings: the default set. */ private val DEFAULT_SET = setOf(DPAD, A, B, X, Y, SELECT, START) private val ALWAYS = setOf(STEAM, THUMB, KEYBOARD) - /** The layout entry for the action set the device was told about (the client counts from 1, - * 0 for an app with none). */ fun layoutIdOf(actionSet: Int) = if (actionSet <= 0) 1 else actionSet private fun presetOf(actionSet: Int) = layoutIdOf(actionSet) - 1 - // ---- Where Steam keeps them ---- - /** A game's name from its app manifest, when it is installed. */ fun gameName(context: Context, appId: Int): String? = try { val kv = KeyValues.parse(File(steamRoot(context), "steamapps/appmanifest_$appId.acf").readText()) (kv.child("AppState") ?: kv).string("name") @@ -181,7 +146,6 @@ object SteamTouchConfig { ?.maxByOrNull { it.lastModified() }?.let { File(it, "config") } } - /** The signed-in account (loginusers.vdf's MostRecent) as a 32-bit account id. */ private fun accountId(context: Context): Long? = try { val users = KeyValues.parse(File(steamRoot(context), "config/loginusers.vdf").readText()) val node = users.child("users") @@ -192,11 +156,8 @@ object SteamTouchConfig { null } - /** The config the client has for app [appId] on the touch controller. Blocking: reads files. */ fun load(context: Context, appId: Int): Config { val dir = configDir(context) - // The app's own saved config is what the client loads when there is one (its configset - // entry); otherwise what it logged loading - a template, an official or a workshop config. val file = dir?.let { autosaveConfig(it, appId) } ?: loggedConfig(context, appId)?.takeIf { isTouchConfig(it) } ?: dir?.let { configsetConfig(context, it, appId) } @@ -220,9 +181,6 @@ object SteamTouchConfig { private fun isTouchConfig(file: File) = file.isFile && file.bufferedReader().use { r -> generateSequence { r.readLine() }.take(40).any { it.contains("controller_mobile_touch") } } - /** From the client's console log: the last config it loaded for [appId] on the touch controller - * (`Loaded Config for ... App ID <n>, Controller <i>: <path>`, where controller i reported - * type 43). Guest paths come back as the app's own. */ private fun loggedConfig(context: Context, appId: Int): File? = try { val log = File(steamRoot(context), "logs/console_log.txt") val tail = RandomAccessFile(log, "r").use { raf -> @@ -249,7 +207,6 @@ object SteamTouchConfig { null } - /** The app's entry in configset_controller_mobile_touch.vdf. */ private fun configsetConfig(context: Context, dir: File, appId: Int): File? = try { val set = KeyValues.parse(File(dir, CONFIGSET).readText()).child("controller_config")?.child(appId.toString()) when { @@ -264,9 +221,7 @@ object SteamTouchConfig { private const val CONFIGSET = "configset_controller_mobile_touch.vdf" - // ---- What a config binds ---- - /** Per preset, the controls its active sources bind - what Steam Link shows (UpdateElementAvailability). */ private fun availability(mappings: KeyValues.Node): Map<Int, Set<Int>> { val groups = mappings.children("group").associateBy({ it.string("id") ?: "" }) { group -> group.child("inputs")?.entries?.filter { (_, v) -> (v as? KeyValues.Node)?.let(::hasBinding) == true } @@ -312,11 +267,7 @@ object SteamTouchConfig { (activator as? KeyValues.Node)?.child("bindings")?.entries?.any { (k, v) -> k == "binding" && (v as? String)?.isNotBlank() == true } == true } == true - // ---- Steam Link's defaults ---- - /** Where Steam Link puts a control with no layout (CVirtualController::BInitializeDefaultElement, - * on a 1280x720 screen), and the same scheme for the controls its table leaves out, made room - * for when a config binds sticks. */ fun defaultElement(type: Int, available: Set<Int>): Element? { val leftStick = JOYSTICK_LEFT in available val rightStick = JOYSTICK_RIGHT in available @@ -352,8 +303,6 @@ object SteamTouchConfig { return Element(type, true, p.first, p.second) } - /** The controls to draw: the layout's own where it has one, Steam Link's defaults otherwise, - * only those the config binds. */ fun elementsFor(config: Config, actionSet: Int, layers: List<Int>): List<Element> { val available = config.availableFor(actionSet, layers) val layout = config.layouts?.forActionSet(layoutIdOf(actionSet)) @@ -371,14 +320,7 @@ object SteamTouchConfig { fun layoutColor(config: Config, actionSet: Int): FloatArray? = config.layouts?.forActionSet(layoutIdOf(actionSet))?.color - // ---- Saving ---- - /** - * Saves [elements] as the layout of [actionSet] in app [appId]'s touch config: into the app's - * autosave config (`<appid>/controller_mobile_touch.vdf`, made from the config in use when the - * app has none) and selected for the app in the configset, as the client saves a layout Steam - * Link sends it. Returns the file written, or null. - */ fun saveLayout( context: Context, appId: Int, @@ -389,10 +331,6 @@ object SteamTouchConfig { bindings: Map<SteamTouchBindings.Ref, (SteamTouchBindings.Binding) -> SteamTouchBindings.Binding> = emptyMap(), ): File? = saveLayouts(context, appId, config, mapOf(layoutIdOf(actionSet) to elements), color?.let { c -> setOf(layoutIdOf(actionSet)).associateWith { c } } ?: emptyMap(), bindings) - /** - * Saves the layouts of several action sets (by layout id) at once, with their colours, any - * binding edits and the per-game [options], into the game's autosave touch config. - */ fun saveLayouts( context: Context, appId: Int, @@ -403,8 +341,6 @@ object SteamTouchConfig { options: Options? = null, ): File? { val dir = configDir(context) ?: return null - // Icons, labels and colours live in the binding strings (SteamTouchBindings), as the - // configurator writes them. val source = bindings.entries.fold(config.text ?: return null) { text, (ref, change) -> SteamTouchBindings.rewrite(text, ref, change) ?: text } @@ -414,7 +350,7 @@ object SteamTouchConfig { val written = sets.map { (id, elements) -> Layout(id, elements, colors[id] ?: old?.forActionSet(id)?.color ?: floatArrayOf(1f, 1f, 1f, 0.4f), old?.forActionSet(id)?.version) } - val rest = old?.rest ?: byteArrayOf(0x10, 0x02) // input_mode = controller + val rest = old?.rest ?: byteArrayOf(0x10, 0x02) val layouts = Layouts((keep + written).sortedBy { it.actionSet }, options?.encodeInto(rest) ?: rest) val hex = bytesToHex(encodeLayouts(layouts)) val line = "\t\"touch_layout\"\t\t\"$hex\"" @@ -431,7 +367,6 @@ object SteamTouchConfig { return target } - /** Points the app's configset entry at its autosave config. */ private fun selectAutosave(dir: File, appId: Int) { val file = File(dir, CONFIGSET) val text = if (file.isFile) file.readText() else "\"controller_config\"\n{\n}\n" @@ -442,7 +377,6 @@ object SteamTouchConfig { File(file.path + ".tmp").apply { writeText(next) }.renameTo(file) } - // ---- touch_layout: CVirtualControllerLayouts ---- fun decodeLayouts(bytes: ByteArray): Layouts { val layouts = mutableListOf<Layout>() @@ -591,7 +525,6 @@ object SteamTouchConfig { fun bytesToHex(b: ByteArray) = b.joinToString("") { "%02x".format(it.toInt() and 0xff) } } -/** Valve's KeyValues text format, as far as controller configs use it. */ class KeyValues private constructor() { class Node(val entries: MutableList<Pair<String, Any>> = mutableListOf()) { fun child(key: String): Node? = entries.firstOrNull { it.first.equals(key, true) && it.second is Node }?.second as Node? diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt index 2995896d1..5a717150e 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt @@ -22,18 +22,6 @@ import kotlin.math.hypot import kotlin.math.max import kotlin.math.min -/** - * Steam's touch controls, drawn by the app. - * - * The layout is Steam's: the game's touch config for the action set the client says is active - * (SteamTouchDevice.State, SteamTouchConfig), placed where the config's layout puts each control or - * where Steam Link would by default, and only the controls the config binds. Touches become the - * touch controller's input report; the client does the rest - bindings, action sets, the virtual - * pad the game reads. Each control shows its binding's icon, label and colours, as Steam keeps them - * (SteamTouchBindings). [startEditing] moves, resizes and hides controls, picks their icons and the - * layout's colour, and saves all of it into the game's touch config, as Steam Link and Steam's - * configurator do. - */ @SuppressLint("ViewConstructor") class SteamTouchControls( context: Context, @@ -42,19 +30,14 @@ class SteamTouchControls( private val onKeyboard: () -> Unit, ) : View(context) { - /** The game's input and mouse modes changed (from its config or the menu): the session routes - * touches outside the controls by them. */ var onOptions: (SteamTouchConfig.Options) -> Unit = {} - /** The Paste control: types the clipboard into the session. */ var onPaste: () -> Unit = {} - // Steam Link's own settings, for every game (CVirtualControllerGlobalConfig). private val prefs = context.getSharedPreferences("steam_touch", Context.MODE_PRIVATE) private var feedback = prefs.getBoolean("feedback", false) private var autoFade = prefs.getBoolean("autoFade", true) private var gyroOn = prefs.getBoolean("gyro", true).also { device.motionEnabled = it } - // Shake the device to hide the controls and again to bring them back (Steam Link's shake_fade). private var shakeToHide = prefs.getBoolean("shake", false) private var shakenAway = false private val sensors by lazy { context.getSystemService(Context.SENSOR_SERVICE) as android.hardware.SensorManager } @@ -69,7 +52,6 @@ class SteamTouchControls( val now = android.os.SystemClock.uptimeMillis() jolts = if (now - lastJolt < 400) jolts + 1 else 1 lastJolt = now - // Three jolts in quick succession, at most once a second. if (jolts >= 3 && now - lastShake > 1000) { lastShake = now jolts = 0 @@ -113,15 +95,12 @@ class SteamTouchControls( private var tint = Color.WHITE private var alphaScale = 0.45f - // What each control is bound to (its icon, label, colours), per element type; the D-pad has four. private class Visual(val ref: SteamTouchBindings.Ref?, val binding: SteamTouchBindings.Binding?) private var visuals: Map<Int, List<Visual>> = emptyMap() - // Touch state. private class Finger(val element: Element?, var x: Float, var y: Float, val toolbar: Int = -1) private val fingers = HashMap<Int, Finger>() - // Editing. private var editing = false private var editElements: MutableList<Element> = mutableListOf() private var hidden: MutableList<Element> = mutableListOf() @@ -130,7 +109,6 @@ class SteamTouchControls( private var pinchStart = 0f private var pinchScale = 1f private var saving = false - // Icon and colour edits not saved yet. private val pendingBindings = HashMap<SteamTouchBindings.Ref, SteamTouchBindings.Binding>() private var pendingColor: FloatArray? = null private var selectedArm = 0 @@ -162,12 +140,9 @@ class SteamTouchControls( super.onDetachedFromWindow() } - /** Follows what the client tells the device: a new app loads its config, a new action set or - * layer redraws. */ private fun refreshState() { val state = device.state() val app = if (state.appId == 0) BIG_PICTURE else state.appId - // The client reads the config afresh when the controller (re)connects: so do we. val reconnected = state.opened > 0 && opened == 0 opened = state.opened if (app != configApp || reconnected) { @@ -195,7 +170,6 @@ class SteamTouchControls( } } - /** Re-reads the config for the current app (after a save). */ fun reloadConfig() { configApp = -1 refreshState() @@ -216,12 +190,9 @@ class SteamTouchControls( private fun applyColor(color: FloatArray?) { tint = if (color != null) Color.rgb((color[0] * 255).toInt(), (color[1] * 255).toInt(), (color[2] * 255).toInt()) else Color.WHITE - // Steam Link's opacity: its default layout colour (alpha about 0.4) draws solid white - // outlines; lower values fade them. alphaScale = ((color?.get(3) ?: 0.45f) * 2.2f).coerceIn(0.15f, 1f) } - /** Each control's bindings for the action set and layers in use, with edits not saved yet. */ private fun rebuildVisuals() { val mappings = config.mappings val preset = SteamTouchConfig.layoutIdOf(actionSet) - 1 @@ -240,7 +211,6 @@ class SteamTouchControls( } } - // ---- Geometry ---- private fun unit() = min(width / 1280f, height / 720f) @@ -248,7 +218,6 @@ class SteamTouchControls( private fun radiusY(e: Element): Float = baseRadius(e.type) * e.yScale * unit() private fun baseRadius(type: Int) = when (type) { - // Steam Link's sizes on its 1280x720 reference. SteamTouchConfig.DPAD -> 112f SteamTouchConfig.JOYSTICK_LEFT, SteamTouchConfig.JOYSTICK_RIGHT -> 90f SteamTouchConfig.TRACKPAD_LEFT, SteamTouchConfig.TRACKPAD_RIGHT, SteamTouchConfig.TRACKPAD_CENTER -> 100f @@ -263,17 +232,14 @@ class SteamTouchControls( private fun cx(e: Element) = e.x * width private fun cy(e: Element) = e.y * height - /** How far (x, y) is from [e]'s centre, in its own radii (it may be an oval). */ private fun reach(e: Element, x: Float, y: Float) = hypot((x - cx(e)) / radius(e), (y - cy(e)) / radiusY(e)) private fun hit(list: List<Element>, x: Float, y: Float): Element? = list.filter { reach(it, x, y) <= 1.15f }.minByOrNull { reach(it, x, y) } - /** What is drawn and touchable: in mouse mode only the menu button, as on Steam Link. */ private fun live(): List<Element> = if (shakenAway) emptyList() else if (options.inputMode == SteamTouchConfig.INPUT_MOUSE) elements.filter { it.type == SteamTouchConfig.THUMB } else elements - // ---- Drawing ---- override fun onDraw(canvas: Canvas) { val shown = if (editing) editElements else live() @@ -281,7 +247,6 @@ class SteamTouchControls( val x = cx(e) val y = cy(e) canvas.save() - // An oval control: drawn as its circle, stretched to its height. if (e.yScale != e.xScale && e.xScale > 0f) canvas.scale(1f, e.yScale / e.xScale, x, y) draw(canvas, e, editing && editElements.indexOf(e) == selected) canvas.restore() @@ -297,10 +262,6 @@ class SteamTouchControls( private fun pressedTypes(): Set<Int> = fingers.values.mapNotNull { it.element?.type }.toSet() - // ---- Steam Link's look (measured from Steam Link 1.3 on the Thor) ---- - // Outlines only: 3 px white strokes on a 1280x720 reference, no fill; a pressed control fills - // white with its content dark. Face letters in Steam's colours; Select and Start are ◀ ▶ pills - // beside a solid white Steam disc; bumpers, triggers, the menu and keyboard are rounded squares. private fun lineWidth() = 3f * unit() @@ -340,7 +301,6 @@ class SteamTouchControls( } } SteamTouchConfig.STEAM -> { - // The guide button: DroidDeck's split D and orb, in a ring. if (pressed) canvas.drawCircle(x, y, r, fill) canvas.drawCircle(x, y, r, stroke) drawMark(canvas, x, y, r * 0.5f, if (pressed) Color.argb(alpha, 23, 26, 33) else ink, alpha) @@ -364,7 +324,6 @@ class SteamTouchControls( } } - /** Controls drawn as rounded squares on Steam Link; the face buttons and the rest are rings. */ private fun isSquare(type: Int) = type in setOf(SteamTouchConfig.BUMPER_LEFT, SteamTouchConfig.BUMPER_RIGHT, SteamTouchConfig.TRIGGER_LEFT, SteamTouchConfig.TRIGGER_RIGHT, SteamTouchConfig.THUMB, SteamTouchConfig.KEYBOARD, SteamTouchConfig.PASTE, SteamTouchConfig.MACRO_1_FINGER, SteamTouchConfig.MACRO_2_FINGER) || @@ -372,8 +331,6 @@ class SteamTouchControls( private val markD by lazy { androidx.core.graphics.PathParser.createPathFromPathData(MARK_D) } - /** DroidDeck's mark (artwork/droiddeck-mark.svg): the split D in [ink] around the blue orb, - * centred at (x, y), [r] its half height. */ private fun drawMark(canvas: Canvas, x: Float, y: Float, r: Float, ink: Int = onSurface, alpha: Int = 255) { val k = r / 111.86f canvas.save() @@ -384,7 +341,6 @@ class SteamTouchControls( canvas.restore() } - /** The Steam logo's mark, drawn: a wheel and a piston rod. */ private fun drawSteamMark(canvas: Canvas, x: Float, y: Float, r: Float, color: Int) { val p = Paint(Paint.ANTI_ALIAS_FLAG).apply { this.color = color; style = Paint.Style.STROKE; strokeCap = Paint.Cap.ROUND } p.strokeWidth = r * 0.16f @@ -396,11 +352,6 @@ class SteamTouchControls( canvas.drawLine(x - r * 0.45f, y + r * 0.28f, x - r * 0.95f, y + r * 0.1f, p) } - /** - * A button as Steam Link draws it: a ring (or rounded square) outline with its name - face - * letters in Steam's colours. With a binding icon, the icon on a disc of its background colour, - * as Steam draws binding icons; with a binding label, the label. - */ private fun drawFace(canvas: Canvas, type: Int, binding: SteamTouchBindings.Binding?, x: Float, y: Float, r: Float, alpha: Int, pressed: Boolean) { val square = isSquare(type) @@ -412,7 +363,6 @@ class SteamTouchControls( val s = r * 0.62f val custom = binding.background.isNotEmpty() && !binding.background.equals(SteamTouchBindings.DEFAULT_BACKGROUND, true) if (custom) { - // Colours chosen for this icon: drawn as Steam draws binding icons. shape(Paint(Paint.ANTI_ALIAS_FLAG).apply { color = parseColor(binding.background, SteamTouchBindings.DEFAULT_BACKGROUND) this.alpha = if (pressed) alpha else (alpha * 0.85f).toInt() @@ -420,7 +370,6 @@ class SteamTouchControls( canvas.drawBitmap(tinted(binding.icon, icon, binding.foreground), null, RectF(x - s, y - s, x + s, y + s), Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply { this.alpha = alpha }) } else { - // Steam's default colours: Steam Link's own look, the icon in the outline's ink. if (pressed) shape(fill) val ink = if (pressed) Color.argb(alpha, 23, 26, 33) else stroke.color canvas.drawBitmap(inked(binding.icon, icon), null, RectF(x - s, y - s, x + s, y + s), @@ -435,7 +384,6 @@ class SteamTouchControls( shape(stroke) if (type == SteamTouchConfig.THUMB) { drawDots(canvas, x, y, r, if (pressed) Color.argb(alpha, 23, 26, 33) else text.color); return } if (type == SteamTouchConfig.KEYBOARD) { drawKeyboardGlyph(canvas, x, y, r, if (pressed) Color.argb(alpha, 23, 26, 33) else text.color); return } - // Steam Link names a control by itself, whatever its binding is labelled ("B", not "Back"). val own = label(type) val shown = own text.textSize = when { @@ -469,14 +417,11 @@ class SteamTouchControls( private val iconsLoading = HashSet<String>() - // Icons as a mask (their alpha), tinted when drawn. private val inkedIcons = android.util.LruCache<String, android.graphics.Bitmap>(48) private fun inked(name: String, icon: android.graphics.Bitmap): android.graphics.Bitmap { val key = "$name|${icon.width}" inkedIcons.get(key)?.let { return it } - // Steam's icons are drawn in greys on clear; their dark parts are the glyph's detail, so the - // mask keeps what is opaque and not near white. val out = icon.copy(android.graphics.Bitmap.Config.ARGB_8888, true) val px = IntArray(out.width * out.height) out.getPixels(px, 0, out.width, 0, 0, out.width, out.height) @@ -491,8 +436,6 @@ class SteamTouchControls( return out } - // Icons already in their foreground colour. Drawn plain: a colour filter on the icon bitmap - // itself stopped drawing on the hardware canvas once a dialog had been over the view. private val tintedIcons = android.util.LruCache<String, android.graphics.Bitmap>(48) private fun tinted(name: String, icon: android.graphics.Bitmap, foreground: String): android.graphics.Bitmap { @@ -505,7 +448,6 @@ class SteamTouchControls( return out } - /** An icon from the cache, or null while it is (re)loaded; the view redraws when it comes in. */ private fun iconOrLoad(name: String): android.graphics.Bitmap? { SteamTouchBindings.cachedIcon(appId, name, ICON_PX)?.let { return it } if (iconsLoading.add(name)) { @@ -521,17 +463,15 @@ class SteamTouchControls( private fun parseColor(hex: String, fallback: String) = try { Color.parseColor(hex.ifEmpty { fallback }) } catch (_: IllegalArgumentException) { Color.parseColor(fallback) } - /** Steam Link's d-pad: four shield-shaped arms pointing at the centre, outlined. */ private fun drawDpad(canvas: Canvas, e: Element, x: Float, y: Float, r: Float) { val dirs = if (editing) 0 else dpadBits(e) val armVisuals = visuals[SteamTouchConfig.DPAD].orEmpty() val isSelected = editing && selected >= 0 && editElements.indexOf(e) == selected - val half = r * 0.36f // half the arm's width - val outer = r // the arm's far end - val inner = r * 0.1f // the point, near the centre - val shoulder = r * 0.42f // where the sides turn into the point + val half = r * 0.36f + val outer = r + val inner = r * 0.1f + val shoulder = r * 0.42f val corner = r * 0.12f - // Arms in report order: up, down, left, right, as (bit, rotation). val arms = listOf(DPAD_UP to 0f, DPAD_DOWN to 180f, DPAD_LEFT to 270f, DPAD_RIGHT to 90f) val oldStroke = stroke.color for ((armIndex, armEntry) in arms.withIndex()) { @@ -561,7 +501,6 @@ class SteamTouchControls( canvas.drawPath(path, stroke) canvas.restore() if (icon != null) { - // Upright, in the middle of the arm. val c = (outer + shoulder) / 2f val (ax, ay) = when (armIndex) { 0 -> x to y - c; 1 -> x to y + c; 2 -> x - c to y; else -> x + c to y } val s = half * 0.8f @@ -595,7 +534,6 @@ class SteamTouchControls( else -> "?" } - // ---- The report ---- private fun dpadBits(e: Element): Long { var bits = 0L @@ -604,7 +542,6 @@ class SteamTouchControls( val dy = f.y - cy(e) if (hypot(dx, dy) < radius(e) * 0.22f) return@forEach val angle = Math.toDegrees(atan2(-dy, dx).toDouble()).let { if (it < 0) it + 360 else it } - // Eight sectors: each direction covers 67.5°, diagonals press two. if (angle < 67.5 || angle > 292.5) bits = bits or DPAD_RIGHT if (angle in 22.5..157.5) bits = bits or DPAD_UP if (angle in 112.5..247.5) bits = bits or DPAD_LEFT @@ -633,7 +570,7 @@ class SteamTouchControls( buttons = buttons or if (right) STICK_RIGHT_TOUCHED else STICK_LEFT_TOUCHED } SteamTouchConfig.TRACKPAD_CENTER, SteamTouchConfig.TRACKPAD_LEFT, SteamTouchConfig.TRACKPAD_RIGHT -> { - val index = e.type - SteamTouchConfig.TRACKPAD_CENTER // centre, left, right + val index = e.type - SteamTouchConfig.TRACKPAD_CENTER val nx = ((f.x - (cx(e) - r)) / (2 * r)).coerceIn(0f, 1f) val ny = ((f.y - (cy(e) - radiusY(e))) / (2 * radiusY(e))).coerceIn(0f, 1f) pads[2 * index] = (((nx * 65535).toInt().coerceIn(0, 65535)) xor 0x8000).toShort() @@ -653,7 +590,6 @@ class SteamTouchControls( invalidate() } - // ---- Touch ---- @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { @@ -694,7 +630,6 @@ class SteamTouchControls( val f = fingers[event.getPointerId(i)] ?: continue f.x = event.getX(i) f.y = event.getY(i) - // A finger on a button can slide onto another, as on Steam Link. val e = f.element ?: continue if (BUTTON_BITS.containsKey(e.type)) { val over = hit(live(), f.x, f.y) @@ -726,12 +661,9 @@ class SteamTouchControls( return true } - // ---- Editing ---- val isEditing get() = editing - /** Moves, resizes and hides controls, action set by action set; Save writes them to the - * game's touch config. */ fun startEditing() { releaseAll() editing = true @@ -744,7 +676,6 @@ class SteamTouchControls( invalidate() } - // The action set being edited (its layout id), and the edits of every set visited. private var editSet = 1 private class EditState(val elements: MutableList<Element>, val hidden: MutableList<Element>) private val editSets = HashMap<Int, EditState>() @@ -752,7 +683,6 @@ class SteamTouchControls( private fun loadEditSet(id: Int) { editSet = id val state = editSets.getOrPut(id) { - // The device's action set numbering is the layout id. val available = config.availableFor(id, emptyList()) val shown = SteamTouchConfig.elementsFor(config, id, emptyList()) val els = available.mapNotNull { type -> @@ -816,7 +746,6 @@ class SteamTouchControls( val e = hit(editElements, x, y) selected = if (e != null) editElements.indexOf(e) else -1 if (e != null) dragOffset = (x - cx(e)) to (y - cy(e)) - // The D-pad's icons are per direction: a tap picks the one under the finger. if (e?.type == SteamTouchConfig.DPAD) { val dx = x - cx(e) val dy = y - cy(e) @@ -832,7 +761,6 @@ class SteamTouchControls( MotionEvent.ACTION_MOVE -> if (selected >= 0 && !trayOpen) { val e = editElements[selected] if (event.pointerCount >= 2 && pinchStart > 0f) { - // A pinch along one axis stretches that axis; a diagonal one scales both. val sx = abs(event.getX(0) - event.getX(1)) val sy = abs(event.getY(0) - event.getY(1)) val min = 40f * unit() @@ -850,7 +778,6 @@ class SteamTouchControls( MotionEvent.ACTION_POINTER_UP -> pinchStart = 0f MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { pinchStart = 0f - // Dropped back on the menu button: back into the tray, as on Steam Link. val e = editElements.getOrNull(selected) if (e != null && editMenuButton().contains(cx(e), cy(e)) && e.type != SteamTouchConfig.THUMB) { hidden += editElements.removeAt(selected).copy(visible = false) @@ -893,7 +820,6 @@ class SteamTouchControls( else -> label(type) } - /** Reset, copy and paste a layout, the layout's colour. */ private fun moreMenu() { val host = parent as? android.view.ViewGroup ?: return val density = resources.displayMetrics.density @@ -918,7 +844,6 @@ class SteamTouchControls( close = SteamTouchIconPicker.showPanel(host, context.getString(R.string.ctrl_layout), list, listOf(context.getString(R.string.common_close) to {})) } - /** A copied layout onto this action set: each control the set has takes the copied place. */ private fun pasteLayout(copied: String) { val placed = copied.split(';').mapNotNull { row -> val f = row.split(',') @@ -932,30 +857,21 @@ class SteamTouchControls( selected = -1 } - // ---- Steam Link's menu ---- - /** The menu button: Steam Link's touch menu. */ private fun openMenu() { releaseAll() menuOpen = true invalidate() } - // ---- Steam Link's panels: the touch menu and the layout tray ---- - // - // Drawn here, on this view, to Steam Link's measurements (a 624x400 panel in its reference - // pixels, navy with a blue top rule, blue tiles), so that controls can be dragged out of the tray - // onto the screen as on Steam Link. private var menuOpen = false private var trayOpen = false - /** A hit area on a panel and what it does. */ private class Hot(val rect: RectF, val onTap: () -> Unit, val onDown: ((Float, Float) -> Unit)? = null) private val hots = mutableListOf<Hot>() private var trayDrag: Element? = null - // DroidDeck's own palette (ui/Theme.kt, the default theme). private val surface = Color.rgb(18, 20, 23) private val surfaceVariant = Color.rgb(26, 29, 34) private val line = Color.rgb(38, 42, 49) @@ -965,7 +881,6 @@ class SteamTouchControls( private val accent = Color.rgb(26, 159, 255) private val onAccent = Color.rgb(3, 17, 31) - /** The panel's rectangle and its scale (screen pixels per reference pixel). */ private fun panelFrame(): Pair<RectF, Float> { val s = min(width * 0.8f / 624f, height * 0.78f / 400f) val w = 624f * s @@ -995,8 +910,6 @@ class SteamTouchControls( canvas.drawText(str, x, y - (p.descent() + p.ascent()) / 2, p) } - /** A button in DroidDeck's style: the accent filled for the main action, a quiet outlined - * surface otherwise. */ private fun blueButton(canvas: Canvas, r: RectF, text: String, s: Float, enabled: Boolean = true, primary: Boolean = false, onTap: () -> Unit) { val radius = 12f * s if (primary && enabled) { @@ -1010,7 +923,6 @@ class SteamTouchControls( if (enabled) hots += Hot(r, onTap) } - /** A toggle: quiet when off, accent-tinted with an accent glyph when on. */ private fun iconButton(canvas: Canvas, r: RectF, active: Boolean, onTap: () -> Unit, glyph: (Canvas, RectF, Int) -> Unit) { val radius = 10f * (r.height() / 40f) canvas.drawRoundRect(r, radius, radius, Paint(Paint.ANTI_ALIAS_FLAG).apply { @@ -1055,12 +967,10 @@ class SteamTouchControls( } } - // -- The touch menu (Steam Link: CVirtualController's choose-mode screen) -- private fun drawMenu(canvas: Canvas) { val (f, s) = panelFrame() canvas.panel(f, s) - // Header: power (DroidDeck's own menu: stop, settings), the Steam mark, what is running, close. iconButton(canvas, ref(f, s, 494f, 18f, 531f, 55f), false, { menuOpen = false; onMenu() }) { c, r, col -> drawPower(c, r, col) } drawMark(canvas, f.left + 104f * s, f.top + 37f * s, 13f * s) text(canvas, context.getString(R.string.touch_title), f.left + 124f * s, f.top + 30f * s, 14f * s, onSurface, align = Paint.Align.LEFT) @@ -1079,7 +989,6 @@ class SteamTouchControls( tile(canvas, ref(f, s, 271f, 81f, 475f, 271f), s, context.getString(R.string.touch_mouse), mouse, { setModes(controller || mouse, !mouse) }) { c, r, col -> drawMouse(c, r, col, crossed = false) } - // Under the controller: gyroscope, fade, vibrate on touch. iconButton(canvas, ref(f, s, 45f, 278f, 91f, 318f), gyroOn, { gyroOn = !gyroOn; device.motionEnabled = gyroOn; prefs.edit().putBoolean("gyro", gyroOn).apply() }) { c, r, col -> drawGyro(c, r, col) } @@ -1092,7 +1001,6 @@ class SteamTouchControls( iconButton(canvas, ref(f, s, 204f, 278f, 249f, 318f), feedback, { feedback = !feedback; prefs.edit().putBoolean("feedback", feedback).apply() }) { c, r, col -> drawHaptic(c, r, col) } - // Under the mouse: direct touch, trackpad, trackpad speed. val relative = options.mouseMode == SteamTouchConfig.MOUSE_RELATIVE iconButton(canvas, ref(f, s, 271f, 278f, 333f, 318f), mouse && !relative, { setOptions(options.copy(mouseMode = SteamTouchConfig.MOUSE_ABSOLUTE)) @@ -1116,8 +1024,6 @@ class SteamTouchControls( private val audio by lazy { context.getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager } private var volumeSlider = RectF() - /** Steam Link's audio column: the speaker (mute), its volume slider - the device's media - * volume here - and the microphone, which DroidDeck does not stream, shown off. */ private fun drawVolume(canvas: Canvas, f: RectF, s: Float) { val stream = android.media.AudioManager.STREAM_MUSIC val maxVol = audio.getStreamMaxVolume(stream).coerceAtLeast(1) @@ -1136,7 +1042,6 @@ class SteamTouchControls( canvas.drawRoundRect(RectF(inner.left, knobY, inner.right, inner.bottom), inner.width() / 2, inner.width() / 2, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = accent }) canvas.drawCircle(inner.centerX(), knobY, inner.width() / 2 * 1.05f, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = onSurface }) - // The mic's slider, dimmed. val micTrack = ref(f, s, 539f, 133f, 577f, 372f) canvas.drawRoundRect(micTrack, micTrack.width() / 2, micTrack.width() / 2, Paint(stroke).apply { color = Color.argb(60, 255, 255, 255) }) hots += Hot(track, {}) { _, y -> setVolumeAt(y) } @@ -1171,7 +1076,6 @@ class SteamTouchControls( c.drawLine(x - u * 2.4f, y - u * 2.4f, x + u * 2.4f, y + u * 2.4f, glyphPaint(Color.rgb(200, 40, 40), r.height() * 0.07f)) } - /** A mode card: tap to turn it on or off; the accent marks it on. */ private fun tile(canvas: Canvas, r: RectF, s: Float, title: String, on: Boolean, toggle: () -> Unit, art: (Canvas, RectF, Int) -> Unit) { val radius = 14f * s canvas.drawRoundRect(r, radius, radius, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = surfaceVariant }) @@ -1179,7 +1083,6 @@ class SteamTouchControls( color = if (on) accent else line; style = Paint.Style.STROKE; strokeWidth = (if (on) 2f else 1.2f) * s }) text(canvas, title, r.left + 16f * s, r.top + 22f * s, 14f * s, onSurface, align = Paint.Align.LEFT, maxWidth = r.width() - 72f * s) - // A switch, top right. val sw = RectF(r.right - 50f * s, r.top + 12f * s, r.right - 14f * s, r.top + 32f * s) canvas.drawRoundRect(sw, sw.height() / 2, sw.height() / 2, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = if (on) accent else line2 }) canvas.drawCircle(if (on) sw.right - sw.height() / 2 else sw.left + sw.height() / 2, sw.centerY(), sw.height() * 0.38f, @@ -1190,7 +1093,6 @@ class SteamTouchControls( hots += Hot(r, toggle) } - // -- The layout tray (Steam Link: CVirtualController's tray) -- private fun drawTray(canvas: Canvas) { val (f, s) = panelFrame() @@ -1246,7 +1148,6 @@ class SteamTouchControls( item(SteamTouchConfig.MACRO_0 + i, 519f, 228f + 43f * i, 14f) item(SteamTouchConfig.MACRO_0 + 4 + i, 562f, 228f + 43f * i, 14f) } - // The colour wheel: the layout's colour and opacity. val wheel = ref(f, s, 511f, 91f, 573f, 153f) canvas.drawCircle(wheel.centerX(), wheel.centerY(), wheel.width() / 2f, Paint(Paint.ANTI_ALIAS_FLAG).apply { shader = android.graphics.SweepGradient(wheel.centerX(), wheel.centerY(), @@ -1270,7 +1171,6 @@ class SteamTouchControls( hots += Hot(RectF(r.left - r.height() * 0.4f, r.top - r.height() * 0.6f, r.right + r.height() * 0.4f, r.bottom + r.height() * 0.6f), onTap) } - /** A control taken out of the tray: on screen under the finger, following it until it lifts. */ private fun beginTrayDrag(type: Int, x: Float, y: Float) { val available = config.availableFor(editSet, emptyList()) val existing = editElements.indexOfFirst { it.type == type } @@ -1286,7 +1186,6 @@ class SteamTouchControls( invalidate() } - // -- While editing: Steam Link's menu button, and a small bar for the selected control -- private fun editMenuButton(): RectF { val r = 37f * unit() @@ -1340,7 +1239,6 @@ class SteamTouchControls( invalidate() } - // -- Glyphs for the menu's buttons -- private fun glyphPaint(col: Int, w: Float) = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = col; style = Paint.Style.STROKE; strokeWidth = w; strokeCap = Paint.Cap.ROUND; strokeJoin = Paint.Join.ROUND @@ -1430,8 +1328,6 @@ class SteamTouchControls( Paint(Paint.ANTI_ALIAS_FLAG).apply { color = col }) } - /** New per-game input options: applied now, and saved into the game's touch layout as Steam - * Link saves them, so they follow the game and the account. */ private fun setOptions(next: SteamTouchConfig.Options) { pendingOptions = next options = next @@ -1449,7 +1345,6 @@ class SteamTouchControls( } } - /** The selected control's icon, label and colours (a D-pad direction: the one tapped). */ private fun pickIcon(e: Element) { val list = visuals[e.type].orEmpty() val visual = list.getOrNull(if (e.type == SteamTouchConfig.DPAD) selectedArm else 0) @@ -1470,7 +1365,6 @@ class SteamTouchControls( }.show() } - /** The layout's colour and opacity, as Steam Link's colour picker sets them. */ private fun pickColor() { val current = pendingColor ?: SteamTouchConfig.layoutColor(config, actionSet) ?: floatArrayOf(1f, 1f, 1f, 0.45f) val density = resources.displayMetrics.density @@ -1521,8 +1415,6 @@ class SteamTouchControls( } val color = pendingColor val colors = if (color != null) sets.keys.associateWith { color } else emptyMap() - // A picked icon replaces the label, icon and colours of every binding of that input; what - // the binding does stays as it is. val bindings = pendingBindings.mapValues { (_, picked) -> { old: SteamTouchBindings.Binding -> old.copy(label = picked.label, icon = picked.icon, foreground = picked.foreground, background = picked.background) @@ -1550,7 +1442,6 @@ class SteamTouchControls( } } - /** Called after a save: the client reads the config again when the controller reconnects. */ var onSaved: () -> Unit = {} companion object { @@ -1558,7 +1449,6 @@ class SteamTouchControls( private const val POLL_MS = 150L const val BIG_PICTURE = 769 private const val ICON_PX = 128 - // artwork/droiddeck-mark.svg: the D's two halves, the orb's ring cut out (Wordmark.kt). private const val MARK_D = "M-31.98 0H56.055V40.489A71.81 71.81 0 0 0 56.055 183.231V223.72H-31.98Z" + "M71.885 0.28A111.86 111.86 0 0 1 71.885 223.44V183.234A71.81 71.81 0 0 0 71.885 40.486Z" private const val FADE_AFTER_MS = 5000L @@ -1573,7 +1463,6 @@ class SteamTouchControls( private const val STICK_RIGHT_TOUCHED = 1L shl 47 private val TRACKPAD_TOUCHED = longArrayOf(1L shl 27, 1L shl 19, 1L shl 20) - /** Steam Link's button bits (CVirtualController's table, by element type). */ private val BUTTON_BITS: Map<Int, Long> = buildMap { put(SteamTouchConfig.STEAM, 1L shl 13) put(SteamTouchConfig.JOYSTICK_LEFT_BUTTON, 1L shl 22) @@ -1591,7 +1480,6 @@ class SteamTouchControls( for (i in 0 until 8) put(SteamTouchConfig.MACRO_0 + i, 1L shl (32 + i)) put(SteamTouchConfig.MACRO_1_FINGER, 1L shl 48) put(SteamTouchConfig.MACRO_2_FINGER, 1L shl 49) - // Not bits: the menu and the keyboard are the app's own. put(SteamTouchConfig.THUMB, 0L) put(SteamTouchConfig.KEYBOARD, 0L) put(SteamTouchConfig.PASTE, 0L) diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchDevice.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchDevice.kt index f0c30b971..2d63358d3 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchDevice.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchDevice.kt @@ -7,20 +7,8 @@ import java.nio.ByteOrder import java.nio.ByteBuffer import java.nio.channels.FileChannel -/** - * Steam's touch controller, as the app's side of it. - * - * libfakeinput offers the Steam client a HID device with the ids of Steam Link's touch controls - * (0000:11fb), and the client builds its own touch controller for it: Steam Input's Mobile Touch - * type, with the game's touch config, action sets, community configs and cloud sync all Steam's - * (docs/development/steam-touch-controller.md). This class is the file the two share - * (fakeinput_steam.cpp: TouchRingFile): the app writes the device's 40-byte input report and - * whether it is plugged in; libfakeinput writes back what the client told the device - the app and - * action set it is on, its layers, rumble. - */ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { - /** What the client last told the device. */ data class State( val opened: Int, val appId: Int, @@ -29,13 +17,9 @@ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { val actionSeq: Int, ) - // The input report (Steam Link's CVirtualController, docs/development/steam-touch-controller.md): - // bytes 0-7 buttons, 8-15 sticks, 16-27 trackpads, 28-33 accelerometer, 34-39 gyro. private val report = ByteArray(REPORT_BYTES) private var reportSeq = 0L - /** The controls part of the report: button bits, sticks (LX, LY, RX, RY, already in the report's - * units) and trackpads (centre, left, right: x, y each). */ @Synchronized fun setControls(buttons: Long, sticks: ShortArray, pads: ShortArray) { val next = report.copyOf() @@ -45,27 +29,19 @@ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { publish(next) } - /** Motion in the Deck's axes and units (PadMotion), which the touch report shares. */ @Synchronized fun setMotion(accel: ShortArray, gyro: ShortArray) { - // Sent only while the client asks for it (setting report 0x30, which Steam Link reads as - // "gyro on") and the player has not turned motion off - as Steam Link does. if (!plugged || !gyroRequested || !motionEnabled) return val next = report.copyOf() for (i in 0 until 3) { - // Accelerometer: the Deck's 1 g = 16384 is the touch report's ±2 g = ±32767 already. putShort(next, 28 + 2 * i, accel[i]) - // Gyro: the Deck's 2000 °/s = 32768; the touch report's full scale is 1000 °/s - // (Steam Link: rad/s × 180/π / 1000 × 32767). putShort(next, 34 + 2 * i, (gyro[i] * 2).coerceIn(-32767, 32767).toShort()) } publish(next) } - /** Whether the client has asked the device for motion (setting report 0x30 non-zero). */ val gyroRequested get() = buffer.getInt(OFF_SETTING30) != 0 - /** The player's motion switch (Steam Link's gyroscope option). */ @Volatile var motionEnabled = true set(value) { field = value @@ -81,7 +57,6 @@ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { into[at + 1] = (value.toInt() shr 8).toByte() } - /** Writes the report under the seqlock libfakeinput reads it with. */ private fun publish(next: ByteArray) { if (next.contentEquals(report)) return next.copyInto(report) @@ -122,7 +97,7 @@ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { private const val TAG = "SteamTouch" const val REPORT_BYTES = 40 private const val SIZE = 256 - private const val MAGIC = 0x31484354 // TCH1 + private const val MAGIC = 0x31484354 private const val OFF_REPORT_SEQ = 8 private const val OFF_REPORT = 16 private const val OFF_PLUGGED = 56 @@ -140,10 +115,8 @@ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { var current: SteamTouchDevice? = null private set - /** The shared file in the session's pad directory, made fresh each session. */ fun ringFile(fakeInputDir: File) = File(fakeInputDir, "touchctl") - /** Creates the file for a new session (unplugged) and maps it. */ fun prepare(fakeInputDir: File): File? = try { val file = ringFile(fakeInputDir) file.delete() diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt index fdd6f423f..17d665f50 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt @@ -24,11 +24,6 @@ import android.widget.TextView import com.droiddeck.launcher.R import java.util.concurrent.Executors -/** - * Picks a touch control's icon, label and colours the way Steam's configurator does: an icon from - * the game's own `TouchMenuIcons` and Steam's binding icon library, a foreground and a background - * from Steam's palette. [onPicked] gets the binding with the new fields (the action untouched). - */ class SteamTouchIconPicker( private val context: Context, private val host: ViewGroup, @@ -47,7 +42,6 @@ class SteamTouchIconPicker( private fun dp(v: Int) = (v * density).toInt() fun show() { - // Side by side, for a landscape screen: what is picked on the left, the icons on the right. val root = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL; setPadding(dp(16), dp(8), dp(16), 0) } val left = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL } val label = EditText(context).apply { @@ -132,8 +126,6 @@ class SteamTouchIconPicker( } } - /** The icon as Steam draws it: on a disc of the background colour, filled with the foreground - * through its own alpha. */ private fun render(icon: Bitmap, size: Int): Bitmap { val out = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) val canvas = Canvas(out) @@ -148,7 +140,6 @@ class SteamTouchIconPicker( private val loading = HashSet<String>() private var refreshQueued = false - /** Loads an icon in the background; the grid redraws from the cache once some have come in. */ private fun load(name: String) { if (!loading.add(name)) return loader.execute { @@ -189,7 +180,6 @@ class SteamTouchIconPicker( return view } - // One thumbnail per icon and colour, reused when the grid rebinds. private val thumbs = android.util.LruCache<String, Bitmap>(64) private fun tinted(name: String, icon: Bitmap): Bitmap { @@ -204,10 +194,6 @@ class SteamTouchIconPicker( } companion object { - /** - * A panel over the session, in the session's own window: a dialog's window of its own left - * new bitmaps and layers undrawn in the session window afterwards on the Thor. - */ fun showPanel(host: ViewGroup, title: String, content: View, buttons: List<Pair<String, () -> Unit>>, onClose: () -> Unit = {}): () -> Unit { val context = host.context val density = context.resources.displayMetrics.density @@ -217,7 +203,6 @@ class SteamTouchIconPicker( isClickable = true isFocusable = true } - // DroidDeck's panel: dark surface, rounded, a hairline border. val card = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL background = GradientDrawable().apply { @@ -266,8 +251,6 @@ class SteamTouchIconPicker( return { if (scrim.parent != null) close() } } - /** Steam's binding icon look (steamui: the icon multiplied with its foreground colour, - * background-blend-mode: multiply, keeping the icon's own alpha). */ fun drawSteamIcon(canvas: Canvas, icon: Bitmap, into: android.graphics.RectF, foreground: Int, paint: Paint) { val tint = Paint(paint).apply { colorFilter = PorterDuffColorFilter(foreground, PorterDuff.Mode.MULTIPLY) } canvas.drawBitmap(icon, null, into, tint) diff --git a/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt b/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt index a3ab87e5a..2dc403a43 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt @@ -5,30 +5,10 @@ import android.os.Looper import android.view.MotionEvent import kotlin.math.abs -/** - * The screen as a touchpad, the way Bannerlator's desktop containers work: the arrow is where it - * was, and a finger dragged anywhere moves it from there. - * - * - drag one finger .................. moves the pointer (relative, with a little acceleration) - * - tap .............................. left click, where the pointer is - * - second finger taps while one holds right click - * - two-finger tap ................... right click - * - two fingers up / down ............ scroll wheel - * - hold still, then drag ............ left-button drag - * - double-tap, hold, drag ........... left-button drag as well - * - one finger held, another slides .. left-button drag, as on a laptop's pad: the held finger is - * the button, the other moves the pointer (a window by its - * title bar or edge, a desktop icon). The sliding finger may - * lift and slide again; lifting the held one drops. - * - * Positions handed to the listener are the pointer's, in the view's pixels; the caller keeps them - * inside the picture. - */ class TouchpadGestures( private val slopPx: Float, private val listener: PointerGestures.Listener, ) { - /** The pointer, tracked here since every move is relative. */ var x = 0f private set var y = 0f @@ -49,7 +29,6 @@ class TouchpadGestures( private var twoFingers = false private var scrollAnchorY = 0f private var scrolled = false - // Hold-and-slide: the first finger stays as the button, the second moves the pointer. private var anchorId = -1 private var anchorStartX = 0f private var anchorStartY = 0f @@ -106,7 +85,6 @@ class TouchpadGestures( downAt = event.eventTime travelled = 0f; held = false; twoFingers = false; scrolled = false; secondWasTap = false anchorId = event.getPointerId(0); moverId = -1; holdDrag = false - // A tap shortly before this touch: holding now drags (double-tap-and-hold). if (event.eventTime - lastTapAt < DOUBLE_TAP_MS) { dragging = true listener.onButton(PointerGestures.BTN_LEFT, true, x, y) @@ -127,7 +105,6 @@ class TouchpadGestures( anchorStartX = event.getX(it); anchorStartY = event.getY(it) } } - // Mid-drag, a finger coming back only picks the sliding up again. if (holdDrag) return true twoFingers = true secondDownAt = event.eventTime @@ -140,8 +117,6 @@ class TouchpadGestures( val mi = if (moverId == -1) -1 else event.findPointerIndex(moverId) if (ai >= 0 && mi >= 0) { val mx = event.getX(mi); val my = event.getY(mi) - // One finger still, the other on the move: that is a drag, not a scroll (which - // moves both). Decided once, before anything has scrolled. if (!holdDrag && !scrolled) { val anchorMoved = abs(event.getX(ai) - anchorStartX) + abs(event.getY(ai) - anchorStartY) val moverMoved = abs(mx - moverStartX) + abs(my - moverStartY) @@ -165,7 +140,6 @@ class TouchpadGestures( return true } } else if (holdDrag) { - // Only the held finger is down: the drag waits for the next slide. return true } if (twoFingers && event.pointerCount >= 2) { @@ -186,7 +160,6 @@ class TouchpadGestures( travelled += abs(dx) + abs(dy) if (travelled > slopPx) { handler.removeCallbacks(longPress) - // A little acceleration: a slow finger is precise, a fast one crosses the screen. val speed = (abs(dx) + abs(dy)) val gain = sensitivity * (1f + minOf(speed / 40f, 1.5f)) place(x + dx * gain, y + dy * gain) @@ -198,7 +171,6 @@ class TouchpadGestures( val gone = event.getPointerId(event.actionIndex) if (holdDrag) { if (gone == anchorId) { - // The held finger lifted: drop. The other finger carries on as the pointer. listener.onButton(PointerGestures.BTN_LEFT, false, x, y) dragging = false; holdDrag = false; twoFingers = false anchorId = moverId; moverId = -1 @@ -213,13 +185,11 @@ class TouchpadGestures( } if (gone == moverId) moverId = -1 if (gone == anchorId) { - // The first finger left a two-finger gesture: the other one is the pointer now. anchorId = event.getPointerId(if (event.actionIndex == 0) 1 else 0) event.findPointerIndex(anchorId).takeIf { it >= 0 }?.let { lastX = event.getX(it); lastY = event.getY(it) } } - // The second finger lifting quickly, with nothing scrolled: a right click. if (twoFingers && secondWasTap && event.eventTime - secondDownAt < TAP_MS && !scrolled) { click(PointerGestures.BTN_RIGHT) secondWasTap = false @@ -232,7 +202,6 @@ class TouchpadGestures( when { dragging -> listener.onButton(PointerGestures.BTN_LEFT, false, x, y) twoFingers -> { - // Both fingers down and up together, no scroll: a right click. if (secondWasTap && !scrolled && !cancelled && event.eventTime - downAt < TAP_MS * 2) { click(PointerGestures.BTN_RIGHT) } diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/AppImageManager.kt b/app/src/main/java/com/droiddeck/launcher/runtime/AppImageManager.kt index 201acb1eb..4fe191a21 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/AppImageManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/AppImageManager.kt @@ -11,14 +11,6 @@ import com.droiddeck.launcher.core.FileUtils import java.io.File import java.io.RandomAccessFile -/** - * AppImages the user brings: picked from storage, checked, and extracted once into the runtime at - * /opt/appimages/user/<id>/app. proot has no FUSE to mount an image, and extracting it at every - * start would cost its whole size in /tmp each time. Each one gets a menu entry on the Linux - * desktop, and the front end starts it under gamescope through droiddeck-appimage-run. - * - * <id>/name holds the name to show, <id>/icon.png the icon when the image has a PNG one. - */ object AppImageManager { private const val TAG = "AppImageManager" const val GUEST_DIR = "/opt/appimages/user" @@ -29,7 +21,6 @@ object AppImageManager { val guestDir: String get() = "$GUEST_DIR/$id" } - /** Why a file cannot be imported: [text] with its [args]. */ internal class Problem(@StringRes val text: Int, vararg val args: Any) { fun describe(context: Context): String = context.getString(text, *args) } @@ -47,10 +38,6 @@ object AppImageManager { ) }?.sortedBy { it.name.lowercase() } ?: emptyList() - /** - * What is wrong with [file] as an AppImage for this device, or null when it can be imported. - * An AppImage is an ELF program (its runtime) with "AI" and the format version at byte 8. - */ internal fun problem(file: File): Problem? { if (!file.isFile) return Problem(R.string.user_apps_file_gone) val head = ByteArray(20) @@ -71,7 +58,6 @@ object AppImageManager { } } - /** A directory name from the image's file name: "Some_App-1.2-aarch64.AppImage" -> "some-app-1-2-aarch64". */ internal fun idFor(fileName: String, taken: Set<String>): String { val base = fileName.removeSuffix(".AppImage").removeSuffix(".appimage").lowercase() .replace(Regex("[^a-z0-9]+"), "-").trim('-').take(48).ifEmpty { "appimage" } @@ -83,17 +69,12 @@ object AppImageManager { fun dir(context: Context, id: String) = File(root(context), id) - /** - * Imports [file]; null on success, else what went wrong. [name] and [icon] replace the image's own; - * each of [meta] is written as a file beside them. - */ fun import( context: Context, file: File, name: String?, icon: File?, meta: Map<String, String> = emptyMap(), onProgress: (String) -> Unit, ): String? { if (!LinuxRuntime.isInstalled(context)) return context.getString(R.string.user_apps_runtime_required) problem(file)?.let { return it.describe(context) } val base = root(context).apply { mkdirs() } - // Room for the image while it is extracted, and the extracted tree (about the same again). if (StatFs(base.path).availableBytes < file.length() * 3) { return context.getString(R.string.appimg_no_space_import, FileUtils.sizeToString(file.length() * 3)) } @@ -114,7 +95,6 @@ object AppImageManager { } } - /** Swaps the program of imported [id] for [file], keeping its name and icon; the old one stays if this fails. */ fun replace(context: Context, id: String, file: File, meta: Map<String, String>, onProgress: (String) -> Unit): String? { problem(file)?.let { return it.describe(context) } val dir = dir(context, id) @@ -134,14 +114,11 @@ object AppImageManager { } } - /** Extracts [file] to [dir]/app, replacing what is there only once the extraction has worked. */ private fun extract(context: Context, dir: File, file: File, onProgress: (String) -> Unit): String? { val image = File(dir, "image.AppImage") val links = File(dir, ".links").apply { mkdirs() } try { dir.mkdirs() - // Copied in, not run where it lies: shared storage is mounted noexec, and proot's loader - // maps a program executable. onProgress(context.getString(R.string.user_apps_copying, file.name)) file.inputStream().use { input -> image.outputStream().use { FileUtils.copy(input, it) } } image.setExecutable(true, false) @@ -149,8 +126,6 @@ object AppImageManager { val unpack = if (LinuxFex.isX86(LinuxFex.elfMachine(image))) "${LinuxFex.TOOL} extract ./image.AppImage" else "./image.AppImage --appimage-extract || { rm -rf squashfs-root AppDir; ${LinuxFex.TOOL} extract ./image.AppImage; }" val out = StringBuilder() - // uruntime's DwarFS images unpack into AppDir with squashfs-root a link to it, and keep - // one program under several names as hard links (sharun), which Android denies apps. val status = GuestCommand.run(context, listOf( "/bin/bash", "-c", "cd \"$1\" && rm -rf squashfs-root AppDir && { $unpack; } >/dev/null && " + @@ -170,10 +145,6 @@ object AppImageManager { } } - /** - * Each name proot's link2symlink left for a hard link becomes a file of its own: a program run - * through a link would see the shared file's name, and sharun picks what to start by its own. - */ private fun copyLinkedFiles(app: File, links: File) { val roots = setOf(links.path + "/", links.canonicalPath + "/") app.walkTopDown().onEnter { !Files.isLink(it) }.forEach { f -> @@ -187,7 +158,6 @@ object AppImageManager { } } - /** Name, comment and icon from the image's own desktop entry; the file name when it has none. */ private fun describe(dir: File, source: File) { val app = File(dir, "app") val entry = app.listFiles { f -> f.isFile && f.name.endsWith(".desktop") }?.firstOrNull() @@ -200,11 +170,6 @@ object AppImageManager { restoreIcon(dir) } - /** - * The image's own icon as [dir]'s: of the files its entry names, beside it or anywhere in its - * icon theme, a PNG of at least [SHARP_ICON] px, else the SVG, else the largest PNG; .DirIcon - * without any. - */ internal fun restoreIcon(dir: File) { val app = File(dir, "app") val entry = app.listFiles { f -> f.isFile && f.name.endsWith(".desktop") }?.firstOrNull() @@ -229,7 +194,6 @@ object AppImageManager { f.inputStream().use { val b = ByteArray(4); it.read(b) == 4 && b[1] == 'P'.code.toByte() && b[2] == 'N'.code.toByte() && b[3] == 'G'.code.toByte() } } catch (e: Exception) { false } - /** A key of a desktop entry's main group, unlocalised. */ internal fun desktopKey(desktop: String, key: String): String? { var inMain = false for (line in desktop.lineSequence()) { @@ -241,11 +205,8 @@ object AppImageManager { } private fun menuEntry(context: Context, id: String) = - // A system directory, not ~/.local/share/applications: the desktop copies game entries from - // here into the user's with a GPU launcher (droiddeck-desktop-gpu), since it composites in software. File(LinuxRuntime.rootDir(context), "usr/local/share/applications/droiddeck-appimage-$id.desktop") - /** The Linux desktop's menu entry: the desktop's own Exec rules (and droiddeck-gpu) apply to it. */ internal fun writeMenuEntry(context: Context, dir: File) { val id = dir.name val name = FileUtils.readString(File(dir, "name"))?.trim() ?: id @@ -256,7 +217,6 @@ object AppImageManager { "Terminal=false\nCategories=$categories\nX-DroidDeck-AppImage=$id\n") } - /** The desktop's import of [key]; older ones of the image at [path], since replaced, are removed. */ fun desktopImport(context: Context, key: String, path: String): String? { var found: String? = null root(context).listFiles()?.forEach { dir -> diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt b/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt index c733c66da..777507a7a 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt @@ -18,26 +18,12 @@ import java.io.IOException import java.nio.file.Files import java.util.concurrent.atomic.AtomicInteger -/** - * Flatpak's sandboxes, started for the runtime's bwrap stand-in (droiddeck-bwrap). - * - * A Flatpak app runs with its runtime as /usr and itself as /app, which bwrap arranges with mount - * namespaces Android gives no app. proot can show the same picture, and started here - beside the - * session's proot rather than inside it - it traces the app once instead of twice. The stand-in - * sends the sandbox Flatpak asked for (its binds, directories, symlinks and files, in guest paths), - * this builds it under the cache and starts it, then streams the output, the pid and the exit - * status back until one side goes away: the stand-in ending takes the sandbox down with it. - * - * The socket is abstract, so anything on the device could connect: only this app's own uid - the - * uid every program in the runtime already runs as - is served. - */ object BwrapSpawner { private const val TAG = "BwrapSpawner" fun socket(context: Context): String = "${context.packageName}.bwrap" private val seq = AtomicInteger() @Volatile private var server: LocalServerSocket? = null - /** Listens for sandboxes for the rest of the process's life; later calls do nothing. */ @Synchronized fun start(context: Context) { if (server != null) return @@ -89,7 +75,6 @@ object BwrapSpawner { } } - /** What proot is started with: the sandbox root, its binds (host to guest), and the program. */ class Plan(val root: File, val binds: List<Bind>, val cwd: String, val argv: List<String>, val env: Map<String, String>) data class Bind(val host: String, val guest: String) { @@ -103,14 +88,9 @@ object BwrapSpawner { } } - /** True when [path] is [dir] or inside it. */ internal fun under(path: String, dir: String): Boolean = dir == "/" || path == dir || path.startsWith(if (dir.endsWith("/")) dir else "$dir/") - /** - * Where a path the session's programs see lives on the host: through the session's own binds - * (the most specific one), or else inside the rootfs. - */ internal fun hostPath(sessionBinds: List<Bind>, rootfs: String, guest: String): String { val bind = sessionBinds.filter { under(guest, it.guest) }.maxByOrNull { it.guest.length } ?: return rootfs.trimEnd('/') + guest @@ -118,11 +98,6 @@ object BwrapSpawner { return if (rest.isEmpty()) bind.host else bind.host.trimEnd('/') + "/" + rest } - /** - * Guest [src] bound at [dest]: the bind itself, then every session bind below [src] moved - * under [dest] - /dev brings the session's /dev/shm and GPU node, /proc its stand-ins for what - * Android keeps apps from reading. - */ internal fun bindsFor(sessionBinds: List<Bind>, rootfs: String, src: String, dest: String): List<Bind> { val out = ArrayList<Bind>() out.add(Bind(hostPath(sessionBinds, rootfs, src), dest)) @@ -146,12 +121,6 @@ object BwrapSpawner { } fun coveringBind(dest: String) = binds.filter { under(dest, it.guest) }.maxByOrNull { it.guest.length } fun newScratch(): File = File(dir, "x${scratch++}") - /** - * Where [dest] can be created on the host: inside the sandbox root, or inside a directory - * of ours bound into it (a --tmpfs under the runtime's /usr, which Flatpak fills with the - * GL extension's links). Null inside anything else bound in - the runtime itself, a host - * directory - which must not be written to. - */ fun writable(dest: String): File? { val cover = coveringBind(dest) ?: return inRoot(root, dest) if (!under(cover.host, dir.path)) return null @@ -199,8 +168,6 @@ object BwrapSpawner { link.parentFile?.mkdirs() if (!Files.isSymbolicLink(link.toPath()) && !link.exists()) Files.createSymbolicLink(link.toPath(), java.nio.file.Paths.get(target)) } else { - // A link inside the runtime's own tree cannot be made; what it points at is - // bound in its place. val abs = if (target.startsWith("/")) target else normalize(dest.substringBeforeLast('/') + "/" + target) val host = hostPath(binds, root.path, abs) if (File(host).exists()) addBind(Bind(host, dest)) else Log.w(TAG, "symlink $dest -> $target: nothing there; left out") @@ -218,24 +185,13 @@ object BwrapSpawner { return Plan(root, binds, request.optString("cwd", "/").ifEmpty { "/" }, argv, env) } - /** Where a sandbox finds the runtime's own GPU driver. */ private const val GPU_DIR = "/run/droiddeck-gpu" - /** - * The GPU, for an app that is given /dev. Flathub's Mesa has Turnip, but built for the DRM - * kernel driver: Adreno on Android is KGSL, so apps fell back to drawing on the CPU. The - * runtime's own Turnip has the KGSL backend and needs nothing the Freedesktop runtime lacks - * but three libraries, which come with it; Mesa's GL then runs on it through Zink. An app's - * own choice of driver (VK_ICD_FILENAMES, MESA_LOADER_DRIVER_OVERRIDE) is left alone. - */ private fun gpu(rootfs: String, root: File, env: MutableMap<String, String>): List<Bind>? { val lib = File(rootfs, "usr/lib") val driver = File(lib, "libvulkan_freedreno.so") if (!driver.isFile || !File("/dev/kgsl-3d0").exists()) return null if (env.containsKey("VK_ICD_FILENAMES") || env.containsKey("VK_DRIVER_FILES")) return null - // A window on the desktop gets shared-memory buffers only (KWin composites in software); - // Zink cannot present there, so such an app keeps Flathub's own Mesa (llvmpipe). From the - // desktop, games go through droiddeck-gpu's gamescope and do get the GPU. if (env["BL_DESKTOP"] == "1" && env["GAMESCOPE_WAYLAND_DISPLAY"] == null) return null val binds = ArrayList<Bind>() for (name in listOf("libvulkan_freedreno.so", "libdisplay-info.so.3", "libSPIRV-Tools.so", "libSPIRV-Tools-opt.so")) { @@ -243,7 +199,6 @@ object BwrapSpawner { if (!f.exists()) { Log.w(TAG, "gpu: the runtime has no $name; apps draw on the CPU"); return null } binds.add(Bind(f.canonicalPath, "$GPU_DIR/$name")) } - // Beside the sandbox root, in the sandbox's own directory: removed with it. val icd = File(root.parentFile, "freedreno_icd.json") icd.writeText("{\"file_format_version\": \"1.0.0\", \"ICD\": {\"library_path\": \"$GPU_DIR/libvulkan_freedreno.so\", \"api_version\": \"1.4.0\"}}\n") binds.add(Bind(icd.path, "$GPU_DIR/freedreno_icd.json")) @@ -253,18 +208,10 @@ object BwrapSpawner { env["MESA_LOADER_DRIVER_OVERRIDE"] = "zink" env["GALLIUM_DRIVER"] = "zink" } - // The driver's own libraries, found beside it; nothing else is in that directory. env["LD_LIBRARY_PATH"] = listOf(env["LD_LIBRARY_PATH"], GPU_DIR).filter { !it.isNullOrEmpty() }.joinToString(":") return binds } - /** - * What every program in the session preloads (SessionFiles writes /etc/ld.so.preload): the - * session shim - SysV shared memory for X11, the network calls Android refuses - and the - * controller reader. A sandbox's /etc is the runtime's, so the same libraries go in through - * LD_PRELOAD instead, with the directory the controllers' rings live in; without them an app - * saw no gamepad. Both are built against an older glibc than any Flathub runtime has. - */ private fun preload(session: List<Bind>, rootfs: String, env: MutableMap<String, String>): List<Bind> { val libs = FileUtils.readString(File(rootfs, "etc/ld.so.preload"))?.lines() ?.map { it.trim() }?.filter { it.startsWith("/") && File(rootfs + it).isFile } ?: return emptyList() @@ -272,9 +219,6 @@ object BwrapSpawner { val binds = ArrayList<Bind>() val paths = libs.map { lib -> "$PRELOAD_DIR/${lib.substringAfterLast('/')}".also { binds.add(Bind(rootfs + lib, it)) } } env["LD_PRELOAD"] = (paths + listOfNotNull(env["LD_PRELOAD"]?.takeIf { it.isNotBlank() })).joinToString(":") - // Not through ZYPAK_LD_PRELOAD to Chromium's children: the session shim's stat hooks then - // hide the setuid bit zypak fakes on chrome-sandbox, and Chromium refuses to start. - // The controllers: FAKE_EVDEV_DIR is <session>/dev/input, the rings beside it in <session>/dev. env["FAKE_EVDEV_DIR"]?.let { File(it).parent }?.let { dev -> binds.addAll(bindsFor(session, rootfs, dev, dev)) } @@ -283,45 +227,22 @@ object BwrapSpawner { private const val PRELOAD_DIR = "/run/droiddeck-preload" - /** - * Browsers sandbox their own children, and neither way works in here. Firefox wants - * namespaces and seccomp from the kernel; its content processes died without them (the - * desktop sets the same for the runtime's own Firefox). Chromium and Electron apps start - * their zygote through Flatpak's portal (zypak's spawn strategy), and the portal refuses: - * it knows a Flatpak by /proc/<pid>/root/.flatpak-info, and a proot sandbox's root is the - * host's. zypak's older strategy mimics the zygote inside the app's own sandbox instead. - */ private fun browserSandboxes(env: MutableMap<String, String>) { env.putIfAbsent("ZYPAK_ZYGOTE_STRATEGY_SPAWN", "0") - // Qt WebEngine is Chromium without zypak: it sets up its own namespace sandbox. env.putIfAbsent("QTWEBENGINE_DISABLE_SANDBOX", "1") - // Chromium on Wayland asks the render node the compositor names for its DRM version; - // KGSL has none, and the GPU process aborts until Chromium gives up ("GPU process isn't - // usable"). Electron picks Wayland from XDG_SESSION_TYPE; with an X server there (every - // session has Xwayland) it stays on X11, which works. GTK and Qt go by their own settings. if (env.containsKey("DISPLAY")) env["XDG_SESSION_TYPE"] = "x11" for (name in listOf("MOZ_DISABLE_CONTENT_SANDBOX", "MOZ_DISABLE_GMP_SANDBOX", "MOZ_DISABLE_RDD_SANDBOX", "MOZ_DISABLE_SOCKET_PROCESS_SANDBOX", "MOZ_DISABLE_UTILITY_SANDBOX", - // Android 16 refuses the size seal Firefox puts on a tab's shared - // memory, and the tab then dies on every page (droiddeck-desktop). "MOZ_SHM_NO_SEALS")) { env.putIfAbsent(name, "1") } } - /** - * /proc/cpuinfo without the cores' names. Snapdragon's ARMv9 cores (Cortex-A510/A715/X3) have - * SVE2 by architecture, and LLVM - llvmpipe's JIT in Flathub's Mesa, which is what apps draw - * with here - takes the CPU part to mean it may use it. Qualcomm leaves SVE off, so the first - * shader compiled killed the app with SIGILL. Without a part number LLVM targets generic - * ARMv8 plus the "Features" line, which lists what the kernel really enabled. - */ private fun genericCpuinfo(context: Context): File? = try { val out = File(context.cacheDir, "cpuinfo-generic") val text = File("/proc/cpuinfo").readLines() .filterNot { it.startsWith("CPU implementer") || it.startsWith("CPU part") || it.startsWith("CPU variant") || it.startsWith("CPU revision") } .joinToString("\n", postfix = "\n") - // Sandboxes start side by side; one must never read another's half-written copy. if (!out.isFile || out.readText() != text) { val staged = File.createTempFile("cpuinfo", null, context.cacheDir) staged.writeText(text) @@ -332,7 +253,6 @@ object BwrapSpawner { Log.w(TAG, "cpuinfo", e); null } - /** [path] with "." and ".." resolved lexically. */ internal fun normalize(path: String): String { val out = ArrayList<String>() for (part in path.split('/')) when (part) { @@ -343,10 +263,6 @@ object BwrapSpawner { return "/" + out.joinToString("/") } - /** - * [guest] inside the sandbox root, or null when the way there crosses a symlink: one of - * Flatpak's (var/run -> ../run) may point anywhere once followed on the host. - */ private fun inRoot(root: File, guest: String): File? { var f = root for (part in guest.split('/').filter { it.isNotEmpty() }) { @@ -358,17 +274,14 @@ object BwrapSpawner { } private fun chmod(f: File, mode: Int) { - try { Os.chmod(f.path, mode) } catch (e: Exception) { /* the default mode serves */ } + try { Os.chmod(f.path, mode) } catch (e: Exception) { } } private fun run(context: Context, plan: Plan, client: LocalSocket, out: DataOutputStream, id: Int) { val cmd = LinuxRuntime.prootPrefix(context, plan.root, plan.cwd) - // DROIDDECK_PROOT_VERBOSE=N in the app's environment (flatpak run --env=...) has proot - // say what it does, to logcat in full: for a sandbox that dies without a word. val verbose = plan.env["DROIDDECK_PROOT_VERBOSE"]?.toIntOrNull() if (verbose != null) { cmd.add(1, "-v"); cmd.add(2, verbose.toString()) } plan.binds.forEach { cmd.add("-b"); cmd.add(it.spec) } - // proot hands its own environment on; the sandbox gets exactly what Flatpak asked for. cmd.add("/usr/bin/env") cmd.add("-i") plan.env.forEach { (k, v) -> cmd.add("$k=$v") } @@ -383,23 +296,18 @@ object BwrapSpawner { } val process = builder.start() val pid = HostProcess.pidOf(process) - // A sandbox lives exactly as long as its bwrap stand-in, which ends it; a session starting - // meanwhile (OrphanReaper) must not take it - an install's extra-data step, say. com.droiddeck.launcher.session.OrphanReaper.keep(pid) Log.i(TAG, "sandbox $id pid $pid: ${plan.argv.joinToString(" ")} (${plan.binds.size} binds)") synchronized(out) { frame(out, 'P', pid.toString().toByteArray()) } - // The stand-in going away (Flatpak killed, the session over) ends the sandbox. Thread({ - try { while (client.inputStream.read() >= 0) { /* nothing is sent after the request */ } } - catch (e: IOException) { /* closed */ } + try { while (client.inputStream.read() >= 0) { } } + catch (e: IOException) { } if (process.isAlive) { Log.i(TAG, "sandbox $id: its bwrap is gone; stopping it") process.destroy() } }, "bwrap-$id-watch").apply { isDaemon = true; start() } val buffer = ByteArray(16384) - // The start of what the sandbox says goes to logcat too: a sandbox that fails at once - // (proot refusing a bind, a missing library) says why there even when nothing reads it. var logged = 0 process.inputStream.use { stream -> while (true) { diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DeckyManager.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DeckyManager.kt index 60058b72c..3e90398e2 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DeckyManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DeckyManager.kt @@ -16,7 +16,6 @@ import java.util.zip.ZipInputStream import org.json.JSONArray import org.json.JSONObject -/** Optional Decky Loader installation for the ARM64 Linux guest. */ object DeckyManager { private const val TAG = "DeckyManager" const val REPOSITORY = "Droid-Deck/decky-loader" @@ -25,7 +24,6 @@ object DeckyManager { private const val VERSION = "root/homebrew/services/.droiddeck-decky-version" private const val CEF_REMOTE_DEBUG_MARKER = "root/.local/share/Steam/.cef-enable-remote-debugging" - /** True while a plugin's remote binary downloads: what the UI branches on, not the stage's words. */ @Volatile var downloadingBinary = false private set @@ -41,7 +39,6 @@ object DeckyManager { fun loader(context: Context) = File(LinuxRuntime.rootDir(context), PATH) fun supervisorEnabled(context: Context) = File(LinuxRuntime.rootDir(context), "root/.droiddeck-decky-enabled").isFile - /** Remove the legacy install-time debugger marker unless the user opted into Decky. */ fun syncCefMarker(context: Context) { val marker = File(LinuxRuntime.rootDir(context), CEF_REMOTE_DEBUG_MARKER) if (!supervisorEnabled(context)) { @@ -105,7 +102,6 @@ object DeckyManager { } catch (e: Exception) { Log.w(TAG, "release metadata", e); ReleaseChannels(emptyList(), emptyList()) } } - /** Downloads to a sibling temp file and atomically renames only after size, digest and ELF checks. */ fun install(context: Context, release: Release, progress: (String, Int) -> Unit): String? { if (SessionState.running) return context.getString(R.string.deckymgr_stop_session_loader) val target = loader(context) @@ -124,7 +120,6 @@ object DeckyManager { if (!validElf(temp, release.machine)) { temp.delete(); return context.getString(R.string.deckymgr_not_elf) } if (SessionState.running) { temp.delete(); return context.getString(R.string.deckymgr_session_started_loader) } if (!temp.setExecutable(true, false)) { temp.delete(); return context.getString(R.string.deckymgr_loader_not_executable) } - // Decky needs Steam's local CEF debugger only when its session supervisor is enabled. val cefMarker = File(LinuxRuntime.rootDir(context), CEF_REMOTE_DEBUG_MARKER) val cefReady = !supervisorEnabled(context) || cefMarker.isFile || runCatching { cefMarker.parentFile?.mkdirs() @@ -149,7 +144,6 @@ object DeckyManager { return null } - /** Imports a Decky distribution ZIP into homebrew/plugins for the next Steam session. */ fun installPluginZip(context: Context, archive: File, progress: (String, Int) -> Unit): String? { if (SessionState.running) return context.getString(R.string.deckymgr_stop_session_plugin) if (installed(context) == null) return context.getString(R.string.deckymgr_loader_required) @@ -216,8 +210,6 @@ object DeckyManager { out.write(buffer, 0, read) } } - // Decky distributions place plugin executables under bin/. Java's ZIP reader - // does not expose Unix mode bits, so restore execution on those payload files. if (relative.split('/').dropLast(1).any { it == "bin" } && !output.setExecutable(true, false)) { throw IllegalStateException(context.getString(R.string.deckymgr_bundled_not_executable)) } diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DeckyPluginInstall.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DeckyPluginInstall.kt index b4223b7af..19414a49e 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DeckyPluginInstall.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DeckyPluginInstall.kt @@ -6,9 +6,7 @@ import java.io.File import org.json.JSONArray import org.json.JSONObject -/** Offline equivalent of Decky's identity-based replacement and loader settings update. */ internal object DeckyPluginInstall { - /** A failure the user is told about: [text] is its string resource. */ class Failure(@StringRes val text: Int) : IllegalStateException() fun activate(staged: File, plugins: File, name: String, settingsFile: File, backupSuffix: String) { @@ -26,7 +24,6 @@ internal object DeckyPluginInstall { .distinct()) if ((0 until order.length()).none { order.optString(it) == name }) order.put(name) settings.put("pluginOrder", order) - // Decky's reinstall removes these flags while retaining the plugin's order and data. for (key in listOf("frozenPlugins", "hiddenPlugins", "disabled_plugins")) { val values = settings.optJSONArray(key) ?: continue settings.put(key, JSONArray((0 until values.length()).map { values.get(it) }.filter { it != name })) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopCatalog.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopCatalog.kt index 971608106..884b09ea9 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopCatalog.kt @@ -10,28 +10,15 @@ import com.droiddeck.launcher.core.FileUtils import org.json.JSONObject import java.io.File -/** - * The hosted packages that go into the runtime on request - the desktop, the emulators, and the - * builds we mirror or make ourselves - described by desktop.json beside the runtime's own catalog. - * - * Two kinds. A `tar` extracts over the rootfs like the runtime itself (the same extractor, links - * and modes preserved). An `appimage` is one file dropped in /opt/appimages with a .desktop entry - * written for it; proot has no FUSE, so it runs extracted (APPIMAGE_EXTRACT_AND_RUN). - */ object DesktopCatalog { private const val TAG = "DesktopCatalog" const val CATALOG_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/main/desktop.json" - /** - * Valve's Proton Experimental (ARM64) with its appmanifest, laid out as the client keeps it. - * In a catalog of its own so it never shows among the packages a user picks from. - */ const val STEAM_SEED_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/main/steam-seed.json" const val PROTON_SEED_ID = "proton-arm64" class Entry( val id: String, val name: String, val tier: Int, val version: String, val kind: String, val url: String, val sha256: String, val size: Long, val notes: String, - /** For an appimage: the icon name and menu category of its .desktop entry. */ val icon: String, val category: String, ) @@ -40,8 +27,6 @@ object DesktopCatalog { return try { val json = JSONObject(body) val arr = json.getJSONArray("packages") - // A package built per memory page size (ARMSX2: 4K and 16K kernels) names the others - // under "pages"; the device's own size picks one, and the top-level file is the 4K one. val pageSize = try { android.system.Os.sysconf(android.system.OsConstants._SC_PAGESIZE).toString() } catch (e: Exception) { "4096" } @@ -62,42 +47,21 @@ object DesktopCatalog { private fun marker(context: Context, id: String) = File(LinuxRuntime.rootDir(context), ".droiddeck-pkg-$id") - /** The installed version of a package, or null. */ fun installed(context: Context, id: String): String? = FileUtils.readString(marker(context, id))?.trim()?.takeIf { it.isNotEmpty() } - /** - * True until the ARM64 Proton has been placed once. Its appmanifest counts too, so an install - * the client fetched itself is never overwritten; after the first placement the client owns it - * and updates it like any other app, and a user who removes it is not given it back. - */ fun protonSeedNeeded(context: Context): Boolean = installed(context, PROTON_SEED_ID) == null && !File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps/appmanifest_4427310.acf").isFile - /** - * The desktop: KDE Plasma (tools/desktop-kde). It replaced an LXQt/labwc package of id - * "desktop", which a runtime may still carry; that one does not count, so the first desktop - * after the update installs this one over it. - */ const val DESKTOP_ID = "desktop-kde" - /** - * The desktop package this apk installs: pinned in tools/desktop-kde/release.env and published - * in DroidDeck-Components, not read from the catalog, so the desktop scripts staged by this - * apk always get the package they were made for. - */ fun desktopEntry(context: Context) = Entry( DESKTOP_ID, context.getString(R.string.content_linux_desktop), 1, BuildConfig.DESKTOP_KDE_TAG, "tar", BuildConfig.DESKTOP_KDE_URL, BuildConfig.DESKTOP_KDE_SHA256, BuildConfig.DESKTOP_KDE_SIZE, "", "", "", ) - // The desktop package installed to the end: KWin is there and the marker install() writes once - // the whole package is extracted. An install cut short (the app killed, storage full) leaves - // KWin without the marker, and the next desktop installs it again. A newer pinned package also - // replaces the installed one, keeping the user's home and settings. The launcher is staged by - // the app at every session, so it cannot tell. SessionFiles uses the same test. fun desktopInstalled(context: Context): Boolean = desktopInstalled(LinuxRuntime.rootDir(context)) fun desktopInstalled(root: File): Boolean = @@ -110,7 +74,6 @@ object DesktopCatalog { return File(root, DesktopRemoval.PENDING).exists() || marker(context, DESKTOP_ID).exists() } - /** The installed desktop's size on disk; off the main thread. */ fun desktopSize(context: Context): Long = DesktopRemoval.installedSize(context) fun cleanLegacy(context: Context, listener: LinuxRuntimeInstaller.ProgressListener? = null) = @@ -119,11 +82,9 @@ object DesktopCatalog { fun removeDesktop(context: Context, listener: LinuxRuntimeInstaller.ProgressListener): String? = DesktopRemoval.remove(context, listener) - /** Downloads, verifies and installs one package. Returns null on success, else a message. */ fun install(context: Context, entry: Entry, listener: LinuxRuntimeInstaller.ProgressListener?): String? { val root = LinuxRuntime.rootDir(context) if (!root.isDirectory) return context.getString(R.string.deskpkg_runtime_missing) - // Every catalog row carries a sha256; one without is refused rather than trusted. if (entry.sha256.isEmpty()) return context.getString(R.string.deskpkg_no_checksum, entry.name) val download = File(context.cacheDir, "pkg-${entry.id}.download") try { @@ -141,7 +102,6 @@ object DesktopCatalog { "appimage" -> { val dir = File(root, "opt/appimages").apply { mkdirs() } val target = File(dir, "${entry.id}.AppImage") - // Some projects zip the AppImage (melonDS); the one file inside is what we want. val placed = if (entry.url.endsWith(".zip", ignoreCase = true)) unzipAppImage(download, target) else download.renameTo(target) if (!placed) return context.getString(R.string.deskpkg_place_failed) @@ -152,8 +112,6 @@ object DesktopCatalog { "Icon=${entry.icon}\nTerminal=false\nCategories=${entry.category};\n") } else -> { - // Not complete until written again at the end: a reinstall cut short must not - // leave the last install's marker saying it is. marker(context, entry.id).delete() if (!LinuxRuntimeInstaller.extract(context, download, root, listener)) return context.getString(R.string.deskpkg_extract_failed) } @@ -192,10 +150,6 @@ object DesktopCatalog { return false } - /** - * Removes an appimage package outright. A tar package is spread through the rootfs and is - * not tracked file by file; only its marker is dropped, which offers it again. - */ fun remove(context: Context, entry: Entry) { val root = LinuxRuntime.rootDir(context) if (entry.kind == "appimage") { diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopFileRemoval.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopFileRemoval.kt index 063a5c9a9..6d2c42b01 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopFileRemoval.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopFileRemoval.kt @@ -7,7 +7,6 @@ import java.nio.file.LinkOption.NOFOLLOW_LINKS import java.nio.file.attribute.BasicFileAttributes import java.security.MessageDigest -/** Removes only unchanged, recorded package files. Never visits home or follows a symlink. */ internal object DesktopFileRemoval { data class Entry(val kind: String, val size: Long, val identity: String, val path: String) { companion object { @@ -37,14 +36,13 @@ internal object DesktopFileRemoval { else -> attrs.isRegularFile && attrs.size() == entry.size && sha(file, buffer) == entry.identity } if (!matches) { kept++; continue } - Files.delete(file.toPath()) // A failure propagates; the pending marker makes retry possible. + Files.delete(file.toPath()) removed++ bytes += if (attrs.isRegularFile) attrs.size() else 0 var parent = file.parentFile while (parent != root && parent != null) { directories.add(parent); parent = parent.parentFile } if (index % 200 == 0) progress(if (total > 0) index * 99 / total else -1) } - // Only now-empty parents of removed files, never a recursive delete of a package directory. for (dir in directories.sortedByDescending { it.path.length }) { if (dir.list()?.isEmpty() == true && !dir.delete()) throw IOException("Cannot remove $dir") } @@ -56,7 +54,6 @@ internal object DesktopFileRemoval { if (Files.isSymbolicLink(root.toPath())) return null val parts = name.split('/') if (parts.firstOrNull() !in setOf("usr", "etc") || parts.any { it.isEmpty() || it == "." || it == ".." }) return null - // Even an in-root symlink in a parent can redirect deletion into a user's data. var parent = root for (part in parts.dropLast(1)) { parent = File(parent, part) @@ -76,7 +73,6 @@ internal object DesktopFileRemoval { } } - /** The base runtime's pacman database is still present: desktop archives never replace it. */ fun runtimeFiles(root: File): Set<String> = buildSet { val database = File(root, "var/lib/pacman/local") if (!database.isDirectory || Files.isSymbolicLink(database.toPath())) throw IOException("Runtime package database missing") diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopRemoval.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopRemoval.kt index 09c93d73d..3d031a85a 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopRemoval.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopRemoval.kt @@ -7,7 +7,6 @@ import com.droiddeck.launcher.core.FileUtils import java.io.File import java.util.zip.GZIPInputStream -/** Inventories are generated from pinned archives by tools/desktop-kde/removal-manifests.py. */ internal object DesktopRemoval { const val PENDING = ".droiddeck-desktop-removing" private const val CLEANING = ".droiddeck-lxqt-cleaning-v1" @@ -19,15 +18,9 @@ internal object DesktopRemoval { private fun protected(context: Context, root: File) = DesktopFileRemoval.runtimeFiles(root) + lines(context, "emulators-keep.txt.gzip") - /** - * What the desktop's own files take on disk, from the same inventory its removal uses (each - * file's size); the bundled apps' and emulators' shared files are not counted. Reads an asset: - * call it off the main thread. - */ fun installedSize(context: Context): Long = lines(context, "kde-remove.tsv.gzip").sumOf { line -> line.split('\t').getOrNull(1)?.toLongOrNull() ?: 0L } - /** Called before a guest starts or just after KDE extraction; retries after a process death. */ fun cleanLegacy(context: Context, listener: LinuxRuntimeInstaller.ProgressListener? = null) { val root = LinuxRuntime.rootDir(context) if (FileUtils.readString(File(root, CLEANED))?.trim() == "1") return @@ -54,7 +47,6 @@ internal object DesktopRemoval { val version = FileUtils.readString(pending)?.trim() ?: FileUtils.readString(marker)?.trim() val versions = context.assets.open("desktop-removal/versions.txt").bufferedReader().use { it.readLines() } check(version in versions) { context.getString(R.string.desktop_removal_unknown) } - // Validate all inventories and the runtime database before dropping the installed marker. val entries = lines(context, "kde-remove.tsv.gzip").map(DesktopFileRemoval.Entry::parse) val keep = protected(context, root) cleanLegacy(context, listener) @@ -63,7 +55,6 @@ internal object DesktopRemoval { if (marker.exists() && !marker.delete()) error("Could not clear desktop marker") val removing = context.getString(R.string.desktop_removing) val result = DesktopFileRemoval.remove(root, entries, keep) { listener.onProgress(removing, it) } - // APK-staged desktop files are matched against the APK too; keep user-modified copies. for ((asset, path) in com.droiddeck.launcher.session.SessionFiles.DESKTOP_FILES) { val target = DesktopFileRemoval.safeFile(root, path) ?: continue if (!isRegularFile(target)) continue diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/FlatpakManager.kt b/app/src/main/java/com/droiddeck/launcher/runtime/FlatpakManager.kt index c5c1fa6db..8dfafd42b 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/FlatpakManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/FlatpakManager.kt @@ -7,31 +7,20 @@ import com.droiddeck.launcher.core.FileUtils import org.json.JSONObject import java.io.File -/** - * Flatpak in the Linux runtime: putting it there (droiddeck-flatpak-setup), the apps installed - * from Flathub, and installing, removing and updating them (droiddeck-flatpak). One per-user - * installation at /root/.local/share/flatpak, so nothing needs the system helper or polkit. - * - * Every command runs in a proot of its own, outside any session: the store works whether or not - * Steam or the desktop is up, and a session starting meanwhile leaves it running (OrphanReaper). - */ object FlatpakManager { private const val TAG = "FlatpakManager" const val FLATPAK = "/usr/bin/flatpak" const val BWRAP = "/usr/local/bin/droiddeck-bwrap" - /** The program the front end starts a Flatpak app through, in a session of its own. */ const val LAUNCHER = "/usr/local/bin/droiddeck-flatpak-run" private const val HELPER = "/usr/local/bin/droiddeck-flatpak" private const val SETUP = "/usr/local/bin/droiddeck-flatpak-setup" private const val USER_DIR = "root/.local/share/flatpak" - /** One store operation at a time: Flatpak locks its installation per transaction anyway. */ @Volatile var busy: String? = null private set class App(val id: String, val name: String, val icon: File?, val branch: String, val summary: String?) - /** Flatpak is in the runtime, with Flathub to install from. */ fun ready(context: Context): Boolean { val root = LinuxRuntime.rootDir(context) return File(root, FLATPAK.substring(1)).isFile && @@ -44,10 +33,6 @@ object FlatpakManager { else -> "aarch64" } - /** - * The installed apps, read from the installation itself: each app's deployed files carry the - * desktop entry and icons it exports, so listing them needs no process in the runtime. - */ fun installedApps(context: Context): List<App> { val apps = File(LinuxRuntime.rootDir(context), "$USER_DIR/app") val arch = arch() @@ -67,7 +52,6 @@ object FlatpakManager { fun isInstalled(context: Context, id: String): Boolean = File(LinuxRuntime.rootDir(context), "$USER_DIR/app/$id/${arch()}").listFiles()?.any { File(it, "active").exists() } == true - /** A key of the desktop entry's main group ([Desktop Entry]), unlocalised. */ private fun entry(desktop: String, key: String): String? { var inMain = false for (line in desktop.lineSequence()) { @@ -78,7 +62,6 @@ object FlatpakManager { return null } - /** The largest PNG the app exports (the front end draws no SVG). */ private fun icon(export: File, id: String): File? { val hicolor = File(export, "icons/hicolor") return hicolor.listFiles() @@ -87,13 +70,11 @@ object FlatpakManager { ?.maxByOrNull { it.first }?.second } - /** Runs [argv] in the runtime, its output also in files/logs/tools/flatpak-<verb>.log. */ private fun runGuest(context: Context, argv: List<String>, fakeRoot: Boolean, onLine: (String) -> Unit): Int { val name = "flatpak-" + (argv.getOrNull(2)?.takeIf { argv.getOrNull(1) == HELPER } ?: "setup") return GuestCommand.run(context, argv, fakeRoot, name, onLine = onLine) } - /** [block]'s result, or [ifBusy]'s while another operation has the installation. */ private inline fun <T> exclusive(what: String, ifBusy: () -> T, block: () -> T): T { synchronized(this) { if (busy != null) return ifBusy() @@ -102,7 +83,6 @@ object FlatpakManager { try { return block() } finally { busy = null } } - /** Puts Flatpak into the runtime and adds Flathub. Null on success, else what went wrong. */ fun setup(context: Context, onProgress: (String, Int) -> Unit): String? { if (!LinuxRuntime.isInstalled(context)) return context.getString(R.string.user_apps_runtime_required) return exclusive("setup", { context.getString(R.string.flatpakmgr_busy) }) { @@ -123,17 +103,12 @@ object FlatpakManager { } } - /** - * One store operation - install, uninstall or update - with its progress as a stage line and - * an overall percentage across the steps Flatpak plans (a runtime, its extensions, the app). - */ private fun transaction( context: Context, verb: String, id: String?, onProgress: (String, Int) -> Unit, onDone: (JSONObject) -> Unit = {}, ): String? { if (!ready(context)) return context.getString(R.string.flatpakmgr_not_ready) return exclusive("$verb:${id ?: "all"}", { context.getString(R.string.flatpakmgr_busy) }) { var error: String? = null - // Progress lines carry no ref; they belong to the step the last "op" line started. var step = context.getString(R.string.flatpakmgr_starting) val argv = listOfNotNull("/usr/bin/python3", HELPER, verb, id) val status = runGuest(context, argv, fakeRoot = false) { line -> @@ -169,10 +144,6 @@ object FlatpakManager { }, name) } - /** - * A ref in words: "app/org.supertuxproject.SuperTux/aarch64/stable" is "SuperTux". [text] - * resolves a string resource with its arguments. - */ internal fun refLabel(ref: String, text: (Int, Array<out Any>) -> String): String { val parts = ref.split('/') val id = parts.getOrNull(1) ?: return ref @@ -190,7 +161,6 @@ object FlatpakManager { } } - /** The whole transaction's percentage: steps done, plus this one's share. */ private fun overall(o: JSONObject, percent: Int): Int { val n = o.optInt("n").coerceAtLeast(1) val i = o.optInt("i").coerceIn(1, n) @@ -200,7 +170,6 @@ object FlatpakManager { fun install(context: Context, id: String, onProgress: (String, Int) -> Unit) = transaction(context, "install", id, onProgress) fun uninstall(context: Context, id: String, onProgress: (String, Int) -> Unit) = transaction(context, "uninstall", id, onProgress) - /** Installs the .flatpak bundle at [guestPath], its runtimes from Flathub: the app's ID, and what went wrong if anything. */ fun installBundle(context: Context, guestPath: String, onProgress: (String, Int) -> Unit): Pair<String?, String?> { var id: String? = null val problem = transaction(context, "install-bundle", guestPath, onProgress) { id = it.optString("id").ifEmpty { null } } @@ -208,7 +177,6 @@ object FlatpakManager { } fun update(context: Context, id: String?, onProgress: (String, Int) -> Unit) = transaction(context, "update", id, onProgress) - /** Installed apps with an update on Flathub, or null when that could not be checked. */ fun updates(context: Context): Set<String>? { if (!ready(context)) return emptySet() var apps: Set<String>? = null diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/GuestCommand.kt b/app/src/main/java/com/droiddeck/launcher/runtime/GuestCommand.kt index ad63fc5bf..2db2c7041 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/GuestCommand.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/GuestCommand.kt @@ -9,18 +9,7 @@ import com.droiddeck.launcher.session.SessionPaths import com.droiddeck.launcher.session.SessionPrefs import java.io.File -/** - * One command in the Linux runtime outside any session - the store's Flatpak work, an AppImage - * import - in a proot of its own. A session starting meanwhile leaves it running (OrphanReaper). - */ object GuestCommand { - /** - * Runs [argv] and hands each output line to [onLine]; returns the exit status. [fakeRoot] is - * for the package tools, which refuse any uid but 0. With [logName], everything the command - * says also goes to files/logs/tools/<logName>.log beside the session logs, so a problem can - * be handed over like a session's. With [linkDir], hard links (which Android denies apps) - * become symlinks to files proot keeps there (its link2symlink). - */ fun run(context: Context, argv: List<String>, fakeRoot: Boolean = false, logName: String? = null, linkDir: File? = null, onLine: (String) -> Unit): Int { val log = logName?.let { @@ -40,7 +29,6 @@ object GuestCommand { val root = LinuxRuntime.rootDir(context) LinuxRuntime.writeAccounts(context) SessionFiles.stage(context, root) - // The resolver otherwise names the DNS of whatever network the last session was on. LinuxNetworkLinkComponent(context, root).publish() val runtimeDir = File(context.filesDir, ".flatpak-rt").apply { mkdirs() } val cmd = LinuxRuntime.prootPrefix(context, root, "/root", fakeRoot) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt index 5b5585c22..3bf43fc34 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt @@ -26,18 +26,6 @@ import java.net.Inet6Address import org.json.JSONArray import org.json.JSONObject -/** - * The device's network link, published for the Linux runtime's processes. - * - * Android denies an app the rtnetlink dump behind getifaddrs() and if_nameindex(), the hardware - * address ioctl and every table under /proc/net. Wine builds its adapter, address, route and - * neighbour tables from those, so a game under the runtime saw no adapter, no address, no gateway - * and no MAC - and one that checks its adapters before going online waited for good. The link the - * app can see through ConnectivityManager is written here, kept current while the session runs, - * and the session shim answers whatever the kernel refuses from it (tools/linuxfs/preload/netif.c). - * - * Shape follows WinNative's LinuxNetworkLinkComponent (maxjivi05, f76a5c4a), GPL-3.0. - */ class LinuxNetworkLinkComponent( context: Context, private val rootDir: File, @@ -71,7 +59,6 @@ class LinuxNetworkLinkComponent( } } - /** Called before the session starts, so its first process already sees the link. */ fun publish() = synchronized(lock) { currentNetwork = connectivity.activeNetwork currentProperties = currentNetwork?.let(connectivity::getLinkProperties) @@ -102,7 +89,6 @@ class LinuxNetworkLinkComponent( } override fun onLost(network: Network) = synchronized(lock) { - // A handover can report the old link lost after the new default arrived. if (network == currentNetwork) { currentNetwork = null currentProperties = null @@ -174,7 +160,6 @@ class LinuxNetworkLinkComponent( } } - /** NetworkManager's view of Android. Kept separate from netif.c's legacy adapter format. */ @Suppress("DEPRECATION") private fun writeNetworkState(properties: LinkProperties?, capabilities: NetworkCapabilities?) { val namesAllowed = canReadWifiNames() @@ -238,8 +223,6 @@ class LinuxNetworkLinkComponent( connectionInfo } if (info != null) { - // SSIDs can be redacted by Android. No location permission is needed to - // report the transport; a hidden name simply displays as Wi-Fi in Steam. if (namesAllowed) { val namedInfo = connectionInfo ?: info namedInfo.ssid?.takeUnless { it == WifiManager.UNKNOWN_SSID || it.isEmpty() } @@ -262,15 +245,6 @@ class LinuxNetworkLinkComponent( } } - /** - * The guest's resolver, from the network it is actually on. glibc reads /etc/resolv.conf, - * and the image ships two public servers there, which is why Steam resolves at all - but a - * captive portal, a private-DNS network or a v6-only carrier wants the network's own - * servers. Those come first, IPv4 before IPv6 and never a link-local one (Android lists - * fe80:: resolvers a guest cannot reach; handing Bannerlator's Wine guests exactly that was - * the Pale Moon "no internet"). The public ones stay as fallback so nothing is ever worse - * than the image was. - */ private fun writeResolver(properties: LinkProperties?) { val own = properties?.dnsServers.orEmpty() .filter { !it.isLinkLocalAddress && !it.isLoopbackAddress && !it.isAnyLocalAddress } @@ -321,10 +295,6 @@ class LinuxNetworkLinkComponent( } } - /** - * A stable, locally administered address for this device. Android hides the real one, and a - * title should see the same hardware identity every session. - */ private fun seededMac(): String { val id = Settings.Secure.getString(appContext.contentResolver, Settings.Secure.ANDROID_ID) ?: "bannerlator" val bytes = java.security.MessageDigest.getInstance("SHA-1").digest(id.toByteArray()) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java index 082bf814e..76ef0995c 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java @@ -16,47 +16,23 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.List; -/** - * The glibc arm64 rootfs at {@code files/linuxfs} and the proot invocation that runs a program in - * it as this app's own uid. It is a second runtime beside the Wine imagefs, not a container: no - * Wine, no box64, no FEX. proot is packaged as {@code libproot.so} so the installer places it, with - * its loader, in the native library directory - the only place an app on targetSdk 28 may execute - * a file from. - * - * <p>Ported from WinNative's gamescope runtime (GPL-3.0). - */ public final class LinuxRuntime { public static final String DIR = "linuxfs"; public static final String SESSION_SCRIPT = "/usr/local/bin/droiddeck-session"; public static final String MODE_DESKTOP = "desktop"; public static final String MODE_STEAM = "steam"; public static final String MODE_RUN = "run"; - /** Shortcut extra naming which of the modes above a Linux entry launches. */ public static final String EXTRA_LINUX_MODE = "linux_mode"; private static final String KGSL_DEVICE = "/dev/kgsl-3d0"; - /** - * Where the guest sees a command's XDG_RUNTIME_DIR. A Unix socket's path must fit in 108 bytes, - * and libwayland checks the guest's path before proot ever sees it: with the app on an SD card - * adopted as internal storage the files directory is /mnt/expand/<uuid>/user/0/<package>/files, - * and gamescope-0 under it is one byte over, so gamescope could not open its socket and the - * session ended at once. A long host path behind it is proot's to shorten. - */ public static final String GUEST_RUNTIME_DIR = "/run/droiddeck"; private static final String SYSTEM_FONTS = "/system/fonts"; private static final String GUEST_SYSTEM_FONTS = "/usr/local/share/fonts/android"; - /** The folder under Downloads where builds before private logs wrote them; read only to move them. */ public static final String DEBUG_LOG_DIR = "DroidDeck"; - /** - * Where every log lands: app-private, {@code files/logs}. Session folders, tools/ (one-off - * commands) and stores/ sit under it; the files directory is bound into every session at the - * same path, so the guest writes there too. Share logs is how a folder leaves the device. - */ public static File logDir(Context context) { return new File(context.getFilesDir(), "logs"); } - /** {@code Download/DroidDeck}, where earlier builds wrote logs. Only LogMigration reads it. */ public static File legacyLogDir() { return new File(android.os.Environment.getExternalStoragePublicDirectory( android.os.Environment.DIRECTORY_DOWNLOADS), DEBUG_LOG_DIR); @@ -68,16 +44,10 @@ public final class LinuxRuntime { return new File(context.getFilesDir(), DIR); } - /** - * The session's own small tree beside the rootfs: the fake evdev nodes and their rings. In - * Bannerlator these lived in the Wine imagefs; here there is no Wine, so the session owns them. - * Bound into the guest at its own host path, so nothing needs translating. - */ public static File sessionRoot(Context context) { return new File(context.getFilesDir(), "session"); } - /** Where the runtime carries the host-side proot; see tools/linuxfs/prebuilt/proot/README.md. */ private static final String HOST_DIR = "opt/android-host"; public static File prootBinary(Context context) { @@ -92,13 +62,11 @@ public final class LinuxRuntime { return new File(rootDir(context), HOST_DIR + "/loader"); } - /** proot links against libtalloc, which sits beside it; empty when the apk copy is in use. */ public static String prootLibraryPath(Context context) { File dir = new File(rootDir(context), HOST_DIR); return dir.equals(prootBinary(context).getParentFile()) ? dir.getPath() : ""; } - /** The rootfs is present with gamescope and the session script the launcher hands control to. */ public static boolean isInstalled(Context context) { if (LinuxRuntimeInstaller.isRemoving() || LinuxRuntimeInstaller.hasRemovalPending(context)) return false; File root = rootDir(context); @@ -109,7 +77,6 @@ public final class LinuxRuntime { && prootLoader(context).isFile(); } - /** The Vulkan ICD manifest the rootfs ships for the device GPU, or null when it has none. */ public static File vulkanIcd(Context context) { File icdDir = new File(rootDir(context), "usr/share/vulkan/icd.d"); File[] manifests = icdDir.listFiles((dir, name) -> name.endsWith(".json")); @@ -120,25 +87,16 @@ public final class LinuxRuntime { return manifests.length > 0 ? manifests[0] : null; } - /** - * The proot command line running {@code guestCommand} inside the rootfs. Host paths the session - * needs - the app's files directory for the compositor and audio sockets, external storage for - * the user's games - are bound at their own paths, so nothing on either side needs translating - * and proot never touches the fds a dma-buf travels in. Android has no /dev/shm; a directory - * under the cache stands in, which glibc's shm_open and Chromium's shared memory accept. - */ public static List<String> command(Context context, File sessionRoot, File runtimeDir, File externalStorage, List<String> guestCommand) { return command(context, sessionRoot, runtimeDir, externalStorage, null, guestCommand); } - /** As above, plus {@code host:guest} bind specs - the installed games handed to Steam. */ public static List<String> command(Context context, File sessionRoot, File runtimeDir, File externalStorage, List<String> extraBinds, List<String> guestCommand) { File root = rootDir(context); List<String> binds = binds(context, sessionRoot, runtimeDir, externalStorage, extraBinds); - // A session's binds are the view its programs have; a one-off command's are not recorded. if (sessionRoot != null) lastBinds = binds; List<String> cmd = prootPrefix(context, root, "/root"); for (String spec : binds) bind(cmd, spec); @@ -146,10 +104,6 @@ public final class LinuxRuntime { return cmd; } - /** - * The binds of the last command built: the guest's view of the host, which the Flatpak - * sandboxes started beside a session (BwrapSpawner) translate their paths through. - */ private static volatile List<String> lastBinds; public static List<String> lastBinds(Context context) { @@ -158,25 +112,15 @@ public final class LinuxRuntime { return binds(context, null, null, android.os.Environment.getExternalStorageDirectory(), null); } - /** proot and its options up to the binds: the rootfs at {@code root}, starting in {@code cwd}. */ public static List<String> prootPrefix(Context context, File root, String cwd) { return prootPrefix(context, root, cwd, false); } - /** - * As above; {@code fakeRoot} has the guest see uid 0 (proot -0), for the package tools that - * refuse any other uid. Files it writes still belong to the app. - */ public static List<String> prootPrefix(Context context, File root, String cwd, boolean fakeRoot) { List<String> cmd = new ArrayList<>(); cmd.add(prootBinary(context).getPath()); cmd.add("--kill-on-exit"); - // Preserve the host kernel identity while giving the guest the app's branded host name. cmd.add("--kernel-release=" + guestUtsname(SessionPrefs.guestHostname(context))); - // Android's app seccomp policy traps the whole set*id family. Xwayland's Popen() calls - // setgid()/setuid() before it execs xkbcomp and _exit(127)s when they fail, so without - // this the keymap never compiles and Xwayland dies. -i makes proot answer those calls - // itself while still reporting our real ids, so nothing inside sees a different user. int uid = Process.myUid(); if (fakeRoot) { cmd.add("-0"); @@ -191,7 +135,6 @@ public final class LinuxRuntime { return cmd; } - /** The {@code host:guest} (or same-path) bind specs of a session; see {@link #command}. */ public static List<String> binds(Context context, File sessionRoot, File runtimeDir, File externalStorage, List<String> extraBinds) { File root = rootDir(context); @@ -221,39 +164,20 @@ public final class LinuxRuntime { bind(cmd, shm.getPath() + ":/dev/shm"); bindSystemFonts(cmd, root); - // Android denies apps these; glibc, Steam and libcap read them at startup. File fakeProc = new File(root, "etc/droiddeck/proc"); - // libpci picks its procfs backend on whether it can read the /proc/bus/pci directory, which - // the app can, then die()s - exit(1) on the calling process - on the devices file inside - // it, which the app cannot. Chromium loads libpci in its GPU process to name the video - // card, so that exit kills the process; after a few tries CEF gives up on hardware and - // draws the rest of the session on SwiftShader: the client's interface rendered on the - // CPU. An empty list is the truthful answer from in here - nothing the app can see is on - // a PCI bus. Created at session start rather than shipped in the rootfs so an installed - // runtime is fixed too, and the table's guard below binds it only when the real file - // cannot be read, so it can never stand in front of real data. (WinNative, maxjivi05, - // deff1ac6, via Bannerlator 8fb668d1: 44 -> 85 fps scrolling the Big Picture library on a - // OnePlus 15, GPU-process crashes 12 -> 0.) File pciDevices = new File(fakeProc, "pci_devices"); if (!pciDevices.isFile()) { try { - //noinspection ResultOfMethodCallIgnored pciDevices.getParentFile().mkdirs(); - //noinspection ResultOfMethodCallIgnored pciDevices.createNewFile(); } catch (java.io.IOException e) { - // It then fails the isFile() test below and the session runs as it did before. } } - // protonfixes, which GE-Proton and proton-cachyos run before every game, opens this to - // check the esync file limit and dies on the PermissionError, so no game starts with them. - // The value is the 64-bit kernel default. File fileMax = new File(fakeProc, "file-max"); if (!fileMax.isFile()) { try { Files.write(fileMax.toPath(), "9223372036854775807\n".getBytes(StandardCharsets.US_ASCII)); } catch (IOException e) { - // Left unbound, as above. } } String[][] procFiles = { @@ -270,7 +194,6 @@ public final class LinuxRuntime { }; for (String[] entry : procFiles) { File fake = new File(fakeProc, entry[0]); - // A live stand-in the session binds itself (CpuStatComponent's /proc/stat) wins. boolean live = false; if (extraBinds != null) for (String spec : extraBinds) live |= spec.endsWith(":" + entry[1]); if (!live && fake.isFile() && !new File(entry[1]).canRead()) { @@ -288,14 +211,6 @@ public final class LinuxRuntime { return specs; } - /** - * An app process may not open {@code /dev/dri} - the nodes exist but are labelled - * {@code graphics_device}, which stock policy grants surfaceflinger and not us - yet libdrm and - * everything built on it identify a GPU by its render node, and gamescope refuses to offer - * linux-dmabuf without one. The KGSL device Turnip actually drives ({@code gpu_device}, which we - * may open) stands in: it appears as a render node with the sysfs entries libdrm reads, and our - * Turnip build reports the same device numbers for it. - */ private static void bindGpuNode(Context context, List<String> cmd) { StructStat st; try { @@ -324,8 +239,6 @@ public final class LinuxRuntime { if (!Files.isSymbolicLink(subsystem.toPath())) { Os.symlink("/sys/bus/platform", subsystem.getPath()); } - // What MangoHud names a GPU's driver by; msm_drm is how an Adreno's render node reads - // on a mainline kernel, and the driver it reads the load of (bindAdrenoStats). File driver = new File(device, "driver"); if (!Files.isSymbolicLink(driver.toPath())) { Os.symlink("/sys/bus/platform/drivers/msm_drm", driver.getPath()); @@ -339,22 +252,11 @@ public final class LinuxRuntime { bindDrmClass(base, cmd, node, major + ":" + minor); } - /** - * MangoHud (Deck mode's performance overlay, mangoapp) finds the GPU by listing - * {@code /sys/class/drm}, and an exception from that listing ends the process: under an - * enforcing SELinux policy - every retail phone - an app may not list it, so mangoapp died on - * start, gamescope restarted it until the session gave up, and there was no overlay. Where the - * app cannot list it, the session's {@code /sys/class/drm} is ours: the render node above, - * named as an Adreno's is. MangoHud then reads the GPU's stats from KGSL's sysfs, which such a - * device refuses too - SessionService's GpuStatsComponent stands in for that. Where the - * listing is allowed it is left alone. - */ private static void bindDrmClass(File base, List<String> cmd, String node, String devNumbers) { if (new File("/sys/class/drm").list() != null) return; File drmClass = new File(base, "class"); try { if (!drmClass.isDirectory() && !drmClass.mkdirs()) return; - // Only symlinks are ever made here; deleting them must not follow one into its target. File[] old = drmClass.listFiles(); if (old != null) for (File f : old) Files.deleteIfExists(f.toPath()); Os.symlink("/sys/dev/char/" + devNumbers, new File(drmClass, node).getPath()); @@ -363,15 +265,12 @@ public final class LinuxRuntime { } bind(cmd, drmClass.getPath() + ":/sys/class/drm"); android.util.Log.i("LinuxRuntime", "hud: /sys/class/drm is not listable here; the session's lists " + node); - // Also refused to list, also asked for by MangoHud twice a second - an error line each - // time in the session log - and holding nothing it reads on an Adreno phone. if (new File("/sys/class/powercap").list() == null) { File empty = new File(base, "empty-powercap"); if (empty.isDirectory() || empty.mkdirs()) bind(cmd, empty.getPath() + ":/sys/class/powercap"); } } - /** The temperature of a CPU zone the app may read (the first by name), or null. */ public static String cpuTempSource() { for (java.util.Map.Entry<String, File> e : new java.util.TreeMap<>(thermalZones()).entrySet()) { File temp = new File(e.getValue(), "temp"); @@ -380,7 +279,6 @@ public final class LinuxRuntime { return null; } - /** The file the GPU's temperature reads from, or null where the app may read none. */ public static String gpuTempSource() { String kgsl = "/sys/class/kgsl/kgsl-3d0/"; return firstReadable(kgsl + "temp", kgsl + "devfreq/temp", thermalZone("gpu")); @@ -391,12 +289,6 @@ public final class LinuxRuntime { return firstReadable(kgsl + "gpu_busy_percentage", kgsl + "devfreq/gpu_load"); } - /** - * Valve's mangoapp (Deck mode's performance overlay) reads an Adreno GPU's load, clock and - * temperatures from where they are on Valve's own hardware; every Adreno under Android keeps - * them in KGSL's sysfs, readable by the app, though which files a kernel has differs between - * Snapdragon generations. - */ private static void bindAdrenoStats(List<String> cmd, List<String> extraBinds) { String kgsl = "/sys/class/kgsl/kgsl-3d0/"; String gpuTemp = gpuTempSource(); @@ -409,13 +301,10 @@ public final class LinuxRuntime { {thermalZone("ddr"), "/sys/class/thermal/thermal_zone22/temp"}, }; for (String[] stat : stats) { - // A value the session feeds itself (GpuStatsComponent, where KGSL is refused) wins. boolean fed = false; if (extraBinds != null) for (String spec : extraBinds) fed |= spec.endsWith(":" + stat[1]); if (stat[0] != null && !fed) bind(cmd, stat[0] + ":" + stat[1]); } - // Which of the overlay's GPU values this device lets the app read: a phone under an - // enforcing policy refuses some of them, and the overlay then has no line for that value. android.util.Log.i("LinuxRuntime", "hud sources: gpu load " + orNone(stats[0][0]) + ", gpu clock " + orNone(stats[1][0]) + ", gpu temp " + orNone(gpuTemp) + ", ddr temp " + orNone(stats[4][0]) + ", thermal zones " + thermalZones().size() @@ -426,12 +315,6 @@ public final class LinuxRuntime { return path != null ? path : "none"; } - /** - * mangoapp's CPU temperature is the mean of the thermal zones named cpuN-thermal or - * cpuN-top-thermal, as mainline kernels name them; Android kernels name the same sensors - * cpuss-0, cpu-1-0, cpu0-silver-usr, apc1-cpu0-usr and the like, so those zones are shown - * under the mainline name. - */ private static void bindCpuTemps(List<String> cmd, File root) { File name = new File(root, "etc/droiddeck/cpu-thermal-type"); try { @@ -454,7 +337,6 @@ public final class LinuxRuntime { return null; } - /** The temp file of the thermal zone for a sensor: the one named so, else the first whose name has it. */ private static String thermalZone(String sensor) { java.util.Map<String, File> zones = thermalZones(); File zone = zones.get(sensor); @@ -464,7 +346,6 @@ public final class LinuxRuntime { return zone != null ? new File(zone, "temp").getPath() : null; } - /** The device's thermal zones by their sensor's name, in lower case. */ private static java.util.Map<String, File> thermalZones() { java.util.Map<String, File> byType = new java.util.HashMap<>(); File[] zones = new File("/sys/class/thermal").listFiles((dir, n) -> n.startsWith("thermal_zone")); @@ -492,14 +373,12 @@ public final class LinuxRuntime { cmd.add(spec); } - /** PRoot's complex -k format: sysname, nodename, release, version, machine, domain, HWCAP. */ static String guestUtsname(String hostname) { StructUtsname host = Os.uname(); return "\\" + host.sysname + "\\" + hostname + "\\" + host.release + "\\" + host.version + "\\" + host.machine + "\\localdomain\\-1\\"; } - /** X access control and Steam look the session user up by uid: the app uid is root inside. */ public static void writeAccounts(Context context) throws IOException { File root = rootDir(context); int uid = Process.myUid(); diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java index c6d0fb22c..b2d0571cf 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java @@ -29,30 +29,19 @@ import java.nio.file.StandardCopyOption; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; -/** - * Downloads and unpacks the Linux runtime rootfs into {@code files/linuxfs}. It is opt-in: nothing - * fetches it until the user asks for it, and a gamescope session refuses to start without it. - * - * <p>The catalog row names the build, its URL and its sha256, so an interrupted or corrupted - * download is caught before anything is written into the app's files directory. - */ public final class LinuxRuntimeInstaller { private static final String TAG = "LinuxRuntimeInstaller"; - /** Catalog row, beside the other component catalogs in winlator-contents. */ public static final String CATALOG_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/main/linuxfs.json"; private static final String VERSION_FILE = ".version"; - /** What a stage is doing, for code that branches on it rather than on its (translated) words. */ public enum Step { DOWNLOADING, VERIFYING, OTHER } public interface ProgressListener { - /** {@code percent} is -1 while the size is unknown. */ void onProgress(String stage, int percent); - /** The same with its {@link Step}; a listener that branches on the step overrides this one. */ default void onProgress(Step step, String stage, int percent) { onProgress(stage, percent); } @@ -72,11 +61,6 @@ public final class LinuxRuntimeInstaller { } } - /** - * One runtime operation per process. The launcher and session can both install, and a - * launcher rebuilt mid-install (a pad plugged in, dark mode switched) forgets it had: a second - * install would share the first one's archive and staging directory and wreck both. - */ private static final class Job { final CopyOnWriteArrayList<ProgressListener> listeners = new CopyOnWriteArrayList<>(); final CountDownLatch done = new CountDownLatch(1); @@ -95,7 +79,6 @@ public final class LinuxRuntimeInstaller { private LinuxRuntimeInstaller() {} - /** The build currently unpacked, or null when the runtime is not installed. */ public static String installedVersion(Context context) { File marker = new File(LinuxRuntime.rootDir(context), VERSION_FILE); if (!marker.isFile() || !LinuxRuntime.isInstalled(context)) return null; @@ -103,7 +86,6 @@ public final class LinuxRuntimeInstaller { return v == null ? null : v.trim(); } - /** The catalog's current build, or null when it cannot be reached or read. */ public static Release fetchRelease() { String body = Downloader.downloadString(CATALOG_URL); if (body == null || body.isEmpty()) return null; @@ -120,22 +102,8 @@ public final class LinuxRuntimeInstaller { } } - /** - * The guest's home directory, carried across an update rather than replaced with the tarball's - * empty one. Steam installs itself here - the client, the signed-in account and every - * downloaded game - so replacing the rootfs wholesale used to delete all three and leave the - * user re-downloading Steam and signing in again, with their library gone. - */ private static final String USER_DATA = "root"; - /** - * Downloads {@code release} and replaces whatever is installed with it. Returns false and - * leaves the existing runtime alone if the download or the checksum fails; the new rootfs is - * only moved into place once it has been unpacked whole. - * - * <p>{@link #USER_DATA} survives the swap: the system is replaced, what the user put in it is - * not. An update therefore keeps Steam, the login and the installed games. - */ public static boolean install(Context context, Release release, ProgressListener listener) { Job job; boolean owner; @@ -169,7 +137,6 @@ public final class LinuxRuntimeInstaller { } } - /** True while an install is running in this process, whoever started it. */ public static boolean isInstalling() { synchronized (JOB_LOCK) { return running != null && !running.removal && !running.maintenance; @@ -188,7 +155,6 @@ public final class LinuxRuntimeInstaller { synchronized (JOB_LOCK) { return running != null && running.maintenance; } } - /** Reserve an in-place package operation with the same exclusion/progress as runtime installs. */ public static Maintenance beginMaintenance(Context context, String stage) { if (com.droiddeck.launcher.session.SessionState.INSTANCE.getRunning()) return null; com.droiddeck.launcher.session.SessionPhase phase = com.droiddeck.launcher.session.SessionState.INSTANCE.getPhase(); @@ -241,10 +207,6 @@ public final class LinuxRuntimeInstaller { return new File(root.getParentFile(), root.getName() + ".removing"); } - /** - * Follows the runtime operation already running, including removal, after a screen rebuild. - * Blocks until it ends and returns its result, or null when there is no current operation. - */ public static Boolean attach(ProgressListener listener) { Job job; synchronized (JOB_LOCK) { @@ -271,15 +233,12 @@ public final class LinuxRuntimeInstaller { private static boolean installOnce(Context context, Release release, ProgressListener listener) { File archive = new File(context.getCacheDir(), "linuxfs.tar.zst"); try { - // A failed/interrupted explicit removal must finish before a fresh installation. - // Its quarantined home is not user data to carry into an update. File pendingRemoval = removalDirectory(LinuxRuntime.rootDir(context)); if (pendingRemoval.exists() && listener != null) listener.onProgress(context.getString(R.string.rtinst_removing_leftovers), -1); if (!discard(pendingRemoval, null)) throw new IOException("Cannot clear the previous runtime: " + pendingRemoval); emptyTrash(LinuxRuntime.rootDir(context).getParentFile()); String downloading = context.getString(R.string.rtinst_downloading); if (listener != null) listener.onProgress(Step.DOWNLOADING, downloading, 0); - // Downloader reports a 0..1 fraction, or -1 while the total size is unknown. boolean ok = Downloader.downloadFile(release.url, archive, true, (fraction) -> { if (listener != null) { listener.onProgress(Step.DOWNLOADING, downloading, @@ -298,8 +257,6 @@ public final class LinuxRuntimeInstaller { return false; } - // Unpack beside the live rootfs and swap, so a failure here cannot leave a half - // runtime that isInstalled() would happily launch. File root = LinuxRuntime.rootDir(context); File staging = new File(root.getParentFile(), LinuxRuntime.DIR + ".new"); File old = new File(root.getParentFile(), LinuxRuntime.DIR + ".old"); @@ -321,16 +278,10 @@ public final class LinuxRuntimeInstaller { return false; } - // Carry the user's home over before the new rootfs takes the name. A rename inside the - // same filesystem, so a 30 GB library costs nothing and cannot half-copy; the tarball's - // own empty /root is dropped first so the rename has somewhere to land. If this fails - // the update is abandoned and the previous runtime is put back untouched - shipping a - // working system with the user's games gone is the worse outcome. try { RuntimeFileTree.carryHome(old, staging); } catch (IOException e) { Log.w(TAG, "could not carry root across the update; rolling back", e); - // Restore the live name even if cleaning the failed staging tree is denied. old.renameTo(root); try { RuntimeFileTree.delete(staging, null); } catch (IOException cleanup) { Log.w(TAG, "could not clear the failed staging tree", cleanup); } @@ -366,15 +317,8 @@ public final class LinuxRuntimeInstaller { } } - /** Where entries the app cannot unlink are set aside, out of the way of the runtime's names. */ static final String TRASH = ".runtime-trash"; - /** - * Gets {@code tree} out from under its name. What can be deleted is; if something inside - * cannot be (a file made by root or {@code su} carries another SELinux category, and the app - * may not unlink it), the remainder is moved aside into {@link #TRASH} instead of blocking - * every later install and removal on it. False only when even that move is refused. - */ static boolean discard(File tree, java.util.function.LongConsumer progress) { if (RuntimeFileTree.deleteWhatCan(tree, progress)) return true; File trash = new File(tree.getParentFile(), TRASH); @@ -388,7 +332,6 @@ public final class LinuxRuntimeInstaller { return !tree.exists(); } - /** Another try at what {@link #discard} set aside. Whatever still cannot go stays there. */ static void emptyTrash(File parent) { File trash = new File(parent, TRASH); if (trash.exists() && !RuntimeFileTree.deleteWhatCan(trash, null)) @@ -400,7 +343,6 @@ public final class LinuxRuntimeInstaller { return dir.isDirectory() && names != null && names.length == 0; } - /** Reserve removal before returning to the UI, so Play cannot race the worker. */ public static Removal beginUninstall(Context context) { if (com.droiddeck.launcher.session.SessionState.INSTANCE.getRunning()) return null; com.droiddeck.launcher.session.SessionPhase phase = com.droiddeck.launcher.session.SessionState.INSTANCE.getPhase(); @@ -408,7 +350,6 @@ public final class LinuxRuntimeInstaller { return beginUninstall(context.getApplicationContext(), LinuxRuntime.rootDir(context)); } - /** {@code app} words the progress; unit tests pass null and get none. */ static Removal beginUninstall(Context app, File root) { synchronized (JOB_LOCK) { if (running != null) return null; @@ -446,17 +387,12 @@ public final class LinuxRuntimeInstaller { } }; File pending = removalDirectory(root); - // Invalidate the installation before traversing it. A process killed halfway - // through leaves a named removal to resume, never a launchable partial runtime. if (root.exists()) { if (!discard(pending, progress) || !root.renameTo(pending)) throw new IOException("Cannot prepare the runtime for removal"); } else if (!pending.isDirectory() && !pending.mkdirs()) { throw new IOException("Cannot prepare the runtime for removal"); } - // Keep the quarantine until all leftovers are gone, so a failure stays retryable. - // An entry the app may not delete is set aside rather than failing the removal: - // a quarantine that can never clear would block reinstalling for good. for (File leftover : new File[]{ new File(root.getParentFile(), root.getName() + ".new"), new File(root.getParentFile(), root.getName() + ".old"), @@ -484,12 +420,6 @@ public final class LinuxRuntimeInstaller { } } - /** - * A whole-rootfs tar, which is not the shape the shared extractor handles: a distribution - * rootfs is full of hard links (one binary under several names), and an entry written as an - * empty file instead of its link target is a rootfs that boots to nothing. Symlinks, hard - * links and the executable bit are all carried over here. - */ public static boolean extract(Context context, File archive, File destination, ProgressListener listener) { long entries = 0; String extracting = context.getString(R.string.rtinst_extracting); @@ -498,7 +428,6 @@ public final class LinuxRuntimeInstaller { TarArchiveInputStream tar = new TarArchiveInputStream(in)) { TarArchiveEntry entry; while ((entry = tar.getNextTarEntry()) != null) { - // Refuse anything that would land outside the runtime directory. File file = ArchivePaths.inside(destination, entry.getName()); if (file == null) { Log.w(TAG, "skipping entry outside the rootfs: " + entry.getName()); @@ -515,10 +444,6 @@ public final class LinuxRuntimeInstaller { file.delete(); FileUtils.symlink(entry.getLinkName(), file.getAbsolutePath()); } else if (entry.isLink()) { - // A hard link to an earlier entry. Link where the filesystem allows it and - // fall back to a copy, which costs space but always works. - // The target is checked too: a link to "../../shared_prefs/x" would pull a - // file from outside the rootfs into it. File target = ArchivePaths.inside(destination, entry.getLinkName()); if (target == null) { Log.w(TAG, "skipping hard link outside the rootfs: " + entry.getLinkName()); @@ -540,7 +465,7 @@ public final class LinuxRuntimeInstaller { } if ((entry.getMode() & 0111) != 0) file.setExecutable(true, false); } else { - continue; // device nodes and fifos: the runtime binds the real ones + continue; } if (++entries % 2000 == 0 && listener != null) { listener.onProgress(extracting, -1); diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt b/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt index 10538c5bc..08ca03667 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt @@ -5,36 +5,14 @@ import com.droiddeck.launcher.session.SessionPrefs import java.io.File import java.security.MessageDigest -/** - * proot's fast path: the session's path lookups answered inside each process by libblfastpath - * (tools/proot/fastpath), which issues the translated call from a trampoline page proot's seccomp - * filter lets through (tools/proot/patches/0014). A stat or an open costs about a microsecond - * instead of a 15-60 us round trip through the tracer. - * - * The library maps guest paths to host paths itself, so it has to be told the rootfs and the - * binds exactly as proot was given them. Both sides carry a key derived from that view - proot - * PROOT_FASTPATH=<key>, the guest PROOT_FP_KEY=<key> - and the library answers nothing unless the - * process that traces it has the same key: a program that inherited the variables but runs under - * another proot (a Flatpak sandbox, a one-off command) leaves every call to its own proot. - * - * Needs proot's seccomp filter, so "Run proot without seccomp" turns it off as well. - */ object ProotFastPath { const val LIBRARY = "/usr/local/lib/libblfastpath.so" - /** MAX_BINDS in fastpath.c: past it the library turns itself off, so the session keeps it off. */ const val MAX_BINDS = 256 - /** - * Only the app's own proot (libproot.so, built with patches/0014) lets the trampoline through. - * The runtime's fallback copy (LinuxRuntime.prootBinary) is Termux's, which traps those calls - * too and translates their host paths a second time: every lookup then fails inside the - * process and goes to proot anyway, paying for both. - */ fun enabled(context: Context): Boolean = SessionPrefs.prootFastPath(context) && !SessionPrefs.prootNoSeccomp(context) && LinuxRuntime.prootBinary(context).parent == context.applicationInfo.nativeLibraryDir - /** The key for this rootfs and these binds, or null when the library cannot be told them. */ fun key(root: File, binds: List<String>): String? { if (binds.size > MAX_BINDS || binds.any { it.contains('|') } || root.path.contains('|')) return null val digest = MessageDigest.getInstance("SHA-256") @@ -42,10 +20,8 @@ object ProotFastPath { return digest.take(12).joinToString("") { "%02x".format(it) } } - /** What proot's own environment carries. */ fun hostEnv(key: String): Pair<String, String> = "PROOT_FASTPATH" to key - /** The guest's assignments (for `env -i`), describing the view proot was given. */ fun guestEnv(root: File, binds: List<String>, key: String): List<String> = listOf( "PROOT_FP_ROOT=" + root.path, "PROOT_FP_BINDS=" + binds.joinToString("|"), diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeFileTree.java b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeFileTree.java index ca59389f2..44c78c84e 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeFileTree.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeFileTree.java @@ -12,11 +12,9 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.function.LongConsumer; import java.util.function.Predicate; -/** Checked removal of an app-owned runtime tree, including read-only directories. */ final class RuntimeFileTree { private RuntimeFileTree() {} - /** Unit tests stand in for an entry the app cannot unlink (a file another SELinux user made). */ static volatile Predicate<Path> refuseForTest; static void delete(File root, LongConsumer progress) throws IOException { @@ -28,8 +26,6 @@ final class RuntimeFileTree { try { attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); } catch (NoSuchFileException e) { return; } if (attrs.isDirectory()) { - // Change directory permissions before opening it, so mode 000 also works. Links - // are inspected with lstat and unlinked directly; never chmod or visit their target. makeWritable(path.toFile()); try (DirectoryStream<Path> children = Files.newDirectoryStream(path)) { for (Path child : children) delete(child, progress, removed); @@ -39,10 +35,6 @@ final class RuntimeFileTree { if (progress != null) progress.accept(++removed[0]); } - /** - * Removes what it can of {@code root} and carries on past entries it cannot, rather than - * stopping at the first. Returns true when nothing is left. - */ static boolean deleteWhatCan(File root, LongConsumer progress) { return deleteWhatCan(root.toPath(), progress, new long[]{0}); } diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeInstallService.kt b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeInstallService.kt index aef352baa..95a96b899 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeInstallService.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeInstallService.kt @@ -12,15 +12,6 @@ import android.os.IBinder import com.droiddeck.launcher.MainActivity import com.droiddeck.launcher.R -/** - * Keeps the process in the foreground while the Linux runtime downloads and unpacks. The runtime - * is several hundred megabytes; without a foreground service Android may kill the app the moment - * the user switches away, and the install starts over from the resumable part file. - * - * The install itself is [LinuxRuntimeInstaller.install], which runs one job per process: the - * screen that asked for it joins the same job to show progress, so it does not matter which of the - * two gets there first. - */ class RuntimeInstallService : Service() { override fun attachBaseContext(newBase: android.content.Context) = super.attachBaseContext(com.droiddeck.launcher.core.AppLanguage.wrap(newBase)) @@ -39,7 +30,6 @@ class RuntimeInstallService : Service() { val manager = getSystemService(NotificationManager::class.java) var shown = "" val progress = LinuxRuntimeInstaller.ProgressListener { stage, percent -> - // Install percentages and throttled removal counts, including stage transitions. if ("$stage:$percent" != shown) { shown = "$stage:$percent" manager?.notify(NOTIFICATION_ID, notification(stage, percent)) @@ -87,7 +77,6 @@ class RuntimeInstallService : Service() { private const val EXTRA_SHA256 = "sha256" private const val EXTRA_SIZE = "size" - /** Starts the install of [release] in the foreground; a running install is joined instead. */ fun start(context: Context, release: LinuxRuntimeInstaller.Release) { val intent = Intent(context, RuntimeInstallService::class.java) .putExtra(EXTRA_VERSION, release.version) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/UserApps.kt b/app/src/main/java/com/droiddeck/launcher/runtime/UserApps.kt index 21325b936..445a56348 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/UserApps.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/UserApps.kt @@ -19,20 +19,10 @@ import org.json.JSONObject import java.io.File import java.io.RandomAccessFile -/** - * The apps a user adds to the Desktop page: shell scripts, AppImages (from storage or a GitHub - * release) and Flatpak apps. Each opens full screen under gamescope, like an emulator. - * - * A script lives at /opt/droiddeck-apps/<id>: its folder at `files`, the script's name in `entry`. - * The folder is linked where it lies; shared storage is mounted noexec, so a folder holding Linux - * programs is copied in instead. A linked folder outside internal storage is bound into the - * session at its own path ([binds]), so the link resolves inside the guest. - */ object UserApps { private const val TAG = "UserApps" const val GUEST_DIR = "/opt/droiddeck-apps" const val SCRIPT_LAUNCHER = "/usr/local/bin/droiddeck-script-run" - /** An app the desktop asked for (droiddeck-open): its key, then the path it was started from there. */ const val DESKTOP_IMPORT = "desktop-import" private const val FLATPAK_OVERRIDES = ".flatpak" private const val ICON_SIZE = 256 @@ -47,10 +37,6 @@ object UserApps { enum class Kind { SCRIPT, APPIMAGE, FLATPAK } - /** - * [copied]: a script whose folder was copied in rather than linked. [repo]: the GitHub repository - * an AppImage came from, [version] the release it is. - */ class App( val key: String, val kind: Kind, val name: String, val icon: Any?, val detail: String?, val program: String, val args: List<String>, val copied: Boolean = false, @@ -74,10 +60,8 @@ object UserApps { class FlatpakBundle(val file: File) : Source() } - /** [icon]: a picked file's path or a suggested icon's URL. [meta]: files written into the added app's folder. */ class Request(val source: Source, val name: String?, val icon: String?, val meta: Map<String, String> = emptyMap()) - /** How a script's folder reaches the session: linked in place, or copied in ([bytes] to copy). */ class ScriptPlan(val folder: File, val copy: Boolean, val bytes: Long) private fun root(context: Context) = File(LinuxRuntime.rootDir(context), GUEST_DIR.substring(1)) @@ -117,7 +101,6 @@ object UserApps { private fun readName(dir: File) = FileUtils.readString(File(dir, "name"))?.trim()?.takeIf { it.isNotEmpty() } - /** Adds [request]; null on success, else what went wrong. */ fun add(context: Context, request: Request, onProgress: (String, Int) -> Unit): String? { if (!LinuxRuntime.isInstalled(context)) return context.getString(R.string.user_apps_runtime_required) val name = request.name?.trim()?.takeIf { it.isNotEmpty() } @@ -132,7 +115,6 @@ object UserApps { } } - /** [icon] as a file for [use]: a path as it is, a URL downloaded for the while. */ private inline fun <T> withIcon(context: Context, icon: String?, use: (File?) -> T): T { if (icon == null || !icon.startsWith("https://")) return use(icon?.let(::File)) val download = File(context.cacheDir, "icon-${System.nanoTime()}") @@ -143,10 +125,6 @@ object UserApps { } } - /** - * Renames [app] and sets its icon: [icon] a path or URL, "" for its default, null to keep it. - * The Linux desktop's menu entry follows. - */ fun edit(context: Context, app: App, name: String, icon: String?, fex: String? = null): String? { val id = app.key.substringAfter(':') if (id.isEmpty() || '/' in id || id.startsWith(".")) return null @@ -177,7 +155,6 @@ object UserApps { return when (app.kind) { Kind.SCRIPT -> { val dir = File(root(context), id) - // The link goes, never the folder it points to. File(dir, "files").delete() FileUtils.delete(dir) menuEntry(context, id).delete() @@ -190,7 +167,6 @@ object UserApps { } } - /** Folders linked from outside internal storage, bound into a session at their own paths. */ fun binds(context: Context): List<String> { val internal = Environment.getExternalStorageDirectory().canonicalPath return root(context).listFiles().orEmpty().mapNotNull { dir -> @@ -199,7 +175,6 @@ object UserApps { }.distinct() } - /** The desktop's import of [key] as the session sees it: an added script's folder, or an AppImage's. */ fun desktopImport(context: Context, key: String, path: String): String? { root(context).listFiles()?.firstOrNull { dir -> File(dir, "entry").isFile && FileUtils.readString(File(dir, DESKTOP_IMPORT))?.lineSequence()?.firstOrNull() == key @@ -263,7 +238,6 @@ object UserApps { "Terminal=false\nCategories=Game;\nX-DroidDeck-App=$id\n") } - /** Copies [from] into [to], keeping Linux programs and scripts executable. */ private fun copyTree(from: File, to: File) { from.walkTopDown().forEach { src -> val dst = File(to, src.relativeTo(from).path) @@ -275,7 +249,6 @@ object UserApps { } } - /** [source] (PNG, JPEG, WebP or SVG) as the app's icon: a PNG no larger than [ICON_SIZE] on a side. */ internal fun saveIcon(source: File, target: File): Boolean { val bitmap = (if (isSvg(source)) renderSvg(source) else decodeScaled(source)) ?: return false return try { @@ -306,7 +279,6 @@ object UserApps { f.inputStream().use { val head = ByteArray(1024); val n = it.read(head); n > 0 && String(head, 0, n).contains("<svg") } } catch (e: Exception) { false } - /** An SVG drawn at [ICON_SIZE], centred in its own proportions. */ private fun renderSvg(source: File): Bitmap? = try { val svg = source.inputStream().use { SVG.getFromInputStream(it) } if (svg.documentViewBox == null && svg.documentWidth > 0 && svg.documentHeight > 0) { @@ -319,7 +291,6 @@ object UserApps { Log.w(TAG, "svg ${source.path}", e); null } - /** "owner/repo" from what was typed: any link into the repository, its clone address, or the short form. */ fun githubRepo(input: String): String? { val t = input.trim() val m = Regex("^(?:https?://)?(?:www\\.)?github\\.com[/:]([\\w.-]+)/([\\w.-]+)", RegexOption.IGNORE_CASE).find(t) @@ -329,10 +300,6 @@ object UserApps { return if (repo.isEmpty() || repo == "." || repo == "..") null else "${m.groupValues[1]}/$repo" } - /** - * The AppImage among a release's files: named for aarch64/arm64, else for no other CPU, else the - * x86_64 one, which runs through FEX. - */ internal fun pickAsset(names: List<String>): String? { val images = names.filter { it.endsWith(".appimage", ignoreCase = true) } return images.firstOrNull { Regex("aarch64|arm64", RegexOption.IGNORE_CASE).containsMatchIn(it) } @@ -340,7 +307,6 @@ object UserApps { ?: images.firstOrNull { Regex("x86_64|x86-64|amd64|x64", RegexOption.IGNORE_CASE).containsMatchIn(it) } } - /** The newest release of [repo] with an AppImage this device runs, preferring full releases; else what went wrong. */ private fun latestRelease(context: Context, repo: String): Pair<Release?, String?> { val body = Downloader.downloadString("https://api.github.com/repos/$repo/releases?per_page=15") val releases = body?.let { runCatching { JSONArray(it) }.getOrNull() } @@ -358,7 +324,6 @@ object UserApps { return pick to if (pick == null) context.getString(R.string.user_apps_github_no_appimage, repo) else null } - /** Downloads [release] and hands it to [use]; null on success, else what went wrong. */ private fun withDownload(context: Context, release: Release, onProgress: (String, Int) -> Unit, use: (File) -> String?): String? { val download = File(context.cacheDir, "github-${release.asset}") try { @@ -382,10 +347,6 @@ object UserApps { } } - /** - * Images in [repo] that look like its icon, best first, then its owner's picture: links to show - * as suggestions and to download once one is picked. - */ fun githubIcons(repo: String): List<String> { val info = Downloader.downloadString("https://api.github.com/repos/$repo")?.let { runCatching { JSONObject(it) }.getOrNull() } ?: return emptyList() val branch = info.optString("default_branch").ifEmpty { return emptyList() } @@ -410,13 +371,11 @@ object UserApps { return urls + listOfNotNull(avatar) } - /** Whether [app], added from GitHub, has a newer release than the one it is. */ fun checkUpdate(context: Context, app: App): UpdateCheck { val repo = app.repo ?: return UpdateCheck.Failed(context.getString(R.string.user_apps_update_failed)) val (release, problem) = latestRelease(context, repo) if (release == null) return UpdateCheck.Failed(problem ?: context.getString(R.string.user_apps_update_failed)) val published = FileUtils.readString(File(AppImageManager.dir(context, app.key.substringAfter(':')), GITHUB_PUBLISHED))?.trim().orEmpty() - // Releases are compared by date, so a newest one without an ARM64 build never reads as a downgrade. val newer = if (published.isNotEmpty() && release.published.isNotEmpty()) release.published > published else release.tag != app.version return if (newer) UpdateCheck.Available(release) else UpdateCheck.Current(app.version ?: release.tag) } @@ -428,7 +387,6 @@ object UserApps { } } - /** A Flathub app ID from what was typed: the ID itself or its Flathub page. */ fun flatpakId(input: String): String? { val t = input.trim().removeSuffix("/").substringAfterLast('/') return t.takeIf { Regex("^[A-Za-z][\\w-]*(\\.[A-Za-z0-9_][\\w-]*){2,}$").matches(it) } @@ -444,7 +402,6 @@ object UserApps { private fun addFlatpakBundle(context: Context, file: File, name: String?, icon: File?, onProgress: (String, Int) -> Unit): String? { if (!file.isFile) return context.getString(R.string.user_apps_file_gone) if (!FlatpakManager.ready(context)) FlatpakManager.setup(context, onProgress)?.let { return it } - // Shared storage is bound into the runtime at its own path; a file anywhere else is copied in. val shared = Environment.getExternalStorageDirectory().canonicalPath val copy = File(LinuxRuntime.rootDir(context), "tmp/droiddeck-bundle.flatpak").takeIf { !file.canonicalPath.startsWith("$shared/") } try { diff --git a/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt index fc6526f59..3b3456139 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt @@ -8,30 +8,11 @@ import com.droiddeck.launcher.core.SessionPart import java.io.File import kotlin.math.abs -/** - * The battery the Steam client can see. - * - * The client reads `/sys/class/power_supply/BAT<n>/...` - a laptop's or a Deck's naming and files - * (`energy_now`, `power_now`, `time_to_empty_now`, a `uevent` of `POWER_SUPPLY_*` lines). Android - * names its supply `battery` and lays it out differently, and on many phones an app may not read - * it at all, so the client showed no battery. This writes one `BAT1` (the Deck's name) - * from Android's own battery API - the same on every device - into a directory the session binds - * over `/sys/class/power_supply`, refreshed every few seconds while the session runs. - * - * Steam's Quick Access Menu takes its time estimates from neither: it reads the files in `/run/vpower` (as on - * SteamOS), and with those absent showed "?h ?m" for projected battery life and for time to full. - * The same estimates are written there too, into [vpowerDir] in the guest's own `/run`. - * - * Values: charge in µAh from the charge counter; energy in µWh from charge × voltage; power in µW - * from |current| × voltage; time to empty from charge ÷ |average current| while discharging. Where - * a device reports no current, the client still gets status and percentage. - */ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart() { @Volatile private var running = false private var thread: Thread? = null override fun start() { - // Older builds exported the same aggregate battery twice; MangoHud counts both supplies. File(dir, "BAT0").deleteRecursively() running = true write() @@ -48,7 +29,6 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart running = false thread?.interrupt() thread = null - // Stale estimates would outlive the session in the rootfs; Steam falls back to "?h ?m". for (name in VPOWER_FILES) File(vpowerDir, name).delete() } @@ -67,7 +47,6 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart val chargeNowUah = bm?.getLongProperty(BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER)?.takeIf { it > 0 && it != Long.MIN_VALUE } ?: 0L val currentNowUa = bm?.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)?.takeIf { it != Long.MIN_VALUE } ?: 0L val currentAvgUa = bm?.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE)?.takeIf { it != Long.MIN_VALUE && it != 0L } ?: currentNowUa - // Android's sign convention varies by vendor; the status says which way the current flows. val discharging = status == BatteryManager.BATTERY_STATUS_DISCHARGING || status == BatteryManager.BATTERY_STATUS_NOT_CHARGING val chargeFullUah = if (chargeNowUah > 0 && pct > 0) chargeNowUah * 100 / pct else 0L val energyNowUwh = chargeNowUah * voltageUv / 1_000_000L @@ -129,7 +108,6 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart "ac_status" to if (plugged) "Connected" else "Disconnected", "battery_percent" to "%.2f".format(java.util.Locale.ROOT, pct.toDouble()), "battery_status" to statusText, - // Steam reads a separate file for each direction; -1 = no estimate. "secs_until_battery_full" to (if (timeToFullS > 0) timeToFullS else -1L).toString(), "secs_until_shutdown_request" to (if (timeToEmptyS > 0) timeToEmptyS else -1L).toString(), ) @@ -139,7 +117,6 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart } } - /** Atomic per file: a reader in the guest never sees a half-written value. */ private fun put(target: File, text: String) { val tmp = File(target.parentFile, "." + target.name + ".tmp") tmp.writeText(text) diff --git a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt index aa489198f..50d02c49b 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt @@ -22,30 +22,9 @@ import java.io.File import java.io.FileInputStream import java.io.FileOutputStream -/** - * FEX, DXVK and VKD3D-Proton per Proton, for the Linux Steam client. - * - * Every Proton the client runs games with keeps its own copy of these three inside its tree. This - * lists them, stores packages (.wcp: tar + zstd with a profile.json) downloaded from the Nightlies - * "-Linux" releases or imported, swaps one into a Proton, keeps the Proton's shipped files as - * "original bundles" - one per Proton build, so a Steam update adds a new one instead of replacing - * the last - restores those, and deletes stored packages. - * - * Where a Proton reads each component (its own `proton` script, ARM64 host): - * DXVK files/lib/wine/dxvk/{aarch64,i386}-windows -> the prefix's system32 / syswow64 - * VKD3D files/lib/wine/vkd3d-proton/{aarch64,i386}-windows -> the same, on every launch - * FEX files/lib/wine/aarch64-windows/lib{arm64ec,wow64}fex.dll - * files/lib/wine/aarch64-unix/lib{arm64ec,wow64}fex.so (FEX 2607 and newer) - * Proton copies DXVK and VKD3D into the prefix on every normal launch, so a swap applies the next - * time a game starts. A swap into a Proton a running game is using waits until that game exits. - * - * Same formats and data layout as the Decky "Components Manager" plugin, whose data is taken over - * once on first use. - */ object ComponentsManager { private const val TAG = "Components" private const val NIGHTLIES = "The412Banner/Nightlies" - /** Release tag -> component. Only the Linux repacks: the Android releases carry bionic unixlibs. */ private val RELEASES = linkedMapOf( "FexCore-Linux" to "fex", "Dxvk-Linux" to "dxvk", @@ -66,13 +45,7 @@ object ComponentsManager { "$WINE/aarch64-unix/libwow64fex.so", ) private val COMP_DIR = mapOf("dxvk" to "$WINE/dxvk", "vkd3d" to "$WINE/vkd3d-proton") - /** The folders a Proton reads on ARM64. */ private val PE_ARCHES = listOf("aarch64-windows", "i386-windows") - /** - * The DLLs that ARE the component. A swap clears exactly these before unpacking, so no file of - * the previous version is left mixed in; anything else a Proton keeps beside them - GE's - * openvr_api_dxvk.dll, which its launcher copies into every prefix and aborts without - stays. - */ private val COMP_DLLS = mapOf( "dxvk" to setOf("d3d8.dll", "d3d9.dll", "d3d10.dll", "d3d10_1.dll", "d3d10core.dll", "d3d11.dll", "dxgi.dll"), "vkd3d" to setOf("d3d12.dll", "d3d12core.dll"), @@ -93,7 +66,6 @@ object ComponentsManager { data class CatalogItem(val file: String, val comp: String, val release: String, val url: String, val size: Long, val digest: String) data class ProtonView( val proton: Proton, val components: Map<String, Component>, val originals: List<Original>, val inUseByGame: Boolean, - /** When a launch last had to put this Proton's chosen files back (epoch seconds), or 0. */ val reappliedAt: Long = 0, val sync: EsyncPacks.ToolState? = null, ) @@ -102,7 +74,6 @@ object ComponentsManager { private val lock = Any() - // ------------------------------------------------------------------ paths private fun root(context: Context) = LinuxRuntime.rootDir(context) private fun steam(context: Context) = File(root(context), "root/.local/share/Steam") @@ -112,10 +83,6 @@ object ComponentsManager { private fun originalsDir(context: Context) = File(dataDir(context), "originals").apply { mkdirs() } private fun stateFile(context: Context) = File(dataDir(context), "state.json") private fun catalogFile(context: Context) = File(dataDir(context), "catalog.json") - /** - * Inside the Linux runtime, read by the Proton launch wrappers (steam-compatibility's - * bl_components) before every game launch: desired.tsv + an unpacked copy of each package in use. - */ private const val LAUNCH_DIR = "root/.local/share/droiddeck-components" private const val LEGACY_LAUNCH_DIR = "root/.local/share/bannerlator-components" private fun launchDir(context: Context): File { @@ -135,7 +102,6 @@ object ComponentsManager { fun safeName(s: String): String = s.replace(Regex("[^A-Za-z0-9._+-]+"), "_").trim('_').ifEmpty { "unnamed" } - // ------------------------------------------------------------------ state private fun pendingStateFile(context: Context) = File(dataDir(context), "state.pending.json") @@ -173,7 +139,6 @@ object ComponentsManager { val expected = transaction.getString("launchSha256") val published = File(launchDir(context), "desired.tsv") val staged = File(launchDir(context), "desired.tsv.tmp") - // The launch-table rename is the commit point. Never read stale app selections after it. if (!staged.exists() && published.isFile && Hashes.sha256(published) == expected) { writeStateFile(stateFile(context), state.toString(2)) Log.i(TAG, "recovered published component selections") @@ -206,7 +171,6 @@ object ComponentsManager { private fun JSONObject.sub(key: String): JSONObject = optJSONObject(key) ?: JSONObject().also { put(key, it) } - // Keep launch-time restore rows after the last per-game selection is removed. private fun rememberProfiledComponent(state: JSONObject, comp: String) { val profiled = state.optJSONArray("profiledComponents")?.let { values -> (0 until values.length()).mapNotNull { values.optString(it).takeIf(COMPONENTS::contains) } @@ -252,7 +216,6 @@ object ComponentsManager { saveState(context, state) } - /** Takes over the Decky plugin's packages, originals and swap records, once. */ private fun migratePlugin(context: Context) { val marker = File(dataDir(context), ".plugin-imported") if (marker.isFile) return @@ -275,7 +238,6 @@ object ComponentsManager { marker.writeText("1\n") } - // ------------------------------------------------------------------ Protons private fun libraryRoots(context: Context): List<File> { val roots = mutableListOf(steam(context)) @@ -298,13 +260,10 @@ object ComponentsManager { if (parts.size > 1) parts[1] else parts.firstOrNull()?.ifEmpty { null } ?: "unknown" }.getOrDefault("unknown") - /** Every Proton tree with a Wine inside. Launch wrappers pointing at another Proton are left out. */ fun protons(context: Context): List<Proton> { val found = LinkedHashMap<String, Proton>() val bases = listOf(File(steam(context), "compatibilitytools.d") to false) + libraryRoots(context).map { File(it, "steamapps/common") to true } - // Not canonical: /data/user/0 is a link to /data/data on many devices, and the guest path is - // what is left after removing the rootfs prefix exactly as the paths were built. val rootPath = root(context).absolutePath.trimEnd('/') for ((base, valve) in bases) { val entries = base.listFiles()?.sortedBy { it.name } ?: continue @@ -322,17 +281,9 @@ object ComponentsManager { private fun proton(context: Context, id: String) = protons(context).firstOrNull { it.id == id } ?: throw IllegalStateException("No Proton with id $id") - /** - * Is anything running on this Proton right now - a game, or the client's own start-up runs, - * whose Wine processes can outlive them? Swaps wait while it is. Read from the command lines. - */ fun inUse(context: Context, proton: Proton): Boolean = anyProcess(context, proton) { paths, text -> processUsesProton(paths, text, gameOnly = false) } - /** - * Is a game running on this Proton? Only a launch runs proton with the verb waitforexitandrun - * (Proton's own script stays up until the game exits); the client's start-up runs do not. - */ fun gameRunning(context: Context, proton: Proton): Boolean = anyProcess(context, proton) { paths, text -> processUsesProton(paths, text, gameOnly = true) } @@ -357,14 +308,12 @@ object ComponentsManager { return false } - // ------------------------------------------------------------------ component files private fun readVersionFile(f: File): String = runCatching { val text = f.readText().trim() Regex("\\(([^)]+)\\)").find(text)?.groupValues?.get(1) ?: text.split(Regex("\\s+")).last() }.getOrDefault("") - /** The FEX build's version name; [unnamed] when its DLL carries none, "" when there is no FEX. */ private fun fexVersion(dir: File, unnamed: String = "FEX (build without a version name)"): String { val dll = File(dir, FEX_FILES[0]) if (!dll.isFile) return "" @@ -384,7 +333,6 @@ object ComponentsManager { private fun fingerprint(dir: File, comp: String): Map<String, String> = compFiles(dir, comp).filter { owned(comp, it) }.associateWith { Hashes.sha256(File(dir, it)) } - // ------------------------------------------------------------------ packages private fun normalize(name: String) = name.removePrefix("./").trimStart('/') @@ -406,14 +354,12 @@ object ComponentsManager { return Package(f.name, comp, prof.optString("versionName", f.nameWithoutExtension), prof.optString("description"), f.length()) } - /** The stored packages, newest version first. */ fun packages(context: Context): List<Package> = packagesDir(context).listFiles() ?.filter { it.isFile && it.name.endsWith(".wcp") } ?.mapNotNull { f -> runCatching { packageInfo(context, f) }.onFailure { Log.w(TAG, "skipping ${f.name}", it) }.getOrNull() } ?.sortedWith(newestFirst { it.version.ifEmpty { it.file } }) ?: emptyList() - /** Unpacks a package's files/ into a Proton; for DXVK/VKD3D the component's own DLLs are replaced as a set. */ private fun installFiles(context: Context, dir: File, comp: String, wcp: File): Map<String, String> { val staged = File(dataDir(context), "staging-${System.nanoTime()}") try { @@ -472,7 +418,6 @@ object ComponentsManager { check(tmp.renameTo(dest)) { "Could not save original bundle ${dest.name}" } } - // ------------------------------------------------------------------ originals private fun originalFile(context: Context, p: Proton, comp: String) = File(originalsDir(context), "${p.id}/${safeName(p.version)}/$comp.wcp").apply { parentFile?.mkdirs() } @@ -488,10 +433,6 @@ object ComponentsManager { } } - /** - * Saves a Proton's shipped files for this component before they are first replaced in this - * Proton build. A build already swapped into is not saved again: its files are ours. - */ private fun ensureOriginal(context: Context, p: Proton, comp: String, state: JSONObject) { val dest = originalFile(context, p, comp) if (dest.isFile) return @@ -520,7 +461,6 @@ object ComponentsManager { Log.i(TAG, "saved original $comp of ${p.name} ${p.version}") } - /** Saves the originals of every Proton build not seen yet (first open, and after a Steam update). */ fun snapshotAll(context: Context) = synchronized(lock) { migratePlugin(context) val state = loadState(context) @@ -530,7 +470,6 @@ object ComponentsManager { runCatching { saveState(context, state) }.onFailure { Log.w(TAG, "launch state", it) } } - // ------------------------------------------------------------------ view fun snapshot(context: Context): Snapshot = synchronized(lock) { migratePlugin(context) @@ -575,26 +514,15 @@ object ComponentsManager { Snapshot(views, packages(context)) } - /** - * The label of a chosen or queued original, in the app's language: the stored "label" was written - * in English when it was chosen. Null for a package, whose label is its version. - */ private fun originalLabel(context: Context, entry: JSONObject): String? = when { entry.optString("kind") == "original" -> context.getString(R.string.cmgr_original_version, entry.optString("protonVersion")) entry.optString("file").startsWith("original ") -> context.getString(R.string.cmgr_original_from, entry.optString("file").removePrefix("original ")) else -> null } - // ------------------------------------------------------------------ launch-time enforcement private data class PreparedLaunchState(val text: String, val keep: Set<String>) - /** - * Stages what each Proton should use for the launch wrappers. General rows use scope `*`; - * per-game rows use the game's compatdata id and replace General for that component. Deletion - * rows remove managed DLLs absent from the chosen package, so switching games cannot leave files - * from the previous game's component set behind. - */ private fun prepareLaunchState(context: Context, state: JSONObject): PreparedLaunchState { val dir = launchDir(context) val store = File(dir, "store").apply { mkdirs() } @@ -694,9 +622,7 @@ object ComponentsManager { return PreparedLaunchState(lines.toString(), keep) } - // ------------------------------------------------------------------ actions - /** Applies a package now, or queues it when a game is running on that Proton. Returns what happened. */ fun swap(context: Context, protonId: String, file: String): String = synchronized(lock) { val p = proton(context, protonId) val wcp = File(packagesDir(context), safeName(file)) @@ -749,7 +675,6 @@ object ComponentsManager { .put("protonVersion", p.version).put("files", JSONObject(fingerprint(p.dir, comp) as Map<*, *>)).put("at", System.currentTimeMillis() / 1000)) } - /** Runs the swaps that waited for a game to close. Cheap when nothing is queued. */ fun applyQueued(context: Context): List<String> = synchronized(lock) { val state = loadState(context) val queued = state.sub("queued") @@ -815,7 +740,6 @@ object ComponentsManager { context.getString(R.string.cmgr_deleted_original, protonVersion, LABEL[comp]) } - /** Imports a -linux .wcp from anywhere the app can read (the file picker hands over a copy). */ fun importPackage(context: Context, source: File, name: String): Package = synchronized(lock) { val info = packageInfo(context, source) val dest = File(packagesDir(context), safeName(name).let { if (it.endsWith(".wcp")) it else "$it.wcp" }) @@ -823,9 +747,7 @@ object ComponentsManager { info.copy(file = dest.name) } - // ------------------------------------------------------------------ catalog - /** The Nightlies listing: cached on disk, fetched again only when [force] (the refresh button). */ fun catalog(context: Context, force: Boolean): Catalog { val cache = catalogFile(context) if (!force && cache.isFile) runCatching { return parseCatalog(JSONObject(cache.readText())) } @@ -838,7 +760,6 @@ object ComponentsManager { val a = assets.getJSONObject(i) val name = a.optString("name") if (!name.endsWith(".wcp")) continue - // Only packages GitHub has a sha256 for are offered: the download is checked against it. if (!Hashes.isGithubSha256(a.optString("digest"))) continue items.put(JSONObject().put("file", name).put("comp", comp).put("release", tag) .put("url", a.optString("browser_download_url")).put("size", a.optLong("size")).put("digest", a.optString("digest"))) @@ -858,15 +779,9 @@ object ComponentsManager { return Catalog(items, o.optLong("fetchedAt")) } - /** By the version in [of], highest first: the newest builds lead a list, as they are what is picked. */ private fun <T> newestFirst(of: (T) -> String): Comparator<T> = Comparator { a, b -> ProtonExtras.compareVersions(of(b), of(a)) } - /** - * A package Proton does not ship, required on the GPUs listed here. An empty [models] set - * means every model in [families]. A row the user already chose in Components is left alone; - * a Steam update that puts the Proton's own files back is replaced on the next session. - */ internal data class ForcedPackage( val families: Set<GpuInfo.Family>, val comp: String, @@ -878,28 +793,15 @@ object ComponentsManager { } internal val FORCED_PACKAGES = listOf( - // DXVK 3 refuses to start unless the device exposes storageBuffer8BitAccess. Turnip on - // Adreno 6xx does not. 2.7.1 is the last DXVK 2 package on the Dxvk-Linux tag. ForcedPackage(setOf(GpuInfo.Family.A6XX), "dxvk", "dxvk-2.7.1-linux.wcp", "Dxvk-Linux"), ) - /** - * True when this Proton should receive the forced package. No active swap means the Proton's - * own copy. A record for a different Proton build means a Steam update put that copy back, - * whichever package was recorded. A record for this build is a Components choice and stays. - */ internal fun forcedPackageWanted(activeFile: String?, protonVersionMatches: Boolean): Boolean = activeFile.isNullOrEmpty() || !protonVersionMatches - /** The catalog row for this file and release, or null. */ internal fun catalogMatch(items: List<CatalogItem>, file: String, release: String): CatalogItem? = items.firstOrNull { it.file == file && it.release == release } - /** - * Install every [FORCED_PACKAGES] row this GPU matches into each Proton that - * [forcedPackageWanted] selects. Returns a line for the log, or null when this GPU or these - * Protons need nothing. A failed fetch throws and the session logs it without stopping. - */ fun ensureForcedPackages(context: Context): String? = ensureForcedPackages( context, GpuInfo.detect(), { file, release -> catalogItem(context, file, release) }, @@ -935,7 +837,6 @@ object ComponentsManager { return lines.joinToString("; ").ifEmpty { null } } - /** Null when [rule]'s package is stored and readable. Otherwise the log line, after a fetch. */ private fun missingPackage( context: Context, rule: ForcedPackage, @@ -951,11 +852,9 @@ object ComponentsManager { return null } - /** The cached Nightlies row for this file, fetching the listing when the cache does not have it. */ internal fun catalogItem(context: Context, file: String, release: String): CatalogItem? = resolveCatalogItem(file, release, { catalog(context, false).items }, { catalog(context, true).items }) - /** [cached] when it has the row, otherwise [refresh]. [refresh] is not called on a hit. */ internal fun resolveCatalogItem( file: String, release: String, @@ -963,7 +862,6 @@ object ComponentsManager { refresh: () -> List<CatalogItem>, ): CatalogItem? = catalogMatch(cached(), file, release) ?: catalogMatch(refresh(), file, release) - /** Downloads a catalog item into storage, verifying the release's sha256 and the package type. */ fun download(context: Context, item: CatalogItem, progress: (Int) -> Unit): Package { require(item.url.startsWith("https://github.com/$NIGHTLIES/releases/download/")) { context.getString(R.string.cmgr_nightlies_only) } require(Hashes.isGithubSha256(item.digest)) { context.getString(R.string.cmgr_list_predates_checksums) } diff --git a/app/src/main/java/com/droiddeck/launcher/session/CpuStatComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/CpuStatComponent.kt index 7c5aa9a28..40e459195 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/CpuStatComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/CpuStatComponent.kt @@ -5,19 +5,6 @@ import android.util.Log import com.droiddeck.launcher.core.SessionPart import java.io.File -/** - * A `/proc/stat` that moves. - * - * Android has refused apps `/proc/stat` since 8.0, and the session's stand-in for it was one - * `cpu0` line of zeros - enough for glibc and Steam to start, but every CPU load in the session - * read 0 % (the performance overlay showed one core, idle). The kernel still lets apps read each - * core's idle residency (`cpuidle/stateN/time`, µs, the same label as the cpufreq files), which is - * how overlays on Android work out CPU load: a core's busy time is the time that passed less the - * time it spent idle. This writes `/proc/stat` from that, one line per core, refreshed twice a - * second. Counters only grow - a sample's idle is never more than the time that passed - so a - * reader taking the difference of two reads never sees a negative. Where `/proc/stat` is readable, - * or the idle times are not, nothing is done. - */ class CpuStatComponent(val file: File) : SessionPart() { @Volatile private var running = false private var thread: Thread? = null @@ -28,7 +15,6 @@ class CpuStatComponent(val file: File) : SessionPart() { private var lastMs = 0L private val bootSeconds = (System.currentTimeMillis() - SystemClock.elapsedRealtime()) / 1000 - /** Writes the first sample; true when it should be bound over `/proc/stat`. */ fun prepare(): Boolean { if (File("/proc/stat").canRead() || cores.isEmpty() || idleUs(0) == null) return false lastMs = SystemClock.elapsedRealtime() @@ -62,7 +48,6 @@ class CpuStatComponent(val file: File) : SessionPart() { if (elapsed <= 0) return lastMs += elapsed * MS_PER_TICK for (i in cores) { - // An offline core has no idle residency to count and runs nothing: all idle. val us = idleUs(i) val idleTicks = if (us == null) elapsed else ((us - lastIdleUs[i]) / US_PER_TICK).coerceIn(0, elapsed) if (us != null) lastIdleUs[i] = us @@ -71,7 +56,6 @@ class CpuStatComponent(val file: File) : SessionPart() { } } - /** The whole file at once, renamed into place: a reader opens either the last sample or this one. */ private fun write() { val text = StringBuilder() text.append("cpu ${busy.sum()} 0 0 ${idle.sum()} 0 0 0 0 0 0\n") @@ -88,14 +72,12 @@ class CpuStatComponent(val file: File) : SessionPart() { companion object { private const val TAG = "SessionService" private const val PERIOD_MS = 500L - /** /proc/stat counts in USER_HZ, 100 a second on every Linux the guest runs. */ private const val MS_PER_TICK = 10L private const val US_PER_TICK = 10_000L private fun coreCount(): Int = File("/sys/devices/system/cpu").list()?.count { it.matches(Regex("cpu\\d+")) } ?: 0 - /** The core's total idle residency in µs, or null when it has none to read (offline). */ private fun idleUs(core: Int): Long? { val states = File("/sys/devices/system/cpu/cpu$core/cpuidle").listFiles { f -> f.name.startsWith("state") } ?: return null diff --git a/app/src/main/java/com/droiddeck/launcher/session/CrashHandler.kt b/app/src/main/java/com/droiddeck/launcher/session/CrashHandler.kt index 50d9213e9..3605db880 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/CrashHandler.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/CrashHandler.kt @@ -6,16 +6,6 @@ import java.io.File import java.io.PrintWriter import java.io.StringWriter -/** - * The last thing that runs when our own code throws on any thread: the stack trace into the - * session's folder, then the folder's ending ([SessionArtifacts.collect]), then Android's own - * handler so the crash is still reported and still lands in the crash buffer. Bounded, because - * a handler that hangs turns a crash into a frozen app: the collect gets a few seconds and the - * process dies regardless. - * - * A native crash (the compositor, a JNI call) never reaches here - the process is gone before - * Java sees anything. [SessionArtifacts.finishAbandoned] at the next start covers that case. - */ object CrashHandler { private const val TAG = "CrashHandler" @@ -40,10 +30,8 @@ object CrashHandler { worker.start() worker.join(8000) } - // The session's tree would outlive us otherwise, and block the next launch. OrphanReaper.reap("app crash") } catch (e: Throwable) { - // Nothing more to do for a crash inside the crash handler. } finally { previous?.uncaughtException(thread, error) ?: Runtime.getRuntime().halt(2) } diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt b/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt index a87a6c096..023835fcc 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt @@ -11,7 +11,6 @@ import java.io.File object GameEnvironmentStore { private const val SETTINGS = "game-environment.json" private const val GUEST_FILE = "root/.config/droiddeck/game-environment.json" - /** The published file's extra DXVK options (droiddeck-game-env reads it; the app's own file has none). */ const val DXVK_CONFIG = "dxvkConfig" const val DXVK_CONFIG_GAMES = "dxvkConfigGames" @@ -76,8 +75,6 @@ object GameEnvironmentStore { fun publish(context: Context, config: GameEnvironment.Config = read(context)) { val resolved = config.copy(shared = GameEnvironment.defaults(SessionPrefs.fexPreset(context), SessionPrefs.forceSsbs(context)) + config.shared) val json = encode(resolved) - // The Display page's texture filtering, as DXVK options the launcher appends to - // DXVK_CONFIG after the profiles - a user's own DXVK_CONFIG entry keeps its options. val dxvk = TextureFiltering.dxvkOptions( SessionPrefs.textureAnisotropy(context), SessionPrefs.textureLodBias(context), TextureFiltering.autoLodBias(SessionState.upscaleRatio), diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameProfileInventoryCache.kt b/app/src/main/java/com/droiddeck/launcher/session/GameProfileInventoryCache.kt index 1574c7d36..b140620e3 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameProfileInventoryCache.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameProfileInventoryCache.kt @@ -6,7 +6,6 @@ internal class GameProfileInventoryCache<T : Any> { private var revision: Long? = null private var value: List<T>? = null - // Called only by the refresh worker; the UI invalidates through request revisions. fun get(requestedRevision: Long, load: () -> List<T>): List<T> = synchronized(lock) { value?.takeIf { revision == requestedRevision } ?: load().toList().also { value = it diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameSaves.kt b/app/src/main/java/com/droiddeck/launcher/session/GameSaves.kt index 8e9fe257d..3a31da011 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameSaves.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameSaves.kt @@ -17,30 +17,18 @@ import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -/** - * Game saves in and out of a game's Proton prefix, as zips other apps read: GameHub keeps a prefix's - * drive_c rooted at "/drive_c/users/steamuser/…", Winlator / WinNative / Bannerlator at - * "drive_c/users/xuser/…". Proton here runs as steamuser too, so on the way in any user folder but - * Public becomes steamuser, and on the way out the chosen layout decides the user segment. - * - * Saves belong to the game's prefix (compatdata/<appid>/pfx), whichever Proton it runs with. - * Ported from Bannerlator's GameSaveBackup and SaveLocator. - */ object GameSaves { enum class Layout(val app: String, val rootPrefix: String, val user: String) { GAMEHUB("GameHub", "/drive_c/", "steamuser"), WINLATOR("Winlator", "drive_c/", "xuser"); - /** "GameHub zip", in the app's language. */ fun label(context: Context): String = context.getString(R.string.saves_layout_zip, app) } - /** A game, its prefix, and the save folders found in it. */ data class Game(val game: Library.SteamGame, val prefix: File?, val saves: List<SaveDir>) { val launched get() = prefix?.let { File(it, "drive_c").isDirectory } == true } - /** A folder of saves, relative to the steamuser profile. */ data class SaveDir(val relPath: String, val files: Int, val bytes: Long) private const val USER = "steamuser" @@ -48,21 +36,14 @@ object GameSaves { fun savesDir(): File = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "DroidDeck/Saves") private fun backupsDir(): File = File(savesDir(), "backups") - /** [game] with its prefix and the saves found there; reads files, so not on the main thread. */ fun game(game: Library.SteamGame): Game = Game(game, game.protonPrefix, game.protonPrefix?.let { locate(it, game) } ?: emptyList()) - // ------------------------------------------------------------------ finding saves - /** Roots of the steamuser profile that hold most PC game saves, in the order they are trusted. */ private val SAVE_ROOTS = listOf("Documents/My Games", "Saved Games", "AppData/Roaming", "Documents", "AppData/Local", "AppData/LocalLow") private const val KEEP_THRESHOLD = 50 private fun profile(prefix: File) = File(prefix, "drive_c/users/$USER") - /** - * Folders under the save roots, one and two deep, whose names match the game's name or its folder's. - * Nested matches keep the outer folder only. - */ fun locate(prefix: File, game: Library.SteamGame): List<SaveDir> { val profile = profile(prefix) if (!profile.isDirectory) return emptyList() @@ -102,7 +83,6 @@ object GameSaves { return Ident(cleaned.filter { it.isLetterOrDigit() }, cleaned.split(Regex("[^a-z0-9]+")).filter { it.isNotEmpty() }.toSet()) } - /** exact 100, one contains the other 70, half the words shared 50, close spelling 40. */ private fun score(a: Ident, b: Ident): Int { if (a.norm.isEmpty() || b.norm.isEmpty()) return 0 if (a.norm == b.norm) return 100 @@ -123,15 +103,10 @@ object GameSaves { return prev[b.length] } - // ------------------------------------------------------------------ export private fun stamp() = SimpleDateFormat("yyyy-MM-dd_HHmm", Locale.US).format(Date()) private fun safe(name: String) = name.replace(Regex("[/\\\\:*?\"<>|]"), "_").trim().ifEmpty { "game" } - /** - * Zips the game's saves into [dir] as "<Game>_<date>.zip" in [layout]. The found save folders when - * there are any, else the whole steamuser profile minus Temp and crash dumps. Returns the zip. - */ fun export(context: Context, game: Game, layout: Layout, dir: File): Pair<File, Int> { val prefix = game.prefix ?: error(context.getString(R.string.saves_not_launched, game.game.name)) dir.mkdirs() @@ -160,13 +135,7 @@ object GameSaves { return count } - // ------------------------------------------------------------------ import - /** - * Unzips a GameHub or Winlator save zip into the game's drive_c, after zipping what its steamuser - * profile holds now into Download/DroidDeck/Saves/backups. Entries outside drive_c, launcher - * shortcuts and anything escaping drive_c are skipped. Returns (files written, backup or null). - */ fun import(context: Context, game: Game, zip: File): Pair<Int, File?> { val prefix = game.prefix ?: error(context.getString(R.string.saves_not_launched, game.game.name)) val driveC = File(prefix, "drive_c") @@ -201,7 +170,6 @@ object GameSaves { return written to backup.takeIf { it.exists() } } - /** Which layout a zip was written in, from its first user folder; null when it has none. */ fun layoutOf(zip: File): Layout? = runCatching { ZipInputStream(BufferedInputStream(zip.inputStream())).use { zis -> var e = zis.nextEntry @@ -223,11 +191,9 @@ object GameSaves { } }.getOrDefault(false) - /** An entry's path below drive_c, with any user folder but Public renamed to steamuser. */ private fun remap(raw: String): String? { val segs = driveCSegments(raw)?.takeIf { it.isNotEmpty() } ?: return null if (segs.any { it == ".." }) return null - // GameHub also packs its own launcher shortcuts; they are never saves. if (segs[0].equals("proton_shortcuts", true)) return null val last = segs.last().lowercase() if ((last.endsWith(".lnk") || last.endsWith(".desktop") || last.endsWith(".url")) && segs.any { it.equals("Desktop", true) }) return null diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt b/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt index df457d2e3..096f8e588 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt @@ -9,20 +9,9 @@ import com.droiddeck.launcher.core.FileUtils import com.droiddeck.launcher.frontend.AddedGames import java.io.File -/** - * Where a second Steam library can live on this device. - * - * An SD card is offered through the app's own folder on it ({@code Android/data/<pkg>/files/steam}): - * the one place on a card a targetSdk-28 app may write without a permission prompt, and a real - * filesystem path the session can bind. Any other folder comes from the File Manager's pick mode - * and is checked for writing before it is accepted. Both are FUSE-backed on Android and stream - * slowly (intro movies, big asset loads); internal stays the default and the dialog says so. - */ object GameStorage { - /** A library root: [label] as the menu lists it (a volume with its free space), [name] alone. */ class Option(val label: String, val path: String, val name: String = label) - /** Every removable volume, as its app folder, with the free space it has. */ fun options(context: Context): List<Option> { val sm = context.getSystemService(StorageManager::class.java) return context.getExternalFilesDirs(null).orEmpty().filterNotNull().mapNotNull { dir -> @@ -40,10 +29,6 @@ object GameStorage { "?" } - /** - * Makes [path] a library root the session can bind - the folder and its steamapps/ - and proves - * it writable. Returns why it cannot be used, or null when it can. - */ fun prepare(context: Context, path: String): String? { val root = File(path) val steamapps = File(root, "steamapps") @@ -56,7 +41,6 @@ object GameStorage { } } - /** The root the session binds, from the setting: automatic = the first card in the phone. */ fun effective(context: Context): Option? { val pref = SessionPrefs.gameStorage(context) return when { @@ -68,12 +52,6 @@ object GameStorage { class Library(val host: File, val guest: String, val label: String) - /** - * Games folders an earlier build registered as Steam libraries that Steam has installed games - * into (an appmanifest in their steamapps/): those stay libraries, so the games are not lost. - * Any other Games folder is no longer one; the session's library list leaves it out and the - * runtime's droiddeck-steam-library forgets it. - */ fun gamesFolderLibraries(context: Context): List<Library> { if (SessionPrefs.gameStorage(context).isNotEmpty()) return emptyList() val card = effective(context)?.let { canonical(File(it.path)) } @@ -90,13 +68,11 @@ object GameStorage { } } - /** Steam has installed a game into [root] as a library: a manifest in its steamapps/. */ internal fun hasInstalledGames(root: File): Boolean = File(root, "steamapps").listFiles { f -> f.isFile && f.name.startsWith("appmanifest_") && f.name.endsWith(".acf") }?.isNotEmpty() == true private fun canonical(file: File): String = runCatching { file.canonicalPath }.getOrDefault(file.absolutePath) - /** The label the client shows for a chosen folder: its last name, or the volume's. */ fun labelFor(context: Context, path: String): String = options(context).firstOrNull { it.path == path }?.name ?: File(path).name.ifEmpty { context.getString(R.string.gstore_folder) } diff --git a/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt b/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt index 8e0c7c8a0..7855c4959 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt @@ -5,18 +5,6 @@ import android.util.Log import com.droiddeck.launcher.wayland.WaylandCompositor import java.io.File -/** - * The GPU held at its top clock for a session, without root: KGSL's power-control property turned - * off on /dev/kgsl-3d0, the node the driver already opens (adrenotools' "turbo"). Left to itself the - * governor lets the GPU sit low between bursts, and a frame that lands on a low clock is a hitch; - * Bannerlator measured the clock going from 231 to 1000 MHz with this set. It costs power and heat, - * so it is opt-in, and the kernel's thermal limits still apply on top of it. - * - * The property belongs to the device, not to this process, and a killed app cannot unwind it: it - * is cleared when a session stops, and at the next app start when a marker file says this app set - * it and never cleared it - only then, so another app's own setting is not undone. - * (Bannerlator, PerfGpuTurbo.) - */ object GpuClockPin { private const val TAG = "GpuClockPin" private const val KGSL_NODE = "/dev/kgsl-3d0" @@ -28,7 +16,6 @@ object GpuClockPin { private val supported: Boolean by lazy { File(KGSL_NODE).exists() } - /** At a session's start: pins the clock when the user asked for it. The line is for the log. */ @Synchronized fun start(context: Context): String { if (!SessionPrefs.gpuClockPin(context)) return "gpu clock: governed" @@ -44,13 +31,11 @@ object GpuClockPin { } } - /** At a session's end. */ @Synchronized fun stop(context: Context) { if (pinned) clear(context) } - /** At the app's start: a process killed with the clock pinned left only the marker behind. */ @Synchronized fun clearLeftover(context: Context) { if (File(context.filesDir, MARKER).exists()) clear(context) diff --git a/app/src/main/java/com/droiddeck/launcher/session/GpuMemComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/GpuMemComponent.kt index efa6a7956..449c2bb7f 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GpuMemComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GpuMemComponent.kt @@ -4,32 +4,11 @@ import android.util.Log import com.droiddeck.launcher.core.SessionPart import java.io.File -/** - * The GPU memory in use, for the performance overlay's VRAM and PVRAM lines. - * - * Valve's mangoapp reads it from the kernel's gpu_mem_total tracepoint, which no Android app may - * reach, so both lines read 0. Our libtracefs for mangoapp (tools/mangoapp/libtracefs-shim.c) hands - * it what this writes to [file] - `/run/droiddeck-hud/gpu-mem` in the guest - every few seconds, in - * bytes, from the best of what KGSL lets the app read: - * - * - `/sys/class/kgsl/kgsl/page_alloc`, every GPU page on the device - what the tracepoint's total - * counts, where the policy allows it; - * - else KGSL's own accounting of each of the session's processes - * (`/sys/class/kgsl/kgsl/proc/<pid>/gpumem_mapped` and `gpumem_unmapped`); - * - else the size of the KGSL mappings in each of the session's processes' `maps` - only the - * buffers the CPU has mapped, so less than the GPU holds (about 40 % of KGSL's own count in a - * game on an AYN Thor), but always readable: every process the app can see in `/proc` is one of - * its own. `maps` rather than `smaps`, which gives the same sizes but walks page tables to do it: - * in a game, 180,000 lines and most of a second of CPU a pass. - * - * Sampled only while the overlay shows (OverlayShown). - */ class GpuMemComponent(private val file: File, private val root: File) : SessionPart() { @Volatile private var running = false private var thread: Thread? = null private var source = "" - /** True when there is a source to write from. */ fun prepare(): Boolean { val bytes = sample() ?: return false write(bytes) @@ -76,11 +55,9 @@ class GpuMemComponent(private val file: File, private val root: File) : SessionP return null } - /** The processes this app may see in /proc: Android hides every other uid's. */ private fun sessionPids(): List<String> = File("/proc").list()?.filter { it.isNotEmpty() && it.all(Char::isDigit) }.orEmpty() - /** Size of a process's mappings of the KGSL device, in kB; null where its maps cannot be read. */ private fun kgslMappedKb(pid: String): Long? = runCatching { var bytes = 0L File("/proc/$pid/maps").forEachLine { line -> @@ -95,7 +72,6 @@ class GpuMemComponent(private val file: File, private val root: File) : SessionP bytes / 1024 }.getOrNull() - /** Whole, renamed into place: the reader opens either the last value or this one. */ private fun write(bytes: Long) { try { file.parentFile?.mkdirs() @@ -109,7 +85,6 @@ class GpuMemComponent(private val file: File, private val root: File) : SessionP companion object { private const val TAG = "SessionService" private const val KGSL_ROOT = "/sys/class/kgsl/kgsl" - /** GPU memory moves slowly; a pass over every process's maps is ~40 ms of CPU in a game. */ private const val PERIOD_MS = 5_000L private fun readLong(path: String): Long? = diff --git a/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt index 205240fa2..fb6efe09f 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt @@ -6,34 +6,15 @@ import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File import java.io.RandomAccessFile -/** - * The Adreno's stats as mangoapp (Deck mode's performance overlay) reads them. - * - * MangoHud takes an Adreno's load, clock and temperature from `gpu_busy_percentage`, `clock_mhz` - * and `temp` in `/sys/class/kgsl/kgsl-3d0`, and ends the process on one of them it may not read - - * not on one that is missing, which it skips. An enforcing SELinux policy (every retail phone) - * refuses apps some or all of KGSL's sysfs, each vendor differently, so mangoapp died on start and - * the session ran with no overlay. Where any of the three is refused, a directory of our own is - * bound there instead. A value with no - * readable source is left out, and the overlay shows no line for it. Where all three are - * readable, nothing is done. - * - * Valve's build also reads load and clock where a mainline msm kernel has them (debugfs - * `perf_now`, the GPU's devfreq `cur_freq` in Hz) - the paths LinuxRuntime.bindAdrenoStats points - * at KGSL's own files when it may - so the same values are fed there too, in the same formats - * ([binds]). - */ class GpuStatsComponent(val dir: File) : SessionPart() { @Volatile private var running = false private var thread: Thread? = null private val feeds = ArrayList<Feed>() - /** One of MangoHud's files and where its value comes from. */ private class Feed( val file: File, val source: String, val read: () -> Long?, val format: String, val percent: Boolean = false, ) - /** Where Valve's msm reader looks, and the feed for it, as host:guest binds. */ fun binds(): List<String> { val list = mutableListOf(dir.path + ":" + KGSL) if (File(dir, "perf_now").exists()) list.add(File(dir, "perf_now").path + ":" + MSM_LOAD) @@ -41,7 +22,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { return list } - /** Writes the directory; true when it should be bound over [KGSL]. */ fun prepare(): Boolean { if (!File(KGSL).exists() || MANGOHUD_FILES.none { File(KGSL, it).let { f -> f.exists() && !f.canRead() } }) return false return try { @@ -50,8 +30,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { feeds.clear() loadFeed()?.let { load -> feeds.add(load) - // KGSL's gpu_busy_percentage reads "37 %"; padded after, so a shorter value - // written over a longer one leaves no digits behind. feeds.add(Feed(File(dir, "perf_now"), load.source, load.read, "%-6s\n", percent = true)) } pick("clock_mhz", clockSources(), "%5d\n") { mhz(it) }?.let { clock -> @@ -88,7 +66,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { thread = null } - /** Load: a file that states it in percent, else KGSL's busy and total of the last sample. */ private fun loadFeed(): Feed? { pick("gpu_busy_percentage", LOAD_SOURCES, "%3d %%\n") { it.coerceIn(0, 100) }?.let { return it } val gpubusy = "$KGSL/gpubusy" @@ -102,7 +79,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { return if (read() != null) Feed(File(dir, "gpu_busy_percentage"), gpubusy, read, "%3d %%\n") else null } - /** The first of [sources] that reads as a number, its value put in MangoHud's unit by [unit]. */ private fun pick(name: String, sources: List<String>, format: String, unit: (Long) -> Long?): Feed? { for (source in sources) { val read = { firstNumber(source)?.let(unit) } @@ -111,7 +87,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { return null } - /** In place and at one length, so a reader that keeps the file open sees each value whole. */ private fun write(feed: Feed, value: Long) { try { val text = if (feed.percent) String.format(feed.format, "$value %") else String.format(feed.format, value) @@ -123,17 +98,14 @@ class GpuStatsComponent(val dir: File) : SessionPart() { companion object { private const val TAG = "SessionService" const val KGSL = "/sys/class/kgsl/kgsl-3d0" - /** Must match LinuxRuntime.bindAdrenoStats. */ const val MSM_LOAD = "/sys/kernel/debug/dri/0/perf_now" const val MSM_CLOCK = "/sys/devices/platform/soc@0/3d00000.gpu/devfreq/3d00000.gpu/cur_freq" private const val PERIOD_MS = 500L private val MANGOHUD_FILES = listOf("gpu_busy_percentage", "clock_mhz", "temp") - /** Qualcomm's own GPU summary, beside KGSL in vendor kernels, under the plain sysfs label. */ private const val QCOM_GPU = "/sys/kernel/gpu" private val LOAD_SOURCES = listOf( "$KGSL/gpu_busy_percentage", "$KGSL/devfreq/gpu_load", "$KGSL/gpuload", "$QCOM_GPU/gpu_busy", ) - /** The GPU's devfreq node is named by its register address, which differs between Snapdragons. */ private val DEVFREQ_NODES = listOf( "kgsl-3d0", "3d00000.qcom,kgsl-3d0", "2c00000.qcom,kgsl-3d0", "5000000.qcom,kgsl-3d0", "5900000.qcom,kgsl-3d0", ) @@ -152,11 +124,9 @@ class GpuStatsComponent(val dir: File) : SessionPart() { private fun tempSources(): List<String> = listOfNotNull("$KGSL/temp", "$KGSL/devfreq/temp", "$QCOM_GPU/temp", LinuxRuntime.gpuTempSource()).distinct() - /** The first integer in the file ("550", "37 %", "550000000"), or null. */ private fun firstNumber(path: String): Long? = runCatching { Regex("-?\\d+").find(File(path).readText())?.value?.toLong() }.getOrNull() - /** Hz, kHz or MHz to MHz, as the files differ; a stopped clock reads 0. */ private fun mhz(raw: Long): Long? = when { raw < 0 -> null raw > 10_000_000 -> raw / 1_000_000 @@ -164,7 +134,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { else -> raw } - /** °C or m°C to MangoHud's m°C; nothing a GPU reads at is below 1 °C or above 1000. */ private fun milliCelsius(raw: Long): Long? = when { raw <= 0 -> null raw < 1000 -> raw * 1000 diff --git a/app/src/main/java/com/droiddeck/launcher/session/HwmonComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/HwmonComponent.kt index fb0048933..2cd84747d 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/HwmonComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/HwmonComponent.kt @@ -8,25 +8,6 @@ import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File import java.io.RandomAccessFile -/** - * The session's `/sys/class/hwmon`, as mangoapp (Deck mode's performance overlay) looks there. - * - * MangoHud finds a CPU's temperature by an hwmon sensor named `cpuN_thermal`, and an Adreno's - * temperature, fan and power by one whose name has `gpuss` in it - a mainline Arm kernel's names. - * An Android kernel has neither (its hwmon holds the PMIC's sensors, or nothing), and where the - * app may not list hwmon at all the overlay logged an error twice a second. So the overlay showed - * no CPU temperature and no fan. This is two sensors named so: - * - * - `hwmon0` (`cpu0_thermal`): `temp1_input` linked to a CPU thermal zone the app can read; - * - `hwmon1` (`gpuss0_thermal`): `temp1_input` linked to the GPU's zone, and `fan1_input` with - * the device's fan speed where it has a fan the app can read - an AYN handheld's tachometer - * (`/sys/class/gpio5_pwm2/speed`), or a `fan*_input` in the device's own hwmon; - * - `hwmon2` (`slg4ax46073v`): the same fan, for Valve's own Qualcomm reader (msm.cpp), which - * looks for its fan controller by that name and halves what it reads - that part reports twice - * the real speed - so this one holds twice it. - * - * Nothing in a phone's own hwmon is read by the overlay, so standing in for it loses nothing. - */ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { @Volatile private var running = false private var thread: Thread? = null @@ -36,7 +17,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { private var lastRpm = 0L private var lastRpmAt = 0L - /** Writes the sensors; true when the directory should be bound over `/sys/class/hwmon`. */ fun prepare(): Boolean = try { dir.mkdirs() clear(dir) @@ -65,7 +45,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { thread = Thread({ while (running) { try { Thread.sleep(FAN_PERIOD_MS) } catch (e: InterruptedException) { break } - // A tachometer read busy-waits 100 ms in the kernel: only while the overlay shows. if (running && OverlayShown.check(root)) writeFan(sampleFan()) } }, "hwmon-fan").apply { isDaemon = true; start() } @@ -77,11 +56,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { thread = null } - /** - * The fan's speed in RPM. An AYN tachometer counts pulses over a short window per read and - * often catches none, reading 0 while the fan turns; a reading is held until a newer one - * comes or [FAN_HOLD_MS] passes with only zeros, when the fan is taken to have stopped. - */ private fun sampleFan(): Long { val now = SystemClock.elapsedRealtime() val rpm = fanSource?.let { readNumber(it) } ?: 0L @@ -94,7 +68,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { return lastRpm } - /** In place and at one length, so a reader that keeps the file open sees each value whole. */ private fun writeFan(rpm: Long) { for ((file, value) in listOf(fanFile to rpm, valveFanFile to rpm * 2)) { try { @@ -112,7 +85,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { Os.symlink(target, link.path) } - /** Only files and links are made here; deleting a link must not follow it into sysfs. */ private fun clear(root: File) { root.listFiles()?.forEach { f -> if (!java.nio.file.Files.isSymbolicLink(f.toPath()) && f.isDirectory) clear(f) @@ -122,7 +94,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { companion object { private const val TAG = "SessionService" - /** The AYN tachometer reads 0 if read again within a couple of seconds. */ private const val FAN_PERIOD_MS = 3_000L private const val FAN_HOLD_MS = 10_000L diff --git a/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt b/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt index 26799d003..a23f23beb 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt @@ -10,45 +10,32 @@ import java.io.File import java.io.RandomAccessFile import java.nio.charset.StandardCharsets -/** - * The loading overlay's state: what to say, read out of the session log, and a clock so a user - * can see that time is passing even when the log is quiet. The runtime script marks its - * milestones with "== STEP"; the client's own bootstrap does not, but it does print its download - * progress, and lifting that out is the difference between "starting the Steam client" for three - * minutes and a percentage that moves. - */ class LoadingState(context: Context, private val steam: Boolean = true) { var visible by mutableStateOf(true) var step by mutableStateOf(context.getString(com.droiddeck.launcher.R.string.loadstate_starting)) private set - /** What [step] is about, so the screen picks its checklist stage without reading the (translated) words. */ var topic by mutableStateOf(Topic.SESSION) private set - /** [step] is a download or install line worth showing as it is, not one of the script's own notes. */ var readable by mutableStateOf(false) private set var percent by mutableIntStateOf(-1) var elapsed by mutableStateOf("") var hint by mutableStateOf("") var ended by mutableStateOf(false) - /** The technical side of an ended session (exit status, log path), under the advice in [step]. */ var endedDetail by mutableStateOf<String?>(null) - // The desktop and the emulators get their own: the Steam ones talk about Steam. private val hints = context.resources.getStringArray( if (steam) com.droiddeck.launcher.R.array.loading_hints else com.droiddeck.launcher.R.array.loading_hints_desktop, ) private val res = context.resources private val startedAt = SystemClock.elapsedRealtime() - /** Once a second: the clock and the hint. */ fun tick() { val seconds = (SystemClock.elapsedRealtime() - startedAt) / 1000 elapsed = res.getString(com.droiddeck.launcher.R.string.loadstate_elapsed, seconds / 60, seconds % 60) hint = hints[((seconds / 8) % hints.size).toInt()] } - /** A line the app itself writes (an install's progress, the session starting). */ fun say(line: String, topic: Topic, readable: Boolean) { step = line this.topic = topic @@ -62,7 +49,6 @@ class LoadingState(context: Context, private val steam: Boolean = true) { endedDetail = detail } - /** Re-reads the end of the log and updates the line and the bar. */ fun update(context: Context, log: File?) { val state = read(context, log) ?: return step = state.line @@ -73,7 +59,6 @@ class LoadingState(context: Context, private val steam: Boolean = true) { private class Line(val line: String, val percent: Int, val topic: Topic, val readable: Boolean) - /** Only the tail is read: the client alone writes megabytes an hour. */ private fun read(context: Context, log: File?): Line? { if (log == null || !log.isFile) return null val text = try { @@ -132,14 +117,9 @@ class LoadingState(context: Context, private val steam: Boolean = true) { } } - /** The checklist stages a loading line can move the screen to. */ enum class Topic { RUNTIME, SESSION, DESKTOP, STEAM, STEAM_STARTING, OTHER } companion object { - /** - * A session script "== STEP" line's topic in a Steam ([steam]) or desktop session; those - * lines are English whatever the app's language. - */ internal fun topicOf(scriptStep: String, steam: Boolean): Topic { val t = scriptStep.lowercase() return when { @@ -152,7 +132,6 @@ class LoadingState(context: Context, private val steam: Boolean = true) { } } - /** The script's install lines are worth reading as they are; its other notes are not. */ internal fun readableScriptStep(scriptStep: String): Boolean { val t = scriptStep.lowercase() return t.startsWith("download") || t.startsWith("checking the linux") || t.startsWith("unpacking") || t.startsWith("installing") diff --git a/app/src/main/java/com/droiddeck/launcher/session/LogMigration.kt b/app/src/main/java/com/droiddeck/launcher/session/LogMigration.kt index c2b34f816..57964944d 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/LogMigration.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/LogMigration.kt @@ -6,13 +6,6 @@ import com.droiddeck.launcher.core.FileUtils import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File -/** - * Once, on the first start of a build with private logs: the session folders, tools/ and the loose - * tool logs that earlier builds left in `Download/DroidDeck/` move into `files/logs/`. Best effort: - * an item that cannot be moved stays where it is. Moved session folders lose their old scrub markers - * ([SessionArtifacts.unmarkMoved]), so [SessionArtifacts.scrubOlder] scrubs them under the current rules. Nothing else in that folder is touched - game - * save backups (Saves/) stay public - and after this pass the app never reads it again. - */ object LogMigration { private const val TAG = "LogMigration" private const val MARKER = ".moved-from-downloads" @@ -24,7 +17,6 @@ object LogMigration { if (marker.exists()) return val legacy = LinuxRuntime.legacyLogDir() val items = if (legacy.isDirectory) legacy.listFiles() else emptyArray() - // Unreadable (no storage access yet): try again next start rather than give up on them. if (items == null) { Log.w(TAG, "$legacy cannot be listed; logs there stay until it can"); return } var moved = 0 val movedFolders = ArrayList<File>() @@ -40,15 +32,11 @@ object LogMigration { if (SessionPaths.isSessionFolder(dest)) movedFolders.add(dest) } else Log.w(TAG, "could not move ${f.name}; left in place") } - // Earlier builds scrubbed these under older rules (or not at all) and marked them done: - // without their markers, the pass over older folders that follows at the same start puts - // them through the current redactor. SessionArtifacts.unmarkMoved(movedFolders) try { marker.writeText("moved $moved item(s)\n") } catch (e: Exception) { Log.w(TAG, "could not mark the move: ${e.message}") } if (moved > 0) Log.i(TAG, "moved $moved log item(s) from $legacy into $target") } - /** A rename where it works (same volume), else a copy then the source's removal; false = left as it was. */ private fun move(src: File, dest: File): Boolean { if (src.renameTo(dest)) return true return try { diff --git a/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt b/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt index 42a5a2866..c276da390 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt @@ -5,19 +5,6 @@ import android.util.Log import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File -/** - * Starting the Steam client signed out of Valve's servers. - * - * The client can run without a connection - installed games launch, Proton and the frame-gen are - * all local - but only if it has credentials from an earlier successful sign-in. Two keys in - * loginusers.vdf decide what it does when it cannot reach a CM: WantsOfflineMode makes it go - * offline on its own, SkipOfflineModeWarning stops it asking first. Both are read once, while the - * client starts, which is why this belongs to the main screen and not to the in-session drawer: - * changing it during a session would write the file and change nothing until the next launch. - * - * The client rewrites this file when it exits, so the request is kept as our own preference and - * written into the file again at every session start. - */ object OfflineMode { private const val TAG = "OfflineMode" private const val KEY = "steam_offline" @@ -30,7 +17,6 @@ object OfflineMode { private fun loginUsers(root: File) = File(root, "root/.local/share/Steam/config/loginusers.vdf") - /** The account the client could sign in as offline: its name, "" when it recorded none, null when there is none. */ fun account(context: Context): String? { val file = loginUsers(LinuxRuntime.rootDir(context)) val text = try { @@ -49,12 +35,6 @@ object OfflineMode { apply(context, LinuxRuntime.rootDir(context)) } - /** - * Write the request into loginusers.vdf. Every account block gets the same answer: the client - * signs in as one of them and we have no say in which. Keys the file does not carry are added - * beside the account's name; a file without an account block is left alone, since there is - * nothing to be offline as. - */ fun apply(context: Context, root: File) { val file = loginUsers(root) if (!file.isFile) return diff --git a/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt b/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt index 9a17a97ae..35ac169a7 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt @@ -3,26 +3,10 @@ package com.droiddeck.launcher.session import android.util.Log import java.io.File -/** - * Kills every process of this app's uid other than the app itself. - * - * A session is a tree of processes under proot, and proot dies with the app only when the app's - * own teardown runs. When the app is killed or crashes instead, the tree stays: proot holding the - * rootfs, gamescope holding the GPU, Steam holding its lock file - and the next session never - * comes up, or comes up beside a client that is already running. A user on a Thor Pro had to - * force-stop the app before every launch. Everything of ours that should be alive between - * sessions is inside the app process, so at session start anything else under our uid is a - * leftover, and this sweeps it. Each pid is checked by uid at kill time, never by name. - * - * The one exception is work the app started outside any session and is still waiting on - a - * Flatpak install from the store - registered with [keep]: that process and everything under it - * are left to finish. - */ object OrphanReaper { private const val TAG = "OrphanReaper" private val kept = java.util.concurrent.ConcurrentHashMap.newKeySet<Int>() - /** Spares [pid] and its descendants until [release]. */ fun keep(pid: Int) { if (pid > 0) kept.add(pid) } fun release(pid: Int) { kept.remove(pid) } @@ -49,14 +33,12 @@ object OrphanReaper { return killed } - /** [roots] and every process below them, by each process's parent in /proc/<pid>/stat. */ private fun descendants(roots: Set<Int>): Set<Int> { if (roots.isEmpty()) return emptySet() val parent = HashMap<Int, Int>() File("/proc").listFiles()?.forEach { entry -> val pid = entry.name.toIntOrNull() ?: return@forEach val stat = try { File(entry, "stat").readText() } catch (e: Exception) { return@forEach } - // "pid (comm) state ppid ...": comm may hold spaces and parentheses; the last ')' ends it. stat.substringAfterLast(") ").split(' ').getOrNull(1)?.toIntOrNull()?.let { parent[pid] = it } } val out = HashSet(roots) diff --git a/app/src/main/java/com/droiddeck/launcher/session/OverlayShown.kt b/app/src/main/java/com/droiddeck/launcher/session/OverlayShown.kt index 028091e44..22f067d54 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/OverlayShown.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/OverlayShown.kt @@ -2,12 +2,6 @@ package com.droiddeck.launcher.session import java.io.File -/** - * Whether Deck mode's performance overlay is on screen. The client sets its level by rewriting - * mangoapp's config (droiddeck-session makes it, `/tmp/mangohud.*`), and `no_display` is the - * level that hides it. The feeds whose reads cost something - the fan's tachometer busy-waits in - * the kernel, GPU memory walks every process's mappings - only run while it shows. - */ object OverlayShown { fun check(root: File): Boolean { val config = File(root, "tmp").listFiles { f -> f.name.startsWith("mangohud.") } diff --git a/app/src/main/java/com/droiddeck/launcher/session/PerfHud.kt b/app/src/main/java/com/droiddeck/launcher/session/PerfHud.kt index b3c3fd6ea..042396b9f 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/PerfHud.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/PerfHud.kt @@ -16,17 +16,7 @@ import java.io.File import java.util.Locale import java.util.concurrent.atomic.AtomicInteger -/** - * The numbers behind the top-right line: the game's own frame rate and, when frame generation - * is on, what the screen is actually being shown - "60 → 118 fps" is the proof that the engine - * is doing something, and "FG starting" or a reason is the proof that it is not. - * - * The base rate is the game window's own frames, counted here from the compositor's per-frame - * callback with or without an engine - what Steam's overlay counts too. The presented rate comes - * from the engine's telemetry while it generates. - */ class PerfHud(context: Context) { - /** Empty when the HUD is off or not yet started. */ var text by mutableStateOf("") private set @@ -36,19 +26,15 @@ class PerfHud(context: Context) { private var running = false private val handler = Handler(Looper.getMainLooper()) - /** The session's own switch (drawer), with the Downloads file as a device-side override. */ private val enabled: Boolean get() = SessionPrefs.hudEnabled(context) && !File(Environment.getExternalStorageDirectory(), "Download/droiddeck-no-hud").exists() - /** Told, on the main thread, when the window presenting frames changes. */ var onPresentingWindowChanged: (() -> Unit)? = null private var lastWindow: String? = null private val listener = object : WaylandCompositor.GameListener { override fun onGameSurface(window: String?, gpuName: String?) { - // gamescope forwards a fullscreen game's own buffers straight through and its composited - // output when the Steam menu is up; each is a different window here. Compositor thread. if (window != null && window != lastWindow) { lastWindow = window handler.post { onPresentingWindowChanged?.invoke() } @@ -68,7 +54,6 @@ class PerfHud(context: Context) { } } - /** Re-reads the switch: starts or stops to match it. */ fun refresh() { if (enabled) start() else stop() } diff --git a/app/src/main/java/com/droiddeck/launcher/session/PerfMode.kt b/app/src/main/java/com/droiddeck/launcher/session/PerfMode.kt index 4a77141c3..f197800a4 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/PerfMode.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/PerfMode.kt @@ -8,33 +8,12 @@ import android.os.Build import android.provider.Settings import android.util.Log -/** - * What a session tells Android about itself so the SoC is run for a game, not for an app. - * - * The manifest already makes the app a game (appCategory + game_mode_config); this is the - * per-window half, applied when the session activity is created: - * - * - NOT sustained performance mode: on Pixel and Qualcomm power HALs it caps the CPU and GPU at - * a level the device can hold indefinitely rather than holding a floor, which cost games and - * Big Picture their peak clocks. WinNative never asks for it; the thermal budget is left to - * the game; - * - the panel's fastest mode at its current size (Android 6+), up to the user's peak refresh rate - * ([peakRefreshCap]): a 120 Hz phone is switched to 120 Hz for the menu, not only when a game - * votes for it. With a frame cap, the fastest mode - * the cap divides evenly, so every capped frame is shown for the same number of refreshes; - * - GameManager's game state (Android 13+): "in gameplay", so an OEM framework that releases - * its boost on loading screens or menus keeps it up. - * - * Every step is version-guarded and skipped silently below its API; the returned line is what - * the session log shows, which is the first thing to read on a device that feels slow. - */ object PerfMode { private const val TAG = "PerfMode" fun apply(a: Activity, fpsLimit: Int = 0): String { val parts = ArrayList<String>(2) - // The fastest display mode at the panel's current size. parts += try { val display = if (Build.VERSION.SDK_INT >= 30) a.display else @Suppress("DEPRECATION") a.windowManager.defaultDisplay if (display == null || Build.VERSION.SDK_INT < 23) "display mode unchanged" else { @@ -54,7 +33,6 @@ object PerfMode { } } catch (t: Throwable) { Log.w(TAG, "display mode", t); "display mode failed" } - // GameManager: what mode the OS put us in, and that we are playing. parts += if (Build.VERSION.SDK_INT >= 31) { val gm = a.getSystemService(GameManager::class.java) if (gm == null) "no GameManager" else { @@ -74,13 +52,6 @@ object PerfMode { return parts.joinToString(" · ") } - /** - * The highest rate Android will run the panel at: the user's "peak refresh rate" (Smooth display, - * or the AYN/Odin quick-settings tile). Above it a mode can be requested and is even reported back - * as active, while SurfaceFlinger keeps composing at the cap. The Thor set to 60 Hz ran 120 Hz - * sessions at 60: gamescope and Steam were told 120, every game topped out at 60, and the QAM's - * Frame Limit read "60 FPS (120 Hz)" and changed nothing. Unset (most phones) is no cap. - */ fun peakRefreshCap(context: Context): Float { val peak = try { Settings.System.getFloat(context.contentResolver, "peak_refresh_rate") @@ -88,7 +59,6 @@ object PerfMode { return if (peak >= 1f) peak else Float.POSITIVE_INFINITY } - /** A panel rate that shows every capped frame for a whole number of refreshes (no cap: any). */ fun cadenceFits(hz: Float, fpsLimit: Int): Boolean { if (fpsLimit <= 0) return true val ratio = hz / fpsLimit diff --git a/app/src/main/java/com/droiddeck/launcher/session/PipPolicy.kt b/app/src/main/java/com/droiddeck/launcher/session/PipPolicy.kt index 5626e35e2..d2d4d49bd 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/PipPolicy.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/PipPolicy.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.session -/** Platform-independent eligibility; a service existing alone does not mean it has a picture. */ object PipPolicy { fun eligible(running: Boolean, frameSeen: Boolean, phase: SessionPhase, suspended: Boolean, automatic: Boolean): Boolean = running && frameSeen && phase in setOf(SessionPhase.READY, SessionPhase.SUSPENDED) && diff --git a/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt b/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt index c51cf01f4..8f51fee77 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt @@ -12,12 +12,10 @@ import java.io.File import org.json.JSONArray import org.json.JSONObject -/** Immediate download and installation of the optional ARM64 Proton builds. */ object ProtonExtras { private const val TAG = "ProtonExtras" private const val NEED_BYTES = 4L * 1024 * 1024 * 1024 private const val RELEASES = "https://api.github.com/repos/%s/releases?per_page=15" - /** How long a release check stands before the next one goes to GitHub. */ const val CHECK_EVERY_MS = 6L * 60 * 60 * 1000 @Volatile var installInProgress = false @@ -25,7 +23,6 @@ object ProtonExtras { class Tool(val id: String, val name: String, val prefix: String, val repo: String, val assetPattern: Regex) - /** [sha256] from GitHub's asset digest; [sha512] the URL of a checksum file published beside it. */ internal data class Asset(val tag: String, val name: String, val url: String, val sha512: String?, val size: Long, val sha256: String? = null) @@ -38,15 +35,11 @@ object ProtonExtras { private fun home(context: Context) = File(LinuxRuntime.rootDir(context), "root") private fun requests(context: Context) = File(home(context), ".bl-proton-extra") private fun toolsDir(context: Context) = File(home(context), ".local/share/Steam/compatibilitytools.d") - /** The release tag each build was installed from, by directory name. */ private fun releasesFile(context: Context) = File(context.filesDir, "proton-extras.json") - /** The newest release seen per source, and when it was looked up. */ private fun latestFile(context: Context) = File(context.filesDir, "proton-latest.json") - /** The installed build's directory name (e.g. GE-Proton11-7), the newest when there are several, or null. */ fun installed(context: Context, tool: Tool): String? = installedIn(toolsDir(context), tool).lastOrNull() - /** Every installed build's directory name, oldest first. */ fun installedAll(context: Context, tool: Tool): List<String> = installedIn(toolsDir(context), tool) internal fun installedIn(dir: File, tool: Tool): List<String> = @@ -56,15 +49,12 @@ object ProtonExtras { ?.sortedWith { a, b -> compareVersions(a, b) } ?: emptyList() - // Architecture names carry digits (aarch64, x86_64) that are not part of a version. private val ARCH = Regex("(?i)aarch64|arm64|x86_64|x86-64|amd64") private val NUMBER = Regex("\\d+") - /** The numbers in a release tag or build folder, in order: GE-Proton11-10 is 11, 10. */ internal fun versionKey(name: String): List<Long> = NUMBER.findAll(ARCH.replace(name, " ")).mapNotNull { it.value.toLongOrNull() }.toList() - /** Orders by those numbers, so 11-10 is after 11-9; the name itself only breaks a tie. */ internal fun compareVersions(a: String, b: String): Int { val x = versionKey(a) val y = versionKey(b) @@ -72,7 +62,6 @@ object ProtonExtras { return if (x.size != y.size) x.size.compareTo(y.size) else a.compareTo(b) } - /** [latest] when it is newer than every installed release, else null. Nothing installed is not an update. */ internal fun updateFor(installed: List<String>, latest: String?): String? { if (latest == null || installed.isEmpty()) return null val newest = installed.maxWithOrNull { a, b -> compareVersions(a, b) } ?: return null @@ -82,28 +71,20 @@ object ProtonExtras { return if (x.size > y.size) latest else null } - /** The release each installed build came from: the tag recorded at install, else its folder name. */ fun installedTags(context: Context, tool: Tool): List<String> { val recorded = readJson(releasesFile(context)) return installedAll(context, tool).map { recorded.optString(it).ifEmpty { it } } } - /** A newer release than the installed one, from the last check (no network), or null. */ fun update(context: Context, tool: Tool): String? = updateFor(installedTags(context, tool), cachedLatest(context, tool)) fun cachedLatest(context: Context, tool: Tool): String? = readJson(latestFile(context)).optJSONObject(tool.id)?.optString("tag")?.ifEmpty { null } - /** Its own lock, not the install's: a check must not wait behind a download. */ private val checkLock = Any() - /** A failed look-up (offline) is not repeated on every resume, only after this long. */ private const val RETRY_MS = 15L * 60 * 1000 private var lastAttempt = 0L - /** - * Looks up each source's newest release unless it was looked up within [CHECK_EVERY_MS] (or - * [force]). Offline or rate limited, the last answer stands. True when anything was asked. - */ fun checkLatest(context: Context, force: Boolean = false, now: Long = System.currentTimeMillis()): Boolean { synchronized(checkLock) { val file = latestFile(context) @@ -143,7 +124,6 @@ object ProtonExtras { private fun readJson(file: File): JSONObject = runCatching { JSONObject(FileUtils.readString(file) ?: "{}") }.getOrDefault(JSONObject()) - /** A request left by an earlier app version, still consumed by the session shim. */ fun queued(context: Context, tool: Tool): Boolean = requestLines(context).any { it.trim() == tool.id || it.trim().startsWith(tool.id + " ") } @@ -154,18 +134,15 @@ object ProtonExtras { else file.writeText(lines.joinToString("\n") + "\n") } - /** Deletes one installed build ([dir], else the newest); Steam discovers its absence on its next start. */ fun remove(context: Context, tool: Tool, dir: String? = null) { val name = if (dir != null) dir.takeIf { it in installedAll(context, tool) } ?: return else installed(context, tool) ?: return File(toolsDir(context), name).deleteRecursively() } - /** Deletes every build but the newest. */ fun removeOlder(context: Context, tool: Tool) { installedAll(context, tool).dropLast(1).forEach { File(toolsDir(context), it).deleteRecursively() } } - /** Downloads, verifies and registers the latest build without waiting for another session. */ @Synchronized fun install(context: Context, tool: Tool, onProgress: (String, Int) -> Unit): String? { if (installInProgress) return context.getString(R.string.pextra_busy) @@ -211,8 +188,6 @@ object ProtonExtras { return context.getString(R.string.pextra_incomplete) } - // One checksum has to be found and has to match: GitHub's sha256 digest, else the - // release's own sha512 file. A download neither vouches for is not installed. onProgress(context.getString(R.string.pextra_verifying), -1) val verified = when { asset.sha256 != null -> asset.sha256.equals(Hashes.sha256(archive), ignoreCase = true) @@ -257,7 +232,6 @@ object ProtonExtras { child.inputStream.bufferedReader().useLines { lines -> lines.forEach { line -> Log.i(TAG, line) - // "droiddeck-proton-extra: installed <folder>", the folder as the tarball names it. line.substringAfter("droiddeck-proton-extra: installed ", "").takeIf { it.isNotEmpty() }?.let { placed = it } if (line.contains("unpacking", ignoreCase = true) || line.contains("adopted", ignoreCase = true)) { onProgress(installing, -1) @@ -296,7 +270,6 @@ object ProtonExtras { } } - /** The newest full release in GitHub's list (newest first) that carries [tool]'s ARM64 archive. */ internal fun pickAsset(tool: Tool, array: JSONArray): Asset? { for (i in 0 until array.length()) { val release = array.getJSONObject(i) diff --git a/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt b/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt index 089f0631c..f12550419 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt @@ -6,7 +6,6 @@ import android.util.Log import com.droiddeck.launcher.BuildConfig import java.io.File -/** Device-detected defaults and launch evidence for experimental Quest compatibility. */ object QuestExperimental { private const val TAG = "QuestExperimental" const val DRIVER = "turnip25.1.0-quest1" @@ -30,14 +29,10 @@ object QuestExperimental { val prefs = context.getSharedPreferences("session", Context.MODE_PRIVATE) val defaultsKey = if (isQuest2()) "quest2DefaultsV1" else "questDefaultsV1" if (!prefs.getBoolean(defaultsKey, false)) { - // Auto would download an unwrapped display driver and lose the compatibility path. - // Apply once so deliberate changes made during headset testing remain possible. prefs.edit() .putString("gpuDriverMode", SessionPrefs.GPU_DRIVERS_MANUAL) .putString("androidDriver", if (isQuest2()) DRIVER_A6XX else DRIVER) .putString("linuxDriver", "") - // The existing HDR option uses SurfaceControl presentation. This is a - // corruption workaround from upstream issue #245, not a claim of HDR accuracy. .putBoolean("hdr.steam", true) .putBoolean(defaultsKey, true) .apply() diff --git a/app/src/main/java/com/droiddeck/launcher/session/RivalClients.kt b/app/src/main/java/com/droiddeck/launcher/session/RivalClients.kt index 75973e467..0d7bd22cc 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/RivalClients.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/RivalClients.kt @@ -4,25 +4,11 @@ import android.app.ActivityManager import android.content.Context import android.util.Log -/** - * Other apps on the device that sign into the user's Steam account with their own client. Steam - * allows one session per account: the moment one of them signs in, ours is signed out - which - * shows up as "Session Replaced" in the client's log 2-3 seconds after every login, and looks - * like a bug in this app. - * - * Installed is the most a normal app can know (since Android 7 `getRunningAppProcesses()` returns - * only the caller's own), so the list is checked for presence and then asked to stop. - * `killBackgroundProcesses` is a normal permission: it ends an app's background processes and - * leaves anything in the foreground alone - and background is exactly the case that keeps - * happening, because GameHub declares boot receivers and is running from the moment the phone - * starts without ever having been opened. Telling someone to close an app they never opened is - * not much help. Ported from Bannerlator's Linux session (GPL-3.0). - */ object RivalClients { private const val TAG = "RivalClients" private val PACKAGES = arrayOf( - "com.xiaoji.egggame", // GameHub + "com.xiaoji.egggame", ) fun installed(context: Context): List<String> = @@ -32,10 +18,6 @@ object RivalClients { fun name(pkg: String): String = if (pkg == "com.xiaoji.egggame") "GameHub" else pkg - /** - * Stop the installed rivals' background processes before the client starts. A rival in the - * foreground survives this. The activity service call and its log messages are unchanged. - */ fun stopBeforeSession(context: Context) { val rivals = installed(context) if (rivals.isEmpty()) return diff --git a/app/src/main/java/com/droiddeck/launcher/session/RumbleComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/RumbleComponent.kt index 4adf1c0a5..8c69891d8 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/RumbleComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/RumbleComponent.kt @@ -17,26 +17,11 @@ import com.droiddeck.launcher.input.PadBridge import java.io.DataInputStream import kotlin.math.max -/** - * Rumble for the pad, on the motors in the player's hands. - * - * When a game plays a force-feedback effect on the fake pad (or Steam Input sends the Deck's - * rumble report), the fake evdev layer inside the guest connects to the abstract socket [NAME] - * and sends one packet: strong, weak, duration in ms and the pad slot, four little-endian 16-bit - * values. This listens for those and plays the effect on the physical controller playing in that - * slot (PadBridge.controllerForSlot) when Android exposes its motors - strong and weak separately - * where it has two - and otherwise on the device's own vibrator, with the stronger motor's - * strength: the on-screen pad, or a controller without rumble. Best effort both ways: a missing - * listener costs the guest nothing, and a bad packet is dropped. - */ class RumbleComponent : SessionPart() { @Volatile private var server: LocalServerSocket? = null private var phone: Motors? = null - /** Per slot, where its last effect went, so the next one (or a stop) can end it there. */ private val playing = HashMap<Int, Motors>() - /** Per slot, the last effect handed to the motors and when, to drop the repeats games stream. */ private val sent = HashMap<Int, Sent>() - /** Each controller's motors, looked up once; null when it has none. */ private val controllers = HashMap<Int, Motors?>() private var preferences: SharedPreferences? = null private var enabled = false @@ -44,7 +29,6 @@ class RumbleComponent : SessionPart() { if (key == null || key == "rumble") refreshEnabled() } - /** The motors of the controller with this input device id, or null when it has none. Tests swap it. */ internal var controllerMotors: (Int) -> Motors? = ::lookupController @Synchronized private fun refreshEnabled() { @@ -69,9 +53,8 @@ class RumbleComponent : SessionPart() { DataInputStream(client.inputStream).readFully(bytes) buzz(u16(bytes, 0), u16(bytes, 2), u16(bytes, 4), u16(bytes, 6)) } catch (e: Exception) { - // A partial packet or a closed peer: nothing to play. } finally { - try { client.close() } catch (e: Exception) { /* already gone */ } + try { client.close() } catch (e: Exception) { } } } }, "rumble").apply { isDaemon = true; start() } @@ -87,7 +70,7 @@ class RumbleComponent : SessionPart() { controllers.clear() val s = server server = null - try { s?.close() } catch (e: Exception) { /* the accept loop ends on the next wake */ } + try { s?.close() } catch (e: Exception) { } } private fun u16(b: ByteArray, at: Int): Int = (b[at].toInt() and 0xFF) or ((b[at + 1].toInt() and 0xFF) shl 8) @@ -104,8 +87,6 @@ class RumbleComponent : SessionPart() { } if (target == null) return if ((strong == 0 && weak == 0) || ms == 0) { target.cancel(); sent.remove(slot); return } - // Games re-send their effect every frame; restarting the motor that often floods the haptics - // driver. ponytail: a change inside MIN_GAP_MS is dropped, the game's next packet brings it. val now = SystemClock.uptimeMillis() val last = sent[slot] if (last != null && (now - last.at < MIN_GAP_MS || (last.strong == strong && last.weak == weak && now < last.until))) return @@ -118,7 +99,6 @@ class RumbleComponent : SessionPart() { } } - /** The motors of the controller playing in this slot when it has any, else the phone's. */ private fun target(slot: Int): Motors? { val id = PadBridge.controllerForSlot(slot) if (id == PadBridge.NO_CONTROLLER) return phone @@ -134,7 +114,6 @@ class RumbleComponent : SessionPart() { private data class Sent(val strong: Int, val weak: Int, val at: Long, val until: Long) - /** One place an effect can play. Strengths are the evdev 0..65535 magnitudes. */ internal interface Motors { val name: String fun play(strong: Int, weak: Int, ms: Long) @@ -147,7 +126,6 @@ class RumbleComponent : SessionPart() { override fun cancel() = vibrator.cancel() } - /** Android 12+: each of the pad's motors on its own, the lower id being the strong one. */ @RequiresApi(31) private class PadMotors(override val name: String, private val manager: VibratorManager, private val ids: IntArray) : Motors { override fun play(strong: Int, weak: Int, ms: Long) { @@ -164,7 +142,6 @@ class RumbleComponent : SessionPart() { override fun cancel() = manager.cancel() } - /** Before Android 12 a pad has one vibrator. */ private class LegacyPadMotors(override val name: String, private val vibrator: Vibrator) : Motors { override fun play(strong: Int, weak: Int, ms: Long) = vibrator.vibrate(oneShot(vibrator, max(strong, weak), ms)) override fun cancel() = vibrator.cancel() @@ -172,9 +149,7 @@ class RumbleComponent : SessionPart() { companion object { private const val TAG = "SessionService" - /** Must match the fake evdev layer (fakeinput_steam.cpp). */ const val NAME = "droiddeck-rumble" - /** The shortest time between two effects started on one slot's motors. */ const val MIN_GAP_MS = 50L private fun oneShot(vibrator: Vibrator, strength: Int, ms: Long): VibrationEffect { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SecondaryLibrary.kt b/app/src/main/java/com/droiddeck/launcher/session/SecondaryLibrary.kt index 10692167d..24c15df82 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SecondaryLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SecondaryLibrary.kt @@ -7,7 +7,6 @@ import java.nio.file.StandardCopyOption.COPY_ATTRIBUTES import java.security.MessageDigest import java.util.Locale -/** Android shared storage cannot create the links in Wine prefixes or native Steam depots. */ object SecondaryLibrary { private val bootstrapTools = linkedMapOf( "4185400" to "SteamLinuxRuntime_4-arm64", @@ -25,12 +24,10 @@ object SecondaryLibrary { val CARD_GUESTS = listOf("/mnt/droiddeck-sd", "/mnt/bannerlator-sd") - /** Parent binds come first; the more specific private directories override them in PRoot. */ fun binds(files: File, library: File, guests: List<String> = CARD_GUESTS): List<String> { val private = privateRoot(files, library) val apps = File(library, "steamapps") val tools = LinkedHashMap(bootstrapTools) - // Include other installed Proton/runtime versions, without routing ordinary game depots. apps.listFiles()?.filter { it.name.matches(Regex("appmanifest_\\d+\\.acf")) }?.forEach { manifest -> val text = manifest.readText() val id = Regex("\"appid\"\\s+\"(\\d+)\"").find(text)?.groupValues?.get(1) @@ -48,13 +45,8 @@ object SecondaryLibrary { for (relative in listOf("steamapps/common/$dir", "steamapps/downloading/$id")) { val target = File(private, relative) migrate(File(library, relative), target) - // FUSE discarded the executable bits. Steam will restore the depot's exact modes - // on verification; its launchers must already be executable when importing it. target.walkTopDown().filter { it.isFile }.forEach { it.setExecutable(true, true) } overrides.add(target to relative) - // Steam normalizes depot install directories to lowercase on Android's - // case-insensitive shared storage. PRoot binds are case-sensitive, so both - // spellings must reach the same private directory during the commit. val lower = relative.lowercase(Locale.ROOT) if (lower != relative) overrides.add(target to lower) } @@ -64,7 +56,6 @@ object SecondaryLibrary { } } - /** Copy once into a staging directory; retain the source and never merge over a live prefix. */ private fun migrate(source: File, target: File) { val migrated = File(target.parentFile, ".${target.name}.migrated") if (target.isDirectory) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SelectedGameProfile.kt b/app/src/main/java/com/droiddeck/launcher/session/SelectedGameProfile.kt index aef506491..41eeac38f 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SelectedGameProfile.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SelectedGameProfile.kt @@ -16,7 +16,6 @@ data class SelectedGameProfile( val componentFiles: Map<String, String>, val environmentOverrides: Int, val windowsComponentNames: List<String>, - /** Start without Steam (GameEnvironment.NO_STEAM) is on for this game. */ val startWithoutSteam: Boolean = false, ) { val hasOverrides: Boolean @@ -53,7 +52,6 @@ data class SelectedGameProfile( inherited = override == null, ) } - // Start without Steam is a switch of its own on the Games page, listed by name. val otherEnvironment = environment.games[game.profileKey].orEmpty().keys - FexPreset.environmentKeys - GameEnvironment.NO_STEAM return SelectedGameProfile( diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionAchievementGame.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionAchievementGame.kt index 62991bd8e..d86c88df4 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionAchievementGame.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionAchievementGame.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.session -/** An active game needs a live window; a launch URL or stale Steam base layer is insufficient. */ internal object SessionAchievementGame { fun activeAppId(focused: Long?, windows: List<Long>, installed: Set<Long>): Long? { if (focused != null && focused > 0 && focused != 769L) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt index cc0f5ad82..9249871d5 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt @@ -11,44 +11,16 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * Finishes a session's folder: the compositor's log, the Steam client's logs scrubbed line by - * line, Android's crash buffer, and a marker that says the folder is complete. - * - * Three callers, because a session ends three ways. [collect] runs at an ordinary stop. The - * app's uncaught-exception handler ([CrashHandler]) runs it as the process dies, so a crash in - * our own code leaves a full folder and not a half one. And [finishAbandoned] runs at the next - * app start for a folder that has no marker: the process was killed outright (Android's phantom - * process killer, a native crash in the compositor, the battery) and nothing of ours got to run. - * Everything written *during* the session - session.log, app.log, wayland.log, audio.log, the - * device report - is already on disk at that point; only the pieces gathered at the end were - * missing, and the crash buffer keeps its entries after the process is gone, which is the whole - * reason it is worth coming back for. - */ object SessionArtifacts { private const val TAG = "SessionArtifacts" - /** Written last; a folder without it did not get its ending. */ const val COMPLETE_MARKER = ".complete" - /** - * Every file in the folder has been through the redactor under its current rules. Named after - * [LogRedactor.RULES_VERSION], so a folder scrubbed under older rules is scrubbed again. - */ private val SCRUBBED_MARKER = ".scrubbed-r${LogRedactor.RULES_VERSION}" private val OLD_SCRUBBED_MARKER = Regex("""\.scrubbed-(r?\d+)""") - /** - * Written by a session's own ending, and only when every file in the folder, subfolders - * included (whatever the session script copied into steam/ and droiddeck-esync/ as well as what - * the app wrote), went through the redactor without a failure. It lists each of those files - * with its size and modification time, under the redactor's rules version ([scrubbedFiles]). - * A share no longer relies on it: every text file goes through the share pass on the way into - * the zip regardless. - */ const val SCRUBBED_TREE_MARKER = ".scrubbed-tree" - /** Everything the end of a session gathers, into [dir]. Safe to call for a dead session. */ fun collect(context: Context, dir: File, reason: String) { try { val wayland = File(dir, "wayland.log") @@ -60,7 +32,6 @@ object SessionArtifacts { LogRedactor.learnFromRuntime(LinuxRuntime.rootDir(context)) val record = Record(dir) copySteamLogs(context, dir, record) - // A session the system killed leaves its trace here and nowhere else. SessionLogCapture.dumpCrashBuffer(File(dir, "crash.log")) markScrubbed(scrubTree(record)) SessionEvents.record("session.artifacts_collected", mapOf("reason" to reason), dir) @@ -70,17 +41,6 @@ object SessionArtifacts { } } - /** - * Every session folder not yet scrubbed under the redactor's current rules - written before - * every file was scrubbed, before the device's own addresses or the Steam account were, or - * moved in from Download/ ([LogMigration] strips their markers first, [unmarkMoved]): the - * whole folder, steam/ and every other subfolder included, through the redactor, and the - * current marker written in place of the old ones. Runs at app start with [finishAbandoned]. - * - * A folder holds some 30 MB of Steam logs, about half a minute's work, so folders go through - * a few at a time; one the process does not get to keeps its old marker and is taken up at - * the next start. - */ @Synchronized fun scrubOlder(context: Context) { val current = SessionPaths.current() @@ -104,27 +64,14 @@ object SessionArtifacts { if (failed > 0) ", $failed not fully (again at the next start)" else "") } - /** - * Folders [LogMigration] just moved in lose every scrub marker an earlier build left, so - * [scrubOlder], which runs right after it, puts them through the current rules whatever those - * markers claimed. - */ fun unmarkMoved(dirs: List<File>) { dirs.filter { SessionPaths.isSessionFolder(it) }.forEach { dir -> dir.listFiles { f -> OLD_SCRUBBED_MARKER.matches(f.name) || f.name == SCRUBBED_TREE_MARKER }?.forEach { it.delete() } } } - /** - * The session's own files (session.log, app.log, desktop.log, the Steam desktop client's - * steam-desktop.log, ...) were written as they happened, unscrubbed; this pass puts every one - * through the redactor before the folder can be shared. A file is rewritten only if a line - * changed. steam/ was scrubbed on the way in. - */ - /** A file's size and modification time as they stood right after it was scrubbed. */ private class Scrubbed(val length: Long, val modified: Long) - /** Files of a folder as they stood right after scrubbing, filled by [scrubTree] and [copySteamLogs]. */ private class Record(val dir: File) { val files = HashMap<File, Scrubbed>() var failed = false @@ -138,12 +85,6 @@ object SessionArtifacts { } } - /** - * Puts [f] through the redactor in place. Returns its size and modification time as it then - * stands, or null if it could not, and [f] may still hold the original. A writer still - * appending to [f] (the app's own log capture) changes its size afterwards, and a share then - * scrubs it again. - */ private fun scrubFile(dir: File, f: File): Scrubbed? { val tmp = File(dir, ".${f.name}.scrub") return try { @@ -152,11 +93,8 @@ object SessionArtifacts { val scrubbed = tmp.readBytes() if (scrubbed.contentEquals(f.readBytes())) { tmp.delete() - // Unchanged by the redactor, and by anything else while it read. before.takeIf { f.length() == it.length && f.lastModified() == it.modified } } else { - // Shared storage can refuse a rename; then the scrubbed bytes are written over the - // original instead, so it never stays behind unscrubbed. if (!tmp.renameTo(f)) { f.writeBytes(scrubbed); tmp.delete() } Scrubbed(scrubbed.size.toLong(), f.lastModified()).takeIf { f.length() == it.length } } @@ -167,14 +105,9 @@ object SessionArtifacts { } } - /** - * Records [record]'s folder as scrubbed - every file went through without a failure - or - * leaves it unmarked, so the next share and the pass over older folders scrub it again. - */ private fun markScrubbed(record: Record): Boolean { val dir = record.dir val tree = File(dir, SCRUBBED_TREE_MARKER) - // A marker from older rules vouches for nothing now, whether or not this pass succeeded. dir.listFiles { f -> OLD_SCRUBBED_MARKER.matches(f.name) }?.forEach { it.delete() } if (record.failed) { tree.delete() @@ -197,11 +130,6 @@ object SessionArtifacts { } } - /** - * The files of [dir] that its own ending scrubbed and that are unchanged since (same size and - * modification time), by path relative to [dir]. Empty when the folder has no such record, or - * one made under other redactor rules. - */ fun scrubbedFiles(dir: File): Set<String> { val lines = try { File(dir, SCRUBBED_TREE_MARKER).takeIf { it.isFile }?.readLines() } catch (e: Exception) { null } ?: return emptySet() @@ -214,21 +142,13 @@ object SessionArtifacts { }.toSet() } - /** The end-of-session scrub of [dir] on its own (steam/ already in place); also the pass over older and moved folders. */ internal fun scrubAndMark(dir: File): Boolean = markScrubbed(scrubTree(Record(dir))) - /** - * [record]'s folder and every folder under it (steam/, droiddeck-esync/): the session script - * copied files there verbatim, and a Steam log the app did not copy over again (gone from the - * runtime, or past its size limit) stayed as the script left it. Files [copySteamLogs] already - * redacted on the way in are in [record] and left alone. - */ private fun scrubTree(record: Record): Record { record.dir.walkTopDown().filter { it.isDirectory }.forEach { scrubFolder(it, record) } return record } - /** Steam's logs: redacted into steam/, never copied verbatim. What it wrote goes in [record]. */ private fun copySteamLogs(context: Context, dir: File, record: Record) { val logs = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/logs") if (!logs.isDirectory) return @@ -247,12 +167,6 @@ object SessionArtifacts { Log.i(TAG, "collected ${out.listFiles()?.size ?: 0} Steam log(s), scrubbed, into $out") } - /** - * Every session folder without a marker gets its ending now. Only the newest of them gets the - * Steam logs - the runtime holds one set, and it belongs to the last session that ran; an - * older folder would be handed logs that are not its own. Runs on a worker thread at app - * start; nothing here touches the session that is about to begin. - */ @Synchronized fun finishAbandoned(context: Context) { val parent = LinuxRuntime.logDir(context) @@ -289,12 +203,6 @@ object SessionArtifacts { } } - /** - * Keeps the newest [SessionPaths.KEEP_SESSIONS] session folders and deletes the rest: a few - * days of testing left hundreds, and the one a report needed was lost among them. Only folders - * that are finished; the session in progress is never touched. Runs at app start after - * [finishAbandoned], on its worker thread. - */ @Synchronized fun prune(context: Context) { val current = SessionPaths.current() @@ -304,11 +212,6 @@ object SessionArtifacts { if (old.isNotEmpty()) Log.i(TAG, "deleted ${old.size} session folder(s) past the newest ${SessionPaths.KEEP_SESSIONS}") } - /** - * Every session folder but the one in progress, and the one-off command logs (tools/, or loose - * beside the folders from before it): the Setup page's Clear logs. Returns how many session - * folders went. - */ @Synchronized fun clearAll(context: Context): Int { val current = SessionPaths.current() diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionDisplay.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionDisplay.kt index bdb557539..ae64b98cf 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionDisplay.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionDisplay.kt @@ -3,26 +3,14 @@ package com.droiddeck.launcher.session import android.content.Context import android.view.WindowManager -/** The virtual display advertised to the guest, before it is fitted onto the Android panel. */ object SessionDisplay { const val MATCH_SCREEN = "screen" - /** The window's size, kept in step while the session runs: a foldable opening or closing, a - * split screen or a freeform window resizes the guest's display instead of letterboxing it. */ const val FOLLOW_SCREEN = "follow" - /** Below this a side is a picture-in-picture or a sliver of split screen: not worth a resize. */ const val FOLLOW_MIN_SIDE = 480 - /** - * Presets are heights, not sizes: "720p" is the panel's own shape at 720 lines, never narrower - * than 16:9 and never taller than the panel (SHAPE_AUTO). The client treats the output as the - * games' native resolution, so the height is what bounds a game's default size on a 1440p - * phone; the shape is what keeps the client's interface edge to edge on a 20:9 one (1600x720) - * and keeps games at 16:9 or wider on a foldable's near-square panel. - */ const val DEFAULT_RESOLUTION = "720p" private val PRESET_HEIGHTS = listOf(720, 900, 1080) - /** The height of a preset choice ("900p"), or null for Match screen and fixed sizes. */ fun presetHeight(choice: String): Int? = if (choice.endsWith("p")) choice.dropLast(1).toIntOrNull()?.takeIf { it > 0 } else null @@ -32,42 +20,27 @@ object SessionDisplay { fun resolveChoice(panel: Pair<Int, Int>, choice: String): Pair<Int, Int> { if (choice == MATCH_SCREEN || choice == FOLLOW_SCREEN) return screenSize(panel) presetHeight(choice)?.let { return resolve(panel, it, SessionPrefs.SHAPE_AUTO) } - // Fixed sizes (custom, or a preset saved before presets were heights) keep their exact size, - // including legacy ones past the custom dialog's limit. val parts = choice.split('x').map { it.toIntOrNull() } return if (parts.size == 2 && parts.all { it != null && it > 0 }) parts[0]!! to parts[1]!! else resolve(panel, 720, SessionPrefs.SHAPE_AUTO) } - /** The height presets the panel is tall enough for, then the panel itself, with no duplicate - * sizes, then Follow screen. */ fun resolutionOptions(panel: Pair<Int, Int>): List<String> { val panelHeight = minOf(panel.first, panel.second) return PRESET_HEIGHTS.filter { it <= panelHeight }.map { "${it}p" } .filter { resolveChoice(panel, it) != screenSize(panel) } + MATCH_SCREEN + FOLLOW_SCREEN } - /** A Follow screen session's display for a window of this size: its own shape, square or - * portrait included, with even sides; null when it is too small to follow (PiP). */ fun followSize(width: Int, height: Int): Pair<Int, Int>? = if (minOf(width, height) < FOLLOW_MIN_SIDE) null else (width and 1.inv()) to (height and 1.inv()) - /** Unfolded controls: the most of the window the game may take, so a window near 16:9 itself - * still leaves the Deck controls room. */ const val UNFOLDED_MAX_GAME_SHARE = 0.8f - /** A foldable: Android 11's hinge angle sensor, which foldables report and slates do not. Unfolded - * controls are offered only there - untested on tablets, whose 16:10 landscape leaves them - * little room under a 16:9 game. */ fun foldable(context: Context): Boolean = context.packageManager.hasSystemFeature("android.hardware.sensor.hinge_angle") - /** A panel at least this wide on its shortest side (dp) is a foldable's unfolded inner screen. */ const val UNFOLDED_MIN_DP = 600 - /** Unfolded controls: the game's height in a window of this size - 16:9 across the full width, - * held either way, with even sides - and the Deck controls take the rest below it, laid out to - * whatever that leaves (DeckControlsPanel's GamepadLayout). */ fun unfoldedGameHeight(width: Int, height: Int): Int = minOf(width * 9 / 16, (height * UNFOLDED_MAX_GAME_SHARE).toInt()) and 1.inv() @@ -79,13 +52,10 @@ object SessionDisplay { return maxOf(mode.physicalWidth, mode.physicalHeight) to minOf(mode.physicalWidth, mode.physicalHeight) } - /** Interpret an older cap/aspect combination without altering the saved picture size. */ fun resolve(panel: Pair<Int, Int>, cap: Int, shape: String, custom: Pair<Int, Int>? = null): Pair<Int, Int> { custom?.let { return it } val panelW = maxOf(panel.first, panel.second).toFloat() val panelH = minOf(panel.first, panel.second).toFloat().coerceAtLeast(1f) - // Auto retains wide panels but floors squarer ones at 16:9 for game compatibility. - // Match screen removes that floor; fixed 16:9 also keeps foldable sessions stable. val aspect = when (shape) { SessionPrefs.SHAPE_WIDE -> 16f / 9f SessionPrefs.SHAPE_EXACT -> panelW / panelH @@ -95,7 +65,6 @@ object SessionDisplay { return ((height * aspect).toInt() and 1.inv()) to (height and 1.inv()) } - /** Mirrors gamescope's even-width 16:9 calculation, including near-16:9 rounding. */ fun canStretch16x9(size: Pair<Int, Int>): Boolean = (size.second * 16 + 4) / 9 / 2 * 2 > size.first } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt index 5f8f2bf96..86b027af6 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt @@ -6,15 +6,11 @@ import org.json.JSONObject import java.io.File import java.io.FileOutputStream -/** Writes the machine-readable lifecycle stream beside the existing session artifacts. */ object SessionEvents { private const val TAG = "SessionEvents" private val lock = Any() - /** [label] names the session's folder for what is being run (SessionPaths.label). */ fun begin(context: Context, mode: String, label: String? = null) { - // A runtime-install failure can leave the folder claimed before the service ever starts. - // Keep that folder, but give the next attempt its own session directory. SessionPaths.take() val dir = SessionPaths.beginOrCurrent(context, label ?: SessionPaths.label(mode, null)) synchronized(SessionState) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt index 9b3d4e650..b61ac122c 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt @@ -11,37 +11,22 @@ import com.droiddeck.launcher.runtime.LinuxRuntime import com.droiddeck.launcher.runtime.ProotFastPath import java.io.File -/** Everything the session needs written into the runtime before it starts. */ object SessionFiles { private const val TAG = "SessionFiles" private const val NO_PAD_SWITCH = "Download/droiddeck-no-pad" - /** Where the DirectAudio driver lives inside the runtime; the wrappers get it as BL_DIRECTAUDIO. */ const val DIRECTAUDIO_DIR = "usr/local/lib/directaudio" - /** The driver sets staged under it: classic Wine 11 (Valve, GE) and the system-thread interface (Proton-CachyOS). */ val DIRECTAUDIO_SETS = arrayOf("linux-wine11", "linux-wine11-systhread") - // Staged only with a complete desktop; removal matches these against the APK as well. internal val DESKTOP_FILES = arrayOf( "usr/local/bin/droiddeck-desktop" to "usr/local/bin/droiddeck-desktop", - // Games and emulators from the menu, full screen in a gamescope of their own. "usr/local/bin/droiddeck-gpu" to "usr/local/bin/droiddeck-gpu", "usr/local/bin/droiddeck-desktop-gpu" to "usr/local/bin/droiddeck-desktop-gpu", "usr/local/bin/droiddeck-desktop-window-rules" to "usr/local/bin/droiddeck-desktop-window-rules", - // KWin at the app's surface size, for Plasma's session (it starts KWin through this name). "usr/local/bin/kwin_wayland_wrapper" to "usr/local/bin/kwin_wayland_wrapper", "usr/lib/firefox/defaults/pref/droiddeck.js" to "usr/lib/firefox/defaults/pref/droiddeck.js", "etc/xdg/autostart/droiddeck-clipboard.desktop" to "etc/xdg/autostart/droiddeck-clipboard.desktop", ) - /** - * The libraries and scripts the session runs, refreshed from the apk at every launch. - * - * The runtime image carries its own copies, but a runtime installed months ago carries the - * copies of that day and a device has no way to replace them from outside the app. Staging - * them here is how a fix inside the session shim, the controller reader or one of the scripts - * reaches an already-installed runtime without a ~790 MB re-download. Each lands through a - * rename, so a session that still has one mapped keeps the file it opened. - */ fun stage(context: Context, root: File) { if (DesktopCatalog.desktopInstalled(root)) try { DesktopCatalog.cleanLegacy(context) } catch (e: Exception) { Log.w(TAG, "LXQt cleanup will retry next session", e) } @@ -81,47 +66,30 @@ object SessionFiles { "usr/local/bin/droiddeck-desktop-bookmarks" to "usr/local/bin/droiddeck-desktop-bookmarks", "usr/local/bin/droiddeck-steam-shim" to "usr/local/bin/droiddeck-steam-shim", "usr/local/bin/droiddeck-steam-shortcuts" to "usr/local/bin/droiddeck-steam-shortcuts", - // Asks the app for an Epic game's sign-in code at launch, from the compat tool (StoreLaunchRequests). "usr/local/bin/droiddeck-store-launch" to "usr/local/bin/droiddeck-store-launch", - // A game's web links to Android's browser (Wine's winebrowser starts it). "usr/local/bin/droiddeck-open-url" to "usr/local/bin/droiddeck-open-url", "usr/local/bin/droiddeck-steam-games" to "usr/local/bin/droiddeck-steam-games", "usr/local/bin/droiddeck-pad-defaults" to "usr/local/bin/droiddeck-pad-defaults", - // Cardboard mode's SteamVR: install, start and stop (docs/cardboard-vr.md). "usr/local/bin/droiddeck-steamvr" to "usr/local/bin/droiddeck-steamvr", - // Flatpak: the bwrap stand-in, the store's helper and setup, and the front end's launcher. "usr/local/bin/droiddeck-bwrap" to "usr/local/bin/droiddeck-bwrap", "usr/local/bin/droiddeck-flatpak" to "usr/local/bin/droiddeck-flatpak", "usr/local/bin/droiddeck-flatpak-setup" to "usr/local/bin/droiddeck-flatpak-setup", "usr/local/bin/droiddeck-flatpak-run" to "usr/local/bin/droiddeck-flatpak-run", - // The user's own AppImages, extracted at import (AppImageManager). "usr/local/bin/droiddeck-appimage-run" to "usr/local/bin/droiddeck-appimage-run", "usr/local/bin/droiddeck-script-run" to "usr/local/bin/droiddeck-script-run", - // The desktop's Run entries and its exec fallback: Windows programs through Steam's Proton, - // x86 Linux programs through FEX. "usr/local/bin/droiddeck-open" to "usr/local/bin/droiddeck-open", "usr/local/bin/droiddeck-proton-run" to "usr/local/bin/droiddeck-proton-run", - // The SteamOS helpers the client calls in Deck mode: the two Armada found it needs, plus - // the three under /usr/bin, all no-ops that answer "nothing to do" (see each file). "usr/bin/steamos-update" to "usr/bin/steamos-update", "usr/bin/steamos-select-branch" to "usr/bin/steamos-select-branch", - // Big Picture's "Switch to Desktop": asks the app for the desktop with Steam in it. "usr/bin/steamos-session-select" to "usr/bin/steamos-session-select", "usr/bin/jupiter-biosupdate" to "usr/bin/jupiter-biosupdate", "usr/bin/steamos-polkit-helpers/steamos-priv-write" to "usr/bin/steamos-polkit-helpers/steamos-priv-write", "usr/bin/steamos-polkit-helpers/steamos-set-timezone" to "usr/bin/steamos-polkit-helpers/steamos-set-timezone", - // On device the client called these four by their polkit-helpers path, not /usr/bin: the - // "Update Error" dialog was steamos-update missing there. "usr/bin/steamos-polkit-helpers/steamos-update" to "usr/bin/steamos-polkit-helpers/steamos-update", "usr/bin/steamos-polkit-helpers/steamos-select-branch" to "usr/bin/steamos-polkit-helpers/steamos-select-branch", "usr/bin/steamos-polkit-helpers/jupiter-biosupdate" to "usr/bin/steamos-polkit-helpers/jupiter-biosupdate", "usr/bin/steamos-polkit-helpers/jupiter-dock-updater" to "usr/bin/steamos-polkit-helpers/jupiter-dock-updater", ) - // The patched gamescope (tools/gamescope): the runtime's own version rebuilt with the ARM64 - // client fixes, over /usr/local/bin so it comes first in the session's PATH. Only when the - // apk carries it - a build without the asset leaves the runtime's copy alone. - // Valve's mangoapp (tools/mangoapp) - Deck mode's performance overlay - with the five - // libraries the runtime lacks beside it, and the wrapper on PATH that points it at them. val mangoapp = listOf( "usr/local/bin/mangoapp", "usr/local/lib/mangoapp/mangoapp", @@ -131,11 +99,9 @@ object SessionFiles { "usr/local/lib/mangoapp/libtraceevent.so.1", "usr/local/lib/mangoapp/libtracefs.so.1", ).map { it to it } - // What the Windows components installer reads .msi packages with (tools/msitools). val msitools = listOf("msiinfo", "cabextract", "7z", "7z.so", "libmsi-1.0.so.0", "libgsf-1.so.114", "libgcab-1.0.so.0") .map { "usr/local/lib/droiddeck-msitools/$it" } + listOf("NOTICE", "GPL-2", "GPL-3", "LGPL-2.1", "7zip-License", "7zip-unRarLicense").map { "usr/local/share/licenses/droiddeck-msitools/$it" } - // Cardboard mode's SteamVR driver and the Vulkan layer its compositor needs (tools/vr). val vr = listOf( "droiddeck/bin/linuxarm64/driver_droiddeck.so", "droiddeck/driver.vrdrivermanifest", @@ -143,8 +109,6 @@ object SessionFiles { "layer/libVkLayer_droiddeck_vr.so", "layer/VK_LAYER_DROIDDECK_vr.json", ).map { "usr/local/lib/droiddeck-vr/$it" to "usr/local/lib/droiddeck-vr/$it" } + - // The same layer, implicit for the whole session: it passes everything through until SteamVR - // runs, then Steam's own web helper gets the shared-texture support it needs (tools/vr). listOf("usr/local/lib/droiddeck-vr/layer/VK_LAYER_DROIDDECK_vr_implicit.json" to "usr/share/vulkan/implicit_layer.d/droiddeck_vr.json") val fexPreloads = listOf("x86_64", "i386").flatMap { arch -> @@ -180,12 +144,6 @@ object SessionFiles { LegacyNames.migrateRootfs(root) ComponentsManager.migrateLaunchDir(context) EsyncPacks.stageBundled(context, root) - // The DirectAudio driver for games under Proton: the glibc build of winedirectaudio, which - // the Proton wrappers add to WINEDLLPATH when the session asks for it (BL_DIRECTAUDIO). Two - // sets, one per Wine audio interface - the wrapper picks by what the Proton's own winepulse.so - // imports (see steam-compatibility), because both Wines call themselves 11.0 and the wrong set - // plays silence. Staged like the scripts, so a driver fix reaches an installed runtime without - // re-hosting. The sets come from the pinned DirectAudio release (tools/directaudio/release.env). val directAudio = arrayOf( "aarch64-unix/winedirectaudio.so", "aarch64-windows/winedirectaudio.drv", @@ -209,18 +167,11 @@ object SessionFiles { } if (!installed) Log.e(TAG, "DirectAudio $set/$relative NOT staged") } - // A runtime from before the sets had names kept the driver at the directory's root; it would - // never be picked again, so it goes. for (old in arrayOf("aarch64-unix", "aarch64-windows", "i386-windows")) File(root, "$DIRECTAUDIO_DIR/lib/wine/$old").deleteRecursively() - // What every process in the session preloads. LD_PRELOAD in the environment would not - // survive: the Steam client rebuilds it for each process it starts and appends its own - // overlay entry without a separator, which silently drops whatever was there. val preload = StringBuilder("/usr/local/lib/libblsession.so\n") if (!File(Environment.getExternalStorageDirectory(), NO_PAD_SWITCH).exists()) { preload.append("/usr/local/lib/libfakeinput.so\n") } - // Last, so the others' opens and stats reach it through RTLD_NEXT. It stays idle in any - // process its proot did not describe to it (ProotFastPath). if (ProotFastPath.enabled(context)) preload.append(ProotFastPath.LIBRARY + "\n") val etc = File(root, "etc").apply { mkdirs() } val staged = File(etc, "ld.so.preload.staged") @@ -234,8 +185,6 @@ object SessionFiles { } val startupMovieDir = File(root, "root/.local/share/Steam/config/uioverrides/movies") - // Steam looks up these conventional names in its user override directory. Keep both - // variants populated because the startup movie name differs across Steam clients. val bigPictureMovieInstalled = stageStartupMovie( context, startupMovieDir, @@ -387,10 +336,5 @@ object SessionFiles { indent + "}", ).joinToString(newline) - /** - * Where the session writes its log: app-private `files/logs`, bound into the session at the same - * path, so the session script's own redirection always opens. A folder leaves the device only - * through Share logs, which scrubs every file again on the way into the zip. - */ fun logDirectory(context: Context): File = LinuxRuntime.logDir(context).apply { mkdirs() } } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt index 85760c822..11f008fe6 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt @@ -17,28 +17,12 @@ import java.io.File import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream -/** - * Share logs: one zip, made when asked, of a session folder (the newest by default) with the last - * seven days of the stores' log and the one-off command logs beside it, handed to Android's share - * sheet. Logs live in app-private `files/logs`; this is how they leave the device. Every text file - * goes through [LogRedactor.redactForShare] on the way in, so even a file written before a rule - * existed is clean in the zip. The zip sits in `cache/share/` until the next share or app start. - */ object SessionLogShare { private val main = Handler(Looper.getMainLooper()) - /** - * How far the zip for a share has got, 0..1, or null when none is being made. The Share logs - * buttons show it in place of their label, so a tap is answered at once. - */ var progress by mutableStateOf<Float?>(null) private set - /** - * Zips [folder] on a worker thread with [progress] following along, then hands the zip (null - * if there was nothing to zip or it failed) to [done] on the main thread. Call on the main - * thread; a tap while a zip is being made is ignored. - */ fun prepare(context: Context, folder: () -> File?, done: (File?) -> Unit) { if (progress != null) return progress = 0f @@ -50,29 +34,17 @@ object SessionLogShare { }, "share-logs").start() } - /** The newest session folder in either place logs are written, or null if there is none. */ fun latest(context: Context): File? = SessionPaths.sessionFolders(context).lastOrNull() - /** Builds the zip (blocking). Returns null when there is no session to share. */ - /** Builds a zip for one specific session folder (blocking). */ fun zipFolder(context: Context, folder: File, onProgress: ((Float) -> Unit)? = null): File? { if (!folder.isDirectory) return null - // The client's logs past STEAM_LOG_MAX_BYTES stay out, as they do from the app's own copy: a - // CEF debug log or an old *.previous.txt reached 17 MB, every share scrubbed them line by - // line, and the share sheet took ten seconds to appear. val files = folder.walkTopDown().filter { it.isFile } .filterNot { it.parentFile?.name == "steam" && it.length() > STEAM_LOG_MAX_BYTES }.toList() if (files.isEmpty()) return null val out = shareDir(context).apply { deleteRecursively(); mkdirs() } val zip = File(out, "DroidDeck-${folder.name}.zip") - // Scrubbed on the way into the zip: a session shared while it runs has not had its end-of- - // session pass yet, and the redactor changes nothing in a line that is already clean. LogRedactor.learnFromRuntime(LinuxRuntime.rootDir(context)) - // A finished folder was scrubbed whole when it ended. A file its record lists, unchanged - // since, goes in as it is; anything else (added or changed since, hidden, or any file of a - // session still running) is scrubbed again. Scrubbing tens of MB of the client's logs on - // every share kept the share sheet from appearing for ten seconds or more. val live = liveSteamLogs(context, folder) val logs = LinuxRuntime.logDir(context) val stores = recent(File(logs, STORES_DIR)) @@ -96,7 +68,6 @@ object SessionLogShare { private fun addEntry(z: ZipOutputStream, name: String, f: File) { z.putNextEntry(ZipEntry(name)) - // Every text file, every time: the folder's own scrub may predate a rule. if (LogRedactor.isText(f)) { val w = z.bufferedWriter() LogRedactor.scrubForShare(f, w) @@ -107,38 +78,26 @@ object SessionLogShare { z.closeEntry() } - /** - * The client's own logs as they stand, for a session shared while it runs. The session script - * copies them into steam/ only as it exits, so a zip made from the drawer had none - and - * controller.txt (which pad the client opened, the touch mode it set) is what a controller or - * touch report needs most. Only for the running session: an older folder would get this - * session's logs. The same files the script copies; nothing holding credentials is in logs/. - */ private fun liveSteamLogs(context: Context, folder: File): List<File> { if (folder != SessionPaths.current() || File(folder, "steam").exists()) return emptyList() val logs = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/logs") return logs.listFiles()?.filter { it.isFile && it.length() <= STEAM_LOG_MAX_BYTES }.orEmpty() } - /** The client's content and bootstrap logs grow large over months and say nothing about a session. */ private const val STEAM_LOG_MAX_BYTES = 8L * 1024 * 1024 - /** The stores' engine log, one file a day, under files/logs. */ const val STORES_DIR = "stores" private const val RECENT_DAYS = 7 private fun shareDir(context: Context): File = File(context.cacheDir, "share") - /** The files in [dir] changed in the last [RECENT_DAYS] days. */ private fun recent(dir: File): List<File> { val since = System.currentTimeMillis() - RECENT_DAYS * 86_400_000L return dir.listFiles { f -> f.isFile && f.lastModified() >= since }?.sortedBy { it.name }.orEmpty() } - /** Deletes the last share's zip; at app start, so a zip never outlives the share it was made for. */ fun clear(context: Context) { shareDir(context).deleteRecursively() - // Where builds before this one put it. File(context.cacheDir, "shared-logs").deleteRecursively() } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPaths.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPaths.kt index 5b94c6a44..bb9940ffe 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPaths.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPaths.kt @@ -7,42 +7,14 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * One folder per session under app-private `files/logs/`, holding everything that session recorded, - * named for the day, that day's session number and what was run, so the one a report is about can - * be picked out without opening any: - * - * ``` - * files/logs/2026-09-30-03-steam/ - * device.txt what this device is, and every setting the session ran with - * session.log the guest session: proot, gamescope, the client's stdout - * wayland.log the app's compositor - * steam.log the Steam client's own log, scrubbed (Steam mode) - * steam/ the rest of the client's logs, scrubbed (Steam mode) - * desktop.log KWin, Plasma and the programs on it (desktop mode) - * files/logs/tools/ one-off commands: Flatpak installs, AppImage imports - * files/logs/stores/ the stores' engine log, one file a day - * ``` - * - * Folders from before this naming (`session-20260930-180642`) are still recognised, shared and - * pruned with the rest. - * - * The folder is claimed by whoever starts first - the activity starts the compositor before the - * service starts the session - so both write into the same one, and a recreated activity (a - * foldable opening mid-session) joins the folder in progress instead of opening another. - */ object SessionPaths { private const val TAG = "SessionPaths" - /** How many session folders are kept; older ones go at the next app start. */ const val KEEP_SESSIONS = 30 - /** Beside the session folders: the logs of one-off commands (GuestCommand's logName). */ const val TOOLS_DIR = "tools" - /** `2026-09-30-03-steam`: day, that day's session number, what was run. */ private val NAMED = Regex("""^(\d{4})-(\d{2})-(\d{2})-(\d{2,})-.+$""") - /** `session-20260930-180642`, from before. */ private val LEGACY = Regex("""^session-(\d{8})-(\d{6})(-\d+)?$""") @Volatile @@ -51,20 +23,12 @@ object SessionPaths { @Volatile private var ended: File? = null - /** - * The folder for the session now starting, or the one already in progress. With logs turned - * off on the main screen the folder lives in the app's cache instead of Downloads - the - * scripts and the compositor still need somewhere to write - and [release] deletes it. - * [label] says what is being run (see [label]); without one it comes from the session's state. - */ @Synchronized fun beginOrCurrent(context: Context, label: String? = null): File { dir?.let { if (it.isDirectory) return it } val parent = if (SessionPrefs.logsEnabled(context)) SessionFiles.logDirectory(context) else File(context.cacheDir, "session-logs") val day = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()) - // The day's highest number plus one, not a count: pruning removes the oldest folders, and - // a count would hand out a number already in use. val last = parent.listFiles()?.mapNotNull { f -> NAMED.matchEntire(f.name)?.takeIf { f.name.startsWith("$day-") }?.groupValues?.get(4)?.toIntOrNull() }?.maxOrNull() ?: 0 @@ -80,14 +44,8 @@ object SessionPaths { fun current(): File? = dir - /** The folder of the session that ended last, while it is still there: what its Share logs sends. */ fun lastEnded(): File? = ended?.takeIf { it.isDirectory } - /** - * The ending session takes its folder with it: the next session (one that replaces this one - * in place) then claims a folder of its own instead of sharing, and this one's collector - * finishes the folder it was handed. Called on the main thread as the session stops. - */ @Synchronized fun take(): File? { val taken = dir @@ -98,22 +56,14 @@ object SessionPaths { fun file(name: String): File? = dir?.let { File(it, name) } - /** Let the next session claim a new folder. Called when a session ends. */ @Synchronized fun release(context: Context, ended: File) { - // Only the session that owns the folder lets go of it (take() usually has already): the - // next session may have claimed its own by the time the last one's collector gets here. if (dir == ended) dir = null - // A folder kept in the cache was never meant to outlive its session. if (ended != null && ended.path.startsWith(context.cacheDir.path)) { com.droiddeck.launcher.core.FileUtils.delete(ended) } } - /** - * What a session's folder is named for: Steam, the desktop, or the program started from the - * rail by its own name (`2026-10-01-02-retroarch`). - */ fun label(mode: String, program: String?): String = when (mode) { SessionService.MODE_RUN -> com.droiddeck.launcher.frontend.Library.nameForProgram(program) ?: program?.substringAfterLast('/') @@ -122,21 +72,15 @@ object SessionPaths { else -> mode } - /** Whether [f] is a session's folder, in either naming. */ fun isSessionFolder(f: File): Boolean = f.isDirectory && (NAMED.matches(f.name) || LEGACY.matches(f.name)) - /** - * Oldest first: by day, then by when in the day. A folder from before the new naming sorts - * ahead of a new one from the same day, which it always predates. - */ val chronological: Comparator<File> = compareBy { f -> NAMED.matchEntire(f.name)?.groupValues?.let { g -> "${g[1]}${g[2]}${g[3]}1${g[4].padStart(6, '0')}" } ?: LEGACY.matchEntire(f.name)?.groupValues?.let { g -> "${g[1]}0${g[2]}${g[3].removePrefix("-").padStart(2, '0')}" } ?: f.name } - /** Every session folder in the places logs are written, oldest first. */ fun sessionFolders(context: Context): List<File> = (com.droiddeck.launcher.runtime.LinuxRuntime.logDir(context).listFiles { f -> isSessionFolder(f) }?.toList() ?: emptyList()) .sortedWith(chronological) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPipController.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPipController.kt index 92070761d..1b26e7de2 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPipController.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPipController.kt @@ -12,7 +12,6 @@ import android.widget.Toast import com.droiddeck.launcher.R import com.droiddeck.launcher.SessionActivity -/** Owns Android's PiP parameters, while the service continues to own the session. */ class SessionPipController( private val activity: SessionActivity, private val bounds: () -> Rect, diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index b941508d4..0d4b827fe 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -12,7 +12,6 @@ import com.droiddeck.launcher.gpu.Cardboard import com.droiddeck.launcher.gpu.ScreenEffects import org.json.JSONObject -/** The in-session switches: the HUD and how the on-screen controls decide to appear. */ object SessionPrefs { data class GameTextureFiltering(val anisotropy: Int? = null, val lodBias: String? = null) @@ -32,10 +31,8 @@ object SessionPrefs { const val OSC_ALWAYS = "always" const val OSC_STEAM_QAM = "steam-qam" const val OSC_NEVER = "never" - /** Steam's touch controller (SteamTouchControls) in place of the app's pad; Steam sessions only. */ const val OSC_STEAM_TOUCH = "steam-touch" - /** What Back does in a Steam session, first press then second: the labels of the two orders. */ val BACK_MENU_THEN_QAM = R.string.back_menu_then_qam val BACK_QAM_THEN_MENU = R.string.back_qam_then_menu @@ -61,7 +58,6 @@ object SessionPrefs { prefs(context).edit().putInt("orientation", orientation).apply() } - /** Whether the launcher hides Android's status and navigation bars. */ fun launcherFullscreen(context: Context): Boolean = prefs(context).getBoolean("launcherFullscreen", true) fun setLauncherFullscreen(context: Context, on: Boolean) { @@ -80,14 +76,12 @@ object SessionPrefs { prefs(context).edit().putBoolean("gameStoresEnabled", on).apply() } - /** The store download engine's speed tier (stores/download/StoreDownloadTier ids); Fast unless chosen otherwise. */ fun gameStoresSpeedTier(context: Context): String = prefs(context).getString("gameStoresSpeedTier", "fast") ?: "fast" fun setGameStoresSpeedTier(context: Context, tier: String) { prefs(context).edit().putString("gameStoresSpeedTier", tier).apply() } - /** Which tab a signed-in store opens on when its chip is picked: [STORES_OPEN_LIBRARY] or [STORES_OPEN_STORE]. */ fun storesOpenTab(context: Context): String = prefs(context).getString("storesOpenTab", STORES_OPEN_LIBRARY)?.takeIf { it == STORES_OPEN_STORE } ?: STORES_OPEN_LIBRARY @@ -95,7 +89,6 @@ object SessionPrefs { prefs(context).edit().putString("storesOpenTab", if (tab == STORES_OPEN_STORE) STORES_OPEN_STORE else STORES_OPEN_LIBRARY).apply() } - /** Mature titles (the stores' own 17+/18 ratings or adult tags) in the storefront's shelves and search; off by default. Owned games always show. */ fun storesShowMature(context: Context): Boolean = prefs(context).getBoolean("storesShowMature", false) fun setStoresShowMature(context: Context, show: Boolean) { @@ -105,25 +98,18 @@ object SessionPrefs { const val STORES_OPEN_LIBRARY = "library" const val STORES_OPEN_STORE = "store" - /** The install root picked last time the Stores asked where (a StoreInstallRoot target's path); only the dialog's default, never a silent choice. */ fun storesInstallTarget(context: Context): String = prefs(context).getString("storesInstallTarget", "") ?: "" fun setStoresInstallTarget(context: Context, path: String) { prefs(context).edit().putString("storesInstallTarget", path).apply() } - /** How many store downloads run at once, 1..3; one by default (a single download already fills the link). */ fun gameStoresParallel(context: Context): Int = prefs(context).getInt("gameStoresParallel", 1).coerceIn(1, 3) fun setGameStoresParallel(context: Context, count: Int) { prefs(context).edit().putInt("gameStoresParallel", count.coerceIn(1, 3)).apply() } - /** - * The session's performance HUD (the fps box). A Deck-mode Steam session with the performance - * overlay has Steam's own (mangoapp, from the QAM), so there the HUD is off unless turned on - * in one - a choice kept apart from every other session's, where it stays on by default. - */ fun hudEnabled(context: Context): Boolean = prefs(context).getBoolean(hudKey(context), !mangoappSession(context)) fun setHudEnabled(context: Context, on: Boolean) { @@ -132,11 +118,9 @@ object SessionPrefs { private fun hudKey(context: Context) = if (mangoappSession(context)) "hudDeck" else "hud" - /** A Steam session with Deck mode's performance overlay (mangoapp). */ private fun mangoappSession(context: Context) = SessionState.mode == SessionService.MODE_STEAM && mangoapp(context) - /** When enabled, a single Back opens Steam QAM and a double Back opens the session menu. */ fun backActionsInverted(context: Context): Boolean = prefs(context).getBoolean("backActionsInverted", false) fun setBackActionsInverted(context: Context, inverted: Boolean) { @@ -148,9 +132,6 @@ object SessionPrefs { const val TOUCH_DIRECT = "direct" const val TOUCH_OFF = "off" - /** How touch drives the pointer: a touchpad (drag moves it from where it is) or direct - * (it jumps under the finger). Auto = touchpad on the desktop, direct in Steam. - * Off ignores touches on the guest picture; Android controls remain usable. */ fun touchMode(context: Context): String = prefs(context).getString("touch", TOUCH_AUTO) ?: TOUCH_AUTO fun setTouchMode(context: Context, mode: String) { @@ -161,12 +142,6 @@ object SessionPrefs { const val SHAPE_WIDE = "16:9" const val SHAPE_EXACT = "exact" - /** - * The shape of the display the session presents: the panel's own (never narrower than 16:9), - * exactly the panel's (a 4:3 or 3:2 handheld, drawn edge to edge), or a fixed 16:9. A foldable defaults to 16:9, which sits with modest bars on either of its - * panels; the panel's own shape would fit one and leave a strip on the other, and gamescope's - * display cannot change size once the session is up. - */ private fun shapeMode(context: Context): String = prefs(context).getString("shape", null) ?: if (context.packageManager.hasSystemFeature("android.hardware.sensor.hinge_angle")) SHAPE_WIDE else SHAPE_AUTO @@ -177,12 +152,6 @@ object SessionPrefs { prefs(context).edit().putString("osc", mode).apply() } - /** - * The imported glibc Turnip every session draws with inside the runtime - Steam, its games and - * the desktop alike, as they run on the same GPU; "" = the driver built into the runtime. - * Resolved by LinuxVulkanDriver at session start. It was once chosen per mode: the Steam - * session's choice, the one nearly everyone set, carries over. - */ fun linuxDriver(context: Context): String = prefs(context).getString("linuxDriver", null) ?: prefs(context).getString("linuxDriver.steam", null) @@ -195,11 +164,6 @@ object SessionPrefs { const val GPU_DRIVERS_AUTO = "auto" const val GPU_DRIVERS_MANUAL = "manual" - /** - * Who picks the GPU drivers: [GPU_DRIVERS_AUTO] (the app, the matched pair recommended for - * this GPU, kept current - DriverPairs) or [GPU_DRIVERS_MANUAL] (the user). Auto for new - * installs; see settleGpuDriverMode. - */ fun gpuDriverMode(context: Context): String = prefs(context).getString("gpuDriverMode", GPU_DRIVERS_AUTO) ?: GPU_DRIVERS_AUTO @@ -207,10 +171,6 @@ object SessionPrefs { prefs(context).edit().putString("gpuDriverMode", mode).apply() } - /** - * Auto arrived after people had picked drivers by hand: an install that chose either driver - * keeps its choice (Manual), everyone else is Auto. Decided once, at process start. - */ fun settleGpuDriverMode(context: Context) { val p = prefs(context) if (p.contains("gpuDriverMode")) return @@ -218,7 +178,6 @@ object SessionPrefs { p.edit().putString("gpuDriverMode", if (chosen) GPU_DRIVERS_MANUAL else GPU_DRIVERS_AUTO).apply() } - /** Drivers Auto downloaded: the only ones it removes when a newer pair replaces them. */ fun gpuAutoInstalled(context: Context): Set<String> = prefs(context).getStringSet("gpuAutoInstalled", emptySet()).orEmpty() @@ -226,9 +185,6 @@ object SessionPrefs { prefs(context).edit().putStringSet("gpuAutoInstalled", ids.toSet()).apply() } - // The Steam client's own sound has one route now: DirectAudio's engine inside the PulseAudio - // daemon (PulseAudioComponent), one step from Android. The old "clientDirectAudio" boolean - // (which meant the relay route, one hop more) is no longer read. fun stretch16x9(context: Context): Boolean = prefs(context).getBoolean("stretch16x9", false) @@ -237,20 +193,14 @@ object SessionPrefs { } - /** - * DirectAudio for games: their Wine audio driver talks to the relay helper on this side. On - * unless the user turned it off. - */ fun directAudio(context: Context): Boolean = prefs(context).getBoolean("directAudio", true) fun setDirectAudio(context: Context, on: Boolean) { prefs(context).edit().putBoolean("directAudio", on).apply() } - /** The microphone for voice chat, off until the user turns it on (which asks for RECORD_AUDIO). */ fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", false) - /** Whether the app has already asked for the microphone once at start-up. */ fun micAsked(context: Context): Boolean = prefs(context).getBoolean("micAsked", false) fun setMicAsked(context: Context) { @@ -261,7 +211,6 @@ object SessionPrefs { prefs(context).edit().putBoolean("mic", on).apply() } - /** Optional SSID reporting and scans. A Location grant alone never opts the user in. */ fun wifiDiscoveryEnabled(context: Context): Boolean = prefs(context).getBoolean("wifiDiscovery", false) fun setWifiDiscoveryEnabled(context: Context, on: Boolean) { @@ -276,7 +225,6 @@ object SessionPrefs { internal data class CoreAllocation(val clientOverride: Boolean, val clientCpus: String, val gameCpus: String) - /** Defaults are read only for absent keys; even a saved empty/all-core choice wins. */ internal fun coreAllocation(context: Context, defaults: CpuAllocationDefaults.Allocation? = CpuAllocationDefaults.device): CoreAllocation { val saved = prefs(context) return CoreAllocation( @@ -286,37 +234,24 @@ object SessionPrefs { ) } - /** Use the device default until the user saves an override or a client mask. */ fun clientCpusOverride(context: Context): Boolean = coreAllocation(context).clientOverride fun setClientCpusOverride(context: Context, on: Boolean) { prefs(context).edit().putBoolean("clientCpusOverride", on).apply() } - /** Comma-separated core list for the client (taskset -c syntax); "" = every core. */ fun clientCpus(context: Context): String = coreAllocation(context).clientCpus fun setClientCpus(context: Context, list: String) { prefs(context).edit().putString("clientCpus", list).apply() } - /** Comma-separated core list for games (taskset -c syntax); "" = every core (explicitly restored when the client is pinned). */ fun gameCpus(context: Context): String = coreAllocation(context).gameCpus fun setGameCpus(context: Context, list: String) { prefs(context).edit().putString("gameCpus", list).apply() } - /** - * Whether Steam's xalia helper is kept out of the session (PROTON_USE_XALIA=0). - * - * xalia is an x86 Windows program Proton launches to give Windows programs gamepad navigation. - * Under FEX it cannot load the session's aarch64 preload shim, so its socket() and memfd calls - * reach the vendor's seccomp filter raw; where that answers ENOSYS - a Galaxy Fold, measured - - * it storms, and the session dies seconds after Big Picture appears. On by default: where it - * does run, it sits beside every game under FEX for nothing a controller-first session needs - * (about 10% of a core beside Once Upon a KATAMARI on an SD 8 Gen 2). - */ fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", true) fun setNoXalia(context: Context, on: Boolean) { @@ -340,10 +275,6 @@ object SessionPrefs { const val SYNC_FSYNC = "fsync" const val SYNC_WINESERVER = "wineserver" - /** - * The sync Proton games use, as the three switches above decide it: droiddeck-ntsync wins - * while it is on, then droiddeck-fsync first, then droiddeck-esync, and wineserver alone while all three are off. - */ fun syncBackend(context: Context): String = syncBackendOf(fastSync(context), fsyncFirst(context), syncFallback(context)) fun syncBackendOf(fastSync: Boolean, fsyncFirst: Boolean, syncFallback: Boolean): String = when { @@ -353,7 +284,6 @@ object SessionPrefs { else -> SYNC_WINESERVER } - /** Picks one sync for Proton games; the switches change together, in one write. */ fun setSyncBackend(context: Context, backend: String) { require(backend == SYNC_ESYNC || backend == SYNC_NTSYNC || backend == SYNC_FSYNC || backend == SYNC_WINESERVER) { "unknown sync $backend" } prefs(context).edit() @@ -369,28 +299,12 @@ object SessionPrefs { prefs(context).edit().putBoolean("syncFallback", on).apply() } - /** - * Whether gamescope asks for realtime-priority Vulkan queues (GAMESCOPE_FORCE_VULKAN_REALTIME=1, - * which the app's gamescope build honours without CAP_SYS_NICE). Off by default, as in - * Bannerlator's session: the compositor's queue preempting the game's buys nothing on a device - * whose GPU is waiting on the CPU. - */ fun gamescopeRealtime(context: Context): Boolean = prefs(context).getBoolean("gamescopeRealtime", false) fun setGamescopeRealtime(context: Context, on: Boolean) { prefs(context).edit().putBoolean("gamescopeRealtime", on).apply() } - /** - * Whether proot runs without its seccomp acceleration (PROOT_NO_SECCOMP=1). - * - * proot normally installs a seccomp filter so only the syscalls it must rewrite stop in the - * tracer; everything else runs untraced, which is most of proot's speed. Where a vendor kernel - * handles that filter badly the wrong calls are trapped or refused - ENOSYS from calls that - * plainly exist is the signature - and the fallback is to trace everything instead: slower, - * but correct. Max's advice for devices whose kernels "don't work well with it". - */ - /** Hold the GPU at its top clock during a session (GpuClockPin). Off by default: power and heat. */ fun gpuClockPin(context: Context): Boolean = prefs(context).getBoolean("gpuClockPin", false) fun setGpuClockPin(context: Context, on: Boolean) { @@ -403,11 +317,6 @@ object SessionPrefs { prefs(context).edit().putBoolean("prootNoSeccomp", on).apply() } - /** - * Whether the session's path lookups take proot's fast path (ProotFastPath): answered inside - * each process instead of a round trip through the tracer. On by default; it needs proot's - * seccomp filter, so it is off whenever proot runs without one. - */ fun prootFastPath(context: Context): Boolean = prefs(context).getBoolean("prootFastPath", true) fun setProotFastPath(context: Context, on: Boolean) { @@ -430,43 +339,21 @@ object SessionPrefs { return trimmed.takeIf { Regex("[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?").matches(it) } } - /** - * Turnip's sysmem rendering (TU_DEBUG=sysmem) for the runtime's driver: bypasses GMEM tiling. - * On by default, as WinNative runs every Linux session: Chromium -> ANGLE -> Zink draws the - * client's interface as many small render passes, each paying GMEM's load/store and binning, - * and WinNative's A/B on an Adreno 840 put a game ahead with it too (Palworld 42.8 against 41.3). - */ fun tuSysmem(context: Context): Boolean = prefs(context).getBoolean("tuSysmem", true) fun setTuSysmem(context: Context, on: Boolean) { prefs(context).edit().putBoolean("tuSysmem", on).apply() } - /* - * The client's interface is drawn Chromium -> ANGLE -> Zink -> Turnip, and that chain is what - * limits its menus (~14 fps on an Adreno 840 while a game ran 89). These make the chain - * cheaper rather than asking for more; the three environment switches are on by default, - * Deck mode off. Bannerlator's LinuxTuning, carried over; none device-proven here yet. - */ - /** mesa_glthread=true: GL marshalled off the calling thread. */ fun glThread(context: Context): Boolean = prefs(context).getBoolean("glThread", true) fun setGlThread(context: Context, on: Boolean) { prefs(context).edit().putBoolean("glThread", on).apply() } - /** MESA_NO_ERROR=1: no GL error checking. */ fun noGlError(context: Context): Boolean = prefs(context).getBoolean("noGlError", true) fun setNoGlError(context: Context, on: Boolean) { prefs(context).edit().putBoolean("noGlError", on).apply() } - /** - * What the pad is to the Steam client: [CONTROLLER_DECK], a Steam Deck controller (Quick Access - * button, gyro, Steam Input's full treatment - SteamDeckPad), [CONTROLLER_STEAM], a 2025 Steam - * Controller (the same, plus grip sense and its own haptics), [CONTROLLER_DS4] or - * [CONTROLLER_DUALSENSE], a PlayStation pad with its touchpad fed by the trackpads, or - * [CONTROLLER_XBOX360], the plain Xbox 360 pad of earlier versions (QAM by the Guide+A chord). - */ fun steamController(context: Context): String = prefs(context).getString("steamController", CONTROLLER_DECK) ?: CONTROLLER_DECK fun setSteamController(context: Context, id: String) { prefs(context).edit().putString("steamController", id).apply() } - /** [steamController]'s choice as libfakeinput's pad type (FakeInputWriter.PAD_TYPE_*). */ fun padType(id: String): Int = when (id) { CONTROLLER_STEAM -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_STEAM CONTROLLER_XBOX360 -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_XBOX @@ -475,24 +362,15 @@ object SessionPrefs { else -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DECK } - /** - * Deck mode's performance overlay (mangoapp, beside gamescope): the QAM's Overlay Level draws - * through it. Off is the way out where Valve's mangoapp crashes (one Turnip build did). - */ fun mangoapp(context: Context): Boolean = prefs(context).getBoolean("mangoapp", true) fun setMangoapp(context: Context, on: Boolean) { prefs(context).edit().putBoolean("mangoapp", on).apply() } - /** - * Zink's lazy descriptor mode (ZINK_DESCRIPTORS=lazy) with its compact set layout - * (ZINK_DEBUG=compact) for the client's GL-on-Vulkan UI, as WinNative runs it. On by default. - */ fun zinkLazy(context: Context): Boolean = prefs(context).getBoolean("zinkLazy", true) fun setZinkLazy(context: Context, on: Boolean) { prefs(context).edit().putBoolean("zinkLazy", on).apply() } - /** The Android driver the compositor loads: "" = pick by GPU, else a bundled or imported id. */ @JvmStatic fun androidDriver(context: Context): String = prefs(context).getString("androidDriver", "") ?: "" @@ -501,12 +379,7 @@ object SessionPrefs { prefs(context).edit().putString("androidDriver", id).apply() } - // ── Storage the session can see ───────────────────────────────────────────────────────────── - /** - * The folder on this device that every session shows at `/root/ROMs`, for the emulators on the - * desktop. "" = none chosen. All of internal storage is at `/root/Storage` regardless. - */ fun romsDir(context: Context): String = prefs(context).getString("romsDir", "") ?: "" @@ -514,11 +387,6 @@ object SessionPrefs { prefs(context).edit().putString("romsDir", path).apply() } - /** - * Whether a session keeps its folder under files/logs. Off, the same logs are kept in - * the app's cache for the session's lifetime (the scripts need somewhere to write) and thrown - * away at the end, so nothing accumulates in Downloads. - */ fun logsEnabled(context: Context): Boolean = prefs(context).getBoolean("logs", true) @@ -526,7 +394,6 @@ object SessionPrefs { prefs(context).edit().putBoolean("logs", on).apply() } - /** Steam storage-call diagnostics are opt-in because they add timing work to file operations. */ fun storageDiagnosticsEnabled(context: Context): Boolean = prefs(context).getBoolean("storageDiagnostics", false) @@ -534,24 +401,13 @@ object SessionPrefs { prefs(context).edit().putBoolean("storageDiagnostics", on).apply() } - // ── Per-mode display ──────────────────────────────────────────────────────────────────── - /** - * The tallest the session's display may be, in pixels, for MODE_STEAM / MODE_DESKTOP: - * 0 = the panel's own height, otherwise a cap. Both modes default to 720p: the client's CEF - * is the heaviest thing in a session, Big Picture is drawn for a TV at arm's length, and on - * a handheld panel 720p is where its menus stay responsive on a regular flagship; the desktop - * and the emulators under it get the same GPU headroom. Read once, when the session's display - * is sized; a cap the user chose wins over the default. - */ private fun resolutionCap(context: Context, mode: String): Int = prefs(context).getInt("resolutionCap.$mode", 720) - /** Whether the user chose the mode's resolution (a cap or a custom size) rather than the default. */ fun resolutionChosen(context: Context, mode: String): Boolean = prefs(context).contains("displayResolution.$mode") || prefs(context).contains("resolutionCap.$mode") || customResolution(context, mode) != null - /** One per-mode choice now owns both dimensions; old caps/shapes are read only for migration. */ fun resolutionChoice(context: Context, mode: String, panel: Pair<Int, Int>): String { val saved = prefs(context) saved.getString("displayResolution.$mode", null)?.let { value -> @@ -574,24 +430,10 @@ object SessionPrefs { prefs(context).edit().putString("displayResolution.$mode", value).apply() } - /** - * The FEXCore preset for the games the client launches (core/FexPreset ids); "" = FEX's defaults. - * Performance + TSO unless chosen, WinNative's default: FEX's own defaults keep half-barrier TSO - * and full-precision x87, which cost every x86 game time; a game that needs them can still be - * given another preset. - */ fun fexPreset(context: Context): String = prefs(context).getString("fexPreset", DEFAULT_FEX_PRESET) ?: DEFAULT_FEX_PRESET private const val DEFAULT_FEX_PRESET = "PERFORMANCE_TSO" - /** - * Force SSBS for Proton games: Wine resumes threads from a Windows CONTEXT that never carries - * PSTATE.SSBS, so they run with speculative store bypass disabled; libssbs.so keeps it set - * (on DiRT 3 / GE-Proton: from ~99% of a game's threads running without it to none). The - * speed-up is reported on Oryon cores (Snapdragon 8 Elite) and was not measurable on an - * 8 Gen 3, so it is off unless turned on. A game's own environment can still say - * DROIDDECK_FORCE_SSBS=0. - */ fun forceSsbs(context: Context): Boolean = prefs(context).getBoolean("forceSsbs", false) fun setForceSsbs(context: Context, on: Boolean) { @@ -606,18 +448,12 @@ object SessionPrefs { .onFailure { android.util.Log.e("GameEnvironment", "Could not update game environment", it) } } - /** Whether opening DroidDeck starts a Steam session instead of showing the front end. */ fun runSteamAtStartup(context: Context): Boolean = prefs(context).getBoolean("runSteamAtStartup", false) fun setRunSteamAtStartup(context: Context, on: Boolean) { prefs(context).edit().putBoolean("runSteamAtStartup", on).apply() } - /** - * The Games folders earlier builds let the user add (one game per subfolder); read only by - * GamesFolderMigration, which turns them into entries and clears them. The single folder an - * even earlier build kept is carried in. - */ fun addedGamesDirs(context: Context): List<String> { val p = prefs(context) val list = p.getString("addedGamesDirs", null) @@ -630,10 +466,6 @@ object SessionPrefs { prefs(context).edit().putString("addedGamesDirs", dirs.distinct().joinToString("\n")).remove("addedGamesDir").apply() } - /** - * The Games folders earlier builds had (host path, guest path), still bound into the session so - * their games keep their paths; written once by GamesFolderMigration. - */ fun gameFolderBinds(context: Context): List<Pair<String, String>> = (prefs(context).getString("gameFolderBinds", "") ?: "").split('\n').mapNotNull { line -> line.split('\t').takeIf { it.size == 2 && it[0].isNotEmpty() && it[1].isNotEmpty() }?.let { it[0] to it[1] } @@ -643,17 +475,12 @@ object SessionPrefs { prefs(context).edit().putString("gameFolderBinds", binds.distinctBy { it.first }.joinToString("\n") { "${it.first}\t${it.second}" }).apply() } - /** The Games folders were turned into entries (GamesFolderMigration); a record, not a gate. */ fun gamesFoldersMigrated(context: Context): Boolean = prefs(context).getBoolean("gamesFoldersMigrated", false) fun setGamesFoldersMigrated(context: Context) { prefs(context).edit().putBoolean("gamesFoldersMigrated", true).apply() } - /** - * Game folders (by path) the user removed from the added games: a scan skips them, so they - * stay off the Games tab and out of Steam until restored. Nothing on disk is touched. - */ fun removedAddedGames(context: Context): List<String> = (prefs(context).getString("removedAddedGames", "") ?: "").split('\n').filter { it.isNotEmpty() } @@ -664,31 +491,24 @@ object SessionPrefs { } } - /** An added game's name as the user set it (in the app, or in Steam's Properties); "" = automatic. */ fun addedGameName(context: Context, folderPath: String): String = prefs(context).getString("addedName:$folderPath", "") ?: "" fun setAddedGameName(context: Context, folderPath: String, name: String) { prefs(context).edit().putString("addedName:$folderPath", name.trim()).apply() } - /** An added game's Start in, as the session sees it; "" = the exe's own folder. */ fun addedGameStartIn(context: Context, folderPath: String): String = prefs(context).getString("addedStartIn:$folderPath", "") ?: "" fun setAddedGameStartIn(context: Context, folderPath: String, guestDir: String) { prefs(context).edit().putString("addedStartIn:$folderPath", guestDir.trim()).apply() } - /** An added game's launch options, as Steam takes them ("-dx11", "VAR=1 %command%"). */ fun addedGameLaunch(context: Context, folderPath: String): String = prefs(context).getString("addedLaunch:$folderPath", "") ?: "" fun setAddedGameLaunch(context: Context, folderPath: String, options: String) { prefs(context).edit().putString("addedLaunch:$folderPath", options.trim()).apply() } - /** - * The last value of [field] (AppName, StartDir, LaunchOptions) taken over from Steam's own - * Properties for this game, so the same Steam edit is not taken again over a later app edit. - */ fun addedGameAdopted(context: Context, folderPath: String, field: String): String? = prefs(context).getString("addedAdopted:$field:$folderPath", null) @@ -696,7 +516,6 @@ object SessionPrefs { prefs(context).edit().putString("addedAdopted:$field:$folderPath", value).apply() } - /** Where an added game's art for [slot] comes from when the user chose it ("folder", "steam", "sgdb", "file"); "" = automatic. */ fun addedGameArtSource(context: Context, folderPath: String, slot: String): String = prefs(context).getString("addedArt:$slot:$folderPath", "") ?: "" @@ -704,14 +523,12 @@ object SessionPrefs { prefs(context).edit().putString("addedArt:$slot:$folderPath", source).apply() } - /** Whether added games without art of their own get Steam's store art fetched for them. */ fun addedGamesArt(context: Context): Boolean = prefs(context).getBoolean("addedGamesArt", true) fun setAddedGamesArt(context: Context, on: Boolean) { prefs(context).edit().putBoolean("addedGamesArt", on).apply() } - /** The .exe the user chose for one game folder (by its path), "" = the scanner's pick. */ fun addedGameExe(context: Context, folderPath: String): String = prefs(context).getString("addedExe:$folderPath", "") ?: "" private val addedGameLock = Any() @@ -729,7 +546,6 @@ object SessionPrefs { unchanged } - /** The shortcut appid stored for the folder, or null before its first scan. */ fun addedGameStoredAppId(context: Context, folderPath: String): Long? = prefs(context).getLong("addedAppId:$folderPath", 0L).takeIf { it != 0L } @@ -739,21 +555,15 @@ object SessionPrefs { ?: first.also { p.edit().putLong("addedAppId:$folderPath", it).apply() } } - /** The app's colour theme (ui/Themes ids); Graphite unless chosen otherwise. */ fun theme(context: Context): String = prefs(context).getString("theme", "graphite") ?: "graphite" fun setTheme(context: Context, id: String) { prefs(context).edit().putString("theme", id).apply() } - /** - * A fixed size for the session's display, per mode, or null. When set it replaces both the - * cap and the shape: the compositor fits it to the panel with bars where the shapes differ. - */ private fun customResolution(context: Context, mode: String): Pair<Int, Int>? = parseResolution(prefs(context).getString("customRes.$mode", null)) - /** "1024x768" (or ×, or *) to an even size inside 320x240..3840x2160; anything else is null. */ fun parseResolution(text: String?): Pair<Int, Int>? { val parts = text?.trim()?.split('x', 'X', '×', '*')?.map { it.trim() } ?: return null if (parts.size != 2) return null @@ -763,25 +573,12 @@ object SessionPrefs { return Pair(w and 1.inv(), h and 1.inv()) } - /** - * HDR10 output for MODE_STEAM / MODE_DESKTOP. Off by default. Honoured only when the panel - * lists HDR10 (HdrSupport), and decided when the compositor starts, which is once per app - * process: a change applies after the app is fully closed and opened again. - */ fun hdr(context: Context, mode: String): Boolean = prefs(context).getBoolean("hdr.$mode", false) fun setHdr(context: Context, mode: String, on: Boolean) { prefs(context).edit().putBoolean("hdr.$mode", on).apply() } - /** - * The session's frame cap, 0 for none. One number used everywhere a frame is paced: gamescope's - * -r (what the client and its games see as the display's rate), the compositor's buffer release - * pacer, the rate the display layer votes for, and the panel mode picked, which is the fastest - * one the cap divides evenly (40 on a 120 Hz panel, not on a 144 Hz one). A 60 fps cap on a - * 144 Hz panel with nothing else changed judders; this is what WinNative's per-shortcut limit - * does. Applies next session. - */ fun fpsLimit(context: Context, mode: String): Int = prefs(context).getInt("fpsLimit.$mode", 0) fun setFpsLimit(context: Context, mode: String, fps: Int) { @@ -790,28 +587,19 @@ object SessionPrefs { val fpsLimitChoices = listOf(0 to "Off", 30 to "30", 40 to "40", 45 to "45", 60 to "60", 90 to "90", 120 to "120") - /** [fpsLimitChoices] labelled in the app's language. */ fun fpsLimitChoices(context: Context): List<Pair<Int, String>> = fpsLimitChoices.map { (fps, label) -> fps to if (fps == 0) context.getString(R.string.frame_gen_off) else label } - /** - * How the compositor resizes the session onto the panel (WaylandCompositor.nativeSetUpscaler's - * modes). Linear is the default; Nearest preserves hard pixel edges. Spatial filters work - * when enlarged; Sharpen only works at any size. The old Off/Linear and FSR/FSR Fit pairs - * are equivalent on Wayland, so saved aliases resolve to one choice. - */ val upscalerChoices = listOf( 0 to "Linear", 2 to "Nearest", 4 to "AMD FSR 1", 3 to "Snapdragon GSR", 8 to "Snapdragon GSR (quality)", 7 to "NVIDIA NIS", 6 to "Sharpen only", ) - /** The [upscalerChoices] labels that are words rather than product names. */ private val upscalerLabels = mapOf( 0 to R.string.sprefs_upscaler_linear, 2 to R.string.sprefs_upscaler_nearest, 8 to R.string.sprefs_upscaler_gsr_quality, 6 to R.string.sprefs_upscaler_sharpen, ) - /** [upscalerChoices] labelled in the app's language; the English list stays for the device report. */ fun upscalerChoices(context: Context): List<Pair<Int, String>> = upscalerChoices.map { (mode, label) -> mode to (upscalerLabels[mode]?.let(context::getString) ?: label) } @@ -835,9 +623,7 @@ object SessionPrefs { prefs(context).edit().putInt("upscaleSharpness", pct.coerceIn(0, 100)).apply() } - // ── Screen effects and texture filtering (the Display page) ───────────────────────────── - /** The compositor's post chain as last set; off until the user picks a Look or moves a row. */ fun screenEffects(context: Context): ScreenEffects { val text = prefs(context).getString("screenEffects", null) ?: return ScreenEffects.OFF return runCatching { ScreenEffects.decode(JSONObject(text)) }.getOrDefault(ScreenEffects.OFF) @@ -847,10 +633,8 @@ object SessionPrefs { prefs(context).edit().putString("screenEffects", effects.encode().toString()).apply() } - /** The key [cardboard] lives under; SessionActivity watches it so a change applies live. */ const val CARDBOARD_KEY = "cardboard" - /** Cardboard mode (gpu/Cardboard) as last set; off by default. */ fun cardboard(context: Context): Cardboard { val text = prefs(context).getString(CARDBOARD_KEY, null) ?: return Cardboard.OFF return runCatching { Cardboard.decode(JSONObject(text)) }.getOrDefault(Cardboard.OFF) @@ -860,21 +644,16 @@ object SessionPrefs { prefs(context).edit().putString(CARDBOARD_KEY, cardboard.encode().toString()).apply() } - /** The Cardboard picture (Cardboard.SOURCE_*) to go back to once SteamVR stops; -1 = none (it was - * not switched for SteamVR). Kept across activity restarts, so a SteamVR session that outlives the - * activity still hands the picture back. */ fun cardboardSourceBeforeSteamVr(context: Context): Int = prefs(context).getInt("cardboardSourceBeforeSteamVr", -1) fun setCardboardSourceBeforeSteamVr(context: Context, source: Int) { prefs(context).edit().putInt("cardboardSourceBeforeSteamVr", source).apply() } - /** The prefs file [cardboard] lives in, for a change listener. */ fun sessionPrefs(context: Context): android.content.SharedPreferences = prefs(context) val textureAnisotropyChoices = TextureFiltering.ANISOTROPY.map { it to if (it == 0) "Off" else "${it}x" } - /** [textureAnisotropyChoices] labelled in the app's language. */ fun textureAnisotropyChoices(context: Context): List<Pair<Int, String>> = textureAnisotropyChoices.map { (value, label) -> value to if (value == 0) context.getString(R.string.frame_gen_off) else label } @@ -886,7 +665,6 @@ object SessionPrefs { } } - /** [textureLodBiasChoices] labelled in the app's language. */ fun textureLodBiasChoices(context: Context): List<Pair<String, String>> = textureLodBiasChoices.map { (value, label) -> value to when (value) { TextureFiltering.LOD_BIAS_OFF -> context.getString(R.string.frame_gen_off) @@ -895,7 +673,6 @@ object SessionPrefs { } } - /** Anisotropic filtering forced on DirectX 9-11 games (core/TextureFiltering); 0 = the game's own. */ fun textureAnisotropy(context: Context): Int = prefs(context).getInt("textureAnisotropy", 0).takeIf { it in TextureFiltering.ANISOTROPY } ?: 0 @@ -904,7 +681,6 @@ object SessionPrefs { publishGameEnvironment(context) } - /** Texture sharpness: the mip LOD bias choice (TextureFiltering.LOD_BIAS), "0" = the game's own. */ fun textureLodBias(context: Context): String = prefs(context).getString("textureLodBias", null)?.takeIf { it in TextureFiltering.LOD_BIAS } ?: TextureFiltering.LOD_BIAS_OFF @@ -981,16 +757,11 @@ object SessionPrefs { prefs(context).edit().putString("gameTextureFiltering", json.toString()).apply() } - /** Hands the change to the next game launch (GameEnvironmentStore); the running game keeps its own. */ private fun publishGameEnvironment(context: Context) { runCatching { GameEnvironmentStore.publish(context) } .onFailure { android.util.Log.e("GameEnvironment", "Could not update game environment", it) } } - /** - * The mode whose per-mode settings apply: a program run under gamescope (MODE_RUN) is a - * fullscreen session like Steam's, so it takes Steam's display, driver and HDR choices. - */ fun prefMode(mode: String): String = if (mode == SessionService.MODE_RUN) SessionService.MODE_STEAM else mode fun pipAutoEnter(context: Context): Boolean = prefs(context).getBoolean("pipAutoEnter", false) @@ -999,7 +770,6 @@ object SessionPrefs { prefs(context).edit().putBoolean("pipAutoEnter", enabled).apply() } - /** A foldable's unfolded panel shows the game above the Deck controls (SessionDisplay.unfoldedGameHeight). */ fun unfoldedControls(context: Context): Boolean = prefs(context).getBoolean("unfoldedControls", false) fun setUnfoldedControls(context: Context, enabled: Boolean) { @@ -1009,8 +779,6 @@ object SessionPrefs { const val SURFACE_STICK = "stick" const val SURFACE_TRACKPAD = "trackpad" - /** The Deck controls' gamepad tab: what each side is - an analog stick or a Steam trackpad. - * Left a stick and right a trackpad until changed, as a Deck is mostly played. */ fun gamepadSurface(context: Context, right: Boolean): String = prefs(context).getString(if (right) "gamepadRight" else "gamepadLeft", null) ?: if (right) SURFACE_TRACKPAD else SURFACE_STICK @@ -1023,7 +791,6 @@ object SessionPrefs { prefs(context).getString("suspendPolicy.${prefMode(mode)}", SUSPEND_MANUAL) ?.let { if (it == LEGACY_SUSPEND_DOWNLOADS) SUSPEND_AUTO else it } ?.takeIf { it in setOf(SUSPEND_AUTO, SUSPEND_NATIVE, SUSPEND_MANUAL, SUSPEND_NEVER) } - // Direct games share Steam's settings but have no Steam client to prepare. ?.let { if (mode != SessionService.MODE_STEAM && it == SUSPEND_NATIVE) SUSPEND_AUTO else it } ?: SUSPEND_MANUAL @@ -1058,7 +825,6 @@ object SessionPrefs { }.apply() } - /** Steam only: the client's memory goes to the device's zram while a game runs (ZramSupport). */ fun zramCompression(context: Context): Boolean { val prefs = prefs(context) return if (prefs.contains("zramCompression")) prefs.getBoolean("zramCompression", false) else ZramSupport.recommended(context) @@ -1068,15 +834,7 @@ object SessionPrefs { prefs(context).edit().putBoolean("zramCompression", on).apply() } - // ── Game storage ──────────────────────────────────────────────────────────────────────── - /** - * A second Steam library on this device: the folder bound at /mnt/droiddeck-sd and - * registered with the client, which then asks where to install every game and shows both - * on its Storage page. "" = automatic: the SD card when one is in the phone (the default, - * so the choice is made inside the client like anywhere else); GAME_STORAGE_OFF = internal - * only; otherwise the folder chosen. - */ fun gameStorage(context: Context): String = prefs(context).getString("gameStorage", "") ?: "" const val GAME_STORAGE_OFF = "off" diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index f068b8f26..1de3c745c 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -50,32 +50,16 @@ import java.util.Date import java.util.Locale import java.util.UUID -/** - * Owns the running session: the proot tree, the audio daemon, the network link and the locks that - * keep all three alive while the app is not on screen. - * - * The session deliberately does **not** belong to the activity. Android demotes a process the - * moment it loses its last visible activity, and the low-memory killer then reaps the guest - so - * leaving Big Picture to answer a message would come back to a dead Steam. A foreground service - * holds the process at perceptible priority, a partial wake lock keeps the CPU from dropping the - * guest's threads, and a high-performance WiFi lock keeps the radio out of power-save so a - * backgrounded download does not throttle to nothing. All three are Bannerlator's recipe, where - * each was added to fix a failure seen on a device. - * - * The activity comes and goes on top of this; see [com.droiddeck.launcher.wayland.CompositorHost]. - */ class SessionService : Service() { override fun attachBaseContext(newBase: android.content.Context) = super.attachBaseContext(com.droiddeck.launcher.core.AppLanguage.wrap(newBase)) private val components = java.util.concurrent.CopyOnWriteArrayList<SessionPart>() - /** The Steam Deck controller's sysfs binds (SteamDeckPad), when this session has one. */ private var deckBinds: List<String> = emptyList() private val stopLock = Any() private var wakeLock: PowerManager.WakeLock? = null private var wifiLock: WifiManager.WifiLock? = null private var sessionPid = -1 - /** Auxiliary PTY-backed proot shells launched from a secondary-display terminal. */ private val auxiliaryProcesses = HashMap<Int, Long>() private val auxiliaryProcessesLock = Any() private val mainHandler = Handler(Looper.getMainLooper()) @@ -89,7 +73,6 @@ class SessionService : Service() { private var activityVisible = true private var screenOn = true private var manualPauseRequested = false - /** An explicit Steam sleep request is independent of the background policy. */ private var steamSleepToken: String? = null private var nativeSleepToken: String? = null private var nativeSleepReady = false @@ -98,7 +81,6 @@ class SessionService : Service() { private var suspendOperationPending = false private var pipTask = false private var suspendAttemptFailed = false - /** One validated game request waiting for this Steam session to reach a usable state. */ private var pendingSteamGameId: String? = null private var pendingSteamGameGeneration = -1 private val flushSteamGame = object : Runnable { @@ -117,7 +99,6 @@ class SessionService : Service() { updateSuspendPolicy() } } - /** Counts sessions this service has started; a process exit from an earlier one is ignored. */ @Volatile private var sessionGen = 0 override fun onCreate() { @@ -246,47 +227,26 @@ class SessionService : Service() { stopSession(0) return START_NOT_STICKY } - // Another Steam client on the device signs ours out seconds after every login; the one that - // does it here runs from boot without being opened. Only the Steam session signs in. - // The desktop too: its Steam launchers run the client right there (droiddeck-steam-launch). if (SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP) RivalClients.stopBeforeSession(this) SessionState.firstFrameSeen = false SessionState.guestPid = -1 SessionEvents.transition(SessionPhase.STARTING_GUEST, "service.started", mapOf("mode" to SessionState.mode)) - // Re-arm after clearing readiness: a retained picture can present between the activity's - // start request and this callback, and that notice must not be erased by the reset above. com.droiddeck.launcher.wayland.CompositorHost.newSession() - // This session's number, claimed here and not when its process starts: the session it - // replaces can report its own exit in the gap between the two, and that exit must not - // be taken as this one's. val gen = ++sessionGen cancelQueuedSteamGame(clearMailbox = false) clearSteamGameMailbox() acquireLocks() Thread({ - // A tree the last session left behind (the app was killed or crashed, so its teardown - // never ran) would hold the rootfs, the GPU and Steam's lock: nothing of ours should - // be alive between sessions outside this process. OrphanReaper.reap("session starting") runSession(gen) }, "session-start").start() - // The activity or the notification stops us; the system must not resurrect a session whose - // guest processes are long gone. return START_NOT_STICKY } - /** - * Finish the session's folder after the session has stopped, then let go of it. The guest - * script copies Steam's logs too, at a clean exit; this runs whatever killed the session, - * which is when they matter. The collecting itself is [SessionArtifacts], shared with the - * crash handler and the next-start sweep so a folder is finished whichever way it ends. - */ private fun collectSessionArtifacts(dir: File) { try { SessionArtifacts.collect(this, dir, "session stopped") } finally { - // Last, so everything above is in the file it is about - and only this session's: - // a session that replaced this one may already own the capture and the folder. SessionLogCapture.stopFor(dir) SessionPaths.release(this, dir) } @@ -315,7 +275,6 @@ class SessionService : Service() { return if (name.contains("710-720") || name.contains("710_720")) "sysmem" else null } - // ── The session ───────────────────────────────────────────────────────────────────────── private fun runSession(gen: Int) { SessionTerminal.clear() @@ -333,7 +292,6 @@ class SessionService : Service() { killStragglers() SessionFiles.stage(this, root) com.droiddeck.launcher.agent.AgentGuest.reset(this) - // The compat tool asks for an Epic game's sign-in code at every launch, the client's Play button included. com.droiddeck.launcher.stores.gog.GogComet.begin() com.droiddeck.launcher.stores.StoreLaunchRequests.start(this) @@ -344,31 +302,21 @@ class SessionService : Service() { val size = SessionState.outputSize val guest = ArrayList<String>() - // The desktop's Steam launchers start the client there (droiddeck-steam-launch), through the - // same set-up as a Steam session: it gets what the client and its games are started with. val steamHere = SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP - // Some GPUs need a component Proton does not ship. Apply those before the guest, so the - // next game launch copies them into the prefix. A download failure is logged and the - // session still starts; the next session tries again. if (steamHere) { runCatching { ComponentsManager.ensureForcedPackages(this) } .onSuccess { if (it != null) Log.i(TAG, it) } .onFailure { Log.w(TAG, "forced components", it) } } addClientEnvironment(guest, steamHere) - // Where the fast path's description of proot's view goes, once the binds are known. val fastPathAt = guest.size val pulse = startAudio(guest, sessionDir) guest.add("BL_WIDTH=" + size.first) guest.add("BL_HEIGHT=" + size.second) - // Follow screen: the output is resized under gamescope when a foldable opens or closes, - // and the session script asks gamescope to resize Steam's X screen along with it. if (SessionState.followScreen) guest.add("BL_FOLLOW_OUTPUT=1") if (SessionState.hdr) { - // The activity opened the compositor's HDR gate: gamescope offers HDR to its clients - // and DXVK takes the HDR10 swapchain when a game asks for one. guest.add("BL_HDR=1") guest.add("DXVK_HDR=1") Log.i(TAG, "hdr: gamescope --hdr-enabled, DXVK_HDR=1") @@ -384,12 +332,8 @@ class SessionService : Service() { deckBinds = emptyList() if (controllersOn) { addControllerEnvironment(guest, fakeInputDir, sessionDir) - // Steam enumerates /dev/input while it starts. Make every attached controller's - // discovery node exist after the rings and guest environment are ready. SessionState.padBridge?.syncConnectedControllers() } - // The user's own games, for the runtime's shortcuts writer to put in the client's library - // before the client starts (see frontend/AddedGames and droiddeck-steam-shortcuts). if (steamHere) { val added = com.droiddeck.launcher.frontend.AddedGames.scan(this) val listing = com.droiddeck.launcher.frontend.AddedGames.writeListing(this, added) @@ -398,9 +342,7 @@ class SessionService : Service() { if (com.droiddeck.launcher.gpu.Lossless.owned(this)) guest.add("BL_LOSSLESS_OWNED=1") if (added.isNotEmpty()) Log.i(TAG, "added games: " + added.joinToString { "${it.name} (${it.exe.name})" }) } - // Where the guest leaves a request for another session (the desktop's Steam launchers). guest.add("BL_LAUNCH_DIR=" + sessionRoot.path) - // The second library's name, for droiddeck-steam-library; the bind itself is made below. GameStorage.effective(this)?.let { guest.add("BL_LIBRARY_LABEL=" + it.label.replace('"', ' ')) } if (SessionState.mode == MODE_STEAM && SessionState.steamUi == "desktop") guest.add("BL_STEAM_UI=desktop") val shellGuest = ArrayList(guest).apply { @@ -416,13 +358,7 @@ class SessionService : Service() { guest.add(it) Log.i(TAG, "steam: handing the client $it") } - // A game whose download finishes in this session gets its Windows components readied in it. if (SessionState.mode == MODE_STEAM) com.droiddeck.launcher.frontend.SessionInstalls.watch(this) - // A program under gamescope: the script's run mode takes the path (an AppImage, a script - // or a binary inside the runtime). This is how an emulator gets the GPU - the desktop's - // KWin composites in software and offers no dma-buf, so a Vulkan swapchain cannot exist - // there (RPCS3 died with VK_ERROR_SURFACE_LOST); gamescope's Xwayland is the path the - // Steam games already render through. if (SessionState.mode == MODE_RUN) { val program = SessionState.program if (program.isNullOrEmpty()) { @@ -435,12 +371,9 @@ class SessionService : Service() { Log.i(TAG, "run: $program (${SessionState.programArgs.size} arguments) under gamescope") } - // Android has no /dev/shm; the cache stands in for it and, unlike the real thing, keeps - // whatever a session leaves behind. The client abandons tens of megabytes of streams a run. FileUtils.clear(File(cacheDir, "shm")) val binds = sessionBinds(controllersOn, fakeInputDir, sessionDir, guest, sessionRoot, runtimeDir) - // The fast path is told exactly the rootfs and binds proot is given (ProotFastPath). val fastPathKey = if (ProotFastPath.enabled(this)) { val prootBinds = LinuxRuntime.binds( this, sessionRoot, runtimeDir, Environment.getExternalStorageDirectory(), binds, @@ -464,10 +397,6 @@ class SessionService : Service() { val hostEnv = HostEnvironment() hostEnv["PROOT_LOADER"] = LinuxRuntime.prootLoader(this).path hostEnv["PROOT_TMP_DIR"] = cacheDir.path - // proot links against a libtalloc beside it, and Android's linker does not search an - // executable's own directory: unnamed, the process dies before it starts and says so only - // in `logcat -b crash`. - // proot reads this itself, so it belongs in proot's own environment rather than the guest's. if (SessionPrefs.prootNoSeccomp(this)) { hostEnv["PROOT_NO_SECCOMP"] = "1" Log.i(TAG, "proot: seccomp acceleration off by request") @@ -488,8 +417,6 @@ class SessionService : Service() { SessionTerminal.prepare(terminalCommand, terminalHostEnvironment.map { (key, value) -> "$key=$value" }.toTypedArray(), root, gen) } - // Whether the client signs in to Valve or starts offline: read once, while it starts, and - // rewritten by the client when it exits, so it is set again here at every session start. if (SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP) OfflineMode.apply(this, root) val networkLink = LinuxNetworkLinkComponent(this, root) @@ -509,10 +436,6 @@ class SessionService : Service() { watchLaunchRequests(sessionRoot) watchSyncWanted(root) EsyncPacks.fetchInBackground(this, root) - // One session replacing another (the desktop's Steam launchers): the old proot is killed - // by the teardown a second after the new one has started, and its exit used to arrive - // here as "session ended: 137" and end the NEW session. An exit belongs to the session - // that started it. val pid = HostProcess.start(line, hostEnv.asArray(), root, { status -> if (gen != sessionGen) { Log.i(TAG, "an earlier session's process ended ($status); the current one carries on") @@ -561,10 +484,7 @@ class SessionService : Service() { } } - /** The session's own log folder, opened and filled with what is known before anything starts. */ private fun openSessionFolder(): File { - // One folder per session, claimed by whoever started first - the activity starts the - // compositor before this service runs - so the compositor's log lands in the same place. val sessionDir = SessionPaths.beginOrCurrent(this) val sessionLog = File(sessionDir, "session.log") SessionState.logFile = sessionLog @@ -572,155 +492,91 @@ class SessionService : Service() { SessionState.sessionId = sessionDir.name SessionState.eventsFile = File(sessionDir, "events.jsonl") SessionEvents.record("session.logs_ready", mapOf("logDir" to sessionDir.path)) - // Written first, so a session that dies in its first second still says what it ran on. DeviceReport.write(this, File(sessionDir, "device.txt"), SessionState.mode) NetworkReport.write(this, File(sessionDir, "network.txt")) - // Everything the app decides from here on - the driver it chose, the audio line, a rival - // client stopped, the exit status - reaches logcat and nowhere a user can get at. Mirror it. SessionLogCapture.start(File(sessionDir, "app.log")) return sessionDir } - /** The guest's base environment: paths, the display, the GL/Vulkan stack and the client's switches. */ private fun addClientEnvironment(guest: MutableList<String>, steamHere: Boolean) { guest.add("/usr/bin/env") guest.add("-i") - // PRoot executes guest[0] directly; assignments belong after env -i. guest.add("DROIDDECK_BWRAP_SOCKET=" + com.droiddeck.launcher.runtime.BwrapSpawner.socket(this)) guest.add("HOME=/root") guest.add("USER=root") guest.add("PATH=/usr/local/bin:/usr/bin:/bin") guest.add("TERM=xterm-256color") guest.add("LANG=C.UTF-8") - // Steam's interface language: the app's (Setup's choice, or the system's). The system's - // region decides between Spain's and Latin American Spanish, which the app has one of. guest.add("BL_STEAM_LANGUAGE=" + SteamLanguage.forLocale( com.droiddeck.launcher.core.AppLanguage.effective(this), com.droiddeck.launcher.core.AppLanguage.system(this).country, )) - // Without this the session is UTC: the client's clock, its logs and every timestamp in a - // session bundle sit hours off the device's. Bannerlator carries the same line. guest.add("TZ=" + java.util.TimeZone.getDefault().id) guest.add("XDG_RUNTIME_DIR=" + LinuxRuntime.GUEST_RUNTIME_DIR) guest.add("XDG_SESSION_TYPE=wayland") guest.add("WAYLAND_DISPLAY=wayland-0") guest.add("BL_ANDROID_CLIPBOARD=" + File(filesDir, "session/android-clipboard").path) guest.add("GAMESCOPE_FORCE_GENERAL_QUEUE=1") - // Steam's CEF needs GL and the rootfs ships no native GL driver: route it through Zink. guest.add("MESA_LOADER_DRIVER_OVERRIDE=zink") guest.add("GALLIUM_DRIVER=zink") guest.add("LIBGL_KOPPER_DRI2=true") LinuxRuntime.vulkanIcd(this)?.let { guest.add("VK_ICD_FILENAMES=" + it.path) } - // An imported glibc Turnip, when one is set (by the user, or by Auto): the session script - // checks the manifest and its library from inside and points the loader at it with - // VK_DRIVER_FILES, so the runtime's own driver above stays untouched and is what a bad - // import falls back to. One driver for every session: the driver's shader cache is keyed on - // its build, and with one per mode every emulator compiled its shaders twice. val linuxDriverId = SessionPrefs.linuxDriver(this) LinuxVulkanDriver.resolveIcdPath(this, linuxDriverId) ?.let { guest.add(LinuxVulkanDriver.ENV + "=" + it) } - // Turnip's own debug switches, for the runtime's driver and everything on it. The file in - // Downloads holds the value verbatim ("sysmem", "sysmem,deck_emu"); with nothing there, an - // imported driver from the A710/A720/A722 legs gets "sysmem" on its own, which is what both - // its authors advise for those GPUs and what nothing else in the list needs. tuDebug(linuxDriverId)?.let { guest.add("TU_DEBUG=$it") } - // Zink renders the client's UI (Chromium -> ANGLE -> Zink -> Turnip). Lazy descriptors is - // the mode Zink recommends where the driver has no descriptor buffer, and what Ludashi ships - // by default for its Zink path; a switch here because on one Fold the menus run at 14 fps. - // compact packs Zink's descriptor sets into fewer, so a draw binds less (WinNative's default). if (SessionPrefs.zinkLazy(this)) { guest.add("ZINK_DESCRIPTORS=lazy") guest.add("ZINK_DEBUG=compact") } - // The rest of the client-interface switches (SessionPrefs): GL marshalled off the calling - // thread and no GL error checks. if (SessionPrefs.glThread(this)) guest.add("mesa_glthread=true") if (SessionPrefs.noGlError(this)) guest.add("MESA_NO_ERROR=1") - // Mesa's shader cache as one database instead of a file per entry. Every lookup in the - // file cache is an open and every store an open and a rename, each a proot stop (a rename - // two), for every pipeline DXVK, vkd3d-proton and Zink compile or find; the database is - // opened once and read and written with pread/pwrite, which proot never sees. Mesa removes - // the old folder itself once it has gone a week untouched. guest.add("MESA_DISK_CACHE_DATABASE=1") - // glibc's per-thread rseq registration is refused by Android's app seccomp policy, so - // every thread start paid a SIGSYS that proot answers; the malloc top pad grows the heap - // 16 MB at a time instead of 128 KB, and every brk(2) is a proot stop too. guest.add("GLIBC_TUNABLES=glibc.pthread.rseq=0:glibc.malloc.top_pad=16777216") if (SessionState.mode == MODE_STEAM) guest.add("BL_MANGOAPP=" + (if (SessionPrefs.mangoapp(this)) "1" else "0")) if (SessionState.mode == MODE_STEAM) { guest.add("BL_GAMESCOPE_STRETCH_16X9=" + (if (SessionPrefs.stretch16x9(this)) "1" else "0")) } - // Proton's own gate for its xalia helper (its `proton` script reads this, and sets - // XALIA_SUPPORTED_ONLY itself otherwise). Skipped by default: under FEX it costs every game - // a slice of a core for gamepad navigation the session already has. if (SessionPrefs.noXalia(this)) guest.add("PROTON_USE_XALIA=0") guest.add("BL_SYNC=" + (if (SessionPrefs.fastSync(this)) "1" else "0")) guest.add("BL_SYNC_FALLBACK=" + (if (SessionPrefs.syncFallback(this)) "1" else "0")) guest.add("BL_FSYNC_FIRST=" + (if (SessionPrefs.fsyncFirst(this)) "1" else "0")) - // gamescope's realtime Vulkan queues (the session script turns this into - // GAMESCOPE_FORCE_VULKAN_REALTIME); off unless the user turns it on. guest.add("BL_GAMESCOPE_REALTIME=" + (if (SessionPrefs.gamescopeRealtime(this)) "1" else "0")) if (SessionState.mode == MODE_STEAM && SessionPrefs.zramCompression(this) && com.droiddeck.launcher.core.ZramSupport.supported()) guest.add("BL_ZRAM=1") guest.add("BANNER_AUDIO_DIRECT_DECAY=0") - // Anything else, for a device that cannot be reached with a debugger: droiddeck-env in the - // app's own external files (envSwitchFile) holds KEY=VALUE lines that go into the session's - // environment as written, after ours, so a line here wins. Zink and Turnip tunables - // (ZINK_DESCRIPTORS=lazy, MESA_*), gamescope's, the client's - whatever the experiment - // needs, without a build per attempt. extraEnv().forEach { guest.add(it) } - // The agent bridge's experiment lines (AgentEnv), after the user's so an agent's win. com.droiddeck.launcher.agent.AgentEnv.beginSession(this).forEach { guest.add(it) } - // A game launched by pinned Steam inherits its mask. Explicitly restore all game cores - // when requested, as well as applying restricted game choices in the Proton wrapper. val clientPinned = steamHere && SessionPrefs.clientCpusOverride(this) if (clientPinned) { guest.add("BL_CLIENT_CPUS=" + CpuCores.listOrAll(SessionPrefs.clientCpus(this))) } - // The game cores also pin the desktop and a program from the rail; without a choice the - // session script picks every core but the slowest cluster for those (program_cores). CpuCores.gameLaunchList(SessionPrefs.gameCpus(this), clientPinned) .takeIf { it.isNotEmpty() }?.let { guest.add("BL_GAME_CPUS=$it") } } - /** PulseAudio, and the DirectAudio relay when a path needs it; returns the daemon for suspend and resume. */ private fun startAudio(guest: MutableList<String>, sessionDir: File): PulseAudioComponent { - // PulseAudio always: the client is a native Linux program and has no other way to make a - // sound - its menus, its music and its voice chat all go through here. DirectAudio is not - // an alternative to it on this path: it replaces the audio driver INSIDE Wine, so it - // changes what games do and leaves the client alone. The microphone is its own opt-in on - // top, and the helper only opens an input stream when asked - so a user who wants game - // sound but no recording gets exactly that, and Android's recording indicator stays off. - // The desktop starts Windows programs through the same Proton wrappers (droiddeck-proton-run). val wantsDirectAudio = (SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP) && SessionPrefs.directAudio(this) val wantsMic = SessionPrefs.micEnabled(this) && checkSelfPermission(android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED - // Both paths sit under the app's files directory, which the session binds at its own path, - // so the same string is valid on both sides and nothing has to be translated. val audioDir = File(filesDir, "directaudio").apply { mkdirs() } val relaySocket = File(audioDir, "relay.sock") val micFifo = if (wantsMic) File(audioDir, "mic.fifo") else null val audioLog = File(sessionDir, "audio.log") val pulse = PulseAudioComponent(this, micFifo?.absolutePath) - // The client's own sound: DirectAudio's engine inside the daemon, one step from Android (the - // daemon runs on the Android side). The relay below is for games and the microphone only. pulse.setLogFile(audioLog) pulse.attach(this) guest.add("PULSE_SERVER=unix:" + pulse.socket().absolutePath) components.add(pulse) if (wantsDirectAudio || wantsMic) { - // After the daemon in the list, so it can wait for the pipe the daemon makes. val relay = DirectAudioRelayComponent(relaySocket, micFifo) relay.setLogFile(audioLog) relay.attach(this) components.add(relay) } if (wantsDirectAudio) { - // Read by the Proton wrappers, which point Wine at the driver and name it in the - // prefix. Absent, they take an early return and the game uses Proton's own audio - so - // this variable is the whole of the selection. guest.add("BL_DIRECTAUDIO=/" + SessionFiles.DIRECTAUDIO_DIR) guest.add("BANNER_AUDIO_DIRECT_RELAY=" + relaySocket.absolutePath) } @@ -730,11 +586,8 @@ class SessionService : Service() { return pulse } - /** The fake evdev pads: the ring files the app writes and the identity SDL and Steam see. */ private fun addControllerEnvironment(guest: MutableList<String>, fakeInputDir: File, sessionDir: File) { FakeInputWriter.prepareRingSlots(fakeInputDir, 4) - // Virtual pads a client made last session (event16 and up, and their hidden rings) are - // not there any more; a client that crashed never took its own down. fakeInputDir.listFiles()?.forEach { file -> val node = Regex("event(\\d+)").matchEntire(file.name)?.groupValues?.get(1)?.toIntOrNull() if (file.name.startsWith(".uinput-") || (node != null && node >= FIRST_VIRTUAL_PAD)) file.delete() @@ -742,24 +595,13 @@ class SessionService : Service() { guest.add("FAKE_EVDEV_DIR=" + fakeInputDir.path) val rings = FakeInputWriter.getRingEnv(fakeInputDir) if (!rings.isNullOrEmpty()) guest.add("FAKE_EVDEV_MEMFD_PATHS=$rings") - // SDL and Steam key their mapping database on bus+vendor+product: only a known - // identity gets the standard layout without the user configuring the pad by hand. guest.add("FAKE_EVDEV_IDENTITY=xbox360") guest.add("FAKE_EVDEV_VIBRATION=1") val uinput = !File(Environment.getExternalStorageDirectory(), NO_UINPUT_SWITCH).exists() - // In Steam, the pad is what a Deck's is: a Steam Deck controller the client reads over - // hidraw (SteamDeckPad), which only works with Steam Input's virtual pad for games to read. - // Decided only once its sysfs is in place: without it the client would find no Deck, and - // with the Deck asked for the pad's own nodes are withdrawn - no controller at all. - // Set up whatever the pad is to be, so the drawer can switch it between a Deck, a Steam - // Controller and an Xbox 360 pad while the session runs (FakeInputWriter.setPadType). val wantsDeck = uinput && SessionState.mode == MODE_STEAM && SessionState.steamUi != "desktop" && !File(Environment.getExternalStorageDirectory(), NO_DECK_PAD_SWITCH).exists() - // Steam's touch controller rides on the Deck's sysfs and is offered only beside it. val wantsTouch = wantsDeck && !File(Environment.getExternalStorageDirectory(), NO_STEAM_TOUCH_SWITCH).exists() - // A paired Triton is exposed as a second native Steam Controller HID, with its original - // report IDs and output path, beside whatever the handheld's own pad is. val wantsTriton = wantsDeck && SteamControllerBle.hasPairedController(this) deckBinds = if (wantsDeck) SteamDeckPad.prepare(this, fakeInputDir.parentFile!!.parentFile!!, wantsTouch, wantsTriton, SessionPrefs.padType(SessionPrefs.steamController(this))) else emptyList() @@ -767,43 +609,29 @@ class SessionService : Service() { if (wantsDeck && !SessionState.deckPad) Log.w(TAG, "deck pad: not available this session; the pad stays an Xbox 360 controller") logControllersAtStart() if (uinput) { - // /dev/uinput, stood in for by libfakeinput: the virtual pad Steam Input makes for a - // game becomes a node the game reads, carrying the player's layout, as on a Deck. guest.add("FAKE_EVDEV_UINPUT=1") if (SessionState.deckPad) { guest.add("FAKE_EVDEV_DECK=1") if (wantsTriton) guest.add("FAKE_EVDEV_TRITON=1") - // What the pad starts as; the drawer's changes reach libfakeinput through the ring. val padType = SessionPrefs.steamController(this) guest.add("FAKE_EVDEV_PAD_TYPE=$padType") com.droiddeck.launcher.input.FakeInputWriter.setPadType(SessionPrefs.padType(padType)) - // Steam's touch controller (SteamTouchDevice): the file the app and libfakeinput share. if (wantsTouch) com.droiddeck.launcher.input.SteamTouchDevice.prepare(fakeInputDir)?.let { guest.add("FAKE_TOUCHCTL_RING=" + it.path) } guest.add("FAKE_DECK_SYSFS_LISTING=" + SteamDeckPad.listingDir(fakeInputDir.parentFile!!.parentFile!!).path) } } else if (SessionState.mode == MODE_STEAM) { - // Without it, games see the pad itself wearing Steam Input's virtual-gamepad identity - - // for games the client starts only. Everywhere else (the desktop, a program from the - // rail) that identity hides the pad: SDL ignores a Steam virtual gamepad unless it - // runs under Steam, so every SDL emulator came up with no controller. guest.add("FAKE_EVDEV_STEAM_VIRTUAL=1") } guest.add("SDL_JOYSTICK_DISABLE_UDEV=1") guest.add("SDL_HIDAPI_JOYSTICK_DISABLE_UDEV=1") - // The client reads a Deck controller through SDL's HIDAPI; a hint in the environment - // outranks the client's own. Nothing else is shown the Deck (libfakeinput). if (!SessionState.deckPad) guest.add("SDL_JOYSTICK_HIDAPI=0") - // The guest side of the pads (libfakeinput: which pads were opened, the Deck's hidraw and why - // it was refused) in pad.log beside the session's other logs, so it is in every shared zip. - // Setup-time lines only, nothing per input event. guest.add("FAKE_EVDEV_LOG=1") guest.add("FAKE_EVDEV_LOG_FILE=" + File(sessionDir, "pad.log").path) SessionState.fakeInputDir = fakeInputDir } - /** What proot binds into the guest: the pads, the battery, storage, the game library, added games and ROMs. */ private fun sessionBinds( controllersOn: Boolean, fakeInputDir: File, @@ -815,11 +643,6 @@ class SessionService : Service() { val binds = ArrayList<String>() if (controllersOn) binds.add(fakeInputDir.path + ":/dev/input") if (controllersOn) binds.addAll(deckBinds) - // The client's battery readout (the Quick Access Menu, the top bar) reads - // /sys/class/power_supply/BAT<n>/..., a laptop's or a Deck's naming; Android's supply is - // called "battery" and its files differ, so the client sees no battery at all. A directory - // of our own, written from Android's battery API every few seconds, is bound over it. - // Steam's time estimates come from /run/vpower instead, written straight into the rootfs. val battery = BatteryComponent( File(filesDir, "session/sys/power_supply"), File(LinuxRuntime.rootDir(this), "run/vpower"), @@ -827,55 +650,39 @@ class SessionService : Service() { battery.attach(this) components.add(battery) binds.add(battery.dir.path + ":/sys/class/power_supply") - // The overlay's CPU and GPU temperatures and the fan, as hwmon sensors it knows by name. val hwmon = HwmonComponent(File(filesDir, "session/sys/hwmon"), LinuxRuntime.rootDir(this)) if (hwmon.prepare()) { hwmon.attach(this) components.add(hwmon) binds.add(hwmon.dir.path + ":/sys/class/hwmon") } - // CPU load for everything in the session that reads /proc/stat, the overlay among them. val cpuStat = CpuStatComponent(File(filesDir, "session/proc-stat")) if (cpuStat.prepare()) { cpuStat.attach(this) components.add(cpuStat) binds.add(cpuStat.file.path + ":/proc/stat") } - // The GPU memory in use for the overlay's VRAM lines, which it would read from tracefs. val gpuMem = GpuMemComponent(File(LinuxRuntime.rootDir(this), "run/droiddeck-hud/gpu-mem"), LinuxRuntime.rootDir(this)) if (gpuMem.prepare()) { gpuMem.attach(this) components.add(gpuMem) } - // The GPU's load and temperature for the performance overlay, where KGSL's sysfs is refused. val gpuStats = GpuStatsComponent(File(filesDir, "session/sys/kgsl-3d0")) if (gpuStats.prepare()) { gpuStats.attach(this) components.add(gpuStats) binds.addAll(gpuStats.binds()) } - // Rumble for the on-screen pad: the fake evdev layer sends force-feedback effects to this - // listener, which drives the phone's vibrator (see RumbleComponent). if (controllersOn) components.add(RumbleComponent().also { it.attach(this) }) - // Where the device's files appear inside the session. Internal storage is bound at its own - // path already, and every program's file dialog opens at home and lists "Computer" from - // /proc/mounts, where a proot bind never shows - so a user saw only the runtime's own - // tree and could not find the phone at all. The same storage is placed under home as - // well, and the ROMs folder chosen on the main screen beside it; a bind rather than a - // link, so a folder on an SD card works the same. val home = File(LinuxRuntime.rootDir(this), "root") File(home, "Storage").mkdirs() binds.add(Environment.getExternalStorageDirectory().path + ":/root/Storage") - // A second Steam library: the storage chosen in the Steam cog, at the path the runtime's - // droiddeck-steam-library registers with the client. Nothing bound = the script removes - // the entry, so the client never offers a place that is not there. val library = GameStorage.effective(this) var storageDiagnosticLibrary: File? = null if (library != null) { val problem = GameStorage.prepare(this, library.path) if (problem == null) { File(LinuxRuntime.rootDir(this), "mnt/droiddeck-sd").mkdirs() - // Links, prefixes and Steam entries made before the rename still name the old path. File(LinuxRuntime.rootDir(this), "mnt/bannerlator-sd").mkdirs() try { binds.addAll(SecondaryLibrary.binds(filesDir, File(library.path))) @@ -900,8 +707,6 @@ class SessionService : Service() { } ?: false, ) if (device != null) { - // guest already has the session command at this point. Put these with the `env -i` - // assignments so they reach the Steam process instead of becoming script args. val envAt = guest.indexOf(LinuxRuntime.SESSION_SCRIPT).takeIf { it >= 0 } ?: guest.size guest.add(envAt, "BL_STORAGE_DIAGNOSTICS=1") guest.add(envAt + 1, "BL_STORAGE_DEVICE=$device") @@ -910,8 +715,6 @@ class SessionService : Service() { Log.w(TAG, "storage diagnostics: selected library could not be identified; metadata snapshot only") } } - // The user's own games folder (the Steam cog's "Added games"), bound at a fixed place so - // the shortcuts the app writes point somewhere whatever storage the folder is on. for (root in com.droiddeck.launcher.frontend.AddedGames.roots(this)) { if (root.host.isDirectory && root.host.canRead()) { File(LinuxRuntime.rootDir(this), root.guest.removePrefix("/")).mkdirs() @@ -921,8 +724,6 @@ class SessionService : Service() { Log.w(TAG, "added games: ${root.host} is not a readable folder this session") } } - // Store games installed on a card: each card's Games root at its own fixed place, so the - // shortcuts point somewhere whether or not the card is also the Steam library. for ((host, guest) in com.droiddeck.launcher.stores.StoreInstallRoot.externalRoots(this)) { if (host.isDirectory && host.canRead()) { File(LinuxRuntime.rootDir(this), guest.removePrefix("/")).mkdirs() @@ -930,7 +731,6 @@ class SessionService : Service() { Log.i(TAG, "store games: $host -> $guest") } } - // Folders of added scripts outside internal storage, where their links point. binds.addAll(com.droiddeck.launcher.runtime.UserApps.binds(this)) val roms = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() }?.let { File(it) } if (roms != null && roms.isDirectory && roms.canRead()) { @@ -987,7 +787,7 @@ class SessionService : Service() { if (!stillSameProcess()) return val tree = descendants(prootPid) if (!stillSameProcess()) return - android.os.Process.sendSignal(prootPid, 15) // SIGTERM + android.os.Process.sendSignal(prootPid, 15) if (!waitForExit(prootPid, GRACE_MS)) { if (!stillSameProcess()) return Log.w(TAG, "proot $prootPid did not exit on SIGTERM; killing it") @@ -995,15 +795,14 @@ class SessionService : Service() { } var killed = 0 for ((pid, started) in tree) { - val stat = readStat(pid) ?: continue // already gone - if (stat.second != started) continue // same number, different process + val stat = readStat(pid) ?: continue + if (stat.second != started) continue android.os.Process.killProcess(pid) killed++ } if (killed > 0) Log.i(TAG, "swept $killed process(es) proot left behind") } - /** Every process under [root], as (pid, start time), from a single walk of /proc. */ private fun descendants(root: Int): List<Pair<Int, Long>> { val started = HashMap<Int, Long>() val children = HashMap<Int, MutableList<Int>>() @@ -1027,7 +826,6 @@ class SessionService : Service() { return out } - /** (parent pid, start time) from /proc/pid/stat, or null when the process is gone. */ private fun readStat(pid: Int): Pair<Int, Long>? { val stat = try { File("/proc/$pid/stat").readText() } catch (e: Exception) { return null } val close = stat.lastIndexOf(')') @@ -1140,19 +938,6 @@ class SessionService : Service() { File(directory, STEAM_GAME_REQUEST + ".tmp").delete() } - /** - * proot's --kill-on-exit takes its tracees down, but a session that died from the inside - * (the client asserting, Xwayland going) leaves gamescopereaper and the session script - * behind, still holding the Wayland socket and the audio server the next session needs. They - * are our uid, so they are ours to kill. - */ - /** - * Every process of ours still running a program out of the Linux runtime once proot is gone. - * proot's --kill-on-exit and the tree sweep only reach what proot still traces; a tracee it - * lost - an Xwayland that aborted from one thread and then looped on a syscall proot's own - * seccomp filter, with no tracer left, answers ENOSYS - survives both, reparented to init, - * and wrote 9 GB of one line into the session log before anything killed it. - */ private fun killGuestLeftovers() { val me = android.os.Process.myPid() val rootfs = try { File(filesDir, "linuxfs").canonicalPath } catch (e: Exception) { return } + "/" @@ -1161,7 +946,6 @@ class SessionService : Service() { for (proc in procs) { val pid = proc.name.toIntOrNull() ?: continue if (pid == me) continue - // Another uid's exe is not ours to read; a process already gone has none. val exe = try { File(proc, "exe").canonicalPath } catch (e: Exception) { continue } if (!exe.startsWith(rootfs)) continue android.os.Process.killProcess(pid) @@ -1189,13 +973,6 @@ class SessionService : Service() { if (killed > 0) Log.w(TAG, "killed $killed leftover process(es) of a previous session") } - /** - * The desktop's Steam launchers cannot start the client where they are (no dma-buf on the - * desktop), so they leave `steam-launch` in the session directory instead: which UI, and a - * steam:// URL or nothing. This ends the session and hands the activity the one to start. - * `session=desktop` (Big Picture's "Switch to Desktop", steamos-session-select) asks for the - * desktop instead, with Steam's desktop client started in it. - */ private var launchWatcher: android.os.FileObserver? = null private fun watchLaunchRequests(dir: File) { @@ -1275,12 +1052,6 @@ class SessionService : Service() { private val desktopImports = java.util.concurrent.ConcurrentHashMap<String, Any>() - /** - * What the desktop cannot run where it lies (droiddeck-open), added as Add an app adds it, menu entry - * included: an AppImage of sharun's, whose hard links Android denies apps and only the app's own proot - * (link2symlink) makes, or a script or program whose folder of Linux programs is on noexec shared - * storage, copied in. The answer goes beside the request, "ok <folder>" or "error <why>". - */ private fun importForDesktop(dir: File, text: String) { val fields = text.lineSequence().mapNotNull { line -> line.split('=', limit = 2).takeIf { it.size == 2 }?.let { it[0] to it[1].trim() } @@ -1291,7 +1062,6 @@ class SessionService : Service() { val binds = LinuxRuntime.lastBinds(this).map { BwrapSpawner.Bind.parse(it) } val file = File(BwrapSpawner.hostPath(binds, LinuxRuntime.rootDir(this).path, guestPath)) val context = applicationContext - // A request for what is already being added waits for that, then finds it. val lock = desktopImports.computeIfAbsent(key) { Any() } Thread({ fun imported() = UserApps.desktopImport(context, key, guestPath)?.let { "ok $it" } @@ -1413,8 +1183,6 @@ class SessionService : Service() { private fun completeSteamSleep() { val token = steamSleepToken ?: return - // Write before SIGCONT: the guest's login1 sees it immediately on thaw - // and delivers PrepareForSleep(false), restoring Steam's main surface. writeSteamSleepState(token, "awake") steamSleepToken = null } @@ -1427,7 +1195,6 @@ class SessionService : Service() { } } - /** Let Steam prepare before freezing; retain the wake lock until that freeze completes. */ private fun prepareNativeSleep() { val token = UUID.randomUUID().toString().replace("-", "") nativeSleepToken = token @@ -1451,7 +1218,6 @@ class SessionService : Service() { updateSuspendPolicy() } nativeSleepTimeout = timeout - // A missing or stalled guest helper must not keep the device awake indefinitely. mainHandler.postDelayed(timeout, 4000L) } @@ -1468,7 +1234,6 @@ class SessionService : Service() { nativeSleepFallback = false } - /** Resume must also recover a Steam sleep whose request never reached Android. */ private fun requestSteamWake() { if (!SessionState.running || SessionState.mode != MODE_STEAM) return val dir = LinuxRuntime.sessionRoot(this) @@ -1491,8 +1256,6 @@ class SessionService : Service() { SessionState.guestPid = -1 com.droiddeck.launcher.agent.AgentGuest.stop() com.droiddeck.launcher.stores.StoreLaunchRequests.stop() - // Cloud saves of a store game played this session, if the compat tool's exit request did - // not upload them (a session closed from the drawer never reaches it). Thread({ com.droiddeck.launcher.stores.CloudSaves.uploadAllDirty(applicationContext, "session-end", running = false) }, "cloud-session-end").start() runCatching { com.droiddeck.launcher.agent.AgentEnv.endSession(this) } .onFailure { Log.w(TAG, "clearing the agent's session environment", it) } @@ -1511,13 +1274,6 @@ class SessionService : Service() { } } - /** - * proot is one thread, and every syscall it traps anywhere in the session - a path lookup in - * Proton's python, Wine's file opens, the Steam client's /proc scans - waits for that thread to - * be scheduled, at nice 0 beside a game that keeps the big cores busy. It is raised to nice -6, - * under the compositor's -8. After a moment, not at once: the first guest process is forked by - * proot itself and would inherit the value, and with it everything the session starts. - */ private fun raiseTracer(pid: Int, gen: Int) { Thread({ try { @@ -1555,13 +1311,6 @@ class SessionService : Service() { launchWatcher = null syncWatcher?.stopWatching() syncWatcher = null - // proot's --kill-on-exit takes the guest tree down only if proot gets to run it, and - // SIGKILL never lets it. A SIGKILLed proot left its tracees alive with no tracer: every - // seccomp-trapped syscall then failed with ENOSYS, they spun on retries at a full core - // for over an hour, and the reaper could not even open /proc to kill them. So: SIGTERM, - // a grace for proot's own cleanup, SIGKILL only if it will not go, then a sweep of the - // tree it had - snapshotted first, each pid checked against its start time so a number - // reused by a new process is never touched. val prootPid = sessionPid sessionPid = -1 val auxiliary = synchronized(auxiliaryProcessesLock) { @@ -1570,11 +1319,6 @@ class SessionService : Service() { SessionTerminal.clear(sessionGen) val controller = suspendController suspendController = null - // On its own thread, never here: stopSession runs on the main thread (the notification's - // Stop action arrives there), and collecting means copying the compositor's log, scrubbing - // every Steam log line by line - 42 files on one measured run - and waiting for logcat to - // dump the crash buffer. That was about four and a half seconds of blocked main thread, - // and Android ANR'd the app for it: the desktop session that would not let go. val ended = SessionPaths.take() if (ended != null) Thread({ collectSessionArtifacts(ended) }, "session-collect").start() components.reversed().forEach { @@ -1606,10 +1350,6 @@ class SessionService : Service() { if (controller != null) controller.closeAndResume(finishAfterTeardown) else finishAfterTeardown() } - /** - * Swiped out of recents. The activity is destroyed without any of our teardown running, so the - * guest would survive as an orphan holding the rootfs and the GPU. Treat the swipe as "quit". - */ override fun onTaskRemoved(rootIntent: Intent?) { if (pipTask) { Log.i(TAG, "PiP task dismissed - retaining the session under its background policy") @@ -1633,7 +1373,6 @@ class SessionService : Service() { override fun onBind(intent: Intent?): IBinder? = null - // ── Keeping the process alive ─────────────────────────────────────────────────────────── private fun acquireLocks() { try { @@ -1652,7 +1391,7 @@ class SessionService : Service() { val wifi = applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager val allowed = DeviceSupport.lowLatencyWifiSafe() if (!allowed) Log.i(TAG, "wifi lock skipped: its low-latency mode resets this device") - @Suppress("DEPRECATION") // deprecated from API 29, still honoured; targetSdk is 28 + @Suppress("DEPRECATION") if (allowed && wifiLock?.isHeld != true) { wifiLock = wifi?.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "DroidDeck:session-wifi") ?.apply { @@ -1662,8 +1401,6 @@ class SessionService : Service() { } Log.i(TAG, "wifi lock held=${wifiLock?.isHeld}") } catch (t: Throwable) { - // A partial wake lock keeps the process alive but does not stop WiFi power-save from - // throttling a backgrounded download to nothing, which is what this lock is for. Log.w(TAG, "no wifi lock (${t.message}) - a backgrounded download may stall") } } @@ -1683,11 +1420,9 @@ class SessionService : Service() { wifiLock = null } - // ── Notification ──────────────────────────────────────────────────────────────────────── private fun buildNotification(): Notification { val manager = getSystemService(NotificationManager::class.java) - // IMPORTANCE_LOW: it must never make a sound or push a heads-up over a game. val channel = NotificationChannel(CHANNEL_ID, getString(R.string.session_channel), NotificationManager.IMPORTANCE_LOW).apply { description = getString(R.string.session_channel_description) @@ -1784,7 +1519,6 @@ class SessionService : Service() { mainHandler.post(poll) } - /** The pads attached as the session starts; the activity logs the ones that come and go after. */ private fun logControllersAtStart() { val pads = ArrayList<String>() for (id in android.view.InputDevice.getDeviceIds()) { @@ -1798,25 +1532,11 @@ class SessionService : Service() { companion object { private const val TAG = "SessionService" - /** proot's tracer: above everything in the guest, under the compositor thread's -8. */ private const val TRACER_NICE = -6 - /** Downloads file whose contents become TU_DEBUG inside the session, e.g. "sysmem". */ private const val TU_DEBUG_SWITCH = "Download/droiddeck-tu-debug" - /** - * Where droiddeck-env (KEY=VALUE lines added to the session environment verbatim) used to - * be. Ignored now: a line there reaches the Steam client's environment as written - * (LD_PRELOAD, VK_ICD_FILENAMES, PATH...), and any app with storage access can write to - * Download - code of its choosing inside our sandbox, next to the client's saved login. - */ private const val LEGACY_ENV_SWITCH = "Download/droiddeck-env" private const val ENV_SWITCH_NAME = "droiddeck-env" - /** - * The session's extra-environment file: Android/data/<package>/files/droiddeck-env. Only - * this app, adb and the user (through the app's own file manager) can write there - from - * Android 11 on. Before that other apps with storage access can reach Android/data too, so - * there is no such file at all. - */ fun envSwitchFile(context: Context): File? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) context.getExternalFilesDir(null)?.let { File(it, ENV_SWITCH_NAME) } @@ -1837,10 +1557,8 @@ class SessionService : Service() { private const val ACTION_STOP_AUXILIARY = "com.droiddeck.launcher.STOP_AUXILIARY" private const val ACTION_AUXILIARY_EXITED = "com.droiddeck.launcher.AUXILIARY_EXITED" private const val EXTRA_AUXILIARY_PID = "auxiliaryPid" - /** Command lines that can only belong to a session of ours. */ private val STRAGGLERS = listOf("droiddeck-session", "gamescope", "Xwayland", "steamrtarm64", "steamwebhelper", "linuxfs/opt/android-host/proot", "/libproot.so", "pulseaudio/libpulseaudio.so") - /** How long proot gets to run its own cleanup before it is killed outright. */ private const val GRACE_MS = 1200L private const val STEAM_PICKUP_MS = 1500L private const val STEAM_EXIT_MS = 10_000L @@ -1851,19 +1569,14 @@ class SessionService : Service() { private const val NO_UINPUT_SWITCH = "Download/droiddeck-no-uinput" private const val NO_DECK_PAD_SWITCH = "Download/droiddeck-no-deck-pad" private const val NO_STEAM_TOUCH_SWITCH = "Download/droiddeck-no-steam-touch" - /** libfakeinput numbers the pads made through its /dev/uinput stand-in from here. */ private const val FIRST_VIRTUAL_PAD = 16 const val EXTRA_MODE = "mode" const val MODE_STEAM = "steam" const val MODE_DESKTOP = "lxqt" - /** A program inside the runtime, fullscreen under gamescope (EXTRA_PROGRAM = its path). */ const val MODE_RUN = "run" const val EXTRA_PROGRAM = "program" - /** MODE_RUN: arguments after the program (a game to boot). */ const val EXTRA_PROGRAM_ARGS = "programArgs" - /** MODE_STEAM: "desktop" for the client's desktop UI (default Big Picture); a steam:// URL to hand it. - * MODE_DESKTOP: "desktop" = open Steam's desktop client in the desktop once it is up. */ const val EXTRA_STEAM_UI = "steamUi" const val EXTRA_STEAM_URL = "steamUrl" const val EXTRA_GAME_ID = "gameId" @@ -1904,7 +1617,6 @@ class SessionService : Service() { context.startService(Intent(context, SessionService::class.java).setAction(ACTION_RESUME)) } - /** Route a validated decimal Steam game id through the current guest session. */ fun launchGame(context: Context, gameId: String): Boolean { if (!GameLaunchLink.validId(gameId) || !SessionState.running || SessionState.mode != MODE_STEAM || SessionState.stopRequested) return false @@ -1923,7 +1635,6 @@ class SessionService : Service() { context.startService(Intent(context, SessionService::class.java).setAction(ACTION_SUSPEND_POLICY_CHANGED)) } - /** Let the session service clean up the PTY's proot tree if the control screen closes. */ fun stopTerminalProcess(context: Context, pid: Int) { if (pid > 1) context.startService( Intent(context, SessionService::class.java) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt index 57c377bcb..2f9f305ba 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt @@ -5,11 +5,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -/** - * The little that the activity and the service have to agree on. Process-wide rather than passed - * through intents, because both halves live in one process and the point of the split is that - * either can outlive the other. - */ object SessionState { @Volatile var phase = SessionPhase.IDLE @@ -48,26 +43,17 @@ object SessionState { var running = false var suspended by mutableStateOf(false) @Volatile var pipActive = false - /** MODE_RUN: the program inside the runtime the session was started for. */ @Volatile var program: String? = null @Volatile var programArgs: List<String> = emptyList() - /** MODE_STEAM: "desktop" for the client's desktop UI, else Big Picture; and a steam:// URL to hand it. */ @Volatile var steamUi: String? = null @Volatile var steamUrl: String? = null - /** A session the guest asked for (the desktop's Steam launchers): started by the activity once this one has ended. */ var relaunch: android.content.Intent? = null - /** HDR10 was asked for and the panel can show it: the compositor was told, and the session - * gets DXVK_HDR=1 and gamescope --hdr-enabled. Decided by the activity before the compositor starts. */ @JvmStatic var hdr = false - /** The pad is presented to the Steam client as a Steam Deck controller this session - * (SteamDeckPad): its Quick Access button is a real button, not the Guide+A chord. Decided by - * the service as the session starts, which can be after the activity has resumed - hence - * [deckPadListener]. */ @Volatile private var deckPadValue = false @JvmStatic var deckPad: Boolean get() = deckPadValue @@ -77,57 +63,40 @@ object SessionState { deckPadListener?.invoke() } - /** Told (on the thread that changed it) whenever [deckPad] changes. */ @Volatile var deckPadListener: (() -> Unit)? = null - /** The session screen's virtual pad, for the agent bridge's pad input (AgentInput). */ @Volatile var padBridge: com.droiddeck.launcher.input.PadBridge? = null - /** What the second screen shows, and on which display: kept for the session, so a screen - * turned off (sleep, a closed lid) or an activity recreated brings it back as it was. */ @Volatile var secondScreenMode = com.droiddeck.launcher.input.SecondScreenMode.NONE @Volatile var secondScreenDisplay = -1 - /** Which session this is: SessionService.MODE_STEAM or MODE_DESKTOP. */ @Volatile var mode = "steam" - /** The size gamescope was told to render at; set by the activity before the service starts. */ @Volatile var outputSize: Pair<Int, Int> = Pair(1920, 1080) - /** The session's resolution is "Follow screen": outputSize tracks the window while it runs. */ @Volatile var followScreen = false @Volatile var refreshHz: Float = 60f - /** - * How far the compositor enlarges the session onto the panel (panel over outputSize, long - * side to long side); set with outputSize. 0 before a session has been sized. Texture - * sharpness "Auto" (core/TextureFiltering) is derived from it when a game launches. - */ @Volatile var upscaleRatio: Float = 0f - /** The session's frame cap (SessionPrefs.fpsLimit), fixed when it starts; 0 = none. */ @Volatile var fpsLimit: Int = 0 - /** The compositor has presented a frame, so the loading panel is behind us for this session. */ @Volatile var firstFrameSeen = false - /** Where this session is writing its log, for the "it ended" message. */ @Volatile var logFile: File? = null - /** The fake evdev directory, once the service has prepared the rings. */ @Volatile var fakeInputDir: File? = null - /** Told when the session ends, so a visible activity can close itself. */ @Volatile var endListener: ((Int) -> Unit)? = null @@ -136,7 +105,6 @@ object SessionState { } } -/** Stable lifecycle values returned by the debug agent bridge. */ enum class SessionPhase { IDLE, PREPARING, diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt index 4db8f8671..c7ecba3bb 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt @@ -4,7 +4,6 @@ import com.termux.terminal.TerminalSession import com.termux.terminal.TerminalSessionClient import java.io.File -/** Immutable launch context for the optional PTY shell attached to the active Linux runtime. */ object SessionTerminal { private data class Launch( val command: Array<String>, diff --git a/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt b/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt index 160c0b3a3..1d4e6aec1 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt @@ -6,15 +6,6 @@ import com.droiddeck.launcher.core.GameEnvironment import com.droiddeck.launcher.input.ControllerPrefs import com.droiddeck.launcher.runtime.LinuxRuntime -/** - * Setup's "Reset all settings": every choice goes back to the default its getter falls back to. - * - * Only settings are removed. The "session" file also holds the library (added games' names, exes, - * appids and art picks), migration records, the drivers Auto installed, the storage and ROMs - * folders and the one-time permission prompts; those stay, as do store sign-ins, imported drivers - * and Lossless Scaling. Game profiles (each game's and the Default profile's environment - * overrides) are cleared only when asked for. - */ object SettingsReset { private const val TAG = "SettingsReset" @@ -30,11 +21,9 @@ object SettingsReset { "guestHostname", "fexPreset", "forceSsbs", "logs", "storageDiagnostics", "upscaler", "upscaleSharpness", "screenEffects", "textureAnisotropy", "textureLodBias", "steamDownloadsInBackground", "steam_offline", - // Cleared so the Quest's one-time compatibility defaults are written again. "questDefaultsV1", ) - /** Per-mode settings, stored as "<prefix><mode>". */ internal val SESSION_PREFIXES = listOf( "resolutionCap.", "displayResolution.", "customRes.", "hdr.", "fpsLimit.", "suspendPolicy.", ) @@ -43,13 +32,6 @@ object SettingsReset { internal fun isSessionSetting(key: String) = key in SESSION_KEYS || SESSION_PREFIXES.any { key.startsWith(it) } - /** - * Resets the settings, then per-game profiles when [gameProfiles] is set ([gameScopes] are the - * library's profile keys; scopes with saved overrides are found on their own). Blocking: call - * off the main thread. Returns false if a part could not be reset; the rest is still done. - * The language is not reset here: AppLanguage.setChosen changes the process locale, so the - * caller does that on the main thread. - */ fun resetAll(context: Context, gameProfiles: Boolean, gameScopes: Collection<String> = emptyList()): Boolean { var ok = true val session = context.getSharedPreferences("session", Context.MODE_PRIVATE) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt b/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt index ca30bf469..4b8daf517 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt @@ -7,33 +7,8 @@ import com.droiddeck.launcher.core.FileUtils import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File -/** - * The session's pad, described to the Steam client as a Steam Deck controller. - * - * On a Deck - and on the handhelds InputPlumber serves as one - the client finds the controller - * through libudev as a `hidraw` device under a `hid` device under a USB interface and reads it - * itself. libfakeinput serves the device node (`/dev/hidraw16`, built from the pad's input ring); - * what the client also needs is the sysfs it walks to find and identify that node, which a sandbox - * shows nothing of. This writes that sysfs as the kernel lays out a Deck's controller interface and - * returns the binds that put it in place: - * - * - `/sys/devices/droiddeck/usb1` - the USB device (Valve, 28de:1205), its interface 2, the HID - * device on it (`HID_ID`, `report_descriptor`) and that device's `hidraw/hidraw16`; - * - `/sys/class/hidraw/hidraw16` - what an enumeration of the `hidraw` subsystem lists; - * - `/sys/dev/char/240:16` - how the node's device number is looked up (in the directory the - * runtime already binds there for the GPU); - * - `/run/udev/data/c240:16` - the udev database entry that marks the device initialised, without - * which libudev's enumeration leaves a device node out; - * - [listingDir] - stand-ins for the listings of `/sys`, `/sys/class` and `/sys/bus`. An app under - * an enforcing SELinux policy (every retail phone) may not list those directories, and libudev - * abandons its whole scan when it cannot, so the client never reached `hidraw`; libfakeinput - * lists these to the client instead when the real listing is refused (FAKE_DECK_SYSFS_LISTING). - * - * Symlinks are absolute guest paths; proot resolves them inside the session. - */ object SteamDeckPad { private const val TAG = "SessionService" - /** Must match libfakeinput (fakeinput_steam.cpp: DECK_HIDRAW_*). */ private const val MAJOR = 240 private const val MINOR = 16 private const val NODE = "hidraw$MINOR" @@ -44,20 +19,14 @@ object SteamDeckPad { private const val HIDRAW = "$HID/hidraw/$NODE" private const val SERIAL = "DROIDDECK0001" - // The paired Bluetooth Steam Controller is a separate Triton HID device. It must not inherit - // the Deck's identity or report format. Must match fakeinput_steam.cpp (TRITON_HIDRAW_*). private const val TRITON_MINOR = 18 private const val TRITON_HID = "$GUEST_DEVICES/bluetooth/0005:28DE:1303.0003" private const val TRITON_SERIAL = "DROIDDECKSC01" - // The handheld's own pad as the Steam Controller (SessionPrefs.CONTROLLER_STEAM): the wired - // controller (28de:1302) on a USB device of its own, since it is built in. private const val TRITON_USB = "$GUEST_DEVICES/usb3" private const val TRITON_INTERFACE = "$TRITON_USB/3-1:1.0" private const val TRITON_WIRED_HID = "$TRITON_INTERFACE/0003:28DE:1302.0004" - /** Must match libfakeinput (DS4_HIDRAW_MINOR, DUALSENSE_HIDRAW_MINOR). */ private const val SONY_DS4_MINOR = 20 private const val SONY_DUALSENSE_MINOR = 21 - /** Must match libfakeinput's kDs4ReportDescriptor and kDualSenseReportDescriptor. */ private val DS4_REPORT_DESCRIPTOR = intArrayOf( 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x06, 0x00, 0xff, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x85, 0x01, 0x09, 0x20, 0x95, 0x3f, 0x81, 0x02, 0x85, 0x05, 0x09, 0x21, 0x95, 0x1f, 0x91, 0x02, @@ -71,11 +40,8 @@ object SteamDeckPad { 0x85, 0x05, 0x09, 0x22, 0x95, 0x28, 0xb1, 0x02, 0x85, 0x09, 0x09, 0x23, 0x95, 0x13, 0xb1, 0x02, 0x85, 0x20, 0x09, 0x24, 0x95, 0x3f, 0xb1, 0x02, 0xc0, ).map { it.toByte() }.toByteArray() - /** Must match libfakeinput (TRITON_PAD_HIDRAW_*): its own node, so a paired controller keeps 18. */ private const val TRITON_PAD_MINOR = 19 - // Steam's touch controller (SteamTouchDevice), a second device beside the Deck's: 0000:11fb, - // "Mobile Touch Control", on a USB device of its own. Must match libfakeinput (TOUCH_*). private const val TOUCH_MINOR = 17 private const val TOUCH_NODE = "hidraw$TOUCH_MINOR" private const val TOUCH_USB = "$GUEST_DEVICES/usb2" @@ -84,7 +50,6 @@ object SteamDeckPad { private const val TOUCH_HIDRAW = "$TOUCH_HID/hidraw/$TOUCH_NODE" private const val TOUCH_SERIAL = "DROIDDECK0001" - /** libfakeinput's kTouchReportDescriptor: Generic Desktop / Game Pad, 40-byte input report. */ private val TOUCH_REPORT_DESCRIPTOR = intArrayOf( 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x09, 0x02, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x28, 0x81, 0x02, @@ -92,14 +57,12 @@ object SteamDeckPad { 0x09, 0x04, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0xb1, 0x02, 0xc0, ).map { it.toByte() }.toByteArray() - /** InputPlumber's CONTROLLER_DESCRIPTOR, as libfakeinput answers HIDIOCGRDESC. */ private val REPORT_DESCRIPTOR = intArrayOf( 0x06, 0xff, 0xff, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x02, 0x09, 0x03, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x06, 0x09, 0x07, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0xb1, 0x02, 0xc0, ).map { it.toByte() }.toByteArray() - /** Triton's native 0x45/0x47 input and 0x80-0x85 haptic output report IDs. */ private val TRITON_REPORT_DESCRIPTOR = intArrayOf( 0x06, 0x00, 0xff, 0x09, 0x01, 0xa1, 0x01, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, @@ -115,23 +78,13 @@ object SteamDeckPad { 0xc0, ).map { it.toByte() }.toByteArray() - /** The wired controller's: its 0x42 state report in place of the Bluetooth 0x45. */ private val TRITON_WIRED_REPORT_DESCRIPTOR = TRITON_REPORT_DESCRIPTOR.copyOf().also { it[15] = 0x42 } - /** Where [prepare] puts the stand-in listings; bound into the guest at the same path. */ fun listingDir(sessionRoot: File) = File(sessionRoot, "sys/deck/listing") - /** Writes the tree and returns the `host:guest` binds for it, or none if it could not be made. */ - /** The pad's own devices by minor (the Deck, its Steam Controller, the PlayStation pads): guest paths. */ private val padNodes = mutableMapOf<Int, String>() @Volatile private var hidrawClassDir: File? = null - /** - * Lists only the device the pad is now ([padType], FakeInputWriter.PAD_TYPE_*) in - * /sys/class/hidraw, none for an Xbox 360 pad. Device scans then find what is plugged in: SDL's - * (the PlayStation pads' driver) keeps a device it found and could not open, and never tries it - * again, while one that disappears from the listing is let go. Before the shim is told. - */ @Synchronized fun showPad(padType: Int) { val dir = hidrawClassDir ?: return @@ -200,11 +153,8 @@ object SteamDeckPad { padNodes.clear() padNodes[MINOR] = HIDRAW - // Only the names matter: libudev reads a listing for the subsystems to descend into and - // then opens /sys/class/hidraw itself, which is the bind above. val listing = listingDir(sessionRoot) for (name in listOf("sys/bus", "sys/class", "sys/devices", "class/hidraw", "bus")) File(listing, name).mkdirs() - // The runtime binds this directory over /sys/dev/char for the GPU (LinuxRuntime.bindGpuNode). link(File(context.cacheDir, "drm/sys/$MAJOR:$MINOR"), HIDRAW) val udevData = File(LinuxRuntime.rootDir(context), "run/udev/data").apply { mkdirs() } @@ -238,8 +188,6 @@ object SteamDeckPad { Log.i(TAG, "deck pad: /dev/$TOUCH_NODE described as Steam's touch controller (0000:11fb)") } - // A Steam Controller's HID device: the handheld's own pad as the wired one, or the paired - // one on Bluetooth. Both are always described; libfakeinput offers whichever is in use. fun describeTriton(wired: Boolean) { val hid = if (wired) TRITON_WIRED_HID else TRITON_HID val product = if (wired) "1302" else "1303" @@ -276,8 +224,6 @@ object SteamDeckPad { File(udevData, "c$MAJOR:$minor").writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n") Log.i(TAG, "steam controller: /dev/$node described as Triton (28de:$product)") } - // The handheld's own pad as a PlayStation controller, each a USB device of its own with - // the report descriptor libfakeinput serves (kDs4/kDualSenseReportDescriptor). fun describeSony(dualSense: Boolean) { val bus = if (dualSense) 5 else 4 val product = if (dualSense) "0ce6" else "09cc" diff --git a/app/src/main/java/com/droiddeck/launcher/session/SteamDownloadMonitor.kt b/app/src/main/java/com/droiddeck/launcher/session/SteamDownloadMonitor.kt index af8515fbb..1744dc28e 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SteamDownloadMonitor.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SteamDownloadMonitor.kt @@ -17,11 +17,6 @@ internal class SteamDownloadMonitor { private var lastReceivedBytes = -1L private var lastReceivedAt = 0L - /** - * [receivedBytes] is the app uid's received-byte counter (the guest runs as that uid), or a - * negative value when Android does not report one. Steam writes its rate line about once a - * minute and flushes manifest counters even less often, so live traffic is what bridges them. - */ fun poll(runtimeRoot: File, now: Long = System.currentTimeMillis(), receivedBytes: Long = -1L): Boolean { val current = manifests(runtimeRoot).associate { it.path to read(it) } val progressed = current.any { (path, state) -> @@ -128,7 +123,6 @@ internal class SteamDownloadMonitor { private companion object { const val MANIFEST_PROGRESS_GRACE_MS = 8_000L - // Longer than Steam's ~60 s between rate lines, for when no traffic counter is available. const val CONTENT_ACTIVITY_GRACE_MS = 90_000L const val MIN_RECEIVE_BYTES_PER_SEC = 64L * 1024L val PATH = Regex("\"path\"\\s+\"([^\"]+)\"") diff --git a/app/src/main/java/com/droiddeck/launcher/session/SteamLanguage.kt b/app/src/main/java/com/droiddeck/launcher/session/SteamLanguage.kt index 11bb6d303..166dc8e23 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SteamLanguage.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SteamLanguage.kt @@ -2,14 +2,9 @@ package com.droiddeck.launcher.session import java.util.Locale -/** - * Steam's name for a language: the API language codes the client and the Steamworks API use - * ("english", "schinese", "koreana"...), for every language Steam's interface comes in. - */ object SteamLanguage { const val DEFAULT = "english" - /** Every language the Steam client's interface is offered in. */ val all: Set<String> = setOf( "arabic", "bulgarian", "schinese", "tchinese", "czech", "danish", "dutch", "english", "finnish", "french", "german", "greek", "hungarian", "indonesian", "italian", "japanese", @@ -17,16 +12,11 @@ object SteamLanguage { "spanish", "latam", "swedish", "thai", "turkish", "ukrainian", "vietnamese", "malay", ) - /** Spanish-speaking regions Steam serves with Latin American Spanish rather than Spain's. */ private val latinAmerica = setOf( "419", "AR", "BO", "CL", "CO", "CR", "CU", "DO", "EC", "GT", "HN", "MX", "NI", "PA", "PE", "PR", "PY", "SV", "US", "UY", "VE", ) - /** - * Steam's language for [locale]. [region] stands in for the locale's own when it has none: the - * app's Spanish carries no region, and a player in Mexico should get Latin American Spanish. - */ fun forLocale(locale: Locale, region: String? = null): String { val country = locale.country.ifEmpty { region.orEmpty() } return when (locale.language) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/StorageDiagnostics.kt b/app/src/main/java/com/droiddeck/launcher/session/StorageDiagnostics.kt index 73b64fb44..62e00be14 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/StorageDiagnostics.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/StorageDiagnostics.kt @@ -5,14 +5,9 @@ import android.system.Os import android.util.Log import java.io.File -/** The opt-in snapshot written beside a session's other logs. */ object StorageDiagnostics { private const val TAG = "StorageDiagnostics" - /** - * Records facts about the library actually selected for this session and returns its device - * number for the preload shim. The path and volume ID stay out of the shared log. - */ fun writeSnapshot( sessionDir: File, library: File, @@ -46,7 +41,6 @@ object StorageDiagnostics { } } - /** The longest matching mount entry determines the filesystem for nested bindable folders. */ private fun filesystemType(path: File): String? = runCatching { val canonical = path.canonicalPath File("/proc/mounts").useLines { lines -> diff --git a/app/src/main/java/com/droiddeck/launcher/session/WinComponentNames.kt b/app/src/main/java/com/droiddeck/launcher/session/WinComponentNames.kt index 5a8d38f5e..e9720e0f9 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/WinComponentNames.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/WinComponentNames.kt @@ -1,11 +1,5 @@ package com.droiddeck.launcher.session -/** - * What to call a catalog component on screen. Bannerlator's catalog keys (vcredist2010_dll, - * oalinst_dll, d3dcompiler_47...) have no display name, and some descriptions do not fit the entry, - * so names come from here: families by pattern, the rest from a table, the key itself otherwise. - * The "_dll" twin of an installer (the direct DLL copy) shares the installer's name. - */ object WinComponentNames { private val TABLE = mapOf( "oalinst" to "OpenAL", "XLiveRedist" to "Games for Windows Live", "gfw" to "Games for Windows Live", diff --git a/app/src/main/java/com/droiddeck/launcher/session/WinComponents.kt b/app/src/main/java/com/droiddeck/launcher/session/WinComponents.kt index 20e3f374c..c79171df2 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/WinComponents.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/WinComponents.kt @@ -19,23 +19,6 @@ import java.io.FileOutputStream import java.security.MessageDigest import java.util.zip.ZipInputStream -/** - * Windows components (OpenAL, d3dx9, XAudio, VC++ DLLs...) for a game's Proton prefix, picked per - * game, from Bannerlator's catalog (components.json on winlator-contents, Bottles' manifest format). - * - * Installing one runs its steps once, into the runtime: the archives it names are downloaded and - * its DLLs copied to /opt/droiddeck/wincomponents/<name>/{system32,syswow64}, with component.json - * holding its DLL overrides and the components it bundles. The picks go to the runtime as - * ~/.config/droiddeck/wincomponents.json, and droiddeck-wincomponents copies the picked components - * into the game's prefix at every launch (see that script for why every launch). - * - * A component whose installer is a Windows Installer package (.msi: XNA, PhysX, Games for Windows - * Live, MSXML...) installs here too, without running it: droiddeck-msi-install reads the package's - * tables in the runtime and lays out its files (Program Files, the .NET GAC, winsxs) and registry - * values the way Wine's msiexec would, under the same store folder. One that runs any other - * installer (.exe), unpacks a cabinet or edits the prefix's registry by itself is listed with the - * reason it cannot be installed yet. - */ object WinComponents { private const val TAG = "WinComponents" const val CATALOG_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/main/components.json" @@ -43,46 +26,23 @@ object WinComponents { private const val GUEST_SELECTION = "root/.config/droiddeck/wincomponents.json" private const val STORE = "opt/droiddeck/wincomponents" private val ID = Regex("[A-Za-z0-9][A-Za-z0-9._-]{0,63}") - // register_dll is left out on purpose: the DLLs it names are ones Wine ships and has registered - // at the same path, so the native file at that path is the one the registration loads. private val FILE_STEPS = setOf("download_archive", "archive_extract", "copy_dll", "copy_file", "override_dll", "register_dll") private val INSTALLER_STEPS = setOf("install_exe", "install_msi") - /** - * Steps done here without any installer running: a Windows version or registry value for the - * prefix (written by droiddeck-wincomponents with the rest), files picked out of a cabinet, and - * "uninstall Wine Mono", which the mscoree override beside it already does for the game. - */ private val OFFLINE_STEPS = setOf("set_windows", "set_register_key", "get_from_cab", "uninstall") - /** - * Installers that are their own setup program (NSIS, InnoSetup, IExpress scripts): nothing - * inside them is a package to lay out, so they wait for a way to run them. - */ private val RUNS_OWN_SETUP = setOf( "K-Lite", "ffdshow", "lavfilters702", "lavfilters741", "quicktime72", "dirac", "webview2", "aairruntime", "gfw", "ie8_kb2936068", "art2k7min", "vcredist6sp6", "VulkanRT", "jet40", "mdac28", "oalinst", "dotnet20", "dotnet20sp1", "dotnet35", "dotnet35sp1", ) private const val MSI_INSTALL = "/usr/local/bin/droiddeck-msi-install" - /** Where a downloaded package or installer waits for the installer, inside the runtime so it can read it. */ private const val MSI_CACHE = "$STORE/.packages" - /** - * Catalog entries every Proton here already ships, which an install would only shadow: Wine - * Gecko and Mono, the Chinese/Japanese/Korean fonts (Malgun, MS Gothic, YaHei, SimSun are in - * share/fonts of Proton Experimental, GE-Proton and proton-cachyos alike), Wine's own - * PowerShell wrapper and the Vulkan loader (vulkan-1.dll, 32- and 64-bit, in all three). - */ private val PROTON_PROVIDES = setOf("gecko", "mono", "cjkfonts", "powershell", "VulkanRT") - /** The keys the catalog nests under "environment" on a few .NET entries. */ private val NESTED_KEYS = setOf("url", "file_name", "file_checksum", "file_size") fun validId(id: String): Boolean = ID.matches(id) && ".." !in id private val BACKGROUND = ThreadLocal<Boolean>() - /** - * Runs [block] as background work: the runtime helpers an install starts on this thread run - * at the lowest CPU and I/O priority (nice 19, ionice idle where the runtime has it). - */ fun <T> background(block: () -> T): T { BACKGROUND.set(true) try { return block() } finally { BACKGROUND.remove() } @@ -101,58 +61,35 @@ object WinComponents { class Component( val name: String, val description: String, val provider: String, val status: String, val dependencies: List<String>, val steps: List<Step>, - /** - * A recording of what the component's own installer leaves in a prefix (winlator-contents' - * component-snapshots release, made by tools/snapshot/record.py): the files it places and - * the registry values it writes. With one, the installer never runs: the files come from - * the archive beside the recording or out of the vendor's installer, the registry from the - * recording. Empty for a component without one. - */ val snapshot: String = "", ) enum class Support { READY, NEEDS_INSTALLER, UNSUPPORTED } - /** The two halves of an install the page shows one after the other, each 0..100. */ enum class Phase { DOWNLOAD, INSTALL } - /** Progress: the component being worked on, what it is doing, the phase, 0..100 or -1 when unknown. */ class Progress(val component: String, val stage: String, val phase: Phase, val percent: Int) - /** The percent a droiddeck-msi-install progress line carries ("progress 37% placing files"), or -1. */ private val PERCENT_LINE = Regex("""^(\d{1,3})% (.*)$""") private fun engineLine(text: String): Pair<String, Int> = PERCENT_LINE.find(text)?.let { it.groupValues[2] to it.groupValues[1].toInt().coerceIn(0, 100) } ?: (text to -1) - /** - * A step that installs a Windows Installer package, or an installer .exe that is a wrapper - * around packages (a WiX bundle, a self-extracting cabinet or 7-Zip archive), which - * droiddeck-msi-install lays out here without running it. - */ private fun isPackage(step: Step): Boolean = step.action in INSTALLER_STEPS && step.str("url").startsWith("https://") && (step.action == "install_msi" || listOf(step.str("file_name"), step.str("url").substringBefore('?')) .any { it.endsWith(".msi", ignoreCase = true) || it.endsWith(".exe", ignoreCase = true) }) - /** - * Catalog entries nothing here can use: the IE8 security update patches a browser no prefix - * has; the OpenAL setup program is a twin of oalinst_dll, which copies the same DLLs; and - * WebView2 can only be installed by running Microsoft's installer in the prefix (its files - * sit in a container nothing opens and may not be hosted), which no runtime here does. - */ private val POINTLESS = setOf("ie8_kb2936068", "oalinst", "webview2") private fun protonProvides(name: String): Boolean = name in PROTON_PROVIDES || name in POINTLESS || name.startsWith("mono-") fun hasSnapshot(c: Component): Boolean = c.snapshot.startsWith("https://github.com/") && c.snapshot.endsWith(".snapshot.json") - /** The steps a recording makes redundant: everything that places files or edits the registry. */ private val SNAPSHOT_COVERS = INSTALLER_STEPS + setOf("download_archive", "archive_extract", "copy_dll", "copy_file") + OFFLINE_STEPS fun fetch(): List<Component>? = Downloader.downloadString(CATALOG_URL)?.let { parse(it) } private const val CATALOG_CACHE = "wincomponents-catalog.json" - /** The catalog from the network, kept for later; the copy kept last time when offline. */ fun fetch(context: Context): List<Component>? { val cache = File(context.filesDir, CATALOG_CACHE) Downloader.downloadString(CATALOG_URL)?.let { body -> @@ -161,7 +98,6 @@ object WinComponents { return runCatching { parse(cache.readText()) }.getOrNull() } - /** The copy of the catalog kept by [fetch], without asking the network; null before the first fetch. */ fun cached(context: Context): List<Component>? = runCatching { parse(File(context.filesDir, CATALOG_CACHE).readText()) }.getOrNull() @@ -186,21 +122,14 @@ object WinComponents { } } - /** - * The catalog entry that installs [name] here: the detector names installers - * (vcredist2022, oalinst), and their DLL-copy twin (vcredist2022_dll, oalinst_dll) is the one - * that installs without running anything; [name] itself when there is no such twin. - */ fun installable(name: String, all: Map<String, Component>): String { val twin = all[if (name == "oalinst") "oalinst_dll" else "${name}_dll"] return if (twin != null && support(twin, all) == Support.READY) twin.name else name } - /** Whether [c] can be installed here, its bundled components included. */ fun support(c: Component, all: Map<String, Component>, depth: Int = 0): Support { if (c.status != "ready" || depth > 8 || protonProvides(c.name)) return Support.UNSUPPORTED val installers = c.steps.filter { it.action in INSTALLER_STEPS } - // A recording of the installer's result stands in for running it, whatever the steps say. if (hasSnapshot(c)) { return c.dependencies.map { all[it]?.let { d -> support(d, all, depth + 1) } ?: Support.UNSUPPORTED } .maxOrNull() ?: Support.READY @@ -219,7 +148,6 @@ object WinComponents { } private fun fileStepOk(step: Step): Boolean = when (step.action) { - // An .exe or .cab archive (DirectX's redistributable) is opened by the runtime's tools. "download_archive", "archive_extract" -> step.str("url").let { url -> !url.startsWith("http") || url.startsWith("https://github.com/") && url.substringBefore('?').lowercase().let { it.endsWith(".tar.xz") || it.endsWith(".zip") || it.endsWith(".exe") || it.endsWith(".cab") } @@ -228,20 +156,14 @@ object WinComponents { else -> true } - /** The installed version of a component, or null. */ fun installed(context: Context, id: String): String? = runCatching { JSONObject(File(LinuxRuntime.rootDir(context), "$STORE/$id/component.json").readText()).optString("version", "?") }.getOrNull() - /** Every component in the runtime, by id. */ fun installedIds(context: Context): List<String> = File(LinuxRuntime.rootDir(context), STORE).listFiles { f -> f.isDirectory && validId(f.name) } .orEmpty().map { it.name }.filter { installed(context, it) != null }.sorted() - /** - * Installs [c] and the components it bundles that are not installed yet. [onProgress] gets a - * line for the step and 0..100 (or -1). Returns null on success, else a message. - */ fun install(context: Context, c: Component, all: Map<String, Component>, onProgress: (Progress) -> Unit): String? { if (support(c, all) != Support.READY) return "${c.name} cannot be installed here yet" val order = ArrayList<Component>() @@ -251,8 +173,6 @@ object WinComponents { if (x.name == c.name || installed(context, x.name) == null) order.add(x) } visit(c, 0) - // One download per archive however many components copy out of it (DirectMusic's eleven - // parts all come from dxnt, 100 MB). val work = File(context.cacheDir, "wincomponents").apply { FileUtils.delete(this); mkdirs() } val archives = HashMap<String, File>() return try { @@ -276,10 +196,7 @@ object WinComponents { val overrides = ArrayList<String>() var source: File? = null var msi: JSONObject? = null - // The registry values the steps themselves set (a Windows version, a key), beside the packages'. val registry = ArrayList<JSONObject>() - // Archives the runtime's tools opened (installer .exe, cabinets), by the name the catalog - // calls them, and the folders they were opened into; cleaned up at the end. val runtimeArchives = HashMap<String, File>() val runtimeDirs = ArrayList<File>() val cabs = HashMap<String, File>() @@ -293,8 +210,6 @@ object WinComponents { for (step in c.steps) when { hasSnapshot(c) && step.action in SNAPSHOT_COVERS -> Unit else -> when (step.action) { - // A component may have several packages (PowerShell's 32- and 64-bit): each adds to the - // same folder, the first one names the component. "install_msi", "install_exe" -> { val (result, problem) = installPackage(context, c, step, staging, onProgress) if (result == null) return problem @@ -303,15 +218,12 @@ object WinComponents { result.optJSONArray("notes")?.let { for (i in 0 until it.length()) notes.put(it.get(i)) } } ?: result } - // A catalog step that makes room for the installer's own copy; ours is copied anyway. "delete_dlls" -> Unit "download_archive", "archive_extract" -> { val url = step.str("url") if (!url.startsWith("http")) continue source = archives[url] ?: run { val name = url.substringBefore('?').substringAfterLast('/') - // A zip or tar.xz is opened here; an .exe or .cab by the runtime's tools, so it - // is downloaded where the runtime can read it and kept for get_from_cab steps. val byRuntime = name.lowercase().let { it.endsWith(".exe") || it.endsWith(".cab") } val file = if (byRuntime) File(root, "$MSI_CACHE/${archives.size}-$name").apply { parentFile?.mkdirs() } else File(work, "${archives.size}-$name") @@ -333,8 +245,6 @@ object WinComponents { dir.also { archives[url] = it } } } - // Files picked out of a cabinet: one the catalog downloaded (DirectX's redistributable, - // a self-extracting cabinet), or cabinets an earlier pick put under temp/. "get_from_cab" -> { val pattern = step.str("source") val containers = runtimeArchives[pattern]?.let { listOf(it) } ?: run { @@ -362,7 +272,6 @@ object WinComponents { } } } - // The Windows version a game sees: HKCU\Software\Wine "Version", the last word wins. "set_windows" -> step.str("version").takeIf { it.isNotEmpty() }?.let { version -> registry.removeAll { it.optString("key") == "Software\\Wine" && it.optString("name") == "Version" } registry += JSONObject().put("hive", "HKCU").put("key", "Software\\Wine").put("name", "Version").put("type", "sz").put("data", version) @@ -385,8 +294,6 @@ object WinComponents { } registry += value } - // "Uninstall Wine Mono" before a real .NET Framework: the mscoree override the catalog - // sets beside it is what takes Mono out of the game's way; nothing to remove here. "uninstall" -> Unit "copy_dll", "copy_file" -> { val from = source ?: return "a copy step before any archive" @@ -424,7 +331,6 @@ object WinComponents { FileUtils.delete(temp) } if (registry.isNotEmpty()) { - // The packages' values are already in registry.json; the steps' own go after them. val file = File(staging, "registry.json") val values = runCatching { JSONObject(file.readText()).optJSONArray("values") }.getOrNull() ?: JSONArray() registry.forEach { values.put(it) } @@ -444,13 +350,6 @@ object WinComponents { return null } - /** - * Lays the component out from its recording: downloads the snapshot, then its files (the - * archive beside it when the recording carries them, else out of the vendor's installer by - * the paths the recording maps, small generated ones inline) into [staging]/drive_c, and the - * recorded registry values into registry.json. Returns a result like the MSI engine's, or - * null and why. - */ private fun installSnapshot(context: Context, c: Component, staging: File, work: File, onProgress: (Progress) -> Unit): Pair<JSONObject?, String> { val root = LinuxRuntime.rootDir(context) val name = c.snapshot.substringAfterLast('/') @@ -481,7 +380,6 @@ object WinComponents { file.delete() placed = drive.walkTopDown().count { it.isFile } } else { - // The files come out of the installer the catalog names, by the paths the recording maps. val step = c.steps.firstOrNull { it.action in INSTALLER_STEPS && it.str("url").startsWith("https://") } ?: return null to "the recording needs the installer, which the catalog does not name" val url = step.str("url") @@ -532,14 +430,9 @@ object WinComponents { .put("notes", JSONArray(notes + listOfNotNull(installerVersion.takeIf { it.isNotEmpty() }?.let { "recorded from $it on ${snapshot.optString("wine")}" }))) to "" } - /** The regex for a catalog file pattern: * matches anything, case does not matter. */ private fun globRegex(pattern: String) = Regex("^" + pattern.split("*").joinToString(".*") { Regex.escape(it) } + "$", RegexOption.IGNORE_CASE) - /** - * Has the runtime's tools open [archive] (an installer .exe, a cabinet, a self-extracting - * one) into a folder of the package cache called [tag]; that folder, or null and a log line. - */ private fun unpackInRuntime(context: Context, c: Component, archive: File, tag: String, onProgress: (Progress) -> Unit): File? { val root = LinuxRuntime.rootDir(context) val cache = File(root, MSI_CACHE) @@ -563,12 +456,6 @@ object WinComponents { return dir } - /** - * Downloads the component's package (.msi) or installer (.exe) into the runtime and has - * droiddeck-msi-install lay it out in [staging]. Returns the installer's result (product, - * version, counts, notes), or null and why. Everything the installer prints also goes to - * files/logs/tools. - */ private fun installPackage(context: Context, c: Component, step: Step, staging: File, onProgress: (Progress) -> Unit): Pair<JSONObject?, String> { val root = LinuxRuntime.rootDir(context) val url = step.str("url") @@ -641,10 +528,6 @@ object WinComponents { Log.w(TAG, "extract ${archive.name}", e); false } - /** - * Bannerlator's rule: the manifest's own temp paths are Bottles', so the files are found by name - * anywhere in the unpacked tree, preferring the ones under the matching win32/win64 folder. - */ private fun copyMatching(srcRoot: File, pattern: String, dest: File, arch: String?) { if (pattern.isEmpty()) return val rx = Regex("^" + pattern.split("*").joinToString(".*") { Regex.escape(it) } + "$", RegexOption.IGNORE_CASE) @@ -658,7 +541,6 @@ object WinComponents { matches.forEach { f -> f.copyTo(File(dest, f.name.lowercase()), overwrite = true) } } - /** Removes a component from the runtime and from every game's picks. */ @Synchronized fun uninstall(context: Context, id: String) { require(validId(id)) @@ -666,17 +548,8 @@ object WinComponents { save(context, selections(context).mapValues { (_, sel) -> Selection(sel.auto - id, sel.user - id) }) } - /** - * Why a component was turned on for a game by itself ([setAuto]): [kind] says whose list it came - * from (see AutoComponents), [args] what the reason line names. Kept beside the picks. - */ class AutoReason(val kind: String, val args: List<String>) - /** - * Everything kept per game, by the appid its prefix is named after: [auto] - turned on by - * itself, with its reason; [user] - a switch the user pressed (true on, false off), which wins - * over [auto] either way. What the launch reads is [effective]. - */ class Selection(val auto: Map<String, AutoReason>, val user: Map<String, Boolean>) { val effective: List<String> get() = (auto.keys.filter { user[it] != false } + user.filterValues { it }.keys).distinct() @@ -697,7 +570,6 @@ object WinComponents { AutoReason(r.optString("kind"), (0 until a.length()).map { a.optString(it) }) } }.orEmpty() - // Before there were overrides, every pick was the user's own. val user = if (users == null) games.optJSONArray(app)?.let { ids -> (0 until ids.length()).map { ids.getString(it) }.filter { validId(it) }.associateWith { true } }.orEmpty() else users.optJSONObject(app)?.let { o -> o.keys().asSequence().filter { validId(it) }.associateWith { o.optBoolean(it) } }.orEmpty() Selection(auto, user) @@ -706,7 +578,6 @@ object WinComponents { emptyMap() } - /** Each game's picks as the launch takes them, by the appid its prefix is named after. */ fun read(context: Context): Map<String, List<String>> = selections(context).mapValues { it.value.effective }.filterValues { it.isNotEmpty() } @@ -714,7 +585,6 @@ object WinComponents { fun selection(context: Context, appKey: String): Selection = selections(context)[appKey] ?: Selection(emptyMap(), emptyMap()) - /** The user's own switch for [id] in [appKey]: on or off, over whatever was picked by itself. */ @Synchronized fun setUser(context: Context, appKey: String, id: String, on: Boolean) { require(validId(id)) @@ -724,7 +594,6 @@ object WinComponents { save(context, all) } - /** The components picked by themselves for [appKey], replacing the earlier ones; the user's switches stay. */ @Synchronized fun setAuto(context: Context, appKey: String, auto: Map<String, AutoReason>) { require(auto.keys.all { validId(it) }) @@ -734,7 +603,6 @@ object WinComponents { save(context, all) } - /** The user's picks for [appKey] set outright (each listed on, every other user switch dropped). */ @Synchronized fun setPicks(context: Context, appKey: String, ids: List<String>) { require(ids.all { validId(it) }) @@ -746,7 +614,6 @@ object WinComponents { private fun save(context: Context, games: Map<String, Selection>) { val kept = games.filterValues { it.auto.isNotEmpty() || it.user.isNotEmpty() } - // Version 1 still: droiddeck-wincomponents reads "games" and ignores the rest. val text = JSONObject().put("version", 1) .put("games", JSONObject().apply { kept.forEach { (app, sel) -> sel.effective.takeIf { it.isNotEmpty() }?.let { put(app, JSONArray(it)) } } }) .put("auto", JSONObject().apply { @@ -762,11 +629,6 @@ object WinComponents { publish(context, text) } - /** - * Writes the picks where the launch script reads them; SessionFiles calls this at every session - * start too, so a runtime reinstalled since keeps them. An empty selection still writes a file, - * which is how a component turned off for every game is taken out of their prefixes. - */ @Synchronized fun publish(context: Context, text: String? = null) { val body = text ?: runCatching { AtomicFile(File(context.filesDir, SELECTION)).readFully().toString(Charsets.UTF_8) }.getOrNull() ?: return diff --git a/app/src/main/java/com/droiddeck/launcher/store/FlathubApi.kt b/app/src/main/java/com/droiddeck/launcher/store/FlathubApi.kt index a26c4d4a4..b95a4c10e 100644 --- a/app/src/main/java/com/droiddeck/launcher/store/FlathubApi.kt +++ b/app/src/main/java/com/droiddeck/launcher/store/FlathubApi.kt @@ -9,10 +9,6 @@ import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder -/** - * Flathub's public API (flathub.org/api/v2): the collections its front page shows, search, and an - * app's AppStream data. Only what this device can install is kept - apps built for aarch64. - */ object FlathubApi { private const val TAG = "FlathubApi" private const val BASE = "https://flathub.org/api/v2" @@ -33,7 +29,6 @@ object FlathubApi { val runtime: String?, val downloadSize: Long, val installedSize: Long, val arches: List<String>, ) - /** A category as Flathub's search filters it, with the name shown for it. */ class Category(val id: String, @StringRes val label: Int) val categories = listOf( @@ -43,14 +38,11 @@ object FlathubApi { Category("Utility", R.string.store_cat_utilities), ) - /** Front-page collections: popular, trending, recently-updated, recently-added. */ fun collection(name: String, count: Int = 30): List<AppSummary>? { - // The collections are not filtered by architecture; ask for more and keep what fits. val body = get("$BASE/collection/$name?page=1&per_page=${count * 2}") ?: return null return hits(body)?.take(count) } - /** Search, optionally within one category (Flathub's main_categories facet). */ fun search(query: String, category: String? = null, count: Int = 48): List<AppSummary>? { val filters = JSONArray().put(JSONObject().put("filterType", "arches").put("value", ARCH)) if (category != null) filters.put(JSONObject().put("filterType", "main_categories").put("value", category)) @@ -60,7 +52,6 @@ object FlathubApi { return hits(body) } - /** Flathub's own copy of an app's icon: for apps that export only an SVG, which is not drawn here. */ fun iconUrl(id: String) = "https://dl.flathub.org/media/icons/128x128/$id.png" fun details(id: String): AppDetails? { @@ -120,7 +111,6 @@ object FlathubApi { Log.w(TAG, "hits: $e"); null } - /** AppStream descriptions are a small HTML subset: paragraphs and lists become lines. */ internal fun plainText(html: String): String = html .replace(Regex("(?i)<li>\\s*"), "• ") .replace(Regex("(?i)</(p|li|ul|ol)>"), "\n") diff --git a/app/src/main/java/com/droiddeck/launcher/store/StoreState.kt b/app/src/main/java/com/droiddeck/launcher/store/StoreState.kt index cd47019d8..c03a98246 100644 --- a/app/src/main/java/com/droiddeck/launcher/store/StoreState.kt +++ b/app/src/main/java/com/droiddeck/launcher/store/StoreState.kt @@ -11,10 +11,6 @@ import androidx.compose.runtime.setValue import com.droiddeck.launcher.R import com.droiddeck.launcher.runtime.FlatpakManager -/** - * The store's state for the life of the process, so an install carries on - and its progress - * shows - while the user moves around the launcher or plays something meanwhile. - */ object StoreState { private val main = Handler(Looper.getMainLooper()) @@ -26,10 +22,8 @@ object StoreState { private set var checkingUpdates by mutableStateOf(false) private set - /** Flathub has been asked this session, so an empty [updates] means up to date. */ var updatesChecked by mutableStateOf(false) private set - /** What is running: "setup", an app id, or "update-all"; null when idle. */ var busy by mutableStateOf<String?>(null) private set var stage by mutableStateOf<String?>(null) @@ -37,7 +31,6 @@ object StoreState { var percent by mutableStateOf(-1) private set - /** The front page's sections, by collection name; absent until loaded, empty list on failure. */ val sections = mutableStateMapOf<String, List<FlathubApi.AppSummary>>() var sectionsFailed by mutableStateOf(false) private set @@ -49,7 +42,6 @@ object StoreState { var searching by mutableStateOf(false) private set - /** An app whose Flathub page was asked for from outside the store (its card on Desktop), once. */ var openRequest by mutableStateOf<String?>(null) private var searchSeq = 0 diff --git a/app/src/main/java/com/droiddeck/launcher/store/UserAppsState.kt b/app/src/main/java/com/droiddeck/launcher/store/UserAppsState.kt index 340eabfbf..9979b0e5d 100644 --- a/app/src/main/java/com/droiddeck/launcher/store/UserAppsState.kt +++ b/app/src/main/java/com/droiddeck/launcher/store/UserAppsState.kt @@ -10,31 +10,23 @@ import androidx.compose.runtime.setValue import com.droiddeck.launcher.R import com.droiddeck.launcher.runtime.UserApps -/** - * The Desktop page's added apps and the one add or remove in progress. State changes only on the - * main thread; the work runs on a thread of its own and posts back. - */ object UserAppsState { private val main = Handler(Looper.getMainLooper()) var items by mutableStateOf<List<UserApps.App>>(emptyList()) private set - /** What is being added or removed; null when idle. */ var working by mutableStateOf<String?>(null) private set var stage by mutableStateOf<String?>(null) private set var percent by mutableStateOf(-1) private set - /** Why the last add or remove failed, shown on the page until the next one. */ var lastError by mutableStateOf<String?>(null) private set - /** The app whose updates are being looked up, and what each lookup found, by app key. */ var checking by mutableStateOf<String?>(null) private set var updates by mutableStateOf<Map<String, UserApps.UpdateCheck>>(emptyMap()) private set - /** Bumped per listing; only the newest one is shown. */ private var listing = 0 fun refresh(context: Context) { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/AchievementNotifications.kt b/app/src/main/java/com/droiddeck/launcher/stores/AchievementNotifications.kt index 818bd0f37..e6bfc35f6 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/AchievementNotifications.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/AchievementNotifications.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import org.json.JSONObject -/** Session-owned unlocks; history reads and upload acknowledgements never create notifications. */ object AchievementNotifications { data class Unlock(val client: String, val id: String, val name: String, val description: String, val icon: String?, val game: String) @@ -38,7 +37,6 @@ object AchievementNotifications { if (pending.firstOrNull() === unlock) pending = pending.drop(1) } - /** Only presentation fields from the owned daemon, bounded before entering Compose. */ fun parse(event: JSONObject, game: String): Unlock? { val client = event.optString("client_id") val id = event.optString("id") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/CloudPlan.kt b/app/src/main/java/com/droiddeck/launcher/stores/CloudPlan.kt index 720b462d6..6368f558a 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CloudPlan.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CloudPlan.kt @@ -2,21 +2,7 @@ package com.droiddeck.launcher.stores import org.json.JSONObject -/** - * What a cloud sync may do, decided from three views of each save file: the local copy, the cloud - * copy, and the baseline - both as they were when this device last synced the game. Pure, so the - * rules are tested on their own: - * - * - No upload before a download has completed once on this device (no baseline): a first launch - * writes new profile files that must never replace the real saves in the cloud. - * - The first download (no baseline) lets the cloud win where the two differ - the local folder is - * backed up first. - * - A file changed on both sides since the baseline is a conflict: neither side is overwritten; the - * user picks Keep cloud or Keep local in Manage saves. - * - Otherwise the side that changed since the baseline wins; one that changed on neither is left. - */ object CloudPlan { - /** One file as this device last saw it in sync: the local MD5, and the cloud's MD5 (when known) and time. */ data class Entry(val localMd5: String, val cloudMd5: String?, val cloudModified: Long) class Local(val md5: String) @@ -24,11 +10,9 @@ object CloudPlan { class Plan(val transfer: List<String>, val conflicts: List<String>, val same: List<String>, val refused: String? = null) - /** The cloud copy differs from what the baseline recorded of it. */ private fun cloudChanged(r: Remote, b: Entry?): Boolean { if (b == null) return true if (r.md5 != null && b.cloudMd5 != null) return !r.md5.equals(b.cloudMd5, ignoreCase = true) - // Times only (Epic's listing): the service's own clock, so a couple of seconds of slack. return r.modified > 0 && b.cloudModified > 0 && kotlin.math.abs(r.modified - b.cloudModified) > 2000 } @@ -36,7 +20,6 @@ object CloudPlan { private fun same(l: Local, r: Remote): Boolean = r.md5 != null && r.md5.equals(l.md5, ignoreCase = true) - /** Cloud → device. [baseline] null = this device never synced the game. */ fun down(local: Map<String, Local>, remote: Map<String, Remote>, baseline: Map<String, Entry>?): Plan { val transfer = ArrayList<String>(); val conflicts = ArrayList<String>(); val same = ArrayList<String>() for ((name, r) in remote) { @@ -53,7 +36,6 @@ object CloudPlan { return Plan(transfer, conflicts, same) } - /** Device → cloud. Refused outright without a baseline. */ fun up(local: Map<String, Local>, remote: Map<String, Remote>, baseline: Map<String, Entry>?): Plan { if (baseline == null) return Plan(emptyList(), emptyList(), emptyList(), refused = "no-baseline") val transfer = ArrayList<String>(); val conflicts = ArrayList<String>(); val same = ArrayList<String>() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/CloudSavePaths.kt b/app/src/main/java/com/droiddeck/launcher/stores/CloudSavePaths.kt index c54297794..62133fbbd 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CloudSavePaths.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CloudSavePaths.kt @@ -1,28 +1,10 @@ -/* - * Cloud-save folder resolution for GOG and Epic games, after Bannerlator's GogCloudSavePaths and - * EpicCloudSavePaths (device-proven there, August 2026): a store's save-location template expanded - * against the game's Proton prefix and its install folder. - */ package com.droiddeck.launcher.stores import java.io.File -/** - * Where a store keeps a game's saves on Windows, made concrete inside this game's Proton prefix. - * - * GOG gives a template per game in its public remote-config (`cloudStorage.locations[].location`): - * a leading `<?TOKEN?>` (or `%TOKEN%`) and a path, e.g. ELDERBORN's - * `<?APPLICATION_DATA_LOCAL_LOW?>/Hyperstrange/ELDERBORN`. Epic gives `CloudSaveFolder` in the - * game's catalog attributes: a leading `{Token}`, with `{EpicId}` / `{AppName}` inside, e.g. - * `{AppData}/Game/Saved/SaveGames`. Profile tokens map under the prefix's Wine user - * (`drive_c/users/steamuser`), the install tokens to the game's folder. The path is walked against - * what is on disk case-insensitively; a result outside its boundary is refused, never guessed. - */ object CloudSavePaths { - /** The Wine user every prefix has (Proton's). */ fun profile(prefix: File): File = File(prefix, "drive_c/users/steamuser") - /** Expands a GOG location template; null for an unknown token or an escape. */ fun gog(template: String, prefix: File, install: File): File? { val norm = template.replace('\\', '/').trim().trimStart('/') val match = Regex("^(<\\?[^?>]+\\?>|%[^%]+%)").find(norm) ?: return null @@ -42,7 +24,6 @@ object CloudSavePaths { return walk(base, boundary, norm.substring(match.value.length), fuzzy = false) } - /** Expands an Epic CloudSaveFolder; null for an unknown token or an escape. */ fun epic(template: String, prefix: File, install: File, accountId: String, appName: String): File? { val norm = template.replace('\\', '/').trim().trimStart('/') val match = Regex("^\\{([^}]+)\\}").find(norm) ?: return null @@ -62,10 +43,6 @@ object CloudSavePaths { return walk(base, boundary, rest, fuzzy = true) } - /** - * Each segment matched against the folder's entries case-insensitively (and, for Epic, by letters - * and digits alone when that names exactly one), else taken as written - a download creates it. - */ private fun walk(base: File, boundary: File, rest: String, fuzzy: Boolean): File? { var dir = base for (seg in rest.trimStart('/').split('/').filter { it.isNotEmpty() && it != "." }) { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/CloudSaves.kt b/app/src/main/java/com/droiddeck/launcher/stores/CloudSaves.kt index e0f4ab6c1..3c3d38d26 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CloudSaves.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CloudSaves.kt @@ -17,34 +17,16 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import org.json.JSONObject -/** - * GOG and Epic cloud saves for an installed store game: before a launch the cloud copy is brought - * down where it is newer, after the game exits what changed goes up - newest wins, file by file. - * Before a download overwrites anything, the folder is copied to the app's storage (the last - * [BACKUPS] per game). Both launch paths reach it: the compat tool's request from - * droiddeck-store-launch ([StoreLaunchRequests]) for every launch, and the Games tab's Manage saves. - * - * The save folder: GOG's remote-config location template (by the game's Galaxy client id) or - * Epic's CloudSaveFolder catalog attribute, cached per game, made concrete by [CloudSavePaths] - * inside the game's Proton prefix. A game whose store gives none has no cloud saves. - */ object CloudSaves { private const val TAG = "CloudSaves" private const val BACKUPS = 3 private const val PARALLEL = 6 - /** What a sync did, for the log and the Manage saves row. */ - /** - * What a sync did, for the log and the Manage saves row. [result]: ok, skipped (nothing to do or - * not possible: [reason] says why), deferred (no prefix yet; it runs once Proton makes one), - * failed. [conflicts]: files changed on both sides, left alone. - */ class Result(val result: String, val files: Int, val bytes: Long, val reason: String, val conflicts: Int = 0) { val ok: Boolean get() = result != "failed" val line: String get() = "result=$result files=$files bytes=$bytes reason=$reason" + if (conflicts > 0) " conflicts=$conflicts" else "" } - /** A game's cloud state for the Manage saves row. */ class Status(val supported: Boolean, val folder: File?, val lastSync: Long, val reason: String, val conflicts: List<String> = emptyList()) private fun game(context: Context, store: Store, id: String): Pair<File, StoreGameSidecar>? = @@ -52,7 +34,6 @@ object CloudSaves { StoreGameSidecar.read(f)?.takeIf { it.store == store && it.id == id && it.isInstalled }?.let { f to it } } - /** The game's prefix: compatdata/<its shortcut appid>/pfx, wherever that library is. */ fun prefix(context: Context, folder: File): File? { val appId = AddedGames.scan(context).firstOrNull { it.folder.absolutePath == folder.absolutePath }?.appId ?: return null return Library.protonPrefix(context, appId and 0xFFFFFFFFL) @@ -64,9 +45,7 @@ object CloudSaves { private fun markSynced(context: Context, store: Store, id: String) = prefs(context, store).edit().putLong("cloud_sync_$id", System.currentTimeMillis()).apply() - // ---- the save folder ------------------------------------------------------------------------ - /** The store's template for this game: cached, else fetched; "" when the store gives none, null when it could not be asked. */ fun template(context: Context, store: Store, id: String, folder: File, sidecar: StoreGameSidecar): String? { val p = prefs(context, store) if (p.contains("cloud_template_$id")) return p.getString("cloud_template_$id", "") @@ -89,7 +68,6 @@ object CloudSaves { GogPrefs.get(context).getString("client_id_$id", null)?.ifEmpty { null } ?: runCatching { JSONObject(File(folder, "goggame-$id.info").readText()).optString("clientId").ifEmpty { null } }.getOrNull() - /** GOG's public remote-config: `content.Windows.cloudStorage.locations`, the one named "saves" first. */ private fun gogTemplate(context: Context, id: String, folder: File): String? { val clientId = gogClientId(context, id, folder) ?: return "" return try { @@ -109,7 +87,6 @@ object CloudSaves { } } - /** The concrete save folder in the prefix, or null with why. */ private fun saveFolder(context: Context, store: Store, id: String): Pair<File?, String> { val (folder, sidecar) = game(context, store, id) ?: return null to "not-installed" val template = template(context, store, id, folder, sidecar) ?: return null to "store-unreachable" @@ -128,7 +105,6 @@ object CloudSaves { return Status(reason != "no-cloud-saves" && reason != "not-installed", dir, lastSync(context, store, id), reason, conflicts(context, store, id)) } - // ---- transport ------------------------------------------------------------------------------ private fun transport(context: Context, store: Store, id: String, folder: File): CloudTransport? = when (store) { Store.GOG -> { @@ -147,13 +123,11 @@ object CloudSaves { Store.AMAZON -> null } - /** GOG cloud storage wants a token issued to the game's own client: the Galaxy refresh token, exchanged with the game's id and secret. */ private fun gogScopedToken(context: Context, id: String, clientId: String): String? { val secret = GogPrefs.get(context).getString("client_secret_$id", null)?.ifEmpty { null } ?: return null return GogAuth.gameToken(context, clientId, secret)?.accessToken } - // ---- sync ----------------------------------------------------------------------------------- private fun baseline(context: Context, store: Store, id: String): Map<String, CloudPlan.Entry>? = CloudPlan.fromJson(prefs(context, store).getString("cloud_baseline_$id", null)) @@ -161,29 +135,21 @@ object CloudSaves { private fun saveBaseline(context: Context, store: Store, id: String, b: Map<String, CloudPlan.Entry>) = prefs(context, store).edit().putString("cloud_baseline_$id", CloudPlan.toJson(b)).apply() - /** Files changed on both sides since the last sync, waiting for Keep cloud / Keep local. */ fun conflicts(context: Context, store: Store, id: String): List<String> = prefs(context, store).getString("cloud_conflicts_$id", "").orEmpty().split('\n').filter { it.isNotEmpty() } private fun saveConflicts(context: Context, store: Store, id: String, names: Collection<String>) = prefs(context, store).edit().putString("cloud_conflicts_$id", names.joinToString("\n")).apply() - /** Cloud → device where the cloud copy changed (the first time: wherever it differs). [force]: conflicts too (Keep cloud). Blocking. */ fun download(context: Context, store: Store, id: String, force: Boolean = false): Result = sync(context, store, id, "down") { dir, cloud -> downWith(context, store, id, dir, cloud, force) } - /** Device → cloud where the local copy changed, once a download has set the baseline. [force]: conflicts too (Keep local). Blocking. */ fun upload(context: Context, store: Store, id: String, force: Boolean = false): Result = sync(context, store, id, "up") { dir, cloud -> upWith(context, store, id, dir, cloud, force) } fun keepCloud(context: Context, store: Store, id: String): Result = download(context, store, id, force = true) fun keepLocal(context: Context, store: Store, id: String): Result = upload(context, store, id, force = true) - /** - * The pre-launch download, or - when Proton has not made the game's prefix yet (a first launch) - * - a deferred one: answered at once, then run in the background as soon as the prefix's user - * folder appears (within [PREFIX_WAIT_MS]), so the launch is not held up for it. - */ fun downloadOrDefer(context: Context, store: Store, id: String): Result { val app = context.applicationContext val g = game(app, store, id) ?: return done(store, id, "down", Result("skipped", 0, 0, "not-installed")) @@ -241,7 +207,6 @@ object CloudSaves { internal fun md5(data: ByteArray): String = MessageDigest.getInstance("MD5").digest(data).joinToString("") { "%02x".format(it) } - /** The cloud listing, with MD5 and time filled in (one HEAD each where the listing lacks them, in parallel). */ private fun remote(cloud: CloudTransport, wanted: (String) -> Boolean): Map<String, CloudFile> { val files = cloud.list() val pool = Executors.newFixedThreadPool(PARALLEL, com.droiddeck.launcher.stores.download.DownloadQueue.workerFactory("cloud")) @@ -272,7 +237,6 @@ object CloudSaves { next[name] = CloudPlan.Entry(md5(data), f.md5, if (modified > 0) modified else f.modifiedMs) } for (name in plan.same) remote[name]?.let { r -> next[name] = CloudPlan.Entry(local.getValue(name).md5, r.md5, r.modifiedMs) } - // A download that completed sets the baseline even when the cloud was empty: uploads may follow. saveBaseline(context, store, id, next) val left = if (force) emptyList() else plan.conflicts saveConflicts(context, store, id, left) @@ -290,7 +254,6 @@ object CloudSaves { val next = HashMap(base) for (name in plan.same) remote[name]?.let { r -> next[name] = CloudPlan.Entry(local.getValue(name).md5, r.md5, r.modifiedMs) } if (send.isNotEmpty()) { - // The cloud copies about to be replaced are kept first, as a download keeps the local ones. backupCloud(context, store, id, cloud, send.mapNotNull { remote[it] }) val data = send.associateWith { files.getValue(it).readBytes() } val stamped = cloud.put(data) @@ -307,15 +270,7 @@ object CloudSaves { return Result("ok", send.size, send.sumOf { files.getValue(it).length() }, if (send.isEmpty()) "up-to-date" else "uploaded", left.size) } - // ---- uploads that do not depend on one hook --------------------------------------------------- - /** - * A game launched with cloud saves is marked dirty in the app's storage when its pre-launch step - * runs. The upload happens on the first of: the compat tool's exit request (`exit`), the end of - * the session (`session-end`), or the app's next start for a mark a killed app left (`recovery`) - * - so a session closed from the drawer, a game Steam killed, or a crash still uploads. The mark - * goes only after an upload that went through, or one skipped on purpose with its reason logged. - */ private fun marks(context: Context) = context.applicationContext.getSharedPreferences("stores.cloud", Context.MODE_PRIVATE) fun markDirty(context: Context, store: Store, id: String) { @@ -329,10 +284,8 @@ object CloudSaves { private val markLock = Any() - /** Why a skip is final: retrying cannot change it, so the mark goes. */ private val FINAL_SKIPS = setOf("off", "no-cloud-saves", "no-baseline", "not-installed", "nothing-local") - /** Uploads [id] if it is marked dirty; null when it is not (another trigger got there first). */ fun uploadDirty(context: Context, store: Store, id: String, trigger: String): Result? = synchronized(markLock) { val key = "dirty:${store.id}:$id" if (!marks(context).contains(key)) return null @@ -341,10 +294,6 @@ object CloudSaves { r } - /** - * Every dirty game, uploaded; [running] says a game session is up, in which case `recovery` waits - * - a marked game may be the one running. - */ fun uploadAllDirty(context: Context, trigger: String, running: Boolean = com.droiddeck.launcher.session.SessionState.running) { if (trigger == "recovery" && running) return for ((store, id) in dirty(context)) uploadDirty(context, store, id, trigger) @@ -353,7 +302,6 @@ object CloudSaves { private fun backupRoot(context: Context, store: Store, id: String) = File(context.filesDir, "stores/cloud-backups/${store.id}-${id.replace(Regex("[^A-Za-z0-9._-]"), "_")}") - /** The local folder as it is, before a download changes it; the last [BACKUPS] of each kind are kept. */ private fun backup(context: Context, store: Store, id: String, dir: File, kind: String) { val root = backupRoot(context, store, id) runCatching { dir.copyRecursively(File(root, "$kind-${System.currentTimeMillis()}"), overwrite = true) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/CloudTransport.kt b/app/src/main/java/com/droiddeck/launcher/stores/CloudTransport.kt index 3446bf4ab..becf14e21 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CloudTransport.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CloudTransport.kt @@ -1,9 +1,3 @@ -/* - * The two stores' cloud-save services, after Bannerlator's GogCloudSaveManager and - * EpicCloudSaveManager (device-proven there, August 2026): GOG's cloudstorage (one object per - * file, Last-Modified and an MD5 ETag on a HEAD) and Epic's savesync data storage (a listing with - * lastModified and signed read links; signed write links asked for per upload). - */ package com.droiddeck.launcher.stores import java.io.ByteArrayOutputStream @@ -16,17 +10,12 @@ import java.util.TimeZone import org.json.JSONArray import org.json.JSONObject -/** One file in a game's cloud: its key (a path with '/'), when it was written (-1 unknown) and its MD5 when known. */ class CloudFile(val name: String, var modifiedMs: Long, var md5: String? = null, val readLink: String? = null) -/** A store's cloud for one game. Every call is blocking network. */ interface CloudTransport { fun list(): List<CloudFile> - /** Fills in [CloudFile.modifiedMs] / [CloudFile.md5] where the listing left them out. */ fun details(file: CloudFile) {} - /** The file's bytes and its cloud time (-1 unknown). */ fun get(file: CloudFile): Pair<ByteArray, Long> - /** Writes [files] (key → bytes); the cloud times assigned, where the service says (-1 unknown). */ fun put(files: Map<String, ByteArray>): Map<String, Long> } @@ -60,15 +49,12 @@ internal fun isoDate(s: String?): Long { } catch (e: Exception) { -1L } } -/** Thrown when the store says the game has no cloud saves for this client. */ class NoCloudSaves : Exception("no cloud saves") -/** GOG cloudstorage: `/v1/<userId>/<clientId>[/<key>]`. */ class GogCloud(private val userId: String, private val clientId: String, private val token: String) : CloudTransport { private val base = "https://cloudstorage.gog.com/v1/$userId/$clientId" private val ua = "GOG Galaxy" - /** A path segment: '+' from a space is wrong in a path (it stores a second, '+'-named copy). */ private fun key(name: String) = URLEncoder.encode(name, "UTF-8").replace("+", "%20") override fun list(): List<CloudFile> { @@ -84,7 +70,6 @@ class GogCloud(private val userId: String, private val clientId: String, private val text = c.body().decodeToString().trim() c.disconnect() if (text.isEmpty() || text == "[]") return emptyList() - // A plain newline-separated list of keys (checked on device); a JSON array is read too. if (text.startsWith("[")) { val a = JSONArray(text) return (0 until a.length()).mapNotNull { a.optJSONObject(it) }.mapNotNull { o -> @@ -122,12 +107,10 @@ class GogCloud(private val userId: String, private val clientId: String, private val code = c.responseCode c.disconnect() if (code !in 200..299) throw java.io.IOException("GOG cloud put HTTP $code") - // The server stamps the object itself; that time, so the next sync sees the two as equal. CloudFile(name, -1L).also { details(it) }.modifiedMs } } -/** Epic savesync: `/api/v1/access/egstore/savesync/<accountId>/<appName>/`. */ class EpicCloud(private val accountId: String, private val appName: String, private val token: String) : CloudTransport { private val base = "https://datastorage-public-service-liveegs.live.use1a.on.epicgames.com/api/v1/access/egstore/savesync/$accountId/$appName/" private val ua = "EpicGamesLauncher/15.17.1-22692490" @@ -146,7 +129,6 @@ class EpicCloud(private val accountId: String, private val appName: String, priv }.toList() } - /** The listing names a file `<...>/<appName>/<key>`; the key is what is under the save folder. */ private fun strip(key: String): String { val marker = "/$appName/" val i = key.indexOf(marker) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/CredentialCipher.kt b/app/src/main/java/com/droiddeck/launcher/stores/CredentialCipher.kt index 2a7e84357..aa0748423 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CredentialCipher.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CredentialCipher.kt @@ -11,44 +11,24 @@ import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec -/** - * The stores' sign-in files at rest: AES-256-GCM under a key that lives in the AndroidKeyStore and - * never leaves it, as a small versioned envelope `{"v":1,"alg":"AES/GCM","iv":...,"ct":...}` - * (base64). The session's Linux side shares the app's files directory but not its Keystore, so - * what it can see of a sign-in is the envelope only. - */ object CredentialCipher { const val VERSION = 1 const val ALG = "AES/GCM" private const val TRANSFORMATION = "AES/GCM/NoPadding" private const val TAG_BITS = 128 - /** Where the key comes from: the AndroidKeyStore on a device, a plain key in tests. */ fun interface KeyProvider { - /** The key, created on first use. Throws when no key can be had (no Keystore on this ROM). */ fun key(): SecretKey - /** The key that sealed earlier files: never a new one. Throws [KeyMissing] when there is none. */ fun existing(): SecretKey = key() } - /** The Keystore answered and holds no key: nothing sealed before can open again. */ class KeyMissing : java.security.GeneralSecurityException("no key") - /** - * Whether [e], from [open], means the file can never open: it does not authenticate (tampered, - * or sealed under another key), the key is permanently invalidated or gone, or the file is not - * an envelope. Anything else - the Keystore busy or erroring, an I/O error - may pass. - */ fun isPermanent(e: Throwable): Boolean = e is javax.crypto.AEADBadTagException || e is android.security.keystore.KeyPermanentlyInvalidatedException || e is KeyMissing || e is org.json.JSONException || e is IllegalArgumentException - /** - * An AES-256 key in the AndroidKeyStore: not exportable, encrypt/decrypt with GCM only, no user - * authentication (background downloads and launch-time code fetches use it with the screen - * off). StrongBox where the device has it, the TEE otherwise. - */ object Keystore : KeyProvider { private const val ALIAS = "droiddeck-store-credentials" private const val PROVIDER = "AndroidKeyStore" @@ -64,7 +44,7 @@ object CredentialCipher { val ks = KeyStore.getInstance(PROVIDER).apply { load(null) } (ks.getKey(ALIAS, null) as? SecretKey)?.let { return it } if (Build.VERSION.SDK_INT >= 28) { - try { return generate(strongBox = true) } catch (e: Exception) { /* no StrongBox: the TEE */ } + try { return generate(strongBox = true) } catch (e: Exception) { } } return generate(strongBox = false) } @@ -81,7 +61,6 @@ object CredentialCipher { } } - /** [plain] sealed into the envelope. The cipher picks the IV (the Keystore requires it). */ fun seal(plain: String, keys: KeyProvider): JSONObject { val cipher = Cipher.getInstance(TRANSFORMATION).apply { init(Cipher.ENCRYPT_MODE, keys.key()) } val ct = cipher.doFinal(plain.toByteArray(Charsets.UTF_8)) @@ -89,7 +68,6 @@ object CredentialCipher { .put("iv", b64(cipher.iv)).put("ct", b64(ct)) } - /** The text inside [envelope]. Throws when it is not ours, of another version, or does not authenticate. */ fun open(envelope: JSONObject, keys: KeyProvider): String { require(envelope.optInt("v", -1) == VERSION && envelope.optString("alg") == ALG) { "unknown envelope" } val iv = Base64.decode(envelope.getString("iv"), Base64.NO_WRAP) @@ -98,7 +76,6 @@ object CredentialCipher { return String(cipher.doFinal(ct), Charsets.UTF_8) } - /** Whether [json] is an envelope rather than plain credentials. */ fun isEnvelope(json: JSONObject): Boolean = json.has("v") && json.has("ct") && json.has("iv") private fun b64(bytes: ByteArray) = Base64.encodeToString(bytes, Base64.NO_WRAP) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/GameFacts.kt b/app/src/main/java/com/droiddeck/launcher/stores/GameFacts.kt index e67922fbb..88b0f0105 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/GameFacts.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/GameFacts.kt @@ -11,38 +11,27 @@ import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.concurrent.ConcurrentHashMap -/** - * What other sites know of a store game, looked up by its title: the Metacritic score (as Steam's - * store search carries it) and HowLongToBeat's times. Neither store publishes these, so every - * lookup is a name match against someone else's catalog, kept on disk a fortnight (a miss for two - * days) and in memory for the process. Blocking: call off the main thread. - */ object GameFacts { private const val TAG = "GameFacts" private const val HIT_TTL_MS = 14L * 24 * 60 * 60 * 1000 private const val MISS_TTL_MS = 2L * 24 * 60 * 60 * 1000 private const val TIMEOUT_MS = 10_000 - /** Metacritic's critic score (0-100) and the game's page there. */ data class Metacritic(val score: Int, val url: String) - /** HowLongToBeat's community times, in seconds (0 when nobody has logged that kind of run), and the game's page there. */ data class Hltb(val main: Int, val extras: Int, val completionist: Int, val url: String) { val isEmpty: Boolean get() = main <= 0 && extras <= 0 && completionist <= 0 } - /** A remembered answer: [value] null is a remembered miss. */ private class Entry<T>(val value: T?, val at: Long) { fun fresh(now: Long) = now - at < if (value != null) HIT_TTL_MS else MISS_TTL_MS } private val memory = ConcurrentHashMap<String, Entry<*>>() - /** The score or the times already in memory, without touching disk or network; null when not yet known. */ fun peekMetacritic(title: String): Lookup<Metacritic>? = peek("metacritic", title) fun peekHltb(title: String): Lookup<Hltb>? = peek("hltb", title) - /** The score, or null when Steam does not list the game or has no score for it. */ fun metacritic(context: Context, title: String): Metacritic? = remembered(context, "metacritic", title, read = { o -> o.optInt("score").takeIf { it > 0 }?.let { Metacritic(it, o.optString("url").ifEmpty { metacriticSearch(title) }) } }, @@ -55,7 +44,6 @@ object GameFacts { write = { o, v -> o.put("main", v.main).put("extras", v.extras).put("completionist", v.completionist).put("url", v.url) }, ) { searchHltb(title) } - // Where a game goes when its own page is not known: the site's search for its name. private fun metacriticSearch(title: String) = "https://www.metacritic.com/search/${encodePath(TitleMatch.searchTerm(title))}/?category=13" private fun hltbSearch(title: String) = "$HLTB/?q=${URLEncoder.encode(TitleMatch.searchTerm(title), "UTF-8")}" private fun encodePath(s: String) = URLEncoder.encode(s, "UTF-8").replace("+", "%20") @@ -66,10 +54,6 @@ object GameFacts { return e.value?.let { Lookup.Found(it) } ?: Lookup.None } - /** - * The memory copy, else the disk copy while fresh, else [fetch]. [fetch] answers a [Fetched]: - * a failed request is not a miss, so it leaves the stale copy (if any) standing. - */ @Suppress("UNCHECKED_CAST") private fun <T : Any> remembered( context: Context, kind: String, title: String, @@ -107,8 +91,6 @@ object GameFacts { private fun fileName(title: String) = TitleMatch.normalize(title).take(80).ifEmpty { "_" + title.hashCode().toUInt() } - // Steam's store search lists a game's Metacritic score beside its name: one request, no key. - // The game's own Metacritic page is in its app details, asked for only once there is a score. private fun searchSteamScore(title: String): Fetched<Metacritic> { val term = URLEncoder.encode(TitleMatch.searchTerm(title), "UTF-8") val body = StoreNet.get("https://store.steampowered.com/api/storesearch/?l=english&cc=US&term=$term", timeoutMs = TIMEOUT_MS) @@ -123,7 +105,6 @@ object GameFacts { }.getOrElse { Fetched.Failed } } - /** Steam's link to the game on Metacritic, without its tracking tag; null when Steam gives none. */ private fun metacriticPage(appId: Long): String? { if (appId <= 0) return null val body = StoreNet.get("https://store.steampowered.com/api/appdetails?appids=$appId&filters=metacritic", timeoutMs = TIMEOUT_MS) ?: return null @@ -133,8 +114,6 @@ object GameFacts { }.getOrNull() } - // HowLongToBeat's own site search: a short-lived token from init, sent back as x-auth-token. - // The token is bound to the User-Agent that asked for it, so both calls send the same one. private const val HLTB = "https://howlongtobeat.com" private val hltbHeaders = mapOf("Referer" to "$HLTB/", "Origin" to HLTB) @Volatile private var hltbToken: Pair<String, Long>? = null @@ -162,7 +141,6 @@ object GameFacts { .put("filter", "").put("sort", 0).put("randomizer", 0)) .put("useCache", true) .toString() - // A token can lapse between init and search; one fresh one is worth a second try. var body: String? = null for (renew in listOf(false, true)) { val token = hltbToken(renew) ?: return Fetched.Failed @@ -185,16 +163,13 @@ object GameFacts { } } - /** A store's release date ("2015.05.19", "2015-05-19T00:00:00+02:00", ...) in the reader's own words, or null. */ fun releaseLabel(raw: String): String? { val m = Regex("""^(\d{4})[.\-/](\d{2})[.\-/](\d{2})""").find(raw.trim()) ?: return null val (y, mo, d) = m.destructured - // Epic dates a title it has no date for 2099-01-01. val date = runCatching { LocalDate.of(y.toInt(), mo.toInt(), d.toInt()) }.getOrNull()?.takeIf { it.year in 1970..2098 } ?: return null return date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) } - /** Whole hours, with a half when the time is nearer one: "51½". Under an hour, null (the caller says minutes). */ fun hours(seconds: Int): String? { if (seconds < 3600) return null val halves = Math.round(seconds / 1800.0).toInt() @@ -202,38 +177,26 @@ object GameFacts { } } -/** A fact looked up off the page: still asking, found, or not to be had. */ sealed interface Lookup<out T> { object Pending : Lookup<Nothing> object None : Lookup<Nothing> data class Found<T>(val value: T) : Lookup<T> } -/** - * Whether two catalogs mean the same game. Store titles carry marks, editions and subtitles the - * other site spells differently ("The Witcher 3: Wild Hunt" is Steam's "...Wild Hunt — Remastered"); - * a sequel is never the same game ("Hades" is not "Hades II"). - */ internal object TitleMatch { private val MARKS = Regex("[™®©]") private val EDITION = Regex( "(?i)[\\s:\\-–—]*\\b(?:(?:complete|definitive|legendary|deluxe|ultimate|enhanced|gold|premium|standard|special|anniversary|collector'?s|digital deluxe|game of the year|goty)\\s+edition|goty|director'?s cut)\\b", ) - // Where a subtitle starts: a colon or dash with space around it, or an opening bracket. private val SUBTITLE = Regex("\\s*[:–—]\\s*|\\s+-\\s+|\\s*\\(") fun normalize(s: String): String = s.lowercase().replace(MARKS, "").replace("&", "and").replace(Regex("[^\\p{L}\\p{N}]"), "") fun stripEdition(s: String): String = s.replace(EDITION, " ").trim().ifEmpty { s } - /** - * What to type into another site's search: the title without its marks, edition or subtitle - * punctuation (Steam finds nothing for "Disco Elysium - The Final Cut", all of it for the words). - */ fun searchTerm(title: String): String = stripEdition(title.replace(MARKS, "")).replace(Regex("\\s+[-–—]\\s+|[:–—()\\[\\]]"), " ").replace(Regex("\\s+"), " ").trim() - /** 3 the same name, 2 the same but for the edition, 1 one is the other with a subtitle, 0 not the same game. */ fun score(want: String, got: String): Int { val w = normalize(want) val g = normalize(got) @@ -246,11 +209,9 @@ internal object TitleMatch { return 0 } - /** The title up to each place a subtitle could start, normalized. */ private fun heads(title: String): Set<String> = SUBTITLE.findAll(title).map { normalize(title.substring(0, it.range.first)) }.filter { it.isNotEmpty() }.toSet() - /** The best-named of [items] in their order (the site's own ranking breaks a tie), or null when none is the game. */ fun <T> pick(want: String, items: List<T>, names: (T) -> List<String>): T? { var best: T? = null var bestScore = 0 diff --git a/app/src/main/java/com/droiddeck/launcher/stores/LoginFlows.kt b/app/src/main/java/com/droiddeck/launcher/stores/LoginFlows.kt index 54f31f751..7a4f579ae 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/LoginFlows.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/LoginFlows.kt @@ -5,7 +5,6 @@ import android.util.Log import android.webkit.WebView import com.droiddeck.launcher.stores.gog.GogAuth -/** The three stores' sign-in flows for [StoreLoginActivity]. */ internal object LoginFlows { private const val TAG = "StoreLogin" @@ -15,15 +14,9 @@ internal object LoginFlows { Store.AMAZON -> amazonFlow() } - /** Set by the Epic and Amazon backends when they are in the build; null keeps the chip's card honest. */ @Volatile var epicFlow: () -> StoreLoginActivity.Flow? = { null } @Volatile var amazonFlow: () -> StoreLoginActivity.Flow? = { null } - /** - * GOG's implicit grant: the token arrives in the redirect URL's fragment. The CSRF state is - * checked only when GOG echoes one back - its social-login path returns none, and treating - * that as a mismatch threw away good tokens. - */ private class GogFlow(private val state: String) : StoreLoginActivity.Flow { override val startUrl: String get() = GogAuth.authUrl(state) override val userAgent: String get() = GogAuth.GALAXY_UA @@ -39,7 +32,6 @@ internal object LoginFlows { } val access = frag.getQueryParameter("access_token") if (access.isNullOrEmpty()) { - // The keys only, never the values: the fragment carries live tokens. Log.w(TAG, "gog: redirect without access_token; keys=${frag.queryParameterNames}") return activity.getString(com.droiddeck.launcher.R.string.stores_login_error_verification) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/SizeCache.kt b/app/src/main/java/com/droiddeck/launcher/stores/SizeCache.kt index c89df6b52..ca1078058 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/SizeCache.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/SizeCache.kt @@ -3,12 +3,6 @@ package com.droiddeck.launcher.stores import java.io.File import org.json.JSONObject -/** - * Game sizes as found, kept per game AND the version they were found for, in - * `filesDir/stores/<store>/sizes.json`: a size is never fetched twice for one version, and a - * new version (a library refresh brings it) simply misses and is looked up again. The file carries - * its format; one in another format is discarded, not read. - */ class SizeCache(private val file: File) { private val sizes = HashMap<String, Pair<String, Long>>() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt b/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt index 250a37346..4f0bcfce3 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt @@ -20,23 +20,11 @@ import java.net.Socket import java.net.URL import java.security.SecureRandom -/** - * Adds and removes a store game's shortcut in a Steam client that is running right now, so it - * shows without waiting for the next client start. The runtime starts the client with its - * DevTools port open (droiddeck-session writes it to session/agent/cdp-port) and the client's own - * JavaScript API has `SteamClient.Apps.AddShortcut`; this speaks the small part of the DevTools - * protocol that one `Runtime.evaluate` needs, over a plain socket, nothing else. - * - * Best-effort throughout: no session, no port, no SharedJSContext, or an API that moved and the - * shortcut simply arrives with the shortcuts writer at the next start. The appid Steam derives - * (CRC of the quoted exe plus the name) is the one the writer computes, so the two never disagree. - */ object SteamLiveShortcuts { private const val TAG = "SteamLive" private const val TAG_NAME = "droiddeck-app" private const val COMPAT_TOOL = "droiddeck-proton-arm64" - /** Adds every game in [games] that [include] takes (store games by default) and the client does not list yet. Blocking; worker thread. */ fun sync(context: Context, games: List<AddedGames.Game>, include: (AddedGames.Game) -> Boolean = { it.source != Library.ADDED }) { val wanted = games.filter(include) if (wanted.isEmpty() || !clientRunning()) return @@ -64,17 +52,12 @@ object SteamLiveShortcuts { if (result != "0") StoresState.logLine("Steam: $result shortcut(s) added to the running client") } - /** Removes the shortcut with [appId] from the running client. Blocking; worker thread. */ fun remove(context: Context, appId: Long) { if (!clientRunning()) return val result = evaluate(context, "(async () => { await SteamClient.Apps.RemoveShortcut(${appId and 0xFFFFFFFFL}); return 1; })()") if (result != null) Log.i(TAG, "live shortcuts: removed $appId") } - /** - * The running client's shortcut for [game] made to match it: name, target, Start in and - * launch options, on the same appid (an edit never adds a second shortcut). Blocking. - */ fun update(context: Context, game: AddedGames.Game) { if (!clientRunning()) return val id = game.appId and 0xFFFFFFFFL @@ -91,11 +74,6 @@ object SteamLiveShortcuts { if (evaluate(context, js) != null) Log.i(TAG, "live shortcuts: updated $id") } - /** - * One piece of art for the running client's shortcut [appId]: [assetType] as the client numbers - * them (0 portrait capsule, 1 hero, 2 logo, 3 wide capsule), from [file]; null clears it. The - * icon goes by path ([iconPath]). Blocking. - */ fun setArt(context: Context, appId: Long, assetType: Int?, file: java.io.File?, iconPath: String? = null) { if (!clientRunning()) return val id = appId and 0xFFFFFFFFL @@ -114,7 +92,6 @@ object SteamLiveShortcuts { private fun clientRunning(): Boolean = SessionState.running && SessionState.mode == SessionService.MODE_STEAM && !SessionState.stopRequested - /** `Runtime.evaluate` of [expression] in the client's SharedJSContext; the value as text, or null. */ private fun evaluate(context: Context, expression: String): String? { val portFile = File(LinuxRuntime.sessionRoot(context), "agent/cdp-port") val port = runCatching { portFile.readText().trim().toInt() }.getOrNull() ?: return null @@ -152,7 +129,6 @@ object SteamLiveShortcuts { try { return c.inputStream.bufferedReader().readText() } finally { c.disconnect() } } - /** The least of RFC 6455 a localhost DevTools talk needs: upgrade, masked text out, text in. */ private class WebSocket(url: String, timeoutMs: Int) : AutoCloseable { private val socket: Socket private val out: OutputStream @@ -198,7 +174,6 @@ object SteamLiveShortcuts { out.write(frame.toByteArray()); out.flush() } - /** The next text message (fragments joined); null on close. */ fun receiveText(): String? { val message = ByteArrayOutputStream() while (true) { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/Store.kt b/app/src/main/java/com/droiddeck/launcher/stores/Store.kt index 9ff97654b..3288e1c6e 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/Store.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/Store.kt @@ -1,9 +1,5 @@ package com.droiddeck.launcher.stores -/** - * The three storefronts the Stores section talks to. [id] is what the sidecar, the prefs file - * names and the Games badge carry; it never changes once a game is installed under it. - */ enum class Store(val id: String, val label: String, val shortLabel: String) { GOG("gog", "GOG", "GOG"), EPIC("epic", "Epic Games", "Epic"), diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreAccounts.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreAccounts.kt index 77599e0a4..6b5f30186 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreAccounts.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreAccounts.kt @@ -6,22 +6,6 @@ import androidx.annotation.VisibleForTesting import org.json.JSONObject import java.io.File -/** - * Each store's sign-in, kept in the app's private files: `filesDir/stores/<store>/credentials.json`. - * Tokens never go to shared storage and never into a log; the file holds whatever the store's auth - * flow handed back (access and refresh tokens, expiry, the account's display name, and for Amazon - * the device registration), as one JSON object the store's own client reads and writes - sealed - * with [CredentialCipher] under a key in the AndroidKeyStore. Every read and write of a sign-in - * goes through here. - * - * - A plain file an earlier build wrote is sealed the first time it is read ([encryptAll] at app - * start), verified by opening it again, and the plain text is replaced. - * - Where the Keystore does not work (some ROMs), the file stays plain, the log says so once, and - * the next start tries again. - * - A sealed file that can never open again (the key is gone: data cleared, a backup restored on - * another device) is deleted and the store reads as signed out; one that fails for a reason that - * may pass is kept and tried again (see [read]). - */ object StoreAccounts { private const val TAG = "StoreAccounts" private const val FILE = "credentials.json" @@ -30,32 +14,19 @@ object StoreAccounts { @Volatile internal var keys: CredentialCipher.KeyProvider = CredentialCipher.Keystore - /** The Keystore failed once in this process: files stay plain until the next start tries again. */ @VisibleForTesting @Volatile internal var keystoreFailed = false - /** The store's private folder: credentials, caches, the engine's scratch. */ fun dir(context: Context, store: Store): File = File(File(context.filesDir, "stores"), store.id).apply { mkdirs() } fun credentialsFile(context: Context, store: Store): File = File(dir(context, store), FILE) - /** Stores whose sealed sign-in could not be opened this time for a reason that may pass (see [read]). */ private val unavailable = java.util.concurrent.ConcurrentHashMap.newKeySet<Store>() @Volatile private var unavailableLogged = false - /** - * The stored credentials, or null when signed out or unreadable (never logged: the text is the - * secret). - * - * A sealed file is deleted - the store signs in again - only when it can never open: it does - * not authenticate (tampered, or another key: data cleared, restored on another device), the - * key is permanently invalidated or gone from the Keystore, or it is not an envelope at all. Any - * other failure (the Keystore busy or erroring, an I/O error) keeps the file, marks the store - * [isUnavailable] - signed in, its actions failing for now - and the next read tries again. - */ @Synchronized fun read(context: Context, store: Store): JSONObject? { val f = credentialsFile(context, store) @@ -74,7 +45,6 @@ object StoreAccounts { } } - /** The store's sign-in is on disk but could not be opened this time (see [read]). */ fun isUnavailable(store: Store): Boolean = store in unavailable private fun unreadable(f: File, store: Store, e: Exception): JSONObject? { @@ -107,30 +77,19 @@ object StoreAccounts { File(credentialsFile(context, store).path + ".tmp").delete() } - /** At app start: every plain sign-in an earlier build left is sealed now. */ fun encryptAll(context: Context) { Store.entries.forEach { store -> - // An older build's temp file held the plain text too. File(credentialsFile(context, store).path + ".tmp").delete() read(context, store) } } - /** - * The account's display name when signed in (the store's own name for it, or a stand-in), else - * null. A sign-in that could not be opened this time ([isUnavailable]) still counts, under the - * store's name, so the sign-in card does not ask for what is still there. - */ fun signedInAs(context: Context, store: Store): String? { val json = read(context, store) ?: return if (isUnavailable(store)) store.label else null if (json.optString("access_token", "").isEmpty()) return null return json.optString("display_name", "").ifEmpty { json.optString("username", "") }.ifEmpty { store.label } } - /** - * [json] sealed into [f], checked by opening what was written. Null - and [f] as it was - when - * the Keystore cannot seal or the check fails. - */ private fun seal(f: File, json: JSONObject): File? { if (keystoreFailed) return null val text = json.toString() @@ -145,7 +104,6 @@ object StoreAccounts { return f } - /** [text] into [f] through a temp file and a rename, readable and writable by the app alone. */ private fun put(f: File, text: String) { val tmp = File(f.path + ".tmp") tmp.writeText(text) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreAchievements.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreAchievements.kt index 9c1eb5fdc..b5fea11a5 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreAchievements.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreAchievements.kt @@ -13,13 +13,8 @@ import java.time.Instant import java.time.OffsetDateTime import java.util.Locale -/** - * A source's achievements with the account's progress, the last good answer cached on disk. Blocking. - * [source] is the library's id for where a game came from ("gog", "epic"; "steam" once it has one). - */ abstract class StoreAchievements(val source: String, val label: String) { - /** [extra]: what the source needs to find the game (Epic: "namespace"). */ data class Game(val source: String, val id: String, val extra: Map<String, String> = emptyMap()) { companion object { fun of(item: CatalogItem) = Game(item.store.id, item.id, item.extra) @@ -33,7 +28,6 @@ abstract class StoreAchievements(val source: String, val label: String) { val description: String, val hidden: Boolean, val unlocked: Boolean, - /** Unix seconds; 0 when locked or unknown. */ val unlockedAt: Long, val icon: String?, val iconLocked: String?, @@ -69,7 +63,6 @@ abstract class StoreAchievements(val source: String, val label: String) { protected fun locale(): String = Locale.getDefault().toLanguageTag() - /** ISO-8601 (including GOG's "+0000" offsets) to Unix seconds; 0 if unreadable. */ protected fun epochSeconds(iso: String?): Long { if (iso.isNullOrBlank()) return 0 val normalized = iso.replace(Regex("([+-]\\d{2})(\\d{2})$"), "$1:$2") @@ -102,7 +95,6 @@ abstract class StoreAchievements(val source: String, val label: String) { private val all: List<StoreAchievements> by lazy { listOf(GogAchievements, EpicAchievements) } - /** Null for a source without achievements. */ fun forSource(source: String?): StoreAchievements? = all.firstOrNull { it.source == source } } } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreArt.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreArt.kt index 4726c2db7..4b2b0b803 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreArt.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreArt.kt @@ -9,17 +9,9 @@ import java.io.File import java.net.HttpURLConnection import java.net.URL -/** - * The store's art, placed in the game's folder under the names AddedGameArt already looks for - - * `cover.jpg` (the portrait capsule), `hero.jpg` and `header.jpg` (the wide art) - so a store game - * shows real art in the Games tab and the session's shortcuts writer copies it into the client's - * grid. Each image is decoded and re-encoded as an opaque JPEG over a dark ground: a store's PNG - * with an alpha fade would otherwise end in white, which is what the washed-out cards were. - */ object StoreArt { private const val TAG = "StoreArt" - /** Fetches what the sidecar names; a missing or unreadable image is skipped, never an error. Blocking. */ fun fetchInto(folder: File, sidecar: StoreGameSidecar) { sidecar.cover?.let { place(it, File(folder, "cover.jpg")) } val wide = sidecar.hero ?: return diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt index 88d6695d5..98b631827 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt @@ -1,49 +1,31 @@ package com.droiddeck.launcher.stores -/** - * One title as the Stores page draws it, whichever store produced it: a card on a shelf, a tile - * in a grid, the hero of a game page. Library games and storefront results both project into - * this, so one card composable serves every tab. - */ data class CatalogItem( val store: Store, - /** Store-native id: GOG product id, Epic catalog item id (offer id for storefront results), Amazon product id. */ val id: String, val title: String, - /** Wide (16:9-ish) art for cards and heroes. */ val imageUrl: String?, - /** Tall (2:3) box art when the store publishes one; falls back to [imageUrl]. */ val tallImageUrl: String? = null, - /** Comma-joined genre / tag line. */ val tags: String = "", val isFree: Boolean = false, - /** False when the endpoint gave no price at all: the price row then draws nothing. */ val hasPrice: Boolean = false, - /** Pre-formatted by the store ("$9.99"). */ val finalPrice: String = "", val originalPrice: String = "", val discountPercent: Int = 0, - /** The web product page: where "Get for free" and "View on <store>" land. */ val storeUrl: String = "", val developer: String = "", val publisher: String = "", val releaseDate: String = "", val description: String = "", - /** True for a title the signed-in account owns (a library entry). */ val owned: Boolean = false, - /** The install size in bytes when known, else 0. */ val sizeBytes: Long = 0L, - /** Per-store identifiers the install needs (Epic namespace / catalog id / app name, Amazon entitlement / sku). */ val extra: Map<String, String> = emptyMap(), - /** Rated or tagged for adults by the store's own data; hidden from the storefront unless Show mature content is on. */ val mature: Boolean = false, ) { val isDiscounted: Boolean get() = discountPercent > 0 && originalPrice.isNotBlank() - /** Stable key across stores ("gog:1207658924"). */ val key: String get() = "${store.id}:$id" } -/** A store's storefront shelves: what the Store tab shows above the account's own library. */ class StoreShelves( val whatsNew: List<CatalogItem> = emptyList(), val deals: List<CatalogItem> = emptyList(), @@ -54,24 +36,12 @@ class StoreShelves( val all: List<CatalogItem> get() = (whatsNew + deals + free + trending).distinctBy { it.id } } -/** A game a store installed, as found on disk through its sidecar. */ -/** Store tags that mark adult content, by slug or name, lowercased. */ private val MATURE_TAGS = setOf("mature", "nsfw", "adult", "adult only", "adults only", "nudity", "sexual content", "sexual-content") -/** Whether a store's own tags mark a title as adult. */ fun matureByTags(tags: Collection<String>): Boolean = tags.any { it.trim().lowercase() in MATURE_TAGS } -/** - * Whether a store's own age ratings mark a title as adult: ESRB M (17) or AO (18), PEGI / USK / the - * store's own rating 18, or any rating system's 17+. Ratings are ages, as GOG gives them. - */ fun matureByAge(ages: Collection<Int>): Boolean = ages.any { it >= 17 } -/** - * The Installed tab's cards for [store], from the installs on disk - the same source as its count: - * each joined to its library item by store id (by title when the ids differ), else a card made - * from the sidecar's own title and art, so an install shows even before the library has loaded. - */ fun installedCards(store: Store, installed: List<InstalledStoreGame>, library: List<CatalogItem>): List<CatalogItem> { val byId = library.associateBy { it.id } val byTitle = library.associateBy { it.title.lowercase() } @@ -82,11 +52,6 @@ fun installedCards(store: Store, installed: List<InstalledStoreGame>, library: L }.distinctBy { it.id }.sortedBy { it.title.lowercase() } } -/** - * A refreshed list with every unchanged item kept as the very object already shown, so a refresh - * that lands while the grid is on screen recomposes only the cards that changed - no reset, the - * scroll position and the pad's focus stay. - */ fun mergeItems(old: List<CatalogItem>?, new: List<CatalogItem>): List<CatalogItem> { if (old.isNullOrEmpty()) return new val before = old.associateBy { it.key } @@ -103,12 +68,6 @@ private val HTML_TAG = Regex("<[^>]+>") private val ENTITY = Regex("&(#\\d+|#x[0-9a-fA-F]+|[a-zA-Z]+);") private val NAMED_ENTITIES = mapOf("amp" to "&", "lt" to "<", "gt" to ">", "quot" to "\"", "apos" to "'", "nbsp" to " ", "ndash" to "–", "mdash" to "—", "hellip" to "…", "trade" to "™", "reg" to "®", "copy" to "©") -/** - * Store copy as plain text: GOG's account endpoints return template keys - * (`product_description_2015545325`) for some products instead of words, and every store's - * descriptions carry HTML. Those keys go, tags go, entities are decoded, whitespace is collapsed; - * "" when nothing readable is left, so a page shows nothing rather than a placeholder. - */ fun cleanStoreText(raw: String?): String { if (raw.isNullOrBlank()) return "" var s = raw.replace(TEMPLATE_KEY, " ") @@ -124,7 +83,6 @@ fun cleanStoreText(raw: String?): String { return s.lines().map { it.replace(Regex("[ \\t\\u00A0]+"), " ").trim() }.filter { it.isNotEmpty() }.joinToString("\n") } -/** Human sizes the way the Downloads page and the cards print them ("810.2 MB", "3.9 GB"). */ fun formatBytes(bytes: Long): String = when { bytes >= 1_073_741_824L -> "%.1f GB".format(bytes / 1_073_741_824.0) bytes >= 1_048_576L -> "%.1f MB".format(bytes / 1_048_576.0) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreExe.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreExe.kt index f1a97541b..ad2d478c1 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreExe.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreExe.kt @@ -2,12 +2,6 @@ package com.droiddeck.launcher.stores import java.io.File -/** - * Which .exe in an install folder is the game. The store's own hint (a manifest's launch - * executable) wins when the file exists; else the candidates, helpers and installers left out, - * the one named most like the title first, then the shallowest. Relative to the folder, forward - * slashes; "" when the folder has none. - */ object StoreExe { private val SKIP = listOf("redist", "unins", "setup", "crash", "report", "helper", "dotnet", "vcredist", "directx", "eac", "easyanticheat", "battleye", "launcher", "updater", "config") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt index 149b6c455..1753003d5 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt @@ -5,25 +5,6 @@ import org.json.JSONArray import org.json.JSONObject import java.io.File -/** - * What a store install leaves beside the game so the rest of the app can treat the folder like - * any added game: `.droiddeck-store.json` in the game's folder. - * - * [AddedGames.scanGame] reads it to pick the exe (instead of guessing from the folder), to name - * the shortcut after the store's title and to tag the game with its [store]. [launcher] is the - * small .bat the install writes when the game needs arguments or environment at launch (Epic's - * identity arguments, Amazon's FuelPump variables): a Steam shortcut can only carry a fixed Exe, - * so the launcher is what the shortcut runs and [exe] is what it starts. Every store install is a - * Steam shortcut; there is no switch for it (an `addToSteam` field in an older sidecar is ignored). - * - * Paths are relative to the game folder with forward slashes, so the folder can move between - * storage volumes and stay valid. - * - * An install writes the sidecar twice: at its start with [state] `installing` (store, id, title - - * no exe yet), and at its end with the exe, the launcher and `installed`. A folder whose sidecar - * still says `installing` is an unfinished install, never a game: the scan leaves it out and the - * Stores page offers to resume it. A sidecar without the field (an earlier build) is installed. - */ class StoreGameSidecar( val store: Store, val id: String, @@ -36,12 +17,9 @@ class StoreGameSidecar( val installedAt: Long = 0L, val cover: String? = null, val hero: String? = null, - /** Per-store identifiers the launcher needs again (Epic namespace / catalog id, Amazon entitlement). */ val extra: Map<String, String> = emptyMap(), val state: String = STATE_INSTALLED, - /** An Epic game's launch choices (the Games tab's Epic card); both launch paths read them from here. */ val epic: EpicOptions = EpicOptions(), - /** GOG / Epic cloud saves for this game: down before a launch, up after it (the game card's switch). */ val cloud: Boolean = true, val achievements: Boolean = true, ) { @@ -93,17 +71,11 @@ class StoreGameSidecar( fun file(folder: File): File = File(folder, FILE_NAME) - /** An installed game's sidecar; walks the install folders. */ fun find(context: Context, store: Store, id: String): StoreGameSidecar? = StoreInstallRoot.gameFolders(context.applicationContext).asSequence() .mapNotNull { read(it) } .firstOrNull { it.store == store && it.id == id } - /** - * Changes an Epic game's launch choices on disk: reads the sidecar fresh (never a copy held - * by a screen), applies [change], writes it, and reads it back. The written sidecar, or null - * when there is none or the write did not take. Blocking. - */ fun updateEpic(folder: File, change: (EpicOptions) -> EpicOptions): StoreGameSidecar? { val current = read(folder) ?: return null val next = current.copy(epic = change(current.epic)) @@ -111,7 +83,6 @@ class StoreGameSidecar( return read(folder)?.takeIf { it.epic == next.epic } } - /** As [updateEpic], for the cloud-saves switch. */ fun updateCloud(folder: File, on: Boolean): StoreGameSidecar? { val current = read(folder) ?: return null current.copy(cloud = on).write(folder) @@ -124,14 +95,12 @@ class StoreGameSidecar( return read(folder)?.takeIf { it.achievements == on } } - /** The sidecar in [folder], or null when there is none or it cannot be read. */ fun read(folder: File): StoreGameSidecar? { val f = file(folder) if (!f.isFile) return null return runCatching { parse(f.readText()) }.getOrNull() } - /** Pure parse; null for anything that is not a usable sidecar (so a scan falls back to guessing). */ fun parse(text: String): StoreGameSidecar? { val o = runCatching { JSONObject(text) }.getOrNull() ?: return null val store = Store.byId(o.optString("store")) ?: return null @@ -140,9 +109,7 @@ class StoreGameSidecar( val exe = o.optString("exe", "").replace('\\', '/') val state = o.optString("state", STATE_INSTALLED).ifEmpty { STATE_INSTALLED } if (id.isEmpty() || title.isEmpty()) return null - // An unfinished install has no exe yet; a finished one must name one. if (exe.isEmpty() && state == STATE_INSTALLED) return null - // A relative path only: a sidecar that points outside its folder is not trusted. if (exe.isNotEmpty() && !relativeInside(exe)) return null val launcher = o.optString("launcher", "").replace('\\', '/').takeIf { it.isNotEmpty() && relativeInside(it) } val args = o.optJSONArray("args")?.let { a -> List(a.length()) { a.optString(it) }.filter { it.isNotEmpty() } } ?: emptyList() @@ -158,24 +125,17 @@ class StoreGameSidecar( hero = o.optString("hero", "").ifEmpty { null }, extra = extra, state = state, - // An "overlay" (or "v") field from an earlier build is read past: there is no overlay. epic = o.optJSONObject("epic")?.let { e -> EpicOptions(eos = e.optBoolean("eos", true), offline = e.optBoolean("offline", false)) } ?: EpicOptions(), cloud = o.optBoolean("cloud", true), achievements = o.optBoolean("achievements", true), ) } - /** Relative, no drive letter, no step up: an absolute or escaping path is refused, not trimmed into shape. */ private fun relativeInside(path: String): Boolean = path.isNotEmpty() && !path.startsWith("/") && !path.contains(":") && path.split('/').none { it == ".." || it.isEmpty() } } } -/** - * An Epic game's launch choices. [eos]: pass a fresh exchange code (Epic sign-in). [offline]: start - * with the Epic identity arguments only, no code - for a game that will not start when sign-in fails. - */ data class EpicOptions(val eos: Boolean = true, val offline: Boolean = false) { - /** Whether a launch asks for a code at all. */ val wantsCode: Boolean get() = eos && !offline } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt index 468f92419..e122aa057 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt @@ -8,52 +8,31 @@ import com.droiddeck.launcher.session.GameStorage import java.io.File import java.util.zip.CRC32 -/** - * Where store games are installed: `<root>/<Store>/<title>`. - * - * The default root is the app's own internal storage - the runtime's tree, where the folder sits at - * /root/Games/Stores and needs no bind - whatever the session's Game storage setting says. An SD - * card the app may write (its app folder, `GameStorage.options`) is offered as a second root at - * Install time and never chosen silently. Every root is scanned, so a game stays in the library - * wherever it went. A card that is the Steam library is already bound (`/mnt/droiddeck-sd`); any - * other card's root is bound at a fixed place of its own (`/mnt/droiddeck-stores/<volume>`), so a - * shortcut's path - and with it its appid - stays the same from one session to the next. - */ object StoreInstallRoot { private const val INTERNAL = "root/Games/Stores" private const val ON_VOLUME = "Games" const val GUEST_EXTERNAL = "/mnt/droiddeck-stores" - /** One place a game can be installed: the folder, its name for the dialog, and whether it is a card. */ class Target(val root: File, val label: String, val removable: Boolean) { val freeBytes: Long get() = try { StatFs((if (root.isDirectory) root else root.parentFile ?: root).path).availableBytes } catch (e: Exception) { 0L } } - /** The internal root: always there, always the default. */ fun internalRoot(context: Context): File = File(LinuxRuntime.rootDir(context), INTERNAL) - /** The roots a game can go to right now: internal first, then every writable card. */ fun targets(context: Context): List<Target> { val internal = Target(internalRoot(context), context.getString(com.droiddeck.launcher.R.string.games_internal), removable = false) val cards = GameStorage.options(context).map { Target(File(it.path, ON_VOLUME), it.name, removable = true) } return listOf(internal) + cards } - /** The default root, when nothing asks: internal storage. */ fun installRoot(context: Context): File = internalRoot(context) - /** Every root that may hold store games: the internal one, every card's, and the Steam library's (older installs). */ fun roots(context: Context): List<File> { val cards = GameStorage.options(context).map { File(it.path, ON_VOLUME) } val library = GameStorage.effective(context)?.let { File(it.path, ON_VOLUME) } return (listOf(internalRoot(context)) + cards + listOfNotNull(library)).distinctBy { it.absolutePath } } - /** - * The card roots that need a bind of their own, with where the session sees them. A root under - * the Steam library is left out: the library's bind already covers it, and the games there keep - * the guest path (and appid) they were registered with. - */ fun externalRoots(context: Context): List<Pair<File, String>> { val library = GameStorage.effective(context)?.path return roots(context) @@ -61,7 +40,6 @@ object StoreInstallRoot { .map { it to guestFor(context, it) } } - /** A card root's guest path, keyed on the volume (its UUID, else a hash of the path), so it is the same across sessions. */ private fun guestFor(context: Context, root: File): String { val sm = context.getSystemService(StorageManager::class.java) val uuid = runCatching { sm?.getStorageVolume(root)?.uuid }.getOrNull()?.takeIf { it.isNotBlank() }?.replace(Regex("[^A-Za-z0-9-]"), "") @@ -71,7 +49,6 @@ object StoreInstallRoot { fun isInternal(context: Context, root: File): Boolean = root.absolutePath == internalRoot(context).absolutePath - /** Where a host path under a card root appears inside the session, or null when it is not under one. */ fun externalGuestPath(context: Context, host: File): String? { val path = host.absolutePath for ((root, guest) in externalRoots(context)) { @@ -82,7 +59,6 @@ object StoreInstallRoot { return null } - /** The target a game folder sits under, for the page and the Downloads row; internal when unknown. */ fun labelFor(context: Context, folder: File): String { val path = folder.absolutePath for (t in targets(context)) if (path.startsWith(t.root.absolutePath + "/")) return t.label @@ -93,23 +69,15 @@ object StoreInstallRoot { fun storeDir(root: File, store: Store): File = File(root, store.shortLabel) - /** The game folders under every root, by store: what AddedGames scans in addition to the user's folders. */ fun gameFolders(context: Context): List<File> = roots(context).flatMap { root -> Store.entries.flatMap { store -> storeDir(root, store).listFiles { f -> f.isDirectory }?.sortedBy { it.name.lowercase() } ?: emptyList() } } - /** - * The folder a game installs into under [root]. An existing folder wins over a fresh one, so a - * repair, an update or a resume lands on what is already there: one with this game's sidecar - * anywhere, else one under any root with this game's folder name and no finished sidecar (an - * install an earlier build left without one, or that stopped before writing it). - */ fun folderFor(context: Context, store: Store, id: String, title: String, root: File = installRoot(context)): File = existingFolder(context, store, id, title) ?: File(storeDir(root, store), folderName(title, id)) - /** The folder an earlier install of this game left, finished or not; null when there is none. */ fun existingFolder(context: Context, store: Store, id: String, title: String): File? { val all = roots(context).flatMap { r -> storeDir(r, store).listFiles { f -> f.isDirectory }?.toList().orEmpty() } all.firstOrNull { folder -> StoreGameSidecar.read(folder)?.let { it.store == store && it.id == id } == true }?.let { return it } @@ -117,17 +85,10 @@ object StoreInstallRoot { return all.firstOrNull { it.name == name && StoreGameSidecar.read(it)?.isInstalled != true } } - /** An unfinished install: [id] from its `installing` sidecar, null when it has none (matched by folder name then). */ class Unfinished(val store: Store, val id: String?, val folder: File) - /** - * A folder under a store root is a game only once its sidecar says installed. Without one it is - * an install that never finished (stopped, killed, or from a build that wrote the sidecar last): - * not a Custom game, and not listed in Steam. - */ fun isFinished(folder: File): Boolean = StoreGameSidecar.read(folder)?.isInstalled == true - /** Every unfinished install under every root, for the Stores page's "Resume install". */ fun unfinished(context: Context): List<Unfinished> = roots(context).flatMap { root -> Store.entries.flatMap { store -> storeDir(root, store).listFiles { f -> f.isDirectory }.orEmpty().mapNotNull { folder -> @@ -141,15 +102,9 @@ object StoreInstallRoot { } } - /** A scratch folder in the app's cache for an install's in-flight pieces, when the install itself is on a card. */ fun scratchDir(context: Context, store: Store, id: String): File = File(File(File(context.cacheDir, "stores"), store.id), id.replace(Regex("[^A-Za-z0-9._-]"), "_")) - /** - * A folder name from a store title: letters, digits and a few safe marks, single spaces, at most - * 60 characters; the store id when nothing printable is left. The name is the shortcut's folder - * for ever, so it is chosen once at install and read back from the sidecar afterwards. - */ fun folderName(title: String, id: String): String { val cleaned = title.replace(Regex("[^A-Za-z0-9 ._'()\\-]"), " ").replace(Regex("\\s+"), " ").trim().trimEnd('.') val cut = if (cleaned.length > 60) cleaned.take(60).trimEnd().trimEnd('.') else cleaned diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt index 97a6c9032..4616c51e6 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt @@ -8,61 +8,31 @@ import com.droiddeck.launcher.frontend.LibraryCache import com.droiddeck.launcher.frontend.Library import java.io.File -/** - * What happens around a store install so the rest of the app sees the game: the sidecar is - * written, the launcher .bat beside it when the game needs one, and Steam is told. - * - * Telling Steam is the same path an added folder takes: the game is one more folder the - * [AddedGames] scan finds, the scan's listing (`session/added-games.json`) is what the runtime's - * shortcuts writer reads at the client's next start, and here the listing is rewritten at once so - * a Steam client that restarts inside the running session already has it. When the client is up, - * [SteamLiveShortcuts] adds the shortcut over the client's DevTools port as well, so the game shows - * without a restart; that part is best-effort. - */ object StoreInstalls { private const val TAG = "StoreInstalls" - /** - * Marks [folder] as an install under way before anything is fetched: a sidecar in state - * `installing` with the store, id and title. Whatever stops the install from here on - a - * failure, a cancel, the process killed - leaves a folder the scan skips and the Stores page - * offers to resume, never a half-written Custom game. A finished install being repaired or - * updated keeps its finished sidecar, so the game stays in Steam meanwhile. - */ fun begin(folder: File, store: Store, id: String, title: String, cover: String?, hero: String?) { if (StoreGameSidecar.read(folder)?.isInstalled == true) return StoreGameSidecar(store, id, title, exe = "", cover = cover, hero = hero, state = StoreGameSidecar.STATE_INSTALLING).write(folder) } - /** - * Finishes an install: writes the launcher and the finished sidecar, then the art, then - * registers the game. Runs on the caller's worker thread. The launcher and sidecar writes - * throw, so a failure there fails the download with its message; the art and the Steam side - * are best-effort and only logged. - */ fun complete(context: Context, folder: File, sidecar: StoreGameSidecar, onStep: ((Int, Int) -> Unit)? = null) { val app = context.applicationContext onStep?.invoke(0, STEPS) - // The user's launch choices survive an update or a repair. val before = StoreGameSidecar.read(folder) val finished = StoreLaunch.writeLauncher(folder, sidecar.copy(state = StoreGameSidecar.STATE_INSTALLED, epic = before?.epic ?: sidecar.epic, cloud = before?.cloud ?: sidecar.cloud, achievements = before?.achievements ?: sidecar.achievements)) finished.write(folder) if (StoreGameSidecar.read(folder)?.isInstalled != true) throw java.io.IOException("the install record could not be written in ${folder.name}") onStep?.invoke(1, STEPS) - // The art before the listing, so the listing already carries it for the client's grid. try { StoreArt.fetchInto(folder, finished) } catch (e: Exception) { Log.w(TAG, "art for ${folder.name}: ${e.message}") } onStep?.invoke(2, STEPS) register(app) - // Its Windows components from the store's list (GOG's dependencies, Epic's prerequisite, - // Amazon's install steps), on a thread of their own. AddedGames.single(app, folder.path)?.let { com.droiddeck.launcher.frontend.AutoComponents.refreshLater(app, Library.addedGame(app, it)) } onStep?.invoke(STEPS, STEPS) } - /** The finishing steps [complete] reports: the record and launcher, the art, Steam. */ private const val STEPS = 3 - /** Rewrites the session's listing from a fresh scan and, with a client running, adds live. */ fun register(context: Context) { val app = context.applicationContext try { @@ -75,17 +45,9 @@ object StoreInstalls { } } - /** - * Removes a store game: its folder with the sidecar, then the shortcut (the listing without - * it makes the writer drop the entry; the live client is asked too). Blocking. [removed] runs - * once the files are gone, before the shortcut list is rebuilt (which reads every added game). - */ fun uninstall(context: Context, game: InstalledStoreGame, progress: (Float) -> Unit = {}, removed: () -> Unit = {}) { val app = context.applicationContext - // The shortcut's id as stored for the folder: scanning every added game for it first held - // the bar at 0% for seconds before a file went. val appId = SessionPrefs.addedGameStoredAppId(app, game.folder.path) - // File by file, deepest first, saying how far it has got: a big game takes a while to go. val files = game.folder.walkBottomUp().toList() val step = (files.size / 100).coerceAtLeast(1) files.forEachIndexed { i, f -> @@ -94,7 +56,6 @@ object StoreInstalls { } deleteTree(game.folder) progress(1f) - // The scratch cache an install on a card may have left in the app's cache. deleteTree(StoreInstallRoot.scratchDir(app, game.sidecar.store, game.sidecar.id)) removed() register(app) @@ -102,12 +63,6 @@ object StoreInstalls { StoresState.logLine("uninstalled \"${game.sidecar.title}\"") } - /** - * What Cancel does: everything the install wrote goes - the game folder with its sidecar and - * the caches in [caches] - unless the folder holds a finished install being repaired or updated; - * then only the caches go and the working game stays. Off the caller's thread; the library - * follows when it is done, so the game reads Install again. - */ fun discard(context: Context, folder: File, caches: List<File>) { val app = context.applicationContext Thread({ diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunch.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunch.kt index 4977baefc..a50dfcd90 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunch.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunch.kt @@ -5,27 +5,11 @@ import android.util.Log import com.droiddeck.launcher.frontend.Library import java.io.File -/** - * How a store game is started inside the session. The shortcut Steam gets can only name an exe, - * so when a game needs arguments or environment the install writes a small `.droiddeck-launch.bat` - * beside it and the shortcut runs that: Proton's steam.exe shim hands a .bat to Wine's cmd, the - * script sets the variables, steps into the exe's folder and starts it, and waits for it as a - * batch file does, so Steam sees the game running. - * - * Epic's online sign-in is a fresh exchange code per launch (it expires in minutes), which no fixed - * command line can carry: [prepare] mints one right before a launch and leaves it in - * `.droiddeck-epic-code`; the script reads the file, deletes it and passes the AUTH arguments. With - * no file the game starts in its offline identity mode, which most EOS titles accept. - */ object StoreLaunch { private const val TAG = "StoreLaunch" const val LAUNCHER = ".droiddeck-launch.bat" const val EPIC_CODE = ".droiddeck-epic-code" - /** - * Writes the launcher for [sidecar] when it needs one (arguments, environment, or an Epic - * sign-in), removes a stale one otherwise, and returns the sidecar with [StoreGameSidecar.launcher] set. - */ fun writeLauncher(folder: File, sidecar: StoreGameSidecar): StoreGameSidecar { val needs = sidecar.args.isNotEmpty() || sidecar.env.isNotEmpty() || sidecar.store == Store.EPIC val file = File(folder, LAUNCHER) @@ -34,7 +18,6 @@ object StoreLaunch { return sidecar.copy(launcher = LAUNCHER) } - /** The batch file's text; pure, so it can be checked without a folder. CRLF line ends, as cmd expects. */ fun launcherText(sidecar: StoreGameSidecar): String { val exe = sidecar.exe.replace('/', '\\') val dir = exe.substringBeforeLast('\\', "") @@ -47,9 +30,6 @@ object StoreLaunch { val args = sidecar.args.joinToString(" ") { quoteArg(it) } val run = "\"%~dp0$exe\"" + (if (args.isNotEmpty()) " $args" else "") if (sidecar.store == Store.EPIC) { - // The one-shot code reaches the game on its command line only. cmd expands %DD_X% when it - // parses the last line, so the variable is cleared on that same line before the game - // starts: nothing of it is left in the environment the game inherits (and dumps). lines += "set \"$CODE_VAR=\"" lines += "if exist \"%~dp0$EPIC_CODE\" set /p $CODE_VAR=<\"%~dp0$EPIC_CODE\"" lines += "if exist \"%~dp0$EPIC_CODE\" del \"%~dp0$EPIC_CODE\"" @@ -66,24 +46,13 @@ object StoreLaunch { private val ENV_NAME = Regex("[A-Za-z_][A-Za-z0-9_]*") - /** An argument on cmd's line: quoted when it has a space and is not already quoted; control characters dropped. */ fun quoteArg(arg: String): String { val clean = arg.filter { it >= ' ' && it != '\u007f' } return if (' ' in clean && !clean.contains('"')) "\"$clean\"" else clean } - /** - * Everything a launch needs done first, then [onReady] on the main thread: for an Epic game a - * fresh exchange code written beside its launcher (a few seconds at most; the launch goes - * ahead without one when the store cannot be reached). Other stores need nothing. - */ - /** The launcher's variable for the one-shot code; a plain name, cleared before the game starts. */ private const val CODE_VAR = "DD_X" - /** - * Rewrites a game's launcher when its text is not what this build writes - an install from an - * earlier build keeps its old .bat otherwise. Called before every Epic launch. - */ fun refreshLauncher(folder: File, sidecar: StoreGameSidecar) { if (sidecar.launcher != LAUNCHER) return val file = File(folder, LAUNCHER) @@ -103,16 +72,8 @@ object StoreLaunch { }, "stores-launch-prep").start() } - /** What [epicCode] did, for the guest's request and the log: whether a code was written, and why not. */ class CodeResult(val written: Boolean, val reason: String) - /** - * Mints an Epic exchange code for the installed game [id] and writes it as [EPIC_CODE] into the - * very folder its sidecar and launcher are in, where the launcher .bat reads and deletes it. - * Blocking (network). Logs `epic launch id=<id> code=yes|no reason=...` - never the code. Every - * launch path comes here: the Games tab and Stores before they launch, and the compat tool for - * any launch, the Steam client's own Play button included ([StoreLaunchRequests]). - */ fun epicCode(context: Context, id: String): CodeResult { val app = context.applicationContext val result = try { @@ -152,9 +113,7 @@ object StoreLaunch { } } -/** What the Epic backend adds for launches: a short-lived exchange code for the signed-in account. */ interface EpicLaunchSupport { fun exchangeCode(context: Context): String? - /** True when the stored sign-in can no longer be refreshed: the user has to sign in again. */ fun signInExpired(context: Context): Boolean } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt index 03864d99c..968aec7cc 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt @@ -7,24 +7,15 @@ import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File import org.json.JSONObject -/** - * The app's half of droiddeck-store-launch: the compat tool asks, before Proton starts a store - * game, for what the store needs at launch, through files under the session root's `stores/` - * (`req/<id>.json` in, `resp/<id>.json` out, both written tmp + rename). Today that is an Epic - * game's exchange code, which [StoreLaunch.epicCode] writes into the game's own folder; the answer - * says only whether it did and why not. Watched while a session runs. - */ object StoreLaunchRequests { private const val TAG = "StoreLaunchRequests" private val lock = Any() private var observer: FileObserver? = null - /** The store game this session last started; covers Steam's own Play button. */ @Volatile var lastLaunched: Pair<Store, String>? = null fun dir(context: Context) = File(LinuxRuntime.sessionRoot(context), "stores") - /** Before each session: nothing of the last one's requests; then watch for new ones. */ fun start(context: Context) { val app = context.applicationContext synchronized(lock) { @@ -56,7 +47,7 @@ object StoreLaunchRequests { val request = try { JSONObject(file.readText()).also { file.delete() } } catch (e: Exception) { - return // withdrawn already, or not complete yet (the rename brings it again) + return } val reply = when (request.optString("op")) { "gog-achievements" -> runCatching { com.droiddeck.launcher.stores.gog.GogComet.prepare(app, request.optString("id")) } @@ -70,15 +61,12 @@ object StoreLaunchRequests { JSONObject().put("ok", true).put("code", code.written).put("reason", code.reason) } } - // Cloud saves: down before Proton starts (the wrapper waits a bounded time), up after the - // game exits (answered at once; the upload runs on without holding Steam). "cloud-down" -> { val store = Store.byId(request.optString("store")) val id = request.optString("id") if (store == null || id.isEmpty()) JSONObject().put("ok", false).put("reason", "no-game") else { lastLaunched = store to id - // Marked before the game runs: whatever ends it, an upload follows (CloudSaves.uploadDirty). CloudSaves.markDirty(app, store, id) CloudSaves.downloadOrDefer(app, store, id).let { JSONObject().put("ok", it.ok).put("result", it.result).put("files", it.files).put("reason", it.reason) } } @@ -89,8 +77,6 @@ object StoreLaunchRequests { if (store != null && id.isNotEmpty()) Thread({ CloudSaves.uploadDirty(app, store, id, "exit") }, "cloud-up").start() JSONObject().put("ok", true) } - // A browser a game opens inside the session (an EOS device sign-in, a store page): - // http(s) only, handed to Android's browser. "open-url" -> { val url = request.optString("url") if (!(url.startsWith("https://") || url.startsWith("http://"))) JSONObject().put("ok", false).put("reason", "not-http") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreLogFiles.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreLogFiles.kt index eb4a14fc2..0af659012 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLogFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLogFiles.kt @@ -8,13 +8,6 @@ import java.util.Date import java.util.Locale import java.util.concurrent.Executors -/** - * The stores' log on disk, app-private: `filesDir/logs/stores/stores-<yyyy-MM-dd>.log` (beside the - * session folders, where every log lives), one file a - * day, the last [KEEP_DAYS] kept, each capped near [MAX_BYTES] (the full one moves to `.1`). Lines - * arrive already redacted ([StoreLog.redactLine]). Written on one background thread, in order. The - * session's Share logs zip carries these files, so they can be attached to a report when asked. - */ object StoreLogFiles { private const val TAG = "StoreLogFiles" private const val KEEP_DAYS = 7 @@ -31,10 +24,6 @@ object StoreLogFiles { Thread({ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); r.run() }, "store-log-flush").apply { isDaemon = true } } - /** - * Queues one stamped line for today's file. Any thread; nothing touches the disk here. Lines are - * written in batches, at most every half second, on [writer] in order. - */ fun append(context: Context, line: String) { val app = context.applicationContext pending.add(line) @@ -61,7 +50,6 @@ object StoreLogFiles { } } - /** Drops the files older than [KEEP_DAYS] days. Called as the app starts. */ fun prune(context: Context) { val app = context.applicationContext writer.execute { @@ -73,7 +61,6 @@ object StoreLogFiles { } } - /** The files there are, oldest first, for a log share. */ fun files(context: Context): List<File> = dir(context).listFiles { f -> f.isFile && NAME.matches(f.name) }?.sortedBy { it.name }.orEmpty() } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreLoginActivity.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreLoginActivity.kt index 277d42bb5..ce678d3f6 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLoginActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLoginActivity.kt @@ -31,26 +31,11 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.stores.gog.GogAuth import java.util.concurrent.atomic.AtomicBoolean -/** - * A store's own sign-in page in a WebView, one activity for the three stores: GOG's OAuth page - * (the token comes back in the redirect's fragment), Epic's web login (an authorization code on - * a JSON page), Amazon's device sign-in (a PKCE code on the return URL). Plain views, as the - * proven GOG flow was: the page is rendered with the store's own client identity, and a social - * sign-in hop to Google / Facebook / Apple runs under a real browser identity in a popup, since - * those providers refuse an embedded browser. - * - * Nothing from the page is logged but redacted URLs and key names; the tokens go straight to - * StoreAccounts' private file and the activity finishes. - */ class StoreLoginActivity : ComponentActivity() { companion object { private const val TAG = "StoreLogin" const val EXTRA_STORE = "store" private const val KEY_STATE = "oauth_state" - /** - * The colour the Stores page flooded to before opening this page, read once: the page opens - * on it, with no window animation, and its chrome rises in, as a session opens on Play's blue. - */ @Volatile internal var floodColor: Int? = null private const val CHROME_UA = "Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36" @@ -61,15 +46,11 @@ class StoreLoginActivity : ComponentActivity() { } } - /** What one store's flow decides: the page to open, the identity to open it with, and the redirect that ends it. */ internal interface Flow { val startUrl: String val userAgent: String - /** True when [url] is the flow's end; the activity then calls [finish]. */ fun isRedirect(url: String): Boolean - /** Runs off the main thread with the redirect; returns null on success, else a message to show. */ fun finish(activity: StoreLoginActivity, view: WebView, url: String): String? - /** Whether a JSON page at the redirect must be read out of the document instead of the URL (Epic). */ val readsPage: Boolean get() = false } @@ -85,7 +66,6 @@ class StoreLoginActivity : ComponentActivity() { private lateinit var titleText: TextView private var oauthState: String? = null private val captured = AtomicBoolean(false) - /** Opened behind a flood: it opens and closes with no window animation. */ private var flooded = false private fun dp(value: Int): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value.toFloat(), resources.displayMetrics).toInt() @@ -126,7 +106,6 @@ class StoreLoginActivity : ComponentActivity() { override fun finish() { super.finish() - // Back to the Stores page, which is still covered in the flood and drains it from there. @Suppress("DEPRECATION") if (flooded) overridePendingTransition(0, 0) } @@ -193,7 +172,6 @@ class StoreLoginActivity : ComponentActivity() { settings.javaScriptEnabled = true settings.domStorageEnabled = true settings.userAgentString = userAgent - // The stores' social-login buttons are window.open() popups; without these the call is dropped silently. settings.setSupportMultipleWindows(true) settings.javaScriptCanOpenWindowsAutomatically = true if (isPopup) CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) @@ -224,7 +202,6 @@ class StoreLoginActivity : ComponentActivity() { override fun onCloseWindow(window: WebView?) { if (isPopup || window === popupWebView) dismissPopup() } } - /** Deferred: destroying a WebView from inside one of its own callbacks crashes chromium. */ private fun dismissPopup() { val child = popupWebView ?: return popupWebView = null @@ -243,12 +220,10 @@ class StoreLoginActivity : ComponentActivity() { private inner class Client(private val isPopup: Boolean) : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) { Log.d(TAG, "page[popup=$isPopup]: ${StoreLog.redactUrl(url)}") - // Amazon's return URL lands as a page load rather than a navigation we can intercept. if (url != null && view != null && flow?.isRedirect(url) == true && flow?.readsPage != true) complete(view, url) } override fun onPageFinished(view: WebView?, url: String?) { - // Epic's redirect is a JSON page: the code is in the document, read once it is loaded. if (url != null && view != null && flow?.readsPage == true && flow?.isRedirect(url) == true) complete(view, url) } @@ -277,9 +252,6 @@ class StoreLoginActivity : ComponentActivity() { val f = flow ?: return false val url = uri.toString() if (f.isRedirect(url) && !f.readsPage) { complete(view, url); return true } - // A social sign-in hop that stays in the main view: the provider gets a browser identity, - // and the store gets its own identity back afterwards (a plain Chrome UA makes GOG's - // page serve a form that never renders in a WebView). if (!isPopup && store == Store.GOG) { if (GogAuth.isGogHost(uri.host) && view.settings.userAgentString != f.userAgent) { view.settings.userAgentString = f.userAgent; return false } if (GogAuth.isIdentityProviderHost(uri.host) && view.settings.userAgentString != CHROME_UA) { @@ -303,7 +275,6 @@ class StoreLoginActivity : ComponentActivity() { if (!f.readsPage) view.stopLoading() } if (f.readsPage) { - // The document's text, then the exchange off the main thread. view.evaluateJavascript("(function(){ try { return document.body.innerText; } catch(e){ return ''; } })()") { json -> Thread({ settle(f.finish(this, view, json ?: "")) }, "store-login").start() } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreNet.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreNet.kt index 4aeac3517..8517a1aa0 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreNet.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreNet.kt @@ -7,16 +7,9 @@ import java.io.OutputStreamWriter import java.net.HttpURLConnection import java.net.URL -/** - * The small HTTP the storefront catalogs and account calls need: a GET or a JSON POST with the - * right User-Agent, a bearer token when the call is signed in, and a body or null. Store URLs - * carry their authorization in the query string or the path, so a URL is only ever logged - * through [StoreLog.redactUrl]. Plain HttpURLConnection, like the rest of the app. - */ internal object StoreNet { private const val TAG = "StoreNet" - /** A desktop browser: GOG's catalog and Epic's store GraphQL answer it; Epic's edge refuses the bare Java UA. */ const val BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36" fun get(url: String, bearer: String? = null, userAgent: String = BROWSER_UA, timeoutMs: Int = 20_000, headers: Map<String, String> = emptyMap()): String? { @@ -66,10 +59,6 @@ internal object StoreNet { } } - /** - * A 600x900 poster for [title] from SteamGridDB by name, or "" - the lookup the GOG library and - * the Amazon poster backfill use, since those stores publish no tall art of their own. - */ fun sgdbPoster(title: String): String = try { val enc = java.net.URLEncoder.encode(title, "UTF-8") val search = get("https://www.steamgriddb.com/api/v2/search/autocomplete/$enc", bearer = SGDB_KEY) @@ -95,14 +84,7 @@ internal object StoreNet { } } -/** - * Keeps credentials out of the logs. A store's CDN, manifest and token URLs carry their - * authorization in the query string (signed, time-limited tokens, `client_secret`, - * `refresh_token`) or in the authority's userinfo; one such line in logcat or a shared log hands a - * reader a live download. Every store URL that is written anywhere goes through [redactUrl] first. - */ object StoreLog { - /** `scheme://host/path` only: query, fragment and `user:pass@` dropped; "[url]" when it cannot be parsed. */ @JvmStatic fun redactUrl(url: String?): String? { if (url == null) return null @@ -126,11 +108,6 @@ object StoreLog { } } - /** - * What any store line may say, for the stores' log and logcat alike: URLs cut to their host and a - * plain first path segment, token values and Authorization / Cookie headers blanked - the shared - * rules of [com.droiddeck.launcher.core.SecretScrub], which a shared log zip applies too. - */ @JvmStatic fun redactLine(text: String): String = com.droiddeck.launcher.core.SecretScrub.scrub(text, com.droiddeck.launcher.core.SecretScrub.Urls.SHORT, "…") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreSizes.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreSizes.kt index 122b3778f..d8253de87 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreSizes.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreSizes.kt @@ -15,17 +15,9 @@ import com.droiddeck.launcher.stores.epic.EpicPrefs import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors -/** - * Install sizes for owned Epic and Amazon games, whose library answers carry none: Epic's from the - * game's build manifest (the files this device installs), Amazon's from its download manifest. - * Asked for a card as it comes on screen, three at a time in the background, once per game - the - * answer is kept in the store's prefs (`size_<id>`, where the library reads it too). Until it is - * known the card shows nothing. - */ object StoreSizes { private const val TAG = "StoreSizes" - /** Sizes found this run, by card key; the cards read them. */ val known = mutableStateMapOf<String, Long>() private val asked = ConcurrentHashMap.newKeySet<String>() @@ -36,7 +28,6 @@ object StoreSizes { private val caches = ConcurrentHashMap<Store, SizeCache>() private fun cache(context: Context, store: Store) = caches.getOrPut(store) { SizeCache.forStore(context.applicationContext.filesDir, store) } - /** A card for [item] is on screen (or about to be): its size from the cache for this version, else looked up. */ fun request(context: Context, item: CatalogItem) { if (!item.owned || item.sizeBytes > 0 || item.store == Store.GOG) return if (known.containsKey(item.key) || !asked.add(item.key)) return @@ -58,7 +49,6 @@ object StoreSizes { } } - /** An installed game's size on disk, by card key; worked out once per install. */ val onDisk = mutableStateMapOf<String, Long>() fun requestDisk(context: Context, key: String, game: InstalledStoreGame) { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoresNative.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoresNative.kt index 6d626353f..a01558661 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoresNative.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoresNative.kt @@ -2,12 +2,6 @@ package com.droiddeck.launcher.stores import android.util.Log -/** - * The native download engine for the three stores: `libdroiddeckstores.so`, loaded the first time - * a store needs it, never at process start. The app has to run without it (a build without the - * Rust side, or an ABI the library was not built for), so [available] is a plain false then and - * Setup says "Stores engine not built"; the store managers fall back to their Java fetch loops. - */ object StoresNative { private const val TAG = "StoresNative" private const val LIBRARY = "droiddeckstores" @@ -16,13 +10,10 @@ object StoresNative { @Volatile private var versionText: String? = null @Volatile private var failure: String? = null - /** True once the library is loaded and its version symbol binds; false when it cannot be. Cached. */ val available: Boolean get() = ensureLoaded() - /** The engine's own version string, or null when the library is not there. */ val version: String? get() { ensureLoaded(); return versionText } - /** Why the library did not load, for the Setup row; null when it did. */ val problem: String? get() { ensureLoaded(); return failure } @Synchronized @@ -34,8 +25,6 @@ object StoresNative { Log.i(TAG, "engine ${versionText ?: "?"} loaded") true } catch (t: Throwable) { - // UnsatisfiedLinkError when the .so is missing or a symbol is not there; anything else is - // a surprise worth the same treatment: the Java paths carry on. failure = t.javaClass.simpleName + (t.message?.let { ": $it" } ?: "") Log.w(TAG, "engine unavailable: $failure") false @@ -44,11 +33,9 @@ object StoresNative { return ok } - /** Resets the cached answer, so a probe after an update tries the library again. */ @Synchronized fun forget() { loaded = null } - /** The crate's version string; only after [ensureLoaded] said yes. */ @JvmStatic external fun nativeVersion(): String } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt index 4f1cb172c..5551535d3 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt @@ -14,65 +14,39 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * The Stores section's state for the life of the process (like the Flathub store's StoreState): - * who is signed in where, each store's library and shelves, what is installed on disk and what is - * being downloaded, so a download carries on - and its progress shows - while the user moves - * around the launcher or plays something meanwhile. - * - * Everything here is read on the main thread by Compose; the workers post their results back. - * The stores themselves plug in as [StoreBackend]s; a store with no backend in this build shows - * its sign-in card and nothing else. - */ object StoresState { private const val TAG = "Stores" private val main = Handler(Looper.getMainLooper()) - /** The signed-in account name per store; absent = signed out. */ val accounts = mutableStateMapOf<Store, String>() - /** Each store's owned games, once synced; absent until then. */ val library = mutableStateMapOf<Store, List<CatalogItem>>() - /** Each store's storefront shelves, once fetched. */ val shelves = mutableStateMapOf<Store, StoreShelves>() - /** What a store is doing for the user right now ("Syncing 12 games…"); absent when idle. */ val status = mutableStateMapOf<Store, String>() - /** Why the last sync or shelf fetch failed, per store; cleared by the next success. */ val problems = mutableStateMapOf<Store, String>() - /** Games the stores installed, as their sidecars say; rebuilt by [refresh]. */ var installed by mutableStateOf<List<InstalledStoreGame>>(emptyList()) private set - /** - * The download queue as the Downloads page lists it, newest first. Replaced only when a row is - * added, removed, reordered or changes state - not on progress - so whatever iterates it does - * not recompose on every byte. A row's progress is read through [download]. - */ var downloads by mutableStateOf<List<DownloadEntry>>(emptyList()) private set - /** Downloads queued, running or paused across the stores (the rail badge); changes only when the count does. */ var activeDownloads by mutableStateOf(0) private set private val rows = HashMap<String, androidx.compose.runtime.MutableState<DownloadEntry?>>() - /** One row's live entry: a composable reading it recomposes for that row's progress only. Main thread. */ fun download(key: String): DownloadEntry? = rows.getOrPut(key) { mutableStateOf(null) }.value - /** Main thread: each row's holder set (equal entries change nothing), the list and the count only when they change. */ - /** The newest list as published, progress included; for the notification, which is not composed. */ @Volatile var latestDownloads: List<DownloadEntry> = emptyList() private set private var notifiedAt = 0L internal fun publishDownloads(list: List<DownloadEntry>) { latestDownloads = list - // The notification follows at most once a second. val now = android.os.SystemClock.uptimeMillis() if (now - notifiedAt >= 1000) { notifiedAt = now; com.droiddeck.launcher.stores.download.StoreDownloadService.tick() } val keys = list.mapTo(HashSet()) { it.key } @@ -84,22 +58,18 @@ object StoresState { } - /** The native engine's version once probed, "" when the probe said it is missing, null before. */ var engine by mutableStateOf<String?>(null) private set private val backends = HashMap<Store, StoreBackend>() - /** A store's implementation, registered once as the app starts; null when this build lacks it. */ fun backend(store: Store): StoreBackend? = backends[store] fun register(backend: StoreBackend) { backends[backend.store] = backend } - /** Reads what is on disk for every store: sign-ins, installed games, the engine probe. Off the main thread. */ fun refresh(context: Context) { val app = context.applicationContext - // The first refresh after the app starts brings back what was in the download queue. com.droiddeck.launcher.stores.download.SavedQueue.restore(app) Thread({ val found = Store.entries.mapNotNull { store -> StoreAccounts.signedInAs(app, store)?.let { store to it } } @@ -120,17 +90,14 @@ object StoresState { fun isSignedIn(store: Store): Boolean = accounts.containsKey(store) - /** A store action's error line when it found no usable sign-in: gone, or on disk but unreadable for now. */ fun notSignedInLine(store: Store): String = if (StoreAccounts.isUnavailable(store)) "${store.shortLabel} sign-in could not be read: try again." else "${store.shortLabel} session expired: sign in again." - /** Stores whose sign-in has run out (a launch could not get a code): their chip dims until the user signs in again. */ val expired = mutableStateMapOf<Store, Boolean>() fun markSignInExpired(store: Store) = post { expired[store] = true } - /** Installs that stopped before they finished; their Install button reads "Resume install". */ var unfinished by mutableStateOf<List<StoreInstallRoot.Unfinished>>(emptyList()) internal set @@ -138,7 +105,6 @@ object StoresState { it.store == item.store && (it.id == item.id || (it.id == null && it.folder.name == StoreInstallRoot.folderName(item.title, item.id))) } - /** Deletes an unfinished install's folder and caches, as Cancel would; drops its row if there is one. */ fun clearUnfinished(context: Context, item: CatalogItem) { val app = context.applicationContext val key = "${item.store.id}:${item.id}" @@ -151,7 +117,6 @@ object StoresState { fun installedGame(store: Store, id: String): InstalledStoreGame? = installed.firstOrNull { it.sidecar.store == store && it.sidecar.id == id } - /** The user's library and the shelves for [store], from the cache first and the network when stale. */ fun open(context: Context, store: Store, force: Boolean = false) { val backend = backends[store] ?: return val app = context.applicationContext @@ -160,7 +125,6 @@ object StoresState { if (force || !shelves.containsKey(store)) backend.loadShelves(app, force) } - /** A store not signed into: its public storefront only, for its sign-in page to show. */ fun preview(context: Context, store: Store) { if (!shelves.containsKey(store)) backends[store]?.loadShelves(context.applicationContext, false) } @@ -183,13 +147,8 @@ object StoresState { }, "stores-signout").start() } - /** A notification asked for the Downloads chip; the Stores page takes it and clears it. */ var openDownloads by mutableStateOf(false) - /** - * Android 13+: notifications need permission. Asked once, the first time a download starts from - * a screen; refused, downloads still run, without their notification. - */ private fun askForNotifications(context: Context) { val activity = context as? android.app.Activity ?: return if (android.os.Build.VERSION.SDK_INT < 33) return @@ -200,23 +159,15 @@ object StoresState { runCatching { activity.requestPermissions(arrayOf("android.permission.POST_NOTIFICATIONS"), 7102) } } - /** An install waiting for the user to say where: the page shows the choice while this is set. */ var pendingInstall by mutableStateOf<CatalogItem?>(null) - /** - * The way a page starts an install: with a card in the device the user is asked where (the - * dialog's default is last time's pick, never a silent choice); with none it goes to internal - * storage at once. - */ fun requestInstall(context: Context, item: CatalogItem) { askForNotifications(context) - // A resume goes where the files already are; there is nothing to choose. if (isUnfinished(item)) { install(context, item); return } val targets = StoreInstallRoot.targets(context) if (targets.size > 1) pendingInstall = item else install(context, item, targets.first().root) } - /** Starts an install into [root] (the internal root when null). */ fun install(context: Context, item: CatalogItem, root: java.io.File? = null) { val app = context.applicationContext val target = root ?: StoreInstallRoot.installRoot(app) @@ -224,14 +175,8 @@ object StoresState { backends[item.store]?.install(app, item, target) ?: logLine("${item.store.label}: this build cannot install yet") } - /** Games being removed, by key ("store:id"), and how far along (0..1): their page shows it. */ val uninstalling = mutableStateMapOf<String, Float>() - /** - * Removes the game's folder and its shortcut, then [onDone] on the main thread. The game reads - * as removed the moment it is gone (its page shows Install, its card its size), not once the - * whole library has been read again. - */ fun uninstall(context: Context, game: InstalledStoreGame, onDone: () -> Unit = {}) { val app = context.applicationContext val key = "${game.sidecar.store.id}:${game.sidecar.id}" @@ -248,18 +193,14 @@ object StoresState { }, "stores-uninstall").start() } - /** Told on the main thread when an install or removal changed what is on disk; the launcher rebuilds its Games list. */ @Volatile var libraryListener: (() -> Unit)? = null - /** A game landed or went: the installed set is re-read and the launcher told. Any thread. */ fun notifyLibraryChanged(context: Context) { refresh(context) main.post { libraryListener?.invoke() } } - /** Appends to the engine log with a clock, keeping the last 80 lines. Any thread. */ fun logLine(raw: String) { - // Every store line passes here, the engines' included: no token or full URL is kept. val text = StoreLog.redactLine(raw) Log.i(TAG, text) val app = appContext ?: return @@ -268,14 +209,11 @@ object StoresState { private var appContext: Context? = null - /** Called once as the app starts: where the log file goes, and the old days pruned. */ fun init(context: Context) { appContext = context.applicationContext StoreLogFiles.prune(context) - // The EOS overlay an earlier build downloaded (656 MB) is gone with the feature. val overlay = java.io.File(com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context), "root/.local/share/droiddeck/epic-overlay") if (overlay.exists()) Thread({ overlay.deleteRecursively(); Log.i(TAG, "removed the old EOS overlay download") }, "epic-overlay-cleanup").start() - // Cloud saves a killed app never uploaded; not while a session (and maybe that game) runs. val app = context.applicationContext Thread({ CloudSaves.uploadAllDirty(app, "recovery") }, "cloud-recovery").start() } @@ -285,15 +223,12 @@ object StoresState { private val CLOCK = SimpleDateFormat("HH:mm:ss.SSS", Locale.US) } -/** What one store contributes: sign-in, its library, its shelves, installs and removals. */ interface StoreBackend { val store: Store fun signIn(context: Context) fun signOut(context: Context) fun syncLibrary(context: Context, force: Boolean) fun loadShelves(context: Context, force: Boolean) - /** Queues an install of [item] under [root] (a StoreInstallRoot target). */ fun install(context: Context, item: CatalogItem, root: java.io.File) - /** The store's own records of the install, before the folder goes (the queue row, cached ids). */ fun uninstall(context: Context, game: InstalledStoreGame) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonApiClient.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonApiClient.java index 7683217d9..a17b6a7cc 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonApiClient.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonApiClient.java @@ -16,16 +16,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -/** - * Amazon's distribution API, as the launcher calls it: every gaming call carries `X-Amz-Target`, - * `x-amzn-token`, the launcher's User-Agent and `Content-Encoding: amz-1.0`. - */ public final class AmazonApiClient { private static final String TAG = "AmazonApi"; private static final String ENTITLEMENTS_URL = "https://gaming.amazon.com/api/distribution/entitlements"; private static final String DISTRIBUTION_URL = "https://gaming.amazon.com/api/distribution/v2/public"; - // As nile sends them: the session's User-Agent, and the launcher's own string under "UserAgent". private static final String GAMING_USER_AGENT = "AGSLauncher/1.0.0"; private static final String LAUNCHER_USER_AGENT = "com.amazon.agslauncher.win/3.0.9202.1"; private static final String DOWNLOAD_USER_AGENT = "nile/0.1 Amazon"; @@ -33,7 +28,6 @@ public final class AmazonApiClient { private AmazonApiClient() {} - /** The owned games, every page, one per productId; null when Amazon refused or could not be reached. */ public static List<AmazonGame> getEntitlements(String accessToken, String deviceSerial) { Map<String, AmazonGame> seen = new HashMap<>(); String nextToken = null; @@ -65,7 +59,6 @@ public final class AmazonApiClient { AmazonGame game = parseEntitlement(entitlements.getJSONObject(i)); if (game != null && !game.productId.isEmpty()) seen.put(game.productId, game); } - // A JSON null reads as "null" through optString; the last page carries none. nextToken = json.isNull("nextToken") ? null : json.optString("nextToken", ""); if (nextToken != null && nextToken.isEmpty()) nextToken = null; } catch (Exception e) { @@ -94,8 +87,6 @@ public final class AmazonApiClient { if (parentId.isEmpty()) parentId = product.optString("parentId", ""); JSONObject detail = product.optJSONObject("productDetail"); if (detail != null) { - // The opaque images only: the square icon, and for the wide card the second - // background (a plain key art) before the first (often faded) or the crown image. game.artUrl = detail.optString("iconUrl", ""); JSONObject details = detail.optJSONObject("details"); if (details != null) { @@ -123,7 +114,6 @@ public final class AmazonApiClient { public String versionId; } - /** The download root and version for a game; takes the entitlement id, not the product id. */ public static GameDownloadSpec getGameDownload(String accessToken, String entitlementId) { try { JSONObject body = new JSONObject().put("entitlementId", entitlementId).put("Operation", "GetGameDownload"); @@ -140,7 +130,6 @@ public final class AmazonApiClient { } } - /** The live version id of a product, for update checks. */ public static String getLiveVersionId(String accessToken, String productId) { try { JSONObject body = new JSONObject().put("adgProductIds", new JSONArray().put(productId)).put("Operation", "GetLiveVersionIds"); @@ -156,7 +145,6 @@ public final class AmazonApiClient { } } - /** `baseUrl/segment`, keeping a query string on the base where it belongs. */ public static String appendPath(String baseUrl, String segment) { int q = baseUrl.indexOf('?'); if (q >= 0) return baseUrl.substring(0, q) + "/" + segment + baseUrl.substring(q); @@ -191,7 +179,6 @@ public final class AmazonApiClient { } } - /** Raw bytes (the manifest). */ public static byte[] getBytes(String urlStr, String accessToken) { try { HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonAuthClient.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonAuthClient.java index 878f659ae..39dce7ca3 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonAuthClient.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonAuthClient.java @@ -16,12 +16,6 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; -/** - * Amazon's device-auth endpoints, as the Amazon Games launcher registers itself: - * POST api.amazon.com/auth/register (the PKCE code for bearer tokens, with a device registration) - * POST api.amazon.com/auth/token (access token renewal; the refresh token stays) - * POST api.amazon.com/auth/deregister (sign-out, best-effort) - */ public final class AmazonAuthClient { private static final String TAG = "AmazonAuth"; @@ -39,13 +33,11 @@ public final class AmazonAuthClient { public String refreshToken; public long expiresIn; public String name; - /** The serial Amazon registered the device under; the entitlements call hashes this one. */ public String deviceSerial; } private AmazonAuthClient() {} - /** Exchanges the PKCE authorization code for bearer tokens; null on failure. */ public static RegisterResult registerDevice(String authCode, String codeVerifier, String deviceSerial, String clientId) { try { JSONObject authData = new JSONObject(); @@ -93,7 +85,6 @@ public final class AmazonAuthClient { } } - /** A new access token; the refresh token is not returned and stays as it was. */ public static RegisterResult refreshAccessToken(String refreshToken) { try { JSONObject body = new JSONObject(); diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt index 960666133..10b74d0df 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt @@ -23,11 +23,6 @@ import com.droiddeck.launcher.stores.download.DownloadStage import java.io.File import java.util.concurrent.atomic.AtomicBoolean -/** - * Amazon Games in the Stores section. Amazon has no browsable catalog for third-party clients - - * Prime Gaming's claims live on gaming.amazon.com, signed in - so the Store tab's shelves are cut - * from the account's own library: owned-not-installed as Trending, installed as Your library. - */ object AmazonBackend : StoreBackend { private const val TAG = "AmazonBackend" override val store: Store = Store.AMAZON @@ -46,7 +41,6 @@ object AmazonBackend : StoreBackend { override fun syncLibrary(context: Context, force: Boolean) { val app = context.applicationContext Thread({ - // The cache off the main thread; with it on screen a background sync keeps quiet. val cached = AmazonLibrary.cached(app) val quiet = cached.isNotEmpty() && !force StoresState.post { @@ -67,7 +61,6 @@ object AmazonBackend : StoreBackend { }, "amazon-sync").start() } - /** The library and, from it, the shelves. */ private fun publish(app: Context, games: List<AmazonGame>) { val items = games.map { AmazonLibrary.toCatalogItem(app, it) } StoresState.library[store] = com.droiddeck.launcher.stores.mergeItems(StoresState.library[store], items) @@ -75,8 +68,6 @@ object AmazonBackend : StoreBackend { StoresState.shelves[store] = StoreShelves(trending = items.filter { it.id !in installed }, free = emptyList(), deals = emptyList(), whatsNew = emptyList()) } - // Nothing public to fetch; the shelves follow the library sync, which StoresState.open starts - // alongside this. A second sync here only came back Busy and cleared the first one's status. override fun loadShelves(context: Context, force: Boolean) {} override fun install(context: Context, item: CatalogItem, root: File) { @@ -135,7 +126,6 @@ object AmazonBackend : StoreBackend { } } - /** Amazon's device sign-in: a PKCE challenge on the sign-in page, the code on the return URL, a device registration for tokens. */ private class LoginFlow : StoreLoginActivity.Flow { private val serial = AmazonPkce.generateDeviceSerial() private val clientId = AmazonPkce.generateClientId(serial) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonCredentialStore.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonCredentialStore.kt index 95944375d..1f05c13f1 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonCredentialStore.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonCredentialStore.kt @@ -6,10 +6,6 @@ import com.droiddeck.launcher.stores.Store import com.droiddeck.launcher.stores.StoreAccounts import org.json.JSONObject -/** - * The Amazon sign-in in StoreAccounts' private file: the bearer tokens, the device serial and - * client id the registration was made with (the entitlements call hashes the serial), the expiry. - */ object AmazonCredentialStore { private const val TAG = "AmazonAuth" @@ -34,7 +30,6 @@ object AmazonCredentialStore { @JvmStatic fun isLoggedIn(context: Context): Boolean = load(context) != null - /** A fresh access token whatever the stored expiry says: for a call Amazon refused with the stored one. */ @JvmStatic fun refresh(context: Context): String? { val creds = load(context) ?: return null @@ -44,7 +39,6 @@ object AmazonCredentialStore { return result.accessToken } - /** A valid access token, refreshed within five minutes of expiry; null when signed out or the refresh failed. */ @JvmStatic fun getValidAccessToken(context: Context): String? { val creds = load(context) ?: return null diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java index 82a8b23ea..2eaaa31a2 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java @@ -26,12 +26,6 @@ import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; -/** - * The Amazon Games install pipeline, ported from Bannerlator: GetGameDownload for the download - * root and version, `manifest.proto`, then every file fetched (the native engine when it is - * there, eight Java threads otherwise), SHA-256 checked, written as `.tmp` and renamed. A - * complete file on disk is skipped by size, so a rerun resumes. Blocking; the queue runs it. - */ public final class AmazonDownloadManager { private static final String TAG = "AmazonDownload"; @@ -46,7 +40,6 @@ public final class AmazonDownloadManager { void onProgress(String message, int pct); default void onBytes(long done, long total, long speedBps) {} default void onLog(String line) {} - /** What this run fetches and what the game takes on disk. */ default void onSizes(long downloadBytes, long diskBytes) {} } @@ -62,7 +55,6 @@ public final class AmazonDownloadManager { private AmazonDownloadManager() {} - /** Installs the game into {@code installDir}; null when cancelled, an exception when it fails. */ public static Result install(Context ctx, AmazonGame game, String accessToken, File installDir, AtomicBoolean cancel, Callback cb) throws InstallException { if (game.entitlementId == null || game.entitlementId.isEmpty()) throw new InstallException("This game has no entitlement to download with"); try { @@ -134,7 +126,6 @@ public final class AmazonDownloadManager { } } - /** One entry per manifest file with the exact URL the Java loop would open and the SHA-256 when it would check it. */ static String buildPlan(List<AmazonManifest.ManifestFile> files, String baseUrl) { JSONArray arr = new JSONArray(); try { @@ -211,7 +202,6 @@ public final class AmazonDownloadManager { return false; } - /** Streams one file; false = cancelled. */ private static boolean downloadFile(String urlStr, File tmpFile, AtomicLong totalDownloaded, AtomicLong lastEmit, long totalSize, AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps, Callback cb, AtomicBoolean cancel) throws IOException { HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); @@ -240,7 +230,6 @@ public final class AmazonDownloadManager { return true; } - /** The install size from the manifest alone, or -1. Blocking. */ public static long fetchInstallSizeBytes(String accessToken, String entitlementId) { try { AmazonApiClient.GameDownloadSpec spec = AmazonApiClient.getGameDownload(accessToken, entitlementId); diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonGame.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonGame.java index 891602b68..3ce769ade 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonGame.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonGame.java @@ -1,17 +1,10 @@ package com.droiddeck.launcher.stores.amazon; -/** - * One Amazon Games library entry. {@code productId} ("amzn1.adg.product.…") names the game, - * {@code entitlementId} (the top-level UUID of GetEntitlements) is what GetGameDownload takes, - * {@code productSku} goes to the game as AMAZON_GAMES_FUEL_PRODUCT_SKU. - */ public class AmazonGame { public String productId = ""; public String entitlementId = ""; public String title = ""; - /** iconUrl: square art. */ public String artUrl = ""; - /** backgroundUrl1: the wide background. */ public String heroUrl = ""; public String developer = ""; public String publisher = ""; @@ -20,7 +13,6 @@ public class AmazonGame { public boolean isDLC = false; public String parentProductId = ""; - /** The id's last segment, for display when there is no title. */ public String shortId() { int dot = productId.lastIndexOf('.'); return (dot >= 0 && dot < productId.length() - 1) ? productId.substring(dot + 1) : productId; diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLaunchHelper.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLaunchHelper.java index bc1bba667..c95bb74f5 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLaunchHelper.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLaunchHelper.java @@ -12,20 +12,12 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; -/** - * How an installed Amazon game is started: its `fuel.json` names the exe, a working subfolder and - * arguments when the publisher shipped one; otherwise the exe is scored (Unreal shipping binaries - * first, the title's name, never crash handlers or installers). The FuelPump variables the Amazon - * Games Services SDK reads go into the game's environment. - */ public final class AmazonLaunchHelper { private static final String TAG = "AmazonLaunch"; public static final class LaunchSpec { - /** The exe relative to the install folder, forward slashes. */ public String exeRelative = ""; - /** The arguments fuel.json names, in order. */ public List<String> args = new ArrayList<>(); } @@ -65,7 +57,6 @@ public final class AmazonLaunchHelper { return spec; } - /** The FuelPump variables, as KEY=VALUE, for the game's environment. */ public static String[] buildFuelEnv(String entitlementId, String productSku, String displayName) { return new String[]{ "FUEL_DIR=C:\\ProgramData\\Amazon Games Services\\Legacy", diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt index 0cd3be6b0..93249b7d2 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt @@ -14,12 +14,6 @@ import java.util.concurrent.Callable import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean -/** - * The signed-in account's Amazon Games library: the entitlements, DLC folded under their base - * game, cached so the tab paints at once, re-synced when forced or 15 minutes stale. Amazon - * publishes a square icon and a wide background only, so a 2:3 poster per title is backfilled - * from SteamGridDB (a few per pass). Ported from Bannerlator's AmazonLibraryRepo. - */ object AmazonLibrary { private const val TAG = "AmazonLibrary" private const val CACHE_KEY = "library_cache" @@ -74,7 +68,6 @@ object AmazonLibrary { val creds = AmazonCredentialStore.load(app) ?: return SyncResult.NotLoggedIn val token = AmazonCredentialStore.getValidAccessToken(app) ?: return SyncResult.Failed(StoresState.notSignedInLine(Store.AMAZON)) onStatus(app.getString(R.string.stores_sync_game_list)) - // Refused with the stored token (revoked, or an expiry read wrong): once more on a fresh one. val all = AmazonApiClient.getEntitlements(token, creds.deviceSerial) ?: AmazonCredentialStore.refresh(app)?.let { AmazonApiClient.getEntitlements(it, creds.deviceSerial) } if (all == null) return if (cachedList.isEmpty()) SyncResult.Failed(app.getString(R.string.stores_sync_amazon_no_answer)) else SyncResult.Ok(cachedList) @@ -105,7 +98,6 @@ object AmazonLibrary { val ed = p.edit() for (f in futures) { val (pid, url) = runCatching { f.get() }.getOrNull() ?: continue - // A miss is recorded as "" so it is not asked again every pass. ed.putString("vcover_$pid", url) } ed.apply() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonManifest.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonManifest.java index 25fb03178..f40db9c77 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonManifest.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonManifest.java @@ -13,18 +13,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -/** - * Amazon's `manifest.proto`: a big-endian header length, a ManifestHeader protobuf naming the - * compression, then an LZMA (or XZ) body holding the Manifest protobuf - packages of files, each - * with a Windows path, a size and a SHA-256. Read with a tiny protobuf walker; the XZ library the - * app already carries does the decompression. - */ public final class AmazonManifest { public static final class ManifestFile { public final String path; public final long size; - /** 0 = SHA-256. */ public final int hashAlgorithm; public final byte[] hashBytes; diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonNative.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonNative.kt index 511dc946f..165ef455c 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonNative.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonNative.kt @@ -1,4 +1,3 @@ -// JNI symbols depend on this package path and class name (app/src/main/rust/stores/JNI.md). package com.droiddeck.launcher.stores.amazon import android.util.Log @@ -6,22 +5,13 @@ import com.droiddeck.launcher.stores.StoresNative import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -/** - * The native Amazon file fetcher in `libdroiddeckstores.so`: one streamed GET per manifest file, - * SHA-256 checked, written as `<dest>.tmp` and renamed. The manager keeps manifest, auth, markers - * and post-install; [runBlocking] mirrors the Java loop's shape and polls the cancel probe. - */ object AmazonNative { const val TAG = "AmazonNative" private const val CANCEL_POLL_MS = 100L - /** Callbacks from the engine; every method is called on a native thread. */ interface Listener { - /** Once up front with the resume-skipped credit, then after each committed file; [bytesDone] includes skipped files. */ fun onProgress(bytesDone: Long, bytesTotal: Long, filesDone: Long, filesTotal: Long) - /** One diagnostic line; the engine itself does not write to logcat. */ fun onLog(line: String) - /** Exactly once, when the run succeeds, fails or is cancelled. */ fun onComplete(success: Boolean, error: String, bytesWritten: Long) } @@ -32,10 +22,6 @@ object AmazonNative { @JvmStatic fun isAvailable(): Boolean = StoresNative.available - /** - * Runs one download to completion. [planJson]: `[{relPath, url, size, sha256hex}]`, every - * manifest file (the engine skips complete ones itself). [maxWorkers] <= 0 = the engine's default. - */ @JvmStatic fun runBlocking(planJson: String, installDir: String, caBundlePath: String, maxWorkers: Int, processWorkers: Int, isCancelled: CancelCheck?, listener: Listener): RunResult { if (!StoresNative.ensureLoaded()) return RunResult(false, false, "engine not built", 0L) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPkce.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPkce.java index 9d0a06403..ff7804782 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPkce.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPkce.java @@ -7,16 +7,13 @@ import java.security.MessageDigest; import java.security.SecureRandom; import java.util.UUID; -/** The PKCE values Amazon's device sign-in takes, generated the way the Amazon Games launcher does. */ public final class AmazonPkce { private static final String DEVICE_TYPE = "A2UMVHOX7UP4V7"; private AmazonPkce() {} - /** One per sign-in: a UUID as uppercase hex. */ public static String generateDeviceSerial() { return UUID.randomUUID().toString().replace("-", "").toUpperCase(); } - /** hex("serial#DEVICE_TYPE"). */ public static String generateClientId(String serial) { byte[] bytes = (serial + "#" + DEVICE_TYPE).getBytes(StandardCharsets.UTF_8); StringBuilder sb = new StringBuilder(); @@ -39,7 +36,6 @@ public final class AmazonPkce { } } - /** SHA-256 of the input as uppercase hex: the entitlements call's hardwareHash. */ public static String sha256Upper(String input) { try { byte[] hash = MessageDigest.getInstance("SHA-256").digest(input.getBytes(StandardCharsets.UTF_8)); diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPrefs.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPrefs.kt index 6469b63af..ef291535c 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPrefs.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.stores.amazon import android.content.Context import android.content.SharedPreferences -/** Amazon's own records that are not credentials: the library cache, posters, versions. Public for the Java clients. */ object AmazonPrefs { @JvmStatic fun get(context: Context): SharedPreferences = context.applicationContext.getSharedPreferences("stores.amazon", Context.MODE_PRIVATE) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadModels.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadModels.kt index 9813f169d..8683efa06 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadModels.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadModels.kt @@ -2,20 +2,9 @@ package com.droiddeck.launcher.stores.download import com.droiddeck.launcher.stores.Store -/** - * The store-agnostic shape every store's download is projected into, so one queue and one - * Downloads page serve all three. Ported from Bannerlator's download registry, trimmed to what - * this app shows: a stage, one byte pair, speed and ETA, and the handles that drive it. - */ -/** Where a download is. The names are what the Downloads page prints over its stage strip. */ enum class DownloadStage { MANIFEST, DOWNLOAD, VERIFY, INSTALL, DONE } -/** - * QUEUED / RUNNING / PAUSED are the active states (in memory only). INSTALLED is the terminal - * success; FAILED and CANCELLED stay listed for the session so the page can offer a retry or a - * dismissal. - */ enum class DownloadState { QUEUED, RUNNING, PAUSED, INSTALLED, FAILED, CANCELLED } data class DownloadEntry( @@ -25,43 +14,28 @@ data class DownloadEntry( val cover: String? = null, val state: DownloadState = DownloadState.QUEUED, val stage: DownloadStage = DownloadStage.MANIFEST, - /** Bytes landed and the total, as the engine reports them (install size). */ val bytesDone: Long = 0L, val bytesTotal: Long = 0L, - /** Smoothed speed in bytes/s (0 = unknown) and seconds left (-1 = unknown). */ val speedBps: Long = 0L, val etaSeconds: Long = -1L, - /** The engine's current line ("Downloading: data.pak"), shown under the bar. */ val detail: String = "", - /** Where it was installed (INSTALLED) or why it failed (FAILED). */ val installPath: String? = null, val error: String? = null, - /** 1-based place in the queue while QUEUED, else 0. */ val queuePosition: Int = 0, - /** When the entry reached a terminal state, for ordering and ageing out. */ val finishedAt: Long = 0L, val startedAt: Long = 0L, - /** Where it is going: the install target's name ("Internal storage", the card's name). */ val location: String = "", - /** - * The active stage's own progress outside Download (which runs on [bytesDone]/[bytesTotal]): - * an amount ([stageDone] of [stageTotal], bytes or items) and, when it counts files, [stageItems] - * of [stageItemsTotal]. Reset when the stage changes. - */ val stageDone: Long = 0L, val stageTotal: Long = 0L, val stageItems: Int = 0, val stageItemsTotal: Int = 0, - /** What the game takes on disk, when the store has said (the download itself is [bytesTotal], often compressed). */ val diskBytes: Long = 0L, - /** Stages already passed, one bit per [DownloadStage] ordinal; stages can come back (Epic checks files before it fetches). */ val stagesPassed: Int = 0, ) { fun passed(s: DownloadStage): Boolean = state == DownloadState.INSTALLED || (stagesPassed and (1 shl s.ordinal)) != 0 val key: String get() = "${store.id}:$id" val isActive: Boolean get() = state == DownloadState.QUEUED || state == DownloadState.RUNNING || state == DownloadState.PAUSED - /** The active stage's progress, 0..1, or -1 while it has nothing to count yet. */ val stageFraction: Float get() = when { state == DownloadState.INSTALLED || stage == DownloadStage.DONE -> 1f stage == DownloadStage.DOWNLOAD -> if (bytesTotal > 0L) (bytesDone.toDouble() / bytesTotal).toFloat().coerceIn(0f, 1f) else -1f @@ -69,7 +43,6 @@ data class DownloadEntry( stageItemsTotal > 0 -> (stageItems.toFloat() / stageItemsTotal).coerceIn(0f, 1f) else -> -1f } - /** 0..1 for a single bar (a card, the game page): the active stage's progress. */ val fraction: Float get() = stageFraction.coerceAtLeast(0f) val percent: Int get() = (fraction * 100).toInt() } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt index 25fd8aa54..93621aa9f 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt @@ -6,53 +6,23 @@ import com.droiddeck.launcher.session.SessionPrefs import com.droiddeck.launcher.stores.StoresState import java.util.concurrent.atomic.AtomicBoolean -/** - * One queue for every store's downloads, [parallel] of them running at a time (1..3, Setup and - * the Downloads page), the rest waiting as data until a slot frees. A store hands in a - * [DownloadJob] - its whole install: manifest, files, verify, post-install - and reports back - * through the job's [JobHandle]; the queue owns the states, the registry row the UI draws and the - * foreground service that keeps the process alive while anything runs. - * - * Pause for these engines is "stop now, keep the files": every store's install skips files that - * are already complete and verified, so resuming is simply running the job again. Cancel removes - * what was downloaded (the job's own cleanup) and the row. - * - * Thread-safe: one [lock] guards the table; jobs run on their own threads, outside it. - */ object DownloadQueue { private const val TAG = "StoreDownloads" - /** The work for one download. [run] blocks until it is over; [cancel] asks a running job to stop. */ interface DownloadJob { - /** Runs the install; returns the install folder's path on success, throws or returns null on failure. */ fun run(handle: JobHandle): String? - /** Stops a running job; [deleteFiles] false keeps what was fetched for a later resume. */ fun cancel(deleteFiles: Boolean) } - /** What a running job reports with. */ class JobHandle internal constructor(val key: String, internal val cancelled: AtomicBoolean) { val isCancelled: Boolean get() = cancelled.get() - /** Enters [stage] (a repeat of the current one only updates the line); a new stage starts its own count from zero. */ fun stage(stage: DownloadStage, detail: String = "") = enter(key, stage, stripSpeed(detail)) - /** - * The active stage's own count outside Download: an amount and, when it counts files, the - * files. [bytes] = the amount is bytes written, so the stage gets its own rate and ETA. - */ fun stageProgress(done: Long, total: Long, items: Int = 0, itemsTotal: Int = 0, bytes: Boolean = false) = stageCount(key, done, total, items, itemsTotal, bytes) - /** The game's size on disk, once the store has said. */ fun diskSize(bytes: Long) = update(key) { if (bytes > 0) it.copy(diskBytes = bytes) else it } - /** - * Bytes so far (negative = unchanged), the total (<= 0 = unchanged), a line. [speedBps] is the - * engine's own figure: a burst rate at the network side, which jumps while the write side - * paces the bar, so it is not what the page shows - the queue measures the install rate - * from the byte deltas itself ([measure]); the engine's figure stays in its log lines. - */ fun progress(bytesDone: Long, bytesTotal: Long, detail: String? = null, @Suppress("UNUSED_PARAMETER") speedBps: Long = -1L) = measure(key, bytesDone, bytesTotal, detail?.let(::stripSpeed)) fun log(line: String) = StoresState.logLine(line) } - /** "Downloading: data.pak 12.3 MB/s" → "Downloading: data.pak": the engine's burst rate is not shown twice. */ private fun stripSpeed(s: String): String = s.replace(Regex("\\s+\\S+ [KMG]B/s\\s*$"), "").trimEnd() private fun enter(key: String, stage: DownloadStage, detail: String) { @@ -61,16 +31,10 @@ object DownloadQueue { val moved = item.entry.stage != stage || item.entry.state != DownloadState.RUNNING transitionLocked(item, stage) item.entry = item.entry.copy(detail = detail.ifEmpty { item.entry.detail }, state = DownloadState.RUNNING) - // A new stage shows at once; a new line within the same stage (one per file) waits its turn. publishLocked(progressOnly = !moved) } } - /** - * Moves an item to [stage] when it is elsewhere: the old stage is marked passed, the new one - * counts from zero, and the measured speed is cleared - the next stage writes or checks, it - * does not fetch, and a stale rate there would read as a download. Caller holds [lock]. - */ private fun transitionLocked(item: Item, stage: DownloadStage) { val e = item.entry if (e.stage == stage) return @@ -81,7 +45,6 @@ object DownloadQueue { ) } - /** How far back the shown speed looks: a few seconds, so the figure settles instead of following each file. */ private const val SPEED_WINDOW_MS = 3000.0 private fun stageCount(key: String, done: Long, total: Long, files: Int, filesTotal: Int, bytes: Boolean) { @@ -96,13 +59,10 @@ object DownloadQueue { } } - /** One sample of the rate behind [done] (bytes so far in this stage), smoothed over [SPEED_WINDOW_MS]. Caller holds [lock]. */ private fun sample(item: Item, done: Long): Long { val now = System.currentTimeMillis() if (item.speedAt == 0L) { item.speedAt = now; item.speedBytes = done; return item.speedEwma.toLong() } val dt = now - item.speedAt - // Sampled no faster than every quarter second, as an exponential average over - // SPEED_WINDOW_MS: one big file landing moves it, it does not define it. if (dt >= 250) { val inst = (done - item.speedBytes).coerceAtLeast(0) * 1000.0 / dt val alpha = 1.0 - Math.exp(-dt / SPEED_WINDOW_MS) @@ -129,7 +89,6 @@ object DownloadQueue { } private class Item(var entry: DownloadEntry, val factory: () -> DownloadJob, var job: DownloadJob? = null, val cancelled: AtomicBoolean = AtomicBoolean(false), var pauseRequested: Boolean = false) { - /** The shown speed's state: the last sample's clock and bytes, and the running average (bytes/s). */ var speedAt = 0L var speedBytes = 0L var speedEwma = 0.0 @@ -137,16 +96,13 @@ object DownloadQueue { private val lock = Any() private val items = LinkedHashMap<String, Item>() - /** Keys a restart brings back paused: queued as paused, not started. */ private val heldPaused = HashSet<String>() - /** [key]'s next [enqueue] comes in paused (a paused download restored after a restart). */ fun holdPaused(key: String) = synchronized(lock) { heldPaused.add(key) } private var running = 0 fun parallel(context: Context): Int = SessionPrefs.gameStoresParallel(context) - /** Threads for a store's download pool: background priority, so the UI and its render thread come first. */ @JvmStatic fun workerFactory(name: String): java.util.concurrent.ThreadFactory { val n = java.util.concurrent.atomic.AtomicInteger() @@ -167,10 +123,6 @@ object DownloadQueue { fun isActive(key: String): Boolean = entry(key)?.isActive == true - /** - * Queues a download, or starts it when a slot is free. A key already active is left alone; a - * finished or failed row with the same key is replaced. - */ fun enqueue(context: Context, entry: DownloadEntry, factory: () -> DownloadJob) { val app = context.applicationContext synchronized(lock) { @@ -230,20 +182,14 @@ object DownloadQueue { item.cancelled.set(true) item.job?.cancel(deleteFiles = true) } else { - // Never started: nothing is on disk, but the job knows where it would have gone. Thread({ runCatching { item.factory().cancel(deleteFiles = true) } }, "store-dl-cancel").start() } StoresState.logLine("cancelled \"${item.entry.name}\"") StoreDownloadService.finish(app, key) advance(app) - // The row says Cancelled for a moment, then goes; the game reads Install again. Thread({ Thread.sleep(3000); synchronized(lock) { if (items[key]?.entry?.state == DownloadState.CANCELLED) { items.remove(key); publishLocked() } } }, "store-dl-cancelled").start() } - /** - * Clear on a failed or paused row: deletes what it kept exactly as Cancel does (the job's own - * cleanup), then drops the row. An installed or cancelled row is only dropped. - */ fun clear(context: Context, key: String) { val item = synchronized(lock) { items[key] } ?: return if (item.entry.state == DownloadState.RUNNING || item.entry.state == DownloadState.QUEUED) return @@ -254,7 +200,6 @@ object DownloadQueue { StoreDownloadService.finish(context.applicationContext, key) } - /** Drops every finished, failed or cancelled row; files a failed one kept stay (its game page's Clear removes them). */ fun dismissFinished() { synchronized(lock) { items.entries.removeAll { !it.value.entry.isActive } @@ -262,7 +207,6 @@ object DownloadQueue { } } - /** Drops a finished, failed or cancelled row from the list. */ fun dismiss(key: String) { synchronized(lock) { val item = items[key] ?: return @@ -287,18 +231,11 @@ object DownloadQueue { private fun publish(context: Context) { synchronized(lock) { publishLocked() }; StoreDownloadService.sync(context) } - /** At most this often a progress-only change reaches the UI; a state or stage change goes at once. */ private const val PROGRESS_PUBLISH_MS = 250L private var lastPublishAt = 0L private var publishPending = false private val main = android.os.Handler(android.os.Looper.getMainLooper()) - /** - * Newest first: what was started last sits on top, as the preview lists them. Caller holds - * [lock]. Engines report per chunk or per file - hundreds of times a second at full speed - so - * [progressOnly] changes are coalesced to [PROGRESS_PUBLISH_MS]; the UI then recomposes a few - * times a second, not on every callback. - */ private fun publishLocked(progressOnly: Boolean = false) { val now = android.os.SystemClock.uptimeMillis() if (progressOnly) { @@ -312,13 +249,11 @@ object DownloadQueue { } } lastPublishAt = now - // A state change (not progress) is written down, so the queue outlives the app. if (!progressOnly) SavedQueue.save(items.values.map { it.entry }) val list = items.values.map { it.entry }.sortedByDescending { it.startedAt } StoresState.post { StoresState.publishDownloads(list) } } - /** Starts queued items while slots are free. */ private fun advance(context: Context) { while (true) { val next: Item @@ -336,8 +271,6 @@ object DownloadQueue { } private fun runItem(context: Context, item: Item) { - // Downloads run beside the UI, never ahead of it: background priority for this thread and - // (through [workerFactory]) every pool a store's manager starts. android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND) val handle = JobHandle(item.entry.key, item.cancelled) var result: String? = null @@ -367,7 +300,6 @@ object DownloadQueue { when (item.entry.state) { DownloadState.INSTALLED -> { StoresState.logLine("installed \"${item.entry.name}\" → ${result}") - // The folder is on disk and registered: the Installed tab and the Games list follow. StoresState.notifyLibraryChanged(context) val games = runCatching { com.droiddeck.launcher.frontend.Library.launchableGames(context).firstOrNull { it.gameFiles?.absolutePath == result }?.let { "app:${it.appId}" } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/SavedQueue.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/SavedQueue.kt index 310e66b0d..0c9fd0ccb 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/SavedQueue.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/SavedQueue.kt @@ -10,14 +10,6 @@ import org.json.JSONObject import java.io.File import java.util.concurrent.Executors -/** - * The download queue kept on disk, so what was queued, running or paused comes back after the app - * closes or is killed: the queue itself lives in memory, and a queued game that never started has - * nothing on disk to find. Each install records the game and where it goes ([remember]); every - * change of state writes the active rows in queue order ([save]); the first refresh after the app - * starts asks the stores for them again ([restore]) - running and queued ones carry on, paused ones - * come back paused. - */ object SavedQueue { private const val TAG = "StoreDownloads" private const val FILE = "store-queue.json" @@ -31,14 +23,12 @@ object SavedQueue { private fun key(item: CatalogItem) = "${item.store.id}:${item.id}" - /** An install is asked for: what to ask the store for again after a restart. */ @Synchronized fun remember(context: Context, item: CatalogItem, root: File?) { app = context.applicationContext wanted[key(item)] = Wanted(item, root) } - /** The active rows, in queue order, as they are now. Any thread; written off it. */ fun save(entries: List<DownloadEntry>) { val context = app ?: return val rows = synchronized(this) { @@ -60,11 +50,9 @@ object SavedQueue { } } - /** Once per process: queues again what was in the queue when the app last closed. */ fun restore(context: Context) { synchronized(this) { if (restored) return; restored = true } val app = context.applicationContext - // Read before anything may write it: an install started at once must not save over it. val rows = runCatching { JSONArray(File(app.filesDir, FILE).readText()) }.getOrNull() this.app = app rows ?: return diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt index e8c71f248..791dd06b1 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt @@ -23,21 +23,10 @@ import com.droiddeck.launcher.stores.formatBytes import com.droiddeck.launcher.stores.formatSpeed import java.util.concurrent.ConcurrentHashMap -/** - * Keeps store downloads going with the screen off or the app in the background: a foreground - * service (dataSync) with a notification showing the active download's progress, Pause / Resume - * and a two-step Cancel; a Wi-Fi lock and a partial wake lock while anything is running, so the - * radio and the CPU do not sleep under a multi-gigabyte install; and a notification when a game - * has installed or failed. - * - * [start] is called when a download begins and [finish] when one ends; the service stops itself when - * nothing is running. - */ class StoreDownloadService : Service() { private var wifi: WifiManager.WifiLock? = null private var wake: PowerManager.WakeLock? = null private var wakeAt = 0L - /** The download whose Cancel was tapped once: its actions read Delete / Keep until [confirmUntil]. */ private var confirmKey: String? = null private var confirmUntil = 0L private val main = Handler(Looper.getMainLooper()) @@ -55,7 +44,6 @@ class StoreDownloadService : Service() { when (intent?.action) { ACTION_PAUSE -> key?.let { DownloadQueue.pause(it) - // The progress notification goes with the last running download; this one stays to resume from. pausedNote(this, it) } ACTION_RESUME -> key?.let { @@ -126,11 +114,6 @@ class StoreDownloadService : Service() { runCatching { (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).notify(ID, build()) } } - /** - * While a download runs: a Wi-Fi lock (high-performance before Android 14, low-latency from it, - * where the old mode is deprecated) and a partial wake lock, taken with a timeout and renewed - * as progress comes in rather than held indefinitely. Released as soon as nothing runs. - */ @SuppressLint("WakelockTimeout") private fun locks() { val running = StoresState.latestDownloads.any { it.state == DownloadState.RUNNING } @@ -188,7 +171,6 @@ class StoreDownloadService : Service() { private const val ACTION_CANCEL = "droiddeck.stores.CANCEL" private const val ACTION_DELETE = "droiddeck.stores.DELETE" private const val ACTION_KEEP = "droiddeck.stores.KEEP" - /** MainActivity: which page to open ("stores:downloads", or a Games selection such as "app:<id>"). */ const val EXTRA_NAV = "droiddeck.nav" private val active = ConcurrentHashMap.newKeySet<String>() @Volatile private var instance: StoreDownloadService? = null @@ -212,7 +194,6 @@ class StoreDownloadService : Service() { private fun openDownloads(context: Context, code: Int) = openPage(context, "stores:downloads", code) - /** A download started: the service is up (idempotent). */ fun start(context: Context) { StoresState.latestDownloads.filter { it.state == DownloadState.RUNNING }.forEach { active.add(it.key) } try { @@ -224,17 +205,14 @@ class StoreDownloadService : Service() { } } - /** Progress moved: redraw the notification (StoresState calls it at most once a second). */ fun tick() { instance?.refresh() } - /** The queue changed: redraw the notification. */ fun sync(context: Context) { instance?.refresh() } - /** A download ended: drop it; the service stops with the last one. */ fun finish(context: Context, key: String) { active.remove(key) val inst = instance ?: return @@ -243,7 +221,6 @@ class StoreDownloadService : Service() { private fun pausedId(key: String) = ID + 0x20000 + (key.hashCode() and 0xFFFF) - /** "<game> paused" with Resume, once the progress notification has gone with the service. */ private fun pausedNote(context: Context, key: String) { val entry = StoresState.latestDownloads.firstOrNull { it.key == key } ?: return val i = Intent(context, StoreDownloadService::class.java).setAction(ACTION_RESUME).putExtra(EXTRA_KEY, key) @@ -258,7 +235,6 @@ class StoreDownloadService : Service() { runCatching { context.getSystemService(NotificationManager::class.java)?.notify(pausedId(key), n) } } - /** "<game> installed" (opens it in Games) or "<game> download failed" (opens Downloads); dismissible. */ fun ended(context: Context, entry: DownloadEntry, installed: Boolean, gamesKey: String?) { val app = context.applicationContext channels(app) @@ -274,7 +250,6 @@ class StoreDownloadService : Service() { } } -/** The notification's one line for a download. Pure, for the test. */ object DownloadNotificationText { fun line(d: DownloadEntry, stageName: String, paused: String, queued: String): String { val parts = ArrayList<String>() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadTier.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadTier.kt index 3b12de899..765ec8a3d 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadTier.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadTier.kt @@ -5,21 +5,11 @@ import androidx.annotation.StringRes import com.droiddeck.launcher.R import com.droiddeck.launcher.session.SessionPrefs -/** - * How hard the native download engine may pull: the ceiling of chunk requests it keeps in flight, - * and the inflate / hash / write threads behind them. Three tiers, one app-wide setting (Setup › - * Stores and the Downloads page), read when a download starts so a change applies to the next one. - * - * The windows are the ones the engine was tuned with (16 / 32 / 96 requests): it ramps towards the - * ceiling only while throughput keeps rising, so a slow link settles well below any of them. - */ enum class StoreDownloadTier(val id: String, @StringRes val label: Int, val networkWindow: Int, val decompressPerCore: Double) { BALANCED("balanced", R.string.stores_tier_balanced, 16, 0.4), FAST("fast", R.string.stores_tier_fast, 32, 0.5), MAX("max", R.string.stores_tier_max, 96, 0.8); - /** Inflate / hash / write workers for this device, at least two so a fetch never waits on a single writer. */ - /** Inflate / hash / write threads: by the tier, but two cores always left for the UI (at least 2 workers). */ val processWorkers: Int get() { val cores = Runtime.getRuntime().availableProcessors() return (cores * decompressPerCore).toInt().coerceIn(2, maxOf(2, minOf(32, cores - 2))) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAchievements.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAchievements.kt index 1c7e6682b..7d278e04d 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAchievements.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAchievements.kt @@ -6,7 +6,6 @@ import com.droiddeck.launcher.stores.StoreAchievements import com.droiddeck.launcher.stores.StoreNet import org.json.JSONObject -/** Epic's GraphQL: the sandbox's achievements, then the player's records in it. */ object EpicAchievements : StoreAchievements(Store.EPIC.id, Store.EPIC.shortLabel) { private const val GRAPHQL = "https://launcher.store.epicgames.com/graphql" private const val USER_AGENT = "EpicGamesLauncher/14.0.8-22004686+++Portal+Release-Live" @@ -44,7 +43,6 @@ object EpicAchievements : StoreAchievements(Store.EPIC.id, Store.EPIC.shortLabel return Result.Found(out) } - /** Achievement name to unlock time, for the ones the player has. */ private fun unlockTimes(token: String, account: String, namespace: String): Map<String, Long> { val out = HashMap<String, Long>() val records = query(token, PLAYER_ACHIEVEMENTS, JSONObject().put("epicAccountId", account).put("sandboxId", namespace)) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicApiClient.java b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicApiClient.java index 44921af76..9ae321671 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicApiClient.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicApiClient.java @@ -10,13 +10,6 @@ import org.json.JSONObject; import java.util.ArrayList; import java.util.List; -/** - * Epic's library, catalog and launcher-assets services, as the launcher calls them: - * library-service.live.use1a.on.epicgames.com/library/api/public/items (owned items, paginated) - * catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/namespace/{ns}/bulk/items - * launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/platform/Windows/... - * All with a bearer token. - */ public final class EpicApiClient { private static final String TAG = "EpicApi"; @@ -27,7 +20,6 @@ public final class EpicApiClient { private EpicApiClient() {} - /** The owned games, every page; Unreal assets, private sandboxes and non-Windows items left out. */ public static List<EpicGame> getLibraryItems(String accessToken) { List<EpicGame> result = new ArrayList<>(); String cursor = null; @@ -72,7 +64,6 @@ public final class EpicApiClient { return result; } - /** Fills title, developer, art, description and the DLC flag from the catalog; true on success. */ public static boolean enrichFromCatalog(String accessToken, EpicGame game) { if (game.namespace.isEmpty() || game.catalogItemId.isEmpty()) return false; try { @@ -118,10 +109,6 @@ public final class EpicApiClient { } } - /** - * The manifest API answer for a game, flattened to {"manifests": [...], "versionId": "..."} as - * the download manager reads it. {@code appName} is the library record's, not the catalog's. - */ public static String getManifestApiJson(String accessToken, String namespace, String catalogItemId, String appName) { try { String url = MANIFEST_BASE + "/" + namespace + "/catalogItem/" + catalogItemId + "/app/" + appName + "/label/Live"; @@ -136,7 +123,6 @@ public final class EpicApiClient { String ver = first.optString("buildVersion", ""); if (ver.isEmpty()) ver = first.optString("versionId", ""); wrapper.put("versionId", ver); - // The sidecar's EOS deployment id, when the app has one. JSONObject sidecar = first.optJSONObject("sidecar"); if (sidecar != null) { String config = sidecar.optString("config", ""); @@ -151,12 +137,10 @@ public final class EpicApiClient { } } - /** `customAttributes.AdditionalCommandLine.value` for the game, "" when it has none, null when the call failed. */ public static String getAdditionalCommandLine(String accessToken, String namespace, String catalogItemId) { return getCustomAttribute(accessToken, namespace, catalogItemId, "AdditionalCommandLine"); } - /** `customAttributes.<name>.value` for the game (CloudSaveFolder, AdditionalCommandLine), "" when it has none, null when the call failed. */ public static String getCustomAttribute(String accessToken, String namespace, String catalogItemId, String name) { try { String url = CATALOG_BASE + "/" + namespace + "/bulk/items?id=" + catalogItemId + "&country=US"; diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAuthClient.java b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAuthClient.java index 959d4302b..2303d06d1 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAuthClient.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAuthClient.java @@ -16,14 +16,6 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; -/** - * Epic's OAuth2 token endpoint, with the launcher's public client credentials (the ones every - * third-party Epic client uses): - * POST account-public-service-prod03.ol.epicgames.com/account/api/oauth/token - * Basic base64(clientId:clientSecret), form grant_type=authorization_code|refresh_token, token_type=eg1 - * GET .../account/api/oauth/exchange with a bearer token: a short-lived exchange code for a launch. - * Error bodies are never logged: they carry correlation ids and account context. - */ public final class EpicAuthClient { private static final String TAG = "EpicAuth"; @@ -91,11 +83,9 @@ public final class EpicAuthClient { } } - /** A short-lived exchange code for a game launch (-AUTH_PASSWORD); null on any failure. */ public static String getExchangeCode(String accessToken) { try { String resp = getRequest(EXCHANGE_URL, accessToken); - // getRequest has logged the HTTP status; the code itself is never logged. if (resp == null) { Log.i(TAG, "exchange code: no answer"); return null; } return new JSONObject(resp).optString("code", null); } catch (Exception e) { @@ -104,7 +94,6 @@ public final class EpicAuthClient { } } - /** Authorized GET; the body or null. */ public static String getRequest(String urlStr, String accessToken) { HttpURLConnection conn = null; try { @@ -127,7 +116,6 @@ public final class EpicAuthClient { } } - /** Authorized GET of raw bytes (the manifest binary). */ public static byte[] getBytes(String urlStr, String accessToken) { HttpURLConnection conn = null; try { @@ -166,7 +154,6 @@ public final class EpicAuthClient { return sb.toString(); } - /** "2026-03-29T12:00:00.000Z" to epoch millis, without java.time. */ private static long parseIso8601(String s) { int year = Integer.parseInt(s.substring(0, 4)); int month = Integer.parseInt(s.substring(5, 7)); diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt index 97c3cd3a9..e631cf78b 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt @@ -24,7 +24,6 @@ import org.json.JSONObject import java.io.File import java.util.concurrent.atomic.AtomicBoolean -/** Epic Games in the Stores section: sign-in, the library, the public store, installs and the per-launch sign-in code. */ object EpicBackend : StoreBackend, EpicLaunchSupport { private const val TAG = "EpicBackend" override val store: Store = Store.EPIC @@ -42,7 +41,6 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { override fun syncLibrary(context: Context, force: Boolean) { val app = context.applicationContext Thread({ - // The cache off the main thread; with it on screen a background sync keeps quiet. val cached = EpicLibrary.cached(app).map { EpicLibrary.toCatalogItem(app, it) } val quiet = cached.isNotEmpty() && !force StoresState.post { @@ -74,13 +72,11 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { }, "epic-shelves").start() } - /** Store offers whose catalog item is in the library are owned: their cards offer Install under the library's id. */ private fun markOwned(s: StoreShelves): StoreShelves { val owned = StoresState.library[store] ?: return s val byCatalogItem = owned.associateBy { it.extra["catalogItemId"] ?: "" } fun mark(l: List<CatalogItem>) = l.map { i -> val match = i.extra["items"]?.split(',')?.firstNotNullOfOrNull { byCatalogItem[it] } - // The offer's art, and its facts where the library's catalog record has none. if (match != null) match.copy( imageUrl = i.imageUrl ?: match.imageUrl, tallImageUrl = i.tallImageUrl ?: match.tallImageUrl, developer = match.developer.ifBlank { i.developer }, publisher = match.publisher.ifBlank { i.publisher }, @@ -102,21 +98,15 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { override fun uninstall(context: Context, game: InstalledStoreGame) {} - /** A fresh exchange code for the signed-in account; null when signed out or offline. Blocking. */ override fun exchangeCode(context: Context): String? { val token = EpicCredentialStore.getValidAccessToken(context) ?: return null return EpicAuthClient.getExchangeCode(token) } - /** The access token is past its time and the refresh just failed: only a new sign-in helps. */ override fun signInExpired(context: Context): Boolean = EpicCredentialStore.expired(context) private class InstallJob(val app: Context, val item: CatalogItem, val namespace: String, val catalogItemId: String, val folder: File) : DownloadQueue.DownloadJob { private val cancelled = AtomicBoolean(false) - // On a card the in-flight chunks go to the app's cache instead: the card's write rate paces - // the whole install, and chunks are read back once at assembly. Internal installs keep the - // cache beside the game. A resume keeps whichever cache the stopped run filled: a - // `.chunks` already beside the game is reused, not fetched again into the scratch folder. private val scratch: File? = if (StoreInstallRoot.isRemovable(app, folder) && !hasChunks(File(folder, ".chunks"))) StoreInstallRoot.scratchDir(app, Store.EPIC, item.id) else null private fun hasChunks(dir: File): Boolean = dir.isDirectory && (dir.list()?.isNotEmpty() == true) @@ -161,7 +151,6 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { Store.EPIC, item.id, item.title, exe = exe.ifEmpty { "game.exe" }, args = args, installVersion = result.buildVersion, installedAt = System.currentTimeMillis(), cover = item.tallImageUrl ?: item.imageUrl, hero = item.imageUrl, - // Both kept even when empty: the manifest was read, and names no prerequisite. extra = EpicLaunchData.extras(item.id, namespace, catalogItemId, deploymentId) + mapOf( com.droiddeck.launcher.frontend.WinCompSources.EPIC_PREREQ_PATH to result.prereqPath, com.droiddeck.launcher.frontend.WinCompSources.EPIC_PREREQ_NAME to result.prereqName, @@ -177,10 +166,6 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { } } - /** - * Epic's web sign-in: the login page redirects to a JSON page carrying an authorization code, - * read out of the document and exchanged for tokens. - */ private object LoginFlow : StoreLoginActivity.Flow { private const val REDIRECT = "https://www.epicgames.com/id/api/redirect" override val startUrl: String = "https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fredirect%3FclientId%3D${EpicAuthClient.CLIENT_ID}%26responseType%3Dcode" @@ -189,7 +174,6 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { override fun isRedirect(url: String): Boolean = url.startsWith(REDIRECT) override fun finish(activity: StoreLoginActivity, view: WebView, url: String): String? { - // The document's text comes JSON-quoted from evaluateJavascript. var text = url if (text.startsWith("\"")) text = text.substring(1, text.length - 1) text = text.replace("\\n", "").replace("\\r", "").replace("\\\"", "\"") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicCredentialStore.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicCredentialStore.kt index abf545ae1..58237eed9 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicCredentialStore.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicCredentialStore.kt @@ -6,11 +6,6 @@ import com.droiddeck.launcher.stores.Store import com.droiddeck.launcher.stores.StoreAccounts import org.json.JSONObject -/** - * The Epic sign-in in StoreAccounts' private file: access and refresh tokens, the account id and - * display name (the launch arguments carry both), the expiry. Refreshed when within five minutes - * of expiring. - */ object EpicCredentialStore { private const val TAG = "EpicAuth" @@ -34,9 +29,7 @@ object EpicCredentialStore { @JvmStatic fun isLoggedIn(context: Context): Boolean = load(context) != null - /** A valid access token, refreshed when within five minutes of expiry; null when signed out or the refresh failed. */ @JvmStatic - /** The stored access token is past its expiry (a refresh, if one was tried, did not replace it). */ fun expired(context: Context): Boolean { val creds = load(context) ?: return false return creds.expiresAt in 1 until System.currentTimeMillis() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java index ad78a04be..958e9b158 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java @@ -37,16 +37,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.zip.Inflater; -/** - * The Epic install pipeline, ported from Bannerlator's EpicDownloadManager: the manifest API's CDN - * list, the manifest binary (ChunksV4, or the older JSON form), the files narrowed to the base - * set plus the device's language, a delta pass that skips files already on disk with the right - * size and SHA-1, the chunk fetch into `.chunks/<GUID>` (the native engine when it is there, the - * Java pool otherwise), and the assembly of files from those chunks. - * - * Chunk URLs carry no auth tokens; the subfolder is the decimal group number; the Cloudflare - * mirror is skipped (it refuses chunks). Blocking; the download queue runs it on its own thread. - */ public final class EpicDownloadManager { private static final String TAG = "EpicDownload"; @@ -56,14 +46,7 @@ public final class EpicDownloadManager { void onProgress(String message, int pct); default void onBytes(long done, long total, long speedBps) {} default void onLog(String line) {} - /** - * The active stage's own count: {@code stage} is "check" (the files already there, before - * anything is fetched), "verify" or "install" (bytes written), {@code done} of - * {@code total} an amount (bytes, or items when there are no bytes), {@code items} of - * {@code itemsTotal} the files, when it counts files. - */ default void onStage(String stage, long done, long total, int items, int itemsTotal) {} - /** What this run fetches (compressed) and what the game takes on disk. */ default void onSizes(long downloadBytes, long diskBytes) {} } @@ -99,7 +82,6 @@ public final class EpicDownloadManager { public long fileSize() { long t = 0; for (ChunkPart p : parts) t += (p.size & 0xFFFFFFFFL); return t; } } - /** The parsed manifest: its chunks, files, and from the meta block the exe the launcher starts. */ public static final class Manifest { public String chunkDir = "ChunksV4"; public List<ChunkInfo> uniqueChunks = new ArrayList<>(); @@ -107,20 +89,16 @@ public final class EpicDownloadManager { public String launchExe = ""; public String launchCommand = ""; public String buildVersion = ""; - /** The prerequisite the launcher runs first (UE4PrereqSetup_x64.exe...): what it is called and its path in the install. */ public String prereqName = ""; public String prereqPath = ""; - /** The prerequisite fields were read (an empty name and path then mean the build names none). */ public boolean prereqRead = false; public List<CdnUrl> cdnUrls = new ArrayList<>(); } - /** What an install leaves for the sidecar. */ public static final class Result { public final String launchExe; public final String buildVersion; public final long bytes; - /** The manifest's prerequisite, for Windows components; "" when it names none. */ public String prereqName = ""; public String prereqPath = ""; Result(String launchExe, String buildVersion, long bytes) { this.launchExe = launchExe; this.buildVersion = buildVersion; this.bytes = bytes; } @@ -132,12 +110,6 @@ public final class EpicDownloadManager { InstallException(String message) { super(message); } } - /** - * Installs the app the manifest API JSON describes into {@code installDir}. {@code installTags} - * null = every file. {@code chunkCacheDirPath} "" keeps the chunk cache beside the game - * ({@code <installDir>/.chunks}); a path puts it there instead - the app's cache when the game - * goes to a card. Returns null when cancelled; throws when it fails. - */ public static Result install(Context ctx, String manifestApiJson, String installDirPath, String chunkCacheDirPath, List<String> installTags, AtomicBoolean cancel, Callback cb) throws InstallException { try { if (cancel.get()) return null; @@ -157,8 +129,6 @@ public final class EpicDownloadManager { File installDir = new File(installDirPath); installDir.mkdirs(); - // "" = the cache beside the game; a path = the scratch cache (the app's own storage when - // the game goes to a card). The engine and the Java loops read the same value. final String cachePath = chunkCacheDirPath == null ? "" : chunkCacheDirPath; final File chunkCacheDir = cachePath.isEmpty() ? new File(installDir, ".chunks") : new File(cachePath); chunkCacheDir.mkdirs(); @@ -168,7 +138,6 @@ public final class EpicDownloadManager { long installBytes = 0; for (FileInfo f : selected) installBytes += f.fileSize(); final long planned = installBytes; - // Free space is checked after the delta pass, against what is still missing (below). cb.onProgress("Checking files…", 0); List<FileInfo> pending = new ArrayList<>(selected.size()); @@ -191,10 +160,6 @@ public final class EpicDownloadManager { return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest); } List<ChunkInfo> needed = uniqueChunksForFiles(manifest, pending); - // The cache holds whole chunk windows (~1 MiB each), and a window is shared with files - // this device does not install (other tags, other builds' data): the cache is often - // larger than the game itself (Metalstorm: 9.1 GB of chunks for 4.4 GB of files). Both - // have to fit at once until assembly, so the check counts both, on their own volumes. long cacheMissing = 0, pendingBytes = 0; for (ChunkInfo c : needed) if (!new File(chunkCacheDir, c.guidStr()).isFile()) cacheMissing += Math.max(c.windowSize, 1); for (FileInfo f : pending) pendingBytes += f.fileSize(); @@ -267,8 +232,6 @@ public final class EpicDownloadManager { if (cancel.get()) return null; if (failCount.get() > 0) throw new InstallException(failCount.get() + " chunk(s) failed to download"); - // The engine's assembler when it starts (it drops each chunk after its last use and the - // cache at the end); the loop below when it does not. if (engine) { NativeOutcome r = runNativeAssembly(manifestBytes, pendingIdx, pending.size(), installDirPath, cachePath, cancel, cb); if (r.started) { @@ -314,7 +277,6 @@ public final class EpicDownloadManager { private static final class NativeOutcome { boolean started, success, cancelled; String error = ""; } - /** The pending files as indices into the manifest's file list, what both native runs take; null when one is not in it. */ private static int[] pendingIndices(Manifest manifest, List<FileInfo> pending) { java.util.IdentityHashMap<FileInfo, Integer> index = new java.util.IdentityHashMap<>(); for (int i = 0; i < manifest.files.size(); i++) index.put(manifest.files.get(i), i); @@ -327,7 +289,6 @@ public final class EpicDownloadManager { return pendingIdx; } - /** The native chunk pool on the same counters; never throws. */ private static NativeOutcome runNativePool(Context ctx, byte[] manifestBytes, int[] pendingIdx, List<ChunkInfo> needed, long totalBytes, String installDirPath, String cachePath, List<CdnUrl> cdnUrls, AtomicBoolean cancel, Callback cb, AtomicLong completedBytes, AtomicInteger completedCount, AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps) { @@ -361,7 +322,6 @@ public final class EpicDownloadManager { return r; } - /** The native assembler: the pending files written from the cache, the cache removed on success; never throws. */ private static NativeOutcome runNativeAssembly(byte[] manifestBytes, int[] pendingIdx, int totalFiles, String installDirPath, String cachePath, AtomicBoolean cancel, Callback cb) { NativeOutcome r = new NativeOutcome(); try { @@ -383,7 +343,6 @@ public final class EpicDownloadManager { return r; } - // ---- CDN list and manifest download --------------------------------------------------------- public static List<CdnUrl> parseCdnUrls(String json) { List<CdnUrl> result = new ArrayList<>(); @@ -419,7 +378,6 @@ public final class EpicDownloadManager { return result; } - /** The manifest binary from the first CDN that serves it; the auth query goes on this URL only. */ public static byte[] downloadManifest(String json, List<CdnUrl> cdnUrls) { try { JSONArray manifests = new JSONObject(json).optJSONArray("manifests"); @@ -439,7 +397,6 @@ public final class EpicDownloadManager { return null; } - // ---- manifest parsing ----------------------------------------------------------------------- public static Manifest parseManifest(byte[] bytes) { try { @@ -468,24 +425,22 @@ public final class EpicDownloadManager { ByteBuffer b = ByteBuffer.wrap(body).order(ByteOrder.LITTLE_ENDIAN); Manifest m = new Manifest(); m.chunkDir = chunkDir; - // ManifestMeta: the launch exe and build version live here; the rest is skipped by size. int metaStart = b.position(); int metaSize = b.getInt(); try { - b.get(); // data version - b.getInt(); // feature level - b.get(); // isFileData - b.getInt(); // app id - readFString(b); // app name + b.get(); + b.getInt(); + b.get(); + b.getInt(); + readFString(b); m.buildVersion = readFString(b); m.launchExe = readFString(b).replace('\\', '/'); m.launchCommand = readFString(b); - // PrereqIds (an array), PrereqName, PrereqPath, PrereqArgs. int prereqIds = b.getInt(); for (int i = 0; i < prereqIds && i < 64; i++) readFString(b); m.prereqName = readFString(b); m.prereqPath = readFString(b).replace('\\', '/'); - readFString(b); // PrereqArgs + readFString(b); m.prereqRead = true; } catch (Exception e) { Log.w(TAG, "manifest meta: " + e.getClass().getSimpleName()); @@ -603,11 +558,6 @@ public final class EpicDownloadManager { } } - /** - * A JSON manifest writes every number as a "blob": each byte as three decimal digits, least - * significant first ("013000000000" = 13), as Legendary's blob_to_num reads it. Bits past 64 - * are dropped; a group that is not digits throws. - */ static long blobToNum(String blob) { long num = 0; for (int i = 0, shift = 0; i < blob.length(); i += 3, shift += 8) { @@ -637,7 +587,6 @@ public final class EpicDownloadManager { return new String(bytes, StandardCharsets.US_ASCII); } - // ---- chunk fetch (the built-in pool) -------------------------------------------------------- public static byte[] downloadBytes(String urlStr) { HttpURLConnection conn = null; @@ -656,7 +605,6 @@ public final class EpicDownloadManager { } } - /** One chunk streamed from the first CDN that serves it, SHA-1 checked, published by rename. */ private static boolean downloadChunkStreaming(ChunkInfo chunk, String chunkDir, List<CdnUrl> cdnUrls, File outFile) { String chunkPath = chunk.getPath(chunkDir); File tmp = new File(outFile.getPath() + ".part"); @@ -743,7 +691,6 @@ public final class EpicDownloadManager { while (remaining > 0) { int n = in.read(skip, 0, Math.min(skip.length, remaining)); if (n < 0) throw new IOException("stream ended"); remaining -= n; } } - // ---- selection and verification ------------------------------------------------------------- public static List<FileInfo> getRequiredInstallFiles(Manifest manifest) { List<FileInfo> required = new ArrayList<>(); @@ -751,7 +698,6 @@ public final class EpicDownloadManager { return required.isEmpty() ? new ArrayList<>(manifest.files) : required; } - /** Base files plus any file carrying one of the tags; null tags = every file; nothing matched = base only. */ public static List<FileInfo> resolveInstallFiles(Manifest manifest, List<String> selectedTags) { if (selectedTags == null) return new ArrayList<>(manifest.files); if (selectedTags.isEmpty()) return getRequiredInstallFiles(manifest); @@ -790,7 +736,6 @@ public final class EpicDownloadManager { } } - /** The manifest's prerequisite as {name, path}, from the manifest alone; null on failure. Blocking. */ public static String[] fetchPrereq(String manifestApiJson) { try { List<CdnUrl> cdnUrls = parseCdnUrls(manifestApiJson); @@ -806,7 +751,6 @@ public final class EpicDownloadManager { } } - /** The install size for the tags (null = every file), from the manifest alone; -1 on failure. Blocking. */ public static long fetchInstallSizeBytes(String manifestApiJson, List<String> installTags) { try { List<CdnUrl> cdnUrls = parseCdnUrls(manifestApiJson); @@ -823,7 +767,6 @@ public final class EpicDownloadManager { } } - // ---- helpers -------------------------------------------------------------------------------- static byte[] readFile(File f) throws IOException { try (FileInputStream fis = new FileInputStream(f)) { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicGame.java b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicGame.java index ba91eb32a..43ba8069b 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicGame.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicGame.java @@ -1,10 +1,5 @@ package com.droiddeck.launcher.stores.epic; -/** - * One Epic Games library entry. {@code appName} is the launcher's app slug (the manifest URL and - * the launch arguments take it), {@code namespace} the product's sandbox, {@code catalogItemId} - * the catalog UUID; the three together address every Epic API call for the game. - */ public class EpicGame { public String appName = ""; public String namespace = ""; @@ -12,9 +7,7 @@ public class EpicGame { public String title = ""; public String developer = ""; public String description = ""; - /** DieselGameBoxTall: the tall portrait art. */ public String artCover = ""; - /** DieselGameBox or Thumbnail. */ public String artSquare = ""; public String version = ""; public long installSize = 0L; diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicInstallTags.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicInstallTags.kt index dcb178d50..9ad136a84 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicInstallTags.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicInstallTags.kt @@ -2,11 +2,6 @@ package com.droiddeck.launcher.stores.epic import java.util.Locale -/** - * Epic tags a manifest's files by language so a client installs the base files plus one language - * instead of every language. The table (the launcher's tag names per language) is Legendary's; - * the language is the device's locale, English when it is not one Epic ships. - */ object EpicInstallTags { private val LANGUAGE_TO_TAGS: Map<String, List<String>> = mapOf( "ar" to listOf("Arabic", "ar"), "bg" to listOf("Bulgarian", "bg-BG", "bg"), @@ -34,11 +29,9 @@ object EpicInstallTags { } } - /** The install tags for the device's language; English when Epic has no tags for it. Never empty. */ @JvmStatic fun tagsForDevice(): List<String> = LANGUAGE_TO_TAGS[key(Locale.getDefault()) ?: "en"] ?: LANGUAGE_TO_TAGS.getValue("en") - /** The two-letter code handed to the game as -epiclocale; "en" when the language is not one Epic knows. */ @JvmStatic fun localeCodeForDevice(): String = key(Locale.getDefault())?.substringBefore('-') ?: "en" } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLaunchData.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLaunchData.kt index 6119bda75..1452ef447 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLaunchData.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLaunchData.kt @@ -4,21 +4,9 @@ import android.content.Context import android.util.Log import com.droiddeck.launcher.stores.StoreGameSidecar -/** - * What an Epic game's launch line needs beyond the exe, built once at install time into the - * sidecar's arguments: the portal and identity arguments every EOS title expects - * (`-EpicPortal -epicusername -epicuserid -epicsandboxid -epiclocale -epicapp -epicenv=Prod`), the - * deployment id when the app has an EOS sidecar, and the extra flags Epic ships for the title - * (`AdditionalCommandLine`). The sign-in triple (`-AUTH_LOGIN/-AUTH_PASSWORD/-AUTH_TYPE`) is not - * here: the exchange code lives minutes, so the launcher .bat takes it from a file the app writes - * right before a launch (StoreLaunch.prepare). - * - * The EOS overlay is never provisioned: nothing points the game's EOS SDK at an overlay component. - */ object EpicLaunchData { private const val TAG = "EpicLaunch" - /** The static launch arguments for [appName] in [namespace]. Blocking: one catalog call for the extra flags. */ fun arguments(context: Context, token: String?, appName: String, namespace: String, catalogItemId: String, deploymentId: String): List<String> { val creds = EpicCredentialStore.load(context) val displayName = creds?.displayName?.takeIf { it.isNotEmpty() } ?: "EpicUser" @@ -42,13 +30,11 @@ object EpicLaunchData { return args } - /** The sidecar's identifiers the launcher reads back. */ fun extras(appName: String, namespace: String, catalogItemId: String, deploymentId: String): Map<String, String> = buildMap { put("appName", appName); put("namespace", namespace); put("catalogItemId", catalogItemId) if (deploymentId.isNotEmpty()) put("deploymentId", deploymentId) } - /** True when [sidecar] is an Epic install that can take a sign-in code. */ fun wantsExchangeCode(sidecar: StoreGameSidecar): Boolean = sidecar.store == com.droiddeck.launcher.stores.Store.EPIC private fun sanitize(s: String): String = s.replace("\n", "").replace("\r", "").replace("\u0000", "") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt index f9c0c04bd..f84fa1dec 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt @@ -10,12 +10,6 @@ import org.json.JSONArray import org.json.JSONObject import java.util.concurrent.atomic.AtomicBoolean -/** - * The signed-in account's Epic library: the library service's records, each enriched from the - * catalog (title, art, DLC flag), cached so the tab paints at once and re-synced when forced or - * 15 minutes stale. DLC entries are folded away under their base game. Ported from Bannerlator's - * EpicLibraryRepo. - */ object EpicLibrary { private const val TAG = "EpicLibrary" private const val CACHE_KEY = "library_cache" @@ -53,7 +47,6 @@ object EpicLibrary { }.getOrDefault(emptyList()) } - /** Remembers a game's install size once the manifest said it, for the Install button. */ fun rememberSize(context: Context, appName: String, bytes: Long) { if (bytes <= 0) return val p = EpicPrefs.get(context) @@ -115,7 +108,6 @@ object EpicLibrary { imageUrl = g.artSquare.ifBlank { null } ?: g.artCover.ifBlank { null }, tallImageUrl = g.artCover.ifBlank { null } ?: g.artSquare.ifBlank { null }, developer = g.developer, publisher = g.publisher, description = com.droiddeck.launcher.stores.cleanStoreText(g.description), owned = true, - // A cache from before the release date was kept still has it under the older per-game key. releaseDate = g.releaseDate.ifEmpty { EpicPrefs.get(context).getString("release_${g.appName}", "").orEmpty() }, sizeBytes = EpicPrefs.get(context).getLong("size_${g.appName}", g.installSize), storeUrl = "https://store.epicgames.com/en-US/browse?q=${java.net.URLEncoder.encode(g.title, "UTF-8")}", diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt index 59849fd60..28510a8f1 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt @@ -1,4 +1,3 @@ -// JNI symbols depend on this package path and class name (app/src/main/rust/stores/JNI.md). package com.droiddeck.launcher.stores.epic import android.util.Log @@ -8,35 +7,16 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference -/** - * The native half of an Epic install in `libdroiddeckstores.so`: [run] fills the chunk cache with - * verified, decompressed chunks for the files the manager says are pending, [assemble] writes those - * files from the cache. Everything around them (manifest, install tags, delta pass, post-install) - * stays in the manager. - * - * The cache is `<installDir>/.chunks` when `chunkCacheDir` is `""`, else the scratch folder given - - * the app's own cache when the game installs to a card, where the assembler drops each chunk right - * after its last use and the folder at the end. Both calls take the same value for one install. - * - * Both block like the Java loops they replace and poll [AtomicBoolean] `cancel` every 250 ms: on - * cancel they flip the native flag, wait up to 5 s for the run to wind down, then return. - */ object EpicNative { private const val TAG = "EpicNative" - /** Callbacks from a native run; every method is called on a native thread. One interface serves both runs. */ interface Listener { - /** Fetch only, once before any fetch: the plan the engine derived (the manager cross-checks it). */ fun onPlan(chunksTotal: Int, bytesTotal: Long, chunkDir: String) - /** Fetch: per accounted chunk (cached-skip or fetched), done/total = chunks. Assembly: per file written, done/total = files. */ fun onProgress(bytesDone: Long, bytesTotal: Long, done: Int, total: Int) - /** Engine log line (already in logcat under `EpicNative`). */ fun onLog(line: String) - /** Terminal. Fetch: bytes credited; assembly: bytes written. */ fun onComplete(success: Boolean, error: String, bytes: Long) } - /** Outcome of a run. `started == false`: nothing happened on disk, the caller runs its own loop. */ class Result( @JvmField val started: Boolean, @JvmField val success: Boolean, @JvmField val cancelled: Boolean, @JvmField val error: String, @JvmField val bytes: Long, @JvmField val done: Int, @JvmField val total: Int, @@ -44,7 +24,6 @@ object EpicNative { private class Completion(val success: Boolean, val error: String, val bytes: Long) - /** The fetch: chunks for [pendingFileIdx] into the cache. */ @JvmStatic fun run( manifest: ByteArray, installDir: String, chunkCacheDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedChunks: Int, expectedBytes: Long, @@ -53,7 +32,6 @@ object EpicNative { nativeStart(manifest, installDir, chunkCacheDir, cdnPrefixes, pendingFileIdx, expectedChunks, expectedBytes, caBundlePath, maxWorkers, processWorkers, inner) } - /** The assembly: the files [fileIdx] written from the cache, the cache removed on success. */ @JvmStatic fun assemble(manifest: ByteArray, installDir: String, chunkCacheDir: String, fileIdx: IntArray, cancel: AtomicBoolean?, listener: Listener): Result = drive("nativeAssemble", cancel, listener) { inner -> nativeAssemble(manifest, installDir, chunkCacheDir, fileIdx, inner) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicPrefs.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicPrefs.kt index ae2bfadb3..1510dbef9 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicPrefs.kt @@ -3,11 +3,6 @@ package com.droiddeck.launcher.stores.epic import android.content.Context import android.content.SharedPreferences -/** - * Epic's own records that are not credentials: the library cache, each game's release date, the - * cached deployment id and extra command line a launch needs. App-private preferences; the sign-in - * lives in StoreAccounts. Public: the Java clients read it too. - */ object EpicPrefs { @JvmStatic fun get(context: Context): SharedPreferences = context.applicationContext.getSharedPreferences("stores.epic", Context.MODE_PRIVATE) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicStoreCatalog.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicStoreCatalog.kt index 4e07c78aa..b7b6ac364 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicStoreCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicStoreCatalog.kt @@ -14,14 +14,6 @@ import java.util.Locale import java.util.TimeZone import java.util.concurrent.ConcurrentHashMap -/** - * The Epic Games Store catalog through the GraphQL endpoint the store website uses - * (`store.epicgames.com/graphql`, which wants a browser User-Agent): `Catalog.searchStore` with - * the base-game category, sorted by price / release date / relevance, on sale or free. Elements - * carry the offer id, the namespace, the catalog item ids (what the library records, so ownership - * is matched on those), key images and pre-formatted prices. "Free this week" comes from the - * static `freeGamesPromotions` feed. - */ object EpicStoreCatalog { private const val TAG = "EpicStore" private const val GRAPHQL = "https://store.epicgames.com/graphql" @@ -82,7 +74,6 @@ object EpicStoreCatalog { val img = imgs.optJSONObject(k) ?: continue val url = img.optString("url", "") if (url.isBlank()) continue - // The opaque box images first; the logo variants are transparent and never used. when (img.optString("type", "")) { "DieselGameBox" -> wide = url "OfferImageWide", "DieselStoreFrontWide" -> if (wide == null) wide = url @@ -105,8 +96,6 @@ object EpicStoreCatalog { val tagLine = buildList { e.optJSONArray("tags")?.let { tags -> for (t in 0 until tags.length()) { val n = tags.optJSONObject(t)?.optString("name").orEmpty(); if (n.isNotBlank() && n != "Windows" && size < 3) add(n) } } }.joinToString(", ") - // Epic's public store GraphQL has no age rating on an offer (no ageGatings/ratings field on - // CatalogOffer or StoreConfig); only a content tag, if Epic sets one, can mark it. val allTags = buildList { e.optJSONArray("tags")?.let { t -> for (k in 0 until t.length()) t.optJSONObject(k)?.optString("name")?.let { add(it) } } } val itemIds = buildList { e.optJSONArray("items")?.let { items -> for (j in 0 until items.length()) items.optJSONObject(j)?.optString("id")?.takeIf { it.isNotBlank() }?.let { add(it) } } @@ -124,7 +113,6 @@ object EpicStoreCatalog { ) } - // An offer's custom attribute (developerName, publisherName), or "". private fun attribute(e: JSONObject, key: String): String { val attrs = e.optJSONArray("customAttributes") ?: return "" return (0 until attrs.length()).mapNotNull { attrs.optJSONObject(it) }.firstOrNull { it.optString("key") == key } @@ -140,11 +128,9 @@ object EpicStoreCatalog { return slug.removeSuffix("/home") } - /** The Store tab's shelves: this week's giveaways as Free, sales as Deals, newest releases, trending = free-to-play picks. Blocking. */ fun featured(context: Context, force: Boolean = false): StoreShelves? { val now = System.currentTimeMillis() featuredCache?.let { if (!force && now - it.at < FEATURED_TTL_MS) return it.value } - // Shelves saved on an earlier run are as good while they are young: shown, not fetched again. if (!force && now - EpicPrefs.get(context).getLong("store_featured_at", 0L) < FEATURED_TTL_MS) cachedFeatured(context)?.let { return it } val freeNow = fetchPromos() val onSale = searchStore(24, sortBy = "currentPrice", sortDir = "ASC", onSale = true).filter { it.discountPercent > 0 } @@ -174,7 +160,6 @@ object EpicStoreCatalog { return results } - /** This week's giveaway titles (100% off right now). */ private fun fetchPromos(): List<CatalogItem> { val body = StoreNet.get("$PROMOS?locale=en-US&country=${country()}&allowCountries=${country()}") ?: return emptyList() return runCatching { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAchievements.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAchievements.kt index 8eb3d0688..cc3bd077a 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAchievements.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAchievements.kt @@ -11,7 +11,6 @@ import java.net.HttpURLConnection import java.net.URL import java.util.zip.InflaterInputStream -/** gameplay.gog.com, with a token scoped to the game's own client. */ object GogAchievements : StoreAchievements(Store.GOG.id, Store.GOG.shortLabel) { private const val TAG = "GogAchievements" @@ -44,7 +43,6 @@ object GogAchievements : StoreAchievements(Store.GOG.id, Store.GOG.shortLabel) { return Result.Found(out) } - /** The game's client id and secret: saved at install, else from the newest build's manifest. */ private fun client(context: Context, productId: String, token: String): Pair<String, String>? { val prefs = GogPrefs.get(context) val savedId = prefs.getString("client_id_$productId", null)?.ifEmpty { null } @@ -76,7 +74,6 @@ object GogAchievements : StoreAchievements(Store.GOG.id, Store.GOG.shortLabel) { } } - /** Gen2 manifests are zlib-compressed; plain JSON passes through. */ private fun inflate(data: ByteArray): String = if (data.isNotEmpty() && data[0] == '{'.code.toByte()) String(data, Charsets.UTF_8) else InflaterInputStream(data.inputStream()).use { String(it.readBytes(), Charsets.UTF_8) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAuth.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAuth.kt index 43191105e..2de0880bd 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAuth.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAuth.kt @@ -10,19 +10,12 @@ import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL -/** - * The GOG sign-in: the token the OAuth page hands back, kept in StoreAccounts' private file, - * refreshed through auth.gog.com when it has expired (the client id and secret are GOG Galaxy's - * own, public ones, as every third-party GOG client uses). Nothing here is ever logged: the token - * endpoint's URL itself carries the secret and the refresh token. - */ object GogAuth { private const val TAG = "GogAuth" private const val CLIENT_ID = "46899977096215655" private const val CLIENT_SECRET = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9" private const val TOKEN_URL = "https://auth.gog.com/token?client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&grant_type=refresh_token&refresh_token=" - /** The login page, as GOG Galaxy opens it: an implicit grant that returns the token in the redirect's fragment. */ const val AUTH_URL = "https://auth.gog.com/auth?client_id=$CLIENT_ID" + "&redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&response_type=token&layout=client2" const val REDIRECT_PREFIX = "https://embed.gog.com/on_login_success" @@ -32,7 +25,6 @@ object GogAuth { fun isSignedIn(context: Context): Boolean = StoreAccounts.read(context, Store.GOG)?.optString("access_token", "")?.isNotEmpty() == true - /** Saves a fresh sign-in and looks the account's name up for the chip. Blocking. */ fun saveLogin(context: Context, accessToken: String, refreshToken: String?, userId: String?) = GogComet.replaceAccount { saveAccount(context, accessToken, refreshToken, userId) } @@ -52,10 +44,8 @@ object GogAuth { Log.i(TAG, "signed in") } - /** The raw stored token, expired or not; null when signed out. */ fun storedToken(context: Context): String? = StoreAccounts.read(context, Store.GOG)?.optString("access_token", "")?.ifEmpty { null } - /** A usable access token, refreshed when the recorded login time says it has expired; null when signed out or the refresh failed. */ @JvmStatic @Synchronized fun validToken(context: Context): String? { @@ -67,7 +57,6 @@ object GogAuth { return if (loginTime == 0L || now >= loginTime + expiresIn - 60) refresh(context) else token } - /** Refreshes the access token; the new one, or null on any failure. Blocking. */ @JvmStatic @Synchronized fun refresh(context: Context): String? { @@ -86,7 +75,6 @@ object GogAuth { StoreAccounts.write(context, Store.GOG, json) access } catch (e: Exception) { - // Not the throwable: HttpURLConnection's messages echo the URL, which embeds the secrets. Log.w(TAG, "refresh failed: ${e.javaClass.simpleName}") null } finally { @@ -94,7 +82,6 @@ object GogAuth { } } - /** A complete companion snapshot. Never exported through the guest request channel. */ @Synchronized fun cometCredentials(context: Context): JSONObject? { val access = validToken(context) ?: return null @@ -117,7 +104,6 @@ object GogAuth { class GameToken(val accessToken: String, val userId: String) - /** The Galaxy refresh token exchanged for one scoped to a game's own client. Blocking. */ fun gameToken(context: Context, clientId: String, clientSecret: String): GameToken? { val json = StoreAccounts.read(context, Store.GOG) ?: return null val refresh = json.optString("refresh_token", "").ifEmpty { return null } @@ -139,7 +125,6 @@ object GogAuth { } } - /** Hosts the GOG login page hands off to for a social sign-in; they refuse an embedded browser's identity. */ fun isIdentityProviderHost(host: String?): Boolean { val h = host?.lowercase() ?: return false return h == "google.com" || h.endsWith(".google.com") || h == "youtube.com" || h.endsWith(".youtube.com") || diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogBackend.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogBackend.kt index f167265e8..2661a99f6 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogBackend.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogBackend.kt @@ -17,7 +17,6 @@ import com.droiddeck.launcher.stores.download.DownloadStage import java.io.File import java.util.concurrent.atomic.AtomicBoolean -/** GOG in the Stores section: sign-in, the library, the public shelves, installs. */ object GogBackend : StoreBackend { override val store: Store = Store.GOG @@ -32,8 +31,6 @@ object GogBackend : StoreBackend { override fun syncLibrary(context: Context, force: Boolean) { val app = context.applicationContext Thread({ - // The cache first, so the tab has something while the sync runs; read here, not on the - // main thread. With a cache on screen a background sync keeps quiet: only Refresh shows its bar. val cached = GogLibrary.cached(app).map { GogLibrary.toCatalogItem(app, it) } val quiet = cached.isNotEmpty() && !force StoresState.post { @@ -62,7 +59,6 @@ object GogBackend : StoreBackend { }, "gog-shelves").start() } - /** The shelves with the library's titles marked owned, so their cards offer Install. */ private fun markOwned(s: com.droiddeck.launcher.stores.StoreShelves): com.droiddeck.launcher.stores.StoreShelves { val owned = StoresState.library[store]?.associateBy { it.id } ?: return s fun mark(l: List<CatalogItem>) = l.map { i -> owned[i.id]?.let { o -> i.copy(owned = true, sizeBytes = o.sizeBytes) } ?: i } @@ -79,7 +75,6 @@ object GogBackend : StoreBackend { override fun uninstall(context: Context, game: InstalledStoreGame) {} - /** One GOG install, as the queue runs it. */ private class InstallJob(val app: Context, val game: GogGame, val item: CatalogItem, val folder: File) : DownloadQueue.DownloadJob { private val cancelled = AtomicBoolean(false) @@ -111,7 +106,6 @@ object GogBackend : StoreBackend { Store.GOG, game.gameId, game.title, exe = result.exeRelative.ifEmpty { "" }, installVersion = result.buildId, installedAt = System.currentTimeMillis(), cover = item.tallImageUrl ?: item.imageUrl, hero = item.imageUrl, - // Kept even when empty: the build was read, and lists none. extra = mapOf(com.droiddeck.launcher.frontend.WinCompSources.GOG_DEPENDENCIES to result.dependencies.joinToString(",")) + (if (result.dependenciesRead) mapOf(com.droiddeck.launcher.frontend.WinCompSources.LIST_READ to "1") else emptyMap()), ) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogComet.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogComet.kt index a514b8db3..d6a4c3a60 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogComet.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogComet.kt @@ -22,7 +22,6 @@ import java.io.File import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -/** Owns one Galaxy companion; only its structured, secret-free events reach Android logs. */ object GogComet { private const val TAG = "GogComet" private const val BIN = "/usr/local/lib/droiddeck-comet/comet" @@ -52,7 +51,6 @@ object GogComet { AchievementNotifications.begin(generation) } - /** Account replacement and launch preparation use the same lock, preventing stale restarts. */ fun replaceAccount(change: () -> Unit) = synchronized(lock) { stopLocked() AchievementNotifications.begin(generation) @@ -95,7 +93,6 @@ object GogComet { return file } - /** No arbitrary paths or credentials from the guest are accepted. */ fun prepare(context: Context, id: String): JSONObject = synchronized(lock) { fun reply(ready: Boolean, reason: String): JSONObject { report(reason) @@ -134,7 +131,6 @@ object GogComet { process = child; account = user; session = SessionState.sessionId Thread({ runCatching { child.inputStream.bufferedReader().useLines { lines -> lines.forEach { line -> - // Raw upstream errors/payloads can contain tokens. Never forward them. when { line.startsWith("DROIDDECK_COMET_READY:") -> { val childPid = line.substringAfter(':').toIntOrNull() ?: -1 @@ -177,7 +173,6 @@ object GogComet { private fun stopLocked() { val child = process ?: return - // Signal the announced daemon, not proot; start time prevents signalling a reused PID. if (started != null && identity(pid) == started) android.os.Process.sendSignal(pid, 2) if (!child.waitFor(5, TimeUnit.SECONDS)) { if (started != null && identity(pid) == started) android.os.Process.sendSignal(pid, 15) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java index a8bd89a80..0bbbaa238 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java @@ -36,68 +36,36 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; -/** - * The GOG install pipeline, ported from Bannerlator's GogDownloadManager and made blocking: the - * download queue runs {@link #install} on its own thread and reads progress through {@link Callback}. - * - * Gen 2: builds → build manifest → depot manifests (base product, matching language) → secure - * link → per file: chunks fetched, inflated, MD5-checked, assembled (the native engine when it is - * there, else the Java pool here). Gen 1: build manifest → per-file Range GET. Neither available: - * the product's Windows installer is downloaded as a file to run once. - * - * Resume and repair come from the per-file size + MD5 check: a complete file is skipped, a damaged - * one re-fetched. Nothing here writes outside the install folder; URLs are only logged redacted. - */ public final class GogDownloadManager { private static final String TAG = "GogDownload"; private static final int TIMEOUT = 30_000; private static final String MARKER = "_gog_manifest.json"; - /** Progress out of a running install. Every method is called on the install's thread. */ public interface Callback { - /** A new phase or file, as text, with the install's overall percentage. */ void onProgress(String message, int pct); - /** Bytes landed against the planned total, and the current speed (0 = unknown). */ default void onBytes(long done, long total, long speedBps) {} - /** A diagnostic line for the engine log. */ default void onLog(String line) {} - /** What this run fetches and what the game takes on disk. */ default void onSizes(long downloadBytes, long diskBytes) {} - /** - * The active stage's own count: {@code stage} is "verify" or "install", {@code done} of - * {@code total} an amount (bytes, or items when there are no bytes), {@code items} of - * {@code itemsTotal} the files, when it counts files. - */ default void onStage(String stage, long done, long total, int items, int itemsTotal) {} } - /** What an install leaves behind for the sidecar. */ public static final class Result { - /** The chosen exe, relative to the install folder (forward slashes), or "" when none was found. */ public final String exeRelative; public final String buildId; public final long bytes; - /** The build manifest's "dependencies" (GOG's redistributable ids: MSVC2017_x64, DirectX...), for Windows components. */ public List<String> dependencies = new ArrayList<>(); - /** The dependencies come from a gen2 build manifest read whole (gen1 builds and installers have none). */ public boolean dependenciesRead = false; Result(String exeRelative, String buildId, long bytes) { this.exeRelative = exeRelative; this.buildId = buildId; this.bytes = bytes; } } - /** Thrown for every way an install can fail; the message is what the Downloads page shows. */ public static final class InstallException extends Exception { InstallException(String message) { super(message); } } private GogDownloadManager() {} - /** - * Installs {@code game} into {@code installPath}. Blocks until done; {@code cancelled} set from - * another thread stops it at the next file boundary (what was written stays for a resume). - * Returns null when cancelled. - */ public static Result install(Context ctx, GogGame game, File installPath, Callback cb, AtomicBoolean cancelled) throws InstallException { cb.onProgress("Checking sign-in…", 0); String token = GogAuth.validToken(ctx); @@ -127,7 +95,6 @@ public final class GogDownloadManager { return runInstaller(ctx, game, token, installPath, cb, cancelled); } - // ---- gen2 ----------------------------------------------------------------------------------- private static final class Gen2Outcome { Result result; String error; boolean hardStop; @@ -148,14 +115,10 @@ public final class GogDownloadManager { tempExe = products.getJSONObject(0).optString("temp_executable", null); if (tempExe != null && tempExe.isEmpty()) tempExe = null; } - // The build manifest lists every product's depots (base + DLC), each tagged by productId; - // the base install takes the base product's only, under a secure link scoped to it. String baseProductId = manifest.optString("baseProductId", null); if ((baseProductId == null || baseProductId.isEmpty()) && products != null && products.length() > 0) baseProductId = products.getJSONObject(0).optString("productId", null); if (baseProductId == null || baseProductId.isEmpty()) baseProductId = game.gameId; - // The build's required redistributables (MSVC, DirectX, ...): logged, and kept in the - // sidecar, where Windows components take them as this game's list (WinCompSources). JSONArray deps = manifest.optJSONArray("dependencies"); List<String> dependencies = new ArrayList<>(); if (deps != null) for (int i = 0; i < deps.length(); i++) { String d = deps.optString(i, ""); if (!d.isEmpty()) dependencies.add(d); } @@ -197,7 +160,6 @@ public final class GogDownloadManager { return out; } } - // Largest first: the biggest archives start first and small files fill the tail. Collections.sort(files, (a, b) -> Long.compare(b.totalSize, a.totalSize)); final int total = files.size(); @@ -264,7 +226,6 @@ public final class GogDownloadManager { deleteCounted(new File(installPath, ".gog_chunks"), cb); String clientId = manifest.optString("clientId", null); if (clientId != null && !clientId.isEmpty()) GogPrefs.get(ctx).edit().putString("client_id_" + game.gameId, clientId).apply(); - // The game's own client secret: cloud saves need a token issued to the game's client. String clientSecret = manifest.optString("clientSecret", null); if (clientSecret != null && !clientSecret.isEmpty()) GogPrefs.get(ctx).edit().putString("client_secret_" + game.gameId, clientSecret).apply(); out.result = new Result(pickExe(installPath, tempExe, game.title), gm.buildId, planned); @@ -282,7 +243,6 @@ public final class GogDownloadManager { Gen2Manifest(JSONObject manifest, String buildId) { this.manifest = manifest; this.buildId = buildId; } } - /** The first Windows build's manifest, inflated and parsed; null on any failure. */ private static Gen2Manifest resolveGen2Manifest(String buildsJson, String token, Callback cb) { try { JSONArray items = new JSONObject(buildsJson).optJSONArray("items"); @@ -309,7 +269,6 @@ public final class GogDownloadManager { } } - /** English or every-language depots; GOG tags the rest by language code. */ private static boolean languageCompatible(JSONObject depot) { JSONArray languages = depot.optJSONArray("languages"); if (languages == null || languages.length() == 0) return true; @@ -317,7 +276,6 @@ public final class GogDownloadManager { return s.contains("*") || s.contains("en-US") || s.contains("\"en\"") || s.contains("english"); } - // ---- gen1 ----------------------------------------------------------------------------------- private static final class Gen1Outcome { Result result; String error; } @@ -420,9 +378,7 @@ public final class GogDownloadManager { } } - // ---- installer fallback --------------------------------------------------------------------- - /** Games with no content-system builds: the Windows installer, as one file, to run once in Proton. */ private static Result runInstaller(Context ctx, GogGame game, String token, File installPath, Callback cb, AtomicBoolean cancelled) throws InstallException { try { String productJson = httpGet("https://api.gog.com/products/" + game.gameId + "?expand=downloads", token); @@ -526,13 +482,7 @@ public final class GogDownloadManager { } } - // ---- native engine -------------------------------------------------------------------------- - /** - * Drives the native engine for one fetch loop, producing what the Java pool produces: the same - * counters, progress strings and percentages. A secure-link expiry (the run dying on - * 401/403/404/500) refreshes the link and re-runs for the files not yet done. - */ private static void runNativeEngine(Context ctx, int kind, String verifiedMsg, List<String> depotJsons, File installPath, AtomicReference<String> cdnBaseRef, String secureLinkUrl, String token, AtomicInteger cdnRefreshCount, int maxCdnRefresh, int maxWorkers, int processWorkers, @@ -542,7 +492,6 @@ public final class GogDownloadManager { StoreDownloadTier tier = StoreDownloadTier.Companion.current(ctx); int workers = Math.max(1, Math.min(128, tier.getNetworkWindow())); int process = Math.max(16, Math.max(processWorkers, tier.getProcessWorkers())); - // The tier's window goes to the engine as its ceiling (Max = 96); the per-host spread is the engine's. cb.onLog("gog: plan kind=" + kind + " files=" + total + " bytes=" + planned + " window=" + workers + " process_workers=" + process + " tier=" + tier.getId() + " engine=native"); final String[] manifests = depotJsons.toArray(new String[0]); final java.util.Set<String> donePaths = java.util.concurrent.ConcurrentHashMap.newKeySet(); @@ -554,9 +503,6 @@ public final class GogDownloadManager { final AtomicBoolean okRef = new AtomicBoolean(false); final AtomicBoolean linkExpiryRef = new AtomicBoolean(false); final AtomicReference<String> errRef = new AtomicReference<>(""); - // The bar between file completions: what earlier runs counted, plus the files this run - // skipped as already verified (the engine credits those no bytes), plus the engine's own - // byte count, which includes the files still in flight. Never shown going back. final long runStart = totalBytes.get(); final AtomicLong verifiedInRun = new AtomicLong(0); final AtomicLong shownBytes = new AtomicLong(runStart); @@ -583,7 +529,6 @@ public final class GogDownloadManager { okRef.set(success); linkExpiryRef.set(linkExpiry); errRef.set(error == null ? "" : error); latch.countDown(); } }; - // Every CDN of the set: the engine spreads its window across the hosts (JNI.md). String[] bases = (base == null || base.isEmpty()) ? new String[0] : base.split("\n"); if (run == 1) cb.onLog("gog: " + bases.length + " CDN host(s) offered"); long handle = GogNative.start(kind, manifests, bases, installPath.getAbsolutePath(), donePaths.toArray(new String[0]), "", @@ -611,7 +556,6 @@ public final class GogDownloadManager { } } - // ---- gen2 helpers (shared with the Java pool) ----------------------------------------------- private static void parseDepotManifest(String json, List<DepotFile> out) { try { @@ -675,17 +619,11 @@ public final class GogDownloadManager { return tmpFile.renameTo(outFile); } - /** - * Every CDN the secure link offers, as one value - their bases, one per line - so the set is - * refreshed (and compared on expiry) as a whole. GOG answers with several hosts in {@code urls[]}; - * taking only the first left every request on one host. - */ private static String parseCdnUrl(String json) { java.util.List<String> all = parseCdnUrls(json); return all.isEmpty() ? null : String.join("\n", all); } - /** The CDN bases of a secure_link answer, in the order GOG gives them. */ static java.util.List<String> parseCdnUrls(String json) { java.util.List<String> out = new ArrayList<>(); if (json == null) return out; @@ -712,13 +650,11 @@ public final class GogDownloadManager { private static final AtomicInteger CDN_TURN = new AtomicInteger(); - /** One of the set's CDNs, in turn, so the Java pool spreads its requests over all of them. */ static String pickCdn(String set) { String[] all = set.split("\n"); return all[Math.floorMod(CDN_TURN.getAndIncrement(), all.length)]; } - /** The first CDN of the set - what a caller that takes a single base gets. */ static String firstCdn(String set) { int nl = set.indexOf('\n'); return nl < 0 ? set : set.substring(0, nl); @@ -768,7 +704,6 @@ public final class GogDownloadManager { static byte[] fetchBytes(String url, String token) { return fetchBytesEx(url, token).body; } - /** gzip or zlib detected by magic, else the bytes as UTF-8. */ static String decompressBytes(byte[] data) { if (data == null || data.length < 2) return null; try { @@ -840,7 +775,6 @@ public final class GogDownloadManager { } } - /** The chunk path goes before the query string so the secure link's token survives. */ private static String buildChunkUrl(String base, String chunkPath) { int q = base.indexOf('?'); return q >= 0 ? base.substring(0, q) + "/" + chunkPath + base.substring(q) : base + "/" + chunkPath; @@ -946,7 +880,6 @@ public final class GogDownloadManager { private static String baseName(String path) { return path.contains("/") ? path.substring(path.lastIndexOf('/') + 1) : path; } - /** Download-pool threads for the built-in loop: cores times two, between 6 and 16. */ private static int downloadThreads() { int threads = Runtime.getRuntime().availableProcessors() * 2; return Math.max(6, Math.min(16, threads)); @@ -956,7 +889,6 @@ public final class GogDownloadManager { try (FileOutputStream fos = new FileOutputStream(f)) { fos.write(data); } } - /** Removes the chunk cache, counting its files as the Install stage's progress (a large cache takes a while). */ private static void deleteCounted(File dir, Callback cb) { File[] files = dir.listFiles(); if (files == null) { dir.delete(); return; } @@ -976,10 +908,6 @@ public final class GogDownloadManager { dir.delete(); } - /** - * The exe to run, relative to the install folder: the manifest's hint when present, else the - * candidate named most like the title, else the shallowest. "" when the folder has none. - */ static String pickExe(File installPath, String tempExe, String title) { if (tempExe != null) { File hinted = new File(installPath, tempExe); @@ -1038,12 +966,6 @@ public final class GogDownloadManager { Gen1File(String path, String url, long offset, long size) { this.path = path; this.url = url; this.offset = offset; this.size = size; } } - /** The install size of the Windows build (English / all-language depots), or -1. Blocking. */ - /** - * The "dependencies" of the build [buildId] of [gameId] (else the newest Windows build), from - * its build manifest alone: no game file is fetched. Null when the builds or the manifest - * could not be had; empty when the build lists none. Blocking. - */ public static List<String> fetchDependencies(String gameId, String buildId, String token) { try { String buildsUrl = "https://content-system.gog.com/products/" + gameId + "/os/windows/builds?generation=2"; @@ -1067,7 +989,6 @@ public final class GogDownloadManager { String manifestStr = decompressBytes(raw); if (manifestStr == null) return null; JSONObject manifest = new JSONObject(manifestStr); - // A manifest without depots is not one this reads right: no list rather than an empty one. JSONArray depots = manifest.optJSONArray("depots"); if (depots == null) { Log.w(TAG, "gog: build manifest for " + gameId + " has no depots; keys " + manifest.names()); return null; } JSONArray deps = manifest.optJSONArray("dependencies"); @@ -1124,6 +1045,5 @@ public final class GogDownloadManager { return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024)); } - /** Keeps the import used: every URL a log line names goes through here. */ static String safe(String url) { return StoreLog.redactUrl(url); } } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogGame.java b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogGame.java index 9f74b2ead..fef9876d0 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogGame.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogGame.java @@ -1,6 +1,5 @@ package com.droiddeck.launcher.stores.gog; -/** One GOG game as the library cache holds it. */ public class GogGame { public final String gameId; public final String title; @@ -8,11 +7,8 @@ public class GogGame { public final String description; public final String developer; public final String category; - /** Content-system generation of the Windows build: 1 or 2 (0 = unknown). */ public final int generation; - /** gamesdb.gog.com 2:3 box art, or null when the lookup found none. */ public final String verticalCover; - /** ISO date-time of the worldwide release, or "" when GOG gave none. */ public final String releaseDate; public final String publisher; diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt index 79d0abf73..61fae1a40 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt @@ -14,18 +14,10 @@ import java.util.concurrent.Callable import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean -/** - * The signed-in account's GOG library: the owned ids from embed.gog.com, each product's details - * from api.gog.com, and a cache so the tab paints at once. A sync fetches only what the cache - * lacks unless forced or 15 minutes stale. Ported from Bannerlator's GogLibraryRepo. - */ object GogLibrary { private const val TAG = "GogLibrary" - // v2: the card art moved from the store page's backdrop to the Galaxy library art; an older cache is re-fetched. private const val CACHE_KEY = "library_cache_v2" private const val LAST_SYNC_KEY = "library_synced_at" - // Owned products that are not Windows games (DLC, films, soundtracks, other platforms): never in - // the cache, so without this every sync would fetch each of them again. private const val SKIPPED_KEY = "library_skipped" private const val THROTTLE_MS = 6L * 60L * 60L * 1000L @@ -38,7 +30,6 @@ object GogLibrary { object Busy : SyncResult } - /** The cached library, alphabetical; empty until the first sync. */ fun cached(context: Context): List<GogGame> { val json = GogPrefs.get(context).getString(CACHE_KEY, null) ?: return emptyList() return runCatching { @@ -54,7 +45,6 @@ object GogLibrary { fun sizeOf(context: Context, gameId: String): Long = GogPrefs.get(context).getLong("size_$gameId", 0L) - /** Owned ids diffed against the cache; fetches what is new, or everything when forced / empty / stale. Blocking. */ fun sync(context: Context, force: Boolean = false, onStatus: (String) -> Unit = {}): SyncResult { if (!syncing.compareAndSet(false, true)) return SyncResult.Busy try { @@ -69,7 +59,6 @@ object GogLibrary { val owned = JSONObject(gamesJson).optJSONArray("owned") if (owned != null) for (i in 0 until owned.length()) { val id = owned.optLong(i).toString() - // 1801418160 is GOG's own "Galaxy" entry, in every library. if (id != "1801418160" && id != "0") ids.add(id) } }.isSuccess @@ -118,9 +107,7 @@ object GogLibrary { } } - /** One product's details, or null for anything that is not a Windows game (DLC, movies, other platforms). */ private fun fetchGame(prefs: SharedPreferences, id: String, token: String, skipped: MutableSet<String>): GogGame? { - // Not a game this app installs: remembered, so the next sync does not ask again. fun skip(): GogGame? { skipped.add(id); return null } try { val productJson = StoreNet.get("https://api.gog.com/products/$id?expand=downloads,description", bearer = token) ?: return null @@ -131,10 +118,6 @@ object GogLibrary { var title = prod.optJSONObject("title")?.optString("*") if (title.isNullOrEmpty()) title = prod.optString("title") if (title.isNullOrEmpty()) return skip() - // Card art from the v2 game record, the way the Galaxy client draws its library: the dark - // Galaxy background for the wide card and the box art for the tall one. The product's - // `images.background` is the store page's fade-out backdrop (it ends in white on a - // card) and is never used; the logo or icon is the last resort. val images = prod.optJSONObject("images") var imageUrl = "" var boxArt = "" @@ -145,12 +128,9 @@ object GogLibrary { } if (imageUrl.isEmpty()) imageUrl = images?.optString("logo2x", "") ?: "" if (imageUrl.isEmpty()) imageUrl = images?.optString("icon", "") ?: "" - // The account endpoint hands back template keys for some products; the v2 record's text - // is tried next, and when that is a template too the game simply has no description. val desc = cleanStoreText(prod.optJSONObject("description")?.optString("lead", "")) .ifEmpty { cleanStoreText(v2?.optString("overview", "")) } .ifEmpty { cleanStoreText(v2?.optString("description", "")) } - // The products endpoint names no developer or publisher; the v2 record does, with the worldwide release date. val embedded = v2?.optJSONObject("_embedded") fun names(key: String) = embedded?.optJSONArray(key) ?.let { arr -> (0 until arr.length()).mapNotNull { arr.optJSONObject(it)?.optString("name")?.takeIf(String::isNotBlank) } } @@ -218,13 +198,11 @@ object GogLibrary { } } - /** A cached game as the page's card. */ fun toCatalogItem(context: Context, g: GogGame): CatalogItem = CatalogItem( store = Store.GOG, id = g.gameId, title = g.title, imageUrl = GogStoreCatalog.absolutize(g.imageUrl).ifBlank { null }, tallImageUrl = GogStoreCatalog.absolutize(g.verticalCover ?: g.imageUrl).ifBlank { null }, tags = g.category, developer = g.developer, publisher = g.publisher, description = g.description, owned = true, - // A cache from before the release date was kept still has it under the older per-game key. releaseDate = g.releaseDate.ifEmpty { GogPrefs.get(context).getString("release_${g.gameId}", "").orEmpty() }, sizeBytes = sizeOf(context, g.gameId), storeUrl = "https://www.gog.com/en/game/${g.gameId}", diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogNative.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogNative.kt index 0922eda9a..f8ab86b14 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogNative.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogNative.kt @@ -1,52 +1,27 @@ -// JNI symbols depend on this package path and class name (app/src/main/rust/stores/JNI.md). package com.droiddeck.launcher.stores.gog import android.util.Log import com.droiddeck.launcher.stores.StoresNative -/** - * The native GOG download engine in `libdroiddeckstores.so`: the gen2 chunk loop (depot manifests - * → chunk fetch + inflate + MD5 → assembled files) and the gen1 range loop. One [start] is one - * download loop; the manager owns everything before and after it. Method names and signatures - * are Bannerlator's `BlGogDownload`, only the package and class changed. - */ object GogNative { private const val TAG = "GogNative" - /** gen2: depot manifests → chunk fetch + inflate + MD5 (base install, DLC, dependencies). */ const val KIND_GEN2_CHUNKS = 0 - /** gen1: build manifest → per-file HTTP Range GET streamed to disk. */ const val KIND_GEN1_RANGES = 1 - /** Callbacks from the engine; every method runs on a native thread. */ interface Listener { - /** - * One file reached its final state. [verified] = resume-skip (existing file passed size+MD5; - * no bytes credited); otherwise a freshly assembled, verified and renamed file ([fileBytes] - * = its decompressed size). [filesDone] counts both; [bytesDone] counts assembled bytes only. - */ fun onProgress(bytesDone: Long, bytesTotal: Long, filesDone: Int, filesTotal: Int, file: String, fileBytes: Long, verified: Boolean) - /** - * Byte progress between file completions, every 250 ms while fetching (and once at the end): - * assembled bytes plus what the files in flight have so far. Drives the bar and the speed. - */ fun onBytes(bytesDone: Long, bytesTotal: Long) - /** Engine diagnostics (already in logcat under `GogNative`). */ fun onLog(line: String) - /** - * Exactly once per [start] that returned a non-zero handle. [linkExpiry] = the run died on an - * HTTP 401/403/404/500, the codes an expired secure link returns: refresh it and run again. - */ fun onComplete(success: Boolean, cancelled: Boolean, linkExpiry: Boolean, error: String, bytesWritten: Long, filesDone: Int) } @Volatile private var available: Boolean? = null - /** True when the library loads and the GOG exports bind. Cached after the first call. */ @JvmStatic fun isAvailable(): Boolean { available?.let { return it } @@ -60,10 +35,6 @@ object GogNative { return ok } - /** - * Starts one download loop on a native thread; returns its handle (0 = not started, no - * callbacks). See JNI.md for the parameters; they are what the manager already holds. - */ @JvmStatic fun start( kind: Int, depotManifests: Array<String>, cdnBases: Array<String>, installDir: String, skipPaths: Array<String>, @@ -78,14 +49,12 @@ object GogNative { } } - /** Requests cancellation; `onComplete(cancelled = true)` follows. Idempotent; 0 is a no-op. */ @JvmStatic fun cancel(handle: Long) { if (handle == 0L) return runCatching { nativeCancel(handle) } } - /** Releases the handle, once, after `onComplete`. */ @JvmStatic fun release(handle: Long) { if (handle == 0L) return diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogPrefs.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogPrefs.kt index b5a3af6cb..f67e26cba 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogPrefs.kt @@ -3,12 +3,6 @@ package com.droiddeck.launcher.stores.gog import android.content.Context import android.content.SharedPreferences -/** - * GOG's own records that are not credentials: the library cache, per-game facts the sync learnt - * (generation, release date, size, cover), a build's client id. App-private preferences; the - * sign-in itself lives in StoreAccounts. Public, not internal: the Java download manager reads it - * and Kotlin mangles internal members' names for Java. - */ object GogPrefs { @JvmStatic fun get(context: Context): SharedPreferences = context.applicationContext.getSharedPreferences("stores.gog", Context.MODE_PRIVATE) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogStoreCatalog.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogStoreCatalog.kt index c05f9fdf9..7ec7454cb 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogStoreCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogStoreCatalog.kt @@ -14,19 +14,11 @@ import java.util.Currency import java.util.Locale import java.util.concurrent.ConcurrentHashMap -/** - * GOG's public catalog - the same unauthenticated service the gog.com store page uses: - * `catalog.gog.com/v1/catalog?order=desc:trending&productType=in:game,pack&...` with `discounted`, - * `price=between:0,0` and `query=like:` filters. Products carry pre-formatted prices and a - * discount string; Windows-only titles are kept. Cached in-process (shelves 30 min, searches - * 5 min) and the last good shelves mirrored to prefs so the tab paints offline too. - */ object GogStoreCatalog { private const val TAG = "GogStore" private const val CATALOG = "https://catalog.gog.com/v1/catalog" private const val FEATURED_TTL_MS = 3 * 60 * 60 * 1000L private const val SEARCH_TTL_MS = 5 * 60 * 1000L - // v2: entries carry the mature mark; an older cache would show everything until it aged out. private const val KEY_FEATURED = "store_featured_v2" private val supportedCurrencies = setOf( @@ -48,7 +40,6 @@ object GogStoreCatalog { private fun baseQuery(limit: Int, order: String, extra: String = ""): String = "$CATALOG?limit=$limit&order=$order&productType=in:game,pack&page=1&countryCode=${country()}&locale=en-US¤cyCode=${currency()}$extra" - /** GET the catalog; on a refusal of the locale's currency, retry as US / USD. */ private fun fetchProducts(url: String): List<CatalogItem> { var body = StoreNet.get(url) if (body == null && !url.contains("countryCode=US&locale=en-US¤cyCode=USD")) { @@ -57,11 +48,9 @@ object GogStoreCatalog { return if (body == null) emptyList() else parseProducts(body) } - /** The Store tab's shelves. Null only when every feed failed and nothing is cached. Blocking. */ fun featured(context: Context, force: Boolean = false): StoreShelves? { val now = System.currentTimeMillis() featuredCache?.let { if (!force && now - it.at < FEATURED_TTL_MS) return it.value } - // Shelves saved on an earlier run are as good while they are young: shown, not fetched again. if (!force && now - GogPrefs.get(context).getLong("store_featured_at", 0L) < FEATURED_TTL_MS) cachedFeatured(context)?.let { return it } val trending = fetchProducts(baseQuery(24, "desc:trending")) val newest = fetchProducts(baseQuery(40, "desc:releaseDate")).filter { !isFuture(it.releaseDate) }.take(24) @@ -105,7 +94,6 @@ object GogStoreCatalog { val id = p.optString("id", "") val title = p.optString("title", "") if (id.isEmpty() || title.isEmpty()) return null - // Windows only: the app cannot run anything else. val os = p.optJSONArray("operatingSystems") if (os != null && os.length() > 0) { var windows = false @@ -140,11 +128,6 @@ object GogStoreCatalog { ) } - /** - * GOG's own data: `ratings[]` carries each system's age (`esrbRating` 17, `pegiRating` 18, - * `uskRating` 18, `gogRating` 18, ...) and `tags[]` its content tags (`mature`, `nsfw`, - * `sexual-content`, `nudity`). The catalog has no filter for either, so this is client-side. - */ fun isMature(p: JSONObject): Boolean { val ages = ArrayList<Int>() p.optJSONArray("ratings")?.let { r -> for (i in 0 until r.length()) r.optJSONObject(i)?.optString("ageRating")?.toIntOrNull()?.let { ages.add(it) } } @@ -153,14 +136,12 @@ object GogStoreCatalog { return com.droiddeck.launcher.stores.matureByAge(ages) || com.droiddeck.launcher.stores.matureByTags(tags) } - /** GOG returns protocol-relative `//images…` URLs in a few places. */ @JvmStatic fun absolutize(url: String?): String { if (url.isNullOrBlank()) return "" return if (url.startsWith("//")) "https:$url" else url } - /** "YYYY.MM.DD" later than today: a pre-order, kept out of What's new. */ private fun isFuture(date: String): Boolean { if (date.length < 10) return false val parts = date.substring(0, 10).split('.', '-') diff --git a/app/src/main/java/com/droiddeck/launcher/ui/AddAppDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/AddAppDialog.kt index 31b8f46d6..bc9618c96 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/AddAppDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/AddAppDialog.kt @@ -111,10 +111,8 @@ private enum class AddSource(val label: Int, val hint: Int) { private val ICON_TYPES = listOf("png", "jpg", "jpeg", "webp") private val FieldShape = RoundedCornerShape(8.dp) -/** What an icon choice shows: a picked file, or a suggestion's link. */ private fun iconModel(ref: String): Any = if (ref.startsWith("https://")) ref else File(ref) -/** The Desktop page's Add: a script, an AppImage, a GitHub release or a Flatpak, with a name and icon. */ @Composable internal fun AddAppDialog(runtimeReady: Boolean, onDismiss: () -> Unit, onAdd: (UserApps.Request, String) -> Unit) { val ctx = LocalContext.current @@ -159,7 +157,6 @@ internal fun AddAppDialog(runtimeReady: Boolean, onDismiss: () -> Unit, onAdd: ( val busy = UserAppsState.working val tabFocus = remember { AddSource.entries.map { FocusRequester() } } val inputMode = LocalInputModeManager.current.inputMode - // Once, as it opens: typing a name switches the input mode too, and must keep its field. LaunchedEffect(Unit) { if (inputMode == InputMode.Keyboard) { repeat(2) { androidx.compose.runtime.withFrameNanos { } } @@ -240,10 +237,6 @@ internal fun AddAppDialog(runtimeReady: Boolean, onDismiss: () -> Unit, onAdd: ( } } -/** - * An added app's name and icon, changed on its page. [icon]: null keeps the current one, "" goes - * back to the default, else a picked file or a suggestion. - */ @Composable internal fun EditAppDialog(app: UserApps.App, onDismiss: () -> Unit, onSave: (String, String?, String?) -> Unit) { val ctx = LocalContext.current @@ -299,7 +292,6 @@ internal fun EditAppDialog(app: UserApps.App, onDismiss: () -> Unit, onSave: (St } } -/** Open from the first frame; [onDismiss] once the closing animation has run. */ @Composable internal fun rememberShown(onDismiss: () -> Unit): MutableTransitionState<Boolean> { val shown = remember { MutableTransitionState(false).apply { targetState = true } } @@ -307,25 +299,17 @@ internal fun rememberShown(onDismiss: () -> Unit): MutableTransitionState<Boolea return shown } -/** - * The card both dialogs sit in, sized to fit a landscape handheld without scrolling: it scales in - * like the app's menus, rises above the keyboard, and closes on a tap outside or B. - */ @Composable internal fun AppDialog( shown: MutableTransitionState<Boolean>, close: () -> Unit, label: String, wide: Boolean, modifier: Modifier = Modifier, maxWidth: Dp? = null, - /** [width] fixed, else as wide as its content when [fitContent], else as wide as it may be. */ width: Dp? = null, fitContent: Boolean = false, - /** What B / Back does, when not [close] (a picker goes up a folder first). */ back: (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit, ) { val pal = LocalPalette.current - // The page's ring stays out of sight behind it. VeilRing() Dialog(onDismissRequest = back ?: close, properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false)) { - // The page's focus memory stays the page's: nothing in here is a place to come back to. CompositionLocalProvider(LocalFrontFocus provides null) { Box( contentAlignment = Alignment.Center, @@ -368,7 +352,6 @@ internal fun AppDialog( } } -/** The eyebrow and title, with [trailing] (the tabs) beside them when there is room, else under them. */ @Composable internal fun DialogHeader(eyebrow: String, title: String, trailing: (@Composable () -> Unit)? = null) { val heading = @Composable { @@ -391,7 +374,6 @@ internal fun DialogHeader(eyebrow: String, title: String, trailing: (@Composable } } -/** Side by side and of one height on a wide card, stacked on a narrow one. */ @Composable private fun Columns(first: @Composable ColumnScope.() -> Unit, second: @Composable ColumnScope.() -> Unit) { Rise(1) { @@ -407,7 +389,6 @@ private fun Columns(first: @Composable ColumnScope.() -> Unit, second: @Composab } } -/** A titled inset of the card; it fills the height its column is given. */ @Composable private fun Panel(title: String, content: @Composable ColumnScope.() -> Unit) { val colors = MaterialTheme.colorScheme @@ -421,7 +402,6 @@ private fun Panel(title: String, content: @Composable ColumnScope.() -> Unit) { } } -/** Why the main button is off (or nothing), then Cancel and the main button, at the card's foot. */ @Composable internal fun DialogFooter(note: String?, confirm: String, enabled: Boolean, onCancel: () -> Unit, onConfirm: () -> Unit) { val lineColor = LocalPalette.current.line @@ -446,7 +426,6 @@ private fun hideKeyboard(view: View) { ?.hideSoftInputFromWindow(view.windowToken, 0) } -/** The app's text field, with what is wrong with its value under it. */ @Composable private fun CheckedField(value: String, onChange: (String) -> Unit, label: String, placeholder: String, problem: String?) { val view = LocalView.current @@ -468,7 +447,6 @@ private fun NameField(name: String, onName: (String) -> Unit, placeholder: Strin ) } -/** The app as the Desktop page will show it: its icon, its name ([name] null: not known yet) and kind. */ @Composable private fun AppPreview(icon: Any?, glyph: ImageVector, name: String?, kind: String) { val colors = MaterialTheme.colorScheme @@ -493,7 +471,6 @@ private fun AppPreview(icon: Any?, glyph: ImageVector, name: String?, kind: Stri } } -/** The icon's picker and [clear] (a label and what it does). */ @Composable private fun IconActions(onChoose: () -> Unit, clear: Pair<Int, () -> Unit>?) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -504,7 +481,6 @@ private fun IconActions(onChoose: () -> Unit, clear: Pair<Int, () -> Unit>?) { } } -/** Icons found in [repo], looked up off the main thread once the name has settled; [selected] is ringed. */ @Composable private fun IconSuggestions(repo: String, selected: String?, onPick: (String) -> Unit) { val found by produceState<List<String>?>(null, repo) { @@ -537,7 +513,6 @@ private fun SuggestedIcon(url: String, selected: Boolean, onClick: () -> Unit) { .controllerConfirm(onClick = onClick), ) { AsyncImage(model = ImageRequest.Builder(LocalContext.current).data(url).crossfade(true).build(), contentDescription = stringResource(R.string.add_app_icon), contentScale = ContentScale.Fit, modifier = Modifier.size(34.dp)) - // The chosen one is ticked; the ring stays with focus. if (selected) Box( contentAlignment = Alignment.Center, modifier = Modifier.align(Alignment.TopEnd).padding(2.dp).size(14.dp).clip(androidx.compose.foundation.shape.CircleShape).background(pal.signal), @@ -545,7 +520,6 @@ private fun SuggestedIcon(url: String, selected: Boolean, onClick: () -> Unit) { } } -/** The chosen file, or none yet, laid out like the app's text fields, with the button that picks it. */ @Composable private fun FileRow(path: String?, onChoose: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -567,7 +541,6 @@ private fun FileRow(path: String?, onChoose: () -> Unit) { } } -/** Whether the script's folder will be linked or copied in, worked out off the main thread. */ @Composable private fun FolderNote(scriptPath: String) { var plan by remember(scriptPath) { mutableStateOf<UserApps.ScriptPlan?>(null) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt b/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt index 1822ee527..672e73cc9 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt @@ -62,7 +62,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File -/** Each art slot's thumbnail, in its true shape: width, height, corner. */ private fun AddedGameArt.Slot.shape(): Triple<Dp, Dp, Dp> = when (this) { AddedGameArt.Slot.COVER -> Triple(28.dp, 41.dp, 5.dp) AddedGameArt.Slot.BACKGROUND -> Triple(60.dp, 34.dp, 5.dp) @@ -92,7 +91,6 @@ private fun artSourceLabel(source: String): String = stringResource( }, ) -/** A thumbnail of [model] (a file or URL) in [slot]'s shape, scaled by [scale]. */ @Composable private fun SlotThumb(slot: AddedGameArt.Slot, model: Any?, scale: Float = 1f, selected: Boolean = false) { val pal = LocalPalette.current @@ -110,7 +108,6 @@ private fun SlotThumb(slot: AddedGameArt.Slot, model: Any?, scale: Float = 1f, s } } -/** A row of the editor: a label column, then the value. */ @Composable private fun FieldRow(label: String, focus: FocusRequester? = null, onClick: () -> Unit, value: @Composable () -> Unit) { ListRow(focus = focus, onClick = onClick) { @@ -119,20 +116,13 @@ private fun FieldRow(label: String, focus: FocusRequester? = null, onClick: () - } } -/** Where the editor opens: its game's folder, straight on Target, and whether it came from the added-games summary. */ internal class EditRequest(val folder: String, val openTarget: Boolean, val fromSummary: Boolean, val preview: EditPreview) -/** - * What the editor shows on its first frame, from what the page already has (the game's row, or - * the summary's); the rest (the ranked exes, where each piece of art came from) follows. - * [exeName] / [startIn] null = not known yet. - */ internal class EditPreview( val name: String, val exeName: String?, val startIn: String?, val launchOptions: String, val art: Map<AddedGameArt.Slot, File?>, ) { companion object { - /** From the Games tab's row for [game] and the few preferences the edits keep. */ fun of(context: android.content.Context, game: com.droiddeck.launcher.frontend.Library.SteamGame, folder: String): EditPreview { val exe = SessionPrefs.addedGameExe(context, folder).ifEmpty { com.droiddeck.launcher.frontend.AddedExes.list(context).firstOrNull { it.folder == folder && it.picked }?.exe.orEmpty() @@ -144,28 +134,20 @@ internal class EditPreview( ) } - /** From a summary row's game and its cover. */ fun of(game: AddedGames.Game, cover: File?) = EditPreview( game.name, game.exe.name, game.guestDir.substringAfterLast('/'), game.launchOptions, mapOf(AddedGameArt.Slot.COVER to cover), ) } } -/** A small spinner where a value is still being read. */ @Composable private fun Loading() { androidx.compose.material3.CircularProgressIndicator(Modifier.size(14.dp), strokeWidth = 2.dp) } -/** - * An added game's editor, Steam's non-Steam Properties in the app: Name, Target (the ranked exes, - * "?" where nothing clearly won, Other exe…), Start in, Launch options, the four pieces of art, and - * Remove (two presses; the files stay). Every change goes through the game's own Steam shortcut. - */ @Composable internal fun AddedGameEditor( request: EditRequest, onBack: () -> Unit, onClose: () -> Unit, onChanged: () -> Unit, - /** Remove pressed twice: the game's folder, its shortcut appid when known, its name. */ onRemove: (folder: String, appId: Long?, name: String) -> Unit, ) { val ctx = LocalContext.current @@ -260,7 +242,6 @@ internal fun AddedGameEditor( RemoveRow(armed) { if (!armed) armed = true else { armed = false - // At once: the page drops the game and the rest runs off this dialog's life. onRemove(folder, game?.appId, game?.name ?: preview.name) if (request.fromSummary) onBack() close() @@ -304,7 +285,6 @@ internal fun AddedGameEditor( } } -/** Start in as a path on the device: the override mapped back under the game's folder, else the exe's folder. */ private fun hostPath(context: android.content.Context, game: AddedGames.Game): String { val folderGuest = AddedGames.guestPath(context, game.folder) ?: return game.exe.parentFile?.path.orEmpty() return when { @@ -322,7 +302,6 @@ private fun ValueText(text: String, muted: Boolean = false) { ) } -/** "Remove" in red; pressed once it asks, a second press within four seconds removes. */ @Composable private fun RemoveRow(armed: Boolean, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -344,7 +323,6 @@ private fun RemoveRow(armed: Boolean, onClick: () -> Unit) { } } -/** One line of text in a compact dialog: [title] and a red X, the field, OK. */ @Composable internal fun TextEntryDialog(title: String, initial: String, onDismiss: () -> Unit, onSave: (String) -> Unit) { val shown = rememberShown(onDismiss) @@ -365,11 +343,6 @@ internal fun TextEntryDialog(title: String, initial: String, onDismiss: () -> Un } } -/** - * The art chooser for one [slot]: back, the slot's name and a red X; the images grouped by where - * they come from (the game's folder, Steam, SteamGridDB, fetched as it opens), the current one - * outlined, a tap applies it; "Pick image…" and "Reset to automatic". - */ @OptIn(ExperimentalLayoutApi::class) @Composable private fun ArtChooserDialog( @@ -456,12 +429,6 @@ private fun ArtOption(slot: AddedGameArt.Slot, option: AddedGameArt.Option, sele } } -/** - * After "Add all games in this folder": how many were added and how many were there already, then - * one row per added game (cover, name, exe, "?" where unsure); a row opens that game's editor. - * [games] are the added games as the add left them, shown at once; the rows then follow edits and - * removals, and the covers arrive. - */ @Composable internal fun AddedGamesSummaryDialog( games: List<AddedGames.Game>, already: Int, checking: String? = null, @@ -478,8 +445,6 @@ internal fun AddedGamesSummaryDialog( var gone by remember { mutableStateOf(emptySet<String>()) } var armed by remember { mutableStateOf<String?>(null) } LaunchedEffect(armed) { if (armed != null) { kotlinx.coroutines.delay(4_000); armed = null } } - // Rows show as the games arrive; each is refreshed once (current name and exe, a removed one - // dropped) and gets its cover, without redoing the ones already done when more arrive. var resolved by remember { mutableStateOf(emptyMap<String, Pair<AddedGames.Game, File?>?>()) } LaunchedEffect(games) { for (old in games) { @@ -534,7 +499,6 @@ internal fun AddedGamesSummaryDialog( } } -/** A summary row's red X: the first press arms it (filled), a second within 4 s removes the game. */ @Composable private fun RemoveX(armed: Boolean, description: String, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/AppScale.kt b/app/src/main/java/com/droiddeck/launcher/ui/AppScale.kt index 806ef22cd..bbdb62c19 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/AppScale.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/AppScale.kt @@ -10,7 +10,6 @@ internal fun scaledAppDensity(base: Density, percent: Int): Density { return if (scale == 1f) base else Density(base.density * scale, base.fontScale) } -/** Responsive layouts and popup height limits must see the same dp space as the scaled UI. */ internal fun scaledAppConfiguration(base: Configuration, percent: Int): Configuration { val scale = AppUiPrefs.normalizeScale(percent) / 100f if (scale == 1f) return base diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt index 48ac4a28d..32dcaa167 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt @@ -97,28 +97,21 @@ import kotlinx.coroutines.withContext import java.text.DateFormat import java.util.Date -/** The Components page's layer for the GPU drivers, under every Proton's. */ const val GPU_TAB = "gpu" -/** The Components page's layer for the Proton itself, the one the indented layers belong to. */ const val PROTON_TAB = "proton" -/** The stack's layers, top to bottom: LB / RB and the d-pad walk them in this order. */ internal val LAYERS = listOf(PROTON_TAB, "dxvk", "vkd3d", "fex", GPU_TAB) -/** The layer [by] steps from [current] (LB -1, RB +1), wrapping round the stack. */ internal fun layerStep(current: String, by: Int): String { val i = LAYERS.indexOf(current).coerceAtLeast(0) return LAYERS[((i + by) % LAYERS.size + LAYERS.size) % LAYERS.size] } -/** One installed row: a Proton's original bundle (one per Proton build) or a stored package. */ private class InstalledItem( val name: String, val detail: String, val tag: String, val selected: Boolean, val removable: Boolean, - /** Stored package file, or null for an original. */ val file: String?, - /** The Proton build an original belongs to. */ val protonVersion: String?, ) @@ -134,14 +127,6 @@ private val QUEUED = Color(0xFFFFB86B) private val ROW_SHAPE = RoundedCornerShape(10.dp) private val LAYER_SHAPE = RoundedCornerShape(12.dp) -/** - * Everything a game runs on, as one stack: the Proton, its DXVK, VKD3D-Proton and FEX indented - * under it because they belong to it, then the GPU drivers that every Proton shares. One selection - * glides along the stack (LB / RB, or the d-pad in it); beside it a detail frame that never moves - - * what is in use, what is installed and what is available, and what else can be done - only its - * rows trade places. A swap is asked for by stepping out of its row, and the row flies home to its - * layer. On a narrow page the stack is the page, and a layer's detail floods out of its card. - */ @Composable fun ComponentsPage( snapshot: Snapshot?, @@ -167,13 +152,10 @@ fun ComponentsPage( generalFexPreset: String = "", onGeneralFexPreset: (String) -> Unit = {}, requestInitialFocus: Boolean = true, - /** The GPU drivers layer ([GPU_TAB]): what it shows and does. */ gpu: GpuDriversState = GpuDriversState(), gpuActions: GpuDriversActions = GpuDriversActions(), - /** The Proton layer ([PROTON_TAB]): the Protons DroidDeck can install, and installing them. */ protons: ProtonsState = ProtonsState(), protonActions: ProtonsActions = ProtonsActions(), - /** The Proton layer's options for every Proton (sync, FEX preset, SSBS, environment). */ protonOptions: @Composable () -> Unit = {}, ) { val colors = MaterialTheme.colorScheme @@ -391,12 +373,10 @@ fun ComponentsPage( val views = snapshot?.protons ?: emptyList() val view = views.firstOrNull { it.proton.id == protonId } ?: views.firstOrNull() - // Where things sit, root px: each layer card, each row (for a swap's dot), the page itself. val cardsAt = remember { mutableStateMapOf<String, Rect>() } val rowsAt = remember { HashMap<String, Rect>() } var stackAt by remember { mutableStateOf(Offset.Zero) } var pageAt by remember { mutableStateOf(Offset.Zero) } - // A layer card pops and rings as a swap's dot lands on it, and rings as a hop arrives on it. val landed = remember { mutableStateMapOf<String, Int>() } val rings = remember { mutableStateMapOf<String, Int>() } fun land(l: String) { landed[l] = (landed[l] ?: 0) + 1; rings[l] = (rings[l] ?: 0) + 1 } @@ -404,24 +384,20 @@ fun ComponentsPage( LaunchedEffect(Hops.arrived) { if (Hops.arrived == arrivedSeen[0]) return@LaunchedEffect arrivedSeen[0] = Hops.arrived - // Once the page has flooded in from the rail. delay(Motion.ms(700).toLong()) rings[layer] = (rings[layer] ?: 0) + 1 } - /** The row at [key] squashes into a dot and flies home to [to]'s card, which pops on landing. */ fun flyHome(key: String, to: String) { val from = rowsAt[key] ?: return land(to) Flights.fly(Flight(from, to = { cardsAt[to]?.center }, onLand = { land(to) })) } - // A confirm stepping out of the row that asked: right of a left-column row, left of a right one. fun ask(key: String, right: Boolean, title: String, note: String, verb: String, danger: Boolean, face: String = "", then: () -> Unit) { val at = rowsAt[key] ?: return then() val cancel = ctx.getString(R.string.common_cancel) Steps.ask(StepAsk( anchor = at, side = if (right) StepSide.Left else StepSide.Right, accent = if (danger) colors.error else pal.signal, pillCorner = 10.dp, items = 3, maxWidth = 340.dp, - // The row's own name rides on top, as the pull's handle. handle = { Box(Modifier.fillMaxSize().padding(start = 46.dp, end = 14.dp), contentAlignment = Alignment.CenterStart) { Text(face, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = if (danger) colors.error else pal.signal, maxLines = 2, overflow = TextOverflow.Ellipsis) @@ -437,7 +413,6 @@ fun ComponentsPage( }) } - // The runtime and display drivers on their own pages, flooding out of the link that opens them. var driverPage by remember { mutableStateOf<String?>(null) } var driverShown by remember { mutableStateOf<String?>(null) } var driverLeaving by remember { mutableStateOf(false) } @@ -450,7 +425,6 @@ fun ComponentsPage( driverShown = null; driverLeaving = false; driverFrom = null } } - // Narrow: a layer's detail floods out of its card, and B draws it back in. var narrowOpen by rememberSaveable { mutableStateOf<String?>(null) } var narrowShown by remember { mutableStateOf<String?>(null) } var narrowLeaving by remember { mutableStateOf(false) } @@ -466,21 +440,17 @@ fun ComponentsPage( BackHandler(enabled = driverPage != null || narrowOpen != null) { if (driverPage != null) driverPage = null else narrowOpen = null } - // Leaving the page: nothing stays stepped out of a row that is no longer on screen. androidx.compose.runtime.DisposableEffect(Unit) { onDispose { Steps.drop() } } val checked = if (catalogAt > 0) DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(catalogAt * 1000)) else stringResource(R.string.comp_never) val firstRow = remember { FocusRequester() } val layerFocus = remember { LAYERS.associateWith { FocusRequester() } } - // Arrived (from the rail, a hop, a game's card): a pad starts on the chosen layer. LaunchedEffect(requestInitialFocus) { if (!requestInitialFocus || selectedGame != null || inputMode.inputMode != InputMode.Keyboard) return@LaunchedEffect delay(Motion.ms(200).toLong()) focusSoon { layerFocus.getValue(layer) } } var travel by remember { mutableIntStateOf(1) } - // LB / RB (taken by the activity before the page sees them): a pad anywhere on the page goes to - // the new layer's card, so one on a row - which leaves with its layer - is never left with nothing. var pageFocused by remember { mutableStateOf(false) } val seenLayer = remember { arrayOf(layer) } LaunchedEffect(layer) { @@ -494,7 +464,6 @@ fun ComponentsPage( if (l != layer) { travel = if (LAYERS.indexOf(l) > LAYERS.indexOf(layer)) 1 else -1; onComp(l) } } - // ---- one layer's detail, a slot at a time ------------------------------------------------------ val compState = { l: String -> view?.components?.get(l) } val bar: @Composable (String) -> Unit = { l -> when (l) { @@ -531,7 +500,6 @@ fun ComponentsPage( } } - // Installed and Available, as (title, rows) for a layer; [right] says which column it is in. var showAllPairs by rememberSaveable { mutableStateOf(false) } val installedTitle: @Composable (String) -> String = { l -> when (l) { @@ -643,8 +611,6 @@ fun ComponentsPage( } } PROTON_TAB -> { - // Not installed, and installed with a newer release out: an update goes in beside the - // build it replaces, which stays until it is removed (games are mapped to it by name). val rows = protons.rows.filter { it.installed == null || it.update != null } if (rows.isEmpty()) EmptyLine(stringResource(R.string.comp_nothing_new)) for (row in rows) GetLine( @@ -731,17 +697,13 @@ fun ComponentsPage( } } - // ---- the frame ------------------------------------------------------------------------------ val detail: @Composable (String, Boolean) -> Unit = { l, narrowPage -> androidx.compose.foundation.layout.BoxWithConstraints(Modifier.fillMaxSize()) { - // Two columns while each gets room for a row's name and its Get; one under the other below that. val single = narrowPage || maxWidth < 600.dp Column(Modifier.fillMaxSize()) { - // The in-use bar keeps its place; only its words change. AnimatedContent(l, transitionSpec = { fadeIn(Motion.tw(240)) togetherWith fadeOut(Motion.tw(160)) }, label = "inUse") { k -> bar(k) } val detailScroll = androidx.compose.runtime.key(l) { rememberScrollState() } Column(Modifier.weight(1f).fillMaxWidth().verticalScroll(detailScroll).padding(top = 12.dp, bottom = 16.dp)) { - // Shared performance settings stay visible before the downloadable catalog. if (l == PROTON_TAB) { if (!protons.runtimeReady) EmptyLine(stringResource(R.string.content_needs_runtime)) if (protons.sessionRunning) EmptyLine(stringResource(R.string.proton_stop_first)) @@ -801,7 +763,6 @@ fun ComponentsPage( onNext = { pick(layerStep(layer, 1)) }, ) else Modifier), ) { - // ---- header: the title, the device, when the lists were checked and the one refresh ---- Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) { Text(stringResource(R.string.comp_title), fontSize = if (narrow) 22.sp else 26.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1) if (!narrow) DeviceChip(gpu) @@ -894,7 +855,6 @@ fun ComponentsPage( ) } else { Row(Modifier.weight(1f).fillMaxWidth()) { - // ---- the stack ---- Box( (if (narrow) Modifier.fillMaxWidth() else Modifier.width(250.dp)).fillMaxHeight() .onGloballyPositioned { stackAt = it.positionInRoot() }, @@ -911,7 +871,6 @@ fun ComponentsPage( else runCatching { firstRow.requestFocus() } }, ) - // Over the cards: under them, their rest fill hid it as it crossed them. ChipGlide(LAYERS.indexOf(layer), local, tint, GlideAxis.Vertical, corner = 12.dp) } if (!narrow) { @@ -921,7 +880,6 @@ fun ComponentsPage( } } } - // Narrow: the layer's detail over the stack, out of its card. narrowShown?.let { l -> val from = cardsAt[l]?.let { Origin(it.translate(-pageAt), 12f * density.density) } val body: @Composable () -> Unit = { @@ -936,7 +894,6 @@ fun ComponentsPage( } if (from == null) body() else PageFlood(from, narrowLeaving) { body() } } - // The runtime or display driver on its own page. driverShown?.let { d -> val body: @Composable () -> Unit = { Box(Modifier.fillMaxSize().background(colors.background).pointerInput(Unit) { detectTapGestures { } }) { @@ -967,7 +924,6 @@ fun ComponentsPage( onDismissRequest = { confirm = null }, title = { Text(confirmTitle) }, text = { Text(body, fontSize = 14.sp) }, - // Opens on Cancel, so a stray A press on a controller never swaps or deletes anything. confirmButton = { val del = confirmVerb == R.string.common_delete || confirmVerb == R.string.comp_game_reset FocusText(stringResource(confirmVerb), if (del) colors.error else pal.signal) { confirm = null; action() } @@ -1008,7 +964,6 @@ fun ComponentsPage( } } -/** The Installed rows of one component layer for the Proton the page shows. */ @Composable private fun installedFor(l: String, view: ComponentsManager.ProtonView?, snapshot: Snapshot?): List<InstalledItem> { view ?: return emptyList() @@ -1027,7 +982,6 @@ private fun installedFor(l: String, view: ComponentsManager.ProtonView?, snapsho } } -/** The Nightlies of one component layer that are not stored yet. */ private fun availableFor(l: String, catalog: List<CatalogItem>, snapshot: Snapshot?): List<CatalogItem> { val stored = (snapshot?.packages ?: emptyList()).map { it.file }.toSet() return catalog.filter { it.comp == l && ComponentsManager.safeName(it.file) !in stored } @@ -1040,14 +994,8 @@ private fun LayerTitle(l: String): String = when (l) { else -> ComponentsManager.LABEL[l] ?: l } -/** Records where a row sits, for the confirm that steps out of it and the dot that flies from it. */ private fun Modifier.trackRow(key: String, into: HashMap<String, Rect>): Modifier = onGloballyPositioned { into[key] = it.boundsInRoot() } -/** - * The stack: a heading over the Proton and the three layers that belong to it (indented, on a - * line), then a heading over the GPU drivers that every Proton shares. Each card is a layer's name - * and what it is set to; a dot marks a layer swapped away from its original. - */ @Composable private fun Stack( layer: String, view: ComponentsManager.ProtonView?, gpu: GpuDriversState, @@ -1096,7 +1044,6 @@ private fun StackHeading(text: String) { ) } -/** One layer: 40dp to see, 44 to touch; its name, what it is set to, and a dot when it is swapped. */ @Composable private fun LayerCard( title: String, value: String, swapped: Boolean, selected: Boolean, landed: Int, ring: Int, @@ -1106,7 +1053,6 @@ private fun LayerCard( val pal = LocalPalette.current val src = remember { MutableInteractionSource() } val hot = rememberHot(src) - // The stack's glide draws the picked card's look by touch only; with a pad, its rest look and the bar. val under = selected && androidx.compose.ui.platform.LocalInputModeManager.current.inputMode != androidx.compose.ui.input.InputMode.Keyboard Box(Modifier.fillMaxWidth().heightIn(min = 44.dp), contentAlignment = Alignment.Center) { Row( @@ -1125,7 +1071,6 @@ private fun LayerCard( if (swapped) Box(Modifier.size(7.dp).clip(CircleShape).background(pal.signal)) Text(title, fontSize = 13.5.sp, fontWeight = FontWeight.Bold, color = if (selected || hot) colors.onBackground else colors.onSurfaceVariant, maxLines = 1) Spacer(Modifier.weight(1f)) - // The value ticks over as a swap lands. AnimatedContent( value, transitionSpec = { (slideInVertically(Motion.tw(260)) { it / 2 } + fadeIn(Motion.tw(260))) togetherWith (slideOutVertically(Motion.tw(160)) { -it / 2 } + fadeOut(Motion.tw(160))) }, @@ -1135,7 +1080,6 @@ private fun LayerCard( } } -/** What is in use for the layer: a dot (green, or amber while a change waits), a sentence, and a note or Cancel. */ @Composable private fun InUseBar(queued: Boolean, sentence: String, note: String, onCancel: (() -> Unit)? = null) { val colors = MaterialTheme.colorScheme @@ -1152,7 +1096,6 @@ private fun InUseBar(queued: Boolean, sentence: String, note: String, onCancel: } } -/** A titled column of rows. */ @Composable private fun RowGroup(title: String, content: @Composable ColumnScope.() -> Unit) { val colors = MaterialTheme.colorScheme @@ -1437,10 +1380,6 @@ internal fun ToolIcon(icon: ImageVector, description: String, busy: Boolean = fa } } -/** - * A small button. Resting: grey outline (blue text when [accent]). Focused or hovered: a solid blue - * fill with an outline in the text colour - unmistakable in a column of identical buttons. - */ @Composable private fun SmallButton(text: String, enabled: Boolean = true, accent: Boolean = false, modifier: Modifier = Modifier, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -1485,10 +1424,6 @@ private fun Tag(text: String) { ) } -/** - * One installed choice: a radio for the one in use, its name and detail, and a trash at the right - * of the ones that can go. [progress] shows a download for it in place of the tag. - */ @Composable private fun ChoiceLine( name: String, detail: String, selected: Boolean, removable: Boolean, @@ -1525,10 +1460,8 @@ private fun ChoiceLine( } DetailLine(detail, badge) } - // The radio says which is in use; a download in flight shows where a tag would. if (progress != null) ProgressBit(progress) } - // The trash column is always there (empty when the row can't be deleted) so rows line up. if (!removable) Spacer(Modifier.padding(end = 6.dp).size(44.dp)) else { val delSrc = remember { MutableInteractionSource() } @@ -1546,7 +1479,6 @@ private fun ChoiceLine( Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line)) } -/** One thing not installed yet: its name and detail, and Get - its download shown in the row once started. */ @Composable private fun GetLine( name: String, detail: String, progress: Int?, enabled: Boolean, @@ -1571,7 +1503,6 @@ private fun GetLine( Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line)) } -/** A row's second line, led by [badge] (Recommended) in green when there is one. */ @Composable private fun DetailLine(detail: String, badge: String?) { val colors = MaterialTheme.colorScheme @@ -1599,10 +1530,6 @@ private fun ProgressBit(progress: Int) { } } -/** - * The GPU and how far it is supported, as a chip: a green dot and "<GPU> · tested", amber when it - * is not. A steps the device's full details out of it, with the copy for a bug report. - */ @Composable private fun DeviceChip(s: GpuDriversState) { val colors = MaterialTheme.colorScheme @@ -1643,7 +1570,6 @@ private fun DeviceChip(s: GpuDriversState) { ) { face() } } -/** What the Proton layer installs: the Protons DroidDeck can download, and one being installed. */ class ProtonsState( val rows: List<ProtonRow> = emptyList(), val busyId: String? = null, @@ -1656,16 +1582,10 @@ class ProtonsState( class ProtonsActions( val onInstall: (String) -> Unit = {}, val onCancel: (String) -> Unit = {}, - /** The source's id and the installed folder to delete. */ val onRemove: (String, String) -> Unit = { _, _ -> }, - /** Deletes every build of the source but the newest. */ val onRemoveOlder: (String) -> Unit = {}, ) -/** - * A Proton DroidDeck can install: the folder its newest build is installed as (null when it is - * not), every installed build oldest first, and a newer release's tag when one is out. - */ class ProtonRow( val id: String, val name: String, val installed: String?, val queued: Boolean, val installedAll: List<String> = listOfNotNull(installed), val update: String? = null, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt index 6b527c97c..8f883c24c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt @@ -11,11 +11,6 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke -/** - * The session's pointer. [image]: the program's own cursor (wl_pointer.set_cursor - labwc's resize - * arrows, a text field's I-beam), in session pixels, drawn [imageScale] times larger with its hotspot - * ([hotX], [hotY]) on [position]; null = the program has not set one, and the built-in arrow is drawn. - */ @Composable fun CursorOverlay( position: Offset, visible: Boolean, scale: Float, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/DeckControlsPanel.kt b/app/src/main/java/com/droiddeck/launcher/ui/DeckControlsPanel.kt index 4bee3b6a8..b030632fc 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DeckControlsPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DeckControlsPanel.kt @@ -27,16 +27,6 @@ import kotlin.math.atan2 import kotlin.math.hypot import kotlin.math.min -/** - * The Steam Deck controller's back grips and trackpads, on the second screen. - * - * Four tabs: the grips as a 2x2 grid (left grips on the left, upper above lower, as they sit on a - * Deck); either trackpad on its own with a bar to click it; and both trackpads with the grips in a - * row beneath. Given the [pad] (the unfolded screen's panel), a fifth: a whole gamepad, each side - * an analog stick or a trackpad as the player picks. A trackpad clicks from its bar or from a second finger while the first is down. - * Drawn for OLED: true black, with outlines rather than filled surfaces, and the Steam blue only - * where something is being touched. - */ @SuppressLint("ViewConstructor") class DeckControlsPanel( context: Context, @@ -61,8 +51,6 @@ class DeckControlsPanel( override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) - // Held flat the gamepad has no Panels button, so another tab would have no way back: - // turning the phone from upright onto its side returns to the gamepad. if (pad != null && selectedWithPad != Tab.GAMEPAD && !GamepadGeometry.tall(w.toFloat(), h.toFloat())) { post { if (selectedWithPad != Tab.GAMEPAD) select(Tab.GAMEPAD) } } @@ -115,7 +103,6 @@ class DeckControlsPanel( view.setTextColor(if (on) ON_ACCENT else TEXT_DIM) } content.removeAllViews() - // The gamepad needs every row of the panel; held upright its Panels button brings the tabs back. topBar.visibility = if (tab == Tab.GAMEPAD) View.GONE else View.VISIBLE content.addView( when (tab) { @@ -159,7 +146,6 @@ class DeckControlsPanel( addView(TrackpadView(context, right = true), LayoutParams(0, LayoutParams.MATCH_PARENT, 1f).apply { leftMargin = dp(6) }) } addView(pads, LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f).apply { bottomMargin = dp(12) }) - // Mirrored, as hands hold them: the upper grips outside, the lower ones in the middle. val grips = LinearLayout(context).apply { orientation = HORIZONTAL val row = listOf("L4" to DeckControls.L4, "L5" to DeckControls.L5, "R5" to DeckControls.R5, "R4" to DeckControls.R4) @@ -173,15 +159,6 @@ class DeckControlsPanel( addView(grips, LayoutParams(LayoutParams.MATCH_PARENT, dp(84))) } - /** - * A whole pad, placed for two thumbs rather than in columns (see [GamepadGeometry]): on each side - * a surface where the thumb rests - most of play is on the sticks or trackpads - and a short - * slide inward along its arc, the d-pad on the left and the face buttons on the right; the bumpers and triggers above, at the top of the arc; View, Steam, QAM and Menu in the - * middle, which a thumb only reaches on purpose. Each surface is an analog stick or a Steam - * trackpad, switched from its corner and remembered (SessionPrefs.gamepadSurface). Its tab bar - * is hidden; held upright, a Panels button brings the other tabs back, and held flat there is no - * room for one. - */ private fun gamepad(pad: PadBridge): View = GamepadLayout(context).apply { fun pill(label: String, write: (PadState, Boolean) -> Unit) = PadButton(context, pad, label, write) place(surfaceSlot(pad, right = false), GamepadGeometry.Slot.LEFT_MAIN) @@ -215,13 +192,11 @@ class DeckControlsPanel( } } - /** One side's stick or trackpad, with a chip in its outer corner that swaps the two. */ private fun surfaceSlot(pad: PadBridge, right: Boolean): View = FrameLayout(context).apply { val slot = this fun show() { slot.removeAllViews() val stick = SessionPrefs.gamepadSurface(context, right) == SessionPrefs.SURFACE_STICK - // No caption: the switch in its corner says which it is. slot.addView(if (stick) StickView(context, pad, right) else TrackpadView(context, right, captioned = false), FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) val chip = TextView(context).apply { @@ -239,7 +214,6 @@ class DeckControlsPanel( show() } } - // In the corner away from the thumb, so play does not swap it by accident. slot.addView(chip, FrameLayout.LayoutParams(dp(30), dp(30), (if (right) Gravity.START else Gravity.END) or Gravity.TOP).apply { setMargins(dp(6), dp(6), dp(6), dp(6)) }) } @@ -280,7 +254,6 @@ class DeckControlsPanel( private fun dp(value: Int): Int = (value * resources.displayMetrics.density + 0.5f).toInt() - /** A surface drawn the same way everywhere on the panel; pressed, it lights up in the accent. */ private abstract class Surface(context: Context) : View(context) { protected val density = resources.displayMetrics.density protected val rect = RectF() @@ -307,7 +280,6 @@ class DeckControlsPanel( } } - /** A held button: down while a finger is on it. */ private open class HoldButton( context: Context, private val label: String, @@ -353,10 +325,6 @@ class DeckControlsPanel( private class ClickBar(context: Context, right: Boolean) : HoldButton(context, context.getString(R.string.deck_click), if (right) context.getString(R.string.deck_right_trackpad) else context.getString(R.string.deck_left_trackpad), { DeckControls.setClick(right, it) }) - /** - * One trackpad: where the first finger is, as the Deck reports it (-1..1, y up), and a click - * while a second finger is down. - */ private class TrackpadView(context: Context, private val right: Boolean, private val captioned: Boolean = true) : Surface(context) { private var touching = false private var clicked = false @@ -396,7 +364,6 @@ class DeckControlsPanel( } MotionEvent.ACTION_POINTER_DOWN -> click(true) MotionEvent.ACTION_POINTER_UP -> { - // The finger that lifted may be the first; the pad follows whichever stays. val stays = if (event.actionIndex == 0) 1 else 0 touch(event.getX(stays), event.getY(stays)) click(event.pointerCount - 1 > 1) @@ -428,7 +395,6 @@ class DeckControlsPanel( } } - /** A pad button held while a finger is on it, written into the shared pad as the on-screen pad does. */ private class PadButton( context: Context, private val pad: PadBridge, @@ -441,11 +407,6 @@ class DeckControlsPanel( } } - /** - * An analog stick that centres where the thumb lands, so it is never missed: the knob follows - * the finger from there out to the ring. Tapping it again and holding (within [CLICK_MS]) is - * the stick's click - L3 or R3 - for as long as the thumb stays down. - */ private class StickView(context: Context, private val pad: PadBridge, private val right: Boolean) : Surface(context) { private var touching = false private var clicking = false @@ -475,7 +436,6 @@ class DeckControlsPanel( val r = reach().coerceAtLeast(1f) when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { - // Centred where the thumb lands, but kept far enough in for the ring to fit. originX = event.x.coerceIn(r, (width - r).coerceAtLeast(r)) originY = event.y.coerceIn(r, (height - r).coerceAtLeast(r)) touching = true @@ -525,10 +485,9 @@ class DeckControlsPanel( } } - /** The d-pad: the direction is the finger's angle from the centre, diagonals included. */ private class DirectionPad(context: Context, private val pad: PadBridge) : View(context) { private val density = resources.displayMetrics.density - private var held = 0 // bits: 1 up, 2 right, 4 down, 8 left + private var held = 0 private val arm = Paint(Paint.ANTI_ALIAS_FLAG) private val rect = RectF() @@ -558,7 +517,6 @@ class DeckControlsPanel( val dx = event.getX(0) - width / 2f val dy = event.getY(0) - height / 2f if (hypot(dx, dy) < min(width, height) * 0.08f) 0 else { - // Eight sectors of 45 degrees, starting with right and going clockwise. val sector = ((Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())) + 360 + 22.5) % 360 / 45).toInt() intArrayOf(2, 2 or 4, 4, 4 or 8, 8, 8 or 1, 1, 1 or 2)[sector] } @@ -585,7 +543,6 @@ class DeckControlsPanel( } } - /** A, B, X and Y in a diamond, as on a Deck; several fingers can hold several at once. */ private class FaceButtons(context: Context, private val pad: PadBridge) : View(context) { private val buttons = listOf(PadState.Y to "Y", PadState.B to "B", PadState.A to "A", PadState.X to "X") private var held = emptySet<Int>() @@ -595,7 +552,6 @@ class DeckControlsPanel( typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } - /** Top, right, bottom, left - the centre of each, and the radius of all. */ private fun layout(): Pair<List<Pair<Float, Float>>, Float> { val cx = width / 2f val cy = height / 2f @@ -626,7 +582,6 @@ class DeckControlsPanel( if (event.actionMasked == MotionEvent.ACTION_POINTER_UP && p == event.actionIndex) continue val x = event.getX(p) val y = event.getY(p) - // The nearest button within reach, so a thumb between two still lands on one. val nearest = centres.indices.minByOrNull { hypot(centres[it].first - x, centres[it].second - y) } ?: continue if (hypot(centres[nearest].first - x, centres[nearest].second - y) <= radius * 1.6f) now.add(buttons[nearest].first) } @@ -649,7 +604,6 @@ class DeckControlsPanel( } } - /** Lays its controls out where [GamepadGeometry] puts them for the panel's size. */ private class GamepadLayout(context: Context) : ViewGroup(context) { private val slots = LinkedHashMap<View, GamepadGeometry.Slot>() @@ -686,12 +640,9 @@ class DeckControlsPanel( } companion object { - /** A second press on a stick this soon after the last is its click (L3, R3). */ private const val CLICK_MS = 300L - /** The tab last shown, kept for the session so reopening the panel lands where it was. */ private var selected = Tab.BOTH - /** The same for the unfolded screen's panel, which opens on the gamepad. */ private var selectedWithPad = Tab.GAMEPAD private const val CORNER = 20f diff --git a/app/src/main/java/com/droiddeck/launcher/ui/DesktopPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/DesktopPage.kt index 9d52bdf50..e1068a53c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DesktopPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DesktopPage.kt @@ -93,12 +93,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -// The Desktop tab in the Steam tab's shape: a tilted wall of little windows behind the word and -// one big Open desktop, with Play's lip and lifted sheet under it (Display, Controls, Session and -// Files). Under the hero the page scrolls to two shelves - the apps that run in the desktop, and -// what can be added - and the hero folds into a slim header as it goes. -/** The desktop's own chips: its session settings, then Files. */ private val DESKTOP_TABS = modeSheetTabs(steam = false) + SheetTab.FILES @Composable @@ -108,7 +103,6 @@ internal fun DesktopPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie val narrow = LocalNarrowPane.current val colors = MaterialTheme.colorScheme LaunchedEffect(Unit) { UserAppsState.refresh(ctx) } - // Flathub's installed apps sit with the others on the dock. LaunchedEffect(s.ready) { com.droiddeck.launcher.store.StoreState.refresh(ctx) } val installed = s.emulators.filter { it.installed } val apps = UserAppsState.items @@ -116,14 +110,11 @@ internal fun DesktopPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie val mode = SessionService.MODE_DESKTOP val tabs = DESKTOP_TABS.map { it to sheetTabLabel(it) } val open = if (s.desktopInstalled) stringResource(R.string.content_open_desktop) else stringResource(R.string.content_install_open_desktop) - // Opened from a card: the page floods out of it, its picture on the tile. fun openFrom(at: Rect?, art: Any?, page: String) { at?.let { PageOrigin.mark(Origin(it, 16f * density.density, art = art)) } onSelect(page) } - // The Steam tab's shape, the whole pane: the dock of apps along the top of the wall, the word - // and Open desktop at the foot, and nothing under the settings sheet but the hero itself. LiftHero( key = "desktop", tabs = tabs, onOpen = a.onDesktopSettings, onPrimary = a.onDesktop, @@ -134,15 +125,12 @@ internal fun DesktopPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie }, scrim = { lifted -> WallFade() - // Over the fades, so the cards read at full strength; gone as the sheet lifts, so - // nothing but the wall is behind it. Box(Modifier.graphicsLayer { val v = lifted(); alpha = 1f - v; translationY = -12.dp.toPx() * v }) { DesktopDock(s, a, installed, apps, openFrom = ::openFrom) } }, words = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - // The word at the wordmark's size. Text( stringResource(R.string.rail_desktop), fontSize = if (narrow) 30.sp else 42.sp, lineHeight = if (narrow) 34.sp else 46.sp, fontWeight = FontWeight.Black, color = colors.onBackground, @@ -151,13 +139,10 @@ internal fun DesktopPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie } }, button = { click -> - // Enabled without a runtime or the desktop: the session's loading screen installs them first. PrimaryButton(open, enabled = !s.busy, main = true, large = true, icon = Icons.Outlined.DesktopWindows, onClick = click) }, beside = { - // Flathub, whose apps run in the desktop: its storefront floods out of this button. HeroIconButton(Icons.Outlined.Storefront, stringResource(R.string.flathub_name), FlathubTeal, "desktop:flathub") { at -> openFrom(at, null, "store") } - // Add your own app or game. HeroIconButton(Icons.Filled.Add, stringResource(R.string.user_apps_add), null, "desktop:add") { if (!s.busy) adding = true } }, rows = { tab -> @@ -168,11 +153,6 @@ internal fun DesktopPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie if (adding && !s.busy) AddAppDialog(s.ready, onDismiss = { adding = false }) { request, label -> UserAppsState.add(ctx, request, label) } } -/** - * The dock along the top of the Desktop hero: what runs in the desktop (emulators installed, apps - * added, Flathub's installed apps), then the emulators that can be added - one - * row of cards that scrolls sideways, over the wall. It sinks back with the wall as the sheet lifts. - */ @Composable private fun DesktopDock( s: FrontEndState, a: FrontEndActions, installed: List<Library.Emulator>, apps: List<UserApps.App>, @@ -184,14 +164,12 @@ private fun DesktopDock( val available = s.emulators.filter { !it.installed } val flatpaks = com.droiddeck.launcher.store.StoreState.installed val keys = installed.map { "emu:${it.id}" } + apps.map { "user:${it.key}" } + flatpaks.map { "flatpak:${it.id}" } - // Where the last of your apps sits, for a new emulator's dot to land after it. var lastAt by remember { mutableStateOf<Rect?>(null) } var dockAt by remember { mutableStateOf<Rect?>(null) } val seen = remember { mutableSetOf<String>() } val mounted = remember { booleanArrayOf(false) } LaunchedEffect(Unit) { mounted[0] = true } val scroll = rememberScrollState() - // Nothing yet (the emulators' catalog still on its way): no heading over an empty row. if (keys.isEmpty() && available.isEmpty() && UserAppsState.working == null && UserAppsState.lastError == null) return Column(Modifier.fillMaxWidth().padding(top = if (narrow) 14.dp else 22.dp)) { Text( @@ -205,7 +183,6 @@ private fun DesktopDock( ) { keys.forEachIndexed { i, k -> key(k) { - // A card that arrives after the dock is up (an emulator just installed) opens in. val state = remember { MutableTransitionState(!mounted[0] || k in seen) }.apply { targetState = true } LaunchedEffect(Unit) { seen += k } AnimatedVisibility(state, enter = expandHorizontally(Motion.sp(0.75f)) + fadeIn(Motion.tw(300, 80))) { @@ -224,7 +201,6 @@ private fun DesktopDock( k, flatpak.name, flatpak.summary ?: stringResource(R.string.flathub_name), art = flatpak.icon ?: com.droiddeck.launcher.store.FlathubApi.iconUrl(flatpak.id), first = i == 0, ) { at -> - // Its page on Flathub: open, update or remove it there. com.droiddeck.launcher.store.StoreState.openRequest = flatpak.id openFrom(at, flatpak.icon, "store") } else if (app != null) DesktopCard(k, app.name, stringResource(app.kind.label()), art = app.icon, fallback = if (app.kind == UserApps.Kind.SCRIPT) Icons.Outlined.Terminal else Icons.Outlined.Apps, first = i == 0) { at -> @@ -234,7 +210,6 @@ private fun DesktopDock( } } } - // Between yours and what can be added: a thin rule. if (keys.isNotEmpty()) Box(Modifier.padding(horizontal = 4.dp).width(1.dp).height(120.dp).background(pal.line2)) available.forEach { e -> key(e.id) { @@ -247,8 +222,6 @@ private fun DesktopDock( art = e.iconRes, badge = if (busyHere) null else stringResource(R.string.comp_get), progress = if (busyHere) s.packagePercent else null, first = keys.isEmpty() && e == available.first(), ) { at -> - // Get installs it there and then, and a dot flies to where its card will be; - // when it cannot (no runtime, a session running), its page says why. if (canGet && pkg != null && at != null) { a.onInstallPackage(pkg.id) val end = lastAt @@ -268,10 +241,6 @@ private fun DesktopDock( } } -/** - * A square button the big one's height beside it on the hero: an icon in [tint] (the palette's - * signal when null). [onClick] gets its bounds, for a page to flood out of. - */ @Composable private fun HeroIconButton(icon: androidx.compose.ui.graphics.vector.ImageVector, description: String, tint: Color?, id: String, onClick: (Rect?) -> Unit) { val colors = MaterialTheme.colorScheme @@ -295,10 +264,6 @@ private fun HeroIconButton(icon: androidx.compose.ui.graphics.vector.ImageVector ) { Icon(icon, null, tint = accent, modifier = Modifier.size(26.dp)) } } -/** - * The hero's wall: little windows tilted and drifting as Steam's capsules do, each a window with a - * bar along its foot and an emulator's or app's icon inside; blank windows when there are none. - */ @Composable private fun DesktopWall(icons: List<Any>) { val pal = LocalPalette.current @@ -308,11 +273,6 @@ private fun DesktopWall(icons: List<Any>) { } } -/** - * One thing that runs in the desktop: its icon over a blurred, darkened copy of itself, a status on - * the art (Get, or how far an install is), and under it its name on up to two lines and what it - * is. [onClick] gets the art's bounds, for a page to flood out of or a dot to fly from. - */ @Composable private fun DesktopCard( key: String, name: String, detail: String, art: Any?, @@ -384,10 +344,6 @@ private fun DesktopCard( } } -/** - * The desktop sheet's Files chip: the file manager and the ROMs folder (both from Setup's old - * tools), how much the desktop takes, and Uninstall - its only home - which asks in red first. - */ @Composable private fun DesktopFiles(s: FrontEndState, a: FrontEndActions) { val ctx = LocalContext.current @@ -408,7 +364,6 @@ private fun DesktopFiles(s: FrontEndState, a: FrontEndActions) { } } -/** Uninstall the desktop: are you sure steps out of it in red, Keep first and focused. */ @Composable private fun UninstallDesktop(enabled: Boolean, onUninstall: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -435,7 +390,6 @@ private fun UninstallDesktop(enabled: Boolean, onUninstall: () -> Unit) { } } -/** The add or remove under way, or why the last one failed. */ @Composable private fun UserAppsProgressLine() { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt index 1e62c42e3..3d3994384 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt @@ -55,13 +55,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -/** - * One driver list as a full page - the Runtime driver or the Display driver - opened from the - * session's settings page. Installed drivers are picked by tapping the row and each carries a trash - * can (the "Auto"/"Runtime default" row is a setting, not a driver, so it has none); what the last - * check of the release repos found is offered below; the refresh button beside the title is the - * only thing that goes online. - */ @Composable fun DriverPage( title: String, @@ -85,16 +78,13 @@ fun DriverPage( val colors = MaterialTheme.colorScheme val pal = LocalPalette.current var confirm by remember { mutableStateOf<DriverRow?>(null) } - // Back (the controller's B too) closes this page only, back to the session's settings. BackHandler(onBack = onBack) - // D-pad: up from the first driver lands on refresh; the page opens with the driver in use focused. val refreshFocus = remember { FocusRequester() } val selectedFocus = remember { FocusRequester() } val inputMode = LocalInputModeManager.current var pageFocused by remember { mutableStateOf(false) } LaunchedEffect(inputMode.inputMode) { if (inputMode.inputMode != InputMode.Keyboard) return@LaunchedEffect - // The chooser holds controller focus until its closing animation finishes. snapshotFlow { Steps.step }.first { it == null } focusWithinFrames({ pageFocused }) { selectedFocus } } @@ -162,7 +152,6 @@ fun DriverPage( fontSize = 13.sp, ) }, - // Opens on Cancel, so a stray A press on a controller never deletes anything. confirmButton = { FocusText(stringResource(R.string.common_delete), colors.error) { confirm = null; onDelete(row.id) } }, dismissButton = { val cancelFocus = remember { FocusRequester() } @@ -180,10 +169,6 @@ private fun InstalledRow(row: DriverRow, selected: Boolean, onSelect: () -> Unit val src = remember { MutableInteractionSource() } val hot = src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value val shape = RoundedCornerShape(10.dp) - // Two focus targets side by side - the driver itself, and its trash can - so a controller's - // d-pad right reaches the trash can and left comes back. (Inside one clickable row the trash - // can was a child of the focused area and "right" had nowhere to go.) The row's outline still - // follows the driver part, so it looks the same. val mainFocus = remember { FocusRequester() } val delFocus = remember { FocusRequester() } Row( @@ -236,7 +221,6 @@ private fun InstalledRow(row: DriverRow, selected: Boolean, onSelect: () -> Unit Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line)) } -/** A text button that shows where controller focus is: an outline in its own colour. */ @Composable internal fun FocusText(text: String, color: Color, modifier: Modifier = Modifier, onClick: () -> Unit) { val src = remember { MutableInteractionSource() } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/EmptyLibrary.kt b/app/src/main/java/com/droiddeck/launcher/ui/EmptyLibrary.kt index a605c38e9..c29d3abd4 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/EmptyLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/EmptyLibrary.kt @@ -45,14 +45,6 @@ import kotlin.math.max import kotlin.math.min import kotlin.math.sin -/** - * The Steam tab with nothing installed. The blank wall drifts as the full one does, and the - * DroidDeck mark wanders over it, bouncing off the page's edges and off [avoid] (the words and - * buttons) like an idle DVD logo, carrying a clearing in the wall with it. On arrival its outline - * traces in, the D fills, and the ball pops toward the viewer, flips twice and slams back into its - * socket; the ring the slam sends out fills each blank capsule it reaches. Poking the ball plays - * the pop again, without the mark stopping. [fade] is drawn over the wall and under the mark. - */ @Composable internal fun EmptyLibrary(avoid: Rect?, fade: @Composable () -> Unit) { val clock = rememberMarkClock(avoid) @@ -63,23 +55,16 @@ internal fun EmptyLibrary(avoid: Rect?, fade: @Composable () -> Unit) { } } -// The trace and fill, in ms from arrival. private const val TRACE_MS = 1250f private const val FILL_FROM = 450f -// The ball's timeline: its fade-in, pop, flip and slam, the ripple and the capsules it lights. private const val POP_FROM = 900f private const val IMPACT = 2200f private const val RIPPLE_MS = 1250f private const val HIT_MS = 900f private const val END = 3600f -// Where a poke restarts the ball's timeline: just before the wind-up. private const val POKE = 880f private const val DRIFT_MS = 75_000f -/** - * Time into the mark's arrival, into the ball's timeline (a poke restarts only that), into the - * wall's drift, and where the mark is on the page and where its last slam landed. - */ private class MarkClock { var intro by mutableFloatStateOf(0f) var ball by mutableFloatStateOf(0f) @@ -107,7 +92,6 @@ private fun rememberMarkClock(avoid: Rect?): MarkClock { if (clock.page == Size.Zero) { withFrameMillis { }; continue } val f = MarkFrame(clock.page, Offset.Zero) if (clock.at == Offset.Unspecified) clock.at = Offset(clock.page.width * 0.64f, clock.page.height * 0.42f) - // Animations off in the system settings: the finished mark, still, on a still wall. if (Motion.scale == 0f) { clock.intro = END clock.ball = END @@ -123,7 +107,6 @@ private fun rememberMarkClock(avoid: Rect?): MarkClock { val before = clock.ball clock.ball += dt - // Drift, bouncing off the page edges and off whatever it should keep clear of. var x = clock.at.x + vx * dt / 1000f var y = clock.at.y + vy * dt / 1000f val hw = f.halfWidth @@ -152,10 +135,6 @@ private fun rememberMarkClock(avoid: Rect?): MarkClock { return clock } -/** - * The mark on a page of [size], centred on [centre]: about a third of the page tall. [k] scales - * the timeline's distances, which were drawn on a 460px mark. - */ private class MarkFrame(size: Size, val centre: Offset) { val height = min(size.height * 0.3f, size.width * 0.28f * LogoShape.HEIGHT / LogoShape.WIDTH) val s = height / LogoShape.HEIGHT @@ -180,11 +159,9 @@ private val HitEase = CubicBezierEasing(0.2f, 0.7f, 0.3f, 1f) private fun clamp01(v: Float) = v.coerceIn(0f, 1f) private fun mix(a: Float, b: Float, t: Float) = a + (b - a) * t -/** One stretch of a keyframed value: [from] to [to] while [p] runs from [a] to [b]. */ private fun span(p: Float, a: Float, b: Float, from: Float, to: Float, ease: Easing) = mix(from, to, ease.transform(clamp01((p - a) / (b - a)))) -/** When the ripple's edge reaches [progress] of its way out, as a share of the ripple's time. */ private val rippleTime: FloatArray = FloatArray(257) { RippleEase.transform(it / 256f) } private fun rippleReaches(progress: Float): Float { var lo = 0 @@ -196,14 +173,12 @@ private fun rippleReaches(progress: Float): Float { return lo / 256f } -/** How lit a capsule the ripple reached [since] ms ago is: up fast, then fading back. */ private fun hitAmount(since: Float): Float { if (since < 0f || since > HIT_MS) return 0f val p = since / HIT_MS return if (p < 0.14f) HitEase.transform(p / 0.14f) else 1f - HitEase.transform((p - 0.14f) / 0.86f) } -/** The blank wall: the same tilted, drifting columns as [CapsuleWall], each capsule lit as the ripple passes. */ @Composable private fun EmptyWall(clock: MarkClock) { val pal = LocalPalette.current @@ -239,7 +214,6 @@ private fun EmptyWall(clock: MarkClock) { val dy = if (c % 2 == 0) -shift else shift - run for (i in 0 until perRun * 2) { val y = top + i * (capH + gap) + dy - // The capsule's centre on the page, out of the wall's tilt. val lx = x + capW / 2 - pivot.x val ly = y + capH / 2 - pivot.y val sx = pivot.x + lx * cs + ly * sn @@ -266,7 +240,6 @@ private fun EmptyWall(clock: MarkClock) { } } } - // The clearing the mark carries with it. drawRect( Brush.radialGradient( 0f to bg, 255f / 420f to bg, 330f / 420f to bg.copy(alpha = 0.7f), 1f to Color.Transparent, @@ -276,7 +249,6 @@ private fun EmptyWall(clock: MarkClock) { } } -/** The mark, its ball's pop and flip, and the ring the slam sends out. */ @Composable private fun Mark(clock: MarkClock) { val pal = LocalPalette.current @@ -303,7 +275,6 @@ private fun Mark(clock: MarkClock) { val t = clock.ball val intro = clock.intro - // The ripple, from where the slam landed, under the mark. if (clock.slam != Offset.Unspecified && t in IMPACT..IMPACT + RIPPLE_MS) { val p = (t - IMPACT) / RIPPLE_MS val alpha = if (p < 0.7f) mix(0.9f, 0.35f, p / 0.7f) else mix(0.35f, 0f, (p - 0.7f) / 0.3f) @@ -323,7 +294,6 @@ private fun Mark(clock: MarkClock) { } else 0f translate(0f, shake) { - // The D, as flat raised slabs with a solid edge underneath. val fill = Out.transform(clamp01((intro - FILL_FROM) / 450f)) if (fill > 0f) { val lift = 8f * k * (1f - fill) @@ -338,7 +308,6 @@ private fun Mark(clock: MarkClock) { drawPath(right, ink.copy(alpha = fill)) } - // The trace around both halves on arrival, the GameCube nod. if (intro < TRACE_MS) { val drawn = TraceEase.transform(clamp01(intro / 775f)) val alpha = if (intro < 775f) 1f else 1f - (intro - 775f) / 475f @@ -352,7 +321,6 @@ private fun Mark(clock: MarkClock) { } } - // The ball. val shown = Out.transform(clamp01((intro - 650f) / 300f)) if (shown > 0f) { val p = (t - POP_FROM) / (IMPACT - POP_FROM) @@ -366,7 +334,6 @@ private fun Mark(clock: MarkClock) { val q = clamp01((pop - 1f) / 0.6f) val bob = if (t > END) -4f * k * (0.5f - 0.5f * cos(2f * PI.toFloat() * (t - END) / 5200f)) else 0f - // Its shadow stays on the slabs, spreading and softening as the ball rises. val blur = mix(10f, 28f, q) * k val sr = f.ball * 0.93f * mix(1f, 1.4f, q) val dark = Color.Black.copy(alpha = mix(0.6f, 0.25f, q) * shown) @@ -396,7 +363,6 @@ private fun Mark(clock: MarkClock) { } } - // The flash where it lands. if (t in IMPACT..IMPACT + 420f) { val p = Out.transform((t - IMPACT) / 420f) drawCircle(pal.signal.copy(alpha = 1f - p), (f.ball + 6f * k) * mix(1f, 1.4f, p), f.centre, style = Stroke(max(4f * k, 2.dp.toPx()))) @@ -405,10 +371,6 @@ private fun Mark(clock: MarkClock) { } } -/** - * The ball as a coin of [thick]ness turned [deg] about its vertical axis: flat faces in the logo's - * blue, and between them a solid rim that shows as it turns edge-on. - */ private fun DrawScope.coin(c: Offset, r: Float, thick: Float, deg: Float, face: Color, rim: Color) { val a = Math.toRadians(deg.toDouble()) val cs = cos(a).toFloat() diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ExePickerDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/ExePickerDialog.kt index cda22fc69..901bf6485 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ExePickerDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ExePickerDialog.kt @@ -62,13 +62,11 @@ import com.droiddeck.launcher.session.GameStorage import com.droiddeck.launcher.stores.formatBytes import java.io.File -/** What a picker lists besides folders. */ internal enum class PickKind(val extensions: Set<String>) { EXE(setOf("exe")), IMAGE(setOf("png", "jpg", "jpeg", "webp", "ico")), } -/** A place a file can be picked from: internal storage or a card, at its root. */ internal class PickVolume(val root: File, val label: String, val removable: Boolean) { val free: Long get() = runCatching { StatFs(root.path).availableBytes }.getOrDefault(0L) } @@ -79,15 +77,10 @@ internal fun pickVolumes(context: Context): List<PickVolume> = File(o.path.substringBefore("/Android/")).takeIf { it.isDirectory }?.let { PickVolume(it, o.name, removable = true) } }.distinctBy { it.root.absolutePath } -/** A folder in a listing; [readable] false = shown greyed, not opened. */ internal class PickFolder(val dir: File, val readable: Boolean) internal class PickListing(val folders: List<PickFolder>, val files: List<File>) -/** - * Every folder of [dir] (dot-folders left out, the unreadable ones marked), then the files [kind] - * takes; each A-Z. - */ internal fun pickListing(dir: File, kind: PickKind): PickListing { val all = dir.listFiles()?.filter { !it.name.startsWith(".") }.orEmpty().sortedBy { it.name.lowercase() } return PickListing( @@ -96,13 +89,6 @@ internal fun pickListing(dir: File, kind: PickKind): PickListing { ) } -/** - * The compact in-app file picker: [title] and a red X on one line; the storage rows (internal - * storage, each card, free space); inside one, its root first, a breadcrumb, "..", every folder - * and the [kind] files, scrolling in the dialog. Its width is what the first step needed and stays; - * long names are cut short. B / Back goes up a folder, and closes from the storage rows. [start] - * opens in a folder. [onAddAll] adds a row on top of a folder with subfolders. - */ @Composable internal fun ExePickerDialog( title: String, kind: PickKind = PickKind.EXE, start: File? = null, @@ -165,7 +151,6 @@ internal fun ExePickerDialog( } } -/** A compact dialog's title line: [leading] (a back arrow), the title, a small red X. */ @Composable internal fun DialogTitleRow(title: String, onClose: () -> Unit, leading: (@Composable () -> Unit)? = null) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) { @@ -175,7 +160,6 @@ internal fun DialogTitleRow(title: String, onClose: () -> Unit, leading: (@Compo } } -/** A 32 dp icon button for a dialog's title line. */ @Composable internal fun SmallIconButton(icon: ImageVector, description: String, tint: androidx.compose.ui.graphics.Color, onClick: () -> Unit) { val src = remember { MutableInteractionSource() } @@ -193,7 +177,6 @@ internal fun SmallIconButton(icon: ImageVector, description: String, tint: andro } } -/** One row of a compact dialog's list, the Install dialog's style: [content] laid out in a row. */ @Composable internal fun ListRow( enabled: Boolean = true, accent: Boolean = false, tall: Boolean = false, focus: FocusRequester? = null, @@ -217,7 +200,6 @@ internal fun ListRow( ) { content(hot) } } -/** A list row with an icon, a name (cut short when long) and a detail on the right. */ @Composable internal fun PickerRow( icon: ImageVector?, name: String, detail: String?, tall: Boolean = false, file: Boolean = false, accent: Boolean = false, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FitText.kt b/app/src/main/java/com/droiddeck/launcher/ui/FitText.kt index 77bdbcada..5935931da 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FitText.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FitText.kt @@ -16,12 +16,6 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp -/** - * One line of text in a slot of fixed width - a rail label, a key cap - set as large as - * [maxSize] when it fits and smaller, down to [minSize], when it does not. Labels are as long as - * their language makes them ("Components" is "コンポーネント", "Componentes", "구성 요소"), so a - * fixed size cuts some of them off; below [minSize] the end is ellipsized rather than clipped. - */ @Composable fun FitText( text: String, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FocusGlide.kt b/app/src/main/java/com/droiddeck/launcher/ui/FocusGlide.kt index 9f8f0b0fe..0bc1ccc47 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FocusGlide.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FocusGlide.kt @@ -48,27 +48,19 @@ import kotlin.math.abs import kotlin.math.hypot import kotlinx.coroutines.coroutineScope -// One focus ring for a whole screen that slides from control to control. When focus moves, the -// edge on the side it is heading springs ahead and the trailing edge catches up a beat later, so -// the ring stretches like a drop and snaps into the new control's shape. -/** A control that can show the ring: where it is and what its ring looks like there. */ internal class GlideSource { var coords: LayoutCoordinates? = null var shape: Shape by mutableStateOf<Shape>(androidx.compose.ui.graphics.RectangleShape) var color by mutableStateOf(Color.Transparent) var width by mutableStateOf(2.dp) - /** Focuses the control again: a step-out hands focus back to the control it came out of. */ val requester = androidx.compose.ui.focus.FocusRequester() } internal class FocusGlide(val view: View) { var host: LayoutCoordinates? = null - /** Controls that are hot, the latest last: focus and a hovering pointer can each hold one. */ val hot = mutableStateListOf<GlideSource>() - /** Bumped when the current control moves (a scroll, a relayout). */ var moves by mutableIntStateOf(0) - /** Off while a page floods in or out (PageFlood); back on, it appears on whatever has focus. */ var hidden by mutableStateOf(false) val current: GlideSource? get() = hot.lastOrNull() @@ -77,7 +69,6 @@ internal class FocusGlide(val view: View) { fun release(s: GlideSource) { hot.remove(s) } fun moved(s: GlideSource) { if (hot.lastOrNull() === s) moves++ } - /** [s]'s bounds in the host, cut to what is on screen; null when it is not laid out or is scrolled away. */ fun boundsOf(s: GlideSource): Rect? { val h = host ?: return null val c = s.coords ?: return null @@ -89,12 +80,10 @@ internal class FocusGlide(val view: View) { internal val LocalFocusGlide = staticCompositionLocalOf<FocusGlide?> { null } -/** Floods on screen (a launch, a stop, the return from one): every ring keeps out of the way of them. */ internal object RingVeil { var count by mutableIntStateOf(0) } -/** Keeps the focus ring off while this is in the composition. */ @Composable internal fun VeilRing() { DisposableEffect(Unit) { @@ -110,11 +99,6 @@ private fun cornerOf(shape: Shape, size: Size, dir: LayoutDirection, density: De is Outline.Generic -> minOf(size.width, size.height) / 2f } -/** - * A control's outline: [restColor] at [restWidth] always, and [hotColor] at [hotWidth] while [hot]. - * Under a [FocusGlideHost] the hot outline is the host's one sliding ring; elsewhere (a dialog, a - * popup, a screen with no host) it is drawn here, as a plain border. - */ @Composable internal fun Modifier.glideBorder( hot: Boolean, @@ -140,54 +124,34 @@ internal fun Modifier.glideBorder( return base.focusRequester(src.requester).onGloballyPositioned { src.coords = it; glide.moved(src) } } -/** Leading edge on a focus move: quick and a little loose. */ -/** A spring an edge chases its target on: damping ratio and stiffness, as Compose's. */ private class Edge(val damping: Float, val stiffness: Float) private fun lead() = Edge(0.62f, 700f) -/** Trailing edge: slower, settles without a bounce to speak of. */ private fun trail() = Edge(0.78f, 360f) -/** Following a control that moves under the ring (a scroll): tight, no wobble. */ private fun follow() = Edge(1f, 2400f) -/** How long after a focus move the ring keeps its stretchy springs and keeps checking where it is. */ private const val SETTLE_MS = 450L -/** How long the control must stand still before the ring stops checking where it is. */ private const val STILL_MS = 120L -/** The longest the ring keeps checking after a move, for a control that never stands still. */ private const val TRACK_MAX_MS = 2_000L -/** How strongly the ring tints what it holds. */ private const val RING_FILL = 0.14f -/** Moves closer together than this are a held direction: the ring runs along as one, no stretch. */ private const val REPEAT_MS = 180L -/** A move further than this goes as a droplet: pinched to a drop, carried across, opened onto the control. */ private val FAR = 360.dp -/** - * Hosts the sliding focus ring for everything inside it. The ring is drawn over [content], in the - * host's space, so put the host inside anything that moves as a whole (a sheet that slides in). - */ @Composable internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable () -> Unit) { val view = LocalView.current val density = LocalDensity.current val dir = LocalLayoutDirection.current val glide = remember(view) { FocusGlide(view) } - val edges = remember { List(4) { Animatable(0f) } } // left, top, right, bottom + val edges = remember { List(4) { Animatable(0f) } } val corner = remember { Animatable(0f) } - /** How solid the ring is: 1 while it travels as a drop, 0 (an outline) on a control. */ val solid = remember { Animatable(0f) } val alpha = remember { Animatable(0f) } val scope = rememberCoroutineScope() - // An edge each, the corner, a droplet carrying all of them, its solid fill, and the chase. val jobs = remember { arrayOfNulls<Job>(8) } - // The chase: each edge on a spring of its own toward where focus is now. A new target only - // moves where it is heading - restarting an animation on every focus move started it from - // its first frame (which does not move), so a held direction over a busy page (each rail item - // swaps the page) left the ring standing while focus ran on ahead. val target = remember { FloatArray(4) } val vel = remember { FloatArray(4) } val springs = remember { Array(4) { follow() } } @@ -206,15 +170,12 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable var delays = LongArray(4) var lastMove = 0L fun go(i: Int, v: Float, spec: Edge, wait: Long) { - // A drop cut short by another move turns back into an outline on the way. if (jobs[5]?.isActive == true || solid.targetValue > 0f) { jobs[5]?.cancel() jobs[6]?.cancel() jobs[6] = scope.launch { solid.animateTo(0f, Motion.tw(120)) } } val chasing = jobs[7]?.isActive == true - // An edge some other animation was moving (the appear, a drop's opening) is taken over - // at the speed it was going. jobs[i]?.let { if (it.isActive) { it.cancel(); vel[i] = edges[i].velocity } } if (!chasing) for (j in 0..3) if (jobs[j]?.isActive != true) vel[j] = edges[j].velocity target[i] = v @@ -225,7 +186,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable var last = withFrameNanos { it } while (true) { val frame = withFrameNanos { it } - // Real time, however long the frame took, in small steps so a stiff spring holds. val dt = ((frame - last) / 1e9f).coerceAtMost(0.1f) / Motion.scale.coerceAtLeast(0.01f) last = frame val now = System.nanoTime() @@ -251,7 +211,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable } } } - /** Pinch into a solid dot where it is, carry it over, and open it into [b] as it arrives. */ fun droplet(s: GlideSource, b: Rect) { jobs.forEach { it?.cancel() } val size = s.coords?.size?.let { Size(it.width.toFloat(), it.height.toFloat()) } ?: b.size @@ -260,19 +219,15 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable val r = with(density) { 6.dp.toPx() } fun dot(c: Offset) = listOf(c.x - r, c.y - r, c.x + r, c.y + r) val here = Offset((edges[0].value + edges[2].value) / 2f, (edges[1].value + edges[3].value) / 2f) - // Pinch into a solid drop where it is. coroutineScope { launch { solid.animateTo(1f, Motion.tw(80)) } launch { corner.animateTo(r, Motion.tw(90)) } dot(here).forEachIndexed { i, v -> launch { edges[i].animateTo(v, Motion.tw(90)) } } } - // Across as a dot, every edge together, on a timed ease, steering each frame for - // where the control is now: one still rising into place is met where it ends up. val distance = hypot(b.center.x - here.x, b.center.y - here.y) / density.density val travel = (180 + distance * 0.12f).toInt().coerceIn(220, 380) val p = Animatable(0f) val flight = launch { p.animateTo(1f, Motion.tw(travel, easing = FastOutSlowInEasing)) } - // Takes the control's shape as it arrives, not after: no resting as a dot. while (flight.isActive && p.value < 0.78f) { val to = (glide.boundsOf(s) ?: b).center dot(Offset(here.x + (to.x - here.x) * p.value, here.y + (to.y - here.y) * p.value)).forEachIndexed { i, v -> edges[i].snapTo(v) } @@ -316,8 +271,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable return@collectLatest } if (src == null) { - // Focus passing between two controls can let go of one a moment before the next - // takes it: wait a beat, so that reads as a move and not a fade out and back in. delay(60) jobs.forEach { it?.cancel() } solid.snapTo(0f) @@ -342,7 +295,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable shown = src first = false if (!wasShowing) { - // Appearing: a hair bigger, pulled in onto the control as it fades up. val out = with(density) { 3.dp.toPx() } val grown = listOf(b.left - out, b.top - out, b.right + out, b.bottom + out) jobs.forEach { it?.cancel() } @@ -352,7 +304,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable cornerTo(src, b, snap = true) scope.launch { alpha.animateTo(1f, Motion.tw(140)) } } else { - // Moving: the edges on the side it is heading lead. val dx = b.center.x - (edges[0].value + edges[2].value) / 2f val dy = b.center.y - (edges[1].value + edges[3].value) / 2f val horizontal = abs(dx) >= abs(dy) @@ -365,7 +316,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable dropped = !held && hypot(dx, dy) > with(density) { FAR.toPx() } if (dropped) droplet(src, b) else { - // A held direction runs along as one piece; a single press stretches. dirSpecs = if (held) List(4) { Edge(0.9f, 900f) } else List(4) { i -> when (i) { leadIdx -> lead(); trailIdx -> trail(); else -> Edge(0.7f, 520f) } } delays = LongArray(4) { i -> if (i == trailIdx && !held) 30L else 0L } @@ -376,15 +326,11 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable if (!dropped) listOf(b.left, b.top, b.right, b.bottom).forEachIndexed { i, v -> go(i, v, dirSpecs[i], delays[i]) } last = b } else if (b != last && jobs[5]?.isActive != true) { - // It moved under the ring: keep the stretch while the move is fresh, else follow. val fresh = (System.nanoTime() - movedAt) / 1_000_000 < SETTLE_MS listOf(b.left, b.top, b.right, b.bottom).forEachIndexed { i, v -> go(i, v, if (fresh) dirSpecs[i] else follow(), 0L) } if (last == null) scope.launch { alpha.animateTo(1f, Motion.tw(120)) } last = b } - // Layer animations (a tile growing, a row sliding, a page rising in) move it without - // a relayout, so look again each frame until it has stood still a moment: a fixed - // window let the ring park mid-rise and jump over when something next laid out. val now = System.nanoTime() val settled = (now - start) / 1_000_000 > SETTLE_MS && (now - stillSince) / 1_000_000 > STILL_MS if (settled || (now - start) / 1_000_000 > TRACK_MAX_MS) break @@ -411,8 +357,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable cornerRadius = CornerRadius(corner.value), alpha = a * f, ) if (f < 0.99f) { - // The ring's own infill: the only fill a pad sees, so it is always exactly what - // the ring holds and moves with it (a row's own sliding fill trailed it). drawRoundRect( color, topLeft = Offset(l + w / 2f, t + w / 2f), size = Size(sw, sh), cornerRadius = CornerRadius(rad), alpha = a * (1f - f) * RING_FILL, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrameGenMenu.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrameGenMenu.kt index 1ec0fbfad..816177bc1 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrameGenMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrameGenMenu.kt @@ -8,11 +8,6 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.gpu.FrameGen import com.droiddeck.launcher.gpu.Lossless -/** - * The frame generation picker, anchored to whatever opened it (host key "fg"). One copy for Setup, - * a game's launch settings and the session drawer, so they say the same thing. Importing a - * Lossless.dll is offered once the account is known to own Lossless Scaling. - */ @Composable internal fun FrameGenMenu( host: MenuHost, mode: FrameGen.Mode, lossless: Lossless.State, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndArt.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndArt.kt index 992e337a6..85386ca93 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndArt.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndArt.kt @@ -59,13 +59,10 @@ import java.io.File import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned -// Cover art: posters, the art grid and its tiles. -/** Where each art tile sits, root px, by its key: a page opened from one floods out of it. */ internal object TileBounds { val at = HashMap<String, androidx.compose.ui.geometry.Rect>() - /** The page about to open came from tile [key] (its art or icon for the flood), if it is on screen. */ fun mark(key: String, art: Any?, corner: Float) { at[key]?.let { PageOrigin.mark(Origin(it, corner, art = art)) } } @@ -97,12 +94,8 @@ internal fun Poster(art: File?, name: String, modifier: Modifier) { @Composable internal fun ArtGrid(tiles: List<Tile>, wide: Boolean = false) { val square = tiles.isNotEmpty() && tiles.all { it.art == null && (it.iconRes != null || it.iconBitmap != null) } - // Big enough to recognise a game by its art and read its name under it; icon tiles are squares. val minSize = if (square) 76.dp else if (wide) 120.dp else 96.dp val gap = 12.dp - // Laid out whole, not lazily: the pad's focus search only finds tiles that exist, and a lazy - // grid composes only the rows on screen, so a press towards the next row bounced back among - // the visible tiles. A few hundred tiles lay out fine; the page's scroll follows the focused one. BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { val avail = maxWidth - 8.dp val cols = ((avail + gap) / (minSize + gap)).toInt().coerceAtLeast(1) @@ -152,10 +145,6 @@ private fun GameTile(t: Tile, wide: Boolean, square: Boolean, src: MutableIntera } } -/** - * Art for a portrait (2:3) tile. Box art fills it; wide art - a PS3 disc's ICON0, a game's header - - * is shown whole over a blurred, darkened copy of itself instead of losing its sides to the crop. - */ @Composable private fun CoverImage(art: File, modifier: Modifier) { var wideArt by remember(art) { mutableStateOf(false) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt index 764967860..db6850226 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt @@ -99,7 +99,6 @@ import com.droiddeck.launcher.runtime.UserApps import com.droiddeck.launcher.store.UserAppsState import java.io.File -// The front end's right-hand pane and the page it shows: backdrop, desktop card and emulators. @OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) @Composable @@ -119,35 +118,25 @@ internal fun Pane( } Backdrop(backdropArt) val pal = LocalPalette.current - // Pages keep drawing while they leave, so one can fold away instead of blinking out. val pages = remember { HashMap<String, @Composable () -> Unit>() } - // Pages opened from a control (the cog), with where that control sat, in this pane's coordinates. val origins = remember { HashMap<String, Origin>() } val paneAt = remember { arrayOf(Offset.Zero) } val livePage by rememberUpdatedState(s.pageKey) - // Picking another game changes the detail beside the list, not the whole page. val railContent = if (selected.startsWith("app:")) "games" else selected val target = if (page != null && s.pageKey != null) s.pageKey else railContent - // The game the Games page shows. It must outlive the selection: while the page animates out, - // drawing it with what is selected now (another rail item) turned it into a copy of that page. val gameShown = remember { arrayOf("games") } if (selected == "games" || selected.startsWith("app:")) gameShown[0] = selected - // What the pane showed before this change: the content a page opened over belongs to it. val before = remember { arrayOf(target) } if (page != null && s.pageKey != null) { pages[s.pageKey] = page if (s.pageKey !in origins) PageOrigin.take()?.let { o -> - // A hop's page came out of its own rail item, and is that item's content. origins[s.pageKey] = inPane(o.translate(-paneAt[0])).let { if (it.owner == null) it.ownedBy(railContent) else it } } } else if (target != before[0] && target !in origins) { - // A section's own page opened from a control in it (an emulator's card, a ROM's) floods - // out of that control and belongs to what was showing; a hop's comes out of its rail item. PageOrigin.take()?.let { o -> origins[target] = inPane(o.translate(-paneAt[0])).let { if (it.owner == null) it.ownedBy(before[0]) else it } } } before[0] = target val targetNow by rememberUpdatedState(target) - // A page leaving through a hop's link is not coming back to its control. val leave = if (Hops.linking) PageLeave.Link else PageLeave.Ordinary LaunchedEffect(target) { Hops.linking = false } AnimatedContent( @@ -155,16 +144,13 @@ internal fun Pane( modifier = Modifier.onGloballyPositioned { paneAt[0] = it.positionInRoot() }, transitionSpec = { when { - // Into a page from its control: the control floods the pane (PageFlood) as it sinks back. targetState in origins -> (fadeIn(Motion.tw(120))) .togetherWith(fadeOut(Motion.tw(320, 80)) + scaleOut(Motion.tw(480), targetScale = 0.95f)) .apply { targetContentZIndex = 1f } - // Back out of it: the flood draws back into the control as the pane comes forward again. initialState in origins && pageExit(origins[initialState]?.owner, targetState, leave) == PageExit.Drain -> (fadeIn(Motion.tw(300, 100)) + scaleIn(Motion.tw(460, 40), initialScale = 0.95f)) .togetherWith(ExitTransition.None) .apply { targetContentZIndex = -1f } - // Away to something else: the control is not coming back, so the page sinks with the pane. initialState in origins -> (fadeIn(Motion.tw(300, 80)) + slideInVertically(Motion.tw(420, 80)) { it / 24 }) .togetherWith(fadeOut(Motion.tw(170)) + scaleOut(Motion.tw(170), targetScale = 0.95f)) .apply { targetContentZIndex = 1f } @@ -177,8 +163,6 @@ internal fun Pane( ) { key -> val drawn = (if (page != null && key == s.pageKey) page else pages[key]) ?: { Content(s, if (key == "games") gameShown[0] else key, a, Modifier.fillMaxSize(), onSelect, onAndroidAppClick, onOpenDeveloperOptions, onRequestWirelessAdb) } - // A hop leaving this content: it sinks back as the dot leaves it (and comes forward - // again, freshly drawn, when the hop is undone). val hopSink by androidx.compose.animation.core.animateFloatAsState( if (Hops.sinking == key) 1f else 0f, Motion.tw(260), label = "hopSink", ) @@ -190,16 +174,13 @@ internal fun Pane( val from = origins[key] if (from == null) shown() else { - // The control's own picture (a card's art, an emulator's icon) on the tile as it grows. val art = when (val m = from.art) { null -> null is Int -> androidx.compose.ui.res.painterResource(m) else -> coil.compose.rememberAsyncImagePainter(m) } val leaving = transition.targetState == EnterExitState.PostExit - // Settled as the page starts to leave, from what is coming in, not from where it came in. val drains = remember(leaving) { !leaving || pageExit(from.owner, target, leave) == PageExit.Drain } - // Keeps a leaving page on screen while its flood draws back into the control. transition.animateFloat( transitionSpec = { if (targetState == EnterExitState.PostExit && drains) Motion.tw(PAGE_RETURN_MS) else snap() }, label = "pageReturn", @@ -212,11 +193,6 @@ internal fun Pane( } } -/** - * An origin left of the pane (a rail item a hop flew to) starts half inside it, at the item's - * height: the page swells out of the pane's edge beside the item. Wholly outside, its tile was a - * bar that raced across from the far edge. - */ internal fun inPane(o: Origin): Origin = if (o.bounds.right > 0f) o else o.translate(Offset(-o.bounds.right + o.bounds.width / 2f, 0f)) @@ -225,8 +201,6 @@ private fun Backdrop(art: File?) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current Crossfade(targetState = art, animationSpec = Motion.tw(700), label = "backdrop") { a -> - // A game's own art, blurred, where it has some; otherwise the plain ground with one quiet - // glow of the theme's signal colour - a colour per title hash read as noise. if (a != null && Build.VERSION.SDK_INT >= 31) { Box(modifier = Modifier.fillMaxSize().alpha(0.26f).blur(70.dp)) { AsyncImage(model = a, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize().graphicsLayer { scaleX = 1.5f; scaleY = 1.5f }) @@ -257,7 +231,6 @@ private fun Content( val density = androidx.compose.ui.platform.LocalDensity.current.density val padH = if (narrow) 16.dp else 22.dp val padV = if (narrow) 12.dp else 18.dp - // Setup scrolls inside itself, under its tabs. if (selected == "setup") { Column(modifier = modifier.padding(horizontal = padH, vertical = padV)) { SetupPanel(s, a, onOpenDeveloperOptions, onRequestWirelessAdb) @@ -268,33 +241,26 @@ private fun Content( UpdatesPage(s, a, modifier.padding(horizontal = padH, vertical = padV)) return } - // Steam is a full-bleed wall of the library with Play over it. if (selected == "steam") { SteamHome(s, a, modifier) return } - // The Store scrolls and navigates inside itself (its tabs and an app's page). if (selected == "store") { StorePage(s, a, modifier) return } - // Stores (GOG, Epic, Amazon) takes the whole pane: chips, shelves and a game's page inside it. if (selected == "stores") { StoresPage(s, a, modifier) return } - // The Games tab lays out its own list and detail. if (selected == "games" || selected.startsWith("app:")) { GamesPage(s, a, selected, onSelect, modifier) return } - // Desktop is the Steam tab's shape: a hero with its lifted sheet, and shelves under it. if (selected == "desktop") { DesktopPage(s, a, modifier, onSelect) return } - // Every other page scrolls as one - header, hero and grid - so a short screen reaches the grid - // instead of showing a sliver of it under a fixed hero. A focused tile scrolls itself into view. Column(modifier = modifier.verticalScroll(rememberScrollState()).padding(horizontal = padH, vertical = padV)) { when { selected == "android-apps" && s.isHomeApp -> { @@ -322,7 +288,6 @@ private fun Content( if (e == null) Note(stringResource(R.string.content_not_installed)) else { val pkgId = Library.packageId(e.id) val pkg = pkgId?.let { id -> s.packages?.firstOrNull { it.id == id } } - // Out of its card on Desktop, with the card's icon; B draws it back in. Rise(1) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -367,8 +332,6 @@ private fun Content( } else Rise(5, Modifier.fillMaxWidth()) { Column { - // RetroArch's games come from many systems: chips filter them, one - // selection gliding between them. val systems = remember(e.games) { e.games.map { romSystem(it) }.distinct() } var system by rememberSaveable(e.id) { mutableStateOf<String?>(null) } if (systems.size > 1) Box(Modifier.padding(top = 10.dp)) { @@ -467,7 +430,6 @@ internal fun UserApps.Kind.label() = when (this) { UserApps.Kind.FLATPAK -> R.string.user_apps_kind_flatpak } -/** An added app's icon: its own, or the kind's when it has none. */ @Composable private fun UserAppIcon(app: UserApps.App, size: Int) { if (app.icon != null) AppIcon(app.icon, app.name, size) @@ -482,7 +444,6 @@ private fun UserAppIcon(app: UserApps.App, size: Int) { } } -/** The add or remove under way, or why the last one failed. */ @Composable private fun UserAppsProgress() { val colors = MaterialTheme.colorScheme @@ -501,7 +462,6 @@ private fun UserAppsProgress() { } } -/** An added app's page: open it, change its name and icon, update one added from GitHub, or remove it (asked first). */ @Composable private fun UserAppPage(app: UserApps.App, s: FrontEndState, a: FrontEndActions, onSelect: (String) -> Unit) { val colors = MaterialTheme.colorScheme @@ -556,7 +516,6 @@ private fun UserAppPage(app: UserApps.App, s: FrontEndState, a: FrontEndActions, } } -/** Remove, which asks first: are you sure steps out of it in red, Keep first and focused. */ @Composable private fun RemoveAppButton(app: UserApps.App, enabled: Boolean, onRemove: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -581,7 +540,6 @@ private fun RemoveAppButton(app: UserApps.App, enabled: Boolean, onRemove: () -> } } -/** Looks up a newer release of an app added from GitHub, then installs it. */ @Composable private fun UpdateButton(app: UserApps.App, s: FrontEndState) { val ctx = LocalContext.current @@ -604,13 +562,8 @@ private fun UpdateButton(app: UserApps.App, s: FrontEndState) { } } -/** - * One emulator: its icon, system and games. One not yet installed says so in words and keeps full - * contrast - dimming it read as disabled rather than one press from installing. - */ @Composable private fun systemLabel(e: Library.Emulator): String = if (e.systemRes != 0) stringResource(e.systemRes) else e.system.replaceFirstChar { it.uppercase() } -/** The system a ROM belongs to: the folder it sits in under the ROMs folder. */ private fun romSystem(rom: Library.Rom): String = rom.hostPath.parentFile?.name.orEmpty() diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt index 0acb72b5d..907f9d5f9 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt @@ -61,12 +61,7 @@ import coil.compose.AsyncImage import com.droiddeck.launcher.frontend.Library import kotlinx.coroutines.launch -// The Games page: the list, the hero for the selected game and its labels. -/** - * The Games tab: installed Steam games down the left, most recently played first, and the one - * picked beside them - its banner, Launch and the launch settings. One game skips the list. - */ @Composable internal fun GamesPage(s: FrontEndState, a: FrontEndActions, selected: String, onSelect: (String) -> Unit, modifier: Modifier) { val games = remember(s.steamGames) { s.steamGames.sortedByDescending { it.lastPlayed } } @@ -155,11 +150,9 @@ private fun GameActions(g: Library.SteamGame, s: FrontEndState, a: FrontEndActio } g.protonPrefix?.takeIf { it.isDirectory }?.let { dir -> SecondaryButton(stringResource(R.string.games_prefix), compact = true) { a.onBrowseFiles(dir) } - // Only games added to the library; Steam titles keep their saves with Steam Cloud. if (g.library == Library.ADDED) ManageSaves(g, dir, a) } if (!s.shortcutPicker) GameShortcutMenu(g, a) - // A game added in DroidDeck (Custom): its editor, Steam's Properties for a non-Steam game. if (!s.shortcutPicker && g.library == Library.ADDED && g.source == Library.ADDED && g.gameFiles != null) EditGameButton { onEdit(g) } BusyChip(s) } @@ -184,10 +177,6 @@ private fun GameShortcutMenu(g: Library.SteamGame, a: FrontEndActions) { } } -/** - * Manage saves: import a GameHub or Winlator save zip into this game's prefix, export its saves as - * either, or open the save folder found for it. The saves are looked for when the menu opens. - */ @Composable private fun ManageSaves(g: Library.SteamGame, prefix: java.io.File, a: FrontEndActions) { var open by remember(g.gameId) { androidx.compose.runtime.mutableStateOf(false) } @@ -211,7 +200,6 @@ private fun ManageSaves(g: Library.SteamGame, prefix: java.io.File, a: FrontEndA } } Box { - // A GOG or Epic game's saves are its cloud saves: the one place for them, named so. val cloudGame = com.droiddeck.launcher.stores.Store.byId(g.source).let { it == com.droiddeck.launcher.stores.Store.GOG || it == com.droiddeck.launcher.stores.Store.EPIC } val label = stringResource(if (cloudGame) R.string.store_cloud_saves else R.string.games_manage_saves) SecondaryButton(label, compact = true) { open = !open } @@ -235,12 +223,6 @@ private fun ManageSaves(g: Library.SteamGame, prefix: java.io.File, a: FrontEndA } } -/** - * A GOG or Epic game's cloud saves: the Cloud saves switch, Upload and Download - three rows from - * the first frame, so nothing moves under the pad while the cloud is checked. Until the check is - * back the two actions are disabled and read "…"; then only their subtitles change: the last sync, - * "No cloud saves", or a conflict, when Upload and Download become Keep local and Keep cloud. - */ @Composable private fun CloudRows(g: Library.SteamGame, open: Boolean, first: androidx.compose.ui.focus.FocusRequester) { val store = com.droiddeck.launcher.stores.Store.byId(g.source)?.takeIf { it != com.droiddeck.launcher.stores.Store.AMAZON } ?: return @@ -253,7 +235,6 @@ private fun CloudRows(g: Library.SteamGame, open: Boolean, first: androidx.compo runCatching { com.droiddeck.launcher.stores.CloudSaves.status(context, store, id) }.getOrNull() } } - // The game's own switch (sidecar "cloud"), known locally: right from the first frame. val folder = g.gameFiles var enabled by remember(g.gameId, open) { androidx.compose.runtime.mutableStateOf(folder?.let { com.droiddeck.launcher.stores.StoreGameSidecar.read(it)?.cloud } ?: true) } val st = status @@ -285,17 +266,14 @@ private fun CloudRows(g: Library.SteamGame, open: Boolean, first: androidx.compo st.lastSync > 0 -> stringResource(R.string.cloud_last_sync, android.text.format.DateUtils.getRelativeTimeSpanString(st.lastSync).toString()) else -> stringResource(R.string.cloud_never) } - // A conflict turns the two actions into its two answers; the rows stay where they are. MenuItem(stringResource(if (conflict) R.string.cloud_keep_local else R.string.cloud_upload), checked = false, enabled = ready, detail = summary) { sync(up = true, force = conflict) } MenuItem(stringResource(if (conflict) R.string.cloud_keep_cloud else R.string.cloud_download), checked = false, enabled = ready, detail = if (st == null || busy != null) pending else null) { sync(up = false, force = conflict) } } -/** When it was last played (or where it is, if never) over its name, then room for Launch. */ @Composable private fun ColumnScope.GameHeroCopy(g: Library.SteamGame, titleSize: androidx.compose.ui.unit.TextUnit) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { SourceChip(g.source) - // A Custom game's chip already says what its eyebrow would; the line then stays empty. val eyebrow = lastPlayedText(g.lastPlayed) ?: libraryLabel(g) if (eyebrow.isNotEmpty()) Text( eyebrow.uppercase(), @@ -316,14 +294,11 @@ private fun GameList( onSelect: (Library.SteamGame) -> Unit, onLaunch: (Library.SteamGame) -> Unit, onAdd: (() -> Unit)?, modifier: Modifier, ) { val colors = MaterialTheme.colorScheme - // Laid out whole, as the art grid is: the pad's focus search only finds rows that exist. Column( verticalArrangement = Arrangement.spacedBy(2.dp), modifier = modifier.verticalScroll(rememberScrollState()).padding(start = 12.dp, end = 10.dp, top = 16.dp, bottom = 16.dp), ) { GamesHeader(games.size, onAdd, Modifier.padding(start = 6.dp, bottom = 10.dp)) - // A game that arrives while the list is up (Steam installed it in the session that just - // ended, or it was added) eases in; the ones there when the page opened are simply there. val seen = remember { HashSet<Int>() } var settled by remember { androidx.compose.runtime.mutableStateOf(false) } for (g in games) key(g.appId) { @@ -338,7 +313,6 @@ private fun GameList( } } -/** "Games N", and the + that adds a game from its .exe beside it ([onAdd] null: no +). */ @Composable private fun GamesHeader(count: Int, onAdd: (() -> Unit)?, modifier: Modifier) { val colors = MaterialTheme.colorScheme @@ -349,17 +323,11 @@ private fun GamesHeader(count: Int, onAdd: (() -> Unit)?, modifier: Modifier) { } } -/** - * The Games tab's dialogs, one at a time: the + picker, the summary after "Add all games in this - * folder", and a game's editor (from the page's ✎ or a summary row, with back to the summary). - */ internal object GamesDialogState { var adding by androidx.compose.runtime.mutableStateOf(false) var summary by androidx.compose.runtime.mutableStateOf<Pair<List<com.droiddeck.launcher.frontend.AddedGames.Game>, Int>?>(null) var editing by androidx.compose.runtime.mutableStateOf<EditRequest?>(null) - /** The subfolder "Add all games in this folder" is looking at; null when it is done. */ var checking by androidx.compose.runtime.mutableStateOf<String?>(null) - /** The summary an editor opened from it goes back to. */ var behindEditor: Pair<List<com.droiddeck.launcher.frontend.AddedGames.Game>, Int>? = null } @@ -409,7 +377,6 @@ private fun GamesDialogs(a: FrontEndActions) { } } -/** The ✎ on a Custom game's page: icon only. */ @Composable private fun EditGameButton(onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -430,7 +397,6 @@ private fun EditGameButton(onClick: () -> Unit) { } } -/** A round + that opens the .exe picker. */ @Composable private fun AddGameButton(onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -452,7 +418,6 @@ private fun AddGameButton(onClick: () -> Unit) { } } -/** Fades and slides a row in once when [arriving]; nothing for one already there (or with animations off). */ @Composable internal fun Modifier.arriveIn(arriving: Boolean): Modifier { val t = remember { androidx.compose.animation.core.Animatable(if (arriving) 0f else 1f) } @@ -460,7 +425,6 @@ internal fun Modifier.arriveIn(arriving: Boolean): Modifier { return graphicsLayer { alpha = t.value; translationY = (1f - t.value) * 12.dp.toPx() } } -/** One game in the list. Moving onto it with the pad shows it; A launches it, a tap only shows it. */ @Composable private fun GameRow(g: Library.SteamGame, selected: Boolean, onSelect: () -> Unit, onLaunch: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -484,7 +448,6 @@ private fun GameRow(g: Library.SteamGame, selected: Boolean, onSelect: () -> Uni } Column(modifier = Modifier.weight(1f)) { Text(g.name, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis) - // Where it came from beside when it was played: a store's games sit among Steam's own. Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { SourceChip(g.source, small = true) Text( @@ -496,7 +459,6 @@ private fun GameRow(g: Library.SteamGame, selected: Boolean, onSelect: () -> Uni } } -/** A page's title, with room at its right for controls or status. */ @Composable internal fun PageHeader(title: String, trailing: @Composable RowScope.() -> Unit = {}) { Row( @@ -511,13 +473,11 @@ internal fun PageHeader(title: String, trailing: @Composable RowScope.() -> Unit } } -/** Why a launch button is greyed out: the runtime is being worked on. Nothing when it is not. */ @Composable internal fun BusyChip(s: FrontEndState) { if (s.busy) ActionChip(if (s.percent >= 0) stringResource(R.string.games_runtime_busy_percent, s.percent) else s.stage, ok = false) } -/** Whether Steam can start, said where Play is rather than only in Setup. */ @Composable internal fun RuntimeChip(s: FrontEndState) = when { s.busy -> Chip(if (s.percent >= 0) stringResource(R.string.setup_runtime_progress, s.stage, s.percent) else s.stage, ok = false) @@ -527,12 +487,10 @@ internal fun RuntimeChip(s: FrontEndState) = when { else -> Chip(stringResource(R.string.games_runtime_ready), ok = true) } -/** "Last played 3 days ago" from Steam's unix seconds; null for a game never played. */ @Composable private fun lastPlayedText(lastPlayed: Long): String? = playedSpan(lastPlayed)?.let { stringResource(R.string.games_last_played, it.replaceFirstChar { c -> c.lowercase() }) } -/** "3 days ago" from Steam's unix seconds; null for a game never played. */ private fun playedSpan(lastPlayed: Long): String? { if (lastPlayed <= 0L) return null return android.text.format.DateUtils.getRelativeTimeSpanString( @@ -540,10 +498,6 @@ private fun playedSpan(lastPlayed: Long): String? { ).toString() } -/** - * Where a never-played game lives, beside its source chip: Steam's library, the Games storage for - * a store install (the chip names the store), "Custom game" for a folder the user added. - */ @Composable private fun libraryLabel(g: Library.SteamGame): String = when { g.library != Library.ADDED -> if (g.library == "internal") stringResource(R.string.games_internal) else g.library @@ -551,11 +505,6 @@ private fun libraryLabel(g: Library.SteamGame): String = when { else -> "" } -/** - * A game's wide banner: Steam's hero art where the client cached one, else its capsule blurred to - * fill the width. The copy sits bottom-left over a scrim of the ground colour so it always reads; - * the art takes the banner's size, so a banner given a minimum height grows to fit its copy. - */ @Composable private fun GameHero(g: Library.SteamGame, modifier: Modifier, content: @Composable ColumnScope.() -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt index c95ae71ae..c5a0c6866 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt @@ -82,14 +82,9 @@ import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned import com.droiddeck.launcher.core.PhantomProcessLimit -// The front end's side rail: the page list and the resume entry under it. internal val AttentionAmber = Color(0xFFFFB547) -/** - * The launcher's sections, always on screen down the left edge: the app is landscape-only, so the - * width a hamburger menu would save is better spent keeping every section one press away. - */ @Composable internal fun SideRail( s: FrontEndState, selected: String, @@ -97,7 +92,6 @@ internal fun SideRail( ) { val colors = MaterialTheme.colorScheme val compact = LocalConfiguration.current.screenHeightDp < 420 - // Under 600dp wide (4:3 and square screens) the labels would cost a fifth of the width. val iconOnly = isNarrowScreen() val setupNeedsAttention = !s.ready || (s.available != null && s.available != s.installed) || PhantomProcessLimit.blocksSteam(s.phantomProcessStatus) @@ -105,11 +99,9 @@ internal fun SideRail( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.width(if (iconOnly) 64.dp else 92.dp).background(colors.surface).padding(vertical = if (compact) 8.dp else 12.dp), ) { - // Every section on screen at once: items shrink (to 44dp, still a touch target) before the rail scrolls. val count = 6 + (if (s.gameStoresEnabled) 1 else 0) + (if (s.isHomeApp) 1 else 0) BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) { val fit = (maxHeight / count - 4.dp).coerceIn(44.dp, if (iconOnly) 48.dp else if (compact) 52.dp else 60.dp) - // A hop in flight moves the selection ahead of the page it is bringing. val shown = Hops.railPreview ?: selected val keys = buildList { add("steam"); add("games") @@ -119,13 +111,10 @@ internal fun SideRail( if (s.isHomeApp) add("android-apps") add("setup"); add("updates") } - // Where each item sits in the column, for the selection that glides between them. val bounds = remember { androidx.compose.runtime.mutableStateMapOf<String, androidx.compose.ui.geometry.Rect>() } val tint = androidx.compose.runtime.rememberUpdatedState(LocalPalette.current.signal) var railAt by remember { mutableStateOf(androidx.compose.ui.geometry.Offset.Zero) } Box(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).onGloballyPositioned { railAt = it.boundsInRoot().topLeft }) { - // One selection for the whole rail, under the items: it glides from item to item as the - // chips' does, its leading edge first, and a jump of more than one item goes as a drop. if (shown in keys) ChipGlide(keys.indexOf(shown), bounds[shown]?.translate(-railAt), tint, GlideAxis.Vertical, corner = 14.dp, fill = 0.14f, outline = 0f) Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -133,22 +122,16 @@ internal fun SideRail( modifier = Modifier.fillMaxWidth(), ) { val place: (String) -> Modifier = { key -> - // In root px: an item that comes and goes sits in its own wrapper, not the column. Modifier.onGloballyPositioned { bounds[key] = it.boundsInRoot() Hops.railBounds[key] = it.boundsInRoot() } } RailItem(stringResource(R.string.rail_steam), Icons.Outlined.SportsEsports, "steam", shown == "steam", compact, iconOnly, fit, place = place, onFocus = { onFocusSelect("steam") }) { onSelect("steam") } - // Always there, so the items below it never move; an empty library says how to fill it. RailItem(stringResource(R.string.rail_games), Icons.Outlined.VideoLibrary, "games", shown == "games", compact, iconOnly, fit, place = place, onFocus = { onFocusSelect("games") }) { onSelect("games") } - // The game stores, right under Games: what they install lands there. Its badge counts - // the downloads in flight, so a download started here is never out of sight. - // A section switched on in Setup comes in as the rail's own entrance; off, it folds away. if (s.gameStoresEnabled) RailItem(stringResource(R.string.rail_stores), Icons.Outlined.LocalMall, "stores", shown == "stores", compact, iconOnly, fit, count = s.storeDownloadsActive, place = place, onFocus = { onFocusSelect("stores") }) { onSelect("stores") } RailItem(stringResource(R.string.rail_desktop), Icons.Outlined.DesktopWindows, "desktop", shown == "desktop", compact, iconOnly, fit, place = place, onFocus = { onFocusSelect("desktop") }) { onSelect("desktop") } RailItem(stringResource(R.string.rail_components), Icons.Outlined.Layers, "components", shown == "components", compact, iconOnly, fit, place = place, onFocus = { onFocusSelect("components") }) { onSelect("components") } - // Home mode's extra section goes last, so it shifts nothing above it. if (s.isHomeApp) RailItem(stringResource(R.string.rail_apps), Icons.Outlined.Apps, "android-apps", shown == "android-apps", compact, iconOnly, fit, place = place, onFocus = { onFocusSelect("android-apps") }) { onSelect("android-apps") } RailItem(stringResource(R.string.rail_setup), Icons.Outlined.Tune, "setup", shown == "setup", compact, iconOnly, fit, badge = setupNeedsAttention, place = place, onFocus = { onFocusSelect("setup") }) { onSelect("setup") } RailItem(stringResource(R.string.rail_updates), Icons.Outlined.SystemUpdate, "updates", shown == "updates", compact, iconOnly, fit, badge = s.updates.hasUpdate, place = place, onFocus = { onFocusSelect("updates") }) { onSelect("updates") } @@ -169,9 +152,7 @@ internal fun SideRail( private fun RailItem( label: String, icon: ImageVector, key: String, current: Boolean, compact: Boolean, iconOnly: Boolean, height: Dp, badge: Boolean = false, - /** A number on the item (downloads in flight); 0 shows nothing. */ count: Int = 0, - /** Reports where the item sits, for the rail's gliding selection and a hop's dot. */ place: (String) -> Modifier = { Modifier }, onFocus: () -> Unit = {}, onClick: () -> Unit, ) { @@ -193,7 +174,6 @@ private fun RailItem( if (current) pal.signal else if (focused || hovered) colors.onBackground else colors.onSurfaceVariant, Motion.tw(220), label = "railFg", ) - // The selected look is the rail's glide under the item, so the item draws only hover here. val fill by animateColorAsState( if (current) Color.Transparent else if (focused || hovered) Color.White.copy(alpha = 0.05f) else Color.Transparent, Motion.tw(220), label = "railFill", @@ -205,7 +185,6 @@ private fun RailItem( .then(if (frontFocus != null) Modifier.focusRequester(frontFocus.railFor(key)) else Modifier) .testTag("rail-$key") .then(place(key)) - // Pops as a hop's dot lands on it. .landingPop(Hops.railLanded[key] ?: 0) .graphicsLayer { scaleX = scale; scaleY = scale } .size(width = if (iconOnly) 52.dp else 80.dp, height = height) @@ -228,14 +207,12 @@ private fun RailItem( if (count > 0) Text( if (count > 9) "9+" else count.toString(), fontSize = 10.sp, fontWeight = FontWeight.Bold, color = pal.onSignal, maxLines = 1, - // Pops with the Downloads chip's count as an install's dot lands there. modifier = Modifier.align(Alignment.TopEnd).padding(top = if (iconOnly) 4.dp else 6.dp, end = if (iconOnly) 5.dp else 12.dp) .landingPop().clip(RoundedCornerShape(8.dp)).background(pal.signal).padding(horizontal = 5.dp, vertical = 1.dp), ) } } -/** The running session, one press from anywhere in the launcher. */ @Composable private fun ResumeRailItem(name: String, compact: Boolean, iconOnly: Boolean, onResume: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -246,7 +223,6 @@ private fun ResumeRailItem(name: String, compact: Boolean, iconOnly: Boolean, on val resumeDescription = stringResource(R.string.rail_resume_named, name) val pulse = rememberInfiniteTransition(label = "pulse") val ringScale by pulse.animateFloat(0.4f, 1.6f, infiniteRepeatable(tween(1600, easing = Motion.Ease), RepeatMode.Restart), label = "ring") - // Animations off: the live dot alone, no pulsing ring. val still = Motion.scale == 0f Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -266,7 +242,6 @@ private fun ResumeRailItem(name: String, compact: Boolean, iconOnly: Boolean, on if (!still) Box(modifier = Modifier.size(14.dp).graphicsLayer { scaleX = ringScale; scaleY = ringScale; alpha = (1.6f - ringScale) / 1.2f }.border(1.5.dp, pal.good, CircleShape)) Box(modifier = Modifier.size(7.dp).clip(CircleShape).background(pal.good)) } - // Icons only: the live dot alone says something is running; its description says what. if (!iconOnly) { Text(stringResource(R.string.resume_session), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, softWrap = false) Text(name, fontSize = 12.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt index 79486e5d8..73212d3f8 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -97,7 +97,6 @@ class FrontEndState( val frameGenLabel: String, val romsDir: String?, val logsEnabled: Boolean, - /** Setup's Debugging tools toggle (AgentAccess): off unless the user turns it on. */ val agentCommands: Boolean = false, val steamGames: List<Library.SteamGame>, val emulators: List<Library.Emulator>, @@ -109,7 +108,6 @@ class FrontEndState( val gameSyncFolder: String? = null, val pageKey: String? = null, val theme: String = Themes.GRAPHITE, - /** The language picked in Setup, or AppLanguage.SYSTEM to follow the system. */ val language: String = "", val appScale: Int = com.droiddeck.launcher.core.AppUiPrefs.DEFAULT_SCALE, val isHomeApp: Boolean = false, @@ -135,19 +133,13 @@ class FrontEndState( val launcherFullscreen: Boolean = true, val animationsEnabled: Boolean = true, val gameStoresEnabled: Boolean = true, - /** Setup › Stores: the download engine's speed tier (stores/download/StoreDownloadTier ids). */ val gameStoresSpeedTier: String = "fast", - /** Setup › Stores: the tab a signed-in store opens on (SessionPrefs.STORES_OPEN_LIBRARY / STORE). */ val storesOpenTab: String = SessionPrefs.STORES_OPEN_LIBRARY, - /** A page to open once: "stores:downloads", or a selection such as "app:<id>" (a notification's tap). */ val navigate: String? = null, - /** Setup: art for added games is fetched; the user has a SteamGridDB key of their own. */ val addedGamesArt: Boolean = true, val sgdbUserKey: Boolean = false, val storesShowMature: Boolean = false, - /** Downloads queued, running or paused across the three stores; the rail item's badge. */ val storeDownloadsActive: Int = 0, - /** The Updates page: DroidDeck's own builds and the channel followed. */ val updates: UpdatesState = UpdatesState(), ) @@ -158,30 +150,21 @@ class FrontEndActions( val onGameShortcut: (Library.SteamGame) -> Unit = {}, val onExportGameFile: (Library.SteamGame) -> Unit = {}, val onSyncGameFiles: () -> Unit = {}, - /** The Games tab's +: the .exe picked in its dialog, to add as a game. */ val onAddGameExe: (path: String) -> Unit = {}, - /** These added games (by folder) were added or edited: the Games tab updates just them. */ val onAddedGamesChanged: (folders: List<String>) -> Unit = {}, - /** An added game removed in its editor: off the Games tab at once, out of Steam after. */ val onAddedGameRemoved: (folder: String, appId: Long?, name: String) -> Unit = { _, _, _ -> }, - /** Setup's Artwork switch and the user's SteamGridDB API key ("" removes it). */ val onAddedGamesArt: (Boolean) -> Unit = {}, val onSgdbKey: (String) -> Unit = {}, val onStopGameFileSync: () -> Unit = {}, val onCopyGameLink: (Library.SteamGame) -> Unit = {}, val onDesktop: () -> Unit, val onEmulator: (Library.Emulator) -> Unit, - /** A Flatpak app by id and name, from the Store. */ val onFlatpakApp: (String, String) -> Unit = { _, _ -> }, - /** An app added on the Desktop page. */ val onUserApp: (com.droiddeck.launcher.runtime.UserApps.App) -> Unit = {}, val onRom: (Library.Rom) -> Unit, val onResume: () -> Unit, - /** Reads Steam's session settings for its lifted sheet (under Play). */ val onSteamSettings: () -> Unit, - /** Reads the desktop's session settings for its lifted sheet (under Open desktop). */ val onDesktopSettings: () -> Unit, - /** A lifted sheet's rows for one tab of [mode]'s settings, as last read. */ val modeSheet: @Composable (mode: String, tab: String, extras: SheetExtras) -> Unit = { _, _, _ -> }, val onRemoveDesktop: () -> Unit = {}, val onInstallPackage: (String) -> Unit, @@ -190,11 +173,8 @@ class FrontEndActions( val onFrameGenPick: (FrameGen.Mode) -> Unit, val onImportLossless: () -> Unit, val onProtons: () -> Unit, - /** The Components page: FEX / DXVK / VKD3D-Proton per Proton. */ val onComponents: (focusContent: Boolean) -> Unit, - /** The Components page on one layer ([GPU_TAB], a component id): a hop's destination. */ val onComponentsTab: (String) -> Unit = {}, - /** A game page's Manage saves: import a save zip into this game, or export its saves in a layout. */ val onSaveImport: (Library.SteamGame) -> Unit = {}, val onSaveExport: (Library.SteamGame, com.droiddeck.launcher.session.GameSaves.Layout) -> Unit = { _, _ -> }, val onPerformance: () -> Unit, @@ -205,7 +185,6 @@ class FrontEndActions( val onAgentCommands: () -> Unit = {}, val onShareLogs: () -> Unit = {}, val onClearLogs: () -> Unit = {}, - /** Setup's Reset all settings: asks first, then puts every setting back to its default. */ val onResetSettings: () -> Unit = {}, val onOffline: () -> Unit, val onPageBack: () -> Unit = {}, @@ -220,7 +199,6 @@ class FrontEndActions( val onStoresOpenTab: (String) -> Unit = {}, val onNavigated: () -> Unit = {}, val onStoresShowMature: (Boolean) -> Unit = {}, - /** The Games list rebuilt now: a store install or removal changed what is on disk. */ val onLibraryChanged: () -> Unit = {}, val onHomeApp: () -> Unit = {}, val onHomeScreen: (Boolean) -> Unit = {}, @@ -243,15 +221,12 @@ class FrontEndActions( internal object Motion { var scale by mutableStateOf(1f) private set - /** App animations can be disabled independently; Android's "Remove animations" always wins. */ fun refresh(context: android.content.Context) { scale = if (SessionPrefs.animationsEnabled(context)) Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) else 0f } val Ease = CubicBezierEasing(0.2f, 0.8f, 0.2f, 1f) - /** A wall-clock wait (a coroutine delay) scaled as the animations around it are. Compose already - * scales its own animation clock by the same setting, so animation specs take unscaled times. */ fun ms(base: Int) = (base * scale).roundToInt() fun <T> tw(base: Int, delay: Int = 0, easing: Easing = Ease): FiniteAnimationSpec<T> = if (scale == 0f) snap() else tween(base, delay, easing) fun <T> sp(damping: Float = 0.7f, stiffness: Float = Spring.StiffnessMediumLow): FiniteAnimationSpec<T> = @@ -306,26 +281,15 @@ internal fun Modifier.shine(trigger: Boolean, strength: Float = 0.22f): Modifier } } -/** - * Controller focus on the front end: each rail item's requester and the page's main button (Play, - * Open desktop, Open <emulator>, Launch...). The pane leaves, to the left, for the selected rail - * item - whatever tile or button it leaves from - and remembers that control, so coming back in - * lands on it again; a page not yet visited enters on its main button. - */ internal class FrontFocus { val rail = HashMap<String, FocusRequester>() val primary = FocusRequester() var primaryAttached by mutableStateOf(0) var focusedRail by mutableStateOf<String?>(null) fun railFor(key: String): FocusRequester = rail.getOrPut(key) { FocusRequester() } - // The pane's controls by id (a tile's key, a button's label), how many of each are on screen, - // and the last one focused. val items = HashMap<String, FocusRequester>() val attached = HashMap<String, Int>() - // State, so the controller hints follow it: A launches from a game row but selects elsewhere. var last by mutableStateOf<String?>(null) - // The first tile of the page's grid: Down from the page's buttons goes to it, not to whichever - // tile happens to sit under the button. val firstTile = FocusRequester() var firstTileAttached = 0 fun paneEntry(): FocusRequester { @@ -340,16 +304,13 @@ internal class FrontFocus { companion object { const val PRIMARY = "\u0000primary" } } -/** Down from this button goes to the first tile of the page's grid, when there is one. */ @Composable internal fun Modifier.downToFirstTile(): Modifier { - // Sheet rows walk their settings; Desktop's dock stays composed behind the sheet. if (LocalSheetBeat.current != null) return this val ff = LocalFrontFocus.current ?: return this return this.focusProperties { down = if (ff.firstTileAttached > 0) ff.firstTile else FocusRequester.Default } } -/** Marks the first tile of the page's grid. */ @Composable internal fun Modifier.firstTile(): Modifier { val ff = LocalFrontFocus.current ?: return this @@ -360,7 +321,6 @@ internal fun Modifier.firstTile(): Modifier { return this.focusRequester(ff.firstTile) } -/** Lets the pane come back to this control: it is remembered when focused. */ @Composable internal fun Modifier.paneItem(id: String): Modifier { val ff = LocalFrontFocus.current ?: return this @@ -374,14 +334,8 @@ internal fun Modifier.paneItem(id: String): Modifier { internal val LocalFrontFocus = staticCompositionLocalOf<FrontFocus?> { null } -/** The pane's exit animation (170ms) and a frame's margin: a new page is in after this. */ private const val PAGE_EXIT_MS = 200 -/** - * Asks [target] for focus once a frame until [done] (or a focus request lands), for at most half a - * second: a control can take focus only once it is laid out, and that is a frame or two, not a - * fixed number of milliseconds. - */ internal suspend fun focusWithinFrames(done: () -> Boolean, target: () -> FocusRequester) { repeat(30) { androidx.compose.runtime.withFrameNanos { } @@ -390,11 +344,6 @@ internal suspend fun focusWithinFrames(done: () -> Boolean, target: () -> FocusR } } -/** - * Asks [target] for focus on the next frames until a request lands (a control can take focus only - * once it is laid out), for at most half a second; then leaves focus alone, so a press right after - * is never pulled back. - */ internal suspend fun focusSoon(target: () -> FocusRequester) { repeat(30) { androidx.compose.runtime.withFrameNanos { } @@ -443,20 +392,15 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com } } } - // A page reached by a hop goes back the way it came; any other page just closes. val pageBack = { if (!Hops.back(a.onPageBack)) a.onPageBack() } BackHandler(enabled = !processSettingsPageVisible && s.pageKey != null && page != null) { pageBack() } - // Left any other way, the hop's page has no way back to keep. LaunchedEffect(s.pageKey, selected) { Hops.forget(s.pageKey ?: selected) } - // A section reached by a hop (Desktop's Flathub card to the Store) goes back the way it came. BackHandler(enabled = !processSettingsPageVisible && s.pageKey == null && Hops.origin?.page == selected) { val rail = Hops.origin?.rail ?: return@BackHandler Hops.back { selected = rail } } val density = androidx.compose.ui.platform.LocalDensity.current.density remember(density) { Hops.density = density; true } - // Back (and B) from a ROM, an emulator, an added app or Flathub steps out one level instead - // of leaving the app: a ROM -> its emulator, the others -> Desktop. BackHandler( enabled = !processSettingsPageVisible && (s.pageKey == null || page == null) && (selected.startsWith("emu:") || selected.startsWith("rom:") || selected.startsWith("user:") || selected == "store"), @@ -464,32 +408,24 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com selected = if (selected.startsWith("rom:")) "emu:" + selected.removePrefix("rom:").substringBefore(':') else "desktop" } LaunchedEffect(s.isHomeApp) { if (!s.isHomeApp && selected == "android-apps") selected = "steam" } - // The last game uninstalled leaves the Games tab on its empty state. LaunchedEffect(s.steamGames.isEmpty()) { if (s.steamGames.isEmpty() && selected.startsWith("app:")) selected = "games" } remember { Motion.refresh(ctx); true } val railSelection = when { - // A hop's page belongs to the rail item it flew to. Hops.origin != null && s.pageKey == Hops.origin?.page -> Hops.origin!!.dest - // Performance opens from Play's sheet, and is Steam's. s.pageKey == "performance" -> "steam" s.pageKey == "controller-mapping" -> "setup" selected.startsWith("app:") -> "games" - // Flathub is a page of Desktop's, as an emulator's is. selected.startsWith("emu:") || selected.startsWith("rom:") || selected.startsWith("user:") || selected == "store" -> "desktop" else -> s.pageKey ?: selected } - // Bumped each time a rail item is picked, so a controller moves on into the new page. var railPicks by remember { mutableStateOf(0) } val showRailPage: (String, Boolean) -> Unit = { key, focusContent -> - // Components is a full page like Protons or Performance, opened over the current rail - // selection rather than replacing it. if (key == "components") a.onComponents(focusContent) else { if (s.pageKey != null) a.onPageBack() - // Another section: no way back to keep, and no sheet to put back. Hops.origin = null Hops.restore = null Hops.sinking = null @@ -512,19 +448,12 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com .windowInsetsPadding(if (s.launcherFullscreen) WindowInsets.displayCutout else WindowInsets.systemBars) .onFocusChanged { anyFocused = it.hasFocus }, ) { - // Start controllers on the current page's main action, else on the rail. LaunchedEffect(processSettingsPageVisible) { if (processSettingsPageVisible) return@LaunchedEffect snapshotFlow { window.isWindowFocused }.first { it } if (inputModeManager.inputMode != InputMode.Keyboard) inputModeManager.requestInputMode(InputMode.Keyboard) focusWithinFrames({ anyFocused }) { if (frontFocus.primaryAttached > 0) frontFocus.primary else frontFocus.railFor(railSelection) } } - // A tile or button that opens a page goes away with the page it was on, and focus with it; - // the pad then had nothing to move from (a press landed back on the rail's first item). So - // once the new page is in, a controller lands on its main button. - // Backing out of a page: it stays on screen while it leaves (and holds focus there), so hand - // focus back now to the control it was opened from. Not when the page closed because - // another rail item was picked: focus is on the rail then, and stays there. var openPage by remember { mutableStateOf<String?>(null) } LaunchedEffect(s.pageKey) { val closed = openPage != null && s.pageKey == null @@ -535,7 +464,6 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com } LaunchedEffect(selected, s.pageKey, processSettingsPageVisible) { if (processSettingsPageVisible) return@LaunchedEffect - // Past the old page's exit (170ms), then the first frame the new page takes focus. kotlinx.coroutines.delay(Motion.ms(PAGE_EXIT_MS).toLong()) if (inputModeManager.inputMode != InputMode.Keyboard) return@LaunchedEffect if (s.pageKey == "performance") { @@ -548,12 +476,10 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com if (anyFocused) return@LaunchedEffect focusWithinFrames({ anyFocused }) { if (frontFocus.primaryAttached > 0) frontFocus.primary else frontFocus.railFor(railSelection) } } - // A rail item picked with a controller moves on into its page, once the page is in. LaunchedEffect(railPicks) { if (railPicks == 0 || processSettingsPageVisible) return@LaunchedEffect kotlinx.coroutines.delay(Motion.ms(PAGE_EXIT_MS).toLong()) if (inputModeManager.inputMode != InputMode.Keyboard) return@LaunchedEffect - // Waits for the new page's main button rather than a fixed guess at how long it takes. withTimeoutOrNull(1_000) { snapshotFlow { frontFocus.primaryAttached > 0 }.first { it } } ?: return@LaunchedEffect focusWithinFrames({ frontFocus.focusedRail == null && anyFocused }) { frontFocus.paneEntry() } } @@ -563,9 +489,6 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com val railFocus = Modifier .focusProperties { enter = { frontFocus.railFor(railSelection) } } .focusGroup() - // Back from a hop onto a surface that is not a sheet (a game's Components card, Desktop's - // Flathub card): focus - // goes back to the control that asked, once the page has gone. LaunchedEffect(Hops.restore) { val r = Hops.restore ?: return@LaunchedEffect if (r.surface == HOP_SURFACE_SHEET) return@LaunchedEffect @@ -595,9 +518,7 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com } } - // Dots on their way somewhere: a hop to another section and back, a swap flying home. FlightsLayer(screenAt) - // A confirm stepping out of the control that asked for it. StepsLayer(screenAt) appToChooseDisplay?.let { app -> @@ -662,11 +583,9 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com } } val hasBackTarget = (s.pageKey != null && page != null) || - // A section reached by a hop goes back to where it came from, not to the rail. (s.pageKey == null && Hops.origin?.page == selected) || ((s.pageKey == null || page == null) && (selected.startsWith("emu:") || selected.startsWith("rom:") || selected.startsWith("user:") || selected == "store")) - // At the top of a section, Back goes to the rail - the launcher itself is never backed out of. BackHandler(enabled = !processSettingsPageVisible && !hasBackTarget) { if (inputModeManager.inputMode != InputMode.Keyboard) inputModeManager.requestInputMode(InputMode.Keyboard) runCatching { frontFocus.railFor(railSelection).requestFocus() } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt index 27e045019..c79bb2790 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -81,11 +81,9 @@ import com.droiddeck.launcher.session.SessionPrefs import com.droiddeck.launcher.stores.download.StoreDownloadTier import com.droiddeck.launcher.R -// The Setup page: runtime and device checks, tools, frame generation and launch settings. private enum class CheckState { OK, WARN, BUSY } -/** One requirement in Setup's system check: a status mark, what it is, and at most one action. */ @Composable private fun CheckRow(state: CheckState, title: String, detail: String?, divider: Boolean = true, action: (@Composable () -> Unit)? = null) { val colors = MaterialTheme.colorScheme @@ -159,7 +157,6 @@ internal fun SetupPanel( } val colors = MaterialTheme.colorScheme val pal = LocalPalette.current - // Tested hardware passes; an Adreno below it (a 610, say) warns rather than claiming support. val gpu = remember { com.droiddeck.launcher.gpu.GpuInfo.detect() } val gpuOk = gpu.support == com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED val gpuName = remember { DeviceSupport.gpuName(ctx) } @@ -168,15 +165,12 @@ internal fun SetupPanel( var showLimitDetails by rememberSaveable { mutableStateOf(false) } var editSgdbKey by remember { mutableStateOf(false) } if (editSgdbKey) SgdbKeyDialog(s.sgdbUserKey, onSave = { a.onSgdbKey(it); editSgdbKey = false }, onDismiss = { editSgdbKey = false }) - // Four tabs by scope, a chip row with one gliding selection; LB and RB turn them from - // anywhere on the page. Build and credits are on the Updates page. val tabs = listOf(stringResource(R.string.setup_tab_overview), stringResource(R.string.setup_tab_controller), stringResource(R.string.setup_tab_launcher), stringResource(R.string.setup_tab_diagnostics)) var tab by rememberSaveable { mutableStateOf(0) } val chipFocus = remember { List(4) { FocusRequester() } } var tabTurned by remember { mutableStateOf(false) } val pick: (Int) -> Unit = { i -> tab = i; tabTurned = true } val inputModeManager = LocalInputModeManager.current - // The control a controller was on went with the old tab: it lands on the new tab's chip. LaunchedEffect(tab) { if (tabTurned && inputModeManager.inputMode == InputMode.Keyboard) { androidx.compose.runtime.withFrameNanos { } @@ -260,8 +254,6 @@ internal fun SetupPanel( } } AnimatedVisibility(showLimitDetails, enter = expandVertically(Motion.sp(1f)) + fadeIn(Motion.sp(1f)), exit = shrinkVertically(Motion.sp(1f)) + fadeOut(Motion.sp(1f))) { - // One sentence and at most three buttons. The computer route and its - // raw command live on the full page Wireless debugging opens. Column(modifier = Modifier.fillMaxWidth().padding(start = 56.dp, end = 14.dp, top = 4.dp, bottom = 10.dp)) { Text( if (limitBlocks) PhantomProcessLimit.gateInstructions(ctx, s.phantomProcessStatus) @@ -298,7 +290,6 @@ internal fun SetupPanel( ) } val all = listOf(gpuCheck, runtimeCheck, limitCheck, accountCheck) - // Everything passes: one line says so, and Details opens the checks in place. ReadinessCard( passing, checks.size, listOfNotNull( @@ -312,7 +303,6 @@ internal fun SetupPanel( val shownChecks = all.filterIndexed { i, _ -> checks[i] } shownChecks.forEachIndexed { i, check -> check(i < shownChecks.lastIndex) } } - // Each check that warns is its own card, its Fix it as it always was. if (passing < checks.size) { SectionTitle(stringResource(R.string.setup_needs_you), null) Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { @@ -392,7 +382,6 @@ internal fun SetupPanel( ) } } - // Two columns on a wide page: how it looks; this device and the library's art. if (wide) Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.Top) { Column(Modifier.weight(1f)) { look() } Column(Modifier.weight(1f)) { device(); art() } @@ -415,10 +404,6 @@ internal fun SetupPanel( } } -/** - * The checks as one card while they all pass: a ring with how many pass, "Ready to play" and one - * line of what that means, and a Details chip that opens the checks themselves in place ([checks]). - */ @Composable private fun ReadinessCard(passing: Int, total: Int, line: String, open: Boolean, onDetails: () -> Unit, checks: @Composable () -> Unit) { val colors = MaterialTheme.colorScheme @@ -460,26 +445,18 @@ private fun FrameGenMenu(s: FrontEndState, a: FrontEndActions, host: MenuHost) { FrameGenMenu(host, s.frameGen, s.lossless, a.onFrameGenPick, a.onImportLossless) } -/** - * What shapes a launch, beside the game rather than three screens away in Setup. They are the - * app-wide settings - each card opens the same page or menu Setup does. - */ @Composable internal fun LaunchSettings(s: FrontEndState, a: FrontEndActions, host: MenuHost, game: com.droiddeck.launcher.frontend.Library.SteamGame? = null) { - // Three across, two on a narrow page; each row's cards share one height. val columns = if (LocalNarrowPane.current) 2 else 3 val controller = a.controller val context = androidx.compose.ui.platform.LocalContext.current val inputMode = androidx.compose.ui.platform.LocalInputModeManager.current - // The game's prefix is compatdata/<id>, as droiddeck-game-env reads it: a Steam title's appid, an - // added game's shortcut appid (unsigned). Recommended scans the game's own folder either way. val wincompKey = game?.takeIf { it.library == com.droiddeck.launcher.frontend.Library.ADDED || it.appId > 0 }?.profileKey var wincompOpen by remember(wincompKey) { mutableStateOf<Boolean?>(null) } val wincompPicks = remember(wincompKey, wincompOpen) { wincompKey?.let { com.droiddeck.launcher.session.WinComponents.picks(context, it) }.orEmpty() } val cards = buildList<@Composable (Modifier) -> Unit> { add { m -> val back = stringResource(R.string.hop_from_games) - // Components is its own section: the card hops there, and B on it comes back here. SettingCard(stringResource(R.string.setup_card_components), stringResource(R.string.setup_card_components_hint), "card:components", m.hopSource("card:components")) { val at = Hops.sources["card:components"] if (at == null) a.onComponents(true) @@ -495,9 +472,6 @@ internal fun LaunchSettings(s: FrontEndState, a: FrontEndActions, host: MenuHost } } if (controller != null) add { m -> SettingCard(stringResource(R.string.setup_card_controls), stringResource(R.string.setup_card_controls_hint), "card:controls", m, controller.onMapping) } - // An Epic game's own launch choices, kept in its sidecar so a launch from the Steam client - // honours them too (droiddeck-store-launch reads the same file). - // Store launch choices; cloud saves have their own view. val cardStore = com.droiddeck.launcher.stores.Store.byId(game?.source.orEmpty())?.takeIf { it == com.droiddeck.launcher.stores.Store.EPIC || it == com.droiddeck.launcher.stores.Store.GOG } if (cardStore != null && game?.gameFiles != null) add { m -> Box(m) { @@ -528,10 +502,8 @@ internal fun LaunchSettings(s: FrontEndState, a: FrontEndActions, host: MenuHost } } -/** Epic's account page behind its sign-in: signing in there presents any step the account still owes. */ private const val EPIC_RESOLVE_URL = "https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Faccount%2Fpersonal" -/** The Epic card: the sign-in and offline switches, read from and written to the game's sidecar, and Resolve Epic sign-in. */ @Composable private fun StoreLaunchCard(host: MenuHost, folder: java.io.File, store: com.droiddeck.launcher.stores.Store) { val appContext = androidx.compose.ui.platform.LocalContext.current.applicationContext @@ -550,8 +522,6 @@ private fun StoreLaunchCard(host: MenuHost, folder: java.io.File, store: com.dro if (epic && options.offline) add(stringResource(R.string.epic_card_offline)) else if (epic && options.eos) add(stringResource(R.string.epic_card_eos)) }.ifEmpty { listOf(stringResource(R.string.epic_card_none)) }.joinToString(" · ") - // Each switch goes straight to the sidecar on disk - both launch paths read it there - and the - // card shows what was read back, so a write that did not take is never shown as done. fun set(change: (com.droiddeck.launcher.stores.EpicOptions) -> com.droiddeck.launcher.stores.EpicOptions) { Thread({ val written = runCatching { com.droiddeck.launcher.stores.StoreGameSidecar.updateEpic(folder, change) } @@ -580,8 +550,6 @@ private fun StoreLaunchCard(host: MenuHost, folder: java.io.File, store: com.dro MenuItem(stringResource(R.string.epic_offline), checked = options.offline) { set { it.copy(offline = !it.offline) } } } if (epic) { - // Epic asks some accounts to accept something once (privacy policy, EULA) before a game may - // sign in - EOS's "corrective action". Signing in on Epic's site shows it. MenuItem(stringResource(R.string.epic_resolve), checked = false) { host.open = null runCatching { @@ -616,10 +584,6 @@ private fun SettingCard(label: String, value: String, id: String, modifier: Modi } } -/** - * The app's language: "System default" (what Android uses, named in brackets) or one of the - * languages the app ships, each in its own script. Steam is set to the same language when it starts. - */ @Composable private fun LanguageRow(host: MenuHost, chosen: String, onPick: (String) -> Unit) { val context = androidx.compose.ui.platform.LocalContext.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt index 20c6871ee..0c3c0904b 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt @@ -62,7 +62,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -// Small building blocks shared by the front end's pages: text styles, chips and buttons. internal val Shape14 = RoundedCornerShape(14.dp) @@ -93,7 +92,6 @@ internal fun SectionTitle(t: String, detail: String?) { @Composable internal fun Note(t: String) = Text(t, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.fillMaxWidth().clip(Shape12).background(MaterialTheme.colorScheme.surface).border(1.dp, LocalPalette.current.line2, Shape12).padding(12.dp)) -/** A page's buttons: they wrap onto a second line rather than run off a narrow page. */ @OptIn(ExperimentalLayoutApi::class) @Composable internal fun Actions(content: @Composable () -> Unit) = FlowRow( @@ -101,13 +99,11 @@ internal fun Actions(content: @Composable () -> Unit) = FlowRow( verticalArrangement = Arrangement.spacedBy(10.dp), ) { content() } -/** A status chip in a row of buttons, centred on their height. */ @Composable internal fun ActionChip(t: String, ok: Boolean) = Box(contentAlignment = Alignment.Center, modifier = Modifier.heightIn(min = 44.dp)) { Chip(t, ok) } internal val Shape16 = RoundedCornerShape(16.dp) -/** One level up, as Back and B do: a real button, big enough to hit ([compact]: for the tightest layouts). */ @Composable internal fun BackLink(label: String, compact: Boolean = false, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -150,14 +146,12 @@ internal fun PrimaryButton( main: Boolean = false, compact: Boolean = false, modifier: Modifier = Modifier, - /** The Steam tab's own Play: bigger than any other button on a page. */ large: Boolean = false, icon: ImageVector? = null, onClick: () -> Unit, ) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current - // The page's main button is where the pane is entered from the rail. val frontFocus = if (main) LocalFrontFocus.current else null if (frontFocus != null) DisposableEffect(Unit) { frontFocus.primaryAttached++ @@ -169,23 +163,18 @@ internal fun PrimaryButton( val pressed by src.collectIsPressedAsState() val scale by animateFloatAsState(if (pressed) 0.955f else if (hot) 1.02f else 1f, Motion.sp(0.5f, Spring.StiffnessMedium), label = "btnScale") val lift by animateFloatAsState(if (hot) 14f else 6f, Motion.tw(300), label = "btnLift") - // Where it sits, so a session it starts can grow out of it (LaunchFlood). val placed = remember { arrayOfNulls<androidx.compose.ui.layout.LayoutCoordinates>(1) } Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier.then(track) .onGloballyPositioned { placed[0] = it } - // 48dp to touch on a handheld; compact rows keep 44. .heightIn(min = if (compact) 44.dp else 48.dp) .then(if (frontFocus != null) Modifier.focusRequester(frontFocus.primary).onFocusChanged { if (it.isFocused) frontFocus.last = FrontFocus.PRIMARY } else Modifier) .graphicsLayer { alpha = if (LaunchOrigin.flooding === placed) 0f else 1f; scaleX = scale; scaleY = scale; shadowElevation = if (enabled) lift.dp.toPx() else 0f; shape = Shape12; clip = false; ambientShadowColor = pal.signal; spotShadowColor = pal.signal } .clip(Shape12) .background(if (enabled) Brush.linearGradient(listOf(colors.primary, pal.primary2)) else Brush.linearGradient(listOf(colors.surfaceVariant, colors.surfaceVariant))) .shine(hot, 0.45f) - // Off, its fill can match the surface it sits on: the outline keeps it a button. .then(if (enabled) Modifier else Modifier.border(1.dp, pal.line2, Shape12)) - // The grow and shine alone barely show on the light fill: outline it when a - // controller is on it, as the other controls are. .glideBorder(hot, Shape12, pal.signal) .hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, enabled = enabled) { placed[0]?.takeIf { it.isAttached }?.let { LaunchOrigin.mark(it.boundsInRoot(), placed) } @@ -207,11 +196,6 @@ internal fun PrimaryButton( } } -/** - * [label] while idle; while [progress] is set, a spinner and how far along it is in its place. - * The label stays laid out underneath, invisible, so the button keeps its width. The spinner - * turns until there is a first figure to show. - */ @Composable internal fun ProgressLabel(label: @Composable (Modifier) -> Unit, progress: Float?, fontSize: androidx.compose.ui.unit.TextUnit = 15.sp) { val pal = LocalPalette.current @@ -253,7 +237,6 @@ internal fun Cog(size: androidx.compose.ui.unit.Dp = 48.dp, onClick: () -> Unit) val colors = MaterialTheme.colorScheme val src = remember { MutableInteractionSource() } val hot = rememberHot(src) - // A press spins it on a half turn as the page it opens blooms out of it (PageOrigin). var spins by remember { mutableIntStateOf(0) } val rot by animateFloatAsState((if (hot) 90f else 0f) + spins * 180f, Motion.sp(0.55f), label = "cog") val pal = LocalPalette.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt b/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt index 51c8fdf5a..7557eb249 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt @@ -48,8 +48,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -// Environment variables for Proton games: one shared profile and one per game, in the app's own -// dialog, with each variable's actions in a menu off its value as everywhere else. @Composable fun GameEnvironmentRow( @@ -58,8 +56,6 @@ fun GameEnvironmentRow( initialScope: String = "", ) { var open by remember { mutableStateOf(false) } - // Opened with a pad, the editor starts with focus in it; the dialog is a window of its own and - // would otherwise give the pad nothing to move from. var byPad by remember { mutableStateOf(false) } val inputMode = LocalInputModeManager.current SettingsRow(stringResource(R.string.game_env_title), if (showHint) stringResource(R.string.game_env_hint) else null) { @@ -71,7 +67,6 @@ fun GameEnvironmentRow( if (open) GameEnvironmentEditor(byPad, initialScope) { open = false } } -/** With a pad driving, focus [target] once the dialog is laid out, and keep the dialog in pad mode. */ @OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) @Composable internal fun PadFocus(byPad: Boolean, target: FocusRequester) { @@ -128,7 +123,6 @@ internal fun GameEnvironmentEditor(byPad: Boolean, initialScope: String = "", on PadFocus(byPad, firstFocus) DialogHeader(stringResource(R.string.game_env_eyebrow), stringResource(R.string.game_env_title)) Small(stringResource(R.string.game_env_applies)) - // Which profile: everything shared, or one game's. Panel { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -173,7 +167,6 @@ internal fun GameEnvironmentEditor(byPad: Boolean, initialScope: String = "", on name, value, inherited = !own.containsKey(name), open = menu == "var:$name", enabled = !busy, onToggle = { menu = if (menu == "var:$name") null else "var:$name" }, onDismiss = { if (menu == "var:$name") menu = null }, onEdit = { menu = null; editByPad = dialogInput.inputMode == InputMode.Keyboard; editing = EnvironmentVariableEdit(scope, profileName, name, value ?: "") }, - // A value set here or inherited can be switched off for this profile; one set here can go back to inheriting. onUnset = if (value != null) ({ menu = null; save(current.withEntries(scope, own + (name to null))) }) else null, onRestore = if (own.containsKey(name)) ({ menu = null; save(current.withEntries(scope, own - name)) }) else null, shared = scope.isEmpty(), @@ -182,9 +175,7 @@ internal fun GameEnvironmentEditor(byPad: Boolean, initialScope: String = "", on } } if (busy) LinearProgressIndicator(Modifier.fillMaxWidth()) - // Footer: add a variable, reset the profile, done. Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(top = 4.dp)) { - // Only once the settings are in: a button drawn disabled first comes up without its box. if (current != null) Box { SecondaryButton(stringResource(R.string.game_env_add), enabled = !busy) { menu = if (menu == "add") null else "add" } AnchoredMenu(menu == "add", onDismiss = { if (menu == "add") menu = null }, title = stringResource(R.string.game_env_add)) { first -> @@ -216,12 +207,10 @@ internal fun GameEnvironmentEditor(byPad: Boolean, initialScope: String = "", on private data class EnvironmentVariableEdit(val scope: String, val profileName: String, val name: String, val value: String) -/** The panel the profile and the variables sit in, as a settings group does. */ @Composable private fun Panel(content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit) = Column(Modifier.fillMaxWidth().clip(Shape14).background(MaterialTheme.colorScheme.surface).border(1.dp, LocalPalette.current.line, Shape14), content = content) -/** One variable: its name, what it is set to (and whether that comes from the shared profile), and a menu of what to do with it. */ @Composable private fun VariableRow( name: String, value: String?, inherited: Boolean, open: Boolean, enabled: Boolean, shared: Boolean, @@ -299,7 +288,6 @@ private fun VariableValueEditor(name: String, value: String, onChange: (String) var open by remember(name) { mutableStateOf(false) } var custom by remember(name) { mutableStateOf(option == null || value !in option.choices && option.type != GameEnvironmentOptions.Type.MULTIPLE) } when { - // On/off variables pick from their two values like any other choice: one way to set a value. option != null && option.choices.isNotEmpty() -> { SettingsRow(stringResource(R.string.game_env_value), null, highlighted = open) { Box { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GamepadGeometry.kt b/app/src/main/java/com/droiddeck/launcher/ui/GamepadGeometry.kt index 242f90937..466d57aec 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GamepadGeometry.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GamepadGeometry.kt @@ -2,32 +2,14 @@ package com.droiddeck.launcher.ui import kotlin.math.min -/** - * Where the unfolded screen's gamepad puts each control, for a panel of a given size: plain - * numbers, so the layout is tested without a device (GamepadGeometryTest). - * - * Each thumb pivots at its bottom corner; its main control (a stick or trackpad) sits where it - * rests, diagonally in from the corner, and the right side mirrors the left. - * - * A wide strip (a foldable held flat as a tablet): the second control - the d-pad, the face - * buttons - a slide inward along the arc, the shoulders above, and View, Steam, QAM and Menu - * between the arcs. The reach R is the strip's height, or a third of its width. - * - * A tall panel (held upright): the second control a slide up the arc instead, the shoulders above - * that, View, Steam, QAM and Menu between the two sticks, and Panels - the Deck controls' other - * tabs - between the shoulders. Each side's column S is half the width, or as much as the height - * holds. A wide strip has no room for Panels and leaves it out. - */ object GamepadGeometry { enum class Slot { LEFT_MAIN, LEFT_SECOND, RIGHT_MAIN, RIGHT_SECOND, LEFT_OUTER_SHOULDER, LEFT_INNER_SHOULDER, RIGHT_INNER_SHOULDER, RIGHT_OUTER_SHOULDER, VIEW, STEAM, QAM, MENU, - /** The Deck controls' other tabs: on a tall panel only. */ PANELS, } - /** A control's box: its centre and size, in pixels. */ data class Box(val cx: Float, val cy: Float, val width: Float, val height: Float) { val left get() = cx - width / 2f val top get() = cy - height / 2f @@ -38,19 +20,15 @@ object GamepadGeometry { left < other.right && other.left < right && top < other.bottom && other.top < bottom } - /** The middle buttons, in a row in this order; two by two as on a Deck, View and Menu above Steam and QAM. */ private val middle = listOf(Slot.VIEW, Slot.STEAM, Slot.QAM, Slot.MENU) private val twoByTwo = mapOf(Slot.VIEW to 0, Slot.MENU to 1, Slot.STEAM to 2, Slot.QAM to 3) - /** A panel this shape is laid out as a tall panel (held upright), which has room for Panels; - * anything flatter is a wide strip. */ fun tall(w: Float, h: Float): Boolean = h >= w * 0.45f fun layout(w: Float, h: Float, density: Float): Map<Slot, Box> { if (w <= 0f || h <= 0f) return emptyMap() val pad = 8 * density val buttonH = 40 * density - // A row needs its buttons comfortable; two by two will take them a little narrower. val rowButton = 64 * density val gridButton = 48 * density val gutter = 8 * density @@ -75,14 +53,12 @@ object GamepadGeometry { val mainTop = h - reach * 0.40f - main / 2f val shoulderW = reach * 0.36f val shoulderH = (reach * 0.16f).coerceAtLeast(buttonH) - // Above the main controls, never over them, and inside the panel. val shoulderY = min(h - reach * 0.94f + shoulderH / 2f, mainTop - gutter - shoulderH / 2f) .coerceAtLeast(pad + shoulderH / 2f) pair(Slot.LEFT_MAIN, Slot.RIGHT_MAIN, reach * 0.40f, h - reach * 0.40f, main, main) pair(Slot.LEFT_SECOND, Slot.RIGHT_SECOND, reach * 1.03f, h - reach * 0.30f, second, second) pair(Slot.LEFT_OUTER_SHOULDER, Slot.RIGHT_OUTER_SHOULDER, reach * 0.22f, shoulderY, shoulderW, shoulderH) pair(Slot.LEFT_INNER_SHOULDER, Slot.RIGHT_INNER_SHOULDER, reach * 0.62f, shoulderY, shoulderW, shoulderH) - // Between the arcs: in a row if they fit, else two by two, else one above another. val gap = w - 2 * reach * 1.32f val columns = when { gap >= middle.size * rowButton -> middle.size @@ -102,12 +78,10 @@ object GamepadGeometry { pair(Slot.LEFT_SECOND, Slot.RIGHT_SECOND, side * 0.74f, secondCy, second, second) pair(Slot.LEFT_OUTER_SHOULDER, Slot.RIGHT_OUTER_SHOULDER, side * 0.20f, shoulderY, shoulderW, shoulderH) pair(Slot.LEFT_INNER_SHOULDER, Slot.RIGHT_INNER_SHOULDER, side * 0.57f, shoulderY, shoulderW, shoulderH) - // Between the two sticks at the bottom: two by two, or one above another. val gap = w - 2 * (side * 0.40f + main / 2f) val columns = if (gap >= 2 * gridButton) 2 else 1 val rows = (middle.size + columns - 1) / columns middleGrid(columns, gap - 2 * gutter, h - pad - rows * buttonH - (rows - 1) * 10 * density) - // Between the inner shoulders, where it fits. val panelsW = min(112 * density, w - 2 * (side * 0.57f + shoulderW / 2f) - 2 * gutter) if (panelsW >= buttonH) boxes[Slot.PANELS] = Box(w / 2f, shoulderY, panelsW, buttonH) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt b/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt index eec0f2087..bd850bd6a 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt @@ -62,14 +62,12 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -/** One matched driver pair in the list (DriverPairs), and what this device makes of it. */ class PairRow( val key: String, val name: String, val version: String, val detail: String, val recommended: Boolean, - /** Built for this GPU's family; the others only show when asked. */ val suits: Boolean, val complete: Boolean, val installed: Boolean, @@ -85,13 +83,11 @@ class GpuDriversState( val unsupported: Boolean = false, val auto: Boolean = true, val pairs: List<PairRow> = emptyList(), - /** The pair key being installed, and how far along (-1 = unknown). */ val busy: String? = null, val percent: Int = -1, val autoStatus: String = "", val releaseStatus: String = "", val checking: Boolean = false, - // Advanced: each driver on its own. val linuxRows: List<DriverRow> = emptyList(), val linuxSelected: String = "", val androidRows: List<DriverRow> = emptyList(), @@ -99,10 +95,8 @@ class GpuDriversState( val linuxDownloads: List<DownloadRow> = emptyList(), val androidDownloads: List<DownloadRow> = emptyList(), val canRestoreBundled: Boolean = false, - /** The Android + Linux bundle both drivers are set to ("DD-Turnip 0.1.0"), or null. */ val activeBundle: String? = null, ) { - /** The pair both drivers are set to, if they make one. */ val activePair: PairRow? get() = pairs.firstOrNull { it.active } } @@ -117,16 +111,10 @@ class GpuDriversActions( val onDownloadDriver: (String) -> Unit = {}, val onImportLinux: () -> Unit = {}, val onImportAndroid: () -> Unit = {}, - /** The tab's own import: a bundle, or a single driver of either kind. */ val onImportZip: () -> Unit = {}, val onRestoreBundled: () -> Unit = {}, ) -/** - * Everything about the chip - SoC, GPU, CPU, memory, system, the phone's own Vulkan driver, the - * drivers in use - gathered once, off the main thread, with a button that copies it for a bug - * report. Stepped out of the Components page's device chip; [first] is where a pad starts. - */ @Composable internal fun DeviceDetails(first: FocusRequester) { val colors = MaterialTheme.colorScheme @@ -159,7 +147,6 @@ internal fun DeviceDetails(first: FocusRequester) { } } -/** One part of the device details: its title, then label/value rows. Focusable, so the d-pad walks the list. */ @Composable private fun DeviceInfoSection(section: DeviceInfo.Section) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Hop.kt b/app/src/main/java/com/droiddeck/launcher/ui/Hop.kt index a5ee4c8f2..8260bf40e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Hop.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Hop.kt @@ -29,17 +29,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.droiddeck.launcher.R -// A link from a control in one section to a page in another (the Play sheet's Graphics drivers, -// a game's Components card): the control squashes into a dot that flies to the destination's rail -// item, the rail's selection glides there as it passes the top of its arc, and the page floods -// out of the rail item once it lands. B on that page flies the dot back and puts the source -// surface back as it was, focus on the control that asked. - -/** - * Where a hop came from: the rail item that was showing ([rail]), the surface on it (a sheet, the - * Games page) and its tab, the control's key and its root bounds, and the words for the way back. - * [page] is the page key the hop opened, so leaving it any other way forgets the way back. - */ internal class HopOrigin( val rail: String, val surface: String, @@ -51,46 +40,29 @@ internal class HopOrigin( val page: String, ) -/** The surfaces a hop can start from and come back to. */ internal const val HOP_SURFACE_GAMES = "games" internal const val HOP_SURFACE_DESKTOP = "desktop" internal const val HOP_SURFACE_SHEET = "sheet" internal object Hops { - /** The hop the page on screen was reached by, while B can still undo it. */ var origin by mutableStateOf<HopOrigin?>(null) - /** The pane content a hop is leaving (a rail key): it sinks back as the dot leaves it. */ var sinking by mutableStateOf<String?>(null) - /** Set while a hop's dot is flying: the rail item its selection has already glided to. */ var railPreview by mutableStateOf<String?>(null) - /** - * Set as a page leaves through a hop (either way), so the pane sinks it rather than draining - * it; the pane clears it once the change has started. - */ var linking by mutableStateOf(false) - /** A surface to put back as it was (a sheet re-lifted on its tab), once, and the control to focus. */ var restore by mutableStateOf<HopOrigin?>(null) - /** Per rail item, bumped when a dot lands on it: the item pops. */ val railLanded = mutableStateMapOf<String, Int>() - /** Bumped when a hop's page has flooded in: its target (a layer card) sends out one ring. */ var arrived by mutableIntStateOf(0) - /** Where each rail item sits, root px. */ val railBounds = HashMap<String, Rect>() - /** Where each control that can start a hop sits, root px. */ val sources = HashMap<String, Rect>() - /** - * Hops from the control at [origin].bounds to [origin].dest: [go] opens the page there once the dot lands. - * With animations off there is no dot and no flood, only the page. - */ fun go(origin: HopOrigin, go: () -> Unit) { val railAt = railBounds[origin.dest] val finish: () -> Unit = { @@ -103,7 +75,6 @@ internal object Hops { arrived++ } if (railAt == null || Motion.scale == 0f) { finish(); return } - // The surface the control is on sinks as its dot leaves, and stays down as it goes. sinking = origin.rail Flights.fly(Flight( origin.bounds, to = { railAt.center }, @@ -112,10 +83,6 @@ internal object Hops { )) } - /** - * B on a page reached by a hop: the page sinks, the source surface comes back as it was, and - * the dot flies home from the rail item to the control. False when there is no hop to undo. - */ fun back(close: () -> Unit): Boolean { val o = origin ?: return false origin = null @@ -133,26 +100,18 @@ internal object Hops { return true } - /** The page the hop opened has gone some other way: there is no way back to keep. */ fun forget(pageKey: String?) { val o = origin ?: return if (pageKey != o.page) { origin = null; sinking = null } } - /** Screen density, for the rail item's corner; set by the front end. */ var density = 1f } -/** Shows a rail section directly (no page closing, nothing forgotten): what a hop's way back calls. */ internal val LocalShowSection = androidx.compose.runtime.staticCompositionLocalOf<(String) -> Unit> { {} } -/** Remembers where this control sits, so a hop from it can fly from it and back to it. */ internal fun Modifier.hopSource(key: String): Modifier = onGloballyPositioned { Hops.sources[key] = it.boundsInRoot() } -/** - * "B · back to <source>" at the right of a page's header while it was reached by a hop; a tap is B. - * Nothing when it was not. - */ @Composable internal fun HopBackChip(onBack: () -> Unit) { val o = Hops.origin ?: return diff --git a/app/src/main/java/com/droiddeck/launcher/ui/LaunchFlood.kt b/app/src/main/java/com/droiddeck/launcher/ui/LaunchFlood.kt index 1ede5c008..0f912aa7a 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/LaunchFlood.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/LaunchFlood.kt @@ -44,25 +44,15 @@ import java.lang.ref.WeakReference import kotlin.math.PI import kotlin.math.sin -// Starting a session from a blue button: the button grows until the page is its blue, and the -// loading screen opens on that blue and gathers it into the ball of the throbber. -// Stopping one from the drawer runs it the other way: Stop grows until the page is blue, and the -// front end opens on that blue and draws it back down into the button the session came from. -// Any other way out (backing out of the loading screen, closing the ended screen, the session -// ending) spreads the blue back out of the throbber's ball instead (FloodSpread). -/** Carries the flood's colour to the session, which opens on it. */ const val EXTRA_FLOOD = "com.droiddeck.launcher.FLOOD" -/** Where the last pressed blue button sat, so a session started by it can grow out of it. */ internal object LaunchOrigin { private var bounds: Rect? = null private var button: Any? = null private var at = 0L - /** The last button a session was started from, for a stop to return into. */ private var last: WeakReference<Any>? = null - /** The button a flood is growing out of: it steps aside so only the flood shows. */ var flooding by mutableStateOf<Any?>(null) fun mark(r: Rect, key: Any) { @@ -72,10 +62,6 @@ internal object LaunchOrigin { at = SystemClock.uptimeMillis() } - /** - * Where the button the running session was started from sits now, if it is still on screen; - * it hides for the return, as for the flood. - */ fun takeReturn(): Rect? { val key = last?.get() as? Array<*> ?: return null val c = key.getOrNull(0) as? LayoutCoordinates ?: return null @@ -84,7 +70,6 @@ internal object LaunchOrigin { return c.boundsInRoot() } - /** The button behind a launch starting now, if one was pressed just before; it hides for the flood. */ fun take(): Rect? { val b = bounds?.takeIf { SystemClock.uptimeMillis() - at < 1_500 } if (b != null) flooding = button @@ -94,7 +79,6 @@ internal object LaunchOrigin { } } -/** A session stopped behind a flood: the blue the front end opens on, for a few seconds. */ internal object QuitFlood { private var color: Int? = null private var at = 0L @@ -111,7 +95,6 @@ internal object QuitFlood { } } -/** The page a flood grows over, sinking back a touch as it is covered; [progress] is the flood's. */ @Composable internal fun FloodBehind(progress: () -> Float, content: @Composable () -> Unit) { Box(Modifier.fillMaxSize().graphicsLayer { val k = sinkScale(progress()); scaleX = k; scaleY = k }) { content() } @@ -121,19 +104,10 @@ private fun sinkScale(progress: Float) = 1f - 0.04f * progress private val Gather = CubicBezierEasing(0.6f, 0f, 0.15f, 1f) -/** - * The pressed button, [from] in root px, stretching out to cover the page. Each edge rides its own - * loose spring: the ones with furthest to go set off first and quickest, the ones near the page's - * edge lag and then snap after them, so it pulls like something elastic rather than zooming. It - * goes blobby in flight, its gradient settles into the flat blue of the ball, and [onProgress] - * (0 to 1) lets the page behind sink a little. Calls [onCovered] once nothing but blue shows. - * Swallows touches while it runs. - */ @Composable internal fun LaunchFlood( from: Rect, onProgress: (Float) -> Unit, - /** The button's own colours, where it is not the page's blue one (a red Stop). */ fromColors: Pair<Color, Color>? = null, cornerAtRest: Dp = 12.dp, onCovered: () -> Unit, @@ -142,10 +116,6 @@ internal fun LaunchFlood( val pal = LocalPalette.current val start = fromColors ?: (colors.primary to pal.primary2) VeilRing() - // Left, top, right, bottom: 0 on the button, 1 on the page's edge. Springs overshoot past the - // edge, off the page, so the wobble is felt in the pull. An underdamped spring then swings back - // short of the edge (about 3% at this damping), so once an edge has reached the page's it holds - // there, or the page would show through in a sliver while the next screen opens. val edges = remember { List(4) { Animatable(0f) } } val reached = remember { BooleanArray(4) } fun edge(i: Int) = edges[i].value.let { if (reached[i]) maxOf(it, 1f) else it } @@ -174,7 +144,6 @@ internal fun LaunchFlood( } } } - // Covered once every edge has reached the page's. snapshotFlow { (0 until 4).all { edge(it) >= 1f } }.first { it } covered() runs.forEach { it.join() } @@ -185,15 +154,9 @@ internal fun LaunchFlood( } } -/** - * The flood from a button at [from] with its edges [v] of the way to the page's (left, top, right, - * bottom): the button sinking with the page behind, blobby in flight, its colours [start] giving - * way to the flat [signal] in the first half. - */ private fun DrawScope.drawFlood(from: Rect, v: List<Float>, start: Pair<Color, Color>, signal: Color, cornerAtRest: Float) { val (l, t, r, b) = v val mean = v.sumOf { it.coerceIn(0f, 1f).toDouble() }.toFloat() / 4f - // The button sinks with the page behind (FloodBehind), so the flood leaves from where it is now. val k = sinkScale(mean) val cx = size.width / 2f val cy = size.height / 2f @@ -203,10 +166,8 @@ private fun DrawScope.drawFlood(from: Rect, v: List<Float>, start: Pair<Color, C val bottom = lerp(cy + (from.bottom - cy) * k, size.height, b) val w = (right - left).coerceAtLeast(0f) val h = (bottom - top).coerceAtLeast(0f) - // A button's corners at rest, a blob in flight, square once it fills the page. val blob = sin(PI * mean).toFloat().coerceAtLeast(0f) val corner = lerp(cornerAtRest * (1f - mean), minOf(w, h) * 0.42f, blob) - // The button's gradient gives way to the ball's flat blue in the first half. val mix = (mean * 2f).coerceAtMost(1f) val brush = Brush.linearGradient( listOf(lerp(start.first, signal, mix), lerp(start.second, signal, mix)), @@ -215,12 +176,6 @@ private fun DrawScope.drawFlood(from: Rect, v: List<Float>, start: Pair<Color, C drawRoundRect(brush, topLeft = Offset(left, top), size = Size(w, h), cornerRadius = CornerRadius(corner)) } -/** - * The front end's first frames after a stop: the page in [flood] after a beat, drawn back down - * into the button at [to] (root px, measured before the page sank) on critically damped springs, - * the far edges first, so it sets down on the button without a bounce. [onProgress] (1 to 0) lets - * the page behind rise back. No button to return to: the blue fades. Calls [onLanded] at the end. - */ @Composable internal fun FloodReturn(flood: Color, to: Rect?, onProgress: (Float) -> Unit, onLanded: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -234,7 +189,6 @@ internal fun FloodReturn(flood: Color, to: Rect?, onProgress: (Float) -> Unit, o LaunchedEffect(page != Size.Zero) { if (page == Size.Zero) return@LaunchedEffect progress(1f) - // A beat of plain blue first, so the change of screen does not read as a cut. delay(Motion.ms(220).toLong()) if (to == null || Motion.scale == 0f) { fade.animateTo(0f, Motion.tw(240, easing = FastOutSlowInEasing)) @@ -262,10 +216,6 @@ internal fun FloodReturn(flood: Color, to: Rect?, onProgress: (Float) -> Unit, o } } -/** - * The session's first frames: the page in [flood], drawing in to a circle over [ball] (centre and - * radius in root px, once the throbber is laid out) that lands the size of it. Calls [onLanded] then. - */ @Composable internal fun FloodGather(flood: Color, ball: Pair<Offset, Float>?, onLanded: () -> Unit) { val gather = remember { Animatable(0f) } @@ -275,7 +225,6 @@ internal fun FloodGather(flood: Color, ball: Pair<Offset, Float>?, onLanded: () LaunchedEffect(ball != null) { if (ball == null || started) return@LaunchedEffect started = true - // A beat of plain blue first, so the change of screen does not read as a cut. delay(Motion.ms(90).toLong()) gather.animateTo(1f, Motion.tw(640, easing = Gather)) landed() @@ -292,18 +241,12 @@ internal fun FloodGather(flood: Color, ball: Pair<Offset, Float>?, onLanded: () } } -/** Where the loading screen's throbber ball sits now (centre and radius, root px), for a flood leaving from it. */ internal object ThrobberSpot { var ball: Pair<Offset, Float>? = null } private val Spread = CubicBezierEasing(0.7f, 0f, 0.4f, 1f) -/** - * Leaving a session: [flood] spreading out of [ball] (centre and radius in root px; the middle of - * the screen when there is none) until it covers the screen, the way FloodGather drew it in. Calls - * [onCovered] then, for the front end to open on the same blue. Swallows touches while it runs. - */ @Composable internal fun FloodSpread(flood: Color, ball: Pair<Offset, Float>?, onCovered: () -> Unit) { VeilRing() diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Layout.kt b/app/src/main/java/com/droiddeck/launcher/ui/Layout.kt index 3ddef26c3..9777ee83b 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Layout.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Layout.kt @@ -5,16 +5,11 @@ import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp -// Sizes that decide how the launcher reflows on 4:3 and square screens (480x360 or 480x480 dp) as -// well as on the 16:9 ones it was first laid out for. -/** A page narrower than this reflows: settings rows stack, grids drop columns, posters go. */ val NarrowPaneWidth = 520.dp -/** Whether the page being drawn is narrow - the pane beside the rail on a 4:3 or square screen. */ val LocalNarrowPane = compositionLocalOf { false } -/** Too narrow a screen for a labelled rail: the rail shows icons only. */ @Composable internal fun isNarrowScreen(): Boolean = LocalConfiguration.current.screenWidthDp < 600 diff --git a/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt b/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt index bf64c327b..69835b0ef 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt @@ -110,14 +110,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.roundToInt -// Play's settings, lifted out from under Play instead of opened on a page of their own. At rest a -// lip tucks out under Play and says what it will do. Down (or a pull up, or a tap on the lip) -// stretches the lip, the wall sinks back, and a sheet rises from the pane's foot with Play riding -// its top edge as the handle - StepOut's move, with the pane for a box. B, Up from the chips or a -// tap above folds it away; A on Play launches straight from it, the sheet folding into the pill -// before the launch flood. Open desktop does the same with the desktop's settings. -/** The sheets' tabs (their chips), by id. */ internal object SheetTab { const val DISPLAY = "display" const val CONTROLS = "controls" @@ -128,33 +121,23 @@ internal object SheetTab { const val FILES = "files" } -/** - * Where the open sheet's top edge sits in a pane [paneH] tall (px), for a button [buttonH] tall - * riding it: 60dp down a 360dp pane, the same share of any other, and never so high that the - * button's top comes closer than [minAbove] to the pane's. [ride] is how far below the edge the - * button's centre sits. - */ internal fun sheetTop(paneH: Float, buttonH: Float, minAbove: Float, ride: Float): Float = maxOf(paneH * 60f / 360f, minAbove + buttonH / 2f - ride) -/** A finger's hold on the sheet: where it started (0 rest, 1 open), where it is, whether it has it. */ private class SheetDrag { var from = 0f var progress = 0f var engaged = false } -/** One piece of the sheet's rows fades and rises in on its own beat (a settings group asks for its index). */ internal class SheetBeat(private val pieces: List<Animatable<Float, AnimationVector1D>>) { private var next = 1 fun take(): Int = next++ fun value(i: Int): Float = pieces[i.coerceIn(0, pieces.lastIndex)].value } -/** The sheet whose rows are being laid out, so each settings group can take its beat. */ internal val LocalSheetBeat = staticCompositionLocalOf<SheetBeat?> { null } -/** A settings group's piece of the sheet's entrance; nothing outside a sheet. */ @Composable internal fun Modifier.sheetPiece(): Modifier { val beat = LocalSheetBeat.current ?: return this @@ -172,13 +155,6 @@ private val ChipsTop = 14.dp private val RowsTop = 54.dp private const val PIECES = 12 -/** - * A hero with one big button and the sheet that lifts out from under it. [key] names the sheet - * (a hop comes back to it, the lip remembers what it said); [tabs] are its chips (id to label) and - * [rows] what each holds. [background] sinks and [words] fade as it opens. [button] draws the - * button itself with the click it should make; [onPrimary] is what that click does once the - * sheet is out of the way. [onOpen] reads the settings the rows show. - */ @OptIn(ExperimentalComposeUiApi::class, androidx.compose.animation.ExperimentalAnimationApi::class) @Composable internal fun LiftHero( @@ -187,26 +163,13 @@ internal fun LiftHero( onOpen: () -> Unit, onPrimary: () -> Unit, background: @Composable () -> Unit, - /** - * Drawn over [background] at the pane's own size: the wall's fades, which must not sink with - * it. Given how far the sheet is up (0 to 1), for anything there that should give way to it. - */ scrim: @Composable (lifted: () -> Float) -> Unit = {}, words: @Composable () -> Unit, - /** - * Buttons beside the big one (Desktop's Flathub and +). They stay where they are and fade as - * the sheet lifts: only the big button rides it. - */ beside: @Composable () -> Unit = {}, button: @Composable (onClick: () -> Unit) -> Unit, rows: @Composable (tab: String) -> Unit, modifier: Modifier = Modifier, - /** - * Down on the button lifts the sheet (Play). False: Down goes to the lip, which A lifts, and Down - * from the lip goes on past it to what is under the hero (Desktop's shelves). - */ downOpens: Boolean = true, - /** Told when the sheet starts to lift (true) and once it has gone (false). */ onOpenChange: (Boolean) -> Unit = {}, start: Dp = 44.dp, bottom: Dp = 44.dp, @@ -217,7 +180,6 @@ internal fun LiftHero( val pal = LocalPalette.current val ff = LocalFrontFocus.current val narrow = LocalNarrowPane.current - // Held as states too, so the sheet's focus rule reads what they are now, not at the last composition. val openState = remember { mutableStateOf(false) } var open by openState var shown by remember { mutableStateOf(false) } @@ -225,11 +187,8 @@ internal fun LiftHero( var launching by launchingState var tab by remember { mutableStateOf(0) } var travel by remember { mutableStateOf(1) } - // The control to focus once a sheet put back by a hop's B has lifted, instead of the first chip. var focusOnLift by remember { mutableStateOf<String?>(null) } - // Set as the sheet closes and Play lands: the lip may come back before the sheet has quite gone. var lipBack by remember { mutableStateOf(false) } - // Set as a finger's drag lets go: the sheet is already where it is going. var dragSettled by remember { mutableStateOf(false) } val scope = androidx.compose.runtime.rememberCoroutineScope() var dragJob by remember { mutableStateOf<Job?>(null) } @@ -248,18 +207,15 @@ internal fun LiftHero( val playSrc = remember { MutableInteractionSource() } val playHovered by playSrc.collectIsHoveredAsState() var playFocused by remember { mutableStateOf(false) } - // Set as a pad leaves the sheet to the left for Play, which folds it once Play has focus. val leftForPlay = remember { booleanArrayOf(false) } var heroAt by remember { mutableStateOf(Offset.Zero) } var rest by remember { mutableStateOf<Rect?>(null) } fun focusPlay() { if (ff != null && inputMode.inputMode == InputMode.Keyboard) runCatching { ff.primary.requestFocus() } } fun close() { if (!open) return - // Focus leaves the rows before they go; the sheet lets it go once it is closing. open = false focusPlay() } - // Back from a hop the sheet started (or a page it opened): put it back as it was. LaunchedEffect(Unit) { val r = Hops.restore ?: return@LaunchedEffect if (r.surface != HOP_SURFACE_SHEET || r.rail != key) return@LaunchedEffect @@ -268,7 +224,6 @@ internal fun LiftHero( focusOnLift = r.control open = true } - // Launched: the session covers the screen, and the sheet is put away under it. val lifecycle = androidx.compose.ui.platform.LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { val obs = androidx.lifecycle.LifecycleEventObserver { _, e -> @@ -282,7 +237,6 @@ internal fun LiftHero( LaunchedEffect(open, launching) { if (!dragSettled) { dragJob?.cancel(); dragJob = null } if (dragSettled) { - // A finger brought it here and has already animated it: only what opening needs. dragSettled = false if (open) { if (inputMode.inputMode == InputMode.Keyboard) { withFrameNanos { }; runCatching { chipFocus[tab].requestFocus() } } @@ -295,10 +249,8 @@ internal fun LiftHero( onOpen() fold.snapTo(0f); foldTop.snapTo(0f) coroutineScope { - // Pull: the lip stretches down as the wall sinks back and dims. launch { pull.animateTo(24f, Motion.sp(0.7f, 380f)) } launch { sink.animateTo(1f, Motion.tw(260)) } - // Lift: the sheet rises with Play riding its top edge; the lip lets go. launch { delay(Motion.ms(120).toLong()) launch { lipGone.animateTo(1f, Motion.tw(140)) } @@ -307,7 +259,6 @@ internal fun LiftHero( launch { ride.animateTo(1f, Motion.sp(0.7f, 380f)) } launch { hot.snapTo(1f); delay(Motion.ms(100).toLong()); hot.animateTo(0f, Motion.tw(200)) } } - // Each piece on its beat from the Lift, as the reel has it: 260ms + 55ms a piece after it starts. pieces.forEachIndexed { i, p -> launch { delay(Motion.ms(120 + 260 + i * 55).toLong()); p.animateTo(1f, Motion.tw(220)) } } if (inputMode.inputMode == InputMode.Keyboard) launch { val control = focusOnLift @@ -319,7 +270,6 @@ internal fun LiftHero( } } } else if (launching) { - // Play from the sheet: the rows go, the sheet folds into the pill, then the launch. coroutineScope { pieces.forEach { launch { it.animateTo(0f, Motion.tw(90)) } } launch { delay(Motion.ms(60).toLong()); fold.animateTo(1f, Motion.sp(1f, 420f)) } @@ -327,8 +277,6 @@ internal fun LiftHero( } shown = false onPrimary() - // The session covers the screen and the sheet is put away under it (ON_STOP). If - // nothing started, put it away anyway. delay(3_000) launching = false } else if (shown) { @@ -337,7 +285,6 @@ internal fun LiftHero( launch { delay(Motion.ms(60).toLong()); lift.animateTo(0f, Motion.sp(1f, 380f)) } launch { delay(Motion.ms(100).toLong()); ride.animateTo(0f, Motion.sp(0.55f, 300f)) } launch { sink.animateTo(0f, Motion.tw(300)) } - // The lip tucks back under Play as it lands, not once every spring has settled. launch { delay(Motion.ms(420).toLong()); lipGone.snapTo(0f); lipBack = true } } shown = false @@ -345,20 +292,16 @@ internal fun LiftHero( lipGone.snapTo(0f) hot.snapTo(1f) } else { - // Snapped shut under a session: everything back at rest, no animation. listOf(pull, sink, lift, ride, lipGone, fold, foldTop).forEach { it.snapTo(0f) } pieces.forEach { it.snapTo(0f) } hot.snapTo(1f) } } BackHandler(enabled = open && !launching) { close() } - // The ring keeps out of the lift and the fold, and comes back on whatever has focus once - // they are done: Play rides the sheet on a layer, and the ring hung where Play had been. if (ride.isRunning || (shown && !open)) VeilRing() val openNow = open || shown LaunchedEffect(openNow) { onOpenChange(openNow) } - // The lip: there while Play has a pad or a pointer on it, or always with touch alone. val touchOnly = inputMode.inputMode == InputMode.Touch val lipFocus = remember { FocusRequester() } val settingsLabel = stringResource(R.string.drawer_page_settings) @@ -366,7 +309,6 @@ internal fun LiftHero( val lipWanted = !open && (!shown || lipBack) && (playFocused || playHovered || touchOnly || lipFocused) LaunchedEffect(lipWanted) { if (lipWanted) { - // Back from the sheet it comes straight away; otherwise a beat after Play has focus. if (!lipBack) delay(Motion.ms(160).toLong()) lipShow.animateTo(1f, Motion.tw(220)) } else { @@ -375,10 +317,8 @@ internal fun LiftHero( } } - // How far the sheet travels, for a finger's drag to follow: set once the hero is measured. val travelPx = remember { floatArrayOf(1f) } val drag = remember { SheetDrag() } - /** The sheet [p] of the way up (0 at rest, 1 open) under a finger. */ fun follow(p: Float) { drag.progress = p scope.launch { @@ -387,7 +327,6 @@ internal fun LiftHero( pieces.forEach { it.snapTo(p.coerceIn(0f, 1f)) } } } - /** A finger takes the sheet where it is: up from rest, or down from open. */ fun beginDrag() { dragJob?.cancel(); dragJob = null dragSettled = false @@ -400,10 +339,6 @@ internal fun LiftHero( onOpenNow.value() } } - /** - * The finger lets go with [v] px/s (up negative): a flick goes the way it was flicked; a slow - * drag stays once it is a third of the way from where it started. - */ fun settleDrag(v: Float) { drag.engaged = false val flick = with(density) { 600.dp.toPx() } @@ -427,8 +362,6 @@ internal fun LiftHero( } if (open != opens) open = opens else dragSettled = false } - // Pulled down past the top of the rows, the sheet itself comes down with the finger; up again - // before letting go, it goes back up before the rows scroll. val rowsPull = remember { object : androidx.compose.ui.input.nestedscroll.NestedScrollConnection { override fun onPreScroll(available: Offset, source: androidx.compose.ui.input.nestedscroll.NestedScrollSource): Offset { @@ -452,9 +385,6 @@ internal fun LiftHero( } BoxWithConstraints( modifier.fillMaxSize().onGloballyPositioned { heroAt = it.boundsInRoot().topLeft } - // A swipe anywhere on the hero moves the sheet under the finger - up from rest, down - // when open - and lets go open or shut by how far it went and how fast. A swipe that - // starts the wrong way (down at rest) is left alone, for the page under it. .pointerInput(Unit) { val tracker = androidx.compose.ui.input.pointer.util.VelocityTracker() var decided = false @@ -466,7 +396,6 @@ internal fun LiftHero( if (launching) return@detectVerticalDragGestures if (!decided) { decided = true - // Up at rest opens; down when open closes. Anything else is not for the sheet. if (if (open) amount > 0f else amount < 0f) beginDrag() } if (!drag.engaged) return@detectVerticalDragGestures @@ -482,17 +411,12 @@ internal fun LiftHero( val restRect = rest?.translate(-heroAt) val buttonH = restRect?.height ?: with(density) { 56.dp.toPx() } val openTop = sheetTop(h, buttonH, with(density) { 16.dp.toPx() }, with(density) { 3.dp.toPx() }) - // Play's raised place: its centre 3dp under the sheet's edge, same left inset. val raise = restRect?.let { (openTop + with(density) { 3.dp.toPx() } - buttonH / 2f) - it.top } ?: 0f travelPx[0] = (h - openTop).coerceAtLeast(1f) - // The wall behind sinks back about the pane's centre. val foot = h Box(Modifier.fillMaxSize().clipToBounds().graphicsLayer { val k = 1f - 0.05f * sink.value; scaleX = k; scaleY = k }) { background() } - // The fades stay put: scaled with the wall they stopped short of the pane's edges and left - // a bright band of unfaded wall down the side and along the top. scrim { sink.value } - // The dim; a tap on it (above the sheet) folds the sheet away. if (shown || open) Box( Modifier.fillMaxSize() .graphicsLayer { alpha = sink.value }.background(Color.Black.copy(alpha = 0.42f)) @@ -500,7 +424,6 @@ internal fun LiftHero( .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, enabled = open && !launching) { close() }, ) - // The sheet: inset at the sides, rounded at the top, no bottom edge - the pane's foot is its foot. if (shown) { val top = lerp(foot, openTop, lift.value) val sheetLeft = inset @@ -526,7 +449,6 @@ internal fun LiftHero( val rimColor = pal.signal.copy(alpha = lerp(0.55f, 0.9f, hot.value)) if (ft > 0f) drawPath(path, rimColor, style = Stroke(rim)) else { - // Sides and top only: the sheet runs on under the pane's foot. rimPath.reset() rimPath.moveTo(l, b) rimPath.lineTo(l, t + c) @@ -537,40 +459,31 @@ internal fun LiftHero( drawPath(rimPath, rimColor, style = Stroke(rim)) } } - // What the sheet holds: the chips to the right of Play, the rows under them. val chipsX = with(density) { if (narrow || pill == null) 16.dp.toPx() else pill.right + 20.dp.toPx() - sheetLeft } val chipsY = with(density) { if (narrow && pill != null) (pill.bottom - openTop) + 10.dp.toPx() else ChipsTop.toPx() } - // The rows start under the chips, which are 44dp to touch: from 54dp they ran under the - // chips' feet and, scrolled, covered them. val rowsY = with(density) { maxOf(RowsTop.toPx(), chipsY + 52.dp.toPx()) } val sheetW = sheetRight - sheetLeft Box( Modifier .offset { IntOffset(sheetLeft.roundToInt(), top.roundToInt()) } .size(with(density) { sheetW.toDp() }, with(density) { (foot - openTop).coerceAtLeast(0f).toDp() }) - // Touches on the sheet stay on it; none reach the dim behind. .pointerInput(Unit) { detectTapGestures { } } .controllerBack { close() } .bumpers( - // LB / RB: focus goes to the new tab's chip, so a pad on a row (which goes with - // its tab) is never left with nothing. onPrevious = { if (tab > 0) { travel = -1; tab--; runCatching { chipFocus[tab].requestFocus() } } }, onNext = { if (tab < tabs.lastIndex) { travel = 1; tab++; runCatching { chipFocus[tab].requestFocus() } } }, ) - // A pad stays in the sheet until it folds; Left from it reaches Play, the handle, and folds it. .focusProperties { exit = { dir -> if (!openState.value || launchingState.value) FocusRequester.Default else if (dir == FocusDirection.Left && ff != null) { leftForPlay[0] = true; ff.primary } else FocusRequester.Cancel } } .focusGroup(), ) { Box( Modifier .offset { IntOffset(chipsX.roundToInt(), chipsY.roundToInt()) } - // Over the rows, whatever they scroll under them. .zIndex(1f) .width(with(density) { (sheetW - chipsX - 64.dp.toPx()).coerceAtLeast(0f).toDp() }) .graphicsLayer { val v = pieces[0].value; alpha = v; translationY = (1f - v) * 6.dp.toPx() } - // Up from the chips folds the sheet (a finger drags it down, see the hero's drag). .onPreviewKeyEvent { e -> val k = e.nativeKeyEvent if (k.keyCode == KeyEvent.KEYCODE_DPAD_UP) { if (k.action == KeyEvent.ACTION_DOWN) close(); true } else false @@ -600,8 +513,6 @@ internal fun LiftHero( CompositionLocalProvider(LocalSheetBeat provides remember { SheetBeat(pieces) }) { Column( Modifier.fillMaxSize() - // Focus never lands in rows on their way out. Same group always, only - // its rule changes (canFocus here made the scroll's own group a stop). .focusProperties { enter = { if (leavingNow.value) FocusRequester.Cancel else FocusRequester.Default } } .focusGroup() .verticalScroll(rememberScrollState()) @@ -625,7 +536,6 @@ internal fun LiftHero( } } - // The words and the button, where they always were; the button rides the sheet's edge. Column(Modifier.align(Alignment.BottomStart).padding(start = start, bottom = bottom, end = 20.dp)) { Box(Modifier.graphicsLayer { alpha = 1f - sink.value }) { words() } Spacer(Modifier.height(18.dp)) @@ -635,7 +545,6 @@ internal fun LiftHero( .onGloballyPositioned { rest = it.boundsInRoot() } .graphicsLayer { translationY = raise * ride.value } .onFocusChanged { - // Left out of the sheet (from the chips or a row) folds it, as Up from the chips does. if (it.hasFocus && leftForPlay[0]) { leftForPlay[0] = false; if (openState.value && !launchingState.value) close() } playFocused = it.hasFocus } @@ -658,7 +567,6 @@ internal fun LiftHero( if (open || shown) { launching = true; open = false } else onPrimary() } } - // Gone with the words while the sheet is up; nothing a pad or a finger can reach then. val besideShown = !(open || shown) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), @@ -668,9 +576,7 @@ internal fun LiftHero( } } - // The lip, under Play: stretched by the pull, gone as the sheet lifts. val r0 = restRect - // A lip a pad can reach stays composed (unseen at rest) so Down always has it to land on. if (r0 != null && (lipShow.value > 0.01f || pull.value > 0.5f || (!downOpens && !open && !shown))) { var handleW by remember { mutableStateOf(0f) } val lipH = 32.dp + pull.value.dp @@ -680,7 +586,6 @@ internal fun LiftHero( .offset { IntOffset((r0.center.x - handleW / 2f).roundToInt(), (r0.bottom + raise * ride.value + (lipShow.value - 1f) * 8.dp.toPx()).roundToInt()) } .onSizeChanged { handleW = it.width.toFloat() } .graphicsLayer { alpha = lipShow.value * (1f - lipGone.value) } - // 44dp to touch, with a 32dp visible handle. .heightIn(min = 44.dp).testTag("sheet-open") .semantics { contentDescription = settingsLabel } .then( @@ -697,10 +602,6 @@ internal fun LiftHero( } } -/** - * The sheet's handle, tucked under Play: "Settings" and a chevron that bobs up every so often, - * saying there is a drawer to pull up. Still with animations off. - */ @Composable private fun Handle(height: Dp) { val colors = MaterialTheme.colorScheme @@ -708,7 +609,6 @@ private fun Handle(height: Dp) { val shape = RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp) val still = Motion.scale == 0f val bob = androidx.compose.animation.core.rememberInfiniteTransition(label = "handle") - // A quick lift and settle, then a rest: a nudge, not a pulse. val lift by bob.animateFloat( 0f, 0f, androidx.compose.animation.core.infiniteRepeatable( @@ -736,7 +636,6 @@ private fun Handle(height: Dp) { } } -/** A 1dp outline down the sides and along the foot, none along the top (the lip tucks under Play). */ private fun Modifier.drawBorderSidesAndFoot(color: Color, corner: Dp): Modifier = this.drawWithContent { drawContent() @@ -753,31 +652,21 @@ private fun Modifier.drawBorderSidesAndFoot(color: Color, corner: Dp): Modifier drawPath(p, color, style = Stroke(sw)) } -/** - * The sheet's chips: equal capsules with one selection gliding under them (the stores' chip row), - * LB and RB a chip at a time, a tap or A on a far one a hop as a drop. A pad picks a chip by - * moving onto it, and comes into the row on the picked one. [first] takes focus as the sheet - * opens. Too narrow for equal chips, the row scrolls. - */ @OptIn(ExperimentalComposeUiApi::class) @Composable internal fun SheetChips(labels: List<String>, selected: Int, focus: List<FocusRequester>? = null, onPick: (Int) -> Unit) { val pal = LocalPalette.current val bounds = remember { mutableStateMapOf<Int, Rect>() } val tint = rememberUpdatedState(pal.signal) - // Each chip its own requester: one moved from chip to chip under a focused chip lost the ring. val chips = focus ?: remember(labels.size) { List(labels.size) { FocusRequester() } } var within by remember { mutableStateOf(false) } - // LB / RB with a pad on the chips: focus goes with the selection. LaunchedEffect(selected) { if (within) runCatching { chips[selected].requestFocus() } } BoxWithConstraints(Modifier.fillMaxWidth().onFocusChanged { within = it.hasFocus }) { val equal = maxWidth / labels.size.coerceAtLeast(1) >= 96.dp val scroll = rememberScrollState() - // Room inside the scroll for the focus ring, which the scroll's clip would otherwise cut. Box(if (equal) Modifier.fillMaxWidth() else Modifier.edgeFade(scroll).horizontalScroll(scroll).padding(3.dp)) { Row( horizontalArrangement = Arrangement.spacedBy(6.dp), - // Up from the rows lands on the picked chip, not whichever sits above: landing picks. modifier = (if (equal) Modifier.fillMaxWidth() else Modifier) .focusProperties { enter = { chips.getOrNull(selected) ?: FocusRequester.Default } } .focusGroup(), @@ -789,7 +678,6 @@ internal fun SheetChips(labels: List<String>, selected: Int, focus: List<FocusRe SheetChip(label, i == selected, m.onFocusChanged { if (it.isFocused && i != selected) onPick(i) }) { onPick(i) } } } - // Over the chips: under them, their rest fill hid it as it crossed them. ChipGlide(selected, bounds[selected], tint) } } @@ -802,8 +690,6 @@ private fun SheetChip(label: String, on: Boolean, modifier: Modifier, onClick: ( val src = remember { MutableInteractionSource() } val hot = rememberHot(src) val shape = RoundedCornerShape(99.dp) - // The row's glide draws the picked chip's look by touch only; with a pad it draws its own rest - // look, and the bar says which is picked. val under = on && LocalInputModeManager.current.inputMode != InputMode.Keyboard Box( contentAlignment = Alignment.Center, @@ -819,10 +705,6 @@ private fun SheetChip(label: String, on: Boolean, modifier: Modifier, onClick: ( } } -/** - * A read-only line that is a link: [title], [value] under it, and "[link] ↗" at the right in the - * signal blue - not a picker, so no value box. A (or a tap) follows it. [id] is its focus key. - */ @Composable internal fun LinkRow(title: String, value: String, link: String, id: String, arrow: String = "↗", onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt index f51d7cb14..ac9a0524a 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt @@ -66,7 +66,6 @@ fun ConfirmDialog(title: String, text: String, confirm: String, onConfirm: () -> ) } -/** Setup's Reset all settings, with the choice to clear game profiles too (off unless ticked). */ @Composable fun ResetSettingsDialog(onConfirm: (gameProfiles: Boolean) -> Unit, onDismiss: () -> Unit) { var gameProfiles by remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt index 3a03fe305..35c7a1765 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt @@ -30,18 +30,15 @@ import com.droiddeck.launcher.runtime.DeckyManager import com.droiddeck.launcher.session.SessionPrefs import com.droiddeck.launcher.session.SessionService -/** [tag] is shown beside the name: [BUNDLED], [DOWNLOADED], [IMPORTED], or "" for Auto / Runtime default. */ class DriverRow(val id: String, val name: String, val detail: String, val removable: Boolean, val tag: String = "") { companion object { const val BUNDLED = "BUNDLED" const val DOWNLOADED = "DOWNLOADED" const val IMPORTED = "IMPORTED" - /** One half of an Android + Linux bundle: picked and deleted together with the other. */ const val BUNDLE = "ANDROID + LINUX" } } -/** A [DriverRow.tag] as shown, in the app's language. */ @Composable internal fun driverTagLabel(tag: String): String = when (tag) { DriverRow.BUNDLED -> stringResource(R.string.driver_tag_bundled) @@ -51,7 +48,6 @@ internal fun driverTagLabel(tag: String): String = when (tag) { else -> tag } -/** A release driver (Banners-Turnip, WinNative) that is not installed yet; [key] is its asset name. */ class DownloadRow(val key: String, val label: String, val detail: String, val progress: Int? = null) class ModeSettings( @@ -60,61 +56,43 @@ class ModeSettings( val panelSize: Pair<Int, Int> = 1280 to 720, val hdr: Boolean, val hdrReason: String?, - /** The GPU drivers in use, as the row that opens them on the Components page says it. */ - /** The drivers line; null shows Auto. */ val gpuDrivers: String? = null, - /** Frames per second the session is capped at; 0 = none. */ val fpsLimit: Int = 0, val upscaler: Int = 0, val upscaleSharpness: Int = 75, val touchMode: String, val suspendPolicy: String, val steamDownloadsInBackground: Boolean = false, - /** Steam only; null hides it. */ val zramCompression: Boolean? = null, - /** Null until checked off the main thread. */ val zramSupported: Boolean? = null, val pipSupported: Boolean = false, val pipAutoEnter: Boolean = false, - /** Steam only. */ val oscMode: String?, - /** Steam only: whether single and double Back actions are swapped. */ val backActionsInverted: Boolean = false, val directAudio: Boolean?, val mic: Boolean?, val gameStorage: String? = null, val storageOptions: List<com.droiddeck.launcher.session.GameStorage.Option> = emptyList(), - /** Steam only: record allocation and sampled storage timings in the next session's Share logs. */ val storageDiagnostics: Boolean = false, val fexPreset: String? = null, - /** Steam only: Force SSBS for Proton games (SessionPrefs.forceSsbs). */ val forceSsbs: Boolean = false, - /** Steam only: SessionPrefs.SYNC_* chosen for Proton games; null outside Steam. */ val syncBackend: String? = null, val stretch16x9: Boolean? = null, val steamRepairQueued: Boolean = false, - /** Steam: the QAM's performance overlay (mangoapp) is started. */ val mangoapp: Boolean = true, - /** Steam only: what the pad is to the client (SessionPrefs.CONTROLLER_*); null outside Steam. */ val steamController: String? = null, - /** Unfolded controls (Steam only; null hides the row). */ val unfoldedControls: Boolean? = null, - /** Steam only: start a Steam session when DroidDeck opens. */ val runSteamAtStartup: Boolean = false, - /** Null outside Steam; the saved choice is separate from Android's access and device switch. */ val wifiDiscovery: Boolean? = null, val wifiDiscoveryPermission: Boolean = false, val wifiDiscoveryLocation: Boolean = false, val wifiDiscoveryAsked: Boolean = false, val wifiDiscoveryBlocked: Boolean = false, - /** Latest Banners-Turnip release: what each driver menu offers to download, and the refresh line. */ - /** Steam only: Decky Loader is managed from the Steam session settings. */ val deckyInstalled: String? = null, val deckyLatestRelease: DeckyManager.Release? = null, val deckyChecking: Boolean = false, val deckyStage: String? = null, val deckyPercent: Int = -1, - /** The stage is the Decky plugin's binary download. */ val deckyDownloadingBinary: Boolean = false, val deckyEnabled: Boolean = false, val deckySessionRunning: Boolean = false, @@ -123,7 +101,6 @@ class ModeSettings( class ModeSettingsActions( val onResolution: (String) -> Unit, val onHdr: (Boolean) -> Unit, - /** Opens the GPU drivers on the Components page: they are shared by every session. */ val onGpuDrivers: () -> Unit = {}, val onFpsLimit: (Int) -> Unit = {}, val onUpscaler: (Int) -> Unit = {}, @@ -159,15 +136,10 @@ class ModeSettingsActions( val onDismiss: () -> Unit, ) -/** - * The chips of a mode's lifted sheet, in order: Steam's six; the desktop's Display, Controls and - * Session (its Files chip is the Desktop page's own). - */ internal fun modeSheetTabs(steam: Boolean): List<String> = if (steam) listOf(SheetTab.STEAM, SheetTab.DISPLAY, SheetTab.CONTROLS, SheetTab.AUDIO, SheetTab.SESSION, SheetTab.LIBRARY) else listOf(SheetTab.DISPLAY, SheetTab.CONTROLS, SheetTab.SESSION) -/** A sheet tab's chip label. */ @Composable internal fun sheetTabLabel(tab: String): String = when (tab) { SheetTab.DISPLAY -> stringResource(R.string.drawer_page_display) @@ -179,19 +151,13 @@ internal fun sheetTabLabel(tab: String): String = when (tab) { else -> stringResource(R.string.setup_tool_files) } -/** - * Rows the front end adds to a sheet's tabs: frame generation (Display), Performance (Session) and - * the Graphics drivers link to Components (Display, given the drivers' summary). - */ class SheetExtras( val frameGen: @Composable () -> Unit = {}, val performance: @Composable () -> Unit = {}, val drivers: @Composable (summary: String?) -> Unit = {}, - /** The Steam tab's account rows (offline mode), from the front end's own state. */ val account: @Composable (host: MenuHost) -> Unit = {}, ) -/** One tab of a mode's lifted sheet: the rows the settings page had, grouped by what they are for. */ @Composable fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras: SheetExtras = SheetExtras()) { val steam = s.mode == SessionService.MODE_STEAM @@ -331,7 +297,6 @@ fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras: if (tab == SheetTab.LIBRARY && steam) { if (s.gameStorage != null) SettingsGroup(stringResource(R.string.mode_storage)) { val custom = s.gameStorage.isNotEmpty() && s.gameStorage != "off" && s.storageOptions.none { it.path == s.gameStorage } - // Each choice: its path, its menu line, the chip's shorter line, and the label it is saved with. class StorageChoice(val path: String, val label: String, val chip: String, val saved: String) val internal = stringResource(R.string.mode_storage_internal) val options = buildList { @@ -478,10 +443,6 @@ fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras: ) } -/** - * The options that apply to every Proton - how Wine syncs, FEX's preset, Force SSBS and the games' - * environment - for the Components page's Proton layer. - */ @Composable internal fun ProtonOptions( syncBackend: String, fexPreset: String, forceSsbs: Boolean, @@ -508,10 +469,6 @@ internal fun ProtonOptions( GameEnvironmentRow() } -/** - * Uninstall for Decky Loader: are you sure steps out of it in red, Cancel first, so a stray A - * costs nothing. - */ @Composable private fun DeckyUninstall(enabled: Boolean, onUninstall: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -538,8 +495,6 @@ private fun DeckyUninstall(enabled: Boolean, onUninstall: () -> Unit) { } } -/** Width × height for the session, with the common handheld shapes one tap away. */ -/** Entry for the user's SteamGridDB API key; the saved key is never shown back. */ @Composable internal fun SgdbKeyDialog(hasKey: Boolean, onSave: (String) -> Unit, onDismiss: () -> Unit) { var key by remember { mutableStateOf("") } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PackageRow.kt b/app/src/main/java/com/droiddeck/launcher/ui/PackageRow.kt index 2bf44f30c..20abe70a3 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PackageRow.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PackageRow.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.ui -/** Package metadata shown on an emulator's own detail page. */ class PackageRow( val id: String, val kind: String, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PageFlood.kt b/app/src/main/java/com/droiddeck/launcher/ui/PageFlood.kt index ab8f4ff34..e0e95343d 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PageFlood.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PageFlood.kt @@ -42,38 +42,20 @@ import kotlinx.coroutines.launch import kotlin.math.PI import kotlin.math.sin -// A page opened from a control (the cog beside Play) grows out of it as a session grows out of -// Play (LaunchFlood): the control stretches out over the pane on loose springs, the far edges -// first, and the page rises into it once it is covered. Back draws it down into the control again. -/** - * Where a page opened from: the control's bounds and corner radius, in px, and the rail item that - * was showing it ([owner]), so the page only draws back into it when that content returns. [art] - * is the control's picture for the tile (a drawable id, or an image Coil loads), if it has one. - */ internal class Origin(val bounds: Rect, val corner: Float, val owner: String? = null, val art: Any? = null) { fun translate(by: Offset) = Origin(bounds.translate(by), corner, owner, art) fun ownedBy(key: String) = Origin(bounds, corner, key, art) } -/** How a page that opened from a control leaves. */ internal enum class PageExit { - /** Back into the control it came from, which is on screen again. */ Drain, - /** Out through a link into another section, which takes the page there itself. */ Hop, - /** Away with the pane, as any page goes: the control it came from is not coming back. */ Sink, } -/** Why a page is leaving: an ordinary change of page, or a link into another section. */ internal enum class PageLeave { Ordinary, Link } -/** - * The exit for a page opened from a control on [owner]'s content, leaving for [incoming]. It only - * draws back into the control when the content that control belongs to is what comes back; a page - * left for another rail item or another page drained into a control that was no longer there. - */ internal fun pageExit(owner: String?, incoming: String, leave: PageLeave): PageExit = when { owner != null && incoming == owner -> PageExit.Drain leave == PageLeave.Link -> PageExit.Hop @@ -84,57 +66,37 @@ internal object PageOrigin { private var origin: Origin? = null private var at = 0L - /** The control a page is about to open from, in root coordinates. */ fun mark(o: Origin) { origin = o at = SystemClock.uptimeMillis() } - /** The control just pressed, once: stale after a second, so a page opened another way never floods. */ fun take(): Origin? = origin.takeIf { SystemClock.uptimeMillis() - at < 1_000 }.also { origin = null } } -/** How long a leaving page is kept for its flood to draw back into the control. */ internal const val PAGE_RETURN_MS = 900 -/** - * [content], a page that opened from the control at [from] (in this element's coordinates): the - * control's tile stretching over it until covered, then the page rising in as the tile fades. - * Once [leaving], the reverse: the tile comes back over the page and draws down into the control. - * A page that [sinks] instead goes with the pane and draws no tile at all. - * The focus ring stays out of it until it is done. - */ @Composable internal fun PageFlood( from: Origin, leaving: Boolean, - /** What the tile shows instead of the control's fill: a game card's art, cropped to the tile as it grows. */ art: Painter? = null, sinks: Boolean = false, - /** - * Where on the page the control's tile lands (root px), when the page has its own picture of - * the control - a game page's hero art: the tile grows onto it instead of over the whole pane, - * corner to corner ([ontoCorner] there), and the page comes in around it. Back leaves from it. - * Null, or off screen: the pane, as any page. - */ onto: (() -> Rect?)? = null, ontoCorner: androidx.compose.ui.unit.Dp = 0.dp, content: @Composable () -> Unit, ) { val pal = LocalPalette.current val glide = LocalFocusGlide.current - // Left, top, right, bottom: 0 on the control, 1 on the pane's edge. val edges = remember { List(4) { Animatable(0f) } } val shown = remember { Animatable(0f) } val tile = remember { Animatable(1f) } var size by remember { mutableStateOf(Size.Zero) } var at by remember { mutableStateOf(Offset.Zero) } - // Where this flood lands (in its own space), read as it opens and again as it leaves. var target by remember { mutableStateOf<Rect?>(null) } val density = androidx.compose.ui.platform.LocalDensity.current LaunchedEffect(size != Size.Zero, leaving, sinks) { if (sinks) { - // Whatever the flood in was doing, the page is shown whole and leaves with the pane. tile.snapTo(0f) shown.snapTo(1f) glide?.hidden = false @@ -142,15 +104,12 @@ internal fun PageFlood( } if (size == Size.Zero) return@LaunchedEffect glide?.hidden = true - // The page lays out under the tile at once, so its picture is there within a frame or two. target = onto?.let { get -> kotlinx.coroutines.withTimeoutOrNull(300) { snapshotFlow { get() }.first { it != null } } }?.translate(-at)?.takeIf { it.height > 1f && it.overlaps(Rect(Offset.Zero, size)) } if (target != null) { coroutineScope { if (!leaving) { - // Onto the page's picture, every edge together on one firm spring: no blob, and - // nowhere to go past it. val runs = edges.map { launch { it.animateTo(1f, Motion.sp(0.85f, 320f)) } } snapshotFlow { edges.all { it.value >= 0.9f } }.first { it } launch { shown.animateTo(1f, Motion.tw(240)) } @@ -202,7 +161,6 @@ internal fun PageFlood( Modifier.fillMaxSize().clipToBounds().onSizeChanged { size = Size(it.width.toFloat(), it.height.toFloat()) } .onGloballyPositioned { at = it.positionInRoot() }, ) { - // Landing on the page's picture, the page must not rise: its picture would move off the tile. val rise = if (onto != null) 0f else 1f Box(Modifier.fillMaxSize().graphicsLayer { alpha = shown.value; translationY = (1f - shown.value) * 14.dp.toPx() * rise }) { content() } val onCorner = with(density) { ontoCorner.toPx() } @@ -212,13 +170,8 @@ internal fun PageFlood( } } -/** - * The control's tile with its edges [v] of the way to the pane's (left, top, right, bottom): - * sinking with the page behind, blobby in flight, square once it fills the pane. - */ private fun DrawScope.drawTile( from: Origin, v: List<Float>, fill: Color, line: Color, alpha: Float, art: Painter? = null, - /** Lands on this instead of the pane (a page's picture of the control), corner [toCorner]. */ to: Rect? = null, toCorner: Float = 0f, ) { val (l, t, r, b) = v @@ -229,7 +182,6 @@ private fun DrawScope.drawTile( left = lerp(f.left, to.left, l); top = lerp(f.top, to.top, t) right = lerp(f.right, to.right, r); bottom = lerp(f.bottom, to.bottom, b) } else { - // The control sinks with the page behind it (scaled to 0.95 about the pane's centre). val k = 1f - 0.05f * mean val cx = size.width / 2f val cy = size.height / 2f @@ -240,14 +192,11 @@ private fun DrawScope.drawTile( } val w = (right - left).coerceAtLeast(0f) val h = (bottom - top).coerceAtLeast(0f) - // Onto a picture it keeps a card's shape, corner to corner; over the pane it blobs in flight. val corner = if (to != null) lerp(from.corner, toCorner, mean).coerceAtMost(minOf(w, h) / 2f) else lerp(from.corner * (1f - mean), minOf(w, h) * 0.42f, sin(PI * mean).toFloat().coerceAtLeast(0f)) drawRoundRect(fill, Offset(left, top), Size(w, h), CornerRadius(corner), alpha = alpha) val pic = art?.intrinsicSize?.takeIf { it.isSpecified && it.width > 0f && it.height > 0f } if (art != null && pic != null && w > 0f && h > 0f) { - // The art covers the tile at every size (a centre crop), so a portrait card opens into the - // wide pane without stretching. val k = maxOf(w / pic.width, h / pic.height) val dw = pic.width * k val dh = pic.height * k @@ -255,7 +204,6 @@ private fun DrawScope.drawTile( translate(left + (w - dw) / 2f, top + (h - dh) / 2f) { with(art) { draw(Size(dw, dh), alpha = alpha) } } } } - // The control's outline, kept while it is still the size of one. val edge = (1f - mean * 3f).coerceIn(0f, 1f) if (edge > 0f) drawRoundRect(line, Offset(left, top), Size(w, h), CornerRadius(corner), alpha = alpha * edge, style = Stroke(1.dp.toPx())) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PcKeyboard.kt b/app/src/main/java/com/droiddeck/launcher/ui/PcKeyboard.kt index e4450662d..0567319f3 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PcKeyboard.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PcKeyboard.kt @@ -54,16 +54,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -/** - * A PC keyboard on screen: every key is a physical key (a Linux evdev code) pressed and released - * on the session's wl_keyboard, as a USB keyboard would - Esc, the function row, Ctrl, Alt, Super, - * the arrows and the editing block included, none of which the Android keyboard can send. - * - * A key goes down when a finger lands on it and up when it lifts, so holding one repeats it the way - * the program's own key repeat does. Ctrl, Shift, Alt and Super are sticky: one tap holds the - * modifier for the next key, a second tap locks it, a third lets it go. A controller moves over the - * keys with the d-pad and presses with A. - */ private data class PcKey(val label: String, val code: Int, val weight: Float = 1f, val shifted: String? = null, @StringRes val labelRes: Int = 0) private fun k(label: String, code: Int, weight: Float = 1f, shifted: String? = null) = PcKey(label, code, weight, shifted) @@ -110,7 +100,6 @@ private const val MOD_LOCKED = 2 @Composable fun PcKeyboard( - /** evdev code, pressed. */ sendKey: (Int, Boolean) -> Unit, onAndroidKeyboard: () -> Unit, onClose: () -> Unit, @@ -142,21 +131,17 @@ fun PcKeyboard( if (key.code == 58) capsOn = !capsOn releaseOnceModifiers() } - // Nothing is left held when the keyboard goes away. androidx.compose.runtime.DisposableEffect(Unit) { onDispose { for ((code, state) in mods.toMap()) if (state != MOD_OFF) sendKey(code, false) } } LaunchedEffect(Unit) { runCatching { firstKey.requestFocus() } } BoxWithConstraints(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { - // Slightly under half the screen, header included, so the program stays in view above it: - // six rows share what is left of 47% after the header, the gaps and the padding. val rowHeight = ((maxHeight * 0.47f - 24.dp - 3.dp * 6 - 10.dp) / 6).coerceIn(24.dp, 44.dp) Box( Modifier.fillMaxWidth() .background(Color(0xE6101418), RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)) .border(1.dp, pal.line2, RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)) - // Touches on the keyboard stop here; the program underneath never sees them. .pointerInput(Unit) { awaitEachGesture { awaitFirstDown().consume(); waitForUpOrCancellation()?.consume() } } .padding(horizontal = 8.dp, vertical = 5.dp), ) { @@ -221,7 +206,6 @@ private fun RowScope.KeyCap( down = false; onUp() } } - // A controller: A (or the d-pad's centre) is the finger. .onPreviewKeyEvent { e -> val kc = e.key.nativeKeyCode if (kc != KeyEvent.KEYCODE_BUTTON_A && kc != KeyEvent.KEYCODE_DPAD_CENTER && kc != KeyEvent.KEYCODE_ENTER) return@onPreviewKeyEvent false diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ResolutionRow.kt b/app/src/main/java/com/droiddeck/launcher/ui/ResolutionRow.kt index 2af914e4d..5a0b88daa 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ResolutionRow.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ResolutionRow.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.res.stringResource import com.droiddeck.launcher.R import com.droiddeck.launcher.session.SessionDisplay -/** Pre-session resolution choice, applied when the session starts. */ @Composable fun ResolutionRow( host: MenuHost, choice: String, panel: Pair<Int, Int>, onPick: (String) -> Unit, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt b/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt index 92cb32117..913f312ea 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt @@ -36,7 +36,6 @@ import com.termux.terminal.TerminalSessionClient import com.termux.view.TerminalView import com.termux.view.TerminalViewClient -/** Touch controls drawn on a second Android display while the compositor remains on the main one. */ class SecondScreenPresentation( private val sessionActivity: SessionActivity, display: Display, @@ -83,7 +82,6 @@ class SecondScreenPresentation( if (created) renderMode() } - /** Releases the auxiliary Linux shell before the window is dismissed. */ fun closeControls() { closeTerminal() keyboardHost?.hide() diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionDrawerPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionDrawerPage.kt index 0955651cc..898900f45 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionDrawerPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionDrawerPage.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.ui -/** Stable page identities keep focus and scroll positions when a secondary display comes and goes. */ internal enum class SessionDrawerPage(val firstControl: String) { CONTROLLER("touch"), SECOND_SCREEN("second-screen:none"), diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionLoading.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionLoading.kt index 3cf190efe..3cb47b070 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionLoading.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionLoading.kt @@ -76,7 +76,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.TextOverflow -// The session's loading screen, its paused screen and the HUD text style. @Composable fun HudText(text: String) { @@ -94,7 +93,6 @@ fun HudText(text: String) { } } -/** One line of the loading screen's checklist; [detail] is what it says while it is the one running. */ private class LoadStage(@androidx.annotation.StringRes val label: Int, @androidx.annotation.StringRes val detail: Int) private fun loadStages(steam: Boolean): List<LoadStage> = if (steam) listOf( @@ -108,11 +106,6 @@ private fun loadStages(steam: Boolean): List<LoadStage> = if (steam) listOf( LoadStage(R.string.load_opening, R.string.load_opening_detail), ) -/** - * The checklist stage a loading line's [topic] belongs to, -1 when it says nothing about the stage. - * The lines are the session script's "== STEP" milestones and the installer's own progress; they - * arrive in order, and the screen never steps back. - */ private fun stageOf(topic: LoadingState.Topic, steam: Boolean): Int = when (topic) { LoadingState.Topic.RUNTIME -> 0 LoadingState.Topic.SESSION -> 1 @@ -125,15 +118,12 @@ private fun stageOf(topic: LoadingState.Topic, steam: Boolean): Int = when (topi @Composable fun LoadingOverlay( step: String, percent: Int, elapsed: String, hint: String, ended: Boolean, - /** What [step] is about, and whether it is a download or install line worth showing as it is (see [LoadingState]). */ topic: LoadingState.Topic, readable: Boolean, title: String = stringResource(R.string.load_starting_steam), steam: Boolean = true, onCancel: (() -> Unit)? = null, - /** An ended session's exit status and log path, shown small under the advice. */ endedDetail: String? = null, onRetry: (() -> Unit)? = null, onShareLogs: (() -> Unit)? = null, onClose: (() -> Unit)? = null, - /** The blue the page opens on when a button flooded to it, gathered into the throbber's ball. */ flood: Color? = null, onFlooded: () -> Unit = {}, ) { @@ -148,7 +138,6 @@ fun LoadingOverlay( val active = reached.coerceAtMost(stages.lastIndex) val detail = if (percent >= 0 || readable) step.replaceFirstChar { it.uppercase() } else stringResource(stages[active].detail) val context = LocalContext.current - // The Steam tab's wall behind it, dimmed and slowed: starting Steam reads as the same place settling in. val games by produceState(emptyList<Library.SteamGame>()) { value = withContext(Dispatchers.IO) { runCatching { Library.steamGames(context).sortedByDescending { it.lastPlayed } }.getOrDefault(emptyList()) } } @@ -176,7 +165,6 @@ fun LoadingOverlay( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.align(Alignment.Center).verticalScroll(rememberScrollState()).padding(horizontal = 32.dp, vertical = 24.dp), ) { - // Held on the plain D while the flood gathers, and started fresh when it lands. key(flood == null) { LogoThrobber( Modifier.width(if (ended) 88.dp else 110.dp).onGloballyPositioned { ball = throbberBall(it.boundsInRoot()).also { b -> ThrobberSpot.ball = b } }, @@ -211,7 +199,6 @@ fun LoadingOverlay( } } -/** What to do about an ended session: nothing leaves on a timer, and the pad starts on Try again. */ @Composable private fun EndedActions(onRetry: (() -> Unit)?, onShareLogs: (() -> Unit)?, onClose: (() -> Unit)?) { val first = remember { FocusRequester() } @@ -229,7 +216,6 @@ private fun EndedActions(onRetry: (() -> Unit)?, onShareLogs: (() -> Unit)?, onC } } -/** One segment per stage: done ones filled, the current one filling (or running, with no percent). */ @Composable private fun LoadSegments(count: Int, active: Int, percent: Int, modifier: Modifier = Modifier) { val pal = LocalPalette.current @@ -257,7 +243,6 @@ private fun LoadSegments(count: Int, active: Int, percent: Int, modifier: Modifi } } -/** Cancel as the pad's B, in the corner; a tap works too. */ @Composable private fun CancelHint(onCancel: () -> Unit, modifier: Modifier = Modifier) { val colors = MaterialTheme.colorScheme @@ -281,7 +266,6 @@ private fun CancelHint(onCancel: () -> Unit, modifier: Modifier = Modifier) { } } -/** A session paused in the background, ready to pick up where it left off. */ @Composable fun SessionPausedOverlay(title: String = stringResource(R.string.load_steam_paused), onResume: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt index 18cd36810..3716418de 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt @@ -180,16 +180,13 @@ private class DrawerFocus { } else if (focused == key) focused = null } - /** Where focus was last on [page], if anywhere. */ fun target(page: SessionDrawerPage) = last[page] fun request(key: String) = runCatching { requester(key).requestFocus() } fun forget(page: SessionDrawerPage) { last.remove(page) } } -/** Everything the drawer shows and does. */ class DrawerActions( val steam: Boolean, - /** The drawer's heading: the emulator for a program from the rail, else Steam or Desktop. */ val title: String? = null, val isHomeApp: Boolean, val androidApps: List<HomeApp.LaunchableApp>, @@ -201,7 +198,6 @@ class DrawerActions( val suspendPolicy: String, val steamDownloadsInBackground: Boolean = false, val backActionsInverted: Boolean, - /** What the pad is to Steam (SessionPrefs.CONTROLLER_*), switched live; null hides the row. */ val padType: String? = null, val onPadType: (String) -> Unit = {}, val touchMode: String, @@ -209,15 +205,10 @@ class DrawerActions( val fexPreset: String, val upscaler: Int = 0, val upscaleSharpness: Int = 75, - /** The compositor's post chain (gpu/ScreenEffects), changed live. */ val effects: ScreenEffects = ScreenEffects.OFF, - /** Cardboard mode (gpu/Cardboard): side-by-side eyes for a phone VR viewer, changed live. */ val cardboard: Cardboard = Cardboard.OFF, - /** Whether the device has a rotation sensor (head tracking possible). */ val cardboardSensor: Boolean = false, - /** SteamVR in the guest for Cardboard mode (gpu/SteamVr); null while unknown. */ val steamVr: SteamVr.Status? = null, - /** Texture filtering for DirectX 9-11 games (core/TextureFiltering); lands on their next launch. */ val textureAnisotropy: Int? = 0, val textureLodBias: String? = TextureFiltering.LOD_BIAS_OFF, val secondScreenMode: SecondScreenMode, @@ -226,18 +217,13 @@ class DrawerActions( val onHud: (Boolean) -> Unit, val onFrameGenPick: (FrameGen.Mode) -> Unit, val onImportLossless: () -> Unit, - /** The Android keyboard (text, turned into key presses). */ val onKeyboard: () -> Unit, - /** The on-screen PC keyboard: real keys, Esc, F1-F12, Ctrl, Alt... */ val onHardwareKeyboard: () -> Unit, val onSteamMenu: (() -> Unit)?, val onQam: (() -> Unit)?, - /** Null hides the achievements row. */ val onAchievements: (() -> Unit)? = null, - /** (unlocked, total), or null when unknown. */ val achievementsProgress: Pair<Int, Int>? = null, val onOsc: (String) -> Unit, - /** Edits the Steam touch layout of the game in front, when Steam's controls are up. */ val onEditSteamTouch: (() -> Unit)? = null, val controller: com.droiddeck.launcher.input.ControllerPrefs.Settings? = null, val onRumble: (Boolean) -> Unit = {}, @@ -254,7 +240,6 @@ class DrawerActions( val onEffects: (ScreenEffects) -> Unit = {}, val onCardboard: (Cardboard) -> Unit = {}, val onCardboardRecenter: () -> Unit = {}, - /** install, start or stop SteamVR. */ val onSteamVr: (String) -> Unit = {}, val onTextureAnisotropy: (Int?) -> Unit = {}, val onTextureLodBias: (String?) -> Unit = {}, @@ -266,17 +251,13 @@ class DrawerActions( val pipAutoEnter: Boolean = false, val onPip: () -> Unit = {}, val onPipAutoEnter: (Boolean) -> Unit = {}, - /** Unfolded controls (on a foldable; null hides the row). */ val unfoldedControls: Boolean? = null, val onUnfoldedControls: (Boolean) -> Unit = {}, val onShareLogs: () -> Unit, val onStop: () -> Unit, - /** Stop with the dialog's button's bounds on screen, for the flood to grow out of; null falls back to [onStop]. */ val onStopFrom: ((androidx.compose.ui.geometry.Rect?) -> Unit)? = null, val onClose: () -> Unit, - /** Games tab: every Proton with what it uses; null until first read. */ val components: ComponentsManager.Snapshot? = null, - /** Re-reads the Protons (and runs swaps that waited for a game to close). */ val onComponentsRefresh: () -> Unit = {}, val componentCatalog: List<ComponentsManager.CatalogItem> = emptyList(), val componentCatalogAt: Long = 0, @@ -285,9 +266,7 @@ class DrawerActions( val componentDownloadRevision: Long = 0, val onComponentCatalogRefresh: () -> Unit = {}, val onComponentDownload: (ComponentsManager.CatalogItem) -> Unit = {}, - /** Installed games available for profile inspection in the session drawer. */ val gameProfiles: List<Pair<Long, String>> = emptyList(), - /** The selected game profile, or null for the Default profile. */ val selectedGameProfileId: Long? = null, val selectedGameProfile: SelectedGameProfile? = null, val gameProfilesLoaded: Boolean = false, @@ -297,12 +276,9 @@ class DrawerActions( val onSelectedGameProfileRefresh: () -> Unit = {}, val gameFexPresetSaving: Boolean = false, val onGameFexPreset: (Long, String?) -> Unit = { _, _ -> }, - /** Saves the selected game's Start without Steam (GameEnvironment.NO_STEAM). */ val gameNoSteamSaving: Boolean = false, val onGameNoSteam: (Long, Boolean) -> Unit = { _, _ -> }, - /** Swaps [value] ("orig:<build>" or a stored package file) into a Proton's component. */ val onComponentSwap: (protonId: String, comp: String, value: String) -> Unit = { _, _, _ -> }, - /** Saves a stored package for a game, or null to inherit that component from the Default profile. */ val onGameComponent: (appId: Long, comp: String, file: String?) -> Unit = { _, _, _ -> }, ) @@ -347,8 +323,6 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont LaunchedEffect(open, controllerActive) { if (open && !controllerActive) focusManager.clearFocus(force = true) } - // The page focus was last put on: turning to another (LB, RB, a tab) starts it at its first - // option; anything else (a menu closing, the drawer opening again) goes back where it was. var focusedPage by remember { mutableStateOf(page) } LaunchedEffect(open, page, controllerActive, host.open, appToChooseDisplay, confirmStop) { if (!open) { @@ -359,7 +333,6 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont if (host.open != null || appToChooseDisplay != null || confirmStop) return@LaunchedEffect val turned = page != focusedPage focusedPage = page - // The page's top control: on Session that is Android apps, when it is shown. val first = if (page == SessionDrawerPage.SESSION && a.isHomeApp) "apps" else page.firstControl if (turned) focus.forget(page) val target = focus.target(page) ?: first @@ -377,8 +350,6 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { host.open = null; a.onClose() }, ) androidx.compose.foundation.layout.BoxWithConstraints(modifier = Modifier.fillMaxSize().exposeTestTags(), contentAlignment = Alignment.CenterEnd) { - // A 4:3 or near-square screen (under 440dp tall) gets tighter padding and a shorter tab row, - // so the settings keep most of the height; a narrow one keeps some of the game in view. val short = maxHeight < 440.dp val sheetWidth = minOf(360.dp, maxWidth * 0.92f) AnimatedVisibility( @@ -500,7 +471,6 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont ChoiceRow(host, "osc", stringResource(R.string.mode_osc), null, if (a.steam) listOf(SessionPrefs.OSC_AUTO to stringResource(R.string.common_auto), SessionPrefs.OSC_ALWAYS to stringResource(R.string.common_always), SessionPrefs.OSC_STEAM_TOUCH to stringResource(R.string.osc_steam_touch), SessionPrefs.OSC_STEAM_QAM to stringResource(R.string.mode_osc_qam), SessionPrefs.OSC_NEVER to stringResource(R.string.common_never)) else listOf(SessionPrefs.OSC_AUTO to stringResource(R.string.common_auto), SessionPrefs.OSC_ALWAYS to stringResource(R.string.common_always), SessionPrefs.OSC_NEVER to stringResource(R.string.common_never)), - // Steam's touch controller is Steam's alone; elsewhere that choice behaves as Auto. if (!a.steam && a.oscMode == SessionPrefs.OSC_STEAM_TOUCH) SessionPrefs.OSC_AUTO else a.oscMode, chipModifier = focus.track(page, "osc"), onPick = a.onOsc) a.onEditSteamTouch?.let { edit -> @@ -736,7 +706,6 @@ private fun StopSessionButton(modifier: Modifier = Modifier, onClick: () -> Unit } } -/** A compact shortcut into Steam, sharing the header with Stop. */ @Composable private fun SteamActionPill( description: String, icon: androidx.compose.ui.graphics.vector.ImageVector, modifier: Modifier, @@ -761,7 +730,6 @@ private fun SteamActionPill( } } -/** A bumper as a keycap, 44dp to touch; LB / RB on the pad turn the page too. */ @Composable private fun DrawerBumper(label: String, description: String, modifier: Modifier = Modifier, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -785,13 +753,6 @@ private fun DrawerBumper(label: String, description: String, modifier: Modifier } } -/** - * The drawer's page tabs, QAM-style: one icon per page standing in a line in page order. The - * selected one steps forward - full size, bright, a soft glow - while the others step back to - * a little smaller and fade, and the whole line leans a little toward the selection. LB / RB still turn - * the page (the buttons beside it and the pad's bumpers); a tab can be tapped or picked with the - * pad like the dots it replaces. - */ @Composable private fun DrawerPageTabs(page: SessionDrawerPage, pages: List<SessionDrawerPage>, modifier: Modifier = Modifier, compact: Boolean = false, onSelect: (SessionDrawerPage) -> Unit) { val colors = MaterialTheme.colorScheme @@ -805,9 +766,6 @@ private fun DrawerPageTabs(page: SessionDrawerPage, pages: List<SessionDrawerPag modifier = modifier.height(if (compact) 44.dp else 64.dp).clipToBounds(), ) { val tab = if (compact) 36f else 44f - // Fit the line to the room between LB and RB. Worst case is an end tab selected: full size - // ([tab] dp) and pushed outward by the lean, which scales with the spacing. So - // (count-1) * spacing * (1 + lean/spacingMax) + tab <= width val spacing = ((maxWidth.value - tab) / ((count - 1) * (1f + DRAWER_TAB_LEAN_DP / DRAWER_TAB_SPACING_DP))).coerceIn(24f, DRAWER_TAB_SPACING_DP) val leanStep = DRAWER_TAB_LEAN_DP * spacing / DRAWER_TAB_SPACING_DP val lean by animateFloatAsState(-(selectedIndex - middle) * leanStep, motion, label = "tabLean") @@ -848,10 +806,8 @@ private fun DrawerPageTabs(page: SessionDrawerPage, pages: List<SessionDrawerPag } } -/** The Look row's value when the rows below match no Look. */ private const val LOOK_CUSTOM = "custom" -/** Presets and simple effects first; detailed adjustments stay behind Advanced. */ @Composable private fun ScreenEffectsGroup( host: MenuHost, a: DrawerActions, advancedExpanded: Boolean, onAdvancedToggle: () -> Unit, @@ -916,7 +872,6 @@ private fun ScreenEffectsGroup( } } -/** Cardboard mode: the eyes, the lens and the virtual screen; everything applies live. */ @Composable private fun CardboardGroup(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) { val c = a.cardboard @@ -963,7 +918,6 @@ private fun CardboardGroup(host: MenuHost, a: DrawerActions, track: (String) -> } } -/** Secondary-display modes are mutually exclusive and apply immediately. */ @Composable private fun SecondScreenGroup(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) { val colors = MaterialTheme.colorScheme @@ -1032,7 +986,6 @@ private fun DrawerFexPresetRow(host: MenuHost, a: DrawerActions, modifier: Modif } } -/** A game's Start without Steam; only with a game selected - the Default profile has no such switch. */ @Composable private fun DrawerNoSteamRow(host: MenuHost, a: DrawerActions, modifier: Modifier) { val appId = a.selectedGameProfileId ?: return @@ -1043,7 +996,6 @@ private fun DrawerNoSteamRow(host: MenuHost, a: DrawerActions, modifier: Modifie ) { on -> a.onGameNoSteam(appId, on) } } -/** Texture filtering on the Games page: DXVK options for the next DirectX 9-11 launch. */ @Composable private fun TextureFilteringGroup(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) { val inherited = a.selectedGameProfileId != null @@ -1194,11 +1146,6 @@ private fun ProfileSummaryRow(label: String, value: String) { } } -/** - * The Games tab's components: quick swaps between what is already installed, per Proton. A swap - * into the Proton a running game uses waits until that game closes. Downloads below share the - * launcher's package store; importing and deleting stay on the Components page. - */ @Composable private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) { val colors = MaterialTheme.colorScheme @@ -1260,7 +1207,6 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin val running = snap.protons.filter { it.inUseByGame } var pick by rememberSaveable { mutableStateOf<String?>(null) } val view = snap.protons.firstOrNull { it.proton.id == pick } ?: running.firstOrNull() ?: snap.protons.firstOrNull() - // Focusable, so the pad can climb from the Proton box up to it and the page scrolls it into view. SettingsGroup(stringResource(R.string.drawer_running_now)) { val src = remember { MutableInteractionSource() } val hot = src.collectIsFocusedAsState().value @@ -1315,7 +1261,6 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin val shown = options.firstOrNull { it.first == current }?.second DrawerStackedChoice( host, "cmp-$comp", ComponentsManager.LABEL.getValue(comp), - // The box already shows the choice: only say more when there is more to say. st.queued?.let { stringResource(R.string.comp_next_queued, it) } ?: st.inUse.takeIf { it != shown }, options, current, note = if (view.inUseByGame) stringResource(R.string.comp_waits_long) else stringResource(R.string.comp_applies_long), @@ -1330,7 +1275,6 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin ) } -/** Downloads only store packages; the profile/Proton choices above apply them separately. */ @Composable internal fun ComponentDownloadsDrawerGroup( host: MenuHost, @@ -1408,10 +1352,6 @@ internal fun ComponentDownloadsDrawerGroup( } } -/** - * A choice for the drawer's narrow column: label, then the box across the full width, then the - * detail underneath - so a long Proton or package name never squeezes the text beside it. - */ @Composable private fun <T> DrawerStackedChoice( host: MenuHost, key: String, label: String, hint: String?, @@ -1445,7 +1385,6 @@ private fun <T> DrawerStackedChoice( Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line)) } -/** A close row of icons: inactive tabs stay legible while the active tab leads. */ private const val DRAWER_TAB_SPACING_DP = 48f private const val DRAWER_TAB_LEAN_DP = 8f @@ -1454,18 +1393,11 @@ private const val DRAWER_TAB_SIDE_SCALE = 0.8f private const val DRAWER_TAB_SIDE_ALPHA = 0.8f -/** Room between the Stop pill and the box that steps out under it: the pull. */ private val StepPull = 6.dp private val StepBoxCorner = 16.dp private val StepFillet = 10.dp private val StepBoxMaxWidth = 264.dp -/** - * Stop's confirm, grown out of the Stop pill at [pill] (px, in the sheet): a pull the pill's width - * drops out of it and a wider box steps out to the left under it for the choices, right edges - * flush, so Stop lands under the pill. Cancel, B and a tap outside fold it back into the pill, - * width first. [onStop] gets Stop's bounds on screen for the flood. - */ @Composable private fun StopConfirm( open: Boolean, @@ -1481,7 +1413,6 @@ private fun StopConfirm( val tint = remember { Animatable(0f) } val dim = remember { Animatable(0f) } val items = remember { List(3) { Animatable(0f) } } - // Composed from the first open until it has folded back into the pill. var shown by remember { mutableStateOf(false) } var grown by remember { mutableStateOf(false) } var content by remember { mutableStateOf<IntSize?>(null) } @@ -1535,7 +1466,6 @@ private fun StopConfirm( val p = pill ?: return Box(Modifier.fillMaxSize()) { - // The rest of the sheet dims behind it, and a tap there cancels. Box( Modifier.fillMaxSize().graphicsLayer { alpha = dim.value }.background(Color(0x59000000)) .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, enabled = open, onClick = onCancel), @@ -1552,7 +1482,6 @@ private fun StopConfirm( drawPath(path, lerp(hotFill, restFill, tint.value)) drawPath(path, colors.error.copy(alpha = lerp(0.9f, 0.55f, tint.value)), style = Stroke(rim)) } - // The pill's label rides on top, as the pull's handle. Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.offset { IntOffset(p.left.roundToInt(), p.top.roundToInt()) } @@ -1570,7 +1499,6 @@ private fun StopConfirm( .onSizeChanged { if (content != it) content = it } .controllerBack(onCancel) .onPreviewKeyEvent { e -> - // Keep a controller in here, as the dialog's own window used to. if (e.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false when (e.key) { Key.DirectionUp, Key.DirectionDown -> true @@ -1609,7 +1537,6 @@ private fun StopConfirm( } } -/** One of the confirm's two choices: a capsule that fills when focused; Stop in the danger red. */ @Composable private fun ConfirmChoice(label: String, danger: Boolean, enabled: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt b/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt index 9de13ee8c..0a07695b3 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt @@ -117,7 +117,6 @@ import androidx.compose.ui.window.PopupProperties private val RowShape = RoundedCornerShape(12.dp) private val GroupShape = RoundedCornerShape(14.dp) -/** Whether a pad's A is held right now, as the activity sees it: a focus move waits for its release. */ internal object HeldKeys { @Volatile var confirm = false } @@ -142,7 +141,6 @@ internal fun Modifier.controllerBack(onBack: () -> Unit): Modifier = onPreviewKe } } -/** LB and RB turn a page's tabs; the keys are taken whenever focus is anywhere inside [this]. */ internal fun Modifier.bumpers(onPrevious: () -> Unit, onNext: () -> Unit): Modifier = onPreviewKeyEvent { event -> val keyEvent = event.nativeKeyEvent val previous = keyEvent.keyCode == KeyEvent.KEYCODE_BUTTON_L1 @@ -154,11 +152,6 @@ internal fun Modifier.bumpers(onPrevious: () -> Unit, onNext: () -> Unit): Modif } } -/** - * A page's tabs with the pad's bumpers drawn beside them. The host turns them with LB/RB (see - * [bumpers]); the keycaps are for touch and stay out of the d-pad's path. The row scrolls sideways - * when a narrow page cannot fit every tab. - */ @OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable internal fun TabStrip( @@ -168,8 +161,6 @@ internal fun TabStrip( val colors = MaterialTheme.colorScheme val pal = LocalPalette.current val shape = RoundedCornerShape(12.dp) - // Tabs that overflow (long labels on a small phone) first close up; only what still does not - // fit scrolls. Once tight the row stays tight, so it cannot flip back and forth. val scroll = rememberScrollState() var tight by remember(tabs) { mutableStateOf(false) } LaunchedEffect(scroll.maxValue) { if (scroll.maxValue in 1 until Int.MAX_VALUE) tight = true } @@ -186,7 +177,6 @@ internal fun TabStrip( val hot = src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value val pick = { onSelect(i) } val tabShape = RoundedCornerShape(9.dp) - // The chosen tab scrolls fully into view, so one at the end is not left cut off. val reveal = remember { androidx.compose.foundation.relocation.BringIntoViewRequester() } if (on) LaunchedEffect(Unit) { reveal.bringIntoView() } Text( @@ -209,7 +199,6 @@ internal fun TabStrip( } } -/** A bumper drawn as a keycap, 44dp to touch; the pad presses the real one. */ @Composable private fun BumperKey(label: String, description: String, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -230,7 +219,6 @@ private fun BumperKey(label: String, description: String, onClick: () -> Unit) { } } -/** The narrowest a setting's value box gets; the session menu's sheet sets it lower. */ val LocalChipMinWidth = androidx.compose.runtime.compositionLocalOf { 150.dp } class MenuHost { @@ -240,7 +228,6 @@ class MenuHost { @Composable fun rememberMenuHost(): MenuHost = remember { MenuHost() } -/** Under the anchor with left edges aligned, kept on screen: a dropdown that belongs to a tab. */ private class BelowStartProvider(private val gap: Int) : PopupPositionProvider { override fun calculatePosition(anchorBounds: IntRect, windowSize: IntSize, layoutDirection: LayoutDirection, popupContentSize: IntSize): IntOffset { var x = anchorBounds.left @@ -267,7 +254,6 @@ private class BelowEndProvider(private val gap: Int) : PopupPositionProvider { @Composable fun AnchoredMenu( open: Boolean, onDismiss: () -> Unit, title: String? = null, note: String? = null, - /** A tab's dropdown: under its anchor, left edges aligned, as wide as its entries (within [minWidth]..260 dp). */ compact: Boolean = false, minWidth: androidx.compose.ui.unit.Dp = 120.dp, content: @Composable ColumnScope.(FocusRequester) -> Unit, ) { @@ -283,8 +269,6 @@ fun AnchoredMenu( android.util.Log.i("AnchoredMenu", "requested first option focus") } } - // Popup installs Android's window density. Keep the app scale from the anchor, including - // the matching dp window dimensions used to cap and scroll the menu. val density = LocalDensity.current val configuration = LocalConfiguration.current val gap = with(density) { 6.dp.roundToPx() } @@ -299,8 +283,6 @@ fun AnchoredMenu( exit = fadeOut(Motion.tw(140)) + scaleOut(Motion.tw(140), targetScale = 0.96f, transformOrigin = TransformOrigin(if (compact) 0f else 1f, 0f)), label = "menu", ) { - // A long list (a driver menu with its downloads) must not run off the screen: the menu is - // capped at most of the window's height and its entries scroll under the fixed title. val maxHeight = (androidx.compose.ui.platform.LocalConfiguration.current.screenHeightDp * 0.86f).dp Column( modifier = Modifier @@ -317,8 +299,6 @@ fun AnchoredMenu( title.uppercase(), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.5.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(start = 10.dp, top = 6.dp, bottom = 6.dp), ) - // While entries remain below the fold the list fades out at the bottom over a down - // arrow, so a short screen (a phone in landscape) shows that the menu goes on. val scroll = rememberScrollState() Box(Modifier.weight(1f, fill = false)) { Column( @@ -368,7 +348,6 @@ fun MenuItem( val src = remember { MutableInteractionSource() } val hot = src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value val shift by animateFloatAsState(if (hot) 2f else 0f, Motion.sp(0.5f), label = "miShift") - // A menu taller than the screen opens scrolled to the current choice, not past it. val reveal = remember { androidx.compose.foundation.relocation.BringIntoViewRequester() } if (checked) LaunchedEffect(Unit) { reveal.bringIntoView() } Row( @@ -431,7 +410,6 @@ fun ValueChip(text: String, open: Boolean, enabled: Boolean = true, modifier: Mo fun SettingsGroup(title: String, compact: Boolean = false, content: @Composable ColumnScope.() -> Unit) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current - // In a lifted sheet each group fades and rises in on its own beat. Column(Modifier.fillMaxWidth().sheetPiece()) { Row( verticalAlignment = Alignment.CenterVertically, @@ -445,11 +423,6 @@ fun SettingsGroup(title: String, compact: Boolean = false, content: @Composable } } -/** - * One setting: its label and [hint] with the [control] beside them. [hintLines] fixes the hint to - * that many lines, for a hint that changes with the value - the row keeps its height, so the rows - * under it stay put. - */ @Composable fun SettingsRow(label: String, hint: String?, highlighted: Boolean = false, hintLines: Int? = null, stacked: Boolean = false, control: @Composable () -> Unit) { val colors = MaterialTheme.colorScheme @@ -459,7 +432,6 @@ fun SettingsRow(label: String, hint: String?, highlighted: Boolean = false, hint Text(hint ?: "", fontSize = size, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp), minLines = hintLines ?: 1, maxLines = hintLines ?: Int.MAX_VALUE, overflow = TextOverflow.Ellipsis) } - // A narrow page stacks the control under its label, so neither squeezes the other. if (stacked || LocalNarrowPane.current) Column( modifier = Modifier.fillMaxWidth().background(bg).padding(start = 14.dp, end = 14.dp, top = 11.dp, bottom = 12.dp), ) { @@ -483,9 +455,7 @@ fun SettingsRow(label: String, hint: String?, highlighted: Boolean = false, hint fun <T> ChoiceRow( host: MenuHost, key: String, label: String, hint: String?, options: List<Pair<T, String>>, selected: T, enabled: Boolean = true, note: String? = null, - /** For the box itself - a page's FocusRequester for its first control. */ chipModifier: Modifier = Modifier, - /** See SettingsRow: a fixed height for a hint that changes with the choice. */ hintLines: Int? = null, onPick: (T) -> Unit, ) { @@ -505,7 +475,6 @@ fun <T> ChoiceRow( } } -/** A small "?" beside a value the app guessed and is not sure of; [description] says what to check. */ @Composable fun UncertainMark(description: String) { val colors = MaterialTheme.colorScheme @@ -524,7 +493,6 @@ fun ToggleRow(host: MenuHost, key: String, label: String, hint: String?, checked ToggleSwitch(checked, enabled, label, chipModifier.testTag("setting-$key")) { host.open = null; onChange(it) } } -/** An on/off switch: one tap or one A press flips it, where a menu of On and Off took three. */ @Composable fun ToggleSwitch(checked: Boolean, enabled: Boolean = true, label: String? = null, modifier: Modifier = Modifier, onChange: (Boolean) -> Unit) { val colors = MaterialTheme.colorScheme @@ -537,7 +505,6 @@ fun ToggleSwitch(checked: Boolean, enabled: Boolean = true, label: String? = nul val onLabel = stringResource(R.string.widgets_on) val offLabel = stringResource(R.string.widgets_off) val shape = RoundedCornerShape(99.dp) - // 48dp tall to touch; the visible track sits inside it. Box( contentAlignment = Alignment.Center, modifier = modifier @@ -562,11 +529,6 @@ fun ToggleSwitch(checked: Boolean, enabled: Boolean = true, label: String? = nul } } -/** - * A bounded value as a track: the d-pad's left and right step it by [step] (held, it keeps - * going), a finger drags or taps it, and [format] prints the value beside it. [modifier] goes on - * the focusable part, for a page's focus tracking. - */ @Composable fun SliderRow( label: String, hint: String?, value: Int, range: IntRange, step: Int = 1, enabled: Boolean = true, @@ -599,7 +561,6 @@ fun ValueSlider( var trackPx by remember { mutableIntStateOf(0) } val text = format(value) val shape = RoundedCornerShape(8.dp) - // Preserve off-grid preset values until the user adjusts; every input snaps to the same step. fun stepped(v: Int) = (((v - range.first).toFloat() / step).roundToInt() * step + range.first).coerceIn(range.first, range.last) fun fromX(x: Float) { val width = trackPx - 2 * thumbInset @@ -619,7 +580,6 @@ fun ValueSlider( val left = keyEvent.keyCode == KeyEvent.KEYCODE_DPAD_LEFT if (!left && keyEvent.keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) false else { - // Repeated key-down events let a held direction keep adjusting. if (enabled && keyEvent.action == KeyEvent.ACTION_DOWN) { val next = stepped(value + if (left) -step else step) if (next != value) currentOnChange(next) @@ -699,13 +659,10 @@ fun SettingsPage( onBack: () -> Unit, eyebrow: String? = null, lede: String? = null, - /** A small control at the right of the title (the driver pages' refresh button). */ action: (@Composable () -> Unit)? = null, - /** Pass one held above the page to keep its scroll position across a page opened over it. */ scroll: androidx.compose.foundation.ScrollState? = null, scrollContent: Boolean = true, compactLayout: Boolean = false, - /** Fixed controls below the title, or beside Back when the title is omitted. */ header: (@Composable () -> Unit)? = null, modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit, @@ -733,7 +690,6 @@ fun SettingsPage( } } } else if (narrow) { - // Back and the title share one line: a 4:3 screen has no height to spare. Rise(0) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -804,11 +760,6 @@ fun SettingsPage( } } -/** - * A short row of tabs with one chosen: a pill in the signal blue slides under the choice and its - * label turns to sit on it. Left and right move between tabs; a tap or A picks one. [selected] - * null leaves no tab chosen. - */ @Composable fun <T> SegmentedTabs(options: List<Pair<T, String>>, selected: T?, modifier: Modifier = Modifier, onPick: (T) -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SharedMotion.kt b/app/src/main/java/com/droiddeck/launcher/ui/SharedMotion.kt index 75fd06660..aa37d5f48 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SharedMotion.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SharedMotion.kt @@ -37,20 +37,9 @@ import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.sin -// The moves the stores made first, for any section: one selection shape that glides along a row -// (or down a column) of choices, and a pressed control that squashes into a dot and flies home. -/** Which way a [ChipGlide]'s choices run. */ internal enum class GlideAxis { Horizontal, Vertical } -/** - * The selection under a row of chips: [at] (in the row) of chip [index], in [tint]. A move to the - * next chip stretches it - the leading edge on the focus ring's lead spring, the trailing one a - * beat later on its trail spring - and a hop past chips stretches the same way, across them. - * Draw it over the chips: under them, their own rest fill hid it in flight. [axis] runs it down a column instead (the rail, a stack of layers). [corner] is the - * chips' own corner; null draws a capsule. [fill] and [outline] are the resting alphas. Not - * [visible], it fades away, and comes back on whichever chip it is on by then. - */ @Composable internal fun BoxScope.ChipGlide( index: Int, @@ -63,12 +52,9 @@ internal fun BoxScope.ChipGlide( visible: Boolean = true, ) { val density = androidx.compose.ui.platform.LocalDensity.current - // The two edges along the axis (left and right, or top and bottom), and the two across it. val start = remember { Animatable(Float.NaN) } val end = remember { Animatable(0f) } val pinch = remember { Animatable(0f) } - // With a pad the focus ring fills what it holds, and is the only fill: a selection of the - // row's own, sliding on its own beat, trailed the ring and sat on items the pad had left. val pad = androidx.compose.ui.platform.LocalInputModeManager.current.inputMode == androidx.compose.ui.input.InputMode.Keyboard @Suppress("NAME_SHADOWING") val visible = visible && !pad val shown = remember { Animatable(if (visible) 1f else 0f) } @@ -91,15 +77,12 @@ internal fun BoxScope.ChipGlide( if (visibleNow.value) shown.snapTo(1f) return@LaunchedEffect } - // Away or on its way back: it goes straight to the chip, unseen. if (!visibleNow.value || shown.value < 0.5f) { start.snapTo(s); end.snapTo(e); pinch.snapTo(0f) return@LaunchedEffect } coroutineScope { run { - // Every move stretches across, however far: a long one used to go as a drop (which - // sat on chips already left) and then as a fade (a flash of no fill at all). launch { pinch.animateTo(0f, Motion.sp(0.6f, 500f)) } launch { shown.animateTo(1f, Motion.tw(120)) } val forward = index > from @@ -119,11 +102,9 @@ internal fun BoxScope.ChipGlide( val size = shape.size val round = minOf(size.width, size.height) / 2f val r = CornerRadius(corner?.let { lerp(minOf(with(density) { it.toPx() }, round), round, p) } ?: round) - // A drop in flight is solid; on a chip it is the chip's tinted fill and outline. val a = shown.value drawRoundRect(tint.value.copy(alpha = lerp(fill, 0.9f, p) * a), at0, size, r) if (outline > 0f) { - // Inside the chip's edge: a stroke on it would lose its outer half to a scrolling row's clip. val w = with(density) { 1.dp.toPx() } val inset = w / 2f drawRoundRect( @@ -134,11 +115,6 @@ internal fun BoxScope.ChipGlide( } } -/** - * The chosen one of a row of chips (or a column, [vertical]): a short bar of [color] along its foot - * (or its start edge). Not a fill or an outline, so it never reads as focus; with a pad the ring's - * own infill is the only fill. Goes inside the chip's clip. - */ @Composable internal fun Modifier.shownBar(on: Boolean, color: Color, vertical: Boolean = false): Modifier { val shown = androidx.compose.animation.core.animateFloatAsState(if (on) 1f else 0f, Motion.tw(if (on) 140 else 100), label = "shownBar").value @@ -156,11 +132,6 @@ internal fun Modifier.shownBar(on: Boolean, color: Color, vertical: Boolean = fa } } -/** - * Where a glide's shape is: its edges along the axis at [start] and [end] (either order), across - * it from [crossStart] to [crossEnd], pinched [pinch] of the way (0 to 1) into a drop [drop] each - * side of its centre. [vertical] runs the axis down instead of across. - */ internal fun glideShape(start: Float, end: Float, crossStart: Float, crossEnd: Float, pinch: Float, drop: Float, vertical: Boolean): Rect { val a0 = minOf(start, end) val a1 = maxOf(start, end) @@ -173,11 +144,6 @@ internal fun glideShape(start: Float, end: Float, crossStart: Float, crossEnd: F return if (vertical) Rect(c0, s0, c1, s1) else Rect(s0, c0, s1, c1) } -/** - * A dot on its way somewhere: from the control at [from] (root px) to the point [to] gives (root - * px, asked each frame, so a target that moves is followed). [color] null is the palette's signal - * blue. [onHalf] runs as it passes the top of its arc, [onLand] once it lands. - */ internal class Flight( val from: Rect, val to: Density.() -> Offset?, @@ -190,27 +156,20 @@ internal class Flight( private companion object { var next = 0L } } -/** Dots flying anywhere outside the Stores page (which keeps its own): a hop, a swap, a toggle. */ internal object Flights { val all = mutableStateListOf<Flight>() - /** Sends [f], or with animations off just runs its ends. */ fun fly(f: Flight) { if (Motion.scale == 0f) { f.onHalf(); f.onLand(); return } all += f } } -/** [Flights] drawn over everything, in the space of an element at [origin] (root px). */ @Composable internal fun FlightsLayer(origin: Offset) { for (f in Flights.all.toList()) key(f.id) { FlightDot(f, origin) { Flights.all.remove(f) } } } -/** - * One dot: the control squashes into a 14dp dot (width first, a little bloop), which then flies - * on an arc to its target, stretched along its path like the focus ring's drop. - */ @Composable internal fun FlightDot(f: Flight, origin: Offset, onDone: () -> Unit) { val pal = LocalPalette.current @@ -232,7 +191,6 @@ internal fun FlightDot(f: Flight, origin: Offset, onDone: () -> Unit) { val b = (f.to(this) ?: return@Canvas) - origin val v = t.value if (v < 1f) { - // Squash: the width goes first, then the height, into the dot; a bloop at the end. val w = lerp(from.width, 2 * r, (v * 1.4f).coerceAtMost(1f)) val h = lerp(from.height, 2 * r, ((v - 0.25f) / 0.75f).coerceIn(0f, 1f)) val k = 1f - 0.15f * sin(PI * v).toFloat() @@ -254,10 +212,6 @@ internal fun FlightDot(f: Flight, origin: Offset, onDone: () -> Unit) { } } -/** - * A row that scrolls sideways fades out at an edge it has more past, instead of slicing a pill or - * a card through: the content's own alpha, so whatever is behind the row shows through. - */ internal fun Modifier.edgeFade(scroll: androidx.compose.foundation.ScrollState, width: Dp = 28.dp): Modifier = this.graphicsLayer { compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen } .drawWithContent { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SheetRows.kt b/app/src/main/java/com/droiddeck/launcher/ui/SheetRows.kt index ac6364a2a..2b2ecf904 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SheetRows.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SheetRows.kt @@ -5,10 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.droiddeck.launcher.R -// The rows the lifted sheets add to what the session settings already had: frame generation, -// Performance and the link to the GPU drivers on Components. -/** Frame generation, moved here from Setup: it is how Play's games are drawn. */ @Composable internal fun FrameGenGroup(s: FrontEndState, a: FrontEndActions) { val host = rememberMenuHost() @@ -22,10 +19,6 @@ internal fun FrameGenGroup(s: FrontEndState, a: FrontEndActions) { } } -/** - * Performance (CPU core assignment), opened from the sheet's Session chip: its page floods out of - * this row, and Back draws it into the row again with the sheet lifted where it was. - */ @Composable internal fun PerformanceLink(a: FrontEndActions, rail: String, tab: String) { val id = "sheet:performance" @@ -41,10 +34,6 @@ internal fun PerformanceLink(a: FrontEndActions, rail: String, tab: String) { } } -/** - * The GPU drivers in use, read only, and a link to where they are set: A hops to Components with - * the GPU drivers layer chosen, and B there comes back to this row. - */ @Composable internal fun DriversLink(a: FrontEndActions, summary: String?, rail: String, back: String) { val id = "sheet:gpu-drivers" diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt b/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt index f6952fc65..a0a306270 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt @@ -67,23 +67,14 @@ import coil.size.Scale import coil.transform.RoundedCornersTransformation import kotlin.math.roundToInt -/** From this many games the Steam tab is a wall; fewer would only repeat the same covers. */ private const val WALL_MIN_GAMES = 4 private const val DRIFT_STEP_NS = 15_000_000L -/** - * The Steam tab: the library as a slowly drifting, tilted wall of capsules (a few games instead - * lean on their own; none, the DroidDeck mark wandering a blank wall), and over it the wordmark and the one thing to do here - - * Play. Everything about a single game lives on the Games tab. - */ @Composable internal fun SteamHome(s: FrontEndState, a: FrontEndActions, modifier: Modifier) { - // A fixed order, ties included: the wall places each game by its position, so an order that - // came out differently from one read of the library to the next moved every capsule. val games = remember(s.steamGames) { s.steamGames.sortedWith(compareByDescending<Library.SteamGame> { it.lastPlayed }.thenBy { it.gameId }) } val narrow = LocalNarrowPane.current - // Where the words and buttons sit, for the empty library's wandering mark to keep off. var words by remember { mutableStateOf<Rect?>(null) } val mode = com.droiddeck.launcher.session.SessionService.MODE_STEAM val tabs = modeSheetTabs(steam = true).map { it to sheetTabLabel(it) } @@ -92,7 +83,6 @@ internal fun SteamHome(s: FrontEndState, a: FrontEndActions, modifier: Modifier) frameGen = { FrameGenGroup(s, a) }, performance = { PerformanceLink(a, "steam", SheetTab.SESSION) }, drivers = { summary -> DriversLink(a, summary, "steam", back) }, - // Offline mode lives with Steam's other settings; Setup only checks the account. account = { host -> SettingsGroup(stringResource(R.string.setup_account)) { ToggleRow( @@ -110,8 +100,6 @@ internal fun SteamHome(s: FrontEndState, a: FrontEndActions, modifier: Modifier) start = if (narrow) 20.dp else 44.dp, bottom = if (narrow) 20.dp else 44.dp, background = { when { - // The mark wanders over this fade, so it sinks with the wall: drawn a little oversized, - // it still reaches the pane's edges at the sink's 0.95. games.isEmpty() -> EmptyLibrary(avoid = words) { Box(Modifier.fillMaxSize().graphicsLayer { scaleX = 1.06f; scaleY = 1.06f }) { WallFade() } } games.size < WALL_MIN_GAMES -> CapsuleFan(games) else -> CapsuleWall(games) @@ -124,19 +112,16 @@ internal fun SteamHome(s: FrontEndState, a: FrontEndActions, modifier: Modifier) modifier = Modifier.onPlaced { words = it.boundsInParent() }, ) { Wordmark(if (narrow) 30.dp else 40.dp) - // Said only when something stands between Play and Steam; a ready runtime needs no words. if (s.busy || !s.ready || (s.available != null && s.available != s.installed)) RuntimeChip(s) } }, button = { click -> - // Enabled without a runtime: the session's loading screen installs it first. PrimaryButton(stringResource(R.string.games_play_steam), enabled = !s.busy, main = true, large = true, icon = Icons.Filled.PlayArrow, modifier = Modifier.testTag("play-steam"), onClick = click) }, rows = { tab -> a.modeSheet(mode, tab, extras) }, ) } -/** The wall fades out behind the words, and toward the bottom where the buttons sit. */ @Composable internal fun WallFade() { val colors = MaterialTheme.colorScheme @@ -153,11 +138,6 @@ internal fun WallFade() { Box(Modifier.fillMaxSize().background(Brush.verticalGradient(0.55f to Color.Transparent, 1f to colors.background.copy(alpha = 0.9f)))) } -/** - * Columns of capsules tilted a few degrees, alternate columns drifting up and down. Each column - * holds its run of covers twice and moves by exactly one run, so the loop has no seam. An empty - * library draws the same wall in blank capsules, slower. - */ @Composable internal fun CapsuleWall( games: List<Library.SteamGame>, @@ -169,37 +149,20 @@ internal fun CapsuleWall( TiltedWall(games.size, driftMs, modifier) { i -> if (games.isEmpty()) blank else WallCapsule(artBrush(hueOf(games[i].name)), games[i].art) } } -/** - * One capsule of a [TiltedWall]: its ground, the art laid over it once loaded, and an outline. A - * tile that is an app rather than a cover carries an [icon] instead (a drawable id or an image), - * drawn whole at the tile's centre, and a window's [bar] along its foot. - */ internal class WallCapsule(val ground: Brush, val art: Any? = null, val edge: Color? = null, val icon: Any? = null, val bar: Color? = null) -/** - * [CapsuleWall]'s tilted, drifting columns for any [count] of capsules; [capsule] describes number i - * (0 until [count], or 0 for each blank one when [count] is 0). Each column is one drawing with the - * covers' corners cut into their bitmaps: a rounded clip under the tilt is rasterised on the CPU, - * for every capsule on every frame. - */ @Composable internal fun TiltedWall( count: Int, driftMs: Int, modifier: Modifier = Modifier, - /** One tile's size, the gap between tiles and their corner: portrait capsules unless a wall says otherwise. */ capW: androidx.compose.ui.unit.Dp = 112.dp, capH: androidx.compose.ui.unit.Dp = 168.dp, gap: androidx.compose.ui.unit.Dp = 14.dp, corner: androidx.compose.ui.unit.Dp = 10.dp, capsule: (index: Int) -> WallCapsule, ) { - // Animations off in the system settings: a still wall. val still = Motion.scale == 0f - // The wall moves a few pixels a step. Stepped on frames at most every 15 ms it looks the same as - // stepped on every frame, which on a 120 Hz screen drew it twice as often. The phase advances by - // the time since the last step over the period now, so a period that changes (Flathub's wall - // slowing while it sets up) changes the speed without the wall jumping. val drift = remember { mutableFloatStateOf(0f) } val period = androidx.compose.runtime.rememberUpdatedState(driftMs) if (!still) LaunchedEffect(Unit) { @@ -214,7 +177,6 @@ internal fun TiltedWall( } val density = LocalDensity.current BoxWithConstraints(modifier.fillMaxSize()) { - // Centred on the page and big enough that, once tilted, it still covers every corner. val tilt = Math.toRadians(13.0) val cos = kotlin.math.cos(tilt).toFloat() val sin = kotlin.math.sin(tilt).toFloat() @@ -263,7 +225,6 @@ internal fun TiltedWall( } } -/** A cover sized for a capsule, its corners already cut, so it draws without a clip. */ @Composable private fun rememberCapsuleArt(model: Any, size: Size, radius: Float): Painter { val context = LocalContext.current @@ -277,7 +238,6 @@ private fun rememberCapsuleArt(model: Any, size: Size, radius: Float): Painter { ) } -/** An app's icon for a wall tile: a bundled drawable, or an image Coil loads. */ @Composable private fun rememberIcon(model: Any): Painter = if (model is Int) androidx.compose.ui.res.painterResource(model) else rememberAsyncImagePainter(model) @@ -286,7 +246,6 @@ private fun DrawScope.drawCapsule(look: WallCapsule, art: Painter?, radius: Floa drawRoundRect(look.ground, size = size, cornerRadius = CornerRadius(radius)) look.bar?.let { drawRect(it, Offset(0f, size.height - barH), Size(size.width, barH)) } if (art != null && look.icon != null) { - // An app's icon whole at the tile's centre (above a window's bar), not cropped over it. val side = minOf(size.width, size.height - (if (look.bar != null) barH else 0f)) * 0.5f val cy = (size.height - (if (look.bar != null) barH else 0f)) / 2f translate((size.width - side) / 2f, cy - side / 2f) { with(art) { draw(Size(side, side)) } } @@ -296,7 +255,6 @@ private fun DrawScope.drawCapsule(look: WallCapsule, art: Painter?, radius: Floa } } -/** One to three games, each a large capsule leaning on the others and floating a little. */ @Composable private fun CapsuleFan(games: List<Library.SteamGame>) { val pal = LocalPalette.current @@ -306,7 +264,6 @@ private fun CapsuleFan(games: List<Library.SteamGame>) { transition.animateFloat(0f, 1f, infiniteRepeatable(tween(ms, easing = FastOutSlowInEasing), RepeatMode.Reverse), label = "float") } val still = Motion.scale == 0f - // Where each sits (share of the page), its height (share of the page's) and its lean; front first. val slots = listOf( FanSlot(0.54f, 0.14f, 0.62f, -9f), FanSlot(0.70f, 0.24f, 0.56f, 5f), diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StepOut.kt b/app/src/main/java/com/droiddeck/launcher/ui/StepOut.kt index 7940d8dbe..7b902eeef 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StepOut.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StepOut.kt @@ -65,17 +65,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.roundToInt -// End session's confirm, for any control: a pull drops out of the control and a wider box steps -// out under it for the choices, one edge flush with the control. Stop draws it from its pill on -// the drawer; the stores draw it from Install, the Library tab and a download's cancel. -/** - * The stair-step outline: the pill [p] on top, a box under it from [left] to the pill's right edge - * and down to [bottom], the pull between them [pullTop] (the box's top). Right edges are flush and - * the inside corner where the pull meets the box is rounded the other way. While the box is still - * no wider than the pill it is just the pill, stretched down. [pillCorner] is the control's own - * corner: a pill's is half its height. - */ internal fun stepPath( path: Path, p: Rect, left: Float, bottom: Float, pullTop: Float, box: Float, fillet: Float, pillCorner: Float = minOf(p.height / 2f, p.width / 2f), @@ -112,13 +102,10 @@ internal fun stepPath( path.close() } -/** [stepPath] for a box that steps out to the right of the pill, out to [right], left edges flush. */ internal fun stepPathRight( path: Path, p: Rect, right: Float, bottom: Float, pullTop: Float, box: Float, fillet: Float, pillCorner: Float = minOf(p.height / 2f, p.width / 2f), ) { - // Drawn as the left-hand outline about the pill's centre, which maps the pill onto itself, - // then mirrored back: x -> (p.left + p.right) - x. val axis = p.left + p.right stepPath(path, p, axis - right, bottom, pullTop, box, fillet, pillCorner) path.transform(Matrix().apply { this[0, 0] = -1f; this[3, 0] = axis }) @@ -126,17 +113,12 @@ internal fun stepPathRight( private fun lerpF(a: Float, b: Float, t: Float) = a + (b - a) * t -/** Which way the box steps out of its control. */ internal enum class StepSide { - /** Out to the left, right edges flush (a control at the right: Stop, a row's cancel). */ Left, - /** Out to the right, left edges flush (a control at the left: Install, a tab). */ Right, } -/** The choices' side of a [StepOut]: each piece fades in on its own beat, and [first] takes focus. */ internal class StepOutScope(private val items: List<Animatable<Float, AnimationVector1D>>, val first: FocusRequester, val open: Boolean, val maxHeight: Dp) { - /** Piece [i] (0 first) of the box: it fades and rises in after the box has stepped out. */ fun Modifier.stepItem(i: Int): Modifier = graphicsLayer { val v = items[i.coerceIn(0, items.lastIndex)].value alpha = v @@ -144,17 +126,10 @@ internal class StepOutScope(private val items: List<Animatable<Float, AnimationV } } -/** Room between the control and the box that steps out under it: the pull. */ private val OutPull = 6.dp private val OutBoxCorner = 16.dp private val OutFillet = 10.dp -/** - * A confirm or a short list grown out of the control at [pill] (px, in this element's space, which - * should fill the screen area it may cover): a pull the control's width drops out of it, then a - * box steps out to [side] for [content]. [onDismiss] (B, a tap outside) asks to fold it; it folds - * back width first once [open] is false, then [onClosed]. Exactly StopConfirm's springs. - */ @OptIn(ExperimentalComposeUiApi::class) @Composable internal fun StepOut( @@ -181,13 +156,9 @@ internal fun StepOut( var grown by remember { mutableStateOf(false) } var measured by remember { mutableStateOf<IntSize?>(null) } val first = remember { FocusRequester() } - // The control focus was on as it opened: focus goes back to it as it folds, not to wherever - // the screen puts a pad with nothing focused (the rail). val glide = LocalFocusGlide.current val opener = remember { arrayOfNulls<GlideSource>(1) } val openNow = androidx.compose.runtime.rememberUpdatedState(open) - // While the box grows or folds the ring keeps out (it flew across the moving shape) and comes - // back on whatever has focus once it has settled: the first choice, or the control again. var moving by remember { mutableStateOf(false) } if (moving) VeilRing() val pull = with(density) { OutPull.toPx() } @@ -199,13 +170,10 @@ internal fun StepOut( val availableHeight = with(density) { maxOf(hostHeight - pill.bottom - pull, pill.top - pull).coerceAtLeast(44.dp.toPx()).toDp() } - // No room under the control (a row near the foot of the screen): it steps out upward - // instead, the same outline mirrored about the control. val up = measured?.let { pill.bottom + pull + it.height > hostHeight && pill.top - pull - it.height >= 0f } ?: false val room = if (side == StepSide.Left) pill.right else hostWidth - pill.left val maxW = with(density) { maxWidth.toPx() }.coerceAtMost(room).coerceAtLeast(pill.width) LaunchedEffect(open, measured) { - // Asked to close before it ever measured: nothing to fold. if (!open && !grown) { onClosed(); return@LaunchedEffect } val c = measured ?: return@LaunchedEffect val rest = if (side == StepSide.Left) pill.left else pill.right @@ -221,8 +189,6 @@ internal fun StepOut( coroutineScope { launch { dim.animateTo(1f, Motion.tw(260)) } launch { tint.animateTo(1f, Motion.tw(200)) } - // Down and out together, on one spring: down first drew the control as a stick - // that then snapped out sideways. moving = true val down = launch { bottom.animateTo(boxBottom, Motion.sp(0.8f, 380f)) } val across = launch { edge.animateTo(out, Motion.sp(0.8f, 380f)) } @@ -242,8 +208,6 @@ internal fun StepOut( coroutineScope { pieces.forEach { launch { it.animateTo(0f, Motion.tw(90)) } } launch { delay(Motion.ms(60).toLong()); dim.animateTo(0f, Motion.tw(300)) } - // In and up together on a timed ease: width first left a tall tail under the - // control, and a damped spring's tail kept a nub of it on screen. moving = true launch { delay(Motion.ms(60).toLong()); edge.animateTo(rest, Motion.tw(240)) } launch { delay(Motion.ms(60).toLong()); bottom.animateTo(pill.bottom, Motion.tw(240)) } @@ -255,11 +219,9 @@ internal fun StepOut( } } - // Opened by touch and then answered with a pad: the pad starts in the box, not behind it. LaunchedEffect(open, grown, inputMode.inputMode) { if (open && grown && inputMode.inputMode == InputMode.Keyboard) { withFrameNanos { }; runCatching { first.requestFocus() } } } - // The rest of the screen dims behind it, and a tap there folds it. Never a pad's stop. Box( Modifier.fillMaxSize().graphicsLayer { alpha = dim.value }.background(Color(0x59000000)) .focusProperties { canFocus = false } @@ -276,11 +238,6 @@ internal fun StepOut( if (side == StepSide.Left) stepPath(path, pill, edge.value, bottom.value, pill.bottom + pull, boxCorner, fillet, corner) else stepPathRight(path, pill, edge.value, bottom.value, pill.bottom + pull, boxCorner, fillet, corner) if (up) path.transform(Matrix().apply { this[1, 1] = -1f; this[3, 1] = pill.top + pill.bottom }) - // Barely out of the control, the pull and its fillet drew a hook off the control's foot: - // the outline comes in over the first part of the way out (and goes over the last part - // of the way back), by how far along it is. A share of the way, not a distance: a box - // no wider than its control (a confirm under a full-width row) never got 24dp out, and - // was never drawn at all. val a = measured?.let { c -> val rest = if (side == StepSide.Left) pill.left else pill.right val out = if (side == StepSide.Left) (pill.right - c.width).coerceAtLeast(0f) else (pill.left + c.width).coerceAtMost(hostWidth) @@ -294,7 +251,6 @@ internal fun StepOut( drawPath(path, lerp(hotFill, restFill, tint.value), alpha = a) drawPath(path, accent.copy(alpha = lerpF(0.9f, 0.55f, tint.value) * a), style = Stroke(rim)) } - // The control's label rides on top, as the pull's handle. if (grown) Box( contentAlignment = Alignment.Center, modifier = Modifier.offset { IntOffset(pill.left.roundToInt(), pill.top.roundToInt()) } @@ -308,7 +264,6 @@ internal fun StepOut( .heightIn(max = availableHeight) .onSizeChanged { if (measured != it) measured = it } .controllerBack(onDismiss) - // A pad stays in the box until it is answered or folded, as it did in Stop's dialog. .focusProperties { exit = { if (openNow.value) FocusRequester.Cancel else FocusRequester.Default } } .focusGroup() .verticalScroll(rememberScrollState()) @@ -317,10 +272,6 @@ internal fun StepOut( } } -/** - * One choice in a [StepOut]: a capsule that fills when focused, like Stop's Cancel and Stop. [danger] - * draws it in red; [icon] and [trailing] make it a row (a place to install to and its free space). - */ @Composable internal fun StepChoice( label: String, @@ -357,7 +308,6 @@ internal fun StepChoice( } } -/** A [StepOut]'s heading line, the first piece to fade in. */ @Composable internal fun StepOutScope.StepTitle(text: String) { Text( @@ -366,7 +316,6 @@ internal fun StepOutScope.StepTitle(text: String) { ) } -/** The choices of a [StepOut] stacked, each the box's width. */ @Composable internal fun StepList(content: @Composable ColumnScope.() -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(6.dp), content = content) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Steps.kt b/app/src/main/java/com/droiddeck/launcher/ui/Steps.kt index d0b9369a3..18a4b9703 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Steps.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Steps.kt @@ -7,12 +7,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset -// The front end's confirms and short lists, each stepping out of the control that asked for it -// (StepOut): a sheet's Uninstall, a Components swap, a remove. The Stores page keeps its own, in -// StoresMotion; everything else asks here and is drawn over the whole screen. internal object Steps { - /** The confirm or list stepping out of a control now, and whether it is open. */ var step by mutableStateOf<StepAsk?>(null) private set var open by mutableStateOf(false) @@ -24,10 +20,8 @@ internal object Steps { open = true } - /** Folds the current one back into its control. */ fun fold() { open = false } - /** Drops it at once: the control it came from has gone. */ fun drop() { step?.let { step = null; it.onDismiss() } open = false @@ -39,11 +33,9 @@ internal object Steps { } } -/** [Steps] drawn over an element at [origin] (root px) that fills the screen. */ @Composable internal fun StepsLayer(origin: Offset) { val ask = Steps.step ?: return - // Back (B, or the system's) folds what is stepped out, before it reaches the page under it. androidx.activity.compose.BackHandler(enabled = Steps.open) { Steps.fold() } key(ask) { StepOut( diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoreAchievementsUi.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoreAchievementsUi.kt index 0e87128b4..f033aa9dc 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoreAchievementsUi.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoreAchievementsUi.kt @@ -73,7 +73,6 @@ internal class AchievementsState(val list: List<Achievement>?, val reason: Reaso companion object { val READING = AchievementsState(null, null, true) } } -/** The cached list at once, then the store's answer (the cache stays if the store fails). */ @Composable internal fun rememberAchievements(game: StoreAchievements.Game?): AchievementsState? { val ctx = LocalContext.current.applicationContext @@ -134,14 +133,12 @@ private fun AchievementGroupLabel(label: String) { } } -/** Unlocked (newest first), locked, then hidden ones not yet unlocked. */ private fun groups(list: List<Achievement>): List<Pair<Int, List<Achievement>>> = listOf( R.string.achievements_group_unlocked to list.filter { it.unlocked }.sortedByDescending { it.unlockedAt }, R.string.achievements_group_locked to list.filter { !it.unlocked && !it.hidden }, R.string.achievements_group_hidden to list.filter { !it.unlocked && it.hidden }, ).filter { it.second.isNotEmpty() } -/** Focusable so the pad can scroll the list. */ @Composable internal fun AchievementRow(a: Achievement, compact: Boolean, modifier: Modifier = Modifier) { val colors = MaterialTheme.colorScheme @@ -200,7 +197,6 @@ internal fun StoreAchievementsSection(game: StoreAchievements.Game) { internal fun StoreAchievementsSectionContent(label: String, state: AchievementsState) { val colors = MaterialTheme.colorScheme val list = state.list - // Hidden for games without achievements. if (list.isNullOrEmpty() && !state.reading && state.reason != Reason.SIGNED_OUT && state.reason != Reason.UNREACHABLE) return val narrow = LocalNarrowPane.current Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().padding(top = 18.dp)) { @@ -278,7 +274,7 @@ internal fun SessionAchievementsPanel(label: String, state: AchievementsState, t verticalArrangement = Arrangement.spacedBy(6.dp), ) { achievementItems(list, short, first) } LaunchedEffect(list.isNotEmpty()) { - withFrameNanos {} // rows lay out a frame after composition + withFrameNanos {} runCatching { first.requestFocus() } } } @@ -286,7 +282,6 @@ internal fun SessionAchievementsPanel(label: String, state: AchievementsState, t } } -/** [progress]: (unlocked, total), or null when unknown. */ @Composable internal fun DrawerAchievementsRow(progress: Pair<Int, Int>?, modifier: Modifier = Modifier, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoreChips.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoreChips.kt index 410d44e99..ec12d19af 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoreChips.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoreChips.kt @@ -17,16 +17,12 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.frontend.Library import com.droiddeck.launcher.stores.Store -// Where a game came from, as a small coloured chip: Steam, one of the stores, or an added folder. -/** Flathub's own accent, as each store has one, and the ink that reads on it. */ internal val FlathubTeal = Color(0xFF3FB8AF) internal val FlathubInk = Color(0xFF04211F) -/** The Store rail section's source id, for its colours. */ internal const val FLATHUB = "flathub" -/** The chip's colours per source: a tinted ground and a readable ink on it, one pair per store. */ internal class SourceColours(val fill: Color, val ink: Color, val dot: Color) internal fun sourceColours(source: String): SourceColours = when (source) { @@ -47,7 +43,6 @@ internal fun sourceLabel(source: String): String = when (source) { else -> stringResource(R.string.source_added) } -/** The source chip itself; [small] for a list row, the regular size for a hero or a card. */ @Composable internal fun SourceChip(source: String, small: Boolean = false, modifier: Modifier = Modifier) { val c = sourceColours(source) @@ -59,7 +54,6 @@ internal fun SourceChip(source: String, small: Boolean = false, modifier: Modifi ) } -/** A status chip in the stores' colours: green for done ("Installed"), blue for busy, amber for a warning. */ @Composable internal fun StateChip(text: String, tone: ChipTone, small: Boolean = false) { val pal = LocalPalette.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoreGameFacts.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoreGameFacts.kt index f5967bdac..d57b63b5e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoreGameFacts.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoreGameFacts.kt @@ -69,13 +69,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -// A store game's facts under its hero: who made it and when, from the store; the Metacritic score -// and HowLongToBeat's times, looked up elsewhere by name. The store's facts are there at once; the -// looked-up ones hold their place while they come and step out if there is nothing to show. internal class GameScores(val metacritic: Lookup<GameFacts.Metacritic>, val hltb: Lookup<GameFacts.Hltb>) -/** Both lookups at once and off the main thread, each landing on its own; what memory already has shows without a wait. */ @Composable internal fun rememberGameScores(title: String?): GameScores { val ctx = LocalContext.current.applicationContext @@ -89,16 +85,10 @@ internal fun rememberGameScores(title: String?): GameScores { return GameScores(metacritic, hltb) } -/** - * The facts: who made it, when, and what critics made of it on the first row; how long it takes, - * across the page, on the second. A looked-up tile holds its place while it asks and steps out if - * the site does not have the game; nothing is drawn when there is nothing to say. - */ @OptIn(ExperimentalLayoutApi::class) @Composable internal fun StoreGameFacts(developer: String, publisher: String, releaseDate: String, scores: GameScores, modifier: Modifier = Modifier) { val released = remember(releaseDate) { GameFacts.releaseLabel(releaseDate) } - // A studio that publishes itself is named once. val publishedBy = publisher.takeUnless { it.isBlank() || it.trim().equals(developer.trim(), ignoreCase = true) } val firstRow = developer.isNotBlank() || publishedBy != null || released != null || scores.metacritic != Lookup.None if (!firstRow && scores.hltb == Lookup.None) return @@ -123,10 +113,8 @@ internal fun StoreGameFacts(developer: String, publisher: String, releaseDate: S } } -/** A looked-up tile: in from the start (holding its place while it asks), out with a fade when there was nothing. */ @Composable private fun <T> Looked(lookup: Lookup<T>, exit: ExitTransition, content: @Composable (Lookup<T>) -> Unit) { - // The last answer worth drawing, so the tile does not blank as it leaves. var shown by remember { mutableStateOf(lookup) } if (lookup != Lookup.None) shown = lookup AnimatedVisibility(visible = lookup != Lookup.None, enter = fadeIn(), exit = fadeOut() + exit) { content(shown) } @@ -135,10 +123,6 @@ private fun <T> Looked(lookup: Lookup<T>, exit: ExitTransition, content: @Compos private val FactShape = RoundedCornerShape(12.dp) private val ValueHeight = 24.dp -/** - * A label over its value; [fill] takes the row's whole width, for a value laid out across it. With - * a [link] it is a control like any other on the page: focus, hover and A or a tap open the page. - */ @Composable private fun FactTile(label: String, fill: Boolean = false, link: String? = null, value: @Composable () -> Unit) { val pal = LocalPalette.current @@ -166,7 +150,6 @@ private fun FactTile(label: String, fill: Boolean = false, link: String? = null, private fun FactText(text: String) = Text(text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis) -/** Metacritic's own bands: green from 75, yellow from 50, red below. */ internal fun metacriticColour(score: Int): Color = when { score >= 75 -> Color(0xFF66CC33) score >= 50 -> Color(0xFFFFCC33) @@ -180,7 +163,6 @@ private fun MetacriticBadge(score: Int) { } } -/** Each kind of run HowLongToBeat has times for, sharing the tile's width: the hours in full, what they are for beside them. */ @Composable private fun HltbTimes(t: GameFacts.Hltb) { val colors = MaterialTheme.colorScheme @@ -201,7 +183,6 @@ private fun HltbTimes(t: GameFacts.Hltb) { } } -/** Where a value will be, breathing while it is asked for. */ @Composable private fun Placeholder(width: Dp) { val pulse by rememberInfiniteTransition(label = "fact").animateFloat( @@ -210,7 +191,6 @@ private fun Placeholder(width: Dp) { Box(Modifier.width(width).height(16.dp).alpha(pulse).clip(RoundedCornerShape(5.dp)).background(LocalPalette.current.line2)) } -// Previews: every state the row can be in, at a TV pane's width and a narrow one. private val previewScores = GameScores( Lookup.Found(GameFacts.Metacritic(93, "https://www.metacritic.com/game/the-witcher-3-wild-hunt/")), @@ -272,7 +252,6 @@ private fun FactsPaperPreview() = FactsPreview(theme = Themes.PAPER) { StoreGameFacts("CD PROJEKT RED", "CD PROJEKT RED", "2015-05-19T00:00:00+02:00", previewScores) } -// The real hero reads the stores' live state, so this stands one in to show where the row sits. @Preview(name = "Game page, facts in place", widthDp = 800, heightDp = 520) @Composable private fun FactsOnPagePreview() = FactsPreview { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoreGridNavigation.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoreGridNavigation.kt index 5676929ba..d019bc009 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoreGridNavigation.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoreGridNavigation.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.ui -/** Vertical controller moves keep the same column, including across lazy item boundaries. */ internal fun storeGridNeighbor(index: Int, count: Int, columns: Int, direction: Int): Int? { if (index !in 0 until count || columns < 1 || direction !in listOf(-1, 1)) return null return (index + columns * direction).takeIf { it in 0 until count } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt index 495fb9ba2..0facc0c07 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt @@ -106,20 +106,13 @@ import com.droiddeck.launcher.store.FlathubApi import com.droiddeck.launcher.store.StoreState import kotlinx.coroutines.delay -// The Store rail section: Flathub's apps for this device's architecture, installed into the -// Linux runtime with Flatpak, and the ones already installed - in the stores' style, in Flathub's -// teal. Until Flatpak is set up it is the stores' sign-in hero: a wall of real app icons, one word, -// one button that becomes its own progress. Then a chip row, shelves of square cards that flood -// open into an app's page, and installs that fly home to the Installed chip. private const val MODE_DISCOVER = "discover" private const val MODE_INSTALLED = "installed" private const val MODE_UPDATES = "updates" -/** One app as a card shows it: Flathub's summary or what Flatpak says is installed. */ private class CardApp(val id: String, val name: String, val icon: Any?, val summary: String? = null, val verified: Boolean = false) -/** Where the Installed chip is, for an install's dot to land in, and how many have landed. */ private object FlathubMotion { var installedChip: Rect? = null var landed by mutableIntStateOf(0) @@ -131,8 +124,6 @@ internal fun StorePage(s: FrontEndState, a: FrontEndActions, modifier: Modifier) LaunchedEffect(s.ready) { StoreState.refresh(ctx) } LaunchedEffect(Unit) { StoreState.loadSections() } FlathubTheme { - // The storefront takes over from the hero once Flatpak is ready: the setup rail rings - // once, then the hero sinks away and the storefront rises. var front by remember { mutableStateOf(StoreState.ready && s.ready) } var ring by remember { mutableIntStateOf(0) } LaunchedEffect(StoreState.ready, s.ready) { @@ -156,11 +147,6 @@ internal fun StorePage(s: FrontEndState, a: FrontEndActions, modifier: Modifier) } } -/** - * Flatpak is not set up yet: the stores' sign-in shape in teal. A tilted wall of the popular apps' - * icons (Flathub's public pages need no Flatpak) drifts behind, fading toward the words; one big - * button sets it up, and stretches into its own progress while it does. - */ @Composable private fun FlathubHero(s: FrontEndState, ring: Int) { val ctx = LocalContext.current @@ -174,7 +160,6 @@ private fun FlathubHero(s: FrontEndState, ring: Int) { BoxWithConstraints(Modifier.fillMaxSize().padding(horizontal = if (narrow) 14.dp else 22.dp, vertical = if (narrow) 10.dp else 14.dp)) { val shape = RoundedCornerShape(20.dp) Box(Modifier.fillMaxWidth().height(maxHeight.coerceAtLeast(320.dp)).clip(shape).background(colors.background).border(1.dp, pal.line, shape)) { - // Slower while it sets up, as the empty wall drifts. androidx.compose.animation.Crossfade(icons, animationSpec = Motion.tw(600), label = "flathubWall") { tiles -> val ground = Brush.linearGradient(listOf(Color(0xFF0B2E2B), Color(0xFF0B2E2B))) val edge = FlathubTeal.copy(alpha = 0.18f) @@ -217,11 +202,6 @@ private fun FlathubHero(s: FrontEndState, ring: Int) { } } -/** - * Set up Flathub, and its own progress: while it sets up the button springs out to the full width - * and fills in teal from the left with how far it is, the stage and the figure inside it. Without - * a runtime it says what is missing instead. - */ @Composable private fun SetupButton(s: FrontEndState, busy: Boolean, full: Dp, ring: Int, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -240,7 +220,6 @@ private fun SetupButton(s: FrontEndState, busy: Boolean, full: Dp, ring: Int, on } return } - // A pad keeps its place on the rail that took the button's. if (ff != null) LaunchedEffect(Unit) { if (inputMode.inputMode == InputMode.Keyboard) focusSoon { ff.primary } } val width = androidx.compose.ui.unit.lerp(restW.coerceAtLeast(120.dp), full, grow) val shape = RoundedCornerShape(12.dp) @@ -269,7 +248,6 @@ private fun SetupButton(s: FrontEndState, busy: Boolean, full: Dp, ring: Int, on } } -// ---- storefront ------------------------------------------------------------------------------------ @OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) @Composable @@ -285,9 +263,7 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { var openApp by rememberSaveable { mutableStateOf<String?>(null) } val updates = StoreState.updates if (mode == MODE_UPDATES && updates.isEmpty()) mode = MODE_INSTALLED - // Once a session is plenty: the Updates chip shows itself when there is something to update. LaunchedEffect(StoreState.ready) { if (StoreState.ready && !StoreState.updatesChecked && !StoreState.checkingUpdates) StoreState.checkUpdates(ctx) } - // A search runs a beat after typing stops, or as a category is picked. LaunchedEffect(query, category) { if (query.isBlank() && category == null) { StoreState.clearSearch(); return@LaunchedEffect } delay(350) @@ -298,7 +274,6 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { val focusManager = LocalFocusManager.current val keyboard = LocalSoftwareKeyboardController.current val view = LocalView.current - // Cards, where they are, for the page that opens from one. val cards = remember { HashMap<String, Rect>() } var viewportAt by remember { mutableStateOf(Offset.Zero) } var detail by remember { mutableStateOf<String?>(null) } @@ -308,8 +283,6 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { LaunchedEffect(openApp) { val id = openApp if (id != null) { - // The grid stays composed for the return animation. Release its native search field - // before covering it, so its focus ring and keyboard do not follow onto the app page. focusManager.clearFocus(force = true) view.findFocus()?.clearFocus() keyboard?.hide() @@ -323,20 +296,17 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { detail = null; leaving = false; detailFrom = null } } - // Back on the grid: focus goes to the card the page came from. LaunchedEffect(detail) { if (detail != null || inputMode.inputMode != InputMode.Keyboard || ff == null) return@LaunchedEffect val id = lastApp ?: return@LaunchedEffect ff.items["tile:store:$id"]?.let { req -> focusSoon { req } } } BackHandler(enabled = openApp != null) { openApp = null } - // An app's card on Desktop asked for its page. LaunchedEffect(StoreState.openRequest) { StoreState.openRequest?.let { openApp = it; StoreState.openRequest = null } } val covering = detail != null && !leaving val sink by animateFloatAsState(if (covering) 0.95f else 1f, if (covering) Motion.tw(480) else Motion.tw(460, 40), label = "flathubSink") val fade by animateFloatAsState(if (covering) 0f else 1f, if (covering) Motion.tw(320, 80) else Motion.tw(300, 100), label = "flathubFade") val modes = buildList { add(MODE_DISCOVER); add(MODE_INSTALLED); if (updates.isNotEmpty()) add(MODE_UPDATES) } - // LB / RB with a pad: focus goes to the new mode's chip; the cards it was on leave with the old mode. val seenMode = remember { arrayOf(mode) } LaunchedEffect(mode) { if (seenMode[0] == mode) return@LaunchedEffect @@ -354,7 +324,6 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { ) { Rise(0) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { - // A page of Desktop's: the way back to it, for a finger (B does the same). val show = LocalShowSection.current BackLink(stringResource(R.string.rail_desktop), compact = true) { show("desktop") } Box(Modifier.weight(1f)) { FlathubChips(mode, modes, StoreState.installed.size, updates.size) { mode = it } } @@ -386,7 +355,6 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { } if (from == null) body() else { - // The card's own icon, cropped over the tile as it grows: the page arrives in the app's colour. val art = coil.compose.rememberAsyncImagePainter( remember(icon) { coil.request.ImageRequest.Builder(ctx).data(icon).size(256, 256).build() }, ) @@ -397,10 +365,6 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { } } -/** - * Discover, Installed (with its count) and Updates (when there are any): one selection glides under - * them in teal; the Installed count pops as an install's dot lands in it. - */ @Composable private fun FlathubChips(mode: String, modes: List<String>, installed: Int, updates: Int, onPick: (String) -> Unit) { val pal = LocalPalette.current @@ -415,7 +379,6 @@ private fun FlathubChips(mode: String, modes: List<String>, installed: Int, upda val hot = rememberHot(src) val shape = RoundedCornerShape(99.dp) val on = m == mode - // Drawn by the row's glide by touch only; with a pad its own rest look and the bar. val under = on && androidx.compose.ui.platform.LocalInputModeManager.current.inputMode != androidx.compose.ui.input.InputMode.Keyboard Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), @@ -446,15 +409,10 @@ private fun FlathubChips(mode: String, modes: List<String>, installed: Int, upda } } } - // Over the chips: under them, their rest fill hid it as it crossed them. ChipGlide(modes.indexOf(mode), bounds[mode], tint) } } -/** - * The card width for a width: room for a name on two lines and a two-line summary under the art, - * so a long name wraps instead of being cut to a few letters. - */ private fun cardSize(width: Dp): Dp = when { width >= 700.dp -> 164.dp width >= 480.dp -> 148.dp @@ -470,7 +428,6 @@ private fun Discover( ModeHeader( stringResource(R.string.store_tab_discover), query, onQuery, stringResource(R.string.store_search_placeholder), ) { - // The categories as small pills, at the right of the header. val pills = rememberScrollState() Row( horizontalArrangement = Arrangement.spacedBy(6.dp), @@ -529,7 +486,6 @@ private fun UpdatesMode(cards: HashMap<String, Rect>, onOpen: (String) -> Unit) Rise(2) { CardGrid(apps.map { CardApp(it.id, it.name, it.icon ?: StoreState.details[it.id]?.icon ?: FlathubApi.iconUrl(it.id), it.summary) }, cards, onOpen) } } -/** A horizontal row of cards under a title, the stores' shelf; nothing while it is empty. */ @Composable private fun Shelf(title: String, apps: List<CardApp>?, first: Boolean, cards: HashMap<String, Rect>, onOpen: (String) -> Unit) { val colors = MaterialTheme.colorScheme @@ -546,7 +502,6 @@ private fun Shelf(title: String, apps: List<CardApp>?, first: Boolean, cards: Ha } } -/** The Installed and Updates modes, and search: a grid of the same cards. */ @Composable private fun CardGrid(apps: List<CardApp>, cards: HashMap<String, Rect>, onOpen: (String) -> Unit) { BoxWithConstraints(Modifier.fillMaxWidth().padding(top = 10.dp)) { @@ -562,11 +517,6 @@ private fun CardGrid(apps: List<CardApp>, cards: HashMap<String, Rect>, onOpen: } } -/** - * One app: its icon over a blurred, darkened copy of itself (darkened only, before Android 12), - * with one status on the art - Get, ✓, or how far an install is - and under it the name, on two - * lines if it needs them, and what the app is. - */ @Composable private fun AppCard(app: CardApp, size: Dp, first: Boolean, cards: HashMap<String, Rect>, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -595,7 +545,6 @@ private fun AppCard(app: CardApp, size: Dp, first: Boolean, cards: HashMap<Strin .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)).background(Color(0xFF0B2E2B)), ) { if (app.icon != null) { - // The icon's own colour, blurred and darkened, as the card's ground. AsyncImage( model = app.icon, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize().graphicsLayer { scaleX = 1.8f; scaleY = 1.8f; alpha = 0.55f } @@ -604,7 +553,6 @@ private fun AppCard(app: CardApp, size: Dp, first: Boolean, cards: HashMap<Strin Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.35f))) AsyncImage(model = app.icon, contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier.size(artH * 0.62f)) } else Icon(Icons.Outlined.Apps, null, tint = colors.onSurfaceVariant, modifier = Modifier.size(artH * 0.5f)) - // One status, on the art: what A on the card leads to. Box(Modifier.align(Alignment.TopEnd).padding(6.dp)) { when { busy -> Box(Modifier.clip(RoundedCornerShape(99.dp)).background(Color.Black.copy(alpha = 0.55f)).padding(4.dp)) { @@ -633,7 +581,6 @@ private fun AppCard(app: CardApp, size: Dp, first: Boolean, cards: HashMap<Strin ) if (app.verified) Icon(Icons.Outlined.Verified, stringResource(R.string.store_verified), tint = pal.signal, modifier = Modifier.padding(top = 2.dp).size(13.dp)) } - // Two lines kept for the summary even when it is short, so cards in a row line up. Text( app.summary.orEmpty(), fontSize = 12.sp, lineHeight = 15.sp, color = colors.onSurfaceVariant, minLines = 2, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(top = 3.dp), @@ -642,11 +589,6 @@ private fun AppCard(app: CardApp, size: Dp, first: Boolean, cards: HashMap<Strin } } -/** - * An app's page, flooded out of its card: its icon, name and what it is, one action (Install or - * Open, with Update beside it when there is one) and Website; then its screenshots and details. - * Install flies a dot to the Installed chip; Remove asks by stepping out of its button, in red. - */ @Composable private fun AppDetail(s: FrontEndState, a: FrontEndActions, id: String, onBack: () -> Unit) { val ctx = LocalContext.current @@ -688,7 +630,6 @@ private fun AppDetail(s: FrontEndState, a: FrontEndActions, id: String, onBack: enabled = StoreState.busy == null && s.ready && (d == null || d.arches.isEmpty() || "aarch64" in d.arches), ) { StoreState.install(ctx, id, name) - // The button squashes into a dot that flies to the Installed chip, whose count pops. val from = at if (from != null) Flights.fly(Flight(from, to = { FlathubMotion.installedChip?.center }, onLand = { FlathubMotion.landed++ })) else FlathubMotion.landed++ @@ -749,7 +690,6 @@ private fun AppDetail(s: FrontEndState, a: FrontEndActions, id: String, onBack: } } -/** Remove, which asks first: are you sure steps out of it in red, Keep first, so a stray A costs nothing. */ @Composable private fun RemoveButton(id: String, name: String, enabled: Boolean) { val ctx = LocalContext.current @@ -774,7 +714,6 @@ private fun RemoveButton(id: String, name: String, enabled: Boolean) { } } -/** A small text action at the right of a header. */ @Composable private fun SmallLink(text: String, enabled: Boolean = true, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -790,7 +729,6 @@ private fun SmallLink(text: String, enabled: Boolean = true, onClick: () -> Unit ) } -/** An app's icon from Flathub (a URL) or from its installed files; a placeholder until it loads. */ @Composable internal fun AppIcon(model: Any?, name: String, size: Int) { val colors = MaterialTheme.colorScheme @@ -800,7 +738,6 @@ internal fun AppIcon(model: Any?, name: String, size: Int) { } } -/** A category pill: small, focusable, and filled when it is the one in use. */ @Composable private fun PillButton(label: String, selected: Boolean, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt index e77bd70a1..1a3480f95 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt @@ -41,7 +41,6 @@ import com.droiddeck.launcher.stores.StoresState import com.droiddeck.launcher.stores.download.DownloadState import com.droiddeck.launcher.stores.formatBytes -// A store game's own page: hero, the one main action, how it is launched and where it lives. @Composable internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: FrontEndActions, onBack: () -> Unit) { @@ -53,7 +52,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro val item = (StoresState.library[store].orEmpty() + StoresState.shelves[store]?.all.orEmpty()).firstOrNull { it.id == id } val installed = StoresState.installedGame(store, id) val download = StoresState.download("${store.id}:$id")?.takeIf { it.isActive } - // Removal asks twice: one stray press of A should not cost a download. val title = item?.title ?: installed?.sidecar?.title ?: id Rise(0) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(bottom = 12.dp)) { @@ -61,13 +59,10 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro SourceChip(store.id) } } - // Out of a card, the hero holds still in its place: the card's flood lands on it, and a hero - // still rising in sat below where the flood had measured it (the art showed twice). val hero: @Composable () -> Unit = { androidx.compose.runtime.DisposableEffect(Unit) { onDispose { StoresMotion.heroAt = null } } Box( Modifier.fillMaxWidth().heightIn(min = if (narrow) 150.dp else 180.dp) - // The card it opened from floods onto this, and draws back into the card from it. .onGloballyPositioned { StoresMotion.heroAt = it.boundsInRoot() } .clip(Shape16).background(artBrush(hueOf(title))), ) { @@ -75,7 +70,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro if (art != null) AsyncImage(model = art, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize()) Spacer(Modifier.matchParentSize().background(Brush.horizontalGradient(0f to colors.background.copy(alpha = 0.94f), 0.5f to colors.background.copy(alpha = 0.55f), 1f to Color.Transparent))) Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.align(Alignment.BottomStart).padding(horizontal = 22.dp, vertical = 18.dp)) { - // An install names where it went (internal storage, the card), a download where it is going. val state = when { installed != null -> stringResource(R.string.stores_eyebrow_installed) + " · " + com.droiddeck.launcher.stores.StoreInstallRoot.labelFor(ctx, installed.folder) download != null && download.location.isNotBlank() -> stringResource(R.string.stores_eyebrow_library) + " · " + download.location @@ -88,11 +82,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro Actions { when { installed != null -> { - // Launching, the prefix and the files belong to the Games tab, where every - // installed game has them; here an install only says it is in Steam and - // offers the one thing Games does not: removing it. - // The row keeps its shape: the chip stays, and Uninstall becomes its own - // progress in place while the files go; then the page reads Install again. ActionChip(stringResource(R.string.stores_in_steam), ok = true) val gone = StoresState.uninstalling["${store.id}:$id"] if (gone != null) SecondaryButton(stringResource(R.string.stores_uninstalling), enabled = false, progress = gone) {} @@ -123,7 +112,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro } if (StoresMotion.heroLands) hero() else Rise(1) { hero() } if (store == Store.GOG || store == Store.EPIC) { - // An owned game resolves to its library entry; the storefront's copy of it may know more. val listed = StoresState.shelves[store]?.all.orEmpty().firstOrNull { it.id == id } val developer = item?.developer?.ifBlank { null } ?: listed?.developer.orEmpty() val publisher = item?.publisher?.ifBlank { null } ?: listed?.publisher.orEmpty() @@ -131,8 +119,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro val scores = rememberGameScores(title.takeIf { it != id }) Rise(2) { StoreGameFacts(developer, publisher, releaseDate, scores) } } - // Plain text only, and nothing at all when the store sent a template key instead of words. - // A description that is only the title again (some stores fill it so) is no description. val description = item?.let { com.droiddeck.launcher.stores.cleanStoreText(it.description) }.orEmpty() .takeUnless { it.trim().equals(title.trim(), ignoreCase = true) }.orEmpty() if (description.isNotBlank()) Rise(3) { @@ -149,10 +135,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro if (achievements != null) Rise(4) { StoreAchievementsSection(achievements) } } -/** - * Uninstall that asks the way Cancel does: a confirm steps out of it, naming the game and what - * goes (its size on disk), with Keep first so a stray A does nothing. - */ @Composable private fun UninstallButton(title: String, game: com.droiddeck.launcher.stores.InstalledStoreGame, enabled: Boolean, onUninstall: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -165,8 +147,6 @@ private fun UninstallButton(title: String, game: com.droiddeck.launcher.stores.I Box(Modifier.onGloballyPositioned { placed[0] = it }) { SecondaryButton(remove, enabled = enabled) { val at = placed[0]?.takeIf { it.isAttached }?.boundsInRoot() ?: return@SecondaryButton - // It starts once the confirm has folded back into the button: started at once, the button - // turned into the progress under the fold and it drew back into a control that had gone. var confirmed = false StoresMotion.ask(StepAsk(anchor = at, side = StepSide.Right, accent = colors.error, items = 3, pillCorner = 12.dp, maxWidth = 340.dp, onDismiss = { if (confirmed) onUninstall() }, handle = { @@ -183,10 +163,6 @@ private fun UninstallButton(title: String, game: com.droiddeck.launcher.stores.I } } -/** - * Install (or Resume install) that says where it sits when pressed: where to install grows out of - * it, and the download it starts leaves it as a dot for the Downloads chip. - */ @Composable private fun InstallButton(label: String, onInstall: () -> Unit) { val placed = remember { arrayOfNulls<androidx.compose.ui.layout.LayoutCoordinates>(1) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt index 47b356e7d..d332ee0e5 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt @@ -92,9 +92,7 @@ import com.droiddeck.launcher.stores.download.StoreDownloadTier import com.droiddeck.launcher.stores.formatBytes import com.droiddeck.launcher.stores.formatSpeed -// The Downloads chip: one queue for the three stores, its settings and the engine log. -/** The queue. Its settings (speed tier, downloads at a time) live in the chip row's cog only. */ @Composable internal fun StoresDownloadsPane(s: FrontEndState, a: FrontEndActions) { DownloadList(s, a) @@ -106,21 +104,12 @@ private fun DownloadList(s: FrontEndState, a: FrontEndActions) { val pal = LocalPalette.current val scope = rememberCoroutineScope() val entries = StoresState.downloads - // Clearing: the finished rows go into the divider's X one by one, bottom first, and only then - // leave the queue. var clearing by remember { mutableStateOf(false) } - // Under way on top; finished below a line whose X clears them all (rows only - a failed - // download's kept files stay until its game page's Clear). - // Under way first, then paused, then the queue in its order: a queued row sat above the one running. val (active, finished) = entries.partition { it.isActive }.let { (on, off) -> on.sortedWith(compareBy<DownloadEntry>({ when (it.state) { DownloadState.RUNNING -> 0; DownloadState.PAUSED -> 1; else -> 2 } }, { it.queuePosition })) to off } val finishedShown = if (clearing) emptyList() else finished - // Installs that stopped without the queue knowing (the app closed or was killed): the queue - // lives in memory, so after a restart they were only on their game's page. They resume here too. val queued = entries.mapTo(HashSet()) { it.key } - // Matched against each store's library: one not read yet (Downloads opened first) is read from - // its cache, quietly, so the rows are there without visiting the store. val ctx = LocalContext.current val missing = StoresState.unfinished.map { it.store }.filter { StoresState.library[it] == null }.toSet() LaunchedEffect(missing) { missing.forEach { StoresState.backend(it)?.syncLibrary(ctx, false) } } @@ -135,9 +124,6 @@ private fun DownloadList(s: FrontEndState, a: FrontEndActions) { Text(stringResource(R.string.stores_downloads_empty), fontSize = 13.sp, color = colors.onSurfaceVariant) } } - // The list changes only when rows come, go or change state; each row reads its own progress. - // A row pours out of the count's side (the right), and leaves the way it came; one that - // installed stays a beat for its bar to gather into Play first. AnimatedRows( active, key = { it.key }, enter = { i, later -> @@ -178,9 +164,7 @@ private fun DownloadList(s: FrontEndState, a: FrontEndActions) { } AnimatedRows( finishedShown, key = { it.key }, - // One that just finished joins after its row above has gathered; the rest are simply there. enter = { i, later -> if (later) expandVertically(Motion.tw(300, GATHER_HOLD_MS)) + fadeIn(Motion.tw(220, GATHER_HOLD_MS + 50)) else fadeIn(Motion.tw(300, 60 * i.coerceAtMost(5))) }, - // Into the X at the top right, bottom row first. exit = { _, i, n -> val wait = 60 * (n - 1 - i).coerceIn(0, 8) scaleOut(Motion.tw(220, wait), targetScale = 0.2f, transformOrigin = TransformOrigin(1f, 0f)) + @@ -193,7 +177,6 @@ private fun DownloadList(s: FrontEndState, a: FrontEndActions) { } } -/** An install that stopped before it finished, from an earlier run: Resume picks it up, Clear deletes what it had. */ @Composable private fun InterruptedRow(item: com.droiddeck.launcher.stores.CatalogItem) { val ctx = LocalContext.current @@ -207,13 +190,8 @@ private fun InterruptedRow(item: com.droiddeck.launcher.stores.CatalogItem) { } } -/** How long a row that installed holds for its bar to gather into Play before it moves under the line. */ private const val GATHER_HOLD_MS = 900 -/** - * A row in the download card's look, shorter: the game's art behind it under the same dark wash, - * the copy in white. Finished and interrupted downloads read as the same thing as one under way. - */ @Composable private fun ArtStrip(cover: String?, name: String, content: @Composable androidx.compose.foundation.layout.RowScope.() -> Unit) { val pal = LocalPalette.current @@ -228,7 +206,6 @@ private fun ArtStrip(cover: String?, name: String, content: @Composable androidx } } -/** A finished download: its name and store, and Play when it installed - blooming out of a dot when it just did. */ @Composable private fun FinishedRow(d: DownloadEntry, s: FrontEndState, a: FrontEndActions, bloom: Boolean = false) { val ctx = LocalContext.current @@ -243,7 +220,6 @@ private fun FinishedRow(d: DownloadEntry, s: FrontEndState, a: FrontEndActions, } } -/** Grows out of a dot (sp 0.5 / 300) after [waitMs], when [on]; otherwise just there. */ private fun Modifier.bloomIn(on: Boolean, waitMs: Int): Modifier = composed { val k = remember { Animatable(if (on && Motion.scale > 0f) 0f else 1f) } LaunchedEffect(Unit) { @@ -255,10 +231,6 @@ private fun Modifier.bloomIn(on: Boolean, waitMs: Int): Modifier = composed { graphicsLayer { scaleX = k.value; scaleY = k.value; alpha = k.value.coerceIn(0f, 1f) } } -/** - * A row's action as a round icon button - the label is its description - sized like the old - * compact buttons and focusable by pad. [primary] fills it with the accent, [danger] in red. - */ @Composable internal fun RoundAction( icon: androidx.compose.ui.graphics.vector.ImageVector, description: String, primary: Boolean = false, danger: Boolean = false, @@ -280,10 +252,6 @@ internal fun RoundAction( ) { androidx.compose.material3.Icon(icon, description, tint = tint, modifier = Modifier.size(size * 0.5f)) } } -/** - * Cancel as End session's confirm: the X drops a pull and Keep and Delete step out to its left, - * in red. Keep's rim runs out the four seconds after which it folds back by itself. - */ @Composable private fun CancelAction(onDelete: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -299,7 +267,6 @@ private fun CancelAction(onDelete: () -> Unit) { ) { val left = remember { Animatable(1f) } LaunchedEffect(Unit) { - // Wall-clock seconds, as before; the rim follows them when animations run. if (Motion.scale > 0f) launch { left.animateTo(0f, androidx.compose.animation.core.tween((CANCEL_HOLD_MS / Motion.scale).toInt(), easing = LinearEasing)) } delay(CANCEL_HOLD_MS.toLong()) StoresMotion.fold() @@ -315,7 +282,6 @@ private fun CancelAction(onDelete: () -> Unit) { private const val CANCEL_HOLD_MS = 4000 -/** A capsule's outline drawn for [left] (1 to 0) of its length, clockwise from the top centre: time running out. */ private fun Modifier.timeRim(left: () -> Float, color: Color): Modifier = drawWithContent { drawContent() val v = left() @@ -343,8 +309,6 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) val narrow = LocalNarrowPane.current val ink = Color.White val dim = Color.White.copy(alpha = 0.72f) - // The game's wide art is the row: cropped to it, a dark wash from the left so the copy reads - // in white there, the art clear on the right. Box(Modifier.fillMaxWidth().heightIn(min = 88.dp).clip(Shape12).background(Color(0xFF101318)).border(1.dp, pal.line, Shape12)) { if (!d.cover.isNullOrEmpty()) AsyncImage(model = d.cover, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize()) else Spacer(Modifier.matchParentSize().background(artBrush(hueOf(d.name)))) @@ -362,7 +326,6 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) val meta = when (d.state) { DownloadState.INSTALLED -> stringResource(R.string.stores_installed_chip) DownloadState.CANCELLED -> stringResource(R.string.stores_dl_cancelled) - // The reason is in the engine log; the row only says what is kept for Resume. DownloadState.FAILED -> if (d.bytesDone > 0) stringResource(R.string.stores_dl_failed_kept, formatBytes(d.bytesDone)) else stringResource(R.string.stores_dl_failed) DownloadState.PAUSED -> stringResource(R.string.stores_dl_paused) DownloadState.QUEUED -> if (d.queuePosition > 0) stringResource(R.string.stores_dl_queued_at, d.queuePosition) else stringResource(R.string.stores_dl_queued) @@ -382,7 +345,6 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) } } } - // The row's actions on the right, over the art: icons, their labels as descriptions. Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { when (d.state) { DownloadState.RUNNING, DownloadState.QUEUED -> { @@ -394,7 +356,6 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) CancelAction { DownloadQueue.cancel(ctx, d.key) } } DownloadState.FAILED -> RoundAction(Icons.Filled.Refresh, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.retry(ctx, d.key) } - // Just installed (this row is on its way under the line): Play blooms where the bar gathered. DownloadState.INSTALLED -> Box(Modifier.bloomIn(true, 420)) { RoundAction(Icons.Filled.PlayArrow, stringResource(R.string.stores_play), primary = true) { launchStoreGame(ctx, d.store, d.id, s, a) } } @@ -405,13 +366,6 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) } } -/** - * One segment per stage - Manifest, Download, Verify, Install - equal widths: a passed or skipped - * stage full, the active one filling with its own progress (a sliver while it has nothing to count), - * the rest empty. Fills ride a spring; a stage that completes swells and settles (the bloop); a - * paused one breathes; and once the download has installed the four gather into one green line - * that draws up into a dot, where Play blooms. - */ @Composable private fun StageBar(d: DownloadEntry) { val pal = LocalPalette.current @@ -422,8 +376,6 @@ private fun StageBar(d: DownloadEntry) { val fills = stages.mapIndexed { i, st -> when { actives[i] -> d.stageFraction.let { if (it < 0f) 0.04f else it } - // A stage a store skips or folds into another (Epic checks chunks while it fetches, - // Amazon writes while it downloads) reads complete once a later one has started. d.passed(st) || st.ordinal < d.stage.ordinal || d.stage == DownloadStage.DONE -> 1f else -> 0f } @@ -452,7 +404,6 @@ private fun StageBar(d: DownloadEntry) { val cy = size.height / 2f val g = gather.value if (g < 0.5f) { - // The segments: the gaps close and every fill turns done-green as the gather starts. val join = (g * 2f).coerceIn(0f, 1f) val gap = 3.dp.toPx() * (1f - join) val segW = (size.width - gap * 3f) / 4f @@ -472,7 +423,6 @@ private fun StageBar(d: DownloadEntry) { ) } } else { - // One line drawing up into a dot at its right end. val p = ((g - 0.5f) * 2f).coerceIn(0f, 1f) val dot = 10.dp.toPx() val left = androidx.compose.ui.util.lerp(0f, size.width - dot, p) @@ -482,7 +432,6 @@ private fun StageBar(d: DownloadEntry) { } } -/** 1, or while [on] (and animations run), an alpha going 1 → 0.5 → 1 over the rail's live-dot period. */ @Composable private fun breathing(on: Boolean): Float { if (!on || Motion.scale == 0f) return 1f @@ -497,10 +446,6 @@ private fun eta(seconds: Long): String = when { else -> "~${seconds / 3600} h ${(seconds % 3600) / 60} m" } -/** - * The queue's two knobs on one line - the speed tier on the left, downloads at a time on the - * right; it sits in the cog's popup. A narrow page puts them on two tight lines. - */ @Composable internal fun DownloadControls(s: FrontEndState, a: FrontEndActions) { val ctx = LocalContext.current @@ -524,12 +469,6 @@ internal fun DownloadControls(s: FrontEndState, a: FrontEndActions) { } } -/** - * The chip row's cog: the section's own settings stepped out of it - which tab a store opens on, - * mature titles, the speed tier and how many at once, and the download engine. Their only home; - * Setup no longer repeats them. Every install is added to Steam; that is not a choice. [s] is - * read live, so a change shows at once in what is stepped out. - */ internal fun storesSettingsStep(at: Rect, s: androidx.compose.runtime.State<FrontEndState>, a: FrontEndActions, accent: androidx.compose.ui.graphics.Color): StepAsk = StepAsk( anchor = at, side = StepSide.Left, accent = accent, items = 2, maxWidth = 440.dp, @@ -541,7 +480,6 @@ internal fun storesSettingsStep(at: Rect, s: androidx.compose.runtime.State<Fron verticalArrangement = Arrangement.spacedBy(6.dp), ) { val st = s.value - // No OK: B or a tap outside folds it, and a pad starts on the first choice. SettingsRow(stringResource(R.string.setup_stores_open_on), null, stacked = true) { Box(Modifier.focusRequester(first).focusGroup()) { SegmentedTabs( listOf(SessionPrefs.STORES_OPEN_LIBRARY to stringResource(R.string.stores_tab_library), SessionPrefs.STORES_OPEN_STORE to stringResource(R.string.stores_tab_store)), @@ -551,7 +489,6 @@ internal fun storesSettingsStep(at: Rect, s: androidx.compose.runtime.State<Fron SettingsRow(stringResource(R.string.stores_show_mature), null) { ToggleSwitch(st.storesShowMature, label = stringResource(R.string.stores_show_mature)) { a.onStoresShowMature(it) } } - // The queue's two knobs as one compact row, the same control the Downloads page has. Column(Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 11.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { DownloadControls(st, a) } val context = androidx.compose.ui.platform.LocalContext.current var gogAchievements by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(com.droiddeck.launcher.stores.gog.GogComet.enabled(context)) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt index e7d645a03..fb7f2d4ee 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt @@ -40,11 +40,6 @@ import com.droiddeck.launcher.stores.StoreInstallRoot import com.droiddeck.launcher.stores.StoresState import com.droiddeck.launcher.stores.formatBytes -/** - * "Install <game>:" over one row per place - internal storage, the card - each the action - * itself: icon, name, free space. Asked only when a card is there. The row picked last time - * starts focused, so a pad confirms the usual place with one press; nothing is chosen for the user. - */ @Composable internal fun InstallWhereDialog(item: CatalogItem, onDismiss: () -> Unit) { val ctx = LocalContext.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresMotion.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresMotion.kt index de7bcce6c..3d5ba671c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresMotion.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresMotion.kt @@ -47,13 +47,8 @@ import kotlin.math.atan2 import kotlin.math.max import kotlin.math.sin -// The Stores section's moves, in the launcher's own vocabulary: an install bloops into a dot that -// flies to the Downloads chip, sign-in floods in the store's colour and comes home to the chip's -// dot, and the confirms step out of the control that asked (StepOut). Everything here is in root -// coordinates; the Stores page draws it in its own space. internal object StoresMotion { - /** The Install (or Resume install) button last pressed, its label and when: a download that starts soon after flies from it. */ var installFrom: Rect? = null private set var installLabel: String = "" @@ -66,28 +61,21 @@ internal object StoresMotion { installAt = SystemClock.uptimeMillis() } - /** The button just pressed, if it was pressed in the last few seconds; not consumed. */ fun recentInstall(): Rect? = installFrom.takeIf { SystemClock.uptimeMillis() - installAt < 5_000 } - /** The button a download that just started came from, once; stale after 20 s (a slow manifest). */ fun takeInstall(): Rect? = installFrom.takeIf { SystemClock.uptimeMillis() - installAt < 20_000 }.also { installAt = 0L } - /** Where the Downloads chip sits, for a download's dot to land in. */ var downloadsChip by mutableStateOf<Rect?>(null) - /** Bumped each time a dot lands: the chip's count and the rail's badge pop on it. */ var landed by mutableIntStateOf(0) - /** Dots in flight to the Downloads chip. */ val flights = mutableStateListOf<Flight>() fun fly(from: Rect) { if (Motion.scale == 0f || downloadsChip == null) { landed++; return } - // The count sits at the chip's right end. flights += Flight(from, to = { downloadsChip?.let { Offset(it.right - 22.dp.toPx(), it.center.y) } }, onLand = { landed++ }) } - /** The confirm or list stepping out of a control on the Stores page now, and whether it is open. */ var step by mutableStateOf<StepAsk?>(null) private set var stepOpen by mutableStateOf(false) @@ -99,14 +87,12 @@ internal object StoresMotion { stepOpen = true } - /** Drops whatever is stepped out or flooding, at once: the page it belonged to has gone. */ fun drop() { step?.let { step = null; it.onDismiss() } stepOpen = false signIn = null } - /** Folds the current one back into its control. */ fun fold() { stepOpen = false } internal fun closed(a: StepAsk) { @@ -114,24 +100,18 @@ internal object StoresMotion { a.onDismiss() } - /** A store's sign-in flooding the page, or draining back out of it. */ var signIn by mutableStateOf<SignInFlood?>(null) - /** Per store, bumped to send its chip's dot one pulse ring (signed in, picked). */ val pulses = androidx.compose.runtime.mutableStateMapOf<Store, Int>() fun pulse(store: Store) { pulses[store] = (pulses[store] ?: 0) + 1 } - /** Where each store's dot sits on its chip, for a sign-in to come home to. */ val dots = HashMap<Store, Rect>() - /** Where the open game page's hero art sits (root px): what a card's flood lands on, and leaves from. */ var heroAt by mutableStateOf<Rect?>(null) - /** The open game page came out of a card: its hero holds still, for the card's flood to land on. */ var heroLands by mutableStateOf(false) - /** The card a game page is about to open from. */ private var card: CardMark? = null private var cardAt = 0L @@ -140,17 +120,11 @@ internal object StoresMotion { cardAt = SystemClock.uptimeMillis() } - /** The card game [key] was just opened from, once; a page opened another way shows without a flood. */ fun takeCard(key: String): CardMark? = card?.takeIf { it.key == key && SystemClock.uptimeMillis() - cardAt < 1_000 }.also { card = null } } -/** A game card at [bounds] (root px) with corner [corner] (px) and its art, for the page it opens to flood out of. */ internal class CardMark(val key: String, val bounds: Rect, val corner: Float, val art: String?) -/** - * A confirm or short list for [StepOut] on the Stores page, asked by a control that sits at [anchor] - * (root px). [content] gets a close that folds it; [onDismiss] runs once it has folded, however. - */ internal class StepAsk( val anchor: Rect, val side: StepSide, @@ -164,22 +138,15 @@ internal class StepAsk( ) internal class SignInFlood(val store: Store, val from: Rect, val start: Color, val color: Color) { - /** Covered and handed to the login page; waiting for the page to come back. */ var away by mutableStateOf(false) - /** Back from the login page: draining into [to]. */ var drainTo by mutableStateOf<Rect?>(null) } -/** The dots flying to the Downloads chip, drawn over the page whose root offset is [origin]. */ @Composable internal fun FlightLayer(origin: Offset) { for (f in StoresMotion.flights.toList()) key(f.id) { FlightDot(f, origin) { StoresMotion.flights.remove(f) } } } -/** - * Pops (1.3 to 1, with one ring) each time [trigger] changes: by default each time a download's - * dot lands, for the count on the chip and the rail; any other landing passes its own count. - */ internal fun Modifier.landingPop(trigger: Int = StoresMotion.landed): Modifier = composed { val pal = LocalPalette.current val scale = remember { Animatable(1f) } @@ -201,11 +168,6 @@ internal fun Modifier.landingPop(trigger: Int = StoresMotion.landed): Modifier = }.graphicsLayer { scaleX = scale.value; scaleY = scale.value } } -/** - * A pressed button in [start] stretching out over this element in [color] (LaunchFlood's move: - * each edge on its own loose spring, the furthest first, blobby in flight). [from] is in this - * element's px. Calls [onCovered] once, when nothing but [color] shows; then holds. - */ @Composable internal fun ColorFlood(from: Rect, start: Color, color: Color, onCovered: () -> Unit) { val edges = remember { List(4) { Animatable(0f) } } @@ -220,7 +182,6 @@ internal fun ColorFlood(from: Rect, start: Color, color: Color, onCovered: () -> val lead = travel[i] / far launch { delay(Motion.ms((120 * (1f - lead)).toInt()).toLong()) - // Overshoots past the edge, off the element, so the wobble is felt in the pull. edge.animateTo(1f, if (Motion.scale == 0f) Motion.sp() else spring(dampingRatio = 0.48f, stiffness = lerp(110f, 190f, lead))) { if (value >= 1f) reached[i] = true } @@ -237,10 +198,6 @@ internal fun ColorFlood(from: Rect, start: Color, color: Color, onCovered: () -> } } -/** - * [color] over the whole element drawing down into [to] (px, here), critically damped, the near - * edges first (PageFlood's way back), then fading out on it; [onDone] after. - */ @Composable internal fun ColorDrain(color: Color, to: Rect, toCorner: Float, onDone: () -> Unit) { val edges = remember { List(4) { Animatable(1f) } } @@ -270,7 +227,6 @@ internal fun ColorDrain(color: Color, to: Rect, toCorner: Float, onDone: () -> U private fun Modifier.onSizeChangedCompat(onSize: (Size) -> Unit) = onSizeChanged { onSize(Size(it.width.toFloat(), it.height.toFloat())) } -/** A rect from [r] with its edges [v] of the way to this scope's (left, top, right, bottom): blobby in flight. */ private fun DrawScope.drawFloodShape(r: Rect, restCorner: Float, v: List<Float>, color: Color) { val left = lerp(r.left, 0f, v[0]) val top = lerp(r.top, 0f, v[1]) @@ -284,7 +240,6 @@ private fun DrawScope.drawFloodShape(r: Rect, restCorner: Float, v: List<Float>, drawRoundRect(color, Offset(left, top), Size(w, h), CornerRadius(corner)) } -/** Sends one pulse ring out of a dot each time [trigger] changes after the first composition. */ internal fun Modifier.pulseRing(trigger: Int, color: Color): Modifier = composed { val ring = remember { Animatable(1f) } val seen = remember { intArrayOf(trigger) } @@ -303,11 +258,6 @@ internal fun Modifier.pulseRing(trigger: Int, color: Color): Modifier = composed } } -/** - * [items] in a column where a row that comes enters with [enter] and a row that goes stays on to - * leave with [exit] (a Column has no item animations of its own). [content] gets the row and - * whether it came after the list was first shown. - */ @Composable internal fun <T> AnimatedRows( items: List<T>, @@ -322,7 +272,6 @@ internal fun <T> AnimatedRows( val later = remember { mutableMapOf<Any, Boolean>() } val mounted = remember { booleanArrayOf(false) } val now = items.associateBy(key) - // Keep a leaving row where it was among the ones that stay. val merged = ArrayList<Any>() var j = 0 for (k in now.keys) { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt index 1d722dd3d..a070e0158 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt @@ -133,24 +133,15 @@ import com.droiddeck.launcher.stores.download.DownloadStage import com.droiddeck.launcher.stores.download.DownloadState import com.droiddeck.launcher.stores.formatBytes -// The Stores section: GOG, Epic Games and Amazon Games libraries and storefronts, and one -// download queue for the three, in a single full-width pane. -/** The chip row's fourth entry, beside the three stores. */ private const val DOWNLOADS = "downloads" private val TABS = listOf("store", "library", "installed") -/** - * The modes a store has, picked from its chip (A steps them out of it) or cycled with LB / RB: - * Store, Library and Installed. Amazon has no public catalog, so no Store. - */ internal fun tabsFor(store: Store?): List<String> = if (store == Store.AMAZON) TABS.drop(1) else TABS -/** [tab] if [store] has it, else Library (the open-on Store choice on Amazon falls back there). */ internal fun tabFor(store: Store?, tab: String): String = if (tab in tabsFor(store)) tab else "library" -/** What the pane under the chips shows: a store's tab (signed in or not) or the downloads. */ private data class PaneKey(val chip: String, val tab: String, val signedIn: Boolean) @OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class, androidx.compose.animation.ExperimentalAnimationApi::class) @@ -164,26 +155,18 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier var tab by rememberSaveable { mutableStateOf(if (SessionPrefs.storesOpenTab(ctx) == SessionPrefs.STORES_OPEN_STORE) "store" else "library") } var query by rememberSaveable { mutableStateOf("") } var openGame by rememberSaveable { mutableStateOf<String?>(null) } - // The cog's settings read the front end's state live while stepped out. val liveState = androidx.compose.runtime.rememberUpdatedState(s) LaunchedEffect(StoresState.openDownloads) { if (StoresState.openDownloads) { chip = DOWNLOADS; StoresState.openDownloads = false } } val store = Store.byId(chip) - // A tab this store does not have (Store or All on Amazon) reads as Library. val shownTab = tabFor(store, tab) val pane = PaneKey( chip, if (store == null) "" else shownTab, store == null || (StoresState.isSignedIn(store) && StoresState.expired[store] != true), ) - // Incoming and outgoing panes must not share a scroll state while both are on screen. val grids = remember { HashMap<PaneKey, LazyGridState>() } val grid = grids.getOrPut(pane) { LazyGridState() } LaunchedEffect(Unit) { StoresState.refresh(ctx) } - // A store the account is signed into fills itself when its chip is on screen. LaunchedEffect(chip, StoresState.accounts[store]) { if (store != null && StoresState.isSignedIn(store)) StoresState.open(ctx, store) } - // A pad's focus sits on something the page is about to replace - the card that opens a game, - // a tab's contents - and would be lost with it, leaving the next press to land on the rail. - // Each move says where focus goes next: into the game page's main action, back to the card - // it was opened from, or onto the first card of a tab or store just picked. val ff = LocalFrontFocus.current val inputMode = androidx.compose.ui.platform.LocalInputModeManager.current var focusMove by remember { mutableStateOf<Pair<String, Int>?>(null) } @@ -192,15 +175,11 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier fun open(key: String) { lastOpened = key; openGame = key; move("detail") } fun closeGame() { openGame = null; move("card") } fun switchTab(to: String) { tab = to; move("first") } - // Picking a chip opens the tab Setup says (Library or Store); a tab the user switches to - // afterwards stays until another chip is picked. val pickChip: (String) -> Unit = { id -> chip = id; openGame = null; query = "" if (id != DOWNLOADS) tab = tabFor(Store.byId(id), if (s.storesOpenTab == SessionPrefs.STORES_OPEN_STORE) "store" else "library") move("first") } - // A pad moving along the chips shows each store it stops on, without A: a beat after it - // settles, so sweeping past two chips does not open both. Focus stays on the chip. var focusedChip by remember { mutableStateOf<String?>(null) } LaunchedEffect(focusedChip) { val id = focusedChip ?: return@LaunchedEffect @@ -210,20 +189,15 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier if (id != DOWNLOADS) tab = tabFor(Store.byId(id), if (s.storesOpenTab == SessionPrefs.STORES_OPEN_STORE) "store" else "library") } } - // A store's chip steps its modes out of itself (End session's shape, in the store's colour); - // picking one shows that store in it. A store not signed in has none: its chip just opens it. val pal = LocalPalette.current fun openModes(st: Store, at: Rect?) { - // Another store's chip (a tap; a pad's focus has already shown it): that store. if (chip != st.id) { pickChip(st.id); return } - // Not signed in: nothing to pick, so A goes on to its Sign in. if (at == null || !StoresState.isSignedIn(st) || StoresState.expired[st] == true) { move("first"); return } val modes = tabsFor(st) val current = if (chip == st.id) shownTab else null var picked = false StoresMotion.ask(StepAsk( anchor = at, side = StepSide.Right, accent = storePalette(pal, st).signal, items = modes.size, - // Folded: focus goes to the first card of what was picked, or back to the chip. onDismiss = { move(if (picked) "first" else "item:storechip:${st.id}") }, handle = { ChipLabel(st, MaterialTheme.colorScheme.onBackground) }, ) { @@ -255,17 +229,10 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier BackHandler(enabled = openGame != null) { closeGame() } val scroll = rememberScrollState() val detailScroll = rememberScrollState() - // A store or tab just picked starts at the top. Closing a game page leaves the grid where it - // was, so the page draws back into the card it came from and focus lands on it. LaunchedEffect(chip, tab) { scroll.scrollTo(0); grid.scrollToItem(0) } LaunchedEffect(openGame) { detailScroll.scrollTo(0) } LaunchedEffect(focusMove, openGame) { if (inputMode.inputMode != androidx.compose.ui.input.InputMode.Keyboard || ff == null) return@LaunchedEffect - // Looked up each frame, and asked again until focus is really there: what it goes to (the - // game page's action, the card under it) is composed and placed a frame or two after the - // move that asks for it, and a request before then is dropped without a word. - // A mode with nothing in it (Installed, none installed) has no first card: after a few - // frames focus goes to the store's chip rather than falling back to the rail. var frames = 0 fun attached(id: String) = (ff.attached[id] ?: 0) > 0 fun target(): Pair<androidx.compose.ui.focus.FocusRequester, String?>? { @@ -276,15 +243,12 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier kind == "card" -> lastOpened?.let { "card:$it" }?.let { id -> ff.items[id]?.takeIf { attached(id) }?.let { it to id } } ?: ff.firstTile.takeIf { ff.firstTileAttached > 0 }?.let { it to null } kind == "first" -> ff.firstTile.takeIf { ff.firstTileAttached > 0 }?.let { it to null } - // A store not signed in: its Sign in. ?: ff.primary.takeIf { ff.primaryAttached > 0 }?.let { it to FrontFocus.PRIMARY } ?: "storechip:$chip".let { id -> ff.items[id]?.takeIf { ++frames > 12 && attached(id) }?.let { it to id } } kind?.startsWith("item:") == true -> kind.removePrefix("item:").let { id -> ff.items[id]?.takeIf { attached(id) }?.let { it to id } } else -> null } } - // The press that asked for this move must not land on where focus goes: its release - // would press that too (A on a chip went on to Sign in). Wait for A to come up. repeat(60) { if (!HeldKeys.confirm) return@repeat; androidx.compose.runtime.withFrameNanos { } } repeat(45) { androidx.compose.runtime.withFrameNanos { } @@ -294,9 +258,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier } } - // The game page is a layer over the grid: it floods out of the card it was opened from (the - // cog's PageFlood, carrying the card's art), and Back draws it into that card again. [detail] - // outlives [openGame] while it draws back. var detail by remember { mutableStateOf<String?>(null) } var leaving by remember { mutableStateOf(false) } var detailFrom by remember { mutableStateOf<Origin?>(null) } @@ -337,9 +298,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier onNext = { if (openGame == null && chip != DOWNLOADS) { val t = tabsFor(store); switchTab(t[(t.indexOf(shownTab) + 1) % t.size]) } }, ), ) { - // The chip row stays put and paints nothing - the page's background shows through it. What - // scrolls is clipped at the row's lower edge and, once scrolled, faded out there: the - // content's own alpha, not a colour laid over it. Box(Modifier.fillMaxWidth().padding(bottom = 8.dp)) { StoreTheme(Store.byId(focusedChip ?: chip)) { Rise(0) { StoreChips(chip, focusedChip, s.storeDownloadsActive, onStore = { st, at -> openModes(st, at) }, onDownloads = { if (chip != DOWNLOADS) pickChip(DOWNLOADS) else move("first") }, @@ -362,16 +320,11 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier ) }, ) { - // Under a game page the grid stays composed - its scroll and the card to come back to - // stay put - but sinks away, and a pad can neither reach it nor be sent to it. Once the - // page starts drawing back the grid takes focus again, so it lands on the card at once. val gridFocus = if (covering) null else ff CompositionLocalProvider(LocalFrontFocus provides gridFocus) { Column( modifier = Modifier.fillMaxSize() .graphicsLayer { scaleX = gridScale; scaleY = gridScale; alpha = gridAlpha } - // Always the same group, only its rule changes: a focus group added around - // the card that has focus leaves the focus tree refusing every request after. .focusProperties { enter = { if (coveringNow.value) androidx.compose.ui.focus.FocusRequester.Cancel else androidx.compose.ui.focus.FocusRequester.Default } } .focusGroup(), ) { @@ -381,29 +334,20 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier transitionSpec = { val sameStore = initialState.chip == targetState.chip && initialState.signedIn == targetState.signedIn (if (sameStore && initialState.tab != targetState.tab) { - // LB / RB: the tab's contents leave the way the bumper points and the - // next tab's come in from the other side, the column nearest leading. val dir = if (TABS.indexOf(targetState.tab) > TABS.indexOf(initialState.tab)) 1 else -1 (slideInHorizontally(Motion.sp(0.7f, Spring.StiffnessMediumLow)) { it / 6 * dir } + fadeIn(Motion.tw(220, 60))) .togetherWith(slideOutHorizontally(Motion.tw(170)) { -it / 6 * dir } + fadeOut(Motion.tw(170)) + scaleOut(Motion.tw(170), targetScale = 0.97f)) } else { - // Another store: the shelf sinks away, the next rises in (the pane's own move). (fadeIn(Motion.tw(300, 80)) + slideInVertically(Motion.tw(420, 80)) { it / 24 }) .togetherWith(fadeOut(Motion.tw(170)) + scaleOut(Motion.tw(170), targetScale = 0.97f)) }).using(SizeTransform(clip = false)) }, label = "storesPane", ) { k -> - // What is leaving is not where a pad's next move should land. val outgoing = transition.targetState == androidx.compose.animation.EnterExitState.PostExit CompositionLocalProvider(LocalFrontFocus provides if (outgoing) null else LocalFrontFocus.current) { - // AnimatedContent stacks its child in a Box: the pane's rows need their own column. - // A pad moving while the pane changes must land on what is coming, not on - // what is leaving: focus there vanishes with it. Same group always, only - // its rule changes (adding one around focus breaks the focus tree). val leavingNow = androidx.compose.runtime.rememberUpdatedState(outgoing) val st = Store.byId(k.chip) - // Keep the outgoing pane's palette stable while the next store comes in. StoreTheme(st) { Column( Modifier.fillMaxSize() @@ -413,7 +357,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier when { k.chip == DOWNLOADS -> Column(Modifier.fillMaxWidth().verticalScroll(scroll).padding(top = 4.dp, bottom = 16.dp)) { StoresDownloadsPane(s, a) } st == null -> {} - // Signed out, or a sign-in that ran out (a launch could not get its code): sign in again. !k.signedIn -> Box(Modifier.fillMaxWidth().verticalScroll(scroll).padding(top = 4.dp, bottom = 16.dp)) { SignInCard(st, viewportH - 20.dp) } else -> Storefront(st, k.tab, query, s, a, grids.getOrPut(k) { LazyGridState() }, onQuery = { query = it }, onOpen = { open(it) }) } @@ -427,9 +370,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier val detailStore = Store.byId(key.substringBefore(':')) ?: store StoreTheme(detailStore) { val body: @Composable () -> Unit = { - // One focus group, so the pad walks the page's own controls - back, the hero's - // actions - and reaches the rail only with Left from them. It takes every touch, - // so nothing reaches the grid under it. Column( Modifier.fillMaxSize().pointerInput(Unit) { detectTapGestures { } } .verticalScroll(detailScroll).padding(top = 4.dp, bottom = 16.dp), @@ -440,9 +380,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier val from = detailFrom if (from == null) body() else { - // Sized up front: the flood draws the art only once it knows its size, and a painter - // left to take its size from drawing would never load. The card's own bitmap stands - // in from the memory cache until the full one is decoded. val art = detailArt?.let { url -> coil.compose.rememberAsyncImagePainter( remember(url) { @@ -451,16 +388,12 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier }, ) } - // Onto the page's own hero art, not the whole pane: grown to the pane, the art - // then cut down to the hero's banner (and back out to the pane to close). PageFlood(from, leaving, art, onto = { StoresMotion.heroAt }, ontoCorner = 16.dp) { body() } } } } } } - // Over the whole page: an install's dot on its way to Downloads, a confirm stepping out of the - // control that asked for it, and a sign-in's flood. FlightLayer(hostAt) StoresMotion.step?.let { ask -> key(ask) { @@ -473,9 +406,7 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier } SignInFloodLayer(hostAt) } - // Leaving the section: nothing stays stepped out of a control that is no longer on screen. androidx.compose.runtime.DisposableEffect(Unit) { onDispose { StoresMotion.drop() } } - // An install that started from a button here flies from it to the Downloads chip. val activeKeys = StoresState.downloads.filter { it.isActive }.map { it.key }.toSet() val seenActive = remember { mutableSetOf<String>().also { it.addAll(activeKeys) } } LaunchedEffect(activeKeys) { @@ -486,10 +417,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier StoresState.pendingInstall?.let { item -> InstallWhere(item) } } -/** - * "Install <game>:" with a row per place, grown out of the Install button that asked (End - * session's step-out, in blue); the plain dialog when that button is not on screen. - */ @Composable private fun InstallWhere(item: CatalogItem) { val ctx = LocalContext.current @@ -519,7 +446,6 @@ private fun InstallWhere(item: CatalogItem) { modifier = Modifier.fillMaxWidth().stepItem(1 + i).then(if (i == defaultIndex) Modifier.focusRequester(first) else Modifier), ) { SessionPrefs.setStoresInstallTarget(ctx, t.root.absolutePath) - // The fold and the dot leave together: the download it starts flies from Install. StoresMotion.markInstall(from, label) StoresState.install(ctx, item, t.root) StoresMotion.fold() @@ -530,7 +456,6 @@ private fun InstallWhere(item: CatalogItem) { } } -/** A sign-in's flood: out of the button in the store's colour, and home again once the login page closes. */ @Composable private fun SignInFloodLayer(hostAt: Offset) { val f = StoresMotion.signIn ?: return @@ -539,7 +464,6 @@ private fun SignInFloodLayer(hostAt: Offset) { val density = androidx.compose.ui.platform.LocalDensity.current key(f) { var paused by remember { mutableStateOf(false) } - // Signed in: into the store's dot on its chip. Not (closed, or it failed): back into the button. fun back() { if (f.drainTo == null) f.drainTo = if (StoresState.isSignedIn(f.store)) StoresMotion.dots[f.store] ?: f.from else f.from } androidx.compose.runtime.DisposableEffect(lifecycle) { val obs = androidx.lifecycle.LifecycleEventObserver { _, e -> @@ -549,7 +473,6 @@ private fun SignInFloodLayer(hostAt: Offset) { lifecycle.addObserver(obs) onDispose { lifecycle.removeObserver(obs) } } - // The store had no login page to open: nothing will come back, so drain now. LaunchedEffect(f.away) { if (f.away) { delay(1_500); if (!paused) back() } } val to = f.drainTo if (to == null) ColorFlood(f.from.translate(-hostAt), f.start, f.color) { @@ -568,27 +491,17 @@ private fun SignInFloodLayer(hostAt: Offset) { } } -// ---- the chip row ------------------------------------------------------------------------------- -/** - * Four equal chips - the stores with a signed-in dot, Downloads with its count - and the cog. The - * selection is one shape that belongs to the row, not each chip's background: it glides from chip - * to chip like the focus ring, its leading edge first, tinted in the store's colour, and a hop - * past a chip goes as a drop. - */ @Composable private fun StoreChips( selected: String, focused: String?, active: Int, onStore: (Store, Rect?) -> Unit, onDownloads: () -> Unit, onSettings: (Rect?) -> Unit, - /** The chip a pad's focus is on (null when it leaves the row): the page shows that store. */ onFocusChip: (String?) -> Unit, ) { val pal = LocalPalette.current val keys = Store.entries.map { it.id } + DOWNLOADS val bounds = remember { androidx.compose.runtime.mutableStateMapOf<String, Rect>() } val rootBounds = remember { HashMap<String, Rect>() } - // The fill travels with focus; only the expensive pane change waits for the debounce. val highlighted = focused ?: selected - // With a pad the row has no fill of its own: the focus ring's infill is the only one. val inputMode = androidx.compose.ui.platform.LocalInputModeManager.current.inputMode val fillShown = inputMode != androidx.compose.ui.input.InputMode.Keyboard val tint = androidx.compose.animation.animateColorAsState( @@ -603,7 +516,6 @@ private fun StoreChips( modifier = Modifier.weight(1f).onGloballyPositioned { bounds[store.id] = it.boundsInParent(); rootBounds[store.id] = it.boundsInRoot() }, lead = { val c = sourceColours(store.id) - // A store's dot pulses when a sign-in lands (and, with the chip's lead, when picked). Box( Modifier.size(12.dp) .onGloballyPositioned { StoresMotion.dots[store] = it.boundsInRoot() } @@ -629,7 +541,6 @@ private fun StoreChips( IconChip(Icons.Filled.Settings, stringResource(R.string.stores_settings)) { onSettings(cogAt) } } } - // Over the chips: the fill is translucent, and the other chips' rest fill would hide it in flight. ChipGlide(keys.indexOf(highlighted), bounds[highlighted], tint, visible = fillShown) } } @@ -649,15 +560,12 @@ private fun StoreChip( val scale by animateFloatAsState(if (pressed) 0.95f else 1f, Motion.sp(0.5f, Spring.StiffnessMedium), label = "chipScale") val shape = RoundedCornerShape(99.dp) val ink = if (on) colors.onBackground else if (hot) colors.onBackground else colors.onSurfaceVariant - // The chip whose page shows has a bar of its colour along its foot. Picking it pulses its - // lead (a store's dot, Downloads' icon). var picked by remember { mutableStateOf(0) } val wasOn = remember { booleanArrayOf(on) } LaunchedEffect(on) { if (on && !wasOn[0]) picked++ wasOn[0] = on } - // The row's glide draws the look of the chip [under] it, so that one draws no rest state of its own. Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), modifier = modifier.paneItem("storechip:$key") @@ -682,7 +590,6 @@ private fun StoreChip( } } -/** The chip row's cog: icon only, the chips' height; opens the section's settings. */ @Composable private fun IconChip(icon: androidx.compose.ui.graphics.vector.ImageVector, description: String, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -703,18 +610,11 @@ internal fun CountPill(count: Int) { val pal = LocalPalette.current Text( count.toString(), fontSize = 11.sp, fontWeight = FontWeight.Bold, color = pal.onSignal, maxLines = 1, - // Pops as an install's dot lands in it. modifier = Modifier.landingPop().clip(RoundedCornerShape(8.dp)).background(pal.signal).padding(horizontal = 6.dp, vertical = 1.dp), ) } -// ---- sign-in ----------------------------------------------------------------------------------- -/** - * No account for this store yet: the Steam tab's shape in the store's colour. A tilted wall of the - * store's own games drifts behind (its public storefront; blank capsules in its colour when it has - * none), fading out toward the words, and Sign in sits large at the foot. [height] is the pane's. - */ @Composable private fun SignInCard(store: Store, height: Dp) { val ctx = LocalContext.current @@ -722,9 +622,7 @@ private fun SignInCard(store: Store, height: Dp) { val pal = LocalPalette.current val narrow = LocalNarrowPane.current val c = sourceColours(store.id) - // The storefront needs no account; it is what the wall is made of. LaunchedEffect(store) { StoresState.preview(ctx, store) } - // A handful of games would repeat down every column: under eight, the blank wall in its colour. val art = remember(StoresState.shelves[store]) { StoresState.shelves[store]?.all.orEmpty().filter { !it.mature }.mapNotNull { it.tallImageUrl ?: it.imageUrl }.distinct().take(48) .takeIf { it.size >= 8 }.orEmpty() @@ -733,11 +631,9 @@ private fun SignInCard(store: Store, height: Dp) { val blank = WallCapsule(Brush.linearGradient(listOf(c.fill, colors.background)), edge = c.dot.copy(alpha = 0.22f)) val ground = SolidColor(c.fill) Box(Modifier.fillMaxWidth().heightIn(min = height.coerceAtLeast(0.dp)).clip(shape).background(colors.background).border(1.dp, pal.line, shape)) { - // The storefront arrives a moment after the page: its wall fades in over the blank one. androidx.compose.animation.Crossfade(art, animationSpec = Motion.tw(600), label = "signInWall", modifier = Modifier.matchParentSize()) { tiles -> TiltedWall(tiles.size, if (tiles.isEmpty()) 75_000 else 50_000) { i -> if (tiles.isEmpty()) blank else WallCapsule(ground, tiles[i]) } } - // The wall gives way to the words: dark at the left and the foot, and the store's glow under them. Box( Modifier.matchParentSize().background( Brush.horizontalGradient( @@ -768,10 +664,6 @@ private fun SignInCard(store: Store, height: Dp) { } } -/** - * Sign in: the button floods the page in the store's colour (LaunchFlood's move), the login page - * opens on that colour, and the flood comes home into the store's dot once it closes. - */ @Composable private fun SignInButton(store: Store, large: Boolean = false) { val ctx = LocalContext.current @@ -786,7 +678,6 @@ private fun SignInButton(store: Store, large: Boolean = false) { } } -/** A square in the store's colour with its name on it: the stores ship no logo this app may bundle. */ @Composable internal fun StoreLogo(store: Store, size: Dp) { val c = sourceColours(store.id) @@ -795,7 +686,6 @@ internal fun StoreLogo(store: Store, size: Dp) { } } -// ---- storefront -------------------------------------------------------------------------------- @Composable private fun Storefront( @@ -807,19 +697,14 @@ private fun Storefront( val colors = MaterialTheme.colorScheme val library = StoresState.library[store] ?: emptyList() val shelves = StoresState.shelves[store] - // The Installed tab, its count and every card's installed mark come from the installs on disk. val installedItems = remember(StoresState.installed, library) { com.droiddeck.launcher.stores.installedCards(store, StoresState.installed, library) } val installedKeys = remember(installedItems) { installedItems.map { it.id }.toSet() + StoresState.installed.filter { it.sidecar.store == store }.map { it.sidecar.id } } val q = query.trim().lowercase() fun matches(i: CatalogItem) = q.isEmpty() || i.title.lowercase().contains(q) - // The storefront leaves out what the store rates or tags as adult unless the user shows it; owned games always show. fun shown(i: CatalogItem) = s.storesShowMature || !i.mature || i.owned fun shelf(l: List<CatalogItem>?) = l?.filter(::shown) - // The Store tab's search: the catalog the shelves brought and the library, each title once. val everything = remember(library, shelves) { (library + (shelves?.all ?: emptyList())).distinctBy { it.id } } - // Installed is a mode of its own now, picked from the store's chip like Store and Library. val installedOnly = tab == "installed" - // Until the cache is read (a moment, off the main thread) there is nothing to count or to say is missing. val loaded = StoresState.library.containsKey(store) val showGrid = tab != "store" || q.isNotEmpty() val visible = remember(library, installedItems, everything, tab, q, s.storesShowMature) { @@ -834,8 +719,6 @@ private fun Storefront( tab == "library" && library.isEmpty() && StoresState.status[store] != null -> R.string.stores_library_loading else -> R.string.stores_nothing_matches }) - // A lazy item can be recomposed or prefetched out of order; the first grid card is identified - // by its key, rather than whichever happens to compose first. var firstPlaced = false var columns by remember { mutableIntStateOf(1) } val density = androidx.compose.ui.platform.LocalDensity.current @@ -856,12 +739,11 @@ private fun Storefront( val next = storeGridNeighbor(index, visible.size, columns, direction) when { direction == 0 || index < 0 -> false - next == null -> direction > 0 // The last row stays in its column; Up can reach the header. + next == null -> direction > 0 else -> { if (key.action == KeyEvent.ACTION_DOWN) { val target = "card:${visible[next].key}" val targetIndex = headerItems + next - // Prefetched cards can accept focus without being on screen. val visibleTarget = grid.layoutInfo.visibleItemsInfo.any { it.index == targetIndex } if (!visibleTarget || (focus.attached[target] ?: 0) == 0 || runCatching { focus.items.getValue(target).requestFocus() }.isFailure) scope.launch { @@ -900,7 +782,6 @@ private fun Storefront( else -> stringResource(R.string.stores_search_library) }, ) { - // A purchase made elsewhere shows on a refresh; otherwise the library refreshes itself every six hours. SmallTextButton(stringResource(R.string.stores_refresh)) { StoresState.open(ctx, store, force = true) } val name = StoresState.accounts[store] ?: "" SignOutButton(store, name) @@ -932,15 +813,10 @@ private fun Storefront( } } -/** - * What the store is showing (Store, Library, Installed), a magnifier beside it that opens into the - * search field across the row, and [trailing] (the account) at the right, over a rule. - */ @Composable internal fun ModeHeader(title: String, query: String, onQuery: (String) -> Unit, placeholder: String, trailing: @Composable () -> Unit) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current - // Open while it holds a query; an empty field closes again once focus leaves it. var searching by remember { mutableStateOf(query.isNotEmpty()) } val open by animateFloatAsState(if (searching) 1f else 0f, Motion.sp(0.75f, 420f), label = "searchOpen") Column(Modifier.fillMaxWidth()) { @@ -951,7 +827,6 @@ internal fun ModeHeader(title: String, query: String, onQuery: (String) -> Unit, ) BoxWithConstraints(Modifier.weight(1f), contentAlignment = Alignment.CenterStart) { if (searching || open > 0.01f) { - // The field grows out of the magnifier, the row's width at most. val w = androidx.compose.ui.unit.lerp(44.dp, maxWidth.coerceAtMost(520.dp), open) Box(Modifier.width(w).graphicsLayer { alpha = (open * 1.6f).coerceAtMost(1f) }) { AdbTextField( @@ -968,7 +843,6 @@ internal fun ModeHeader(title: String, query: String, onQuery: (String) -> Unit, } } -/** A store chip's face: its dot and short name, for what steps out of it to carry. */ @Composable private fun ChipLabel(store: Store, ink: Color) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -977,7 +851,6 @@ private fun ChipLabel(store: Store, ink: Color) { } } -/** The page's accent in [store]'s own colour (GOG purple, Epic white, Amazon orange); [base] for none. */ internal fun storePalette(base: Palette, store: Store?): Palette { val (accent, deep) = when (store) { Store.GOG -> Color(0xFFC25BC8) to Color(0xFF9A3FA1) @@ -993,14 +866,12 @@ internal fun storePalette(base: Palette, store: Store?): Palette { ) } -/** The Store section's accent: Flathub's teal, with its dark ink on it. */ internal fun flathubPalette(base: Palette): Palette = Palette( base.id, base.label, base.detail, base.background, base.surface, base.surfaceVariant, base.line, base.line2, base.onBackground, base.onSurfaceVariant, primary = FlathubTeal, primary2 = Color(0xFF2E958D), onPrimary = FlathubInk, signal = FlathubTeal, good = base.good, error = base.error, ) -/** [content] with the focus ring, buttons, step-outs and the chip glide in Flathub's teal. */ @Composable internal fun FlathubTheme(content: @Composable () -> Unit) { val base = LocalPalette.current @@ -1009,12 +880,10 @@ internal fun FlathubTheme(content: @Composable () -> Unit) { CompositionLocalProvider(LocalPalette provides p) { MaterialTheme(colorScheme = scheme, typography = MaterialTheme.typography, content = content) } } -/** [content] with the focus ring, buttons and step-outs in [store]'s colour. */ @Composable internal fun StoreTheme(store: Store?, content: @Composable () -> Unit) { val base = LocalPalette.current val p = remember(base, store) { storePalette(base, store) } - // Downloads uses the base palette, but the provider stays in the same focus tree. val scheme = MaterialTheme.colorScheme.copy(primary = p.primary, onPrimary = p.onPrimary) CompositionLocalProvider(LocalPalette provides p) { MaterialTheme(colorScheme = scheme, typography = MaterialTheme.typography, content = content) } } @@ -1033,10 +902,6 @@ private fun SmallTextButton(text: String, key: String = text, onClick: () -> Uni ) } -/** - * "<account> · sign out", which asks first: are you sure steps out of it (End session's shape, in - * red), Stay signed in first, so a stray A costs nothing. - */ @Composable private fun SignOutButton(store: Store, name: String) { val ctx = LocalContext.current @@ -1066,7 +931,6 @@ private fun SignOutButton(store: Store, name: String) { } } -/** The header's magnifier: a round button the search field opens out of. */ @Composable internal fun SearchIcon(description: String, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -1082,7 +946,6 @@ internal fun SearchIcon(description: String, onClick: () -> Unit) { ) { Icon(Icons.Filled.Search, description, tint = if (hot) pal.signal else colors.onSurfaceVariant, modifier = Modifier.size(20.dp)) } } -/** A horizontal row of cards under a title; nothing at all when the list is empty or not here yet. */ @Composable private fun Shelf(title: String, items: List<CatalogItem>?, card: @Composable (CatalogItem) -> Unit) { if (items.isNullOrEmpty()) return @@ -1093,7 +956,6 @@ private fun Shelf(title: String, items: List<CatalogItem>?, card: @Composable (C } } -/** Only visible cards (and the grid's prefetch) compose; [empty] spans the row when there are none. */ private fun LazyGridScope.storeGrid(games: List<CatalogItem>, empty: String, card: @Composable (CatalogItem) -> Unit) { if (games.isEmpty()) item(key = "empty", span = { GridItemSpan(maxLineSpan) }) { Box(Modifier.padding(vertical = 32.dp).fillMaxWidth(), contentAlignment = Alignment.Center) { @@ -1103,19 +965,8 @@ private fun LazyGridScope.storeGrid(games: List<CatalogItem>, empty: String, car else items(games, key = { it.key }, contentType = { "game" }) { card(it) } } -/** - * A card's least width: shelf cards are this wide and a grid fits as many columns as the width - * allows at it (the columns then share the leftover), so a display-density setting changes the - * column count rather than the cards' size. 25% under the first 150 dp: five columns where there - * were four on the Pocket FIT. - */ private val CardWidth = 112.dp -/** - * One title: its wide art, the title, a price or status line and one button that says the one - * thing to do next - Install with its size, Play, the download's progress, Get for free, or View - * on the store. Tapping the card itself opens the game's page. - */ @Composable private fun GameCard(item: CatalogItem, store: Store, s: FrontEndState, a: FrontEndActions, installedKeys: Set<String>, onOpen: (String) -> Unit, first: Boolean = false, navigation: Modifier = Modifier) { val ctx = LocalContext.current @@ -1127,14 +978,12 @@ private fun GameCard(item: CatalogItem, store: Store, s: FrontEndState, a: Front val scale by animateFloatAsState(if (pressed) 0.985f else 1f, Motion.sp(0.5f, Spring.StiffnessMedium), label = "cardScale") val installed = item.id in installedKeys val download = StoresState.download("${store.id}:${item.id}")?.takeIf { it.isActive } - // Where it sits, so the game page can flood out of it (and draw back into it). val placed = remember { arrayOfNulls<androidx.compose.ui.layout.LayoutCoordinates>(1) } val corner = with(androidx.compose.ui.platform.LocalDensity.current) { 10.dp.toPx() } val open = { placed[0]?.takeIf { it.isAttached }?.let { StoresMotion.markCard(CardMark(item.key, it.boundsInRoot(), corner, item.imageUrl ?: item.tallImageUrl)) } onOpen(item.key) } - // A size the library did not give is looked up once the card is on screen or close to it. val screen = with(androidx.compose.ui.platform.LocalDensity.current) { androidx.compose.ui.platform.LocalConfiguration.current.screenHeightDp.dp.toPx() } val ahead = screen / 2 Column( @@ -1154,7 +1003,6 @@ private fun GameCard(item: CatalogItem, store: Store, s: FrontEndState, a: Front } } -/** A store's sync as a thin bar and its count ("25/55"); a moving bar when it has no count yet. */ @Composable private fun SyncBar(line: String) { val count = Regex("(\\d+)\\s*/\\s*(\\d+)").find(line) @@ -1169,14 +1017,8 @@ private fun SyncBar(line: String) { } } -/** How tall the card's one line is, over the blurred foot of its art. */ private val CardStrip = 26.dp -/** - * The card is its art: sharp above, and under the one line a blurred, darkened copy of the same - * picture, placed so the two meet without a seam. Blur needs API 31; below it the copy is only - * darkened, more strongly. - */ @OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable private fun CardFace(item: CatalogItem, download: com.droiddeck.launcher.stores.download.DownloadEntry?, hot: Boolean, marker: @Composable () -> Unit) { @@ -1196,7 +1038,6 @@ private fun CardFace(item: CatalogItem, download: com.droiddeck.launcher.stores. verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.matchParentSize().padding(horizontal = 8.dp), ) { - // A title too long for the strip scrolls through while the card has focus. Text( item.title, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, maxLines = 1, overflow = if (hot) TextOverflow.Clip else TextOverflow.Ellipsis, @@ -1205,7 +1046,6 @@ private fun CardFace(item: CatalogItem, download: com.droiddeck.launcher.stores. marker() } } - // A soft edge where the sharp art meets the strip. Spacer(Modifier.align(Alignment.BottomCenter).padding(bottom = CardStrip).fillMaxWidth().height(14.dp).background(Brush.verticalGradient(0f to Color.Transparent, 1f to Color.Black.copy(alpha = 0.35f)))) if (download != null) Box(Modifier.align(Alignment.BottomCenter).padding(start = 6.dp, end = 6.dp, bottom = CardStrip + 5.dp)) { ProgressBarThin(download.fraction, paused = download.state == DownloadState.PAUSED, verify = download.stage == DownloadStage.VERIFY) @@ -1214,14 +1054,12 @@ private fun CardFace(item: CatalogItem, download: com.droiddeck.launcher.stores. } } -/** The card's right-hand value: done, resume, progress, size, or the price. */ @Composable private fun CardMarker(item: CatalogItem, installed: Boolean, download: com.droiddeck.launcher.stores.download.DownloadEntry?) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current val small = 10.sp when { - // Installed: its size on disk and a small check after it. installed -> Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(3.dp)) { val ctx = LocalContext.current val game = StoresState.installedGame(item.store, item.id) @@ -1231,7 +1069,6 @@ private fun CardMarker(item: CatalogItem, installed: Boolean, download: com.droi } download != null -> Text("${download.percent}%", fontSize = small, fontWeight = FontWeight.Bold, color = pal.signal, maxLines = 1) item.owned && StoresState.isUnfinished(item) -> Text(stringResource(R.string.stores_resume), fontSize = small, fontWeight = FontWeight.Bold, color = pal.signal, maxLines = 1) - // The size in the title's own type; nothing until it is known. item.owned -> com.droiddeck.launcher.stores.StoreSizes.size(item).takeIf { it > 0 }?.let { Text(formatBytes(it), fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, maxLines = 1) } item.isFree -> Text(stringResource(R.string.stores_free), fontSize = small, fontWeight = FontWeight.Bold, color = pal.good, maxLines = 1) item.isDiscounted -> Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { @@ -1246,8 +1083,6 @@ private fun CardMarker(item: CatalogItem, installed: Boolean, download: com.droi internal fun CardArt(item: CatalogItem, modifier: Modifier) { val url = item.imageUrl ?: item.tallImageUrl val colors = MaterialTheme.colorScheme - // A dark ground under the image, so art with transparency never ends in white, and a quiet - // fade into the card's surface at the foot. Box(modifier.background(colors.surfaceVariant).then(if (url == null) Modifier.background(artBrush(hueOf(item.title))) else Modifier)) { if (url != null) AsyncImage(model = url, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize()) else Text(item.title, fontSize = 10.sp, fontWeight = FontWeight.Black, color = Color.White, maxLines = 3, overflow = TextOverflow.Ellipsis, modifier = Modifier.align(Alignment.BottomStart).padding(6.dp)) @@ -1260,8 +1095,6 @@ internal fun ProgressBarThin(fraction: Float, paused: Boolean = false, verify: B val pal = LocalPalette.current val colors = MaterialTheme.colorScheme val fill = when { done -> pal.good; paused -> Color(0xFFFFC24D); verify -> Color(0xFF9B6DFF); else -> pal.signal } - // The fill glides to each new value over a quarter second - progress arrives a few times a second - // and in steps - and is read only while drawing, so the bar animates without relayout. val shown = androidx.compose.animation.core.animateFloatAsState( fraction.coerceIn(0f, 1f), androidx.compose.animation.core.tween(250, easing = androidx.compose.animation.core.LinearEasing), label = "bar", ) @@ -1278,7 +1111,6 @@ internal fun downloadLabel(d: com.droiddeck.launcher.stores.download.DownloadEnt else -> if (d.stageFraction >= 0f) stringResource(R.string.stores_dl_active_percent, activeStageLabel(d), d.percent) else activeStageLabel(d) } -/** The active stage as the row and the page name it: Checking (files already there), Downloading, Verifying, Installing. */ @Composable internal fun activeStageLabel(d: com.droiddeck.launcher.stores.download.DownloadEntry): String = when (d.stage) { DownloadStage.MANIFEST -> if (d.stageItemsTotal > 0) stringResource(R.string.stores_stage_checking) else stringResource(R.string.stores_stage_manifest) @@ -1288,7 +1120,6 @@ internal fun activeStageLabel(d: com.droiddeck.launcher.stores.download.Download DownloadStage.DONE -> stringResource(R.string.stores_stage_done) } -/** Two taps: the first arms it ([confirm] shows), the second acts; it disarms itself after a few seconds. */ @Composable internal fun ConfirmButton(label: String, confirm: String, compact: Boolean = false, onConfirm: () -> Unit) { var armed by remember { mutableStateOf(false) } @@ -1305,19 +1136,13 @@ internal fun stageLabel(stage: DownloadStage): String = when (stage) { DownloadStage.DONE -> stringResource(R.string.stores_stage_done) } -/** The game's web page, in the device's browser: this app buys nothing and claims nothing itself. */ internal fun openStoreUrl(ctx: android.content.Context, item: CatalogItem) = openUrl(ctx, item.storeUrl) -/** A web page in the browser; nothing when the URL is blank or no app can open it. */ internal fun openUrl(ctx: android.content.Context, url: String) { if (url.isBlank()) return runCatching { ctx.startActivity(android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(url)).addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)) } } -/** - * Launches an installed store game the way the Games tab does: through its Steam shortcut. The - * shortcut exists once the client has (re)started since the install; until then the user is told. - */ internal fun launchStoreGame(ctx: android.content.Context, store: Store, id: String, s: FrontEndState, a: FrontEndActions) { val game = s.steamGames.firstOrNull { it.source == store.id && it.storeId == id } if (game == null) { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt index b4e6e5443..c2580a329 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt @@ -5,9 +5,7 @@ import androidx.compose.ui.res.stringResource import com.droiddeck.launcher.R import com.droiddeck.launcher.stores.StoresNative -// Controls the Stores section and its Setup group share. -/** Whether the native download engine is in this build, said where the stores are turned on. */ @Composable internal fun StoresEngineRow() { val version = StoresNative.version diff --git a/app/src/main/java/com/droiddeck/launcher/ui/TestTags.kt b/app/src/main/java/com/droiddeck/launcher/ui/TestTags.kt index 12c5ee390..4e69a2264 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/TestTags.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/TestTags.kt @@ -5,10 +5,5 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId -/** - * Compose test tags as Android resource ids below this node, so UI Automator (`uiautomator dump`, - * droiddeckctl ui-dump) sees stable ids such as `rail-steam` or `setting-logs` rather than only - * labels that change with the language. Tags are named in docs/agent-control.md. - */ @OptIn(ExperimentalComposeUiApi::class) fun Modifier.exposeTestTags(): Modifier = semantics { testTagsAsResourceId = true } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Theme.kt b/app/src/main/java/com/droiddeck/launcher/ui/Theme.kt index d8c44bae0..86980362f 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Theme.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Theme.kt @@ -66,10 +66,6 @@ object Themes { val LocalPalette = staticCompositionLocalOf { Themes.byId(Themes.GRAPHITE) } -/** - * Text and icons drawn on a [Palette.signal] fill: near-black where the blue is light enough that - * white would not read (Graphite's #1A9FFF gives white 2.8:1), white where it is deep. - */ val Palette.onSignal: Color get() = if (signal.luminance() > 0.18f) Color(0xFF03111F) else Color.White @Composable diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Throbber.kt b/app/src/main/java/com/droiddeck/launcher/ui/Throbber.kt index c08188bd8..9e77f1504 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Throbber.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Throbber.kt @@ -20,17 +20,12 @@ import kotlin.math.cos import kotlin.math.sin import kotlin.math.sqrt -/** - * The DroidDeck mark as the loading indicator. Every loop the D's square half bends into an arc, - * the ring they make turns once around the ball, and the half straightens back into the D. - */ @Composable internal fun LogoThrobber(modifier: Modifier = Modifier, running: Boolean = true) { val ink = MaterialTheme.colorScheme.onBackground val ball = LocalPalette.current.signal val loop = rememberInfiniteTransition(label = "throbber") .animateFloat(0f, 1f, infiniteRepeatable(tween(LOOP_MS, easing = LinearEasing)), label = "loop") - // Animations off in the system settings, or nothing running: the plain D. val still = !running || Motion.scale == 0f val left = remember { Path() } val right = remember { Path() } @@ -64,7 +59,6 @@ internal fun LogoThrobber(modifier: Modifier = Modifier, running: Boolean = true } } -/** The ball of a throbber laid out in [bounds]: its centre and its radius, at rest. */ internal fun throbberBall(bounds: androidx.compose.ui.geometry.Rect): Pair<Offset, Float> { val s = bounds.width / LogoShape.WIDTH return Offset(bounds.left + (LogoShape.CX - LogoShape.LEFT) * s, bounds.top + LogoShape.CY * s) to LogoShape.BALL * s @@ -74,12 +68,6 @@ private const val LOOP_MS = 1900 private val Settle = CubicBezierEasing(0.5f, 0f, 0.3f, 1f) private val Turn = CubicBezierEasing(0.65f, 0f, 0.35f, 1f) -/** - * The logo's geometry in its SVG's own units (viewBox -55.98 0 239.92 223.72): a D of a square - * left side and a round right side, cut by a ring around the ball and split top and bottom. - * The left half is sampled along rays from the centre so it can bend, point for point, into the - * mirror of the right half's arc. - */ internal object LogoShape { const val LEFT = -55.98f const val WIDTH = 239.92f @@ -97,7 +85,6 @@ internal object LogoShape { private fun at(a: Double, r: Float) = Pt((CX + r * cos(a)).toFloat(), (CY + r * sin(a)).toFloat()) private fun angle(x: Float, y: Float) = atan2((y - CY).toDouble(), (x - CX).toDouble()) - // Angles taken the long way round, through the left: from about -94 degrees down to -266. private fun leftward(a: Double) = if (a < 0) a else a - 2 * Math.PI private val rays: List<Double> = run { @@ -105,7 +92,6 @@ internal object LogoShape { val topCorner = angle(SQUARE_X, 0f) val bottomCorner = leftward(angle(SQUARE_X, HEIGHT)) val bottom = leftward(angle(SPLIT_LEFT, HEIGHT)) - // The corners are rays of their own, so the square half keeps sharp corners. fun span(a: Double, b: Double, n: Int) = List(n) { i -> a + (b - a) * i / (n - 1) } span(top, topCorner, 9).dropLast(1) + span(topCorner, bottomCorner, 25).dropLast(1) + span(bottomCorner, bottom, 9) } @@ -148,7 +134,6 @@ internal object LogoShape { List(40) { i -> at(innerFrom + (innerTo - innerFrom) * i / 39, INNER) } } - /** The left half, [bend] of the way from the square D side (0) to the arc (1). */ fun left(path: Path, bend: Float, px: (Float, Float) -> Offset) { path.reset() square.forEachIndexed { i, q -> diff --git a/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt index ff6613365..a44f5ca19 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt @@ -68,16 +68,13 @@ import com.droiddeck.launcher.update.AppUpdates.Follow import com.droiddeck.launcher.update.AppUpdates.Offer import com.droiddeck.launcher.update.AppUpdates.Release -/** The Updates page: the latest build of each channel, the one followed, and an update in progress. */ class UpdatesState( val catalog: AppUpdates.Catalog? = null, val follow: Follow = Follow(Channel.STABLE), val checking: Boolean = false, val error: String? = null, - /** "Downloading", "Installing", ... while an update is under way; [percent] is -1 when unknown. */ val stage: String? = null, val percent: Int = -1, - /** Explain "Install unknown apps" before sending the user to it. */ val askPermission: Boolean = false, ) { val hasUpdate: Boolean get() = AppUpdates.hasUpdate(catalog, follow) @@ -99,14 +96,11 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie val colors = MaterialTheme.colorScheme Column(modifier = modifier) { PageHeader(stringResource(R.string.upd_title)) { - // The build that is running, beside the title where it is always in view. Text( stringResource(R.string.upd_build, me.version, s.buildLabel), fontSize = 12.5.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f).padding(top = 6.dp), ) val checked = u.catalog?.checkedAt?.takeIf { it > 0 } - // The relative time comes capitalized to stand alone (Spanish "Hace 5 minutos"); here it - // ends a sentence. if (!LocalNarrowPane.current) Text( if (u.checking) stringResource(R.string.common_checking) else if (checked != null) stringResource(R.string.upd_checked, ago(checked).replaceFirstChar { it.lowercase() }) else stringResource(R.string.upd_not_checked), fontSize = 13.sp, color = colors.onSurfaceVariant, @@ -114,9 +108,6 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie ToolIcon(Icons.Outlined.Refresh, stringResource(R.string.store_check_updates), busy = u.checking, enabled = !u.checking && u.stage == null, onClick = ua.onCheck) } BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) { - // Landscape has the width for channels and status side by side; a narrow pane, or a small - // phone's (where each channel's name and date would be cut to a word), stacks them. The - // Thor's top screen leaves about 690dp here and keeps them side by side. val stacked = LocalNarrowPane.current || maxWidth < 600.dp Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(bottom = 8.dp)) { if (stacked) { @@ -125,7 +116,6 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie ChannelLabel() ChannelPicker(u, ua) } else { - // The label sits over both columns, so the status card lines up with the first channel. ChannelLabel() Row(horizontalArrangement = Arrangement.spacedBy(18.dp)) { Box(Modifier.weight(1f)) { ChannelPicker(u, ua) } @@ -148,7 +138,6 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie ) } -/** Where things stand, as a system updater says it: a status line, what you'd get, and one button. */ @Composable private fun StatusPanel(s: FrontEndState, u: UpdatesState, ua: UpdatesActions, me: AppUpdates.Installed) { val colors = MaterialTheme.colorScheme @@ -216,7 +205,6 @@ private fun StatusPanel(s: FrontEndState, u: UpdatesState, ua: UpdatesActions, m } } -/** The change and its notes, two lines each; More opens the rest when either is cut short. */ @Composable private fun ReleaseNotes(title: String, notes: String, key: String) { val colors = MaterialTheme.colorScheme @@ -246,7 +234,6 @@ private fun ReleaseNotes(title: String, notes: String, key: String) { } } -/** The headline for a build on offer: "DroidDeck 0.3.0", "New Preview build", "PR #93 test build". */ @Composable private fun newBuild(f: Follow, r: Release) = when (f.channel) { Channel.STABLE -> stringResource(R.string.upd_new_stable, r.version ?: r.tag) @@ -254,14 +241,12 @@ private fun newBuild(f: Follow, r: Release) = when (f.channel) { Channel.TEST -> stringResource(R.string.upd_new_test, r.pr) } -/** A PR title as a sentence: "fix(steam): keep the client alive" -> "Keep the client alive". */ private fun changeTitle(t: String): String = t.replace(Regex("""^[a-z]+(\([^)]*\))?!?:\s*"""), "").replaceFirstChar { it.uppercase() } @Composable private fun megabytes(bytes: Long) = stringResource(R.string.upd_mb, ((bytes + 524_288) / 1_048_576).toInt()) -/** The three channels as cards to pick from; Test builds opens its list of PRs under it. */ @Composable private fun ChannelLabel() { Text( @@ -297,7 +282,6 @@ private fun ChannelPicker(u: UpdatesState, ua: UpdatesActions) { } } -/** Read-only Preview history below the status card action; only the latest build is installable. */ @Composable private fun PreviewHistory(catalog: AppUpdates.Catalog?, me: AppUpdates.Installed) { val history = catalog?.let { AppUpdates.previewHistory(it, me) } ?: return @@ -363,8 +347,6 @@ private fun ChannelCard( val pal = LocalPalette.current val src = remember { MutableInteractionSource() } val hot = rememberHot(src) - // Focus shows at once, as on the Components page: no ripple, no fade. Selection is the radio - // dot, never a ring of its own: the ring is only ever focus. val fill = if (hot) pal.signal.copy(alpha = 0.14f) else Color.Transparent Row( verticalAlignment = Alignment.CenterVertically, @@ -391,7 +373,6 @@ private fun ChannelCard( } } -/** The one mark of a chosen option: a filled dot in a ring, beside it. */ @Composable private fun RadioDot(selected: Boolean, size: androidx.compose.ui.unit.Dp = 22.dp) { val pal = LocalPalette.current @@ -400,7 +381,6 @@ private fun RadioDot(selected: Boolean, size: androidx.compose.ui.unit.Dp = 22.d } } -/** One test build under Test builds: its PR, what it fixes, and how fresh it is. */ @Composable private fun TestRow(t: Release, selected: Boolean, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/WinComponentsEditor.kt b/app/src/main/java/com/droiddeck/launcher/ui/WinComponentsEditor.kt index bfd227952..155c6e9b2 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/WinComponentsEditor.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/WinComponentsEditor.kt @@ -52,15 +52,9 @@ import androidx.compose.runtime.withFrameNanos import kotlinx.coroutines.withContext import java.io.File -// Windows components for one added game, Bannerlator's list: what the game's folder suggests first, -// then everything that installs here, then the ones that need a Windows installer (not yet). Each -// one is a switch, downloaded the first time it is turned on for any game; the launch puts the -// picked ones into the game's prefix (droiddeck-wincomponents). -/** Bannerlator's detector names installers; here the DLL-copy twin is the one that installs. */ private fun installable(name: String, all: Map<String, WinComponents.Component>): String = WinComponents.installable(name, all) -/** A store as a list's owner names it in a reason line. */ @Composable private fun originName(origin: String): String = stringResource( when (origin) { @@ -71,7 +65,6 @@ private fun originName(origin: String): String = stringResource( }, ) -/** Why a component was turned on by itself, from [WinComponents.AutoReason] (args: what the list called it, then the origin's own). */ @Composable private fun autoReason(r: WinComponents.AutoReason): String = when (r.kind) { WinCompSources.Origin.STEAM.name -> stringResource(R.string.wincomp_auto_steam, r.args.getOrElse(1) { "" }, r.args.getOrElse(2) { "" }, r.args.getOrElse(3) { "" }) @@ -81,7 +74,6 @@ private fun autoReason(r: WinComponents.AutoReason): String = when (r.kind) { else -> "" } -/** The reason line of a finding that is only recommended. */ @Composable private fun findingReason(f: WinCompSources.Finding): String = when (f.origin) { WinCompSources.Origin.FOLDER -> stringResource( @@ -96,9 +88,7 @@ private fun findingReason(f: WinCompSources.Finding): String = when (f.origin) { @Composable internal fun WinComponentsDialog( appKey: String, gameName: String, gameDir: File?, byPad: Boolean, - /** compatdata/<id>: what the prefix already has. [steamAppId]: a Steam title, whose appmanifest lists Steam's own redists. */ compat: File? = null, steamAppId: Int? = null, - /** The game as the Games tab has it: its source decides whose list is trusted ([WinCompSources]). */ game: com.droiddeck.launcher.frontend.Library.SteamGame? = null, onClose: () -> Unit, ) { @@ -106,33 +96,22 @@ internal fun WinComponentsDialog( val coroutine = rememberCoroutineScope() val shown = rememberShown(onClose) val close = { shown.targetState = false } - // null while the catalog loads; empty when it could not be had. var catalog by remember { mutableStateOf<Map<String, WinComponents.Component>?>(null) } var offline by remember { mutableStateOf(false) } var findings by remember { mutableStateOf(emptyList<WinCompSources.Finding>()) } - // Whether the game's list has been worked out at least once (kept from an earlier time counts). var findingsKnown by remember { mutableStateOf(game == null) } - // "Loading the components" only when the catalog takes a while. var slowCatalog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { kotlinx.coroutines.delay(300); slowCatalog = true } var installed by remember { mutableStateOf(emptySet<String>()) } - // Detector names (oalinst, physx...) already in the prefix from elsewhere. var present by remember { mutableStateOf(emptySet<String>()) } var selection by remember { mutableStateOf(WinComponents.selection(context, appKey)) } val picks = selection.effective - // The row being worked on: its id, the latest progress (a dependency's while that installs - // first), and whether it is turning off rather than on. The row greys out and shows the bar - // the moment the switch is pressed, and only that row; the rest of the page stays as it is. var busyId by remember { mutableStateOf<String?>(null) } var progress by remember { mutableStateOf<WinComponents.Progress?>(null) } var reverting by remember { mutableStateOf(false) } var error by remember { mutableStateOf<String?>(null) } - // The component the error is about, so it shows on its own row and not only at the bottom. var failed by remember { mutableStateOf<String?>(null) } var showWaiting by remember { mutableStateOf(false) } - // Done holds focus while the list loads, then the top switch takes it - opened by touch or by - // pad, so the d-pad always has somewhere to start; LB and RB jump between the sections, since - // the full list runs past sixty switches. val recFocus = remember { FocusRequester() } val recFocusAfterAuto = remember { FocusRequester() } val activeFocus = remember { FocusRequester() } @@ -145,10 +124,7 @@ internal fun WinComponentsDialog( LaunchedEffect(Unit) { val (entries, found) = withContext(Dispatchers.IO) { val list = WinComponents.fetch(context) - // The source's list first (and what it turns on by itself, already installed), then the folder scan. if (game != null) { - // The list kept from last time, at once; it is worked out again (and its components - // queued) on the background worker, which this page then follows. com.droiddeck.launcher.frontend.AutoComponents.queueGame(context, game, asked = true) val kept = com.droiddeck.launcher.frontend.AutoComponents.cachedFindings(context, appKey) if (kept != null) findingsKnown = true @@ -166,11 +142,7 @@ internal fun WinComponentsDialog( present = withContext(Dispatchers.IO) { PrefixInstalledDetector.detect(compat) } installed = withContext(Dispatchers.IO) { WinComponents.installedIds(context).toSet() } } - /** The user's own switch for [id]: kept apart from what was turned on by itself, and over it. */ - // Automatic picks still downloading show on their rows; when one lands the page reads the picks again. val autoDownloading by com.droiddeck.launcher.frontend.AutoComponents.downloading.collectAsState() - // Without a network a component not downloaded yet cannot be turned on; the rows say so and - // come back the moment the network does. val online = rememberOnline() val autoChanges by com.droiddeck.launcher.frontend.AutoComponents.changes.collectAsState() val pendingGames by com.droiddeck.launcher.frontend.AutoComponents.pendingGames.collectAsState() @@ -186,14 +158,10 @@ internal fun WinComponentsDialog( coroutine.launch(Dispatchers.IO) { WinComponents.setUser(context, appKey, id, on) } } fun toggle(id: String, on: Boolean) { - // Switches stay enabled during a download - a disabled one drops the pad's focus - so a press - // then is ignored here; the busy row is greyed instead. if (busyId != null) return error = null failed = null if (!on) { - // Turning off is a saved pick, quick, but the row greys out until it is written so the - // press is seen to land; the files leave the prefix at the game's next launch. busyId = id; reverting = true; progress = null selection = WinComponents.Selection(selection.auto, selection.user + (id to false)) coroutine.launch { @@ -219,21 +187,12 @@ internal fun WinComponentsDialog( } val all = catalog - // The first data is in: rows that show up from now on ease in. var arrived by remember { mutableStateOf(false) } LaunchedEffect(all != null && findingsKnown) { if (all != null && findingsKnown) { withFrameNanos { }; arrived = true } } val here = if (all == null) emptySet() else present.map { installable(it, all) }.toSet() fun supportOf(id: String): Support = all?.get(id)?.let { WinComponents.support(it, all) } ?: if (id in installed) Support.READY else Support.UNSUPPORTED - // Turned on by itself for this game, each with its reason; then what is only recommended (the - // folder scan, a name match, an auto pick that cannot install here); then what the lists name - // that is no component here, by its own name. - // Still being worked out on the background worker: the page says so instead of "nothing found". - // Only before the game's list has ever been worked out: a list kept from before shows at once and - // is checked again quietly, its changes easing in. val checking = game != null && !findingsKnown - // Automatic picks on their way (queued or downloading) sit in their section already, with - // their progress, and settle there when they land. val turningOn = if (all == null) emptyMap() else findings.filter { it.auto && it.component != null } .associateBy { installable(it.component!!, all) } .filterKeys { it !in selection.auto && selection.user[it] != false && (it in pendingComponents || it in autoDownloading) } @@ -244,10 +203,6 @@ internal fun WinComponentsDialog( val suggestions = findings.filter { it.component == null }.distinctBy { it.origin to it.original } val ready = all.orEmpty().values.filter { WinComponents.support(it, all.orEmpty()) == Support.READY }.map { it.name } val extra = (installed + picks).filter { all?.containsKey(it) != true } - // What is turned on for this game gets its own section under the recommendations, which stay - // where they are; the rest of the list is everything else, so a component turned off goes back - // to its place in the alphabet. A component is listed once: the recommendations are left out - // of the full list too. val recSet = recIds.map { installable(it.component!!, all.orEmpty()) }.toSet() + autoIds val active = picks.filter { it !in recSet && (all?.containsKey(it) == true || it in installed) }.distinct() .sortedBy { WinComponentNames.of(it).lowercase() } @@ -263,12 +218,10 @@ internal fun WinComponentsDialog( runCatching { sections[section].requestFocus() } } LaunchedEffect(all != null) { - // The dialog's window starts in touch mode, where a switch cannot hold focus at all. inputMode.requestInputMode(InputMode.Keyboard) section = 0 val target = if (all == null) doneFocus else sections.first() switchFocused = false - // It animates in and the list composes over a few frames: ask until the switch has it. repeat(60) { withFrameNanos { } runCatching { target.requestFocus() } @@ -296,7 +249,6 @@ internal fun WinComponentsDialog( support == Support.UNSUPPORTED -> stringResource(R.string.wincomp_unsupported) else -> null } - // The catalog key stays in the detail: it is what a log or a bug report names. val detail = listOfNotNull(reason, c?.description?.takeIf { it.isNotEmpty() }, status, id).joinToString(" · ") val offlineHere = !online && id !in installed val usable = support == Support.READY && !offlineHere @@ -323,13 +275,11 @@ internal fun WinComponentsDialog( .onFocusChanged { if (it.isFocused) switchFocused = true }, ) { on -> toggle(id, on) } } - /** The section's requester goes to its first switch that can take focus. */ fun firstUsable(ids: List<String>, focus: FocusRequester): (String) -> FocusRequester? { val first = ids.firstOrNull { supportOf(it) == Support.READY } return { id -> focus.takeIf { id == first } } } - // The lists only once the catalog is in, faded in once: no half list counted before it. androidx.compose.animation.AnimatedVisibility( visible = all != null, enter = androidx.compose.animation.fadeIn(Motion.tw(220)), @@ -429,7 +379,6 @@ internal fun WinComponentsDialog( } } -/** Once the page has its first data ([arrived]), a row that shows up eases in; the first ones are simply there. */ @Composable private fun Arrive(arrived: Boolean, content: @Composable () -> Unit) { val state = remember { androidx.compose.animation.core.MutableTransitionState(!arrived) }.apply { targetState = true } @@ -442,7 +391,6 @@ private fun Arrive(arrived: Boolean, content: @Composable () -> Unit) { ) { content() } } -/** Whether the default network is there and validated, followed live. */ @Composable private fun rememberOnline(): Boolean { val context = LocalContext.current @@ -474,12 +422,6 @@ private fun Panel(content: @Composable androidx.compose.foundation.layout.Column private fun Divider() = Box(Modifier.fillMaxWidth().padding(horizontal = 14.dp).heightIn(min = 1.dp, max = 1.dp).background(LocalPalette.current.line)) -/** - * One component: name, detail and its switch; while [busy] the row is greyed and a thin bar runs - * across it with [caption] above (the download, then the install, each 0..100 as [percent], or - * indeterminate at -1). The switch stays enabled while busy so a pad's focus does not drop; the - * press is ignored by the caller instead. - */ @Composable private fun ComponentRow( name: String, detail: String, checked: Boolean, enabled: Boolean, dim: Boolean, busy: Boolean = false, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt index f934ac8ba..8117c3ee5 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt @@ -358,7 +358,6 @@ internal fun AdbTextField( onFocusChange: (Boolean) -> Unit = {}, onNext: (() -> Unit)? = null, onDone: (() -> Unit)? = null, - /** Takes focus (and the keyboard) once, as soon as it is shown: a field that opened on request. */ focusOnShow: Boolean = false, ) { val context = LocalContext.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Wordmark.kt b/app/src/main/java/com/droiddeck/launcher/ui/Wordmark.kt index 48af1ea26..c0aefad25 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Wordmark.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Wordmark.kt @@ -17,10 +17,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import com.droiddeck.launcher.R -/** - * The DroidDeck wordmark from the README banner (artwork/droiddeck-banner-*.svg): the mark - * standing in as the D, then "roidDeck". Letters and D in the theme's ink, the ball in its blue. - */ @Composable internal fun Wordmark(height: Dp, modifier: Modifier = Modifier) { val ink = MaterialTheme.colorScheme.onBackground @@ -34,7 +30,6 @@ internal fun Wordmark(height: Dp, modifier: Modifier = Modifier) { scale(k, k, Offset.Zero) { translate(-BOX_X, -BOX_Y) { drawPath(letters, ink) - // The mark's placement in the banner: translate(239.135 123.36) scale(0.527445) translate(31.98 0). translate(239.135f, 123.36f) { scale(MARK_SCALE, MARK_SCALE, Offset.Zero) { translate(31.98f, 0f) { @@ -49,7 +44,6 @@ internal fun Wordmark(height: Dp, modifier: Modifier = Modifier) { } } -// The banner's own units: the wordmark spans x 239.135..1040.7 and y 120.02..243.2. private const val BOX_X = 239.135f private const val BOX_Y = 120.02f private const val BOX_W = 1040.7f - 239.135f diff --git a/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt b/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt index fc6a4f02c..22f0cc22f 100644 --- a/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt +++ b/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt @@ -12,13 +12,6 @@ import java.net.HttpURLConnection import java.net.URL import java.security.MessageDigest -/** - * DroidDeck's published builds. CI turns GitHub Releases into one static catalog in DroidDeck-CI; - * the app consumes that contract instead of inferring channels from tags and release-note prose. - * - * [Channel.NIGHTLY] is intentionally kept as the internal enum/pref value for upgrades from older - * DroidDeck versions. The channel is presented to users as Preview. - */ object AppUpdates { private const val REPO = "Droid-Deck/DroidDeck" private const val CI_REPO = "Droid-Deck/DroidDeck-CI" @@ -164,7 +157,6 @@ object AppUpdates { r.commit.startsWith(me.commit, ignoreCase = true) } - /** Recent Preview changes relative to a CI-installed commit, when its position is known. */ fun previewHistory(catalog: Catalog, me: Installed = installed()): PreviewHistory? { if (!me.ciBuild || catalog.recentPreviews.isEmpty()) return null val stable = catalog.stable @@ -195,14 +187,12 @@ object AppUpdates { (installed.length < 40 && published.startsWith(installed, ignoreCase = true)) } - /** Why Android cannot install this published build over [me], before downloading anything. */ fun installBlock(r: Release, me: Installed = installed()): InstallBlock? = when { r.apk == null -> InstallBlock(R.string.upd_block_no_apk) r.apk.versionCode < me.versionCode -> InstallBlock(R.string.upd_block_version_code, r.apk.versionCode, me.versionCode) else -> null } - /** Why Android would refuse a build, as a line for the user. */ class InstallBlock(@androidx.annotation.StringRes val text: Int, private vararg val args: Any) { fun message(context: Context): String = context.getString(text, *args) } @@ -222,7 +212,6 @@ object AppUpdates { } fun setFollow(context: Context, follow: Follow) { - // Keep "nightly" on disk so an older DroidDeck still understands the preference after rollback. val value = if (follow.channel == Channel.TEST) "test:${follow.pr}" else follow.channel.name.lowercase() prefs(context).edit().putString(KEY_CHANNEL, value).apply() @@ -245,11 +234,9 @@ object AppUpdates { return runCatching { readCachedCatalog(JSONObject(raw)) }.getOrNull() } - /** One static request, rather than several unauthenticated GitHub API requests. */ fun refresh(context: Context): Catalog { val checkedAt = System.currentTimeMillis() val published = JSONObject(get(context, "$CATALOG_URL?checked=$checkedAt")) - // Its checks say why in English; the user gets that inside a sentence in the app's language. val catalog = try { readPublishedCatalog(published, context.packageName, checkedAt) } catch (e: IOException) { diff --git a/app/src/main/java/com/droiddeck/launcher/update/SelfInstaller.kt b/app/src/main/java/com/droiddeck/launcher/update/SelfInstaller.kt index 4700302e2..959dcde2a 100644 --- a/app/src/main/java/com/droiddeck/launcher/update/SelfInstaller.kt +++ b/app/src/main/java/com/droiddeck/launcher/update/SelfInstaller.kt @@ -19,23 +19,15 @@ import java.net.HttpURLConnection import java.net.URL import java.security.MessageDigest -/** - * Installs one of DroidDeck's own builds over itself: download into the cache, check the file - * against GitHub's sha256, then hand it to PackageInstaller. Android shows its own "Update this - * app?" prompt, and the app is restarted on the new build. - */ object SelfInstaller { - /** The broadcast PackageInstaller answers on; the launcher registers for it while it is open. */ const val ACTION_STATUS = "com.droiddeck.launcher.update.INSTALL_STATUS" - /** Android lets an app install packages only once the user allows it in "Install unknown apps". */ fun canInstall(context: Context): Boolean = Build.VERSION.SDK_INT < 26 || context.packageManager.canRequestPackageInstalls() fun permissionIntent(context: Context): Intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:${context.packageName}")) - /** Downloads [apk] into the cache and checks it; a download left from before is replaced. */ fun download(context: Context, apk: AppUpdates.Apk, progress: (Int) -> Unit): File { val dir = File(context.cacheDir, "updates") FileUtils.delete(dir) @@ -80,7 +72,6 @@ object SelfInstaller { } } - /** Inspect the actual downloaded APK before giving it to Android's installer. */ private fun validateDownloaded(context: Context, file: File, apk: AppUpdates.Apk) { if (apk.packageName != context.packageName) { throw IOException(context.getString(R.string.selfinst_wrong_package, apk.packageName, context.packageName)) @@ -108,8 +99,6 @@ object SelfInstaller { throw IOException(context.getString(R.string.selfinst_downgrade, code, BuildConfig.VERSION_CODE)) } - // Before Android 9 this lineage intentionally presents the public legacy test key. Android - // 9+ understands the v3 hand-over and must see DroidDeck's private release key as current. if (Build.VERSION.SDK_INT >= 28) { val signing = info.signingInfo ?: throw IOException(context.getString(R.string.selfinst_no_signing)) val digests = signing.apkContentsSigners.orEmpty().map { cert -> @@ -122,13 +111,11 @@ object SelfInstaller { } } - /** Writes [apk] into an install session and commits it; the answer comes as an [ACTION_STATUS] broadcast. */ fun install(context: Context, apk: File) { val installer = context.packageManager.packageInstaller val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply { setAppPackageName(context.packageName) setSize(apk.length()) - // Skips Android's prompt where it allows that (the app installed its current build itself). if (Build.VERSION.SDK_INT >= 31) setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED) } val id = installer.createSession(params) @@ -145,7 +132,6 @@ object SelfInstaller { } } - /** What went wrong, in words for the user, from PackageInstaller's status and message. */ fun describeFailure(context: Context, status: Int, message: String?): String = when { message?.contains("VERSION_DOWNGRADE") == true -> context.getString(R.string.upd_fail_downgrade) status == PackageInstaller.STATUS_FAILURE_CONFLICT || message?.contains("UPDATE_INCOMPATIBLE") == true -> diff --git a/app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt b/app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt index 797ae8469..9df4d4dfd 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt @@ -4,12 +4,6 @@ import android.view.Choreographer import android.view.Surface import com.droiddeck.launcher.session.SessionState -/** - * The compositor is a process-wide thing, not an activity's: it is started once, keeps running - * while the session does, and merely swaps the Surface it presents into as the activity comes and - * goes. An activity that started it again on re-creation would be starting a second compositor on - * a socket the first one owns. - */ object CompositorHost { @Volatile private var started = false @@ -22,7 +16,6 @@ object CompositorHost { } } - /** True when this call was the one that started the compositor. */ @Synchronized fun startOrAttach( surface: Surface, @@ -36,10 +29,6 @@ object CompositorHost { fpsLimit: Int, ): Boolean { pace(refreshHz, fpsLimit) - // The compositor outlives sessions, and the next one may be on the other screen of a dual-screen - // device: gamescope reads the size from the toplevel configure and the wl_output mode when it - // connects, and kept the first session's (the Thor's 1920x1080 top screen letterboxed the bottom - // one's session to 16:9). A running session keeps the size it was started at. if (!SessionState.running) WaylandCompositor.nativeSetOutputSize(outputWidth, outputHeight) if (started) { attached = surface @@ -57,34 +46,14 @@ object CompositorHost { return true } - /** - * The session's frame cap, on every attach: the compositor outlives sessions, and the next one - * may have a different cap. The release pacer hands a game one buffer back per capped frame - * (per refresh without a cap), and the zero-copy layer votes for the same cadence. Left at 0 - * the vote is inferred from the rate already being achieved, and on a phone whose vendor picks - * clocks from that a session that has been slowed reads as one that wants to be. (WinNative, - * WaylandSession.) - */ private fun pace(refreshHz: Float, fpsLimit: Int) { WaylandCompositor.nativeSetFpsLimit(fpsLimit) WaylandCompositor.nativeSetLayerFrameRate(if (fpsLimit > 0) fpsLimit.toFloat() else refreshHz) } - /** - * The Surface changed size under the compositor (a foldable opening or closing). Android does - * not always recreate the Surface for that, and a swapchain built for the old size keeps - * presenting: the system then scales those buffers onto the new window, which on a Fold showed - * a 16:9 picture stretched to the square panel. Rebinding the window rebuilds the swapchain at - * the new size, and the scale mode letterboxes as it should. - */ @Synchronized fun resize(surface: Surface, rearm: () -> Unit) { if (!started) return - // Frame generation does not live through a resize: its ring, its effects chain and its - // multi-present pacing were all built against the old extent, and on a Fold the picture - // came back stretched with an engine on and right with it off. Disarmed, the window is - // rebound and the swapchain rebuilt; the caller re-arms once that has settled, and every - // engine-side object is made again for the new size. WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0) WaylandCompositor.nativeLog("screen", "surface resized: rebinding the window, frame generation re-armed after") WaylandCompositor.nativeSetSurface(null) @@ -93,12 +62,6 @@ object CompositorHost { android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(rearm, 600) } - /** - * The window the guest presents through has changed (the Steam menu opening or closing over a - * fullscreen game swaps gamescope between forwarding the game's own buffers and its composited - * output). The engine's ring and history straddle that switch and come back wrong-sized - a - * game shown as a strip in a corner - so it is disarmed and re-armed against the new window. - */ fun rearmFrameGen(rearm: () -> Unit) { if (!started) return WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0) @@ -106,19 +69,8 @@ object CompositorHost { android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(rearm, 600) } - /** - * The activity is going away. The compositor keeps running with nothing to present into - - * the guest carries on, and its next frames land on the Surface the next activity brings. - */ - /** The Surface the compositor presents on; only its own owner may take it away. */ private var attached: Surface? = null - /** - * Only for the Surface that is attached. When one session hands over to the next (the - * desktop's Steam launchers), the new activity attaches its Surface before the old one is - * destroyed - and the old one's surfaceDestroyed then arrived here and took the new Surface - * away: gamescope's frames came in by the thousand and nothing reached the screen. - */ @Synchronized fun detach(surface: Surface?) { if (!started) return @@ -128,7 +80,6 @@ object CompositorHost { WaylandCompositor.nativeSetSurface(null) } - /** Frames are only worth drawing while a Surface is attached. */ private fun resumeVsync() { if (vsyncRunning) return vsyncRunning = true @@ -139,10 +90,6 @@ object CompositorHost { vsyncRunning = false } - /** - * A new session behind a compositor that has already presented: the first-frame notice is - * one-shot in native code, so it is re-armed here or the loading panel would never leave. - */ fun newSession() { if (started) WaylandCompositor.nativeResetFirstFrame() } diff --git a/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt b/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt index 714746d20..605a1579d 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt +++ b/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt @@ -6,12 +6,6 @@ import android.view.Display import android.view.WindowManager import com.droiddeck.launcher.R -/** - * What the panel says about HDR, read from android.view.Display: the gate the compositor's HDR10 - * output sits behind (droiddeck_color.h). A property of the display, not the device - and the - * settings screen greys the HDR switch out with the reason when it is not there, rather than - * offering a switch that a session would then silently ignore. - */ class HdrProbe( val hdr10: Boolean, val formats: String, @@ -22,7 +16,6 @@ class HdrProbe( val ratio: Float, val displayId: Int, val name: String, - /** Why HDR cannot be offered, or null when it can. */ val reason: String?, ) diff --git a/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java b/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java index 14016b29f..97cfcf3c4 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java +++ b/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java @@ -2,15 +2,6 @@ package com.droiddeck.launcher.wayland; import android.view.Surface; -/** - * Embedded Wayland compositor (experimental parallel display runtime). - * - * Brings up a libwayland-server compositor in-process so games launched through - * Wine's winewayland.drv can present into it (companion to the winewayland Proton - * build). Committed frames (dmabufs from winewayland's Vulkan WSI) are composited - * onto the given Surface via the native Vulkan present backend. Not on the default - * X11 path. - */ public final class WaylandCompositor { static { System.loadLibrary("droiddeckwayland"); @@ -20,35 +11,21 @@ public final class WaylandCompositor { private static volatile Runnable firstFrameListener; - /** Register a callback fired once, when the compositor presents the first client - * frame to the output Surface. Used to dismiss the launch overlay in wayland mode - * (there is no XServer window-content hook). Runs on the compositor thread - the - * listener must marshal to the UI thread itself. */ public static void setFirstFrameListener(Runnable r) { firstFrameListener = r; } public static synchronized void clearFirstFrameListener(Runnable r) { if (firstFrameListener == r) firstFrameListener = null; } - /** Invoked from native (droiddeck_on_first_frame) on the first present. */ @SuppressWarnings("unused") static void onFirstFramePresented() { Runnable r = firstFrameListener; if (r != null) r.run(); } - /** The in-game performance HUD's feed in wayland mode (X11 binds the HUD to the window - * carrying _MESA_DRV and counts X presents; there is no X server here). */ public interface GameListener { - /** A window started presenting GPU frames ({@code window} describes it; {@code gpuName} is the - * compositor's GPU), or {@code window == null} when that window closed. Compositor thread. */ void onGameSurface(String window, String gpuName); - /** One GPU frame from that window. Compositor thread - keep it cheap. */ void onGameFrame(); - /** The program behind that window, right after {@link #onGameSurface}: its Linux pid (the - * Wayland client's credentials) and executable name ({@code ""} when unknown). The app arms its - * launch-time CPU affinity on it - on X11 that comes from window events, which a Wayland session - * has none of. Compositor thread - marshal to the UI thread. */ default void onGameProgram(int pid, String program) {} } @@ -60,7 +37,6 @@ public final class WaylandCompositor { if (gameListener == l) gameListener = null; } - /** Invoked from native (droiddeck_on_game_surface). */ @SuppressWarnings("unused") static void onGameSurface(String window, String gpuName) { GameListener l = gameListener; @@ -70,17 +46,12 @@ public final class WaylandCompositor { private static volatile long lastFrameNanos; private static volatile long frameIntervalNanos; - /** - * How long frames are taking to arrive right now, in ms, or -1 before the first: the smoothed - * interval, or the time since the last frame when that is longer (a session that has stalled). - */ public static long recentFrameIntervalMs() { long last = lastFrameNanos; if (last == 0) return -1; return Math.max(frameIntervalNanos, System.nanoTime() - last) / 1_000_000; } - /** Invoked from native (droiddeck_on_game_frame) for every frame of the HUD's window. */ @SuppressWarnings("unused") static void onGameFrame() { long now = System.nanoTime(); @@ -95,19 +66,13 @@ public final class WaylandCompositor { if (l != null) l.onGameFrame(); } - /** Invoked from native (droiddeck_on_game_program) once per game window that starts presenting. */ @SuppressWarnings("unused") static void onGameProgram(int pid, String program) { GameListener l = gameListener; if (l != null) l.onGameProgram(pid, program != null ? program : ""); } - /** Pointer lock (zwp_pointer_constraints_v1) state, for the app's input path. */ public interface PointerLockListener { - /** A program locked the pointer ({@code locked}): the app must feed the compositor deltas - * (scene input type 6) instead of absolute positions. When the lock ends, {@code x,y} is - * where the pointer now is (scene = virtual-desktop coordinates) for the app to re-sync - * its own pointer to. Compositor thread. */ void onPointerLock(boolean locked, int x, int y); } @@ -115,21 +80,17 @@ public final class WaylandCompositor { public static void setPointerLockListener(PointerLockListener l) { pointerLockListener = l; } - /** Unhooks {@code l} only if it is still the listener, so a finishing activity never unhooks its successor's. */ public static void clearPointerLockListener(PointerLockListener l) { if (pointerLockListener == l) pointerLockListener = null; } - /** Invoked from native (droiddeck_on_pointer_lock). */ @SuppressWarnings("unused") static void onPointerLock(boolean locked, int x, int y) { PointerLockListener l = pointerLockListener; if (l != null) l.onPointerLock(locked, x, y); } - /** Clipboard text a program in the guest copied (wl_data_device / zwlr_data_control). */ public interface ClipboardListener { - /** {@code text} is what the guest put on its clipboard. Compositor thread. */ void onGuestClipboardText(String text); } @@ -139,23 +100,17 @@ public final class WaylandCompositor { public static boolean isClipboardListener(ClipboardListener l) { return clipboardListener == l; } - /** A replaced activity must not unhook the clipboard listener of its successor. */ public static void clearClipboardListener(ClipboardListener l) { if (clipboardListener == l) clipboardListener = null; } - /** Invoked from native (droiddeck_on_clipboard_text) with UTF-8 bytes. */ @SuppressWarnings("unused") static void onClipboardText(byte[] utf8) { ClipboardListener l = clipboardListener; if (l != null && utf8 != null) l.onGuestClipboardText(new String(utf8, java.nio.charset.StandardCharsets.UTF_8)); } - /** Text input (zwp_text_input_v3) state: a program accepts IME text, or stopped. */ public interface TextInputListener { - /** {@code enabled}: {@code program} (its exe name) accepts IME text; {@code x,y,w,h} is its caret - * rectangle in scene (virtual desktop) pixels, all 0 until the program positions one. Compositor - * thread. */ void onTextInput(boolean enabled, String program, int x, int y, int w, int h); } @@ -163,7 +118,6 @@ public final class WaylandCompositor { public static void setTextInputListener(TextInputListener l) { textInputListener = l; } - /** Invoked from native (droiddeck_on_text_input). */ @SuppressWarnings("unused") static void onTextInput(boolean enabled, String program, int x, int y, int w, int h) { TextInputListener l = textInputListener; @@ -174,16 +128,12 @@ public final class WaylandCompositor { return s == null ? new byte[0] : s.getBytes(java.nio.charset.StandardCharsets.UTF_8); } - /** Android's clipboard text becomes the guest's selection ({@code null}/empty clears it). Any thread. */ public static void setClipboardText(String text) { nativeSetClipboardText(utf8(text)); } - /** Commit soft-keyboard text to the program accepting text input. Any thread. */ public static void textInputCommit(String text) { if (text != null && !text.isEmpty()) nativeTextInputCommit(utf8(text)); } - /** Composing (pre-edit) text with the caret at character index {@code cursor} (-1 = end); empty clears. */ public static void textInputPreedit(String text, int cursor) { nativeTextInputPreedit(utf8(text), cursor, cursor); } - /** The IME deleted {@code before} characters before and {@code after} after the caret. */ public static void textInputDelete(int before, int after) { nativeTextInputDelete(before, after); } private static native void nativeSetClipboardText(byte[] utf8); @@ -191,104 +141,46 @@ public final class WaylandCompositor { private static native void nativeTextInputPreedit(byte[] utf8, int cursorBegin, int cursorEnd); private static native void nativeTextInputDelete(int before, int after); - /** Start the compositor headless (no output window) - bring-up tests only. */ public static native void nativeStart(String xdgRuntimeDir); - /** Start the compositor rendering to {@code surface}. XDG_RUNTIME_DIR = an - * app-writable dir for the wayland socket (e.g. context.getFilesDir()). - * driverPath/libraryName/nativeLibDir select the Turnip driver via adrenotools - * (all null -> system libvulkan, which can't do dmabuf import). */ public static native void nativeStartWithSurface(Surface surface, String xdgRuntimeDir, String driverPath, String libraryName, String nativeLibDir); - /** Replace/clear the output window when the SurfaceView is (re)created/destroyed. */ public static native void nativeSetSurface(Surface surface); - /** Inject a pointer event into the compositor (from the SurfaceView touch listener). - * action: 0=down, 1=move, 2=up. x/y in compositor output space (0..1919, 0..1079). */ public static native void nativeSendPointer(int action, int x, int y); public static native void nativeSendTouch(int action, int pointerId, int x, int y); - /** - * The client's pointer image, straight from wl_pointer.set_cursor (labwc's resize arrows, a text - * field's I-beam). out = [serial, hidden, w, h, hotspotX, hotspotY, ARGB8888 pixels...]; returns - * the number of ints written, or 0 if {@code out} is too small. Serial 0 = the client has not - * set one yet; [1] = 1 means it asked for NO pointer. - */ public static native int nativeCursorSnapshot(int[] out); - /** The serial {@link #nativeCursorSnapshot} would report, without copying the image. */ public static native int nativeCursorSerial(); - /** Enough for the header plus a 256x256 cursor, the largest the compositor snapshots. */ public static final int CURSOR_BUF_INTS = 6 + 256 * 256; - /** Inject a key event. evdev = Linux input keycode (KEY_A=30…); state: 1=down, 0=up. */ public static native void nativeSendKey(int evdev, int state); - /** One screen refresh (Choreographer frame callback). The compositor draws the newest state - * once per tick, so games run unthrottled and the screen shows their latest frame. */ public static native void nativeVsync(long frameTimeNanos); - /** The compositor thread's Linux tid, or 0 before it has started. */ public static native int nativeCompositorTid(); - /** Shortcut launches: don't draw explorer's windows (desktop, taskbar, Start menu), matching the - * X11 renderer's unviewable "explorer.exe". Set before the compositor starts. */ public static native void nativeSetHideShell(boolean hide); - /** The panel's refresh rate in Hz, advertised on the Wayland output so Wine's display modes - * carry the real rate (games that insist on their saved 144 Hz mode find it, as on X11). - * Set before the compositor starts. */ public static native void nativeSetOutputRefreshRate(float hz); - /** Show a single fullscreen window on its own Android layer (SurfaceControl) instead of blitting - * it into the compositor's swapchain. DROIDDECK_WAYLAND_ZERO_COPY=1 in the container's environment - * variables is the launch default; see waylandcomp/ZERO_COPY_SPIKE.md. - * - * Live and thread-safe: before the compositor starts this is the initial state, and afterwards - * the call is marshalled onto the compositor thread, which flips the state, tells every bound - * game over banner_ahb_v1.mode to rebuild its swapchain for it, and redraws. The switch is one - * frame's worth of latency; the picture never goes black, because the old swapchain's buffers - * keep being shown until the game has replaced them. */ public static native void nativeSetZeroCopy(boolean on); - /** Zero-copy frames the compositor presented in its LAST completed 10 s stats window (the - * "| N zero-copy frames" figure of its session-log stats line); 0 while zero-copy is off or - * before the first window closes. Read-only, any thread - the in-game drawer polls it. */ public static native int nativeZeroCopyFrames(); - /** Milliseconds since the compositor last put a game frame on the display layer without a copy; - * -1 if it never has this session. Unlike nativeZeroCopyFrames this updates on every such frame - * rather than once per 10 s window, so the drawer can tell "switching..." from "running" within - * a frame or two of the live toggle. Read-only, any thread. */ public static native int nativeZeroCopyLastFrameAgeMs(); - /** Compressed (UBWC) game buffers: the compositor advertises DRM_FORMAT_MOD_QCOM_COMPRESSED next to - * LINEAR on zwp_linux_dmabuf_v1 for every format its driver can import that way, so the game's - * Turnip allocates compressed swapchain images instead of resolving every frame to a linear copy. - * Default on; DROIDDECK_WAYLAND_UBWC=0 in the container's environment variables turns it off (A/B). - * Set before the compositor starts. */ public static native void nativeSetUbwc(boolean on); - /** Debug: advertise no DRM device (main device 0:0) in the dma-buf feedback, as a phone that - * exposes no /dev/dri node to apps does. DROIDDECK_WAYLAND_NO_RENDER_NODE=1 in the container's or - * shortcut's environment variables. Set before the compositor starts. */ public static native void nativeSetNoRenderNode(boolean on); - /** VRR / refresh-rate matching: mirror the panel refresh-rate vote the activity puts on the - * compositor's SurfaceView onto the game's own SurfaceControl layer. Under zero-copy the game's - * frames go straight onto that layer and never reach the app's surface, so the surface vote alone - * does not tell SurfaceFlinger the game's cadence. Same rate and compatibility as - * {@code Surface.setFrameRate}; {@code 0} clears the vote. Callable any time from any thread; the - * compositor applies it on its next layer transaction and writes one `layer` line per change to - * the session log. A no-op on Android versions without {@code ASurfaceTransaction_setFrameRate}. */ public static native void nativeSetLayerFrameRate(float hz); - /** Where the compositor was told to write its log, for the session collector; null if it was - * left to choose. Set before the compositor starts - it opens the file once, at start. */ private static volatile java.io.File sessionLogFile; public static void setSessionLogFile(java.io.File file) { @@ -299,233 +191,116 @@ public final class WaylandCompositor { return sessionLogFile; } - /** Write one line into the compositor's session log (Download/Wayland-logs) under the "display" - * area, from Java. Used for facts the platform knows and the native side does not - the panel's - * HDR capability, which lives behind android.view.Display. Safe before the compositor thread is - * up (the line then only reaches logcat) and safe after it has gone. */ public static native void nativeLogDisplay(String message); - /** The same under the "perf" area (e.g. the CPU cores Prefer Big Cores picked). Same safety. */ public static native void nativeLogPerf(String message); - /** The container's screen size, advertised as the Wayland output's mode so Wine's display-mode - * list stops at the desktop size, as the X server's does on X11. Set before the compositor starts. */ public static native void nativeSetOutputSize(int width, int height); - /** A new output size for a running compositor ("Follow screen": a foldable opening or closing). - * The compositor thread re-sends the wl_output mode and an xdg_toplevel configure to every - * client, and gamescope / the desktop's KWin resize their output to it. Any thread. */ public static native void nativeResizeOutput(int width, int height); - /** Re-arms the one-shot first-frame notice, for a new session under a compositor that has - * already presented a previous one. */ public static native void nativeResetFirstFrame(); - // ── HDR10 output, round 1 (waylandcomp/src/droiddeck_color.h, wl_color_mgmt.c) ───────────────── - // Opt-in: DROIDDECK_WAYLAND_HDR=1 in the container's or shortcut's env vars. The compositor offers - // games HDR10 (wp_color_manager_v1 + 10-bit buffers, frames tagged BT2020_PQ on the game's own - // display layer) only when the game's display lists HDR10 as well; otherwise nothing changes and - // the session log says why. Not the drawer's "HDR" effect, which is an SDR bloom/contrast filter. - /** HDR_MODE_OFF / HDR_MODE_ON (DROIDDECK_WAYLAND_HDR=1) / HDR_MODE_FORCE (=force: skip the display - * check, for testing the negotiation on an SDR panel). */ public static final int HDR_MODE_OFF = 0, HDR_MODE_ON = 1, HDR_MODE_FORCE = 2; - /** The opt-in: mode, where it came from ("container env" / "shortcut env"), whether DXVK_HDR=1 is - * in the game's environment, and whether the app turned zero-copy on for this session because HDR - * needs it. Set before the compositor starts. */ public static native void nativeSetHdrRequest(int mode, String source, boolean dxvkHdr, boolean zeroCopyForced); - /** The game's display as {@code android.view.Display} reports it. Before the compositor starts (it - * feeds the gate) and again whenever it changes (logged; the gate is decided once per session). */ public static native void nativeSetHdrDisplay(int displayId, String name, String formats, boolean hdr10, float maxLuminance, float maxAverageLuminance, float minLuminance, boolean hdrSdrRatioAvailable, float hdrSdrRatio, int apiLevel); - /** One {@code Display.getHdrSdrRatio()} reading ({@code < 0} = not available); {@code listener} = it - * came from the display's ratio listener rather than the periodic sampler. Any thread. */ public static native void nativeHdrSdrRatioSample(float ratio, boolean listener); - /** Milliseconds since an HDR frame last reached a display layer; -1 = none this session. Any thread. */ public static native int nativeHdrLastFrameAgeMs(); - /** -1 = the compositor has not decided the HDR gate yet, 0 = closed, 1 = open. Any thread. */ public static native int nativeHdrGateState(); - /** The session is ending: the compositor writes its "HDR on screen: …" summary line. Once. */ public static native void nativeHdrSessionEnd(); - /** True while HDR frames are really on screen: frames tagged BT2020_PQ reached the display in the - * last 1.5 s and, where the display reports an HDR/SDR ratio, it is above 1 (the HUD's badge). */ public static native boolean nativeHdrOnScreen(); - /** One "color" line in the session log, from Java (the HDR environment, the DXVK warning). */ public static native void nativeLogColor(String message); - /** One line in the session log under {@code area} (the log's 9-character column: "gpu", "nvapi"), - * from Java: launch facts the app decides (the GPU name spoof, the Unreal Engine HDR mode). Like - * {@link #nativeLogDisplay}, safe before the session file exists (the line is written when it - * opens) and after it has gone. */ public static native void nativeLog(String area, String message); - /** Nits at which SDR content is placed when the compositor composes an HDR picture (a window over - * an HDR game, the desktop around a windowed one). Default 203 (BT.2408). Before the start. */ public static native void nativeSetHdrSdrWhite(float nits); - /** The in-game drawer's HDR output switch (only meaningful while the HDR gate is open; per session, - * starts on). On = the game's HDR frames go to the display as HDR; off = the same frames are shown - * tone-mapped to SDR. Live, any thread (queued to the compositor, which logs the flip); the game is - * told nothing - DXVK_HDR and the colour-manager offer were decided at launch. */ public static native void nativeSetHdrOutput(boolean on); - /** The HDR output switch's current state (any thread). */ public static native boolean nativeHdrOutput(); - /** True while an HDR game's frames are being shown tone-mapped to SDR (the last one under 1.5 s ago). */ public static native boolean nativeHdrToneMappedOnScreen(); - /** 0 = no HDR frames on screen (or not confirmed yet), 1 = HDR frames on screen with HDR headroom, - * 2 = HDR frames on screen but the display has given them no headroom (HDR/SDR ratio 1.00) for 5 s - * or more - the brightness slider at maximum, or a screen recording (Android turns HDR headroom off - * while the screen is recorded). The HUD badge and the drawer's HDR row. */ public static native int nativeHdrState(); - /** Device evidence beside the HDR/SDR headroom, HDR sessions only (any thread): PowerManager thermal - * status (0..6, -1 unknown) and getThermalHeadroom(10) (NaN / negative = not available), and - * Settings.System SCREEN_BRIGHTNESS (0..255, -1 unknown) + SCREEN_BRIGHTNESS_MODE (1 auto, 0 manual, - * -1 unknown). The compositor logs changes of status and brightness and puts the values on every - * no-headroom line, the 10 s HDR line and the verdict. */ public static native void nativeHdrEnvSample(int thermalStatus, float thermalHeadroom, int brightness, int brightnessMode); - /** Display.getHighestHdrSdrRatio() (Android 16+), <= 0 = not reported: the display's own ceiling, capped - * into the headroom request and logged in the verdict. Any thread. */ public static native void nativeSetHdrHighestRatio(float ratio); - /** The HDR headroom the SCREEN surface should ask for while HDR frames go through the HDR10 swapchain - * (frame generation), 0 = none; the app applies it (SurfaceView.setDesiredHdrHeadroom, API 35). */ public static native float nativeHdrScreenHeadroom(); - /** Why that value ("content peak 1207 nits (max CLL) / SDR 203 ..."), for the log line. */ public static native String nativeHdrScreenHeadroomWhy(); - /** What the app asked for on the screen surface: > 0 the ratio, 0 cleared, -1 not possible (< Android 15). */ public static native void nativeHdrNoteHeadroomRequest(float ratio); - /** Fullscreen mode ({@code Container.FULLSCREEN_OFF/FIT/STRETCH/FILL/INTEGER}) and screen alignment - * ({@code Container.ALIGN_CENTER/TOP/BOTTOM}): how the compositor fits the desktop onto the screen, - * with the same arithmetic as {@code ViewTransformation} (which maps touch input), so the picture and - * the pointer agree. OFF and FIT both letterbox. Callable any time; the next frame uses it. */ public static native void nativeSetScaleMode(int fullscreenMode, int screenAlignment); - // ---- Screen effects: the X11 Vulkan renderer's post chain, run by the compositor between the - // composited scene and the output blit (waylandcomp/src/effects_chain.c). Same modes, ranges and - // pass order as VulkanRenderer's setters, so a saved preset looks the same on both backends. - // Callable any time from any thread; the compositor applies them on its next frame and writes - // one `effects` line to the session log per change. While any effect is on, a zero-copy - // (layer-mode) game is presented through the compositor pass instead. - /** Scaling mode: 0=None 1=Linear 2=Nearest 3=SGSR 4=FSR 5=FSR Fit 6=Sharpen 7=NIS 8=SGSR HQ. - * Resizes the scene to its mapped output size (there is no render scale on Wayland). */ public static native void nativeSetUpscaler(int mode); - /** The upscaler's own sharpness slider 0..100 (RCAS lobe scale, SGSR edge 0.5..4.5, NIS). */ public static native void nativeSetUpscaleSharpness(int sharpness); - /** AMD CAS sharpen toggle + level 0..100 (0 = the pass is off). */ public static native void nativeSetCas(boolean enabled, int sharpness); - /** Fake-HDR toggle. */ public static native void nativeSetHdr(boolean enabled); - /** Terminal debanding toggle + strength 0..200 (100 = 1 LSB). */ public static native void nativeSetDeband(boolean enabled, int strength); - /** Colour grade in the drawer's slider units (brightness/contrast -100..100, gamma 0.5..3.0, - * saturation 0..200 percent; 0/0/1/100 = neutral, pass off) plus the FXAA / Toon / CRT / NTSC - * toggles - the same signature as {@code VulkanRenderer.setScreenEffects}. */ public static native void nativeSetScreenEffects(float brightness, float contrast, float gamma, float saturation, boolean fxaa, boolean toon, boolean crt, boolean ntsc); - /** The Look the controls currently match ({@code null} = Custom); only named in the log line. */ public static native void nativeSetLookName(String name); - /** Cardboard mode (waylandcomp/src/cardboard.h): two lens-distorted eye views side by side. - * {@code source} 0 = the frame as a virtual screen, 1 = the frame is already a side-by-side - * stereo pair. Distances in metres, field of view in degrees. */ public static native void nativeSetCardboard(boolean enabled, int source, float ipdM, float screenDistM, float screenWidthM, float k1, float k2, float fovDeg, float lensInset, float chroma); - /** The head pose for Cardboard mode: head -> world rotation, row-major 3x3 (any thread). {@code live} - * = a sensor produced it. */ public static native void nativeSetCardboardPose(float[] rotation, boolean live); - /** Where the head pose is shared with the guest (waylandcomp/src/headpose_shm.h); set once. */ public static native void nativeSetCardboardPoseFile(String path); - /** The in-game FPS limiter: frames per second, 0 = unlimited. Paces when replaced buffers go - * back to the game, like the X11 IdleNotify pacer, so the game itself slows to the cap. */ public static native void nativeSetFpsLimit(int fps); - /** KGSL power control off (true: the GPU held at its top clock) or back on. Device-wide and - * outlives the process; see {@code GpuClockPin}. A no-op without /dev/kgsl-3d0. */ public static native void nativeSetGpuTurbo(boolean on); - /** setpriority() on one of our own processes or threads, only ever raising it (no cgroup move, - * unlike android.os.Process.setThreadPriority). Returns the nice it is left at; 100 = unknown. */ public static native int nativeRaisePriority(int tid, int nice); - /** Inject the app's X-server input in scene (virtual desktop) pixels. type 2 = move to a,b; - * 3 = evdev button a (BTN_LEFT=0x110…) pressed (b=1) or released (b=0); 4 = a wheel steps, - * negative = up. */ public static native void nativeSendSceneInput(int type, int a, int b); - // ── Frame generation (waylandcomp/src/framegen_bridge.c) ───────────────────────────────── - // LSFG Native and Win-FG Native run inside the compositor's Turnip device, the same engines - // the X11 VulkanRenderer hosts. Every setter only stores a value; the compositor thread applies - // it on its next frame, so they are safe from any thread and before the compositor starts. - // bionic-fg (the guest-side win-fg layer) is X11-only and has no Wayland counterpart. public static final int FG_ENGINE_LSFG = 0, FG_ENGINE_WINFG = 1; - /** Which native engine generates: {@link #FG_ENGINE_LSFG} or {@link #FG_ENGINE_WINFG}. */ public static native void nativeSetFrameGenEngine(int kind); - /** Arm (multiplier 2..4: one real frame plus multiplier-1 interpolated ones per game frame) or - * disarm. A targetFps above 0 makes LSFG adaptive instead: it generates 0..3 frames per game - * frame, whatever reaches that rate. Generated frames are presented ahead of the real frame - * on consecutive vblanks. */ public static native void nativeSetFrameGenArmed(boolean armed, int multiplier, int targetFps); - /** LSFG Native: the SPIR-V cache built from the user's Lossless.dll ({@code Lossless.cacheFile}). */ public static native void nativeSetLsfgCachePath(String path); - /** Flow scale (0.25-1.0) and the panel's real refresh rate (the pacer never generates above it). */ public static native void nativeSetFrameGenTuning(float flowScale, float refreshHz); - /** Win-FG Native only: interpolation model (3/4) and performance preset (0..2). */ public static native void nativeSetWinFgTuning(int model, int perfPreset); - /** Same codes as {@code VulkanRenderer.getFrameGenProblem()}: -1 not known yet (the compositor's - * device is not up), 0 fine, 1 the driver lacks what the selected engine needs - * ({@link #nativeFrameGenCapsReason()} says what), 2 the engine failed to start. */ public static native int nativeFrameGenProblem(); public static native String nativeFrameGenCapsReason(); - /** Same shape as {@code VulkanRenderer.getFrameGenStats()}: {generations trusted, generations - * planned, real fps, presented fps (generated frames included), thermal (-1 = none), - * GPU ms per generated frame (-1 = unknown)}. */ public static native float[] nativeFrameGenStats(); - /** Relative pointer motion by dx,dy scene pixels (the Relative Mouse / captured-mouse path): - * while a program holds a pointer lock this is what it receives as relative_motion; otherwise - * the compositor moves its pointer by the delta. */ public static void sendPointerDelta(int dx, int dy) { if (dx == 0 && dy == 0) return; nativeSendSceneInput(6, dx * 256, dy * 256); } - /** As {@link #sendPointerDelta(int, int)}, keeping a captured mouse's fractions of a pixel. */ public static void sendPointerDelta(float dx, float dy) { int x = Math.round(dx * 256f), y = Math.round(dy * 256f); if (x == 0 && y == 0) return; diff --git a/app/src/main/res/anim/session_hold.xml b/app/src/main/res/anim/session_hold.xml index 60e4638f6..8a828450e 100644 --- a/app/src/main/res/anim/session_hold.xml +++ b/app/src/main/res/anim/session_hold.xml @@ -1,3 +1,2 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- The front end stays put while the session slides over or off it. --> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="1" android:toAlpha="1" android:duration="380" /> diff --git a/app/src/main/res/anim/session_rise.xml b/app/src/main/res/anim/session_rise.xml index 7aa0b7074..57f4afd23 100644 --- a/app/src/main/res/anim/session_rise.xml +++ b/app/src/main/res/anim/session_rise.xml @@ -1,5 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- The session surface rises over the front end. --> <set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:interpolator/decelerate_cubic"> <translate android:fromYDelta="100%p" android:toYDelta="0" android:duration="380" /> <alpha android:fromAlpha="0.6" android:toAlpha="1" android:duration="380" /> diff --git a/app/src/main/res/anim/session_sink.xml b/app/src/main/res/anim/session_sink.xml index 6854de13d..e5f80c5aa 100644 --- a/app/src/main/res/anim/session_sink.xml +++ b/app/src/main/res/anim/session_sink.xml @@ -1,5 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Leaving a session: the surface sinks back down to reveal the front end. --> <set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:interpolator/accelerate_cubic"> <translate android:fromYDelta="0" android:toYDelta="100%p" android:duration="320" /> </set> diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5f2842f42..224591154 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1153,16 +1153,12 @@ <string name="game_file_exported">Archivos de juegos guardados en %1$s</string> <string name="game_frontend_files">Archivos para frontends</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">Espera a que termine la operación del runtime</string> <string name="main_wait_proton_install">Espera a que termine la instalación de la herramienta de compatibilidad</string> <string name="main_wait_desktop_install">Espera a que termine la instalación de la app de escritorio</string> <string name="main_wait_install_game">Espera a que termine la instalación antes de iniciar un juego</string> <string name="main_saves_close_session">Cierra primero la sesión de Steam para que el juego no guarde encima de lo importado</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">Importando en %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="one">Se importó %1$d archivo del %2$s en %3$s</item> <item quantity="many">Se importaron %1$d archivos del %2$s en %3$s</item> @@ -1174,17 +1170,13 @@ <item quantity="other">Se importaron %1$d archivos del %2$s en %3$s. Copia de las partidas anteriores en Download/DroidDeck/Saves/backups</item> </plurals> <string name="main_saves_pick_zip">Elige un zip de partidas guardadas para %1$s</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">Exportando %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="one">Se exportó %1$d archivo como %2$s: %3$s</item> <item quantity="many">Se exportaron %1$d archivos como %2$s: %3$s</item> <item quantity="other">Se exportaron %1$d archivos como %2$s: %3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">Elige dónde guardar %1$s (%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">%1$s falló: %2$s</string> <string name="main_no_logs_yet">Aún no hay registros de sesión: inicia una sesión primero.</string> <string name="main_no_logs_to_clear">No hay registros de sesión que borrar.</string> @@ -1199,68 +1191,55 @@ <string name="main_adb_copied">Comando ADB copiado</string> <string name="main_pick_roms">Elige la carpeta de ROMs</string> <string name="main_non_adreno_title">La GPU no es Adreno</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip es compatible con GPU Adreno. En %1$s, Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB.</string> <string name="main_install_anyway">Instalar de todos modos</string> <string name="main_remove_runtime_title">Eliminar el runtime de Linux</string> <string name="main_remove_runtime_text">Esto elimina el runtime, el cliente de Steam que contiene y todos los juegos instalados en él.</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">Aplicado: %1$s</string> <string name="android_app_open_failed_primary">No se pudo abrir %1$s en la pantalla principal</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">No se pudo abrir %1$s en %2$s</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">No se pudo abrir %1$s en la pantalla %2$d</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">Importando</string> <string name="comp_busy_swapping">Reemplazando</string> <string name="comp_busy_restoring">Restaurando</string> <string name="comp_busy_cancelling">Cancelando</string> <string name="comp_busy_deleting">Eliminando</string> <string name="comp_cannot_read">no se puede leer el archivo</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">%1$s importado</string> <string name="comp_swap_cancelled">Se canceló el reemplazo pendiente.</string> <string name="comp_pick_wcp">Elige un paquete de componentes (-linux .wcp)</string> <string name="comp_nightlies_unreachable">No se pudo acceder a las Nightlies</string> - <!-- %1$s is the package version --> <string name="comp_stored">%1$s guardado</string> <string name="comp_download_failed">La descarga falló: %1$s</string> <string name="drivers_pick_linux">Elige un controlador de runtime de Linux (zip -Linux o paquete Android + Linux)</string> <string name="drivers_pick_android">Elige un controlador de pantalla (zip de AdrenoTools o paquete Android + Linux)</string> <string name="drivers_pick_any">Elige un zip de controlador o un paquete Android + Linux</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s: %2$s</string> <string name="main_removing_named">Quitando %1$s…</string> <string name="main_forgetting_named">Olvidando %1$s…</string> <string name="main_pick_game_storage">Elige la carpeta de almacenamiento de juegos</string> <string name="main_pick_decky_zip">Elige un ZIP de plugin de Decky</string> <string name="main_storage_not_usable">No se puede usar: %1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">Programa</string> <string name="main_removing_runtime">Eliminando el runtime de Linux</string> <string name="main_settings_main_screen">No se pudieron abrir los Ajustes en la pantalla inferior; se abrieron en la pantalla principal.</string> <string name="main_open_dev_options">Abre Opciones de desarrollador en Ajustes</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">No se pudo abrir el ajuste \"Instalar apps desconocidas\" de Android</string> <string name="upd_couldnt_check">No se pudo comprobar: %1$s</string> <string name="upd_no_download">Esta versión no tiene descarga para esta copia de DroidDeck</string> <string name="upd_stop_first">Detén primero la sesión en ejecución y después actualiza</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">Descargando</string> <string name="upd_stage_installing">Instalando</string> <string name="upd_stage_confirm">Esperando tu confirmación</string> <string name="upd_failed">La actualización falló</string> <string name="upd_not_offered">Android no ofreció instalar la actualización</string> - <!-- Decky --> <string name="decky_install_failed">Falló la instalación de Decky: %1$s</string> <string name="decky_plugin_starting">Iniciando la importación del plugin…</string> <string name="decky_plugin_failed">Falló la importación del plugin: %1$s</string> <string name="decky_plugin_installed">Plugin instalado. Reinicia Steam para cargarlo.</string> - <!-- GPU drivers --> <string name="drivers_not_checked">Aún sin comprobar - toca actualizar para buscar controladores nuevos</string> <string name="drivers_bundle_sets_display">Android + Linux: define también el controlador de pantalla</string> <string name="drivers_bundle_sets_runtime">Android + Linux: define también el controlador de runtime</string> @@ -1268,16 +1247,13 @@ <string name="drivers_system_vulkan">Vulkan del sistema: no hay versión incluida para esta GPU</string> <string name="drivers_bundled">%1$s (incluido)</string> <string name="drivers_manual">Manual</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">Por ahora solo se ha publicado una de las mitades</string> <string name="drivers_pair_size">%1$.0f MB para ambos</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">No hay controladores que definir: %1$s</string> <string name="drivers_checking_latest">Buscando los controladores más recientes…</string> <string name="drivers_up_to_date_check_failed">Al día (no se pudo comprobar: %1$s)</string> <string name="drivers_recommended_unpublished">Los controladores recomendados no están publicados ahora mismo</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">Descargando %1$s %2$s…</string> <string name="drivers_updated_to">Actualizado a %1$s %2$s.</string> <string name="drivers_updated_to_restart">Actualizado a %1$s %2$s. El controlador de pantalla se aplica al reiniciar DroidDeck.</string> @@ -1292,9 +1268,7 @@ <string name="drivers_using_bundle_restart">Usando %1$s como controlador de runtime y de pantalla. El controlador de pantalla se aplica al reiniciar DroidDeck.</string> <string name="drivers_deleted_bundle">%1$s eliminado (ambos controladores)</string> <string name="drivers_deleted">%1$s eliminado</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">Últimas: %1$s · comprobado %2$s</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">Últimas: %1$s · sin acceso a %2$s · comprobado %3$s</string> <plurals name="drivers_ago_minutes"> <item quantity="one">hace %1$d min</item> @@ -1313,33 +1287,25 @@ </plurals> <string name="drivers_checking_sources">Comprobando Banners-Turnip, WinNative y DroidDeck…</string> <string name="drivers_check_failed">falló la comprobación</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">%1$s instalado - elígelo en cualquiera de las listas para definir ambos controladores</string> <string name="drivers_installed_pick">%1$s instalado - elígelo en el menú</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">Espera a que termine la eliminación del runtime</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">Juego</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">touchpad</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">directo</string> <string name="session_swap_failed">El reemplazo falló: %1$s</string> <string name="session_profile_save_failed">No se pudo guardar el perfil: %1$s</string> <string name="session_desktop_paused">El escritorio está en pausa</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s está en pausa</string> <string name="session_program_paused">El programa está en pausa</string> <string name="session_starting_desktop">Iniciando el escritorio</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">Iniciando %1$s</string> <string name="session_starting_program">Iniciando el programa</string> <string name="session_no_logs">No hay registros de esta sesión.</string> <string name="session_logs_archive_failed">No se pudo crear el archivo de registros de la sesión.</string> <string name="session_logs_share_failed">No se pudieron compartir los registros de la sesión.</string> <string name="session_starting_session">Iniciando la sesión…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">descargando el runtime de Linux</string> <string name="session_runtime_downloading_mb">descargando el runtime de Linux · %1$d de %2$d MB</string> <string name="session_runtime_checking">comprobando el runtime de Linux</string> @@ -1354,29 +1320,22 @@ <string name="session_proton_unpacking">descomprimiendo Proton Experimental (ARM64)</string> <string name="session_runtime_catalog_unreachable">No se pudo acceder al catálogo del runtime. Revisa la conexión y vuelve a pulsar Jugar.</string> <string name="session_runtime_install_failed">El runtime de Linux no se instaló. Revisa el runtime en Configuración y vuelve a pulsar Jugar.</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">El escritorio no se instaló (%1$s). Revisa la conexión y pulsa Escritorio de nuevo.</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s se detuvo inesperadamente. Comparte los registros en un informe de error o inténtalo de nuevo.</string> <string name="session_ended_program">El programa se detuvo inesperadamente. Comparte los registros en un informe de error o inténtalo de nuevo.</string> <string name="session_ended_desktop">El escritorio se detuvo inesperadamente. Comparte los registros en un informe de error o inténtalo de nuevo.</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">Código de salida %1$d · %2$s</string> <string name="session_ended_no_log">sin registro</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="one">El registro muestra %1$d error \"Function not implemented\": la aceleración seccomp de proot hace fallar un proceso auxiliar en este dispositivo. Prueba %2$s → \"%3$s\" (o \"%4$s\") y vuelve a iniciar.</item> <item quantity="many">El registro muestra %1$d errores \"Function not implemented\": la aceleración seccomp de proot hace fallar un proceso auxiliar en este dispositivo. Prueba %2$s → \"%3$s\" (o \"%4$s\") y vuelve a iniciar.</item> <item quantity="other">El registro muestra %1$d errores \"Function not implemented\": la aceleración seccomp de proot hace fallar un proceso auxiliar en este dispositivo. Prueba %2$s → \"%3$s\" (o \"%4$s\") y vuelve a iniciar.</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">Plugins</string> <string name="mode_decky_plugins_hint">Instala un ZIP de plugin para la próxima sesión de Steam.</string> <string name="mode_decky_plugin_downloading">Descargando…</string> <string name="mode_decky_plugin_install_zip">Instalar desde ZIP</string> - <!-- Store --> <string name="store_cat_games">Juegos</string> <string name="store_cat_audio_video">Audio y vídeo</string> <string name="store_cat_graphics">Gráficos</string> @@ -1389,21 +1348,14 @@ <string name="store_cat_utilities">Utilidades</string> <string name="store_section_popular">Populares</string> <string name="store_section_trending">En tendencia</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">Nuevas</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">Actualizadas</string> <string name="store_section_loading">cargando…</string> <string name="store_flatpak_setup">Configuración de Flatpak</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s: %2$s</string> <string name="store_updates_check_failed">No se pudieron buscar actualizaciones en Flathub</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">instalado</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">carpeta</string> - <!-- Launcher themes --> <string name="theme_graphite">Grafito</string> <string name="theme_graphite_detail">Superficies grafito, con el azul del orbe del logotipo para todo lo seleccionado, enfocado o pulsado.</string> <string name="theme_paper">Papel sobre negro</string> @@ -1412,7 +1364,6 @@ <string name="theme_icon_blue_detail">Fondo negro, texto blanco y el azul para todo lo seleccionado, enfocado o pulsado.</string> <string name="theme_electric">Azul marino eléctrico</string> <string name="theme_electric_detail">Un azul más vivo, sobre superficies que tiran a azul marino en vez de gris.</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Insert</string> <string name="pckb_key_del">Supr</string> @@ -1430,22 +1381,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">Espacio</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">Instala primero el runtime</string> <string name="game_sync_err_steam_library">La biblioteca de Steam no está disponible</string> <string name="game_sync_err_previous_sd">La biblioteca anterior de la SD no está disponible</string> <string name="game_sync_err_game_storage">El almacenamiento de juegos no está disponible</string> <string name="game_sync_err_added_games">La carpeta de juegos añadidos no está disponible</string> <string name="game_sync_err_manifests">Los manifiestos de Steam no están disponibles</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">Varios sistemas</string> - <!-- File manager --> <string name="fm_drive_internal">Interno</string> <string name="fm_drive_linux">Sistema Linux</string> <string name="fm_drive_sd">Tarjeta SD</string> @@ -1463,7 +1409,6 @@ <string name="fm_no_favorites">Aún no hay favoritos - fija una carpeta con su menú ⋮ para volver aquí rápidamente.</string> <string name="fm_unpin_favorite">Quitar de favoritos</string> <string name="fm_size_zero">0 bytes</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">bytes</string> <string name="fm_prop_location">Ubicación</string> <string name="fm_prop_type">Tipo</string> @@ -1471,7 +1416,6 @@ <string name="fm_prop_modified">Modificado</string> <string name="fm_type_folder">Carpeta</string> <string name="fm_type_file">Archivo</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">Archivo %1$s</string> <string name="fm_read_only">Solo lectura</string> <string name="fm_read_only_hint">Impide que los juegos (y Wine) sobrescriban o eliminen este archivo.</string> @@ -1485,15 +1429,11 @@ <string name="fm_delete_failed">No se pudo eliminar</string> <string name="fm_op_moving">Moviendo...</string> <string name="fm_op_copying">Copiando...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">Moviendo - %1$s</string> <string name="fm_op_copying_item">Copiando - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">Moviendo %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">Copiando %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d hechos, %2$d con error</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d hechos, %2$d omitidos</string> <plurals name="fm_paste_moved"> <item quantity="one">%1$d elemento movido</item> @@ -1505,7 +1445,6 @@ <item quantity="many">%1$d elementos copiados</item> <item quantity="other">%1$d elementos copiados</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">\"%1$s\" ya existe</string> <string name="fm_op_renaming">Renombrando...</string> <string name="fm_rename_failed">No se pudo renombrar</string> @@ -1516,7 +1455,6 @@ <string name="fm_create">Crear</string> <string name="fm_new_name">Nuevo nombre</string> <string name="fm_delete_title">¿Eliminar?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">¿Eliminar \"%1$s\" de forma permanente?</string> <plurals name="fm_delete_items_title"> <item quantity="one">¿Eliminar %1$d elemento?</item> @@ -1524,13 +1462,11 @@ <item quantity="other">¿Eliminar %1$d elementos?</item> </plurals> <string name="fm_cannot_undo">Esta acción no se puede deshacer.</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="one">…y %1$d más</item> <item quantity="many">…y %1$d más</item> <item quantity="other">…y %1$d más</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">Eliminando %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="one">No se pudo eliminar %1$d</item> @@ -1546,7 +1482,6 @@ <string name="fm_conflict_skip">Omitir</string> <string name="fm_select_this_folder">Seleccionar esta carpeta</string> <string name="fm_back">Atrás</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s está montado, pero ahora mismo no se puede leer</string> <string name="fm_list_view">Vista de lista</string> <string name="fm_grid_view">Vista de cuadrícula</string> @@ -1561,12 +1496,9 @@ <string name="fm_hide_favorites">Ocultar favoritos</string> <string name="fm_show_favorites">Mostrar favoritos</string> <string name="fm_filter_folder">Filtrar esta carpeta</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">%1$s libres</string> <string name="fm_selected_count">%1$d seleccionados</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">Ninguno</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">Todo</string> <string name="fm_paste">Pegar</string> <plurals name="fm_items"> @@ -1574,30 +1506,23 @@ <item quantity="many">%1$d elementos</item> <item quantity="other">%1$d elementos</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">Pegar %1$s aquí</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">Pegar %1$s aquí (mover)</string> <string name="fm_rail_title">Archivos</string> <string name="fm_rail_storage">ALMACENAMIENTO</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">ATAJOS</string> <string name="fm_rail_favorites">FAVORITOS</string> <string name="fm_rail_downloads">Descargas</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">Descargas</string> <string name="fm_rail_roms">ROMs</string> <string name="fm_rail_session_logs">Registros de sesión</string> <string name="fm_rail_pictures">Imágenes</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">Se añadió \"%1$s\" a Favoritos</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">Se quitó \"%1$s\" de Favoritos</string> <string name="fm_empty_directory">Carpeta vacía</string> <string name="fm_up_one_level">Subir un nivel</string> <string name="fm_choose_folder">Elige una carpeta</string> <string name="fm_choose_file">Elige un archivo</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Azul Steam</string> <string name="ctrl_tint_sky">Celeste</string> <string name="ctrl_tint_white">Blanco</string> @@ -1606,7 +1531,6 @@ <string name="ctrl_tint_amber">Ámbar</string> <string name="ctrl_tint_red">Rojo</string> <string name="ctrl_tint_pink">Rosa</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">Botón A</string> <string name="ctrl_button_b">Botón B</string> <string name="ctrl_button_x">Botón X</string> @@ -1617,28 +1541,21 @@ <string name="ctrl_button_rt">Gatillo derecho</string> <string name="ctrl_button_view">Botón Vista</string> <string name="ctrl_button_menu">Botón Menú</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">Vista</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">Menú</string> <string name="ctrl_target_up">Cruceta arriba</string> <string name="ctrl_target_down">Cruceta abajo</string> <string name="ctrl_target_left">Cruceta izquierda</string> <string name="ctrl_target_right">Cruceta derecha</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">Oculto</string> <string name="ctrl_editor_drag">Arrastra para mover</string> <string name="ctrl_editor_snap">Ajustar</string> <string name="ctrl_editor_save">Guardar</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">Nada</string> <string name="second_mode_keyboard_trackpad">Teclado + trackpad</string> <string name="second_mode_terminal">Terminal</string> <string name="second_mode_deck_controls">Agarres de Deck + trackpads</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">Eliminando el runtime de Linux</string> <plurals name="rtinst_removing_entries"> <item quantity="one">Eliminando el runtime de Linux · %1$d elemento</item> @@ -1655,63 +1572,47 @@ <string name="rtinst_title_installing">Instalando el runtime de Linux</string> <string name="rtinst_title_removing">Eliminando el runtime de Linux</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">El runtime de Linux no está instalado</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">El catálogo no tiene suma de verificación para %1$s</string> <string name="deskpkg_checksum_mismatch">La suma de verificación no coincide - no se cambió nada</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">Instalando %1$s</string> <string name="deskpkg_place_failed">No se pudo colocar el AppImage</string> <string name="deskpkg_extract_failed">La extracción falló</string> <string name="deskpkg_install_failed">La instalación falló</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">La configuración de Flatpak falló (salida %1$d)</string> <string name="flatpakmgr_setup_unfinished">La configuración de Flatpak no terminó</string> <string name="flatpakmgr_busy">Hay otra tarea de la tienda en curso</string> <string name="flatpakmgr_not_ready">Configura Flatpak primero</string> <string name="flatpakmgr_starting">Iniciando</string> <string name="flatpakmgr_exit_status">Flatpak terminó con el código %1$d</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">Instalando %1$s</string> <string name="flatpakmgr_updating">Actualizando %1$s</string> <string name="flatpakmgr_removing">Quitando %1$s</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">Instalando %1$s (%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">Actualizando %1$s (%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">Quitando %1$s (%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">traducciones</string> <string name="flatpakmgr_ref_graphics_drivers">controladores gráficos</string> <string name="flatpakmgr_ref_media_codecs">códecs multimedia</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">runtime de %1$s</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">runtime de %1$s %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">El archivo es demasiado pequeño para ser un AppImage</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">No se puede leer el archivo: %1$s</string> <string name="appimg_not_linux">Esto no es un AppImage (no es un programa de Linux)</string> <string name="appimg_wrong_arch">Este AppImage no está hecho ni para ARM64 (aarch64) ni para PC x86</string> <string name="appimg_not_appimage">Es un programa de Linux, pero no un AppImage</string> <string name="appimg_wrong_type">Solo se pueden importar AppImages de tipo 2 (este es de tipo %1$d)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">No hay espacio libre suficiente: importarlo necesita unos %1$s</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">No hay espacio libre suficiente: actualizarlo necesita unos %1$s</string> <string name="appimg_import_failed">La importación falló</string> <string name="appimg_gone">Ese AppImage ya no está</string> <string name="appimg_update_failed">La actualización falló</string> - <!-- %1$s: file name --> <string name="appimg_extracting">Extrayendo %1$s</string> <string name="appimg_extract_failed">No se pudo extraer el AppImage</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">No se pudo extraer el AppImage: %1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">Detén la sesión activa antes de instalar Decky Loader</string> <string name="deckymgr_download_failed">Falló la descarga de Decky Loader</string> <string name="deckymgr_size_mismatch">El tamaño de Decky Loader no coincide con los metadatos de la versión</string> @@ -1747,20 +1648,13 @@ <string name="deckymgr_remote_entry_invalid">Entrada remote_binary no válida</string> <string name="deckymgr_remote_name_invalid">Nombre de binario remoto no válido</string> <string name="deckymgr_remote_url_https">La URL del binario remoto debe usar HTTPS</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">El binario remoto %1$s tiene un hash SHA-256 no válido</string> <string name="deckymgr_bin_dir_failed">No se pudo crear la carpeta bin del plugin</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">Descargando el binario del plugin · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">No se pudo descargar el binario del plugin %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">La suma de verificación del binario del plugin no coincide · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">No se pudo marcar como ejecutable el binario del plugin · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">No se pudo reemplazar el binario del plugin · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">No se pudo instalar el binario del plugin · %1$s</string> <string name="deckymgr_folder_taken">La carpeta del plugin pertenece a otro plugin</string> <string name="deckymgr_preserve_failed">No se pudo conservar el plugin existente</string> @@ -1768,20 +1662,14 @@ <string name="deckymgr_settings_failed">No se pudieron actualizar los ajustes de plugins de Decky</string> <string name="deckymgr_restore_failed">No se pudo restaurar el plugin existente</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">No se pudo guardar la identidad de la Depuración inalámbrica</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">Falló el comando ADB (%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android no confirmó el cambio del límite de procesos secundarios: %1$s</string> <string name="adbfix_no_pairing">No hay ninguna vinculación de Depuración inalámbrica guardada</string> <string name="adbfix_enter_address">Introduce la dirección IP y el puerto actuales de la Depuración inalámbrica</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">Se agotó el tiempo de espera de la ventana de vinculación. Inténtalo de nuevo desde DroidDeck.</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">No se pudo buscar la ventana de vinculación (%1$d). Comprueba que el Wi-Fi esté activado.</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">No se pudo buscar la ventana de vinculación: %1$s</string> <string name="adbpair_six_digits">Introduce los 6 dígitos del código</string> <string name="adbpair_pairing">Vinculando…</string> @@ -1795,27 +1683,22 @@ <string name="adbpair_title_pair">Vincular Depuración inalámbrica</string> <string name="adbpair_waiting_text">Activa la Depuración inalámbrica y toca “Vincular dispositivo con código de vinculación”.</string> <string name="adbpair_waiting_long">Activa la Depuración inalámbrica y toca “Vincular dispositivo con código de vinculación”. Quédate en Ajustes: el código se introduce en esta notificación.</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">Código de vinculación de 6 dígitos</string> <string name="adbpair_code_hint">Toca Introducir código y escribe los 6 dígitos que muestra Ajustes.</string> <string name="adbpair_title_code">Introduce el código de vinculación Wi-Fi</string> <string name="adbpair_title_retry">Vuelve a probar el código</string> - <!-- Notification action button --> <string name="adbpair_enter_code">Introducir código</string> <string name="adbpair_title_working">Preparando Steam</string> <string name="adbpair_title_done">Steam está listo</string> <string name="adbpair_done_text">Toca para volver a DroidDeck. Ya puedes desactivar la Depuración inalámbrica.</string> <string name="adbpair_title_failed">Se detuvo la configuración de la Depuración inalámbrica</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">La descarga respondió HTTP %1$d</string> <string name="selfinst_size_mismatch">El tamaño de la descarga no coincidía con el APK publicado y se descartó - inténtalo de nuevo</string> <string name="selfinst_checksum_mismatch">La descarga no coincidía con su suma de verificación y se descartó - inténtalo de nuevo</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">La actualización es para el paquete de Android %1$s, no %2$s</string> <string name="selfinst_metadata_signer">Los metadatos de la actualización no indican la clave de firma de publicación de DroidDeck</string> <string name="selfinst_unreadable">Android no pudo leer el APK descargado</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">El APK descargado es para %1$s, no %2$s</string> <string name="selfinst_version_mismatch">El APK descargado indica versionCode %1$d, no el %2$d publicado</string> <string name="selfinst_downgrade">Android no instalará versionCode %1$d sobre el %2$d instalado</string> @@ -1823,16 +1706,11 @@ <string name="selfinst_wrong_signer">El APK descargado no está firmado con la clave de publicación de DroidDeck</string> <string name="appupd_http">El catálogo de actualizaciones respondió HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">Iniciando la sesión…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">%1$d:%2$02d transcurridos · sigue en marcha</string> <string name="loadstate_steam_update">Descargando la actualización del cliente de Steam · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">Descargando el cliente de Steam · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">Ya se está instalando una herramienta de compatibilidad</string> <string name="pextra_stop_session">Detén la sesión activa antes de instalar herramientas de compatibilidad</string> <string name="pextra_no_release">No se encontró una versión ARM64 de %1$s</string> @@ -1849,48 +1727,38 @@ <string name="pextra_checksum_mismatch">La suma de verificación no coincide; se eliminó la descarga</string> <string name="pextra_session_started">Se inició una sesión durante la descarga; detenla antes de instalar herramientas de compatibilidad</string> <string name="pextra_installing">Instalando %1$s</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">Falló la instalación de %1$s (salida %2$d)</string> <string name="pextra_could_not_install">No se pudo instalar %1$s</string> <string name="esync_fetching">Descargando el paquete droiddeck-esync</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">con auxiliares de Linux</string> <string name="cmgr_fex_dlls_only">Solo DLL</string> <string name="cmgr_original_proton_updated">Original (Proton se actualizó desde el último reemplazo)</string> <string name="cmgr_changed_outside">Cambiado fuera de Componentes</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">no presente</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">Original %1$s</string> <string name="cmgr_original_from">Original de %1$s</string> <string name="cmgr_swap_queued">%1$s %2$s entrará en %3$s cuando se cierre el juego que lo está usando.</string> <string name="cmgr_swap_done">%1$s %2$s ya está en %3$s. Se aplica la próxima vez que se inicie un juego.</string> <string name="cmgr_original_missing">Ese paquete original no está guardado</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%1$s de %2$s volverá a su original cuando se cierre el juego que lo está usando.</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">%1$s de %2$s restaurado desde su original de %3$s.</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$s está en uso en %2$s. Cambia antes ese Proton a otra cosa.</string> <string name="cmgr_package_not_stored">%1$s no está guardado</string> <string name="cmgr_deleted">%1$s eliminado.</string> <string name="cmgr_original_kept">El original de la versión instalada se conserva: es el camino de vuelta.</string> <string name="cmgr_not_stored">No guardado</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">Se eliminó el original de %1$s de %2$s.</string> <string name="cmgr_nightlies_only">Las descargas solo proceden de las versiones Nightlies</string> <string name="cmgr_list_predates_checksums">Esta lista de paquetes es anterior a las sumas de verificación - actualízala e inténtalo de nuevo</string> <string name="cmgr_checksum_mismatch">La suma de verificación no coincide</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">Lineal</string> <string name="sprefs_upscaler_nearest">Más cercano</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (calidad)</string> <string name="sprefs_upscaler_sharpen">Solo nitidez</string> <string name="sprefs_lod_bias_auto">Auto (según el escalado)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">Sin efectos.</string> <string name="look_game_clarity">Claridad</string> <string name="look_game_clarity_desc">Nitidez con un toque de contraste: el de todos los días.</string> @@ -1915,27 +1783,23 @@ <string name="look_anime_edge">Bordes anime</string> <string name="look_anime_edge_desc">Contornos toon más nitidez.</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">La GPU no es Adreno</string> <string name="gpuinfo_supported">Compatible</string> <string name="gpuinfo_experimental">Experimental: sus controladores son versiones de prueba</string> <string name="gpuinfo_below_tested">Por debajo del hardware probado (Adreno 725 y posteriores): puede que no funcione</string> <string name="gpuinfo_unsupported">No compatible: DroidDeck necesita una GPU Adreno (Snapdragon)</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">Se alcanzó el límite de solicitudes de GitHub - inténtalo más tarde</string> <string name="turnip_github_http">GitHub respondió HTTP %1$d</string> <string name="turnip_download_http">la descarga respondió HTTP %1$d</string> <string name="turnip_list_predates_checksums">Esta lista de controladores es anterior a las sumas de verificación - actualízala e inténtalo de nuevo</string> <string name="turnip_checksum_mismatch">La suma de verificación no coincide - se descartó la descarga</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">No es un controlador de AdrenoTools: %1$s</string> <string name="turnip_reject_no_meta">no hay meta.json</string> <string name="turnip_reject_meta_unreadable">no se puede leer meta.json (%1$s)</string> <string name="turnip_reject_linux_driver">es un controlador de runtime de Linux (impórtalo en \"Controlador de runtime\")</string> <string name="turnip_reject_wayland">es un controlador de juegos para Wayland, que esta app no usa</string> <string name="turnip_reject_no_library_name">meta.json no indica ningún libraryName, así que no se pudo pasar nada a AdrenoTools</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json indica %1$s, que no está en el zip</string> <string name="turnip_reject_not_aarch64">%1$s no es una biblioteca compartida ELF AArch64 de 64 bits</string> <string name="turnip_reject_glibc">%1$s enlaza glibc - es un Turnip \"-Linux\", que va en \"Controlador de runtime\"</string> @@ -1946,90 +1810,61 @@ <string name="bundle_entry_outside">No es un paquete de controladores: %1$s apunta fuera de él</string> <string name="bundle_too_large">No es un paquete de controladores: ocupa más de %1$d MB al descomprimirse</string> - <!-- Game storage --> <string name="gstore_sd_card">Tarjeta SD</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">Carpeta</string> <string name="logshare_chooser">Compartir registros de sesión</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">requiere Android 10 o posterior (capas de pantalla con espacio de color)</string> <string name="hdr_none">esta pantalla no indica compatibilidad con HDR</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">esta pantalla admite %1$s, no HDR10</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">esta GPU</string> <string name="phantom_title_on">El límite de procesos secundarios está activado</string> <string name="phantom_title_unset">El límite de procesos secundarios no está definido</string> <string name="phantom_title_unreadable">No se pudo comprobar el límite de procesos secundarios</string> <string name="phantom_title_off">El límite de procesos secundarios está desactivado</string> <string name="phantom_title_not_required">El límite de procesos secundarios no es necesario</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s Si no es posible, conecta este dispositivo a un ordenador con ADB y ejecuta el comando de abajo. Después vuelve aquí y comprueba de nuevo.</string> <string name="phantom_instructions_off">Ya se pueden iniciar sesiones de Steam.</string> <string name="phantom_instructions_not_applicable">Esta versión de Android no usa este límite.</string> <string name="phantom_fix_toggle">Activa “Desactivar restricciones de procesos secundarios” en Opciones de desarrollador y deja activadas las Opciones de desarrollador después.</string> <string name="phantom_fix_no_toggle">Esta versión de Android no tiene un interruptor para ello. Usa la Depuración inalámbrica para cambiarlo desde este dispositivo.</string> <string name="upd_block_no_apk">Esta versión no tiene descarga para esta copia de DroidDeck.</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">Android no la instalará sobre esta versión porque su versionCode %1$d es inferior al %2$d instalado.</string> <string name="upd_fail_downgrade">Android no instala una versión anterior sobre una más reciente. Quédate en esta versión hasta que el canal la alcance.</string> <string name="upd_fail_signature">Esta copia de DroidDeck está firmada de otro modo (¿una compilación local?), así que Android no la actualizará sobre la instalada.</string> <string name="upd_fail_storage">No hay espacio libre suficiente para la actualización.</string> <string name="upd_fail_aborted">Se canceló la actualización.</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">La actualización no se instaló: %1$s</string> <string name="upd_fail_generic">La actualización no se instaló.</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">zip de %1$s</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s aún no se ha iniciado</string> <string name="saves_none_found">No se encontraron partidas guardadas de %1$s</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s no tiene carpeta drive_c, así que no es un zip de partidas de GameHub ni de Winlator</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">no se pueden crear carpetas en %1$s</string> <string name="gstore_cannot_write">no se puede escribir en %1$s</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">no se puede escribir en %1$s (%2$s)</string> <string name="gfiles_folder_unavailable">La carpeta no está disponible</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">Otro archivo ya usa %1$s</string> <string name="gfiles_art_unavailable">La carpeta de imágenes no está disponible</string> <string name="gfiles_unrelated_inventory">La carpeta tiene un inventario de sincronización ajeno</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">No se pudo quitar %1$s</string> <string name="gfiles_outside_folder">El archivo sale de la carpeta de exportación</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX (compilación sin nombre de versión)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">no hay profile.json en %1$s</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s no es un paquete de componentes de Linux (tipo %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · %2$s libres</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">Automático</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">El catálogo de actualizaciones no superó las comprobaciones (%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">Ajuste de procesos secundarios de DroidDeck</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">Sesión iniciada · modo sin conexión</string> <string name="setup_signed_in_unnamed">Sesión iniciada</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">INCLUIDO</string> <string name="driver_tag_downloaded">DESCARGADO</string> <string name="driver_tag_imported">IMPORTADO</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> <string name="display_session">Pantalla de la sesión</string> <string name="display_resolution">Resolución</string> diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1b9c0d2dd..3506af5f1 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1141,16 +1141,12 @@ <string name="game_file_exported">ゲームファイルを %1$s に保存しました</string> <string name="game_frontend_files">フロントエンドファイル</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">実行環境の処理が完了するまでお待ちください</string> <string name="main_wait_proton_install">互換性ツールのインストールが完了するまでお待ちください</string> <string name="main_wait_desktop_install">デスクトップアプリのインストールが完了するまでお待ちください</string> <string name="main_wait_install_game">インストールが完了してからゲームを起動してください</string> <string name="main_saves_close_session">インポートしたデータがゲームに上書き保存されないよう、先にSteamセッションを終了してください</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">%1$sにインポート中</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="other">%2$sから%1$d個のファイルを%3$sにインポートしました</item> </plurals> @@ -1158,15 +1154,11 @@ <item quantity="other">%2$sから%1$d個のファイルを%3$sにインポートしました。以前のセーブデータはDownload/DroidDeck/Saves/backupsにバックアップされました</item> </plurals> <string name="main_saves_pick_zip">%1$sのセーブデータzipを選択</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">%1$sをエクスポート中</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="other">%1$d個のファイルを%2$sとしてエクスポートしました:%3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">%1$sの保存先を選択(%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">%1$sに失敗しました:%2$s</string> <string name="main_no_logs_yet">セッションログはまだありません。先にセッションを実行してください。</string> <string name="main_no_logs_to_clear">消去するセッションログはありません。</string> @@ -1179,68 +1171,55 @@ <string name="main_adb_copied">ADBコマンドをコピーしました</string> <string name="main_pick_roms">ROMフォルダを選択</string> <string name="main_non_adreno_title">Adreno GPUではありません</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">TurnipはAdreno GPUをサポートしています。%1$sでは、Steamの画面が真っ暗になる可能性があります。ダウンロード:%2$.0f MB。</string> <string name="main_install_anyway">インストールする</string> <string name="main_remove_runtime_title">Linux実行環境を削除</string> <string name="main_remove_runtime_text">実行環境、その中のSteamクライアント、そこにインストールされたすべてのゲームが削除されます。</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">適用しました:%1$s</string> <string name="android_app_open_failed_primary">メイン画面で%1$sを開けませんでした</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">%2$sで%1$sを開けませんでした</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">ディスプレイ%2$dで%1$sを開けませんでした</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">インポート中</string> <string name="comp_busy_swapping">置き換え中</string> <string name="comp_busy_restoring">復元中</string> <string name="comp_busy_cancelling">キャンセル中</string> <string name="comp_busy_deleting">削除中</string> <string name="comp_cannot_read">ファイルを読み取れません</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">%1$sをインポートしました</string> <string name="comp_swap_cancelled">待機中の置き換えをキャンセルしました。</string> <string name="comp_pick_wcp">コンポーネントパッケージ(-linux .wcp)を選択</string> <string name="comp_nightlies_unreachable">Nightliesに接続できませんでした</string> - <!-- %1$s is the package version --> <string name="comp_stored">%1$sを保存しました</string> <string name="comp_download_failed">ダウンロードに失敗しました:%1$s</string> <string name="drivers_pick_linux">Linuxランタイムドライバー(-Linux zipまたはAndroid + Linuxバンドル)を選択</string> <string name="drivers_pick_android">ディスプレイドライバー(AdrenoTools zipまたはAndroid + Linuxバンドル)を選択</string> <string name="drivers_pick_any">ドライバーzipまたはAndroid + Linuxバンドルを選択</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s:%2$s</string> <string name="main_removing_named">%1$sを削除中…</string> <string name="main_forgetting_named">%1$sを登録解除中…</string> <string name="main_pick_game_storage">ゲームの保存先フォルダを選択</string> <string name="main_pick_decky_zip">Deckyプラグインのzipを選択</string> <string name="main_storage_not_usable">使用できません:%1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">プログラム</string> <string name="main_removing_runtime">Linux実行環境を削除中</string> <string name="main_settings_main_screen">下部画面で設定を開けなかったため、メイン画面で開きました。</string> <string name="main_open_dev_options">設定で開発者向けオプションを開いてください</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">Androidの「不明なアプリのインストール」設定を開けませんでした</string> <string name="upd_couldnt_check">確認できませんでした:%1$s</string> <string name="upd_no_download">このビルドには、このDroidDeckコピー用のダウンロードがありません</string> <string name="upd_stop_first">実行中のセッションを停止してから更新してください</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">ダウンロード中</string> <string name="upd_stage_installing">インストール中</string> <string name="upd_stage_confirm">確認を待機中</string> <string name="upd_failed">アップデートに失敗しました</string> <string name="upd_not_offered">Androidがアップデートのインストールを表示しませんでした</string> - <!-- Decky --> <string name="decky_install_failed">Deckyのインストールに失敗しました:%1$s</string> <string name="decky_plugin_starting">プラグインのインポートを開始中…</string> <string name="decky_plugin_failed">プラグインのインポートに失敗しました:%1$s</string> <string name="decky_plugin_installed">プラグインをインストールしました。読み込むにはSteamを再起動してください。</string> - <!-- GPU drivers --> <string name="drivers_not_checked">未確認 - 更新をタップして新しいドライバーを検索</string> <string name="drivers_bundle_sets_display">Android + Linux:ディスプレイドライバーも一緒に設定されます</string> <string name="drivers_bundle_sets_runtime">Android + Linux:ランタイムドライバーも一緒に設定されます</string> @@ -1248,16 +1227,13 @@ <string name="drivers_system_vulkan">システムのVulkan:このGPU用の同梱ビルドはありません</string> <string name="drivers_bundled">%1$s(同梱)</string> <string name="drivers_manual">手動</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">現在、片方のみが公開されています</string> <string name="drivers_pair_size">両方で%1$.0f MB</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">設定できるドライバーがありません:%1$s</string> <string name="drivers_checking_latest">最新のドライバーを確認中…</string> <string name="drivers_up_to_date_check_failed">最新です(確認できませんでした:%1$s)</string> <string name="drivers_recommended_unpublished">推奨ドライバーは現在公開されていません</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">%1$s %2$sをダウンロード中…</string> <string name="drivers_updated_to">%1$s %2$sに更新しました。</string> <string name="drivers_updated_to_restart">%1$s %2$sに更新しました。ディスプレイドライバーはDroidDeckの再起動後に有効になります。</string> @@ -1272,9 +1248,7 @@ <string name="drivers_using_bundle_restart">%1$sをランタイムドライバーとディスプレイドライバーの両方として使用中。ディスプレイドライバーはDroidDeckの再起動後に有効になります。</string> <string name="drivers_deleted_bundle">%1$sを削除しました(両方のドライバー)</string> <string name="drivers_deleted">%1$sを削除しました</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">最新:%1$s · 確認 %2$s</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">最新:%1$s · %2$sに接続できません · 確認 %3$s</string> <plurals name="drivers_ago_minutes"> <item quantity="other">%1$d分前</item> @@ -1287,33 +1261,25 @@ </plurals> <string name="drivers_checking_sources">Banners-Turnip、WinNative、DroidDeckを確認中…</string> <string name="drivers_check_failed">確認に失敗</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">%1$sをインストールしました - どちらかの一覧で選択すると両方のドライバーが設定されます</string> <string name="drivers_installed_pick">%1$sをインストールしました - メニューで選択してください</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">実行環境の削除が完了するまでお待ちください</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">ゲーム</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">トラックパッド</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">ダイレクト</string> <string name="session_swap_failed">置き換えに失敗しました:%1$s</string> <string name="session_profile_save_failed">プロファイルの保存に失敗しました:%1$s</string> <string name="session_desktop_paused">デスクトップは一時停止中です</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$sは一時停止中です</string> <string name="session_program_paused">プログラムは一時停止中です</string> <string name="session_starting_desktop">デスクトップを起動中</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">%1$sを起動中</string> <string name="session_starting_program">プログラムを起動中</string> <string name="session_no_logs">このセッションのログはありません。</string> <string name="session_logs_archive_failed">セッションログのアーカイブを作成できませんでした。</string> <string name="session_logs_share_failed">セッションログを共有できませんでした。</string> <string name="session_starting_session">セッションを開始中…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">Linux実行環境をダウンロード中</string> <string name="session_runtime_downloading_mb">Linux実行環境をダウンロード中 · %1$d / %2$d MB</string> <string name="session_runtime_checking">Linux実行環境を確認中</string> @@ -1328,27 +1294,20 @@ <string name="session_proton_unpacking">Proton Experimental(ARM64)を展開中</string> <string name="session_runtime_catalog_unreachable">実行環境のカタログに接続できませんでした。接続を確認して、もう一度Playをタップしてください。</string> <string name="session_runtime_install_failed">Linux実行環境をインストールできませんでした。「設定」で実行環境を確認して、もう一度Playをタップしてください。</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">デスクトップをインストールできませんでした(%1$s)。接続を確認して、もう一度「デスクトップ」をタップしてください。</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$sが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。</string> <string name="session_ended_program">プログラムが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。</string> <string name="session_ended_desktop">デスクトップが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">終了ステータス %1$d · %2$s</string> <string name="session_ended_no_log">ログなし</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="other">ログに「Function not implemented」エラーが%1$d件あります。このデバイスでは、prootのseccomp高速化が補助プロセスの処理に失敗しています。%2$s →「%3$s」(または「%4$s」)を試してから、もう一度起動してください。</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">プラグイン</string> <string name="mode_decky_plugins_hint">次回のSteamセッション用にプラグインのzipをインストールします。</string> <string name="mode_decky_plugin_downloading">ダウンロード中…</string> <string name="mode_decky_plugin_install_zip">zipからインストール</string> - <!-- Store --> <string name="store_cat_games">ゲーム</string> <string name="store_cat_audio_video">オーディオ・ビデオ</string> <string name="store_cat_graphics">グラフィック</string> @@ -1361,21 +1320,14 @@ <string name="store_cat_utilities">ユーティリティ</string> <string name="store_section_popular">人気</string> <string name="store_section_trending">トレンド</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">新着</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">最近の更新</string> <string name="store_section_loading">読み込み中…</string> <string name="store_flatpak_setup">Flatpakのセットアップ</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s:%2$s</string> <string name="store_updates_check_failed">Flathubでアップデートを確認できませんでした</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">インストール済み</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">フォルダ</string> - <!-- Launcher themes --> <string name="theme_graphite">グラファイト</string> <string name="theme_graphite_detail">グラファイト調の画面に、選択・フォーカス・押下時にはロゴのオーブと同じ青を使用します。</string> <string name="theme_paper">ペーパー・オン・ブラック</string> @@ -1384,7 +1336,6 @@ <string name="theme_icon_blue_detail">黒の背景に白の文字、選択・フォーカス・押下時には青を使用します。</string> <string name="theme_electric">エレクトリックネイビー</string> <string name="theme_electric_detail">より明るい青を、グレーではなくネイビー寄りの画面に使用します。</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1402,22 +1353,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">スペース</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">先に実行環境をインストールしてください</string> <string name="game_sync_err_steam_library">Steamライブラリを利用できません</string> <string name="game_sync_err_previous_sd">以前のSDライブラリを利用できません</string> <string name="game_sync_err_game_storage">ゲームストレージを利用できません</string> <string name="game_sync_err_added_games">追加したゲームのフォルダを利用できません</string> <string name="game_sync_err_manifests">Steamのマニフェストを利用できません</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">多数のシステム</string> - <!-- File manager --> <string name="fm_drive_internal">内部</string> <string name="fm_drive_linux">Linux システム</string> <string name="fm_drive_sd">SDカード</string> @@ -1435,7 +1381,6 @@ <string name="fm_no_favorites">お気に入りはまだありません。フォルダの⋮メニューからピン留めすると、すぐにここへ戻れます。</string> <string name="fm_unpin_favorite">お気に入りから削除</string> <string name="fm_size_zero">0バイト</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">バイト</string> <string name="fm_prop_location">場所</string> <string name="fm_prop_type">種類</string> @@ -1443,7 +1388,6 @@ <string name="fm_prop_modified">更新日時</string> <string name="fm_type_folder">フォルダ</string> <string name="fm_type_file">ファイル</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$sファイル</string> <string name="fm_read_only">読み取り専用</string> <string name="fm_read_only_hint">ゲーム(およびWine)がこのファイルを上書き・削除できないようにします。</string> @@ -1457,15 +1401,11 @@ <string name="fm_delete_failed">削除に失敗しました</string> <string name="fm_op_moving">移動中...</string> <string name="fm_op_copying">コピー中...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">移動中 - %1$s</string> <string name="fm_op_copying_item">コピー中 - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">移動中 %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">コピー中 %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d件完了、%2$d件失敗</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d件完了、%2$d件スキップ</string> <plurals name="fm_paste_moved"> <item quantity="other">%1$d個の項目を移動しました</item> @@ -1473,7 +1413,6 @@ <plurals name="fm_paste_copied"> <item quantity="other">%1$d個の項目をコピーしました</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">「%1$s」はすでに存在します</string> <string name="fm_op_renaming">名前を変更中...</string> <string name="fm_rename_failed">名前を変更できませんでした</string> @@ -1484,17 +1423,14 @@ <string name="fm_create">作成</string> <string name="fm_new_name">新しい名前</string> <string name="fm_delete_title">削除しますか?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">「%1$s」を完全に削除しますか?</string> <plurals name="fm_delete_items_title"> <item quantity="other">%1$d個の項目を削除しますか?</item> </plurals> <string name="fm_cannot_undo">この操作は元に戻せません。</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="other">…ほか%1$d件</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">削除中 %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="other">%1$d件を削除できませんでした</item> @@ -1508,7 +1444,6 @@ <string name="fm_conflict_skip">スキップ</string> <string name="fm_select_this_folder">このフォルダを選択</string> <string name="fm_back">戻る</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$sはマウントされていますが、現在読み取れません</string> <string name="fm_list_view">リスト表示</string> <string name="fm_grid_view">グリッド表示</string> @@ -1523,41 +1458,31 @@ <string name="fm_hide_favorites">お気に入りを非表示</string> <string name="fm_show_favorites">お気に入りを表示</string> <string name="fm_filter_folder">このフォルダを絞り込み</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">空き %1$s</string> <string name="fm_selected_count">%1$d個選択中</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">解除</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">すべて</string> <string name="fm_paste">貼り付け</string> <plurals name="fm_items"> <item quantity="other">%1$d個の項目</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">%1$sをここに貼り付け</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">%1$sをここに貼り付け(移動)</string> <string name="fm_rail_title">ファイル</string> <string name="fm_rail_storage">ストレージ</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">クイック</string> <string name="fm_rail_favorites">お気に入り</string> <string name="fm_rail_downloads">ダウンロード</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">DL</string> <string name="fm_rail_roms">ROM</string> <string name="fm_rail_session_logs">セッションログ</string> <string name="fm_rail_pictures">画像</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">「%1$s」をお気に入りに追加しました</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">「%1$s」をお気に入りから削除しました</string> <string name="fm_empty_directory">空のフォルダ</string> <string name="fm_up_one_level">1つ上の階層へ</string> <string name="fm_choose_folder">フォルダを選択</string> <string name="fm_choose_file">ファイルを選択</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steamブルー</string> <string name="ctrl_tint_sky">スカイ</string> <string name="ctrl_tint_white">ホワイト</string> @@ -1566,7 +1491,6 @@ <string name="ctrl_tint_amber">アンバー</string> <string name="ctrl_tint_red">レッド</string> <string name="ctrl_tint_pink">ピンク</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">Aボタン</string> <string name="ctrl_button_b">Bボタン</string> <string name="ctrl_button_x">Xボタン</string> @@ -1577,28 +1501,21 @@ <string name="ctrl_button_rt">右トリガー</string> <string name="ctrl_button_view">ビューボタン</string> <string name="ctrl_button_menu">メニューボタン</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">ビュー</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">メニュー</string> <string name="ctrl_target_up">十字キー上</string> <string name="ctrl_target_down">十字キー下</string> <string name="ctrl_target_left">十字キー左</string> <string name="ctrl_target_right">十字キー右</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">非表示</string> <string name="ctrl_editor_drag">ドラッグして移動</string> <string name="ctrl_editor_snap">スナップ</string> <string name="ctrl_editor_save">保存</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">なし</string> <string name="second_mode_keyboard_trackpad">キーボード + トラックパッド</string> <string name="second_mode_terminal">ターミナル</string> <string name="second_mode_deck_controls">Deckグリップ + トラックパッド</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">Linux実行環境を削除中</string> <plurals name="rtinst_removing_entries"> <item quantity="other">Linux実行環境を削除中 · %1$d項目</item> @@ -1613,63 +1530,47 @@ <string name="rtinst_title_installing">Linux実行環境をインストール中</string> <string name="rtinst_title_removing">Linux実行環境を削除中</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">Linux実行環境がインストールされていません</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">カタログに%1$sのチェックサムがありません</string> <string name="deskpkg_checksum_mismatch">チェックサムが一致しません - 何も変更されていません</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">%1$sをインストール中</string> <string name="deskpkg_place_failed">AppImageを配置できませんでした</string> <string name="deskpkg_extract_failed">展開に失敗しました</string> <string name="deskpkg_install_failed">インストールに失敗しました</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpakのセットアップに失敗しました(終了コード %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpakのセットアップが完了しませんでした</string> <string name="flatpakmgr_busy">別のストアのタスクを実行中です</string> <string name="flatpakmgr_not_ready">先にFlatpakをセットアップしてください</string> <string name="flatpakmgr_starting">開始中</string> <string name="flatpakmgr_exit_status">Flatpakがステータス%1$dで終了しました</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">%1$sをインストール中</string> <string name="flatpakmgr_updating">%1$sを更新中</string> <string name="flatpakmgr_removing">%1$sを削除中</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">%1$sをインストール中(%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">%1$sを更新中(%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">%1$sを削除中(%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">翻訳</string> <string name="flatpakmgr_ref_graphics_drivers">グラフィックドライバー</string> <string name="flatpakmgr_ref_media_codecs">メディアコーデック</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$sランタイム</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$sランタイム %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">ファイルが小さすぎるため、AppImageではありません</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">ファイルを読み取れません:%1$s</string> <string name="appimg_not_linux">AppImageではありません(Linuxプログラムではありません)</string> <string name="appimg_wrong_arch">このAppImageはARM64(aarch64)向けでもx86 PC向けでもありません</string> <string name="appimg_not_appimage">Linuxプログラムですが、AppImageではありません</string> <string name="appimg_wrong_type">インポートできるのはタイプ2のAppImageのみです(このファイルはタイプ%1$d)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">空き容量不足:インポートには約%1$sが必要です</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">空き容量不足:更新には約%1$sが必要です</string> <string name="appimg_import_failed">インポートに失敗しました</string> <string name="appimg_gone">このAppImageは存在しません</string> <string name="appimg_update_failed">更新に失敗しました</string> - <!-- %1$s: file name --> <string name="appimg_extracting">%1$sを展開中</string> <string name="appimg_extract_failed">AppImageを展開できませんでした</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">AppImageを展開できませんでした:%1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">Decky Loaderをインストールする前に、実行中のセッションを停止してください</string> <string name="deckymgr_download_failed">Decky Loaderのダウンロードに失敗しました</string> <string name="deckymgr_size_mismatch">Decky Loaderのサイズがリリース情報と一致しません</string> @@ -1705,20 +1606,13 @@ <string name="deckymgr_remote_entry_invalid">remote_binaryの項目が無効です</string> <string name="deckymgr_remote_name_invalid">リモートバイナリ名が無効です</string> <string name="deckymgr_remote_url_https">リモートバイナリのURLにはHTTPSを使用する必要があります</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">リモートバイナリ%1$sのSHA-256ハッシュが無効です</string> <string name="deckymgr_bin_dir_failed">プラグインのbinフォルダを作成できませんでした</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">プラグインバイナリをダウンロード中 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">プラグインバイナリ%1$sをダウンロードできませんでした</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">プラグインバイナリのチェックサムが一致しません · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">プラグインバイナリを実行可能にできませんでした · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">プラグインバイナリを置き換えられませんでした · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">プラグインバイナリをインストールできませんでした · %1$s</string> <string name="deckymgr_folder_taken">このプラグインフォルダは別のプラグインが使用しています</string> <string name="deckymgr_preserve_failed">既存のプラグインを保持できませんでした</string> @@ -1726,20 +1620,14 @@ <string name="deckymgr_settings_failed">Deckyのプラグイン設定を更新できませんでした</string> <string name="deckymgr_restore_failed">既存のプラグインを復元できませんでした</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">ワイヤレスデバッグの識別情報を保存できませんでした</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADBコマンドに失敗しました(%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Androidは子プロセス制限の変更を確認しませんでした:%1$s</string> <string name="adbfix_no_pairing">ワイヤレスデバッグのペアリング情報が保存されていません</string> <string name="adbfix_enter_address">現在のワイヤレスデバッグのIPアドレスとポートを入力してください</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">ペアリングのポップアップを待機中にタイムアウトしました。DroidDeckからもう一度お試しください。</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">ペアリングのポップアップを検索できませんでした(%1$d)。Wi-Fiがオンになっているか確認してください。</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">ペアリングのポップアップを検索できませんでした:%1$s</string> <string name="adbpair_six_digits">6桁のコードをすべて入力してください</string> <string name="adbpair_pairing">ペアリング中…</string> @@ -1753,27 +1641,22 @@ <string name="adbpair_title_pair">ワイヤレスデバッグのペアリング</string> <string name="adbpair_waiting_text">ワイヤレスデバッグをオンにして、「ペアリングコードでデバイスをペア設定」をタップしてください。</string> <string name="adbpair_waiting_long">ワイヤレスデバッグをオンにして、「ペアリングコードでデバイスをペア設定」をタップしてください。設定画面のままにしてください:コードはこの通知に入力します。</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6桁のペアリングコード</string> <string name="adbpair_code_hint">「コードを入力」をタップし、設定に表示された6桁の数字を入力してください。</string> <string name="adbpair_title_code">Wi-Fiペアリングコードを入力</string> <string name="adbpair_title_retry">コードをもう一度入力</string> - <!-- Notification action button --> <string name="adbpair_enter_code">コードを入力</string> <string name="adbpair_title_working">Steamをセットアップ中</string> <string name="adbpair_title_done">Steamの準備ができました</string> <string name="adbpair_done_text">タップしてDroidDeckに戻ります。ワイヤレスデバッグはオフにしてかまいません。</string> <string name="adbpair_title_failed">ワイヤレスデバッグのセットアップが中断しました</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">ダウンロード先がHTTP %1$dを返しました</string> <string name="selfinst_size_mismatch">ダウンロードしたファイルのサイズが公開されているAPKと一致しないため、破棄しました。もう一度お試しください</string> <string name="selfinst_checksum_mismatch">ダウンロードしたファイルがチェックサムと一致しないため、破棄しました。もう一度お試しください</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">このアップデートはAndroidパッケージ%1$s用で、%2$s用ではありません</string> <string name="selfinst_metadata_signer">アップデート情報にDroidDeckのリリース署名キーが記載されていません</string> <string name="selfinst_unreadable">AndroidがダウンロードしたAPKを読み取れませんでした</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">ダウンロードしたAPKは%1$s用で、%2$s用ではありません</string> <string name="selfinst_version_mismatch">ダウンロードしたAPKのversionCodeは%1$dで、公開されている%2$dと一致しません</string> <string name="selfinst_downgrade">AndroidはインストールされているversionCode %2$dの上に%1$dをインストールしません</string> @@ -1781,16 +1664,11 @@ <string name="selfinst_wrong_signer">ダウンロードしたAPKはDroidDeckのリリースキーで署名されていません</string> <string name="appupd_http">アップデートカタログがHTTP %1$dを返しました</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">セッションを開始中…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">%1$d:%2$02d 経過 · 処理中</string> <string name="loadstate_steam_update">Steamクライアントのアップデートをダウンロード中 · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">Steamクライアントをダウンロード中 · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">互換性ツールのインストールはすでに実行中です</string> <string name="pextra_stop_session">互換性ツールをインストールする前に、実行中のセッションを停止してください</string> <string name="pextra_no_release">%1$sのARM64リリースが見つかりませんでした</string> @@ -1807,48 +1685,38 @@ <string name="pextra_checksum_mismatch">チェックサムが一致しないため、ダウンロードを削除しました</string> <string name="pextra_session_started">ダウンロード中にセッションが開始されました。停止してから互換性ツールをインストールしてください</string> <string name="pextra_installing">%1$sをインストール中</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$sのインストールに失敗しました(終了コード %2$d)</string> <string name="pextra_could_not_install">%1$sをインストールできませんでした</string> <string name="esync_fetching">droiddeck-esyncパックを取得中</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">Linux補助プログラム付き</string> <string name="cmgr_fex_dlls_only">DLLのみ</string> <string name="cmgr_original_proton_updated">元(前回の置き換え以降にProtonが更新されました)</string> <string name="cmgr_changed_outside">「コンポーネント」以外で変更されました</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">なし</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">元 %1$s</string> <string name="cmgr_original_from">%1$sの元のファイル</string> <string name="cmgr_swap_queued">%3$sで実行中のゲームが終了すると、%1$s %2$sがそこに入ります。</string> <string name="cmgr_swap_done">%1$s %2$sが%3$sに入りました。次回のゲーム起動時に有効になります。</string> <string name="cmgr_original_missing">その元のバンドルは保存されていません</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%2$sで実行中のゲームが終了すると、その%1$sが元に戻ります。</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">%2$sの%1$sを%3$sの元のファイルから復元しました。</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$sは%2$sで使用中です。先にそのProtonを別のものに置き換えてください。</string> <string name="cmgr_package_not_stored">%1$sは保存されていません</string> <string name="cmgr_deleted">%1$sを削除しました。</string> <string name="cmgr_original_kept">インストール済みビルドの元のファイルは、元に戻すために保持されます。</string> <string name="cmgr_not_stored">保存されていません</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">%2$sの%1$s版の元のファイルを削除しました。</string> <string name="cmgr_nightlies_only">ダウンロードはNightliesのリリースからのみ行われます</string> <string name="cmgr_list_predates_checksums">このパッケージ一覧はチェックサム導入前のものです。更新してからもう一度お試しください</string> <string name="cmgr_checksum_mismatch">チェックサムが一致しません</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">リニア</string> <string name="sprefs_upscaler_nearest">ニアレスト</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR(品質)</string> <string name="sprefs_upscaler_sharpen">シャープのみ</string> <string name="sprefs_lod_bias_auto">自動(スケーリングに合わせる)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">何も適用されません。</string> <string name="look_game_clarity">ゲームクラリティ</string> <string name="look_game_clarity_desc">シャープ化に少しコントラストを加えた、普段使い向け。</string> @@ -1873,27 +1741,23 @@ <string name="look_anime_edge">アニメエッジ</string> <string name="look_anime_edge_desc">トゥーン輪郭線とシャープ化。</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">Adreno GPUではありません</string> <string name="gpuinfo_supported">サポート対象</string> <string name="gpuinfo_experimental">実験的:ドライバーはテストビルドです</string> <string name="gpuinfo_below_tested">テスト済みハードウェア(Adreno 725以降)未満:動作しない可能性があります</string> <string name="gpuinfo_unsupported">サポート対象外:DroidDeckにはAdreno(Snapdragon)GPUが必要です</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">GitHubのレート制限に達しました。後でもう一度お試しください</string> <string name="turnip_github_http">GitHubがHTTP %1$dを返しました</string> <string name="turnip_download_http">ダウンロード先がHTTP %1$dを返しました</string> <string name="turnip_list_predates_checksums">このドライバー一覧はチェックサム導入前のものです。更新してからもう一度お試しください</string> <string name="turnip_checksum_mismatch">チェックサムが一致しないため、ダウンロードを破棄しました</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">AdrenoToolsドライバーではありません:%1$s</string> <string name="turnip_reject_no_meta">meta.jsonがありません</string> <string name="turnip_reject_meta_unreadable">meta.jsonを読み取れません(%1$s)</string> <string name="turnip_reject_linux_driver">これはLinuxランタイムドライバーです(「ランタイムドライバー」でインポートしてください)</string> <string name="turnip_reject_wayland">これはWaylandゲーム用ドライバーで、このアプリでは使用しません</string> <string name="turnip_reject_no_library_name">meta.jsonにlibraryNameが指定されていないため、AdrenoToolsに渡せるものがありません</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.jsonで指定された%1$sがzipに含まれていません</string> <string name="turnip_reject_not_aarch64">%1$sは64ビットAArch64 ELF共有ライブラリではありません</string> <string name="turnip_reject_glibc">%1$sはglibcにリンクしています。これは「-Linux」Turnipなので、「ランタイムドライバー」でインポートしてください</string> @@ -1904,90 +1768,61 @@ <string name="bundle_entry_outside">ドライバーバンドルではありません:%1$sがバンドルの外を指しています</string> <string name="bundle_too_large">ドライバーバンドルではありません:展開後のサイズが%1$d MBを超えます</string> - <!-- Game storage --> <string name="gstore_sd_card">SDカード</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">フォルダ</string> <string name="logshare_chooser">セッションログを共有</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">Android 10以降が必要です(色空間付きのディスプレイレイヤー)</string> <string name="hdr_none">このディスプレイはHDRに対応していません</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">このディスプレイは%1$sに対応していますが、HDR10には対応していません</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">このGPU</string> <string name="phantom_title_on">子プロセス制限がオンです</string> <string name="phantom_title_unset">子プロセス制限が未設定です</string> <string name="phantom_title_unreadable">子プロセス制限を確認できませんでした</string> <string name="phantom_title_off">子プロセス制限はオフです</string> <string name="phantom_title_not_required">子プロセス制限の変更は不要です</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s それができない場合は、このデバイスをADBが使えるPCに接続し、以下のコマンドを実行してください。その後ここに戻って再確認してください。</string> <string name="phantom_instructions_off">Steamセッションを開始できます。</string> <string name="phantom_instructions_not_applicable">このAndroidバージョンではこの制限は使用されません。</string> <string name="phantom_fix_toggle">開発者向けオプションで「子プロセス制限を無効にする」をオンにし、その後も開発者向けオプションをオンのままにしてください。</string> <string name="phantom_fix_no_toggle">このAndroidバージョンにはこのスイッチがありません。ワイヤレスデバッグを使って、このデバイスから変更してください。</string> <string name="upd_block_no_apk">このビルドには、このDroidDeckコピー用のダウンロードがありません。</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">このビルドのversionCode %1$dはインストール済みの%2$dより小さいため、Androidは上書きインストールしません。</string> <string name="upd_fail_downgrade">Androidは新しいバージョンの上に古いバージョンをインストールしません。チャンネルが追いつくまで、このビルドを使い続けてください。</string> <string name="upd_fail_signature">このDroidDeckコピーは署名が異なる(ローカルビルド?)ため、Androidは上書き更新しません。</string> <string name="upd_fail_storage">アップデートに必要な空き容量が不足しています。</string> <string name="upd_fail_aborted">アップデートはキャンセルされました。</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">アップデートをインストールできませんでした:%1$s</string> <string name="upd_fail_generic">アップデートをインストールできませんでした。</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s zip</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$sはまだ起動されていません</string> <string name="saves_none_found">%1$sのセーブファイルが見つかりません</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$sにはdrive_cフォルダがないため、GameHubまたはWinlatorのセーブデータzipではありません</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">%1$sにフォルダを作成できません</string> <string name="gstore_cannot_write">%1$sに書き込めません</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">%1$sに書き込めません(%2$s)</string> <string name="gfiles_folder_unavailable">フォルダを利用できません</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">%1$sは別のファイルがすでに使用しています</string> <string name="gfiles_art_unavailable">画像フォルダを利用できません</string> <string name="gfiles_unrelated_inventory">フォルダに無関係な同期情報があります</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">%1$sを削除できませんでした</string> <string name="gfiles_outside_folder">ファイルがエクスポートフォルダの外を指しています</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX(バージョン名のないビルド)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">%1$sにprofile.jsonがありません</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$sはLinuxコンポーネントパッケージではありません(タイプ %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · 空き %2$s</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">自動</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">アップデートカタログの検証に失敗しました(%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeckの子プロセス設定</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">ログイン中 · オフラインモード</string> <string name="setup_signed_in_unnamed">ログイン中</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">同梱</string> <string name="driver_tag_downloaded">ダウンロード</string> <string name="driver_tag_imported">インポート</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s。%2$s\n\n%3$s</string> <string name="display_session">セッションの表示</string> <string name="display_resolution">解像度</string> diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f0672279d..f47ca0874 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1141,16 +1141,12 @@ <string name="game_file_exported">게임 파일이 %1$s에 저장되었습니다</string> <string name="game_frontend_files">프론트엔드 파일</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">런타임 작업이 끝날 때까지 기다려 주세요</string> <string name="main_wait_proton_install">호환성 도구 설치가 끝날 때까지 기다려 주세요</string> <string name="main_wait_desktop_install">데스크톱 앱 설치가 끝날 때까지 기다려 주세요</string> <string name="main_wait_install_game">설치가 끝난 후 게임을 실행하세요</string> <string name="main_saves_close_session">게임이 가져온 데이터를 덮어쓰지 않도록 먼저 Steam 세션을 종료하세요</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">%1$s(으)로 가져오는 중</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="other">%2$s에서 %3$s(으)로 파일 %1$d개를 가져왔습니다</item> </plurals> @@ -1158,15 +1154,11 @@ <item quantity="other">%2$s에서 %3$s(으)로 파일 %1$d개를 가져왔습니다. 이전 저장 데이터는 Download/DroidDeck/Saves/backups에 백업되었습니다</item> </plurals> <string name="main_saves_pick_zip">%1$s의 저장 데이터 zip 선택</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">%1$s 내보내는 중</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="other">파일 %1$d개를 %2$s(으)로 내보냈습니다: %3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">%1$s을(를) 저장할 위치 선택(%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">%1$s 실패: %2$s</string> <string name="main_no_logs_yet">아직 세션 로그가 없습니다. 먼저 세션을 실행하세요.</string> <string name="main_no_logs_to_clear">지울 세션 로그가 없습니다.</string> @@ -1179,68 +1171,55 @@ <string name="main_adb_copied">ADB 명령이 복사되었습니다</string> <string name="main_pick_roms">ROM 폴더 선택</string> <string name="main_non_adreno_title">Adreno GPU가 아님</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip은 Adreno GPU를 지원합니다. %1$s에서는 Steam 화면이 검게 표시될 수 있습니다. 다운로드: %2$.0f MB.</string> <string name="main_install_anyway">그래도 설치</string> <string name="main_remove_runtime_title">Linux 런타임 제거</string> <string name="main_remove_runtime_text">런타임과 그 안의 Steam 클라이언트, 그리고 런타임에 설치된 모든 게임이 삭제됩니다.</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">적용됨: %1$s</string> <string name="android_app_open_failed_primary">메인 화면에서 %1$s을(를) 열 수 없습니다</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">%2$s에서 %1$s을(를) 열 수 없습니다</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">디스플레이 %2$d에서 %1$s을(를) 열 수 없습니다</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">가져오는 중</string> <string name="comp_busy_swapping">교체하는 중</string> <string name="comp_busy_restoring">복원하는 중</string> <string name="comp_busy_cancelling">취소하는 중</string> <string name="comp_busy_deleting">삭제하는 중</string> <string name="comp_cannot_read">파일을 읽을 수 없음</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">%1$s 가져옴</string> <string name="comp_swap_cancelled">대기 중이던 교체가 취소되었습니다.</string> <string name="comp_pick_wcp">구성 요소 패키지 선택(-linux .wcp)</string> <string name="comp_nightlies_unreachable">Nightlies에 연결할 수 없습니다</string> - <!-- %1$s is the package version --> <string name="comp_stored">%1$s 저장됨</string> <string name="comp_download_failed">다운로드 실패: %1$s</string> <string name="drivers_pick_linux">Linux 런타임 드라이버 선택(-Linux zip 또는 Android + Linux 번들)</string> <string name="drivers_pick_android">디스플레이 드라이버 선택(AdrenoTools zip 또는 Android + Linux 번들)</string> <string name="drivers_pick_any">드라이버 zip 또는 Android + Linux 번들 선택</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s: %2$s</string> <string name="main_removing_named">%1$s 제거 중…</string> <string name="main_forgetting_named">%1$s 잊는 중…</string> <string name="main_pick_game_storage">게임 저장 폴더 선택</string> <string name="main_pick_decky_zip">Decky 플러그인 ZIP 선택</string> <string name="main_storage_not_usable">사용할 수 없음: %1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">프로그램</string> <string name="main_removing_runtime">Linux 런타임 제거 중</string> <string name="main_settings_main_screen">하단 화면에서 설정을 열 수 없어 메인 화면에서 열었습니다.</string> <string name="main_open_dev_options">설정에서 개발자 옵션을 여세요</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">Android의 \"출처를 알 수 없는 앱 설치\" 설정을 열 수 없습니다</string> <string name="upd_couldnt_check">확인할 수 없음: %1$s</string> <string name="upd_no_download">이 빌드에는 이 DroidDeck 복사본용 다운로드가 없습니다</string> <string name="upd_stop_first">실행 중인 세션을 먼저 중지한 후 업데이트하세요</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">다운로드 중</string> <string name="upd_stage_installing">설치 중</string> <string name="upd_stage_confirm">확인 대기 중</string> <string name="upd_failed">업데이트 실패</string> <string name="upd_not_offered">Android에서 업데이트 설치를 진행하지 않았습니다</string> - <!-- Decky --> <string name="decky_install_failed">Decky 설치 실패: %1$s</string> <string name="decky_plugin_starting">플러그인 가져오기 시작 중…</string> <string name="decky_plugin_failed">플러그인 가져오기 실패: %1$s</string> <string name="decky_plugin_installed">플러그인이 설치되었습니다. 불러오려면 Steam을 다시 시작하세요.</string> - <!-- GPU drivers --> <string name="drivers_not_checked">아직 확인하지 않음 - 새로 고침을 탭하여 새 드라이버를 찾으세요</string> <string name="drivers_bundle_sets_display">Android + Linux: 디스플레이 드라이버도 함께 설정</string> <string name="drivers_bundle_sets_runtime">Android + Linux: 런타임 드라이버도 함께 설정</string> @@ -1248,16 +1227,13 @@ <string name="drivers_system_vulkan">시스템 Vulkan: 이 GPU용 내장 빌드 없음</string> <string name="drivers_bundled">%1$s(내장)</string> <string name="drivers_manual">수동</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">현재 한쪽만 배포되어 있습니다</string> <string name="drivers_pair_size">둘 다 %1$.0f MB</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">설정할 드라이버 없음: %1$s</string> <string name="drivers_checking_latest">최신 드라이버 확인 중…</string> <string name="drivers_up_to_date_check_failed">최신 버전(확인 실패: %1$s)</string> <string name="drivers_recommended_unpublished">현재 권장 드라이버가 배포되지 않았습니다</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">%1$s %2$s 다운로드 중…</string> <string name="drivers_updated_to">%1$s %2$s(으)로 업데이트했습니다.</string> <string name="drivers_updated_to_restart">%1$s %2$s(으)로 업데이트했습니다. 디스플레이 드라이버는 DroidDeck을 다시 시작한 후 적용됩니다.</string> @@ -1272,9 +1248,7 @@ <string name="drivers_using_bundle_restart">%1$s을(를) 런타임 및 디스플레이 드라이버로 사용 중. 디스플레이 드라이버는 DroidDeck을 다시 시작한 후 적용됩니다.</string> <string name="drivers_deleted_bundle">%1$s 삭제됨(드라이버 2개)</string> <string name="drivers_deleted">%1$s 삭제됨</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">최신: %1$s · %2$s 확인</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">최신: %1$s · %2$s 연결 불가 · %3$s 확인</string> <plurals name="drivers_ago_minutes"> <item quantity="other">%1$d분 전</item> @@ -1287,33 +1261,25 @@ </plurals> <string name="drivers_checking_sources">Banners-Turnip, WinNative 및 DroidDeck 확인 중…</string> <string name="drivers_check_failed">확인 실패</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">%1$s 설치됨 - 두 목록 중 어디에서든 선택하면 두 드라이버가 모두 설정됩니다</string> <string name="drivers_installed_pick">%1$s 설치됨 - 메뉴에서 선택하세요</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">런타임 제거가 끝날 때까지 기다려 주세요</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">게임</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">터치패드</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">직접 터치</string> <string name="session_swap_failed">교체 실패: %1$s</string> <string name="session_profile_save_failed">프로필 저장 실패: %1$s</string> <string name="session_desktop_paused">데스크톱이 일시 중지됨</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s 일시 중지됨</string> <string name="session_program_paused">프로그램이 일시 중지됨</string> <string name="session_starting_desktop">데스크톱 시작 중</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">%1$s 시작 중</string> <string name="session_starting_program">프로그램 시작 중</string> <string name="session_no_logs">이 세션의 로그가 없습니다.</string> <string name="session_logs_archive_failed">세션 로그 압축 파일을 만들 수 없습니다.</string> <string name="session_logs_share_failed">세션 로그를 공유할 수 없습니다.</string> <string name="session_starting_session">세션 시작 중…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">Linux 런타임 다운로드 중</string> <string name="session_runtime_downloading_mb">Linux 런타임 다운로드 중 · %1$d/%2$d MB</string> <string name="session_runtime_checking">Linux 런타임 확인 중</string> @@ -1328,27 +1294,20 @@ <string name="session_proton_unpacking">Proton Experimental(ARM64) 압축 해제 중</string> <string name="session_runtime_catalog_unreachable">런타임 카탈로그에 연결할 수 없습니다. 연결을 확인한 후 \"Steam 실행\"을 다시 누르세요.</string> <string name="session_runtime_install_failed">Linux 런타임이 설치되지 않았습니다. \"설정\"에서 런타임을 확인한 후 \"Steam 실행\"을 다시 누르세요.</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">데스크톱이 설치되지 않았습니다(%1$s). 연결을 확인한 후 \"데스크톱\"을 다시 누르세요.</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s이(가) 예기치 않게 중지되었습니다. 버그 리포트와 함께 로그를 공유하거나 다시 시도하세요.</string> <string name="session_ended_program">프로그램이 예기치 않게 중지되었습니다. 버그 리포트와 함께 로그를 공유하거나 다시 시도하세요.</string> <string name="session_ended_desktop">데스크톱이 예기치 않게 중지되었습니다. 버그 리포트와 함께 로그를 공유하거나 다시 시도하세요.</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">종료 상태 %1$d · %2$s</string> <string name="session_ended_no_log">로그 없음</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="other">로그에 \"Function not implemented\" 오류가 %1$d개 있습니다. 이 기기에서 proot의 seccomp 가속이 보조 프로세스를 실패시키고 있습니다. %2$s → \"%3$s\"(또는 \"%4$s\")를 시도한 후 다시 시작하세요.</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">플러그인</string> <string name="mode_decky_plugins_hint">다음 Steam 세션에 사용할 플러그인 ZIP을 설치합니다.</string> <string name="mode_decky_plugin_downloading">다운로드 중…</string> <string name="mode_decky_plugin_install_zip">ZIP에서 설치</string> - <!-- Store --> <string name="store_cat_games">게임</string> <string name="store_cat_audio_video">오디오 및 비디오</string> <string name="store_cat_graphics">그래픽</string> @@ -1361,21 +1320,14 @@ <string name="store_cat_utilities">유틸리티</string> <string name="store_section_popular">인기</string> <string name="store_section_trending">급상승</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">신규</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">업데이트됨</string> <string name="store_section_loading">불러오는 중…</string> <string name="store_flatpak_setup">Flatpak 설정</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s: %2$s</string> <string name="store_updates_check_failed">Flathub에서 업데이트를 확인할 수 없습니다</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">설치됨</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">폴더</string> - <!-- Launcher themes --> <string name="theme_graphite">그래파이트</string> <string name="theme_graphite_detail">그래파이트 색상의 화면에, 선택·포커스·누름 상태에는 로고의 오브 블루를 사용합니다.</string> <string name="theme_paper">블랙 앤 화이트</string> @@ -1384,7 +1336,6 @@ <string name="theme_icon_blue_detail">검은색 배경에 흰색 글자, 선택·포커스·누름 상태에는 파란색을 사용합니다.</string> <string name="theme_electric">일렉트릭 네이비</string> <string name="theme_electric_detail">회색 대신 네이비 톤의 화면에 더 밝은 파란색을 사용합니다.</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1402,22 +1353,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">Space</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">먼저 런타임을 설치하세요</string> <string name="game_sync_err_steam_library">Steam 라이브러리를 사용할 수 없습니다</string> <string name="game_sync_err_previous_sd">이전 SD 라이브러리를 사용할 수 없습니다</string> <string name="game_sync_err_game_storage">게임 저장 공간을 사용할 수 없습니다</string> <string name="game_sync_err_added_games">추가된 게임 폴더를 사용할 수 없습니다</string> <string name="game_sync_err_manifests">Steam 매니페스트를 사용할 수 없습니다</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">다양한 기종</string> - <!-- File manager --> <string name="fm_drive_internal">내부</string> <string name="fm_drive_linux">Linux 시스템</string> <string name="fm_drive_sd">SD 카드</string> @@ -1435,7 +1381,6 @@ <string name="fm_no_favorites">즐겨찾기가 없습니다. 폴더의 ⋮ 메뉴에서 고정하면 여기로 빠르게 이동할 수 있습니다.</string> <string name="fm_unpin_favorite">즐겨찾기에서 제거</string> <string name="fm_size_zero">0바이트</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">바이트</string> <string name="fm_prop_location">위치</string> <string name="fm_prop_type">유형</string> @@ -1443,7 +1388,6 @@ <string name="fm_prop_modified">수정한 날짜</string> <string name="fm_type_folder">폴더</string> <string name="fm_type_file">파일</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$s 파일</string> <string name="fm_read_only">읽기 전용</string> <string name="fm_read_only_hint">게임(및 Wine)이 이 파일을 덮어쓰거나 삭제하지 못하게 합니다.</string> @@ -1457,15 +1401,11 @@ <string name="fm_delete_failed">삭제 실패</string> <string name="fm_op_moving">이동 중...</string> <string name="fm_op_copying">복사 중...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">이동 중 - %1$s</string> <string name="fm_op_copying_item">복사 중 - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">이동 중 %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">복사 중 %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d개 완료, %2$d개 실패</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d개 완료, %2$d개 건너뜀</string> <plurals name="fm_paste_moved"> <item quantity="other">항목 %1$d개 이동됨</item> @@ -1473,7 +1413,6 @@ <plurals name="fm_paste_copied"> <item quantity="other">항목 %1$d개 복사됨</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">\"%1$s\"이(가) 이미 있습니다</string> <string name="fm_op_renaming">이름 바꾸는 중...</string> <string name="fm_rename_failed">이름 바꾸기 실패</string> @@ -1484,17 +1423,14 @@ <string name="fm_create">만들기</string> <string name="fm_new_name">새 이름</string> <string name="fm_delete_title">삭제하시겠습니까?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">\"%1$s\"을(를) 영구적으로 삭제하시겠습니까?</string> <plurals name="fm_delete_items_title"> <item quantity="other">항목 %1$d개를 삭제하시겠습니까?</item> </plurals> <string name="fm_cannot_undo">이 작업은 되돌릴 수 없습니다.</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="other">…외 %1$d개</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">삭제 중 %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="other">%1$d개를 삭제할 수 없습니다</item> @@ -1508,7 +1444,6 @@ <string name="fm_conflict_skip">건너뛰기</string> <string name="fm_select_this_folder">이 폴더 선택</string> <string name="fm_back">뒤로</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s이(가) 마운트되었지만 지금은 읽을 수 없습니다</string> <string name="fm_list_view">목록 보기</string> <string name="fm_grid_view">격자 보기</string> @@ -1523,41 +1458,31 @@ <string name="fm_hide_favorites">즐겨찾기 숨기기</string> <string name="fm_show_favorites">즐겨찾기 표시</string> <string name="fm_filter_folder">이 폴더에서 필터</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">%1$s 사용 가능</string> <string name="fm_selected_count">%1$d개 선택됨</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">선택 해제</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">전체</string> <string name="fm_paste">붙여넣기</string> <plurals name="fm_items"> <item quantity="other">항목 %1$d개</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">여기에 %1$s 붙여넣기</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">여기에 %1$s 붙여넣기(이동)</string> <string name="fm_rail_title">파일</string> <string name="fm_rail_storage">저장소</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">바로 가기</string> <string name="fm_rail_favorites">즐겨찾기</string> <string name="fm_rail_downloads">다운로드</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">다운로드</string> <string name="fm_rail_roms">ROMs</string> <string name="fm_rail_session_logs">세션 로그</string> <string name="fm_rail_pictures">사진</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">\"%1$s\"을(를) 즐겨찾기에 추가했습니다</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">\"%1$s\"을(를) 즐겨찾기에서 제거했습니다</string> <string name="fm_empty_directory">빈 폴더</string> <string name="fm_up_one_level">상위 폴더로</string> <string name="fm_choose_folder">폴더 선택</string> <string name="fm_choose_file">파일 선택</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steam 블루</string> <string name="ctrl_tint_sky">하늘색</string> <string name="ctrl_tint_white">흰색</string> @@ -1566,7 +1491,6 @@ <string name="ctrl_tint_amber">호박색</string> <string name="ctrl_tint_red">빨간색</string> <string name="ctrl_tint_pink">분홍색</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">A 버튼</string> <string name="ctrl_button_b">B 버튼</string> <string name="ctrl_button_x">X 버튼</string> @@ -1577,28 +1501,21 @@ <string name="ctrl_button_rt">오른쪽 트리거</string> <string name="ctrl_button_view">보기 버튼</string> <string name="ctrl_button_menu">메뉴 버튼</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">보기</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">메뉴</string> <string name="ctrl_target_up">방향 패드 위</string> <string name="ctrl_target_down">방향 패드 아래</string> <string name="ctrl_target_left">방향 패드 왼쪽</string> <string name="ctrl_target_right">방향 패드 오른쪽</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">숨김</string> <string name="ctrl_editor_drag">드래그하여 이동</string> <string name="ctrl_editor_snap">맞춤</string> <string name="ctrl_editor_save">저장</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">없음</string> <string name="second_mode_keyboard_trackpad">키보드 + 터치패드</string> <string name="second_mode_terminal">터미널</string> <string name="second_mode_deck_controls">Deck 그립 + 터치패드</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">Linux 런타임 제거 중</string> <plurals name="rtinst_removing_entries"> <item quantity="other">Linux 런타임 제거 중 · 항목 %1$d개</item> @@ -1613,63 +1530,47 @@ <string name="rtinst_title_installing">Linux 런타임 설치 중</string> <string name="rtinst_title_removing">Linux 런타임 제거 중</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">Linux 런타임이 설치되지 않았습니다</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">카탈로그에 %1$s의 체크섬이 없습니다</string> <string name="deskpkg_checksum_mismatch">체크섬이 일치하지 않습니다. 변경된 내용은 없습니다</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">%1$s 설치 중</string> <string name="deskpkg_place_failed">AppImage를 배치할 수 없습니다</string> <string name="deskpkg_extract_failed">압축 해제 실패</string> <string name="deskpkg_install_failed">설치 실패</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpak 설정 실패(종료 코드 %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpak 설정이 완료되지 않았습니다</string> <string name="flatpakmgr_busy">다른 스토어 작업이 진행 중입니다</string> <string name="flatpakmgr_not_ready">먼저 Flatpak을 설정하세요</string> <string name="flatpakmgr_starting">시작 중</string> <string name="flatpakmgr_exit_status">Flatpak이 종료되었습니다(상태 %1$d)</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">%1$s 설치 중</string> <string name="flatpakmgr_updating">%1$s 업데이트 중</string> <string name="flatpakmgr_removing">%1$s 제거 중</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">%1$s 설치 중(%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">%1$s 업데이트 중(%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">%1$s 제거 중(%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">번역</string> <string name="flatpakmgr_ref_graphics_drivers">그래픽 드라이버</string> <string name="flatpakmgr_ref_media_codecs">미디어 코덱</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$s 런타임</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$s 런타임 %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">파일이 너무 작아 AppImage가 아닙니다</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">파일을 읽을 수 없습니다: %1$s</string> <string name="appimg_not_linux">AppImage가 아닙니다(Linux 프로그램이 아님)</string> <string name="appimg_wrong_arch">이 AppImage는 ARM64(aarch64)용도, x86 PC용도 아닙니다</string> <string name="appimg_not_appimage">Linux 프로그램이지만 AppImage가 아닙니다</string> <string name="appimg_wrong_type">유형 2 AppImage만 가져올 수 있습니다(이 파일은 유형 %1$d)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">사용 가능한 공간 부족: 가져오려면 약 %1$s이(가) 필요합니다</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">사용 가능한 공간 부족: 업데이트하려면 약 %1$s이(가) 필요합니다</string> <string name="appimg_import_failed">가져오기 실패</string> <string name="appimg_gone">AppImage가 더 이상 존재하지 않습니다</string> <string name="appimg_update_failed">업데이트 실패</string> - <!-- %1$s: file name --> <string name="appimg_extracting">%1$s 압축 해제 중</string> <string name="appimg_extract_failed">AppImage의 압축을 풀 수 없습니다</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">AppImage의 압축을 풀 수 없습니다: %1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">Decky Loader를 설치하기 전에 실행 중인 세션을 중지하세요</string> <string name="deckymgr_download_failed">Decky Loader 다운로드 실패</string> <string name="deckymgr_size_mismatch">Decky Loader 크기가 릴리스 정보와 일치하지 않습니다</string> @@ -1705,20 +1606,13 @@ <string name="deckymgr_remote_entry_invalid">잘못된 remote_binary 항목</string> <string name="deckymgr_remote_name_invalid">잘못된 원격 바이너리 이름</string> <string name="deckymgr_remote_url_https">원격 바이너리 URL은 HTTPS를 사용해야 합니다</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">원격 바이너리 %1$s의 SHA-256 해시가 잘못되었습니다</string> <string name="deckymgr_bin_dir_failed">플러그인 bin 폴더를 만들 수 없습니다</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">플러그인 바이너리 다운로드 중 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">플러그인 바이너리 %1$s을(를) 다운로드할 수 없습니다</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">플러그인 바이너리 체크섬 불일치 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">플러그인 바이너리를 실행 가능으로 설정할 수 없습니다 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">플러그인 바이너리를 교체할 수 없습니다 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">플러그인 바이너리를 설치할 수 없습니다 · %1$s</string> <string name="deckymgr_folder_taken">플러그인 폴더를 다른 플러그인이 사용하고 있습니다</string> <string name="deckymgr_preserve_failed">기존 플러그인을 보존할 수 없습니다</string> @@ -1726,20 +1620,14 @@ <string name="deckymgr_settings_failed">Decky 플러그인 설정을 업데이트할 수 없습니다</string> <string name="deckymgr_restore_failed">기존 플러그인을 복원할 수 없습니다</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">무선 디버깅 ID를 저장할 수 없습니다</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADB 명령 실패(%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android에서 하위 프로세스 제한 변경을 확인하지 않았습니다: %1$s</string> <string name="adbfix_no_pairing">저장된 무선 디버깅 페어링이 없습니다</string> <string name="adbfix_enter_address">현재 무선 디버깅의 IP 주소 및 포트를 입력하세요</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">페어링 팝업을 기다리는 시간이 초과되었습니다. DroidDeck에서 다시 시도하세요.</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">페어링 팝업을 찾을 수 없습니다(%1$d). Wi-Fi가 켜져 있는지 확인하세요.</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">페어링 팝업을 찾을 수 없습니다: %1$s</string> <string name="adbpair_six_digits">코드 6자리를 모두 입력하세요</string> <string name="adbpair_pairing">페어링 중…</string> @@ -1753,27 +1641,22 @@ <string name="adbpair_title_pair">무선 디버깅 페어링</string> <string name="adbpair_waiting_text">무선 디버깅을 켠 다음 “페어링 코드로 기기 페어링”을 탭하세요.</string> <string name="adbpair_waiting_long">무선 디버깅을 켠 다음 “페어링 코드로 기기 페어링”을 탭하세요. 코드는 이 알림에 입력하므로 설정 화면에 머물러 있으세요.</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6자리 페어링 코드</string> <string name="adbpair_code_hint">\"코드 입력\"을 탭하고 설정에 표시된 6자리 숫자를 입력하세요.</string> <string name="adbpair_title_code">Wi-Fi 페어링 코드 입력</string> <string name="adbpair_title_retry">코드를 다시 입력하세요</string> - <!-- Notification action button --> <string name="adbpair_enter_code">코드 입력</string> <string name="adbpair_title_working">Steam 설정 중</string> <string name="adbpair_title_done">Steam 준비 완료</string> <string name="adbpair_done_text">탭하여 DroidDeck으로 돌아가세요. 이제 무선 디버깅을 꺼도 됩니다.</string> <string name="adbpair_title_failed">무선 디버깅 설정이 중단되었습니다</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">다운로드 응답: HTTP %1$d</string> <string name="selfinst_size_mismatch">다운로드 크기가 배포된 APK와 일치하지 않아 삭제했습니다. 다시 시도하세요</string> <string name="selfinst_checksum_mismatch">다운로드한 파일이 체크섬과 일치하지 않아 삭제했습니다. 다시 시도하세요</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">이 업데이트는 %2$s이(가) 아닌 Android 패키지 %1$s용입니다</string> <string name="selfinst_metadata_signer">업데이트 메타데이터에 DroidDeck 릴리스 서명 키가 지정되어 있지 않습니다</string> <string name="selfinst_unreadable">Android에서 다운로드한 APK를 읽을 수 없습니다</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">다운로드한 APK는 %2$s이(가) 아닌 %1$s용입니다</string> <string name="selfinst_version_mismatch">다운로드한 APK의 versionCode가 배포된 %2$d이(가) 아닌 %1$d입니다</string> <string name="selfinst_downgrade">Android에서 설치된 versionCode %2$d 위에 %1$d을(를) 설치하지 않습니다</string> @@ -1781,16 +1664,11 @@ <string name="selfinst_wrong_signer">다운로드한 APK가 DroidDeck 릴리스 키로 서명되지 않았습니다</string> <string name="appupd_http">업데이트 카탈로그 응답: HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">세션 시작 중…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">%1$d:%2$02d 경과 · 아직 진행 중</string> <string name="loadstate_steam_update">Steam 클라이언트 업데이트 다운로드 중 · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">Steam 클라이언트 다운로드 중 · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">호환성 도구를 이미 설치하고 있습니다</string> <string name="pextra_stop_session">호환성 도구를 설치하기 전에 실행 중인 세션을 중지하세요</string> <string name="pextra_no_release">%1$s의 ARM64 릴리스를 찾을 수 없습니다</string> @@ -1807,48 +1685,38 @@ <string name="pextra_checksum_mismatch">체크섬이 일치하지 않아 다운로드를 삭제했습니다</string> <string name="pextra_session_started">다운로드 중에 세션이 시작되었습니다. 호환성 도구를 설치하기 전에 세션을 중지하세요</string> <string name="pextra_installing">%1$s 설치 중</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$s 설치 실패(종료 코드 %2$d)</string> <string name="pextra_could_not_install">%1$s을(를) 설치할 수 없습니다</string> <string name="esync_fetching">droiddeck-esync 팩 받는 중</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">Linux 도우미 포함</string> <string name="cmgr_fex_dlls_only">DLL만</string> <string name="cmgr_original_proton_updated">원본(마지막 교체 이후 Proton 업데이트됨)</string> <string name="cmgr_changed_outside">\"구성 요소\" 외부에서 변경됨</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">없음</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">원본 %1$s</string> <string name="cmgr_original_from">%1$s의 원본</string> <string name="cmgr_swap_queued">%3$s을(를) 사용하는 게임이 종료되면 %1$s %2$s(으)로 교체됩니다.</string> <string name="cmgr_swap_done">이제 %3$s에 %1$s %2$s이(가) 적용되었습니다. 다음 게임 실행 시 적용됩니다.</string> <string name="cmgr_original_missing">해당 원본 번들이 저장되어 있지 않습니다</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%2$s의 %1$s은(는) 해당 Proton을 사용하는 게임이 종료되면 원본으로 복원됩니다.</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">%2$s의 %1$s을(를) %3$s 원본으로 복원했습니다.</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$s은(는) %2$s에서 사용 중입니다. 먼저 해당 Proton을 다른 항목으로 교체하세요.</string> <string name="cmgr_package_not_stored">%1$s이(가) 저장되어 있지 않습니다</string> <string name="cmgr_deleted">%1$s을(를) 삭제했습니다.</string> <string name="cmgr_original_kept">설치된 빌드의 원본은 되돌리기 위해 유지됩니다.</string> <string name="cmgr_not_stored">저장되지 않음</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">%2$s의 %1$s 원본을 삭제했습니다.</string> <string name="cmgr_nightlies_only">Nightlies 릴리스에서만 다운로드할 수 있습니다</string> <string name="cmgr_list_predates_checksums">이 패키지 목록은 체크섬 도입 이전의 것입니다. 새로 고친 후 다시 시도하세요</string> <string name="cmgr_checksum_mismatch">체크섬 불일치</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">선형</string> <string name="sprefs_upscaler_nearest">최근접</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR(품질)</string> <string name="sprefs_upscaler_sharpen">샤프닝 전용</string> <string name="sprefs_lod_bias_auto">자동(스케일링에 맞춤)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">적용된 효과 없음.</string> <string name="look_game_clarity">게임 선명도</string> <string name="look_game_clarity_desc">샤프닝에 약간의 대비를 더한 일상용 프리셋.</string> @@ -1873,27 +1741,23 @@ <string name="look_anime_edge">애니메 윤곽</string> <string name="look_anime_edge_desc">툰 윤곽선과 샤프닝.</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">Adreno GPU가 아님</string> <string name="gpuinfo_supported">지원됨</string> <string name="gpuinfo_experimental">실험적: 드라이버가 테스트 빌드입니다</string> <string name="gpuinfo_below_tested">테스트된 하드웨어(Adreno 725 이상)보다 낮음: 실행되지 않을 수 있습니다</string> <string name="gpuinfo_unsupported">지원되지 않음: DroidDeck에는 Adreno(Snapdragon) GPU가 필요합니다</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">GitHub 요청 한도에 도달했습니다. 나중에 다시 시도하세요</string> <string name="turnip_github_http">GitHub 응답: HTTP %1$d</string> <string name="turnip_download_http">다운로드 응답: HTTP %1$d</string> <string name="turnip_list_predates_checksums">이 드라이버 목록은 체크섬 도입 이전의 것입니다. 새로 고친 후 다시 시도하세요</string> <string name="turnip_checksum_mismatch">체크섬이 일치하지 않아 다운로드를 삭제했습니다</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">AdrenoTools 드라이버가 아닙니다: %1$s</string> <string name="turnip_reject_no_meta">meta.json 없음</string> <string name="turnip_reject_meta_unreadable">meta.json을 읽을 수 없음(%1$s)</string> <string name="turnip_reject_linux_driver">Linux 런타임 드라이버입니다(\"Linux 런타임 드라이버\"에서 가져오세요)</string> <string name="turnip_reject_wayland">Wayland 게임 드라이버이며, 이 앱에서는 사용하지 않습니다</string> <string name="turnip_reject_no_library_name">meta.json에 libraryName이 지정되지 않아 AdrenoTools에 전달할 수 없습니다</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json에 지정된 %1$s이(가) zip에 없습니다</string> <string name="turnip_reject_not_aarch64">%1$s은(는) 64비트 AArch64 ELF 공유 라이브러리가 아닙니다</string> <string name="turnip_reject_glibc">%1$s은(는) glibc에 링크되어 있습니다. \"-Linux\" Turnip이므로 \"Linux 런타임 드라이버\"에서 가져오세요</string> @@ -1904,90 +1768,61 @@ <string name="bundle_entry_outside">드라이버 번들이 아닙니다: %1$s이(가) 번들 외부를 가리킵니다</string> <string name="bundle_too_large">드라이버 번들이 아닙니다: 압축을 풀면 %1$d MB를 초과합니다</string> - <!-- Game storage --> <string name="gstore_sd_card">SD 카드</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">폴더</string> <string name="logshare_chooser">세션 로그 공유</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">Android 10 이상 필요(색 공간을 지원하는 디스플레이 레이어)</string> <string name="hdr_none">이 디스플레이는 HDR을 지원하지 않습니다</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">이 디스플레이는 HDR10이 아닌 %1$s 형식을 지원합니다</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">이 GPU</string> <string name="phantom_title_on">하위 프로세스 제한 켜짐</string> <string name="phantom_title_unset">하위 프로세스 제한 설정되지 않음</string> <string name="phantom_title_unreadable">하위 프로세스 제한을 확인할 수 없음</string> <string name="phantom_title_off">하위 프로세스 제한 꺼짐</string> <string name="phantom_title_not_required">하위 프로세스 제한 해당 없음</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s 이 방법을 사용할 수 없으면 이 기기를 ADB가 설치된 PC에 연결하고 아래 명령을 실행하세요. 그런 다음 여기로 돌아와 다시 확인하세요.</string> <string name="phantom_instructions_off">Steam 세션을 시작할 수 있습니다.</string> <string name="phantom_instructions_not_applicable">이 Android 버전에서는 이 제한을 사용하지 않습니다.</string> <string name="phantom_fix_toggle">개발자 옵션에서 “하위 프로세스 제한 사용 중지”를 켜고, 이후에도 개발자 옵션을 켜 둔 상태로 유지하세요.</string> <string name="phantom_fix_no_toggle">이 Android 버전에는 해당 스위치가 없습니다. 무선 디버깅을 사용하여 이 기기에서 변경하세요.</string> <string name="upd_block_no_apk">이 빌드에는 이 DroidDeck 복사본용 다운로드가 없습니다.</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">versionCode %1$d이(가) 설치된 %2$d보다 낮아 Android에서 이 빌드 위에 설치하지 않습니다.</string> <string name="upd_fail_downgrade">Android에서는 최신 버전 위에 이전 버전을 설치할 수 없습니다. 채널이 따라잡을 때까지 현재 빌드를 유지하세요.</string> <string name="upd_fail_signature">이 DroidDeck 복사본은 다른 키로 서명되어 있어(로컬 빌드일 수 있음) Android에서 덮어쓰기 업데이트를 할 수 없습니다.</string> <string name="upd_fail_storage">업데이트에 필요한 저장 공간이 부족합니다.</string> <string name="upd_fail_aborted">업데이트가 취소되었습니다.</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">업데이트가 설치되지 않았습니다: %1$s</string> <string name="upd_fail_generic">업데이트가 설치되지 않았습니다.</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s zip</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s을(를) 아직 실행한 적이 없습니다</string> <string name="saves_none_found">%1$s의 저장 파일을 찾을 수 없습니다</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s에 drive_c 폴더가 없으므로 GameHub 또는 Winlator 저장 데이터 zip이 아닙니다</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">%1$s에 폴더를 만들 수 없음</string> <string name="gstore_cannot_write">%1$s에 쓸 수 없음</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">%1$s에 쓸 수 없음(%2$s)</string> <string name="gfiles_folder_unavailable">폴더를 사용할 수 없습니다</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">다른 파일이 이미 %1$s 이름을 사용하고 있습니다</string> <string name="gfiles_art_unavailable">이미지 폴더를 사용할 수 없습니다</string> <string name="gfiles_unrelated_inventory">폴더에 관련 없는 동기화 목록이 있습니다</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">%1$s을(를) 제거할 수 없습니다</string> <string name="gfiles_outside_folder">파일이 내보내기 폴더 밖을 가리킵니다</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX(버전 이름 없는 빌드)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">%1$s에 profile.json이 없음</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s은(는) Linux 구성 요소 패키지가 아닙니다(유형 %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · %2$s 사용 가능</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">자동</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">업데이트 카탈로그가 검사를 통과하지 못했습니다(%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeck 하위 프로세스 설정</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">로그인됨 · 오프라인 모드</string> <string name="setup_signed_in_unnamed">로그인됨</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">내장</string> <string name="driver_tag_downloaded">다운로드됨</string> <string name="driver_tag_imported">가져옴</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> <string name="display_session">세션 디스플레이</string> <string name="display_resolution">해상도</string> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f93e48b38..cd85930f9 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1141,16 +1141,12 @@ <string name="game_file_exported">游戏文件已保存到 %1$s</string> <string name="game_frontend_files">前端文件</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">请等待运行时操作完成</string> <string name="main_wait_proton_install">请等待兼容性工具安装完成</string> <string name="main_wait_desktop_install">请等待桌面应用安装完成</string> <string name="main_wait_install_game">请等待安装完成后再启动游戏</string> <string name="main_saves_close_session">请先关闭 Steam 会话,以免游戏存档覆盖导入的内容</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">正在导入到 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="other">已从 %2$s 导入 %1$d 个文件到 %3$s</item> </plurals> @@ -1158,15 +1154,11 @@ <item quantity="other">已从 %2$s 导入 %1$d 个文件到 %3$s。旧存档已备份到 Download/DroidDeck/Saves/backups</item> </plurals> <string name="main_saves_pick_zip">为 %1$s 选择存档 zip 包</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">正在导出 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="other">已将 %1$d 个文件导出为 %2$s:%3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">选择 %1$s 的保存位置(%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">%1$s失败:%2$s</string> <string name="main_no_logs_yet">还没有会话日志:请先运行一次会话。</string> <string name="main_no_logs_to_clear">没有可清除的会话日志。</string> @@ -1179,68 +1171,55 @@ <string name="main_adb_copied">已复制 ADB 命令</string> <string name="main_pick_roms">选择 ROM 文件夹</string> <string name="main_non_adreno_title">不是 Adreno GPU</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip 支持 Adreno GPU。在 %1$s 上,Steam 可能显示黑屏。需下载:%2$.0f MB。</string> <string name="main_install_anyway">仍要安装</string> <string name="main_remove_runtime_title">移除 Linux 运行时</string> <string name="main_remove_runtime_text">这会删除运行时、其中的 Steam 客户端,以及安装在其中的所有游戏。</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">已应用:%1$s</string> <string name="android_app_open_failed_primary">无法在主屏幕上打开 %1$s</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">无法在 %2$s 上打开 %1$s</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">无法在显示器 %2$d 上打开 %1$s</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">正在导入</string> <string name="comp_busy_swapping">正在替换</string> <string name="comp_busy_restoring">正在还原</string> <string name="comp_busy_cancelling">正在取消</string> <string name="comp_busy_deleting">正在删除</string> <string name="comp_cannot_read">无法读取该文件</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">已导入 %1$s</string> <string name="comp_swap_cancelled">等待中的替换已取消。</string> <string name="comp_pick_wcp">选择组件包(-linux .wcp)</string> <string name="comp_nightlies_unreachable">无法连接 Nightlies</string> - <!-- %1$s is the package version --> <string name="comp_stored">已存储 %1$s</string> <string name="comp_download_failed">下载失败:%1$s</string> <string name="drivers_pick_linux">选择 Linux 运行时驱动(-Linux zip 包或 Android + Linux 组合包)</string> <string name="drivers_pick_android">选择显示驱动(AdrenoTools zip 包或 Android + Linux 组合包)</string> <string name="drivers_pick_any">选择驱动 zip 包或 Android + Linux 组合包</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s:%2$s</string> <string name="main_removing_named">正在移除 %1$s…</string> <string name="main_forgetting_named">正在移除 %1$s…</string> <string name="main_pick_game_storage">选择游戏存储文件夹</string> <string name="main_pick_decky_zip">选择 Decky 插件 ZIP 包</string> <string name="main_storage_not_usable">无法使用:%1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">程序</string> <string name="main_removing_runtime">正在移除 Linux 运行时</string> <string name="main_settings_main_screen">无法在底部屏幕上打开设置,已改为在主屏幕上打开。</string> <string name="main_open_dev_options">在设置中打开开发者选项</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">无法打开 Android 的“安装未知应用”设置</string> <string name="upd_couldnt_check">无法检查:%1$s</string> <string name="upd_no_download">此构建没有适用于此 DroidDeck 副本的下载</string> <string name="upd_stop_first">请先停止正在运行的会话,再进行更新</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">正在下载</string> <string name="upd_stage_installing">正在安装</string> <string name="upd_stage_confirm">等待你确认</string> <string name="upd_failed">更新失败</string> <string name="upd_not_offered">Android 未提供安装此更新的选项</string> - <!-- Decky --> <string name="decky_install_failed">Decky 安装失败:%1$s</string> <string name="decky_plugin_starting">正在开始导入插件…</string> <string name="decky_plugin_failed">插件导入失败:%1$s</string> <string name="decky_plugin_installed">插件已安装。重启 Steam 即可加载。</string> - <!-- GPU drivers --> <string name="drivers_not_checked">尚未检查 — 点按刷新以查找新驱动</string> <string name="drivers_bundle_sets_display">Android + Linux:同时设置显示驱动</string> <string name="drivers_bundle_sets_runtime">Android + Linux:同时设置运行时驱动</string> @@ -1248,16 +1227,13 @@ <string name="drivers_system_vulkan">系统 Vulkan:没有适用于此 GPU 的内置构建</string> <string name="drivers_bundled">%1$s(内置)</string> <string name="drivers_manual">手动</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">目前只发布了其中一半</string> <string name="drivers_pair_size">两者共 %1$.0f MB</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">没有可设置的驱动:%1$s</string> <string name="drivers_checking_latest">正在检查最新驱动…</string> <string name="drivers_up_to_date_check_failed">已是最新(无法检查:%1$s)</string> <string name="drivers_recommended_unpublished">推荐的驱动目前尚未发布</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">正在下载 %1$s %2$s…</string> <string name="drivers_updated_to">已更新到 %1$s %2$s。</string> <string name="drivers_updated_to_restart">已更新到 %1$s %2$s。显示驱动将在 DroidDeck 重启后生效。</string> @@ -1272,9 +1248,7 @@ <string name="drivers_using_bundle_restart">正在将 %1$s 同时用作运行时驱动和显示驱动。显示驱动将在 DroidDeck 重启后生效。</string> <string name="drivers_deleted_bundle">已删除 %1$s(两个驱动)</string> <string name="drivers_deleted">已删除 %1$s</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">最新:%1$s · %2$s检查</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">最新:%1$s · 无法连接 %2$s · %3$s检查</string> <plurals name="drivers_ago_minutes"> <item quantity="other">%1$d 分钟前</item> @@ -1287,33 +1261,25 @@ </plurals> <string name="drivers_checking_sources">正在检查 Banners-Turnip、WinNative 和 DroidDeck…</string> <string name="drivers_check_failed">检查失败</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">已安装 %1$s — 在任一列表中选择它即可同时设置两个驱动</string> <string name="drivers_installed_pick">已安装 %1$s — 请在菜单中选择它</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">请等待运行时移除完成</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">游戏</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">触控板</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">直接</string> <string name="session_swap_failed">替换失败:%1$s</string> <string name="session_profile_save_failed">配置档案保存失败:%1$s</string> <string name="session_desktop_paused">桌面已暂停</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s 已暂停</string> <string name="session_program_paused">程序已暂停</string> <string name="session_starting_desktop">正在启动桌面</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">正在启动 %1$s</string> <string name="session_starting_program">正在启动程序</string> <string name="session_no_logs">此会话没有日志。</string> <string name="session_logs_archive_failed">无法创建会话日志压缩包。</string> <string name="session_logs_share_failed">无法分享会话日志。</string> <string name="session_starting_session">正在启动会话…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">正在下载 Linux 运行时</string> <string name="session_runtime_downloading_mb">正在下载 Linux 运行时 · %1$d / %2$d MB</string> <string name="session_runtime_checking">正在检查 Linux 运行时</string> @@ -1328,27 +1294,20 @@ <string name="session_proton_unpacking">正在解包 Proton Experimental(ARM64)</string> <string name="session_runtime_catalog_unreachable">无法连接运行时目录。请检查网络连接,然后再次点“游玩”。</string> <string name="session_runtime_install_failed">Linux 运行时未能安装。请在“配置”中检查运行时,然后再次点“游玩”。</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">桌面未能安装(%1$s)。请检查网络连接,然后再次点“桌面”。</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s 意外停止。请将日志随错误报告一起分享,或重试。</string> <string name="session_ended_program">程序意外停止。请将日志随错误报告一起分享,或重试。</string> <string name="session_ended_desktop">桌面意外停止。请将日志随错误报告一起分享,或重试。</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">退出状态 %1$d · %2$s</string> <string name="session_ended_no_log">无日志</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="other">日志中有 %1$d 个“Function not implemented”错误:proot 的 seccomp 加速在此设备上导致某个辅助进程失败。请尝试 %2$s → “%3$s”(或“%4$s”),然后重新启动。</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">插件</string> <string name="mode_decky_plugins_hint">安装插件 ZIP 包,将在下次 Steam 会话中生效。</string> <string name="mode_decky_plugin_downloading">正在下载…</string> <string name="mode_decky_plugin_install_zip">从 ZIP 安装</string> - <!-- Store --> <string name="store_cat_games">游戏</string> <string name="store_cat_audio_video">影音</string> <string name="store_cat_graphics">图形</string> @@ -1361,21 +1320,14 @@ <string name="store_cat_utilities">实用工具</string> <string name="store_section_popular">热门</string> <string name="store_section_trending">趋势</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">新上架</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">最近更新</string> <string name="store_section_loading">加载中…</string> <string name="store_flatpak_setup">Flatpak 配置</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s:%2$s</string> <string name="store_updates_check_failed">无法从 Flathub 检查更新</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">已安装</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">文件夹</string> - <!-- Launcher themes --> <string name="theme_graphite">石墨</string> <string name="theme_graphite_detail">石墨色界面,选中、聚焦或按下的元素均使用标志自带的球形蓝色。</string> <string name="theme_paper">黑底白纸</string> @@ -1384,7 +1336,6 @@ <string name="theme_icon_blue_detail">黑色背景、白色文字,选中、聚焦或按下的元素使用蓝色。</string> <string name="theme_electric">电光藏青</string> <string name="theme_electric_detail">更明亮的蓝色,配以偏藏青而非灰色的界面。</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1402,22 +1353,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">空格</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">请先安装运行时</string> <string name="game_sync_err_steam_library">Steam 库不可用</string> <string name="game_sync_err_previous_sd">之前的 SD 卡库不可用</string> <string name="game_sync_err_game_storage">游戏存储不可用</string> <string name="game_sync_err_added_games">已添加游戏的文件夹不可用</string> <string name="game_sync_err_manifests">Steam 清单文件不可用</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">多种平台</string> - <!-- File manager --> <string name="fm_drive_internal">内部存储</string> <string name="fm_drive_linux">Linux 系统</string> <string name="fm_drive_sd">SD 卡</string> @@ -1435,7 +1381,6 @@ <string name="fm_no_favorites">还没有收藏 — 用文件夹的 ⋮ 菜单固定它,之后即可快速跳回。</string> <string name="fm_unpin_favorite">从收藏夹移除</string> <string name="fm_size_zero">0 字节</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">字节</string> <string name="fm_prop_location">位置</string> <string name="fm_prop_type">类型</string> @@ -1443,7 +1388,6 @@ <string name="fm_prop_modified">修改时间</string> <string name="fm_type_folder">文件夹</string> <string name="fm_type_file">文件</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$s 文件</string> <string name="fm_read_only">只读</string> <string name="fm_read_only_hint">防止游戏(及 Wine)覆盖或删除此文件。</string> @@ -1457,15 +1401,11 @@ <string name="fm_delete_failed">删除失败</string> <string name="fm_op_moving">正在移动…</string> <string name="fm_op_copying">正在复制…</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">正在移动 - %1$s</string> <string name="fm_op_copying_item">正在复制 - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">正在移动 %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">正在复制 %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d 项完成,%2$d 项失败</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d 项完成,%2$d 项已跳过</string> <plurals name="fm_paste_moved"> <item quantity="other">已移动 %1$d 项</item> @@ -1473,7 +1413,6 @@ <plurals name="fm_paste_copied"> <item quantity="other">已复制 %1$d 项</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">“%1$s”已存在</string> <string name="fm_op_renaming">正在重命名…</string> <string name="fm_rename_failed">重命名失败</string> @@ -1484,17 +1423,14 @@ <string name="fm_create">创建</string> <string name="fm_new_name">新名称</string> <string name="fm_delete_title">删除?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">要永久删除“%1$s”吗?</string> <plurals name="fm_delete_items_title"> <item quantity="other">删除 %1$d 项?</item> </plurals> <string name="fm_cannot_undo">此操作无法撤销。</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="other">…以及其他 %1$d 项</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">正在删除 %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="other">%1$d 项无法删除</item> @@ -1508,7 +1444,6 @@ <string name="fm_conflict_skip">跳过</string> <string name="fm_select_this_folder">选择此文件夹</string> <string name="fm_back">返回</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s 已挂载,但当前无法读取</string> <string name="fm_list_view">列表视图</string> <string name="fm_grid_view">网格视图</string> @@ -1523,41 +1458,31 @@ <string name="fm_hide_favorites">隐藏收藏夹</string> <string name="fm_show_favorites">显示收藏夹</string> <string name="fm_filter_folder">筛选此文件夹</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">可用 %1$s</string> <string name="fm_selected_count">已选择 %1$d 项</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">不选</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">全选</string> <string name="fm_paste">粘贴</string> <plurals name="fm_items"> <item quantity="other">%1$d 项</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">将 %1$s 粘贴到此处</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">将 %1$s 移动到此处</string> <string name="fm_rail_title">文件</string> <string name="fm_rail_storage">存储</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">快速访问</string> <string name="fm_rail_favorites">收藏夹</string> <string name="fm_rail_downloads">下载</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">下载</string> <string name="fm_rail_roms">ROM</string> <string name="fm_rail_session_logs">会话日志</string> <string name="fm_rail_pictures">图片</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">已将“%1$s”添加到收藏夹</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">已将“%1$s”从收藏夹移除</string> <string name="fm_empty_directory">空文件夹</string> <string name="fm_up_one_level">返回上一级</string> <string name="fm_choose_folder">选择文件夹</string> <string name="fm_choose_file">选择文件</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steam 蓝</string> <string name="ctrl_tint_sky">天蓝</string> <string name="ctrl_tint_white">白色</string> @@ -1566,7 +1491,6 @@ <string name="ctrl_tint_amber">琥珀</string> <string name="ctrl_tint_red">红色</string> <string name="ctrl_tint_pink">粉色</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">A 键</string> <string name="ctrl_button_b">B 键</string> <string name="ctrl_button_x">X 键</string> @@ -1577,28 +1501,21 @@ <string name="ctrl_button_rt">右扳机</string> <string name="ctrl_button_view">视图键</string> <string name="ctrl_button_menu">菜单键</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">视图</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">菜单</string> <string name="ctrl_target_up">方向键上</string> <string name="ctrl_target_down">方向键下</string> <string name="ctrl_target_left">方向键左</string> <string name="ctrl_target_right">方向键右</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">隐藏</string> <string name="ctrl_editor_drag">拖动以移动</string> <string name="ctrl_editor_snap">对齐</string> <string name="ctrl_editor_save">保存</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">无</string> <string name="second_mode_keyboard_trackpad">键盘 + 触控板</string> <string name="second_mode_terminal">终端</string> <string name="second_mode_deck_controls">Deck 背键 + 触控板</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">正在移除 Linux 运行时</string> <plurals name="rtinst_removing_entries"> <item quantity="other">正在移除 Linux 运行时 · %1$d 项</item> @@ -1613,63 +1530,47 @@ <string name="rtinst_title_installing">正在安装 Linux 运行时</string> <string name="rtinst_title_removing">正在移除 Linux 运行时</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">未安装 Linux 运行时</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">目录中没有 %1$s 的校验和</string> <string name="deskpkg_checksum_mismatch">校验和不匹配 — 未做任何更改</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">正在安装 %1$s</string> <string name="deskpkg_place_failed">无法放置 AppImage</string> <string name="deskpkg_extract_failed">解压失败</string> <string name="deskpkg_install_failed">安装失败</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpak 配置失败(退出代码 %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpak 配置未完成</string> <string name="flatpakmgr_busy">另一个商店任务正在运行</string> <string name="flatpakmgr_not_ready">请先配置 Flatpak</string> <string name="flatpakmgr_starting">启动中</string> <string name="flatpakmgr_exit_status">Flatpak 已退出,状态码 %1$d</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">正在安装 %1$s</string> <string name="flatpakmgr_updating">正在更新 %1$s</string> <string name="flatpakmgr_removing">正在移除 %1$s</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">正在安装 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">正在更新 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">正在移除 %1$s(%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">翻译</string> <string name="flatpakmgr_ref_graphics_drivers">图形驱动</string> <string name="flatpakmgr_ref_media_codecs">媒体编解码器</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$s 运行时</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$s 运行时 %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">文件太小,不可能是 AppImage</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">无法读取文件:%1$s</string> <string name="appimg_not_linux">这不是 AppImage(不是 Linux 程序)</string> <string name="appimg_wrong_arch">此 AppImage 既不是为 ARM64(aarch64)构建,也不是为 x86 PC 构建</string> <string name="appimg_not_appimage">这是 Linux 程序,但不是 AppImage</string> <string name="appimg_wrong_type">只能导入类型 2 的 AppImage(此文件为类型 %1$d)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">可用空间不足:导入约需 %1$s</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">可用空间不足:更新约需 %1$s</string> <string name="appimg_import_failed">导入失败</string> <string name="appimg_gone">该 AppImage 已不存在</string> <string name="appimg_update_failed">更新失败</string> - <!-- %1$s: file name --> <string name="appimg_extracting">正在解压 %1$s</string> <string name="appimg_extract_failed">无法解压该 AppImage</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">无法解压该 AppImage:%1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">安装 Decky Loader 前请先停止当前会话</string> <string name="deckymgr_download_failed">Decky Loader 下载失败</string> <string name="deckymgr_size_mismatch">Decky Loader 的大小与发布元数据不符</string> @@ -1705,20 +1606,13 @@ <string name="deckymgr_remote_entry_invalid">无效的 remote_binary 条目</string> <string name="deckymgr_remote_name_invalid">无效的远程二进制文件名</string> <string name="deckymgr_remote_url_https">远程二进制文件的 URL 必须使用 HTTPS</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">远程二进制文件 %1$s 的 SHA-256 哈希无效</string> <string name="deckymgr_bin_dir_failed">无法创建插件 bin 目录</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">正在下载插件二进制文件 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">无法下载插件二进制文件 %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">插件二进制文件校验和不匹配 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">无法将插件二进制文件设为可执行 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">无法替换插件二进制文件 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">无法安装插件二进制文件 · %1$s</string> <string name="deckymgr_folder_taken">该插件文件夹属于另一个插件</string> <string name="deckymgr_preserve_failed">无法保留现有插件</string> @@ -1726,20 +1620,14 @@ <string name="deckymgr_settings_failed">无法更新 Decky 插件设置</string> <string name="deckymgr_restore_failed">无法恢复现有插件</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">无法保存无线调试身份信息</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADB 命令执行失败(%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android 未确认子进程限制已更改:%1$s</string> <string name="adbfix_no_pairing">未保存任何无线调试配对</string> <string name="adbfix_enter_address">请输入当前的无线调试 IP 地址与端口</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">等待配对弹窗超时。请从 DroidDeck 重试。</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">无法查找配对弹窗(%1$d)。请检查 Wi-Fi 是否已开启。</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">无法查找配对弹窗:%1$s</string> <string name="adbpair_six_digits">请输入完整的 6 位配对码</string> <string name="adbpair_pairing">正在配对…</string> @@ -1753,27 +1641,22 @@ <string name="adbpair_title_pair">配对无线调试</string> <string name="adbpair_waiting_text">开启无线调试,然后点按“使用配对码配对设备”。</string> <string name="adbpair_waiting_long">开启无线调试,然后点按“使用配对码配对设备”。请停留在“设置”中:配对码在此通知中输入。</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6 位配对码</string> <string name="adbpair_code_hint">点按“输入配对码”,然后输入“设置”中显示的 6 位数字。</string> <string name="adbpair_title_code">输入 Wi-Fi 配对码</string> <string name="adbpair_title_retry">请重新输入配对码</string> - <!-- Notification action button --> <string name="adbpair_enter_code">输入配对码</string> <string name="adbpair_title_working">正在配置 Steam</string> <string name="adbpair_title_done">Steam 已就绪</string> <string name="adbpair_done_text">点按即可返回 DroidDeck。现在可以关闭无线调试了。</string> <string name="adbpair_title_failed">无线调试配置已中止</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">下载服务器返回 HTTP %1$d</string> <string name="selfinst_size_mismatch">下载的文件大小与发布的 APK 不符,已丢弃 — 请重试</string> <string name="selfinst_checksum_mismatch">下载的文件与校验和不符,已丢弃 — 请重试</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">此更新适用于 Android 软件包 %1$s,而不是 %2$s</string> <string name="selfinst_metadata_signer">更新元数据中未指明 DroidDeck 的发布签名密钥</string> <string name="selfinst_unreadable">Android 无法读取下载的 APK</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">下载的 APK 适用于 %1$s,而不是 %2$s</string> <string name="selfinst_version_mismatch">下载的 APK 的 versionCode 为 %1$d,而不是已发布的 %2$d</string> <string name="selfinst_downgrade">Android 不会用 versionCode %1$d 覆盖已安装的 %2$d</string> @@ -1781,16 +1664,11 @@ <string name="selfinst_wrong_signer">下载的 APK 未使用 DroidDeck 的发布密钥签名</string> <string name="appupd_http">更新目录返回 HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">正在启动会话…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">已用时 %1$d:%2$02d · 仍在处理</string> <string name="loadstate_steam_update">正在下载 Steam 客户端更新 · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">正在下载 Steam 客户端 · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">已有兼容性工具正在安装</string> <string name="pextra_stop_session">安装兼容性工具前请先停止当前会话</string> <string name="pextra_no_release">找不到 %1$s 的 ARM64 发布版本</string> @@ -1807,48 +1685,38 @@ <string name="pextra_checksum_mismatch">校验和不匹配;已删除下载的文件</string> <string name="pextra_session_started">下载期间有会话启动;请先停止它,再安装兼容性工具</string> <string name="pextra_installing">正在安装 %1$s</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$s 安装失败(退出代码 %2$d)</string> <string name="pextra_could_not_install">无法安装 %1$s</string> <string name="esync_fetching">正在获取 droiddeck-esync 包</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">含 Linux 辅助组件</string> <string name="cmgr_fex_dlls_only">仅 DLL</string> <string name="cmgr_original_proton_updated">原件(上次替换后 Proton 已更新)</string> <string name="cmgr_changed_outside">已在“组件”之外被更改</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">不存在</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">原件 %1$s</string> <string name="cmgr_original_from">原件来自 %1$s</string> <string name="cmgr_swap_queued">%1$s %2$s 将在 %3$s 上运行的游戏关闭后放入其中。</string> <string name="cmgr_swap_done">%1$s %2$s 已放入 %3$s,将在下次启动游戏时生效。</string> <string name="cmgr_original_missing">未存储该原件包</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%2$s 的 %1$s 将在其上运行的游戏关闭后恢复为原件。</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">已从 %3$s 原件还原 %2$s 的 %1$s。</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$s 正在 %2$s 中使用。请先将该 Proton 替换为其他版本。</string> <string name="cmgr_package_not_stored">%1$s 未存储</string> <string name="cmgr_deleted">已删除 %1$s。</string> <string name="cmgr_original_kept">已安装构建的原件会保留:它是还原的途径。</string> <string name="cmgr_not_stored">未存储</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">已删除 %2$s 的 %1$s 原件。</string> <string name="cmgr_nightlies_only">只能从 Nightlies 发布版本下载</string> <string name="cmgr_list_predates_checksums">此软件包列表早于校验和功能 — 请刷新后重试</string> <string name="cmgr_checksum_mismatch">校验和不匹配</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">线性</string> <string name="sprefs_upscaler_nearest">最近邻</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR(质量)</string> <string name="sprefs_upscaler_sharpen">仅锐化</string> <string name="sprefs_lod_bias_auto">自动(匹配缩放)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">不应用任何效果。</string> <string name="look_game_clarity">游戏清晰</string> <string name="look_game_clarity_desc">锐化并略增对比度 — 适合日常使用。</string> @@ -1873,27 +1741,23 @@ <string name="look_anime_edge">动漫描边</string> <string name="look_anime_edge_desc">卡通描边加锐化。</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">不是 Adreno GPU</string> <string name="gpuinfo_supported">受支持</string> <string name="gpuinfo_experimental">实验性:其驱动为测试构建</string> <string name="gpuinfo_below_tested">低于已测试的硬件(Adreno 725 及更新):可能无法运行</string> <string name="gpuinfo_unsupported">不受支持:DroidDeck 需要 Adreno(骁龙)GPU</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">已达到 GitHub 的请求频率限制 — 请稍后重试</string> <string name="turnip_github_http">GitHub 返回 HTTP %1$d</string> <string name="turnip_download_http">下载返回 HTTP %1$d</string> <string name="turnip_list_predates_checksums">此驱动列表早于校验和功能 — 请刷新后重试</string> <string name="turnip_checksum_mismatch">校验和不匹配 — 已丢弃下载的文件</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">不是 AdrenoTools 驱动:%1$s</string> <string name="turnip_reject_no_meta">缺少 meta.json</string> <string name="turnip_reject_meta_unreadable">无法读取 meta.json(%1$s)</string> <string name="turnip_reject_linux_driver">这是 Linux 运行时驱动(请在“运行时驱动”下导入)</string> <string name="turnip_reject_wayland">这是 Wayland 游戏驱动,本应用用不到它</string> <string name="turnip_reject_no_library_name">meta.json 未指定 libraryName,因此无法交给 AdrenoTools</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json 指定了 %1$s,但它不在 zip 包中</string> <string name="turnip_reject_not_aarch64">%1$s 不是 64 位 AArch64 ELF 共享库</string> <string name="turnip_reject_glibc">%1$s 链接了 glibc — 这是“-Linux”Turnip,应在“运行时驱动”下导入</string> @@ -1904,90 +1768,61 @@ <string name="bundle_entry_outside">不是驱动组合包:%1$s 指向包外</string> <string name="bundle_too_large">不是驱动组合包:解包后超过 %1$d MB</string> - <!-- Game storage --> <string name="gstore_sd_card">SD 卡</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">文件夹</string> <string name="logshare_chooser">分享会话日志</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">需要 Android 10 或更高版本(支持色彩空间的显示图层)</string> <string name="hdr_none">此显示器未报告支持 HDR</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">此显示器支持 %1$s,但不支持 HDR10</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">此 GPU</string> <string name="phantom_title_on">子进程限制已开启</string> <string name="phantom_title_unset">子进程限制未设置</string> <string name="phantom_title_unreadable">无法检查子进程限制</string> <string name="phantom_title_off">子进程限制已关闭</string> <string name="phantom_title_not_required">无需更改子进程限制</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s 如果无法做到,请将此设备连接到装有 ADB 的电脑并运行下方命令,然后返回此处重新检查。</string> <string name="phantom_instructions_off">可以启动 Steam 会话了。</string> <string name="phantom_instructions_not_applicable">此 Android 版本不使用该限制。</string> <string name="phantom_fix_toggle">在开发者选项中打开“禁用子进程限制”,之后请保持开发者选项开启。</string> <string name="phantom_fix_no_toggle">此 Android 版本没有该开关。请使用无线调试在此设备上更改。</string> <string name="upd_block_no_apk">此构建没有适用于此 DroidDeck 副本的下载。</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">Android 不会用它覆盖此构建,因为它的 versionCode %1$d 低于已安装的 %2$d。</string> <string name="upd_fail_downgrade">Android 不会用旧版本覆盖新版本。请继续使用此构建,直到该通道的版本跟上。</string> <string name="upd_fail_signature">此 DroidDeck 副本的签名不同(可能是本地构建?),因此 Android 无法就地更新。</string> <string name="upd_fail_storage">可用空间不足,无法更新。</string> <string name="upd_fail_aborted">更新已取消。</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">更新未能安装:%1$s</string> <string name="upd_fail_generic">更新未能安装。</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s zip 包</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip 包</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s 尚未启动过</string> <string name="saves_none_found">未找到 %1$s 的存档文件</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s 中没有 drive_c 文件夹,因此不是 GameHub 或 Winlator 存档 zip 包</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">无法在 %1$s 中创建文件夹</string> <string name="gstore_cannot_write">无法写入 %1$s</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">无法写入 %1$s(%2$s)</string> <string name="gfiles_folder_unavailable">文件夹不可用</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">已有另一个文件使用 %1$s</string> <string name="gfiles_art_unavailable">美术图文件夹不可用</string> <string name="gfiles_unrelated_inventory">文件夹中有无关的同步清单</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">无法移除 %1$s</string> <string name="gfiles_outside_folder">文件超出了导出文件夹</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX(无版本名称的构建)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">%1$s 中没有 profile.json</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s 不是 Linux 组件包(类型为 %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · 可用 %2$s</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">自动</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">更新目录未通过校验(%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeck 子进程设置</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">已登录 · 离线模式</string> <string name="setup_signed_in_unnamed">已登录</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">内置</string> <string name="driver_tag_downloaded">已下载</string> <string name="driver_tag_imported">已导入</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">Android + Linux</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s。%2$s\n\n%3$s</string> <string name="display_session">会话显示</string> <string name="display_resolution">分辨率</string> diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 226b0f500..a871f960a 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1141,16 +1141,12 @@ <string name="game_file_exported">遊戲檔案已儲存至 %1$s</string> <string name="game_frontend_files">前端檔案</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">請等待運行環境操作完成</string> <string name="main_wait_proton_install">請等待兼容性工具安裝完成</string> <string name="main_wait_desktop_install">請等待桌面應用程式安裝完成</string> <string name="main_wait_install_game">請等待安裝完成後再啟動遊戲</string> <string name="main_saves_close_session">請先關閉 Steam 會話,以免遊戲存檔覆蓋匯入的內容</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">正在匯入至 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="other">已從 %2$s 匯入 %1$d 個檔案至 %3$s</item> </plurals> @@ -1158,15 +1154,11 @@ <item quantity="other">已從 %2$s 匯入 %1$d 個檔案至 %3$s。舊存檔已備份至 Download/DroidDeck/Saves/backups</item> </plurals> <string name="main_saves_pick_zip">為 %1$s 選擇存檔 zip</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">正在匯出 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="other">已將 %1$d 個檔案匯出為 %2$s:%3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">選擇 %1$s 的儲存位置(%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">操作失敗(%1$s):%2$s</string> <string name="main_no_logs_yet">尚未有會話記錄:請先執行一次會話。</string> <string name="main_no_logs_to_clear">沒有可清除的會話記錄。</string> @@ -1179,68 +1171,55 @@ <string name="main_adb_copied">已複製 ADB 指令</string> <string name="main_pick_roms">選擇 ROM 資料夾</string> <string name="main_non_adreno_title">不是 Adreno GPU</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string> <string name="main_install_anyway">仍要安裝</string> <string name="main_remove_runtime_title">移除 Linux 運行環境</string> <string name="main_remove_runtime_text">此操作會刪除運行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">已套用:%1$s</string> <string name="android_app_open_failed_primary">無法在主螢幕開啟 %1$s</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">無法在 %2$s 開啟 %1$s</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">無法在顯示器 %2$d 開啟 %1$s</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">正在匯入</string> <string name="comp_busy_swapping">正在替換</string> <string name="comp_busy_restoring">正在還原</string> <string name="comp_busy_cancelling">正在取消</string> <string name="comp_busy_deleting">正在刪除</string> <string name="comp_cannot_read">無法讀取檔案</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">已匯入 %1$s</string> <string name="comp_swap_cancelled">已取消等待中的替換。</string> <string name="comp_pick_wcp">選擇元件套件(-linux .wcp)</string> <string name="comp_nightlies_unreachable">無法連接 Nightlies</string> - <!-- %1$s is the package version --> <string name="comp_stored">已儲存 %1$s</string> <string name="comp_download_failed">下載失敗:%1$s</string> <string name="drivers_pick_linux">選擇 Linux 運行環境驅動程式(-Linux zip 或 Android + Linux 組合包)</string> <string name="drivers_pick_android">選擇顯示驅動程式(AdrenoTools zip 或 Android + Linux 組合包)</string> <string name="drivers_pick_any">選擇驅動程式 zip 或 Android + Linux 組合包</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s:%2$s</string> <string name="main_removing_named">正在移除 %1$s…</string> <string name="main_forgetting_named">正在忘記 %1$s…</string> <string name="main_pick_game_storage">選擇遊戲儲存資料夾</string> <string name="main_pick_decky_zip">選擇 Decky 插件 ZIP</string> <string name="main_storage_not_usable">無法使用:%1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">程式</string> <string name="main_removing_runtime">正在移除 Linux 運行環境</string> <string name="main_settings_main_screen">無法在下方螢幕開啟「設定」,已改為在主螢幕開啟。</string> <string name="main_open_dev_options">在「設定」中開啟開發人員選項</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">無法開啟 Android 的「安裝不明應用程式」設定</string> <string name="upd_couldnt_check">無法檢查:%1$s</string> <string name="upd_no_download">此版本沒有適用於此 DroidDeck 版本的下載</string> <string name="upd_stop_first">請先停止正在執行的會話,然後再更新</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">正在下載</string> <string name="upd_stage_installing">正在安裝</string> <string name="upd_stage_confirm">正在等待你確認</string> <string name="upd_failed">更新失敗</string> <string name="upd_not_offered">Android 沒有提供安裝更新的選項</string> - <!-- Decky --> <string name="decky_install_failed">Decky 安裝失敗:%1$s</string> <string name="decky_plugin_starting">正在開始匯入插件…</string> <string name="decky_plugin_failed">插件匯入失敗:%1$s</string> <string name="decky_plugin_installed">已安裝插件。請重新啟動 Steam 以載入。</string> - <!-- GPU drivers --> <string name="drivers_not_checked">尚未檢查 - 輕按重新整理以尋找新驅動程式</string> <string name="drivers_bundle_sets_display">Android + Linux:同時設定顯示驅動程式</string> <string name="drivers_bundle_sets_runtime">Android + Linux:同時設定運行環境驅動程式</string> @@ -1248,16 +1227,13 @@ <string name="drivers_system_vulkan">系統 Vulkan:沒有適用於此 GPU 的內置版本</string> <string name="drivers_bundled">%1$s(內置)</string> <string name="drivers_manual">手動</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">目前只發佈了其中一半</string> <string name="drivers_pair_size">兩者合共 %1$.0f MB</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">沒有可設定的驅動程式:%1$s</string> <string name="drivers_checking_latest">正在檢查最新驅動程式…</string> <string name="drivers_up_to_date_check_failed">已是最新(無法檢查:%1$s)</string> <string name="drivers_recommended_unpublished">建議的驅動程式目前尚未發佈</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">正在下載 %1$s %2$s…</string> <string name="drivers_updated_to">已更新至 %1$s %2$s。</string> <string name="drivers_updated_to_restart">已更新至 %1$s %2$s。顯示驅動程式會在 DroidDeck 重新啟動後生效。</string> @@ -1272,9 +1248,7 @@ <string name="drivers_using_bundle_restart">正在使用 %1$s 作為運行環境及顯示驅動程式。顯示驅動程式會在 DroidDeck 重新啟動後生效。</string> <string name="drivers_deleted_bundle">已刪除 %1$s(兩個驅動程式)</string> <string name="drivers_deleted">已刪除 %1$s</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">最新:%1$s · %2$s檢查</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">最新:%1$s · 無法連接 %2$s · %3$s檢查</string> <plurals name="drivers_ago_minutes"> <item quantity="other">%1$d 分鐘前</item> @@ -1287,33 +1261,25 @@ </plurals> <string name="drivers_checking_sources">正在檢查 Banners-Turnip、WinNative 和 DroidDeck…</string> <string name="drivers_check_failed">檢查失敗</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">已安裝 %1$s - 在任一清單中選擇即可同時設定兩個驅動程式</string> <string name="drivers_installed_pick">已安裝 %1$s - 請在選單中選擇</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">請等待運行環境移除完成</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">遊戲</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">觸控板</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">直接</string> <string name="session_swap_failed">替換失敗:%1$s</string> <string name="session_profile_save_failed">設定檔儲存失敗:%1$s</string> <string name="session_desktop_paused">桌面已暫停</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s 已暫停</string> <string name="session_program_paused">程式已暫停</string> <string name="session_starting_desktop">正在啟動桌面</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">正在啟動 %1$s</string> <string name="session_starting_program">正在啟動程式</string> <string name="session_no_logs">此會話沒有記錄。</string> <string name="session_logs_archive_failed">無法建立會話記錄封存檔。</string> <string name="session_logs_share_failed">無法分享會話記錄。</string> <string name="session_starting_session">正在啟動會話…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">正在下載 Linux 運行環境</string> <string name="session_runtime_downloading_mb">正在下載 Linux 運行環境 · %1$d/%2$d MB</string> <string name="session_runtime_checking">正在檢查 Linux 運行環境</string> @@ -1328,27 +1294,20 @@ <string name="session_proton_unpacking">正在解壓 Proton Experimental (ARM64)</string> <string name="session_runtime_catalog_unreachable">無法連接運行環境目錄。請檢查網絡連線,然後再按「開啟 Steam」。</string> <string name="session_runtime_install_failed">Linux 運行環境未能安裝。請在「設定」中檢查運行環境,然後再按「開啟 Steam」。</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">桌面未能安裝(%1$s)。請檢查網絡連線,然後再按「桌面」。</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s 意外停止。請在錯誤報告中附上記錄,或再試一次。</string> <string name="session_ended_program">程式意外停止。請在錯誤報告中附上記錄,或再試一次。</string> <string name="session_ended_desktop">桌面意外停止。請在錯誤報告中附上記錄,或再試一次。</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">結束狀態 %1$d · %2$s</string> <string name="session_ended_no_log">沒有記錄</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="other">記錄顯示 %1$d 個「Function not implemented」錯誤:proot 的 seccomp 加速令此裝置上的一個輔助程式失敗。請嘗試 %2$s →「%3$s」(或「%4$s」),然後重新啟動。</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">插件</string> <string name="mode_decky_plugins_hint">安裝插件 ZIP,在下次 Steam 會話使用。</string> <string name="mode_decky_plugin_downloading">正在下載…</string> <string name="mode_decky_plugin_install_zip">從 ZIP 安裝</string> - <!-- Store --> <string name="store_cat_games">遊戲</string> <string name="store_cat_audio_video">音訊及影片</string> <string name="store_cat_graphics">圖形</string> @@ -1361,21 +1320,14 @@ <string name="store_cat_utilities">公用程式</string> <string name="store_section_popular">熱門</string> <string name="store_section_trending">趨勢</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">最新</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">最近更新</string> <string name="store_section_loading">正在載入…</string> <string name="store_flatpak_setup">Flatpak 設定</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s:%2$s</string> <string name="store_updates_check_failed">無法在 Flathub 檢查更新</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">已安裝</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">資料夾</string> - <!-- Launcher themes --> <string name="theme_graphite">石墨</string> <string name="theme_graphite_detail">石墨色介面,所有已選取、焦點所在或按下的項目都使用標誌本身的球體藍色。</string> <string name="theme_paper">黑底白紙</string> @@ -1384,7 +1336,6 @@ <string name="theme_icon_blue_detail">黑色背景、白色文字,所有已選取、焦點所在或按下的項目都使用藍色。</string> <string name="theme_electric">電光海軍藍</string> <string name="theme_electric_detail">更明亮的藍色,搭配偏海軍藍而非灰色的介面。</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1402,22 +1353,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">空格</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">請先安裝運行環境</string> <string name="game_sync_err_steam_library">無法使用 Steam 收藏庫</string> <string name="game_sync_err_previous_sd">無法使用先前的 SD 卡收藏庫</string> <string name="game_sync_err_game_storage">無法使用遊戲儲存位置</string> <string name="game_sync_err_added_games">無法使用已新增遊戲的資料夾</string> <string name="game_sync_err_manifests">無法使用 Steam 清單檔案</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">多個系統</string> - <!-- File manager --> <string name="fm_drive_internal">內部</string> <string name="fm_drive_linux">Linux 系統</string> <string name="fm_drive_sd">SD 卡</string> @@ -1435,7 +1381,6 @@ <string name="fm_no_favorites">尚未有我的最愛 - 使用資料夾的 ⋮ 選單將其釘選,即可快速返回。</string> <string name="fm_unpin_favorite">從我的最愛移除</string> <string name="fm_size_zero">0 位元組</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">位元組</string> <string name="fm_prop_location">位置</string> <string name="fm_prop_type">類型</string> @@ -1443,7 +1388,6 @@ <string name="fm_prop_modified">修改日期</string> <string name="fm_type_folder">資料夾</string> <string name="fm_type_file">檔案</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$s 檔案</string> <string name="fm_read_only">唯讀</string> <string name="fm_read_only_hint">防止遊戲(及 Wine)覆寫或刪除此檔案。</string> @@ -1457,15 +1401,11 @@ <string name="fm_delete_failed">刪除失敗</string> <string name="fm_op_moving">正在移動...</string> <string name="fm_op_copying">正在複製...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">正在移動 - %1$s</string> <string name="fm_op_copying_item">正在複製 - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">正在移動 %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">正在複製 %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d 個完成,%2$d 個失敗</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d 個完成,%2$d 個已略過</string> <plurals name="fm_paste_moved"> <item quantity="other">已移動 %1$d 個項目</item> @@ -1473,7 +1413,6 @@ <plurals name="fm_paste_copied"> <item quantity="other">已複製 %1$d 個項目</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">「%1$s」已存在</string> <string name="fm_op_renaming">正在重新命名...</string> <string name="fm_rename_failed">重新命名失敗</string> @@ -1484,17 +1423,14 @@ <string name="fm_create">建立</string> <string name="fm_new_name">新名稱</string> <string name="fm_delete_title">刪除?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">要永久刪除「%1$s」嗎?</string> <plurals name="fm_delete_items_title"> <item quantity="other">刪除 %1$d 個項目?</item> </plurals> <string name="fm_cannot_undo">此操作無法復原。</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="other">…及另外 %1$d 個</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">正在刪除 %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="other">%1$d 個無法刪除</item> @@ -1508,7 +1444,6 @@ <string name="fm_conflict_skip">略過</string> <string name="fm_select_this_folder">選擇此資料夾</string> <string name="fm_back">返回</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s 已掛載,但目前無法讀取</string> <string name="fm_list_view">清單檢視</string> <string name="fm_grid_view">格狀檢視</string> @@ -1523,41 +1458,31 @@ <string name="fm_hide_favorites">隱藏我的最愛</string> <string name="fm_show_favorites">顯示我的最愛</string> <string name="fm_filter_folder">篩選此資料夾</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">可用 %1$s</string> <string name="fm_selected_count">已選取 %1$d 個</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">無</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">全部</string> <string name="fm_paste">貼上</string> <plurals name="fm_items"> <item quantity="other">%1$d 個項目</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">在此貼上 %1$s</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">移動 %1$s 至此</string> <string name="fm_rail_title">檔案</string> <string name="fm_rail_storage">儲存空間</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">快速</string> <string name="fm_rail_favorites">我的最愛</string> <string name="fm_rail_downloads">下載</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">下載</string> <string name="fm_rail_roms">ROMs</string> <string name="fm_rail_session_logs">會話記錄</string> <string name="fm_rail_pictures">圖片</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">已將「%1$s」加入我的最愛</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">已將「%1$s」從我的最愛移除</string> <string name="fm_empty_directory">空白目錄</string> <string name="fm_up_one_level">返回上一層</string> <string name="fm_choose_folder">選擇資料夾</string> <string name="fm_choose_file">選擇檔案</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steam 藍</string> <string name="ctrl_tint_sky">天藍</string> <string name="ctrl_tint_white">白色</string> @@ -1566,7 +1491,6 @@ <string name="ctrl_tint_amber">琥珀色</string> <string name="ctrl_tint_red">紅色</string> <string name="ctrl_tint_pink">粉紅色</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">A 鍵</string> <string name="ctrl_button_b">B 鍵</string> <string name="ctrl_button_x">X 鍵</string> @@ -1577,28 +1501,21 @@ <string name="ctrl_button_rt">右扳機鍵</string> <string name="ctrl_button_view">檢視按鈕</string> <string name="ctrl_button_menu">選單按鈕</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">檢視</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">選單</string> <string name="ctrl_target_up">十字鍵上</string> <string name="ctrl_target_down">十字鍵下</string> <string name="ctrl_target_left">十字鍵左</string> <string name="ctrl_target_right">十字鍵右</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">隱藏</string> <string name="ctrl_editor_drag">拖曳以移動</string> <string name="ctrl_editor_snap">對齊</string> <string name="ctrl_editor_save">儲存</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">無</string> <string name="second_mode_keyboard_trackpad">鍵盤 + 觸控板</string> <string name="second_mode_terminal">終端機</string> <string name="second_mode_deck_controls">Deck 握把 + 觸控板</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">正在移除 Linux 運行環境</string> <plurals name="rtinst_removing_entries"> <item quantity="other">正在移除 Linux 運行環境 · %1$d 個項目</item> @@ -1613,63 +1530,47 @@ <string name="rtinst_title_installing">正在安裝 Linux 運行環境</string> <string name="rtinst_title_removing">正在移除 Linux 運行環境</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">尚未安裝 Linux 運行環境</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">目錄中沒有 %1$s 的校驗和</string> <string name="deskpkg_checksum_mismatch">校驗和不符 - 未作任何更改</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">正在安裝 %1$s</string> <string name="deskpkg_place_failed">無法放置 AppImage</string> <string name="deskpkg_extract_failed">解壓縮失敗</string> <string name="deskpkg_install_failed">安裝失敗</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpak 設定失敗(結束代碼 %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpak 設定未完成</string> <string name="flatpakmgr_busy">另一項商店工作正在進行</string> <string name="flatpakmgr_not_ready">請先設定 Flatpak</string> <string name="flatpakmgr_starting">正在開始</string> <string name="flatpakmgr_exit_status">Flatpak 已結束,狀態為 %1$d</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">正在安裝 %1$s</string> <string name="flatpakmgr_updating">正在更新 %1$s</string> <string name="flatpakmgr_removing">正在移除 %1$s</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">正在安裝 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">正在更新 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">正在移除 %1$s(%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">翻譯</string> <string name="flatpakmgr_ref_graphics_drivers">圖形驅動程式</string> <string name="flatpakmgr_ref_media_codecs">媒體編解碼器</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$s 運行環境</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$s 運行環境 %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">檔案太小,不可能是 AppImage</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">無法讀取檔案:%1$s</string> <string name="appimg_not_linux">這不是 AppImage(它不是 Linux 程式)</string> <string name="appimg_wrong_arch">此 AppImage 既不是為 ARM64 (aarch64) 亦不是為 x86 電腦而設</string> <string name="appimg_not_appimage">這是 Linux 程式,但不是 AppImage</string> <string name="appimg_wrong_type">只可匯入第 2 類 AppImage(此檔案為第 %1$d 類)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">可用空間不足:匯入約需 %1$s</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">可用空間不足:更新約需 %1$s</string> <string name="appimg_import_failed">匯入失敗</string> <string name="appimg_gone">該 AppImage 已不存在</string> <string name="appimg_update_failed">更新失敗</string> - <!-- %1$s: file name --> <string name="appimg_extracting">正在解壓縮 %1$s</string> <string name="appimg_extract_failed">無法解壓縮 AppImage</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">無法解壓縮 AppImage:%1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">請先停止目前的會話,再安裝 Decky Loader</string> <string name="deckymgr_download_failed">Decky Loader 下載失敗</string> <string name="deckymgr_size_mismatch">Decky Loader 的大小與發佈中繼資料不符</string> @@ -1705,20 +1606,13 @@ <string name="deckymgr_remote_entry_invalid">remote_binary 項目無效</string> <string name="deckymgr_remote_name_invalid">遠端二進位檔名稱無效</string> <string name="deckymgr_remote_url_https">遠端二進位檔網址必須使用 HTTPS</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">遠端二進位檔 %1$s 的 SHA-256 雜湊值無效</string> <string name="deckymgr_bin_dir_failed">無法建立插件 bin 目錄</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">正在下載插件二進位檔 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">無法下載插件二進位檔 %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">插件二進位檔校驗和不符 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">無法將插件二進位檔設為可執行 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">無法取代插件二進位檔 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">無法安裝插件二進位檔 · %1$s</string> <string name="deckymgr_folder_taken">插件資料夾已屬於另一個插件</string> <string name="deckymgr_preserve_failed">無法保留現有插件</string> @@ -1726,20 +1620,14 @@ <string name="deckymgr_settings_failed">無法更新 Decky 插件設定</string> <string name="deckymgr_restore_failed">無法還原現有插件</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">無法儲存無線偵錯身分</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADB 指令失敗(%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android 未確認子進程限制已更改:%1$s</string> <string name="adbfix_no_pairing">沒有已儲存的無線偵錯配對</string> <string name="adbfix_enter_address">請輸入目前的無線偵錯 IP 位址和連接埠</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">等待配對彈出式視窗逾時,請在 DroidDeck 重試。</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">無法尋找配對彈出式視窗(%1$d)。請確認 Wi-Fi 已開啟。</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">無法尋找配對彈出式視窗:%1$s</string> <string name="adbpair_six_digits">請輸入完整的 6 位數配對碼</string> <string name="adbpair_pairing">正在配對…</string> @@ -1753,27 +1641,22 @@ <string name="adbpair_title_pair">配對無線偵錯</string> <string name="adbpair_waiting_text">開啟無線偵錯,然後輕按「使用配對碼配對裝置」。</string> <string name="adbpair_waiting_long">開啟無線偵錯,然後輕按「使用配對碼配對裝置」。請留在「設定」中:配對碼需在此通知中輸入。</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6 位數配對碼</string> <string name="adbpair_code_hint">輕按「輸入配對碼」,然後輸入「設定」中顯示的 6 位數字。</string> <string name="adbpair_title_code">輸入 Wi-Fi 配對碼</string> <string name="adbpair_title_retry">請再次輸入配對碼</string> - <!-- Notification action button --> <string name="adbpair_enter_code">輸入配對碼</string> <string name="adbpair_title_working">正在設定 Steam</string> <string name="adbpair_title_done">Steam 已準備就緒</string> <string name="adbpair_done_text">輕按即可返回 DroidDeck。你現在可以關閉無線偵錯。</string> <string name="adbpair_title_failed">無線偵錯設定已停止</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">下載回應 HTTP %1$d</string> <string name="selfinst_size_mismatch">下載的大小與已發佈的 APK 不符,已被捨棄 - 請再試一次</string> <string name="selfinst_checksum_mismatch">下載的檔案與其校驗和不符,已被捨棄 - 請再試一次</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">此更新屬於 Android 套件 %1$s,而非 %2$s</string> <string name="selfinst_metadata_signer">更新中繼資料未指明 DroidDeck 的發佈簽署金鑰</string> <string name="selfinst_unreadable">Android 無法讀取已下載的 APK</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">已下載的 APK 屬於 %1$s,而非 %2$s</string> <string name="selfinst_version_mismatch">已下載的 APK 的 versionCode 為 %1$d,而非已發佈的 %2$d</string> <string name="selfinst_downgrade">Android 不會以 versionCode %1$d 覆蓋已安裝的 %2$d</string> @@ -1781,16 +1664,11 @@ <string name="selfinst_wrong_signer">已下載的 APK 並非使用 DroidDeck 的發佈金鑰簽署</string> <string name="appupd_http">更新目錄回應 HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">正在啟動會話…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">已過 %1$d:%2$02d · 仍在處理</string> <string name="loadstate_steam_update">正在下載 Steam 用戶端更新 · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">正在下載 Steam 用戶端 · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">已有兼容性工具正在安裝</string> <string name="pextra_stop_session">請先停止目前的會話,再安裝兼容性工具</string> <string name="pextra_no_release">找不到 %1$s 的 ARM64 發佈版本</string> @@ -1807,48 +1685,38 @@ <string name="pextra_checksum_mismatch">校驗和不符;已刪除下載的檔案</string> <string name="pextra_session_started">下載期間有會話啟動;請先停止該會話,再安裝兼容性工具</string> <string name="pextra_installing">正在安裝 %1$s</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$s 安裝失敗(結束代碼 %2$d)</string> <string name="pextra_could_not_install">無法安裝 %1$s</string> <string name="esync_fetching">正在下載 droiddeck-esync 套件</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">包含 Linux 輔助程式</string> <string name="cmgr_fex_dlls_only">只有 DLL</string> <string name="cmgr_original_proton_updated">原始(上次替換後 Proton 已更新)</string> <string name="cmgr_changed_outside">已在「元件」以外更改</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">不存在</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">原始 %1$s</string> <string name="cmgr_original_from">原始版本來自 %1$s</string> <string name="cmgr_swap_queued">%1$s %2$s 會在 %3$s 上執行的遊戲關閉後放入其中。</string> <string name="cmgr_swap_done">%1$s %2$s 現已放入 %3$s,會在下次啟動遊戲時生效。</string> <string name="cmgr_original_missing">未儲存該原始組合包</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%2$s 的 %1$s 會在其上執行的遊戲關閉後還原為原始版本。</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">已從 %3$s 的原始版本還原 %2$s 的 %1$s。</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$s 正在 %2$s 中使用。請先將該 Proton 替換為其他版本。</string> <string name="cmgr_package_not_stored">未儲存 %1$s</string> <string name="cmgr_deleted">已刪除 %1$s。</string> <string name="cmgr_original_kept">已安裝版本的原始版本會保留,以便還原。</string> <string name="cmgr_not_stored">未儲存</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">已刪除 %2$s 的 %1$s 原始版本。</string> <string name="cmgr_nightlies_only">只會從 Nightlies 發佈版本下載</string> <string name="cmgr_list_predates_checksums">此軟件包清單早於校驗和推出 - 請重新整理後再試一次</string> <string name="cmgr_checksum_mismatch">校驗和不符</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">線性</string> <string name="sprefs_upscaler_nearest">最近鄰</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR(品質)</string> <string name="sprefs_upscaler_sharpen">只銳化</string> <string name="sprefs_lod_bias_auto">自動(配合縮放)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">沒有套用任何效果。</string> <string name="look_game_clarity">遊戲清晰</string> <string name="look_game_clarity_desc">銳化並略增對比度 - 日常之選。</string> @@ -1873,27 +1741,23 @@ <string name="look_anime_edge">動畫描邊</string> <string name="look_anime_edge_desc">卡通描邊加上銳化。</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">不是 Adreno GPU</string> <string name="gpuinfo_supported">受支援</string> <string name="gpuinfo_experimental">實驗性:其驅動程式為測試版本</string> <string name="gpuinfo_below_tested">低於已測試的硬件(Adreno 725 或更新):可能無法執行</string> <string name="gpuinfo_unsupported">不受支援:DroidDeck 需要 Adreno (Snapdragon) GPU</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">已達 GitHub 的速率限制 - 請稍後再試</string> <string name="turnip_github_http">GitHub 回應 HTTP %1$d</string> <string name="turnip_download_http">下載回應 HTTP %1$d</string> <string name="turnip_list_predates_checksums">此驅動程式清單早於校驗和推出 - 請重新整理後再試一次</string> <string name="turnip_checksum_mismatch">校驗和不符 - 已捨棄下載的檔案</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">不是 AdrenoTools 驅動程式:%1$s</string> <string name="turnip_reject_no_meta">沒有 meta.json</string> <string name="turnip_reject_meta_unreadable">無法讀取 meta.json(%1$s)</string> <string name="turnip_reject_linux_driver">這是 Linux 運行環境驅動程式(請在「運行環境驅動程式」下匯入)</string> <string name="turnip_reject_wayland">這是 Wayland 遊戲驅動程式,此應用程式用不上</string> <string name="turnip_reject_no_library_name">meta.json 沒有指明 libraryName,因此無法交給 AdrenoTools</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json 指明了 %1$s,但 zip 中沒有此檔案</string> <string name="turnip_reject_not_aarch64">%1$s 不是 64 位元 AArch64 ELF 共用程式庫</string> <string name="turnip_reject_glibc">%1$s 連結 glibc - 這是「-Linux」Turnip,應在「運行環境驅動程式」下匯入</string> @@ -1904,90 +1768,61 @@ <string name="bundle_entry_outside">不是驅動程式組合包:%1$s 指向組合包以外</string> <string name="bundle_too_large">不是驅動程式組合包:解壓後超過 %1$d MB</string> - <!-- Game storage --> <string name="gstore_sd_card">SD 卡</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">資料夾</string> <string name="logshare_chooser">分享會話記錄</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">需要 Android 10 或更新版本(支援色彩空間的顯示圖層)</string> <string name="hdr_none">此螢幕沒有回報支援 HDR</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">此螢幕支援 %1$s,但不支援 HDR10</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">此 GPU</string> <string name="phantom_title_on">子進程限制已開啟</string> <string name="phantom_title_unset">子進程限制未設定</string> <string name="phantom_title_unreadable">無法檢查子進程限制</string> <string name="phantom_title_off">子進程限制已關閉</string> <string name="phantom_title_not_required">無需理會子進程限制</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s 如無法這樣做,請將此裝置連接到已安裝 ADB 的電腦,並執行以下指令,然後返回這裡再檢查一次。</string> <string name="phantom_instructions_off">可以啟動 Steam 會話。</string> <string name="phantom_instructions_not_applicable">此 Android 版本不使用此限制。</string> <string name="phantom_fix_toggle">在開發人員選項中開啟「停用子進程限制」,之後請保持開發人員選項開啟。</string> <string name="phantom_fix_no_toggle">此 Android 版本沒有此開關。請使用無線偵錯,直接在此裝置上更改。</string> <string name="upd_block_no_apk">此版本沒有適用於此 DroidDeck 版本的下載。</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">Android 不會以此版本覆蓋安裝,因為其 versionCode %1$d 低於已安裝的 %2$d。</string> <string name="upd_fail_downgrade">Android 不會以舊版本覆蓋較新的版本。請繼續使用此版本,直至該頻道追上為止。</string> <string name="upd_fail_signature">此 DroidDeck 的簽署方式不同(可能是本機自行建置的版本?),因此 Android 無法直接更新。</string> <string name="upd_fail_storage">可用空間不足,無法更新。</string> <string name="upd_fail_aborted">已取消更新。</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">更新未能安裝:%1$s</string> <string name="upd_fail_generic">更新未能安裝。</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s zip</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s 尚未啟動過</string> <string name="saves_none_found">找不到 %1$s 的存檔</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s 沒有 drive_c 資料夾,因此不是 GameHub 或 Winlator 存檔 zip</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">無法在 %1$s 中建立資料夾</string> <string name="gstore_cannot_write">無法寫入 %1$s</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">無法寫入 %1$s(%2$s)</string> <string name="gfiles_folder_unavailable">無法使用資料夾</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">另一個檔案已使用 %1$s</string> <string name="gfiles_art_unavailable">無法使用圖片資料夾</string> <string name="gfiles_unrelated_inventory">資料夾中有不相關的同步清單</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">無法移除 %1$s</string> <string name="gfiles_outside_folder">檔案超出匯出資料夾範圍</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX(沒有版本名稱的版本)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">%1$s 中沒有 profile.json</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s 不是 Linux 元件套件(類型為 %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · 可用 %2$s</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">自動</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">更新目錄未通過檢查(%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeck 子進程設定</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">已登入 · 離線模式</string> <string name="setup_signed_in_unnamed">已登入</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">內置</string> <string name="driver_tag_downloaded">已下載</string> <string name="driver_tag_imported">已匯入</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s。%2$s\n\n%3$s</string> <string name="display_session">會話顯示</string> <string name="display_resolution">解像度</string> diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index e5fc5d76a..5e37513a1 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1141,16 +1141,12 @@ <string name="game_file_exported">遊戲檔案已儲存到 %1$s</string> <string name="game_frontend_files">前端檔案</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">請等待執行環境作業完成</string> <string name="main_wait_proton_install">請等待相容性工具安裝完成</string> <string name="main_wait_desktop_install">請等待桌面應用程式安裝完成</string> <string name="main_wait_install_game">請等待安裝完成後再啟動遊戲</string> <string name="main_saves_close_session">請先關閉 Steam 工作階段,以免遊戲存檔覆蓋匯入的內容</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">正在匯入到 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="other">已從 %2$s 匯入 %1$d 個檔案到 %3$s</item> </plurals> @@ -1158,15 +1154,11 @@ <item quantity="other">已從 %2$s 匯入 %1$d 個檔案到 %3$s。舊存檔已備份到 Download/DroidDeck/Saves/backups</item> </plurals> <string name="main_saves_pick_zip">選擇 %1$s 的存檔壓縮檔</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">正在匯出 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="other">已將 %1$d 個檔案匯出為 %2$s:%3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">選擇 %1$s 的儲存位置(%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">失敗(%1$s):%2$s</string> <string name="main_no_logs_yet">還沒有工作階段日誌:請先執行一次工作階段。</string> <string name="main_no_logs_to_clear">沒有可清除的工作階段日誌。</string> @@ -1179,68 +1171,55 @@ <string name="main_adb_copied">已複製 ADB 指令</string> <string name="main_pick_roms">選擇 ROM 資料夾</string> <string name="main_non_adreno_title">不是 Adreno GPU</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string> <string name="main_install_anyway">仍要安裝</string> <string name="main_remove_runtime_title">移除 Linux 執行環境</string> <string name="main_remove_runtime_text">這會刪除執行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">已套用:%1$s</string> <string name="android_app_open_failed_primary">無法在主螢幕上開啟 %1$s</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">無法在 %2$s 上開啟 %1$s</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">無法在螢幕 %2$d 上開啟 %1$s</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">正在匯入</string> <string name="comp_busy_swapping">正在換入</string> <string name="comp_busy_restoring">正在還原</string> <string name="comp_busy_cancelling">正在取消</string> <string name="comp_busy_deleting">正在刪除</string> <string name="comp_cannot_read">無法讀取檔案</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">已匯入 %1$s</string> <string name="comp_swap_cancelled">已取消等待中的換入。</string> <string name="comp_pick_wcp">選擇元件套件(-linux .wcp)</string> <string name="comp_nightlies_unreachable">無法連線到 Nightlies</string> - <!-- %1$s is the package version --> <string name="comp_stored">已儲存 %1$s</string> <string name="comp_download_failed">下載失敗:%1$s</string> <string name="drivers_pick_linux">選擇 Linux 執行環境驅動程式(-Linux zip 或 Android + Linux 套件組合)</string> <string name="drivers_pick_android">選擇顯示驅動程式(AdrenoTools zip 或 Android + Linux 套件組合)</string> <string name="drivers_pick_any">選擇驅動程式 zip 或 Android + Linux 套件組合</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s:%2$s</string> <string name="main_removing_named">正在移除 %1$s…</string> <string name="main_forgetting_named">正在忘記 %1$s…</string> <string name="main_pick_game_storage">選擇遊戲儲存資料夾</string> <string name="main_pick_decky_zip">選擇 Decky 外掛程式 ZIP</string> <string name="main_storage_not_usable">無法使用:%1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">程式</string> <string name="main_removing_runtime">正在移除 Linux 執行環境</string> <string name="main_settings_main_screen">無法在底部螢幕開啟「設定」,已改在主螢幕上開啟。</string> <string name="main_open_dev_options">在「設定」中開啟開發人員選項</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">無法開啟 Android 的「安裝不明應用程式」設定</string> <string name="upd_couldnt_check">無法檢查:%1$s</string> <string name="upd_no_download">此組建版本沒有適用於此 DroidDeck 複本的下載檔案</string> <string name="upd_stop_first">請先停止正在執行的工作階段,再進行更新</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">正在下載</string> <string name="upd_stage_installing">正在安裝</string> <string name="upd_stage_confirm">等待你確認</string> <string name="upd_failed">更新失敗</string> <string name="upd_not_offered">Android 未提供安裝此更新的選項</string> - <!-- Decky --> <string name="decky_install_failed">Decky 安裝失敗:%1$s</string> <string name="decky_plugin_starting">正在開始匯入外掛程式…</string> <string name="decky_plugin_failed">外掛程式匯入失敗:%1$s</string> <string name="decky_plugin_installed">外掛程式已安裝。請重新啟動 Steam 以載入。</string> - <!-- GPU drivers --> <string name="drivers_not_checked">尚未檢查 - 點擊重新整理以尋找新驅動程式</string> <string name="drivers_bundle_sets_display">Android + Linux:同時設定顯示驅動程式</string> <string name="drivers_bundle_sets_runtime">Android + Linux:同時設定執行環境驅動程式</string> @@ -1248,16 +1227,13 @@ <string name="drivers_system_vulkan">系統 Vulkan:沒有適用於此 GPU 的內建版本</string> <string name="drivers_bundled">%1$s(內建)</string> <string name="drivers_manual">手動</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">目前只發布了其中一半</string> <string name="drivers_pair_size">兩者共 %1$.0f MB</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">沒有可設定的驅動程式:%1$s</string> <string name="drivers_checking_latest">正在檢查最新驅動程式…</string> <string name="drivers_up_to_date_check_failed">已是最新(無法檢查:%1$s)</string> <string name="drivers_recommended_unpublished">建議的驅動程式目前尚未發布</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">正在下載 %1$s %2$s…</string> <string name="drivers_updated_to">已更新到 %1$s %2$s。</string> <string name="drivers_updated_to_restart">已更新到 %1$s %2$s。顯示驅動程式會在 DroidDeck 重新啟動後生效。</string> @@ -1272,9 +1248,7 @@ <string name="drivers_using_bundle_restart">正在將 %1$s 同時用作執行環境驅動程式和顯示驅動程式。顯示驅動程式會在 DroidDeck 重新啟動後生效。</string> <string name="drivers_deleted_bundle">已刪除 %1$s(兩個驅動程式)</string> <string name="drivers_deleted">已刪除 %1$s</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">最新:%1$s · 檢查於 %2$s</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">最新:%1$s · 無法連線到 %2$s · 檢查於 %3$s</string> <plurals name="drivers_ago_minutes"> <item quantity="other">%1$d 分鐘前</item> @@ -1287,33 +1261,25 @@ </plurals> <string name="drivers_checking_sources">正在檢查 Banners-Turnip、WinNative 和 DroidDeck…</string> <string name="drivers_check_failed">檢查失敗</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">已安裝 %1$s - 在任一清單中選擇它即可同時設定兩個驅動程式</string> <string name="drivers_installed_pick">已安裝 %1$s - 請在選單中選擇</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">請等待執行環境移除完成</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">遊戲</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">觸控板</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">直接</string> <string name="session_swap_failed">換入失敗:%1$s</string> <string name="session_profile_save_failed">設定檔儲存失敗:%1$s</string> <string name="session_desktop_paused">桌面已暫停</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s 已暫停</string> <string name="session_program_paused">程式已暫停</string> <string name="session_starting_desktop">正在啟動桌面</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">正在啟動 %1$s</string> <string name="session_starting_program">正在啟動程式</string> <string name="session_no_logs">此工作階段沒有日誌。</string> <string name="session_logs_archive_failed">無法建立工作階段日誌封存檔。</string> <string name="session_logs_share_failed">無法分享工作階段日誌。</string> <string name="session_starting_session">正在啟動工作階段…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">正在下載 Linux 執行環境</string> <string name="session_runtime_downloading_mb">正在下載 Linux 執行環境 · %1$d/%2$d MB</string> <string name="session_runtime_checking">正在檢查 Linux 執行環境</string> @@ -1328,27 +1294,20 @@ <string name="session_proton_unpacking">正在解壓縮 Proton Experimental(ARM64)</string> <string name="session_runtime_catalog_unreachable">無法連線到執行環境目錄。請檢查網路連線,然後再按一次「執行」。</string> <string name="session_runtime_install_failed">Linux 執行環境未能安裝。請在「設定」中檢查執行環境,然後再按一次「執行」。</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">桌面未能安裝(%1$s)。請檢查網路連線,然後再按一次「桌面」。</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s 意外停止。請在錯誤報告中附上日誌,或再試一次。</string> <string name="session_ended_program">程式意外停止。請在錯誤報告中附上日誌,或再試一次。</string> <string name="session_ended_desktop">桌面意外停止。請在錯誤報告中附上日誌,或再試一次。</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">結束狀態 %1$d · %2$s</string> <string name="session_ended_no_log">沒有日誌</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="other">日誌中出現 %1$d 個「Function not implemented」錯誤:proot 的 seccomp 加速在這部裝置上導致輔助程式失敗。請嘗試 %2$s →「%3$s」(或「%4$s」),然後重新啟動。</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">外掛程式</string> <string name="mode_decky_plugins_hint">安裝外掛程式 ZIP,供下次 Steam 工作階段使用。</string> <string name="mode_decky_plugin_downloading">正在下載…</string> <string name="mode_decky_plugin_install_zip">從 ZIP 安裝</string> - <!-- Store --> <string name="store_cat_games">遊戲</string> <string name="store_cat_audio_video">影音</string> <string name="store_cat_graphics">圖形</string> @@ -1361,21 +1320,14 @@ <string name="store_cat_utilities">工具程式</string> <string name="store_section_popular">熱門</string> <string name="store_section_trending">趨勢</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">最新上架</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">最近更新</string> <string name="store_section_loading">正在載入…</string> <string name="store_flatpak_setup">Flatpak 設定</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s:%2$s</string> <string name="store_updates_check_failed">無法從 Flathub 檢查更新</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">已安裝</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">資料夾</string> - <!-- Launcher themes --> <string name="theme_graphite">石墨</string> <string name="theme_graphite_detail">石墨色介面,選取、聚焦或按下的項目都使用標誌本身的球體藍。</string> <string name="theme_paper">黑底白紙</string> @@ -1384,7 +1336,6 @@ <string name="theme_icon_blue_detail">黑色背景、白色文字,選取、聚焦或按下的項目使用藍色。</string> <string name="theme_electric">電光藏青</string> <string name="theme_electric_detail">更亮的藍色,搭配偏藏青而非灰色的介面。</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1402,22 +1353,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">Space</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">請先安裝執行環境</string> <string name="game_sync_err_steam_library">無法使用 Steam 收藏庫</string> <string name="game_sync_err_previous_sd">無法使用先前的 SD 卡收藏庫</string> <string name="game_sync_err_game_storage">無法使用遊戲儲存空間</string> <string name="game_sync_err_added_games">無法使用已新增遊戲的資料夾</string> <string name="game_sync_err_manifests">無法使用 Steam 資訊清單</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">多種主機</string> - <!-- File manager --> <string name="fm_drive_internal">內部</string> <string name="fm_drive_linux">Linux 系統</string> <string name="fm_drive_sd">SD 卡</string> @@ -1435,7 +1381,6 @@ <string name="fm_no_favorites">還沒有我的最愛 - 透過資料夾的 ⋮ 選單釘選,即可快速回到這裡。</string> <string name="fm_unpin_favorite">從我的最愛移除</string> <string name="fm_size_zero">0 位元組</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">位元組</string> <string name="fm_prop_location">位置</string> <string name="fm_prop_type">類型</string> @@ -1443,7 +1388,6 @@ <string name="fm_prop_modified">修改日期</string> <string name="fm_type_folder">資料夾</string> <string name="fm_type_file">檔案</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$s 檔案</string> <string name="fm_read_only">唯讀</string> <string name="fm_read_only_hint">防止遊戲(和 Wine)覆寫或刪除此檔案。</string> @@ -1457,15 +1401,11 @@ <string name="fm_delete_failed">刪除失敗</string> <string name="fm_op_moving">正在移動...</string> <string name="fm_op_copying">正在複製...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">正在移動 - %1$s</string> <string name="fm_op_copying_item">正在複製 - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">正在移動 %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">正在複製 %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d 個完成,%2$d 個失敗</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d 個完成,%2$d 個略過</string> <plurals name="fm_paste_moved"> <item quantity="other">已移動 %1$d 個項目</item> @@ -1473,7 +1413,6 @@ <plurals name="fm_paste_copied"> <item quantity="other">已複製 %1$d 個項目</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">「%1$s」已存在</string> <string name="fm_op_renaming">正在重新命名...</string> <string name="fm_rename_failed">重新命名失敗</string> @@ -1484,17 +1423,14 @@ <string name="fm_create">建立</string> <string name="fm_new_name">新名稱</string> <string name="fm_delete_title">要刪除嗎?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">要永久刪除「%1$s」嗎?</string> <plurals name="fm_delete_items_title"> <item quantity="other">要刪除 %1$d 個項目嗎?</item> </plurals> <string name="fm_cannot_undo">此動作無法復原。</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="other">…及其他 %1$d 個</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">正在刪除 %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="other">有 %1$d 個無法刪除</item> @@ -1508,7 +1444,6 @@ <string name="fm_conflict_skip">略過</string> <string name="fm_select_this_folder">選擇此資料夾</string> <string name="fm_back">返回</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s 已掛載,但目前無法讀取</string> <string name="fm_list_view">清單檢視</string> <string name="fm_grid_view">格狀檢視</string> @@ -1523,41 +1458,31 @@ <string name="fm_hide_favorites">隱藏我的最愛</string> <string name="fm_show_favorites">顯示我的最愛</string> <string name="fm_filter_folder">篩選此資料夾</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">可用 %1$s</string> <string name="fm_selected_count">已選取 %1$d 個</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">全不選</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">全選</string> <string name="fm_paste">貼上</string> <plurals name="fm_items"> <item quantity="other">%1$d 個項目</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">將 %1$s 貼到這裡</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">將 %1$s 移動到這裡</string> <string name="fm_rail_title">檔案</string> <string name="fm_rail_storage">儲存空間</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">快速存取</string> <string name="fm_rail_favorites">我的最愛</string> <string name="fm_rail_downloads">下載</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">下載</string> <string name="fm_rail_roms">ROM</string> <string name="fm_rail_session_logs">工作階段日誌</string> <string name="fm_rail_pictures">圖片</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">已將「%1$s」加入我的最愛</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">已將「%1$s」從我的最愛移除</string> <string name="fm_empty_directory">空的資料夾</string> <string name="fm_up_one_level">返回上一層</string> <string name="fm_choose_folder">選擇資料夾</string> <string name="fm_choose_file">選擇檔案</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steam 藍</string> <string name="ctrl_tint_sky">天藍</string> <string name="ctrl_tint_white">白色</string> @@ -1566,7 +1491,6 @@ <string name="ctrl_tint_amber">琥珀</string> <string name="ctrl_tint_red">紅色</string> <string name="ctrl_tint_pink">粉紅</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">A 按鈕</string> <string name="ctrl_button_b">B 按鈕</string> <string name="ctrl_button_x">X 按鈕</string> @@ -1577,28 +1501,21 @@ <string name="ctrl_button_rt">右扳機</string> <string name="ctrl_button_view">檢視按鈕</string> <string name="ctrl_button_menu">選單按鈕</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">檢視</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">選單</string> <string name="ctrl_target_up">方向鍵上</string> <string name="ctrl_target_down">方向鍵下</string> <string name="ctrl_target_left">方向鍵左</string> <string name="ctrl_target_right">方向鍵右</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">隱藏</string> <string name="ctrl_editor_drag">拖曳以移動</string> <string name="ctrl_editor_snap">對齊</string> <string name="ctrl_editor_save">儲存</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">無</string> <string name="second_mode_keyboard_trackpad">鍵盤 + 觸控板</string> <string name="second_mode_terminal">終端機</string> <string name="second_mode_deck_controls">Deck 握把 + 觸控板</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">正在移除 Linux 執行環境</string> <plurals name="rtinst_removing_entries"> <item quantity="other">正在移除 Linux 執行環境 · %1$d 個項目</item> @@ -1613,63 +1530,47 @@ <string name="rtinst_title_installing">正在安裝 Linux 執行環境</string> <string name="rtinst_title_removing">正在移除 Linux 執行環境</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">尚未安裝 Linux 執行環境</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">目錄中沒有 %1$s 的校驗碼</string> <string name="deskpkg_checksum_mismatch">校驗碼不符 - 未進行任何變更</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">正在安裝 %1$s</string> <string name="deskpkg_place_failed">無法放置 AppImage</string> <string name="deskpkg_extract_failed">解壓縮失敗</string> <string name="deskpkg_install_failed">安裝失敗</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpak 設定失敗(結束代碼 %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpak 設定未完成</string> <string name="flatpakmgr_busy">另一個商店工作正在執行</string> <string name="flatpakmgr_not_ready">請先設定 Flatpak</string> <string name="flatpakmgr_starting">正在啟動</string> <string name="flatpakmgr_exit_status">Flatpak 已結束,狀態碼 %1$d</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">正在安裝 %1$s</string> <string name="flatpakmgr_updating">正在更新 %1$s</string> <string name="flatpakmgr_removing">正在移除 %1$s</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">正在安裝 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">正在更新 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">正在移除 %1$s(%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">翻譯</string> <string name="flatpakmgr_ref_graphics_drivers">圖形驅動程式</string> <string name="flatpakmgr_ref_media_codecs">媒體轉碼器</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$s 執行環境</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$s 執行環境 %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">檔案太小,不可能是 AppImage</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">無法讀取檔案:%1$s</string> <string name="appimg_not_linux">這不是 AppImage(不是 Linux 程式)</string> <string name="appimg_wrong_arch">此 AppImage 既不是為 ARM64(aarch64)也不是為 x86 電腦組建的</string> <string name="appimg_not_appimage">這是 Linux 程式,但不是 AppImage</string> <string name="appimg_wrong_type">只能匯入第 2 型 AppImage(此檔案為第 %1$d 型)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">可用空間不足:匯入需要約 %1$s</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">可用空間不足:更新需要約 %1$s</string> <string name="appimg_import_failed">匯入失敗</string> <string name="appimg_gone">該 AppImage 已不存在</string> <string name="appimg_update_failed">更新失敗</string> - <!-- %1$s: file name --> <string name="appimg_extracting">正在解壓縮 %1$s</string> <string name="appimg_extract_failed">無法解壓縮 AppImage</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">無法解壓縮 AppImage:%1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">安裝 Decky Loader 前,請先停止執行中的工作階段</string> <string name="deckymgr_download_failed">Decky Loader 下載失敗</string> <string name="deckymgr_size_mismatch">Decky Loader 的大小與發行版本中繼資料不符</string> @@ -1705,20 +1606,13 @@ <string name="deckymgr_remote_entry_invalid">remote_binary 項目無效</string> <string name="deckymgr_remote_name_invalid">遠端二進位檔名稱無效</string> <string name="deckymgr_remote_url_https">遠端二進位檔 URL 必須使用 HTTPS</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">遠端二進位檔 %1$s 的 SHA-256 雜湊值無效</string> <string name="deckymgr_bin_dir_failed">無法建立外掛程式的 bin 目錄</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">正在下載外掛程式二進位檔 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">無法下載外掛程式二進位檔 %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">外掛程式二進位檔校驗碼不符 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">無法將外掛程式二進位檔標示為可執行 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">無法取代外掛程式二進位檔 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">無法安裝外掛程式二進位檔 · %1$s</string> <string name="deckymgr_folder_taken">外掛程式資料夾屬於另一個外掛程式</string> <string name="deckymgr_preserve_failed">無法保留現有的外掛程式</string> @@ -1726,20 +1620,14 @@ <string name="deckymgr_settings_failed">無法更新 Decky 外掛程式設定</string> <string name="deckymgr_restore_failed">無法還原現有的外掛程式</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">無法儲存無線偵錯身分</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADB 指令失敗(%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android 未確認子處理程序限制已變更:%1$s</string> <string name="adbfix_no_pairing">尚未儲存任何無線偵錯配對</string> <string name="adbfix_enter_address">請輸入目前的無線偵錯 IP 位址和連接埠</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">等待配對彈出視窗逾時。請從 DroidDeck 再試一次。</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">無法尋找配對彈出視窗(%1$d)。請確認 Wi-Fi 已開啟。</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">無法尋找配對彈出視窗:%1$s</string> <string name="adbpair_six_digits">請輸入完整的 6 位數配對碼</string> <string name="adbpair_pairing">正在配對…</string> @@ -1753,27 +1641,22 @@ <string name="adbpair_title_pair">配對無線偵錯</string> <string name="adbpair_waiting_text">開啟無線偵錯,然後點擊「使用配對碼配對裝置」。</string> <string name="adbpair_waiting_long">開啟無線偵錯,然後點擊「使用配對碼配對裝置」。請留在「設定」中:配對碼要輸入在這則通知裡。</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6 位數配對碼</string> <string name="adbpair_code_hint">點擊「輸入配對碼」,然後輸入「設定」中顯示的 6 位數字。</string> <string name="adbpair_title_code">輸入 Wi-Fi 配對碼</string> <string name="adbpair_title_retry">請再次輸入配對碼</string> - <!-- Notification action button --> <string name="adbpair_enter_code">輸入配對碼</string> <string name="adbpair_title_working">正在設定 Steam</string> <string name="adbpair_title_done">Steam 已就緒</string> <string name="adbpair_done_text">點擊即可返回 DroidDeck。現在可以關閉無線偵錯了。</string> <string name="adbpair_title_failed">無線偵錯設定已停止</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">下載回應 HTTP %1$d</string> <string name="selfinst_size_mismatch">下載的大小與發布的 APK 不符,已捨棄 - 請再試一次</string> <string name="selfinst_checksum_mismatch">下載的檔案與校驗碼不符,已捨棄 - 請再試一次</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">此更新適用於 Android 套件 %1$s,而非 %2$s</string> <string name="selfinst_metadata_signer">更新中繼資料未列出 DroidDeck 的發行簽署金鑰</string> <string name="selfinst_unreadable">Android 無法讀取下載的 APK</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">下載的 APK 適用於 %1$s,而非 %2$s</string> <string name="selfinst_version_mismatch">下載的 APK 標示 versionCode 為 %1$d,與發布的 %2$d 不符</string> <string name="selfinst_downgrade">Android 不會以 versionCode %1$d 覆蓋已安裝的 %2$d</string> @@ -1781,16 +1664,11 @@ <string name="selfinst_wrong_signer">下載的 APK 並非以 DroidDeck 的發行金鑰簽署</string> <string name="appupd_http">更新目錄回應 HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">正在啟動工作階段…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">已經過 %1$d:%2$02d · 仍在處理中</string> <string name="loadstate_steam_update">正在下載 Steam 用戶端更新 · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">正在下載 Steam 用戶端 · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">已有相容性工具正在安裝</string> <string name="pextra_stop_session">安裝相容性工具前,請先停止執行中的工作階段</string> <string name="pextra_no_release">找不到 %1$s 的 ARM64 發行版本</string> @@ -1807,48 +1685,38 @@ <string name="pextra_checksum_mismatch">校驗碼不符;已刪除下載內容</string> <string name="pextra_session_started">下載期間有工作階段啟動;請先停止它,再安裝相容性工具</string> <string name="pextra_installing">正在安裝 %1$s</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$s 安裝失敗(結束代碼 %2$d)</string> <string name="pextra_could_not_install">無法安裝 %1$s</string> <string name="esync_fetching">正在取得 droiddeck-esync 套件</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">含 Linux 輔助程式</string> <string name="cmgr_fex_dlls_only">僅 DLL</string> <string name="cmgr_original_proton_updated">原始(上次換入後 Proton 已更新)</string> <string name="cmgr_changed_outside">已在「元件」之外變更</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">不存在</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">原始 %1$s</string> <string name="cmgr_original_from">原始版本來自 %1$s</string> <string name="cmgr_swap_queued">%1$s %2$s 會在 %3$s 上執行的遊戲關閉後換入。</string> <string name="cmgr_swap_done">%1$s %2$s 已換入 %3$s。下次啟動遊戲時生效。</string> <string name="cmgr_original_missing">未儲存該原始套件組合</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%2$s 的 %1$s 會在其上執行的遊戲關閉後還原為原始版本。</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">已從 %3$s 的原始版本還原 %2$s 的 %1$s。</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%2$s 正在使用 %1$s。請先為該 Proton 換入其他版本。</string> <string name="cmgr_package_not_stored">未儲存 %1$s</string> <string name="cmgr_deleted">已刪除 %1$s。</string> <string name="cmgr_original_kept">已安裝組建的原始版本會保留,以便還原。</string> <string name="cmgr_not_stored">未儲存</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">已刪除 %2$s 來自 %1$s 的原始版本。</string> <string name="cmgr_nightlies_only">只會從 Nightlies 發行版本下載</string> <string name="cmgr_list_predates_checksums">此套件清單早於校驗碼機制 - 請重新整理後再試一次</string> <string name="cmgr_checksum_mismatch">校驗碼不符</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">線性</string> <string name="sprefs_upscaler_nearest">最近鄰</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR(品質)</string> <string name="sprefs_upscaler_sharpen">僅銳化</string> <string name="sprefs_lod_bias_auto">自動(依縮放)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">未套用任何效果。</string> <string name="look_game_clarity">遊戲清晰</string> <string name="look_game_clarity_desc">銳化並稍微提高對比 - 適合日常使用。</string> @@ -1873,27 +1741,23 @@ <string name="look_anime_edge">動畫描邊</string> <string name="look_anime_edge_desc">卡通描邊加上銳化。</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">不是 Adreno GPU</string> <string name="gpuinfo_supported">受支援</string> <string name="gpuinfo_experimental">實驗性:其驅動程式為測試版本</string> <string name="gpuinfo_below_tested">低於已測試的硬體(Adreno 725 及更新機型):可能無法執行</string> <string name="gpuinfo_unsupported">不受支援:DroidDeck 需要 Adreno(Snapdragon)GPU</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">已達 GitHub 速率限制 - 請稍後再試</string> <string name="turnip_github_http">GitHub 回應 HTTP %1$d</string> <string name="turnip_download_http">下載回應 HTTP %1$d</string> <string name="turnip_list_predates_checksums">此驅動程式清單早於校驗碼機制 - 請重新整理後再試一次</string> <string name="turnip_checksum_mismatch">校驗碼不符 - 已捨棄下載內容</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">不是 AdrenoTools 驅動程式:%1$s</string> <string name="turnip_reject_no_meta">沒有 meta.json</string> <string name="turnip_reject_meta_unreadable">無法讀取 meta.json(%1$s)</string> <string name="turnip_reject_linux_driver">這是 Linux 執行環境驅動程式(請在「執行環境驅動程式」下匯入)</string> <string name="turnip_reject_wayland">這是 Wayland 遊戲驅動程式,此應用程式用不到</string> <string name="turnip_reject_no_library_name">meta.json 未指定 libraryName,因此無法交給 AdrenoTools</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json 指定了 %1$s,但 zip 中沒有此檔案</string> <string name="turnip_reject_not_aarch64">%1$s 不是 64 位元 AArch64 ELF 共用程式庫</string> <string name="turnip_reject_glibc">%1$s 連結 glibc - 這是「-Linux」Turnip,應放在「執行環境驅動程式」下</string> @@ -1904,90 +1768,61 @@ <string name="bundle_entry_outside">不是驅動程式套件組合:%1$s 指向組合外部</string> <string name="bundle_too_large">不是驅動程式套件組合:解壓縮後超過 %1$d MB</string> - <!-- Game storage --> <string name="gstore_sd_card">SD 卡</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">資料夾</string> <string name="logshare_chooser">分享工作階段日誌</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">需要 Android 10 或更新版本(具備色彩空間的顯示圖層)</string> <string name="hdr_none">此顯示器未回報支援 HDR</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">此顯示器支援 %1$s,但不支援 HDR10</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">此 GPU</string> <string name="phantom_title_on">子處理程序限制已開啟</string> <string name="phantom_title_unset">子處理程序限制未設定</string> <string name="phantom_title_unreadable">無法檢查子處理程序限制</string> <string name="phantom_title_off">子處理程序限制已關閉</string> <string name="phantom_title_not_required">不需要變更子處理程序限制</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s 如果無法這麼做,請將此裝置連接到裝有 ADB 的電腦並執行下方指令,然後返回這裡重新檢查。</string> <string name="phantom_instructions_off">可以啟動 Steam 工作階段。</string> <string name="phantom_instructions_not_applicable">此 Android 版本不使用這項限制。</string> <string name="phantom_fix_toggle">在開發人員選項中開啟「停用子處理程序限制」,之後請保持開發人員選項開啟。</string> <string name="phantom_fix_no_toggle">此 Android 版本沒有這個開關。請使用無線偵錯,直接在此裝置上變更。</string> <string name="upd_block_no_apk">此組建版本沒有適用於此 DroidDeck 複本的下載檔案。</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">Android 不會以此版本覆蓋目前的組建,因為它的 versionCode %1$d 低於已安裝的 %2$d。</string> <string name="upd_fail_downgrade">Android 不會以舊版本覆蓋新版本。請先留在此組建版本,等更新頻道趕上。</string> <string name="upd_fail_signature">此 DroidDeck 複本的簽章不同(是本機組建的嗎?),因此 Android 無法直接覆寫更新。</string> <string name="upd_fail_storage">可用空間不足,無法更新。</string> <string name="upd_fail_aborted">已取消更新。</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">未能安裝更新:%1$s</string> <string name="upd_fail_generic">未能安裝更新。</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s 壓縮檔</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">壓縮檔</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s 尚未啟動過</string> <string name="saves_none_found">找不到 %1$s 的存檔</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s 中沒有 drive_c 資料夾,因此不是 GameHub 或 Winlator 存檔壓縮檔</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">無法在 %1$s 中建立資料夾</string> <string name="gstore_cannot_write">無法寫入 %1$s</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">無法寫入 %1$s(%2$s)</string> <string name="gfiles_folder_unavailable">無法使用資料夾</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">已有其他檔案使用 %1$s</string> <string name="gfiles_art_unavailable">無法使用圖片資料夾</string> <string name="gfiles_unrelated_inventory">資料夾中有不相關的同步清單</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">無法移除 %1$s</string> <string name="gfiles_outside_folder">檔案位於匯出資料夾之外</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX(未標示版本名稱的組建)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">%1$s 中沒有 profile.json</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s 不是 Linux 元件套件(類型 %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · 可用 %2$s</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">自動</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">更新目錄未通過檢查(%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeck 子處理程序設定</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">已登入 · 離線模式</string> <string name="setup_signed_in_unnamed">已登入</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">內建</string> <string name="driver_tag_downloaded">已下載</string> <string name="driver_tag_imported">已匯入</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s。%2$s\n\n%3$s</string> <string name="display_session">工作階段顯示</string> <string name="display_resolution">解析度</string> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3415cc58d..cc33a651f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1121,11 +1121,8 @@ <string name="proton_installed">Installed %1$s</string> <string name="proton_queued">Pending from an earlier request</string> <string name="proton_not_installed">Not installed</string> - <!-- %1$s is the newer release's tag --> <string name="proton_update_available">Update available → %1$s</string> - <!-- %1$s is the Proton's name (GE-Proton, proton-cachyos, DroidDeck-Proton) --> <string name="proton_remove_older">Remove old %1$s versions</string> - <!-- %1$s lists the folders that are removed --> <string name="proton_remove_older_body">Removes %1$s. Games set to these versions must be switched to another Proton.</string> <string name="proton_remove_body">Games set to this version must be switched to another Proton.</string> <string name="proton_install_now">Install now</string> @@ -1183,16 +1180,12 @@ <string name="game_file_exported">Game files saved to %1$s</string> <string name="game_frontend_files">Frontend files</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">Wait for the runtime operation to finish</string> <string name="main_wait_proton_install">Wait for the compatibility tool install to finish</string> <string name="main_wait_desktop_install">Wait for the desktop app install to finish</string> <string name="main_wait_install_game">Wait for the install to finish before launching a game</string> <string name="main_saves_close_session">Close the Steam session first, so the game can\'t save over the import</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">Importing into %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="one">Imported %1$d file from the %2$s into %3$s</item> <item quantity="other">Imported %1$d files from the %2$s into %3$s</item> @@ -1202,16 +1195,12 @@ <item quantity="other">Imported %1$d files from the %2$s into %3$s. Old saves backed up to Download/DroidDeck/Saves/backups</item> </plurals> <string name="main_saves_pick_zip">Choose a save zip for %1$s</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">Exporting %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="one">Exported %1$d file as a %2$s: %3$s</item> <item quantity="other">Exported %1$d files as a %2$s: %3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">Choose where to save %1$s (%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">%1$s failed: %2$s</string> <string name="main_no_logs_yet">No session logs yet: run a session first.</string> <string name="main_no_logs_to_clear">No session logs to clear.</string> @@ -1225,68 +1214,55 @@ <string name="main_adb_copied">ADB command copied</string> <string name="main_pick_roms">Choose the ROMs folder</string> <string name="main_non_adreno_title">Not an Adreno GPU</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip supports Adreno GPUs. On %1$s, Steam may show a black screen. Download: %2$.0f MB.</string> <string name="main_install_anyway">Install anyway</string> <string name="main_remove_runtime_title">Remove Linux runtime</string> <string name="main_remove_runtime_text">This deletes the runtime, the Steam client inside it, and every game installed there.</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">Applied: %1$s</string> <string name="android_app_open_failed_primary">Could not open %1$s on the primary screen</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">Could not open %1$s on %2$s</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">Could not open %1$s on display %2$d</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">Importing</string> <string name="comp_busy_swapping">Swapping</string> <string name="comp_busy_restoring">Restoring</string> <string name="comp_busy_cancelling">Cancelling</string> <string name="comp_busy_deleting">Deleting</string> <string name="comp_cannot_read">cannot read the file</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">Imported %1$s</string> <string name="comp_swap_cancelled">The waiting swap was cancelled.</string> <string name="comp_pick_wcp">Choose a component package (-linux .wcp)</string> <string name="comp_nightlies_unreachable">The Nightlies could not be reached</string> - <!-- %1$s is the package version --> <string name="comp_stored">Stored %1$s</string> <string name="comp_download_failed">Download failed: %1$s</string> <string name="drivers_pick_linux">Choose a Linux runtime driver (-Linux zip or Android + Linux bundle)</string> <string name="drivers_pick_android">Choose a display driver (AdrenoTools zip or Android + Linux bundle)</string> <string name="drivers_pick_any">Choose a driver zip or an Android + Linux bundle</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s: %2$s</string> <string name="main_removing_named">Removing %1$s…</string> <string name="main_forgetting_named">Forgetting %1$s…</string> <string name="main_pick_game_storage">Choose the game storage folder</string> <string name="main_pick_decky_zip">Choose a Decky plugin ZIP</string> <string name="main_storage_not_usable">Not usable: %1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">Program</string> <string name="main_removing_runtime">Removing Linux runtime</string> <string name="main_settings_main_screen">Could not open Settings on the bottom screen; opened it on the main screen.</string> <string name="main_open_dev_options">Open Developer options in Settings</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">Couldn\'t open Android\'s \"Install unknown apps\" setting</string> <string name="upd_couldnt_check">Couldn\'t check: %1$s</string> <string name="upd_no_download">This build has no download for this copy of DroidDeck</string> <string name="upd_stop_first">Stop the running session first, then update</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">Downloading</string> <string name="upd_stage_installing">Installing</string> <string name="upd_stage_confirm">Waiting for you to confirm</string> <string name="upd_failed">The update failed</string> <string name="upd_not_offered">Android didn\'t offer to install the update</string> - <!-- Decky --> <string name="decky_install_failed">Decky install failed: %1$s</string> <string name="decky_plugin_starting">Starting plugin import…</string> <string name="decky_plugin_failed">Plugin import failed: %1$s</string> <string name="decky_plugin_installed">Plugin installed. Restart Steam to load it.</string> - <!-- GPU drivers --> <string name="drivers_not_checked">Not checked yet - tap refresh to look for new drivers</string> <string name="drivers_bundle_sets_display">Android + Linux: sets the display driver with it</string> <string name="drivers_bundle_sets_runtime">Android + Linux: sets the runtime driver with it</string> @@ -1294,16 +1270,13 @@ <string name="drivers_system_vulkan">system Vulkan: no bundled build for this GPU</string> <string name="drivers_bundled">%1$s (bundled)</string> <string name="drivers_manual">Manual</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">Only one half is published right now</string> <string name="drivers_pair_size">%1$.0f MB for both</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">No drivers to set: %1$s</string> <string name="drivers_checking_latest">Checking for the latest drivers…</string> <string name="drivers_up_to_date_check_failed">Up to date (couldn\'t check: %1$s)</string> <string name="drivers_recommended_unpublished">The recommended drivers aren\'t published right now</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">Downloading %1$s %2$s…</string> <string name="drivers_updated_to">Updated to %1$s %2$s.</string> <string name="drivers_updated_to_restart">Updated to %1$s %2$s. The display driver applies after DroidDeck restarts.</string> @@ -1318,9 +1291,7 @@ <string name="drivers_using_bundle_restart">Using %1$s as both the runtime and display driver. The display driver applies after DroidDeck restarts.</string> <string name="drivers_deleted_bundle">Deleted %1$s (both drivers)</string> <string name="drivers_deleted">Deleted %1$s</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">Latest: %1$s · checked %2$s</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">Latest: %1$s · %2$s unreachable · checked %3$s</string> <plurals name="drivers_ago_minutes"> <item quantity="one">%1$d min ago</item> @@ -1336,33 +1307,25 @@ </plurals> <string name="drivers_checking_sources">Checking Banners-Turnip, WinNative and DroidDeck…</string> <string name="drivers_check_failed">check failed</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">Installed %1$s - pick it in either list to set both drivers</string> <string name="drivers_installed_pick">Installed %1$s - pick it in the menu</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">Wait for runtime removal to finish</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">Game</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">touchpad</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">direct</string> <string name="session_swap_failed">Swap failed: %1$s</string> <string name="session_profile_save_failed">Profile save failed: %1$s</string> <string name="session_desktop_paused">The desktop is paused</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s is paused</string> <string name="session_program_paused">The program is paused</string> <string name="session_starting_desktop">Starting the desktop</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">Starting %1$s</string> <string name="session_starting_program">Starting the program</string> <string name="session_no_logs">No logs for this session.</string> <string name="session_logs_archive_failed">Could not create the session log archive.</string> <string name="session_logs_share_failed">Could not share the session logs.</string> <string name="session_starting_session">Starting the session…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">downloading the Linux runtime</string> <string name="session_runtime_downloading_mb">downloading the Linux runtime · %1$d of %2$d MB</string> <string name="session_runtime_checking">checking the Linux runtime</string> @@ -1377,28 +1340,21 @@ <string name="session_proton_unpacking">unpacking Proton Experimental (ARM64)</string> <string name="session_runtime_catalog_unreachable">Could not reach the runtime catalog. Check the connection and press Play again.</string> <string name="session_runtime_install_failed">The Linux runtime did not install. Check the runtime in Setup and press Play again.</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">The desktop did not install (%1$s). Check the connection and press Desktop again.</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s stopped unexpectedly. Share the logs with a bug report, or try again.</string> <string name="session_ended_program">The program stopped unexpectedly. Share the logs with a bug report, or try again.</string> <string name="session_ended_desktop">The desktop stopped unexpectedly. Share the logs with a bug report, or try again.</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">Exit status %1$d · %2$s</string> <string name="session_ended_no_log">no log</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="one">The log shows %1$d \"Function not implemented\" error: proot\'s seccomp acceleration is failing a helper on this device. Try %2$s → \"%3$s\" (or \"%4$s\") and start again.</item> <item quantity="other">The log shows %1$d \"Function not implemented\" errors: proot\'s seccomp acceleration is failing a helper on this device. Try %2$s → \"%3$s\" (or \"%4$s\") and start again.</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">Plugins</string> <string name="mode_decky_plugins_hint">Install a plugin ZIP for the next Steam session.</string> <string name="mode_decky_plugin_downloading">Downloading…</string> <string name="mode_decky_plugin_install_zip">Install from ZIP</string> - <!-- Store --> <string name="store_cat_games">Games</string> <string name="store_cat_audio_video">Audio & Video</string> <string name="store_cat_graphics">Graphics</string> @@ -1411,21 +1367,14 @@ <string name="store_cat_utilities">Utilities</string> <string name="store_section_popular">Popular</string> <string name="store_section_trending">Trending</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">New</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">Updated</string> <string name="store_section_loading">loading…</string> <string name="store_flatpak_setup">Flatpak setup</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s: %2$s</string> <string name="store_updates_check_failed">Could not check Flathub for updates</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">installed</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">folder</string> - <!-- Launcher themes --> <string name="theme_graphite">Graphite</string> <string name="theme_graphite_detail">Graphite surfaces, with the logo\'s own orb blue for everything selected, focused or pressed.</string> <string name="theme_paper">Paper on black</string> @@ -1434,7 +1383,6 @@ <string name="theme_icon_blue_detail">Black ground, white type, the blue for everything selected, focused or pressed.</string> <string name="theme_electric">Electric navy</string> <string name="theme_electric_detail">The blue brighter, on surfaces that lean navy instead of grey.</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1452,22 +1400,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">Space</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">Install the runtime first</string> <string name="game_sync_err_steam_library">Steam library is unavailable</string> <string name="game_sync_err_previous_sd">Previous SD library is unavailable</string> <string name="game_sync_err_game_storage">Game storage is unavailable</string> <string name="game_sync_err_added_games">Added games folder is unavailable</string> <string name="game_sync_err_manifests">Steam manifests are unavailable</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">Many systems</string> - <!-- File manager --> <string name="fm_drive_internal">Internal</string> <string name="fm_drive_linux">Linux system</string> <string name="fm_drive_sd">SD card</string> @@ -1485,7 +1428,6 @@ <string name="fm_no_favorites">No favorites yet - pin a folder with its ⋮ menu to jump back here fast.</string> <string name="fm_unpin_favorite">Remove from favorites</string> <string name="fm_size_zero">0 bytes</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">bytes</string> <string name="fm_prop_location">Location</string> <string name="fm_prop_type">Type</string> @@ -1493,7 +1435,6 @@ <string name="fm_prop_modified">Modified</string> <string name="fm_type_folder">Folder</string> <string name="fm_type_file">File</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$s file</string> <string name="fm_read_only">Read-only</string> <string name="fm_read_only_hint">Stops games (and Wine) from overwriting or deleting this file.</string> @@ -1507,15 +1448,11 @@ <string name="fm_delete_failed">Delete failed</string> <string name="fm_op_moving">Moving...</string> <string name="fm_op_copying">Copying...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">Moving - %1$s</string> <string name="fm_op_copying_item">Copying - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">Moving %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">Copying %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d done, %2$d failed</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d done, %2$d skipped</string> <plurals name="fm_paste_moved"> <item quantity="one">%1$d item moved</item> @@ -1525,7 +1462,6 @@ <item quantity="one">%1$d item copied</item> <item quantity="other">%1$d items copied</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">\"%1$s\" already exists</string> <string name="fm_op_renaming">Renaming...</string> <string name="fm_rename_failed">Rename failed</string> @@ -1536,19 +1472,16 @@ <string name="fm_create">Create</string> <string name="fm_new_name">New name</string> <string name="fm_delete_title">Delete?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">Delete \"%1$s\" permanently?</string> <plurals name="fm_delete_items_title"> <item quantity="one">Delete %1$d item?</item> <item quantity="other">Delete %1$d items?</item> </plurals> <string name="fm_cannot_undo">This can\'t be undone.</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="one">…and %1$d more</item> <item quantity="other">…and %1$d more</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">Deleting %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="one">%1$d couldn\'t be deleted</item> @@ -1563,7 +1496,6 @@ <string name="fm_conflict_skip">Skip</string> <string name="fm_select_this_folder">Select this folder</string> <string name="fm_back">Back</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s is mounted but not readable right now</string> <string name="fm_list_view">List view</string> <string name="fm_grid_view">Grid view</string> @@ -1578,42 +1510,32 @@ <string name="fm_hide_favorites">Hide favorites</string> <string name="fm_show_favorites">Show favorites</string> <string name="fm_filter_folder">Filter this folder</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">%1$s free</string> <string name="fm_selected_count">%1$d selected</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">None</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">All</string> <string name="fm_paste">Paste</string> <plurals name="fm_items"> <item quantity="one">%1$d item</item> <item quantity="other">%1$d items</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">Paste %1$s here</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">Paste %1$s (move) here</string> <string name="fm_rail_title">Files</string> <string name="fm_rail_storage">STORAGE</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">QUICK</string> <string name="fm_rail_favorites">FAVORITES</string> <string name="fm_rail_downloads">Downloads</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">Downlds</string> <string name="fm_rail_roms">ROMs</string> <string name="fm_rail_session_logs">Session logs</string> <string name="fm_rail_pictures">Pictures</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">Added \"%1$s\" to Favorites</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">Removed \"%1$s\" from Favorites</string> <string name="fm_empty_directory">Empty directory</string> <string name="fm_up_one_level">Up one level</string> <string name="fm_choose_folder">Choose a folder</string> <string name="fm_choose_file">Choose a file</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steam blue</string> <string name="ctrl_tint_sky">Sky</string> <string name="ctrl_tint_white">White</string> @@ -1622,7 +1544,6 @@ <string name="ctrl_tint_amber">Amber</string> <string name="ctrl_tint_red">Red</string> <string name="ctrl_tint_pink">Pink</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">A button</string> <string name="ctrl_button_b">B button</string> <string name="ctrl_button_x">X button</string> @@ -1633,7 +1554,6 @@ <string name="ctrl_button_rt">Right trigger</string> <string name="ctrl_button_view">View button</string> <string name="ctrl_button_menu">Menu button</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> <string name="ctrl_target_a" translatable="false">A</string> <string name="ctrl_target_b" translatable="false">B</string> <string name="ctrl_target_x" translatable="false">X</string> @@ -1645,27 +1565,21 @@ <string name="ctrl_target_l3" translatable="false">L3</string> <string name="ctrl_target_r3" translatable="false">R3</string> <string name="ctrl_target_steam" translatable="false">Steam</string> - <!-- Controller View button --> <string name="ctrl_target_view">View</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">Menu</string> <string name="ctrl_target_up">D-pad up</string> <string name="ctrl_target_down">D-pad down</string> <string name="ctrl_target_left">D-pad left</string> <string name="ctrl_target_right">D-pad right</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">Hidden</string> <string name="ctrl_editor_drag">Drag to move</string> <string name="ctrl_editor_snap">Snap</string> <string name="ctrl_editor_save">Save</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">None</string> <string name="second_mode_keyboard_trackpad">Keyboard + trackpad</string> <string name="second_mode_terminal">Terminal</string> <string name="second_mode_deck_controls">Deck grips + trackpads</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">Removing Linux runtime</string> <plurals name="rtinst_removing_entries"> <item quantity="one">Removing Linux runtime · %1$d entry</item> @@ -1681,63 +1595,47 @@ <string name="rtinst_title_installing">Installing the Linux runtime</string> <string name="rtinst_title_removing">Removing the Linux runtime</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">The Linux runtime is not installed</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">The catalog has no checksum for %1$s</string> <string name="deskpkg_checksum_mismatch">Checksum mismatch - nothing was changed</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">Installing %1$s</string> <string name="deskpkg_place_failed">Could not place the AppImage</string> <string name="deskpkg_extract_failed">Extraction failed</string> <string name="deskpkg_install_failed">Install failed</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpak setup failed (exit %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpak setup did not finish</string> <string name="flatpakmgr_busy">Another store task is running</string> <string name="flatpakmgr_not_ready">Set up Flatpak first</string> <string name="flatpakmgr_starting">Starting</string> <string name="flatpakmgr_exit_status">Flatpak exited with status %1$d</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">Installing %1$s</string> <string name="flatpakmgr_updating">Updating %1$s</string> <string name="flatpakmgr_removing">Removing %1$s</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">Installing %1$s (%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">Updating %1$s (%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">Removing %1$s (%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">translations</string> <string name="flatpakmgr_ref_graphics_drivers">graphics drivers</string> <string name="flatpakmgr_ref_media_codecs">media codecs</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$s runtime</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$s runtime %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">The file is too small to be an AppImage</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">The file cannot be read: %1$s</string> <string name="appimg_not_linux">This is not an AppImage (it is not a Linux program)</string> <string name="appimg_wrong_arch">This AppImage is built for neither ARM64 (aarch64) nor x86 PCs</string> <string name="appimg_not_appimage">This is a Linux program but not an AppImage</string> <string name="appimg_wrong_type">Only type 2 AppImages can be imported (this one is type %1$d)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">Not enough free space: importing needs about %1$s</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">Not enough free space: updating needs about %1$s</string> <string name="appimg_import_failed">Import failed</string> <string name="appimg_gone">That AppImage is gone</string> <string name="appimg_update_failed">Update failed</string> - <!-- %1$s: file name --> <string name="appimg_extracting">Extracting %1$s</string> <string name="appimg_extract_failed">The AppImage could not be extracted</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">The AppImage could not be extracted: %1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">Stop the active session before installing Decky Loader</string> <string name="deckymgr_download_failed">Decky Loader download failed</string> <string name="deckymgr_size_mismatch">Decky Loader size did not match the release metadata</string> @@ -1773,20 +1671,13 @@ <string name="deckymgr_remote_entry_invalid">Invalid remote_binary entry</string> <string name="deckymgr_remote_name_invalid">Invalid remote binary name</string> <string name="deckymgr_remote_url_https">Remote binary URL must use HTTPS</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">Remote binary %1$s has an invalid SHA-256 hash</string> <string name="deckymgr_bin_dir_failed">Could not create plugin bin directory</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">Downloading plugin binary · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">Could not download plugin binary %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">Plugin binary checksum mismatch · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">Could not mark plugin binary executable · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">Could not replace plugin binary · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">Could not install plugin binary · %1$s</string> <string name="deckymgr_folder_taken">Plugin folder belongs to another plugin</string> <string name="deckymgr_preserve_failed">Could not preserve the existing plugin</string> @@ -1794,20 +1685,14 @@ <string name="deckymgr_settings_failed">Could not update Decky plugin settings</string> <string name="deckymgr_restore_failed">Could not restore the existing plugin</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">Could not save Wireless debugging identity</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADB command failed (%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android did not confirm the child-process limit was changed: %1$s</string> <string name="adbfix_no_pairing">No Wireless debugging pairing is saved</string> <string name="adbfix_enter_address">Enter the current Wireless debugging IP address & Port</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">Timed out waiting for the pairing pop-up. Try again from DroidDeck.</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">Could not look for the pairing pop-up (%1$d). Check that Wi-Fi is on.</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">Could not look for the pairing pop-up: %1$s</string> <string name="adbpair_six_digits">Enter all 6 digits of the code</string> <string name="adbpair_pairing">Pairing…</string> @@ -1821,27 +1706,22 @@ <string name="adbpair_title_pair">Pair Wireless debugging</string> <string name="adbpair_waiting_text">Turn on Wireless debugging, then tap “Pair device with pairing code”.</string> <string name="adbpair_waiting_long">Turn on Wireless debugging, then tap “Pair device with pairing code”. Stay in Settings: the code goes in this notification.</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6-digit pairing code</string> <string name="adbpair_code_hint">Tap Enter code and type the 6 digits shown in Settings.</string> <string name="adbpair_title_code">Enter the Wi-Fi pairing code</string> <string name="adbpair_title_retry">Try the code again</string> - <!-- Notification action button --> <string name="adbpair_enter_code">Enter code</string> <string name="adbpair_title_working">Setting up Steam</string> <string name="adbpair_title_done">Steam is ready</string> <string name="adbpair_done_text">Tap to go back to DroidDeck. You can turn Wireless debugging off now.</string> <string name="adbpair_title_failed">Wireless debugging setup stopped</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">The download answered HTTP %1$d</string> <string name="selfinst_size_mismatch">The download size didn\'t match the published APK and was discarded - try again</string> <string name="selfinst_checksum_mismatch">The download didn\'t match its checksum and was discarded - try again</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">The update is for Android package %1$s, not %2$s</string> <string name="selfinst_metadata_signer">The update metadata doesn\'t name DroidDeck\'s release signing key</string> <string name="selfinst_unreadable">Android couldn\'t read the downloaded APK</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">The downloaded APK is for %1$s, not %2$s</string> <string name="selfinst_version_mismatch">The downloaded APK says versionCode %1$d, not the published %2$d</string> <string name="selfinst_downgrade">Android won\'t install versionCode %1$d over the installed %2$d</string> @@ -1849,16 +1729,11 @@ <string name="selfinst_wrong_signer">The downloaded APK isn\'t signed with DroidDeck\'s release key</string> <string name="appupd_http">The update catalog answered HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">Starting the session…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">%1$d:%2$02d elapsed · still working</string> <string name="loadstate_steam_update">Downloading the Steam client update · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">Downloading the Steam client · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos / DroidDeck-Proton (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">A compatibility tool install is already running</string> <string name="pextra_stop_session">Stop the active session before installing compatibility tools</string> <string name="pextra_no_release">Could not find an ARM64 release for %1$s</string> @@ -1875,48 +1750,38 @@ <string name="pextra_checksum_mismatch">Checksum mismatch; the download was deleted</string> <string name="pextra_session_started">A session started during the download; stop it before installing compatibility tools</string> <string name="pextra_installing">Installing %1$s</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$s installation failed (exit %2$d)</string> <string name="pextra_could_not_install">Could not install %1$s</string> <string name="esync_fetching">Fetching droiddeck-esync pack</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">with Linux helpers</string> <string name="cmgr_fex_dlls_only">DLLs only</string> <string name="cmgr_original_proton_updated">Original (Proton updated since the last swap)</string> <string name="cmgr_changed_outside">Changed outside Components</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">not present</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">Original %1$s</string> <string name="cmgr_original_from">Original from %1$s</string> <string name="cmgr_swap_queued">%1$s %2$s goes into %3$s when the game running on it closes.</string> <string name="cmgr_swap_done">%1$s %2$s is now in %3$s. It applies the next time a game starts.</string> <string name="cmgr_original_missing">That original bundle is not stored</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%1$s of %2$s goes back to its original when the game running on it closes.</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">%1$s of %2$s restored from its %3$s original.</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$s is in use in %2$s. Swap that Proton to something else first.</string> <string name="cmgr_package_not_stored">%1$s is not stored</string> <string name="cmgr_deleted">Deleted %1$s.</string> <string name="cmgr_original_kept">The installed build\'s original is kept: it is the way back.</string> <string name="cmgr_not_stored">Not stored</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">Deleted the %1$s original of %2$s.</string> <string name="cmgr_nightlies_only">Downloads come only from the Nightlies releases</string> <string name="cmgr_list_predates_checksums">This package list predates checksums - refresh it and try again</string> <string name="cmgr_checksum_mismatch">Checksum mismatch</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">Linear</string> <string name="sprefs_upscaler_nearest">Nearest</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (quality)</string> <string name="sprefs_upscaler_sharpen">Sharpen only</string> <string name="sprefs_lod_bias_auto">Auto (match scaling)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">Nothing applied.</string> <string name="look_game_clarity">Game Clarity</string> <string name="look_game_clarity_desc">Sharpening with a touch of contrast - the everyday one.</string> @@ -1941,27 +1806,23 @@ <string name="look_anime_edge">Anime Edge</string> <string name="look_anime_edge_desc">Toon outlines plus sharpening.</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">Not an Adreno GPU</string> <string name="gpuinfo_supported">Supported</string> <string name="gpuinfo_experimental">Experimental: its drivers are test builds</string> <string name="gpuinfo_below_tested">Below tested hardware (Adreno 725 and newer): it may not run</string> <string name="gpuinfo_unsupported">Not supported: DroidDeck needs an Adreno (Snapdragon) GPU</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">GitHub\'s rate limit was hit - try again later</string> <string name="turnip_github_http">GitHub answered HTTP %1$d</string> <string name="turnip_download_http">download answered HTTP %1$d</string> <string name="turnip_list_predates_checksums">This driver list predates checksums - refresh it and try again</string> <string name="turnip_checksum_mismatch">Checksum mismatch - the download was discarded</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">Not an AdrenoTools driver: %1$s</string> <string name="turnip_reject_no_meta">no meta.json</string> <string name="turnip_reject_meta_unreadable">meta.json is unreadable (%1$s)</string> <string name="turnip_reject_linux_driver">this is a Linux runtime driver (import it under \"Linux runtime driver\")</string> <string name="turnip_reject_wayland">this is a Wayland game driver, which this app has no use for</string> <string name="turnip_reject_no_library_name">meta.json names no libraryName, so nothing could be handed to AdrenoTools</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json names %1$s, which is not in the zip</string> <string name="turnip_reject_not_aarch64">%1$s is not a 64-bit AArch64 ELF shared library</string> <string name="turnip_reject_glibc">%1$s links glibc - a \"-Linux\" Turnip, which belongs under \"Linux runtime driver\"</string> @@ -1972,91 +1833,62 @@ <string name="bundle_entry_outside">Not a driver bundle: %1$s points outside it</string> <string name="bundle_too_large">Not a driver bundle: it unpacks to more than %1$d MB</string> - <!-- Game storage --> <string name="gstore_sd_card">SD card</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">Folder</string> <string name="gstore_folder_on_volume" translatable="false">%1$s (%2$s)</string> <string name="logshare_chooser">Share session logs</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">needs Android 10 or newer (display layers with a color space)</string> <string name="hdr_none">this display reports no HDR support</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">this display supports %1$s, not HDR10</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">this GPU</string> <string name="phantom_title_on">Child-process limit is on</string> <string name="phantom_title_unset">Child-process limit is unset</string> <string name="phantom_title_unreadable">Child-process limit could not be checked</string> <string name="phantom_title_off">Child-process limit is off</string> <string name="phantom_title_not_required">Child-process limit not required</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s If that is not possible, connect this device to a computer with ADB and run the command below. Return here and check again.</string> <string name="phantom_instructions_off">Steam sessions can start.</string> <string name="phantom_instructions_not_applicable">This Android version does not use this limit.</string> <string name="phantom_fix_toggle">Turn on “Disable child process restrictions” in Developer options, and keep Developer options on afterwards.</string> <string name="phantom_fix_no_toggle">This Android version has no switch for it. Use Wireless debugging to change it from this device.</string> <string name="upd_block_no_apk">This build has no download for this copy of DroidDeck.</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">Android won\'t install it over this build because its versionCode %1$d is below the installed %2$d.</string> <string name="upd_fail_downgrade">Android won\'t install an older version over a newer one. Stay on this build until the channel catches up.</string> <string name="upd_fail_signature">This copy of DroidDeck is signed differently (a local build?), so Android won\'t update it in place.</string> <string name="upd_fail_storage">There isn\'t enough free space for the update.</string> <string name="upd_fail_aborted">The update was cancelled.</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">The update didn\'t install: %1$s</string> <string name="upd_fail_generic">The update didn\'t install.</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s zip</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s has not been launched yet</string> <string name="saves_none_found">No save files found for %1$s</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s has no drive_c folder, so it isn\'t a GameHub or Winlator save zip</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">cannot create folders in %1$s</string> <string name="gstore_cannot_write">cannot write in %1$s</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">cannot write in %1$s (%2$s)</string> <string name="gfiles_folder_unavailable">Folder is unavailable</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">A different file already uses %1$s</string> <string name="gfiles_art_unavailable">Artwork folder is unavailable</string> <string name="gfiles_unrelated_inventory">Folder has an unrelated sync inventory</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">Could not remove %1$s</string> <string name="gfiles_outside_folder">File leaves the export folder</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX (build without a version name)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">no profile.json in %1$s</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s is not a Linux component package (type %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · %2$s free</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">Automatic</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">The update catalog failed its checks (%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeck child-process setting</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">Signed in · offline mode</string> <string name="setup_signed_in_unnamed">Signed in</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">BUNDLED</string> <string name="driver_tag_downloaded">DOWNLOADED</string> <string name="driver_tag_imported">IMPORTED</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> <string name="display_session">Session display</string> <string name="display_resolution">Resolution</string> @@ -2085,7 +1917,6 @@ <string name="force_ssbs_title">Force SSBS</string> <string name="force_ssbs_hint">Keeps a CPU speed setting on that Wine keeps turning off. Can make Windows games faster on Snapdragon 8 Elite; little change seen on older chips. Applies on the next game launch.</string> - <!-- Stores: GOG, Epic Games and Amazon Games --> <string name="rail_stores">Stores</string> <string name="source_steam">Steam</string> <string name="source_added">Custom</string> @@ -2131,7 +1962,6 @@ <string name="stores_play">Play</string> <string name="stores_install">Install</string> <string name="stores_resume_install">Resume install</string> - <!-- A Downloads row for an install that stopped when the app closed: it can be resumed --> <string name="stores_dl_interrupted">Interrupted</string> <string name="stores_install_size">Install · %1$s</string> <string name="stores_get_free">Get for free</string> @@ -2166,7 +1996,6 @@ <string name="stores_eyebrow_installed">installed</string> <string name="stores_eyebrow_library">in your library</string> <string name="stores_eyebrow_store">store</string> - <!-- A GOG or Epic game's achievements: the store page's section and the session panel. --> <string name="achievements_title">Achievements</string> <string name="achievements_count">%1$d of %2$d unlocked</string> <string name="achievements_count_short">%1$d / %2$d</string> @@ -2182,7 +2011,6 @@ <string name="achievements_none">%1$s lists no achievements for this game.</string> <string name="achievements_show_all">Show all %1$d</string> <string name="achievements_dialog_title">Achievements · %1$s</string> - <!-- A store game's facts row: the store's own facts, then Metacritic and HowLongToBeat, looked up by name. --> <string name="stores_fact_developer">Developer</string> <string name="stores_fact_publisher">Publisher</string> <string name="stores_fact_released">Released</string> @@ -2196,7 +2024,6 @@ <string name="stores_in_steam">✓ In Steam</string> <string name="stores_uninstall">Uninstall</string> <string name="stores_uninstalling">Uninstalling…</string> - <!-- The heading of the confirm that steps out of a store game's Uninstall; %1$s is the game --> <string name="stores_uninstall_title">Uninstall %1$s?</string> <string name="stores_uninstall_body_size">Frees %1$s and removes it from Steam.</string> <string name="stores_uninstall_body">Removes its files and its Steam shortcut.</string> @@ -2312,13 +2139,10 @@ <item quantity="one">%1$d already in Games</item> <item quantity="other">%1$d already in Games</item> </plurals> - <!-- A page reached through a link from another section: the way back, named for where it came from. --> <string name="hop_back">B · Back to %1$s</string> <string name="hop_from_games">the game</string> <string name="hop_from_play">Play settings</string> - <!-- The lifted settings sheets under Play and Open desktop. --> <string name="sheet_graphics_drivers">Graphics drivers</string> - <!-- Components as one stack of layers. --> <string name="comp_this_proton">This Proton</string> <string name="comp_every_proton">Every Proton</string> <string name="comp_for_every_proton">For every Proton</string> @@ -2335,7 +2159,6 @@ <string name="gpu_separately">Runtime and display drivers separately</string> <string name="gpu_use_title">Use %1$s?</string> <string name="proton_remove_title">Remove %1$s?</string> - <!-- Setup sorted by scope. --> <string name="setup_tab_diagnostics">Diagnostics</string> <string name="setup_ready">Ready to play</string> <string name="setup_not_ready">Not ready to play yet</string> @@ -2346,7 +2169,6 @@ <string name="setup_group_look">Look</string> <string name="setup_group_device">This device</string> <string name="setup_group_art">Library art</string> - <!-- Flathub, in the stores' style. --> <string name="flathub_eyebrow">LINUX APPS</string> <string name="flathub_name">Flathub</string> <string name="flathub_line">Desktop apps for the Linux desktop, opened from Desktop.</string> @@ -2354,7 +2176,6 @@ <string name="flathub_remove_title">Remove %1$s?</string> <string name="flathub_remove_note">Its settings and files are kept.</string> <string name="flathub_keep">Keep</string> - <!-- Desktop in the Steam tab's shape. --> <string name="desktop_size">Size on disk</string> <string name="desktop_uninstall_title">Uninstall the desktop?</string> <string name="desktop_all_systems">All</string> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index bf4532cea..005841e7a 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,7 +3,6 @@ <style name="AppTheme" parent="@android:style/Theme.Material.NoActionBar"> <item name="android:colorBackground">#FF101418</item> </style> - <!-- A session owns the whole panel: no bars, no title, and black behind the compositor. --> <style name="SessionTheme" parent="@android:style/Theme.Material.NoActionBar.Fullscreen"> <item name="android:windowBackground">@android:color/black</item> </style> diff --git a/app/src/main/res/xml/game_mode_config.xml b/app/src/main/res/xml/game_mode_config.xml index b3f3e0d31..adf601e00 100644 --- a/app/src/main/res/xml/game_mode_config.xml +++ b/app/src/main/res/xml/game_mode_config.xml @@ -1,13 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- - What Android's GameManager (12+) and the OEM game services keyed to it may do to a session. - Performance mode is welcome: it is what raises the clock floor on a regular flagship the way a - gaming phone's mode does out of the box. What is refused is the two "helpful" interventions - that make a game look slow: Samsung's Game Optimizing Service and its cousins cap unknown - games' frame rate (allowGameFpsOverride) and render them at a lower resolution - (allowGameDownscaling). gamescope already owns the output size; a cap on top of Big Picture's - own pacing is exactly the low-fps-on-a-flagship symptom. ---> <game-mode-config xmlns:android="http://schemas.android.com/apk/res/android" android:supportsPerformanceGameMode="true" android:supportsBatteryGameMode="true" diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 62fd269ce..c6ade4b06 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -1,6 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- The languages DroidDeck ships, for Android's per-app language setting. Keep in step with - AppLanguage.supported and the values-* folders. --> <locale-config xmlns:android="http://schemas.android.com/apk/res/android"> <locale android:name="en" /> <locale android:name="id" /> diff --git a/app/src/main/rust/stores/Cargo.lock b/app/src/main/rust/stores/Cargo.lock index b259f7d3b..d46174b20 100644 --- a/app/src/main/rust/stores/Cargo.lock +++ b/app/src/main/rust/stores/Cargo.lock @@ -1,5 +1,3 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. version = 4 [[package]] diff --git a/app/src/main/rust/stores/Cargo.toml b/app/src/main/rust/stores/Cargo.toml index 49c36453e..f9e9ce49f 100644 --- a/app/src/main/rust/stores/Cargo.toml +++ b/app/src/main/rust/stores/Cargo.toml @@ -2,9 +2,6 @@ name = "droiddeckstores" version = "0.1.0" edition = "2021" -# The engines are a port of Bannerlator's bl-steam-client store modules (GPL-3.0-or-later, derived -# from WinNative's wn-steam-client; the Epic selection/verify rules trace back to GameNative and -# Legendary, both GPL-3.0). DroidDeck is GPL-3.0, so the crate keeps the licence it came with. license = "GPL-3.0-or-later" publish = false @@ -14,24 +11,12 @@ crate-type = ["cdylib"] [dependencies] jni = "0.21" -# setpriority/gettid: every engine thread runs at nice +10 (see src/priority.rs). Already in the -# dependency tree through tokio, at the version Cargo.lock pins. libc = "0.2" -# zlib inflate for GOG gen2 chunks and Epic chunk bodies. The pure-Rust backend keeps the .so free -# of a C zlib and of the NDK sysroot's libz, which the apk does not ship. flate2 = { version = "1", default-features = false, features = ["rust_backend"] } -# The fetch core drives every request through ONE tokio current-thread runtime and feeds a sync -# process pool; rustls (via ring) is the TLS stack so there is no OpenSSL to cross-compile. `gzip` -# transparently inflates a CDN answer that comes back content-encoded. No `blocking` client: the -# three engines are async-fetch only. reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "gzip"] } tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "sync"] } futures-util = { version = "0.3", default-features = false, features = ["std"] } -# preserve_order keeps JSON object keys in source order, as in Bannerlator, so the GOG depot plan -# walks a manifest's files in the order the manifest lists them (largest-first sorting happens -# after, explicitly). serde_json = { version = "1", features = ["preserve_order"] } -# Epic chunks are SHA-1 verified, Amazon files SHA-256; GOG's MD5 is the in-tree md5_small. sha1 = "0.10" sha2 = "0.10" diff --git a/app/src/main/rust/stores/src/amazon.rs b/app/src/main/rust/stores/src/amazon.rs index 55826f941..1f24533b2 100644 --- a/app/src/main/rust/stores/src/amazon.rs +++ b/app/src/main/rust/stores/src/amazon.rs @@ -1,22 +1,3 @@ -//! Amazon Games download adapter over the shared fetch core. -//! -//! Replaces ONLY the byte-fetching pool of `AmazonDownloadManager.install()` (Step 4). -//! Java still resolves the download spec, fetches and parses `manifest.proto`, and hands -//! this module the resolved file list (one JSON entry per manifest file with the exact -//! URL the Java loop would have opened). The adapter mirrors the Java rules 1:1 where the -//! core allows it — see Bannerlator's `docs/RUST_AMAZON_PARITY.md` §4 / §9 for the table and the -//! documented deviations: -//! -//! * one `FetchItem` per WHOLE file, streamed (`FetchOptions.stream = true`): pieces are -//! appended to `<dest>.tmp` as they arrive, 8 files wide like Java's pool, memory bounded -//! to the pieces not yet written; -//! * resume-skip = `st_size == manifest size`, no hash check on skip (Java `:250`); -//! * SHA-256 is accumulated while streaming and checked in `on_finish` when the manifest -//! carries a hash; a mismatch deletes the tmp and is a retryable failure (Java `:269`); -//! * `<dest>.tmp` → delete existing dest → rename (Java `:280`); rename failure is fatal -//! without retry (Java `:281`); -//! * a piece credits its wire length to progress as it is written (Java `:347`), and pieces -//! of an attempt that later fails stay counted (Java never rolls `totalDownloaded` back). pub mod jni; @@ -30,34 +11,20 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -/// Java `AmazonDownloadManager.MAX_PARALLEL`. pub const MAX_PARALLEL: usize = 8; -/// Java `AmazonDownloadManager.DOWNLOAD_USER_AGENT`. pub const DOWNLOAD_USER_AGENT: &str = "nile/0.1 Amazon"; -/// Java `conn.setReadTimeout(120000)`; in stream mode the core applies this to the response -/// headers and then as an idle deadline per received piece — the same shape as a read timeout. pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(120); -/// Java `new File(installDir, file.unixPath() + ".tmp")`. pub const TMP_SUFFIX: &str = ".tmp"; -/// Log label used in the core's `fetch-window` lines. pub const LABEL: &str = "amazon"; -/// One resolved manifest file as serialised by Java (`AmazonDownloadManager.buildRustPlan`). #[derive(Clone, Debug, PartialEq, Eq)] pub struct PlanEntry { - /// `ManifestFile.unixPath()` — relative to the install dir, forward slashes. pub rel_path: String, - /// `AmazonApiClient.appendPath(downloadUrl, "files/" + hashHex)` — signed, whole file. pub url: String, - /// `ManifestFile.size`. pub size: u64, - /// SHA-256 to verify against; EMPTY = Java would not verify (`hashAlgorithm != 0` or - /// no hash bytes). pub sha256: Vec<u8>, } -/// Parse the JSON array `[{relPath, url, size, sha256hex}]`. `relPath` is normalised the same -/// way as `ManifestFile.unixPath()` so a caller that forgot is still byte-identical. pub fn parse_plan(json: &str) -> Result<Vec<PlanEntry>, String> { let value: serde_json::Value = serde_json::from_str(json).map_err(|err| format!("plan json: {err}"))?; @@ -106,12 +73,10 @@ pub fn parse_plan(json: &str) -> Result<Vec<PlanEntry>, String> { Ok(entries) } -/// `ManifestFile.unixPath()`. pub fn unix_path(path: &str) -> String { path.replace('\\', "/") } -/// Lowercase/uppercase hex → bytes; `None` on odd length or a non-hex digit. pub fn hex_decode(hex: &str) -> Option<Vec<u8>> { let bytes = hex.as_bytes(); if bytes.len() % 2 != 0 { @@ -132,29 +97,21 @@ pub fn hex_decode(hex: &str) -> Option<Vec<u8>> { Some(out) } -/// `new File(installDir, rel)` — Java resolves the child textually under the parent, so -/// build the path by concatenation (never `Path::join`, which would let a leading `/` escape). pub fn dest_path(install_dir: &str, rel_path: &str) -> PathBuf { let base = install_dir.trim_end_matches('/'); PathBuf::from(format!("{base}/{rel_path}")) } -/// `new File(installDir, rel + ".tmp")`. pub fn tmp_path(install_dir: &str, rel_path: &str) -> PathBuf { dest_path(install_dir, &format!("{rel_path}{TMP_SUFFIX}")) } -/// Java `:250`: `destFile.exists() && destFile.length() == file.size`. `File.length()` is -/// `st_size`, which is what `Metadata::len()` returns — including for a directory, so the -/// (degenerate) behaviours match too. No hash is checked on skip, on purpose. pub fn is_present_and_complete(install_dir: &str, entry: &PlanEntry) -> bool { fs::metadata(dest_path(install_dir, &entry.rel_path)) .map(|meta| meta.len() == entry.size) .unwrap_or(false) } -/// `scheme://host[:port]` of a URL — the per-host cap key. Amazon serves every file from the -/// one signed CDN base, so this yields a single host. pub fn host_key(url: &str) -> String { let (scheme, rest) = match url.find("://") { Some(idx) => (&url[..idx], &url[idx + 3..]), @@ -164,33 +121,22 @@ pub fn host_key(url: &str) -> String { format!("{scheme}://{}", &rest[..end]) } -/// Improvements round 1: `max(6, ceil(max_workers / distinct_hosts))` — a single-host store -/// (Amazon's one signed a2z base) gets the whole ceiling on its one host; the core clamps the -/// window to `hosts × per_host_cap`, so anything smaller would silently cut the ceiling. pub fn per_host_cap_for(max_workers: usize, distinct_hosts: usize) -> usize { let hosts = distinct_hosts.max(1); let per_host = max_workers.max(1).div_ceil(hosts); per_host.max(crate::fetch_tuning::PER_HOST_CAP) } -/// The resolved work for one run. #[derive(Debug, Default)] pub struct Plan { - /// Items to fetch (index into `entries` via `FetchItem.id`). pub items: Vec<FetchItem>, - /// Every manifest file, in manifest order (fetched and skipped alike). pub entries: Vec<PlanEntry>, - /// Σ size of all entries = `ParsedManifest.totalInstallSize`. pub total_bytes: u64, - /// Bytes credited up front for files that passed the resume check. pub skipped_bytes: u64, - /// Number of files that passed the resume check. pub skipped_files: u64, - /// Distinct host keys in fetch order. pub hosts: Vec<String>, } -/// Apply the Java resume-skip rule to every entry and turn the rest into whole-file items. pub fn build_plan(entries: Vec<PlanEntry>, install_dir: &str) -> Plan { let mut plan = Plan { total_bytes: entries.iter().map(|e| e.size).sum(), @@ -220,23 +166,17 @@ pub fn build_plan(entries: Vec<PlanEntry>, install_dir: &str) -> Plan { plan } -/// One `<dest>.tmp` being streamed: the open handle, the running hash and the bytes written -/// by the current attempt. struct OpenTmp { file: File, hasher: Sha256, written: u64, } -/// Sink: stream pieces into `<dest>.tmp` (SHA-256 accumulated on the fly), verify and rename -/// in `on_finish`. `process` (whole-body mode) does the same in one step. pub struct AmazonSink { install_dir: String, entries: Vec<PlanEntry>, cancel: Arc<AtomicBool>, files_done: AtomicU64, - /// Per-item open tmp files. The map lock is held only to look the entry up; the write - /// itself happens under the per-item lock so 8 streams never serialise on one mutex. open: Mutex<HashMap<u64, Arc<Mutex<OpenTmp>>>>, } @@ -261,16 +201,12 @@ impl AmazonSink { .ok_or_else(|| SinkError::Fatal(format!("plan index {} out of range", item.id))) } - /// Java `:260` — `destFile.getParentFile().mkdirs()` before each attempt; a failure - /// surfaces on the create/write below, like Java's. fn ensure_parent(&self, entry: &PlanEntry) { if let Some(parent) = dest_path(&self.install_dir, &entry.rel_path).parent() { let _ = fs::create_dir_all(parent); } } - /// Java `:280-284`: delete an existing dest, rename tmp → dest; rename failure fails the - /// file with NO retry (→ whole install fails). fn rename_into_place(&self, entry: &PlanEntry) -> Result<(), SinkError> { let dest = dest_path(&self.install_dir, &entry.rel_path); let tmp = tmp_path(&self.install_dir, &entry.rel_path); @@ -288,8 +224,6 @@ impl AmazonSink { Ok(()) } - /// Java `:267-277`: SHA-256 check when the manifest carries a hash; a mismatch deletes the - /// tmp and is retried. fn verify(&self, entry: &PlanEntry, digest: &[u8]) -> Result<(), SinkError> { if !entry.sha256.is_empty() && digest != entry.sha256.as_slice() { let _ = fs::remove_file(tmp_path(&self.install_dir, &entry.rel_path)); @@ -301,7 +235,6 @@ impl AmazonSink { Ok(()) } - /// Whole-body path (`FetchOptions.stream = false`): verify → write tmp → rename. pub fn commit(&self, entry: &PlanEntry, body: &[u8]) -> Result<u64, SinkError> { if !entry.sha256.is_empty() { let digest = Sha256::digest(body); @@ -322,10 +255,6 @@ impl AmazonSink { Ok(body.len() as u64) } - /// After the run: close and delete every tmp that never reached `on_finish` (a stream that - /// failed after max attempts, or was cut by cancel / a fatal error). Java deletes the tmp - /// of every failed or cancelled attempt (`:271`, `:288`, `:293`, `:297`), so no `.tmp` - /// may survive a run on either engine. Returns how many were removed. pub fn cleanup_partials(&self) -> usize { let leftovers: Vec<(u64, Arc<Mutex<OpenTmp>>)> = match self.open.lock() { Ok(mut open) => open.drain().collect(), @@ -355,13 +284,11 @@ impl FetchSink for AmazonSink { fn on_chunk(&self, item: &FetchItem, offset: u64, data: &[u8]) -> Result<(), SinkError> { if self.cancel.load(Ordering::Relaxed) { - // Java `:342-344`: cancel inside the read loop → tmp deleted (cleanup_partials). return Err(SinkError::Fatal("cancelled".to_string())); } let entry = self.entry(item)?; let tmp = tmp_path(&self.install_dir, &entry.rel_path); let open = if offset == 0 { - // (Re)start of an attempt: Java opens a fresh FileOutputStream (truncating). self.ensure_parent(entry); let file = File::create(&tmp).map_err(|err| { let _ = fs::remove_file(&tmp); @@ -405,7 +332,6 @@ impl FetchSink for AmazonSink { ))); } if let Err(err) = state.file.write_all(data) { - // Java: IOException inside downloadFile → tmp deleted → next attempt. let _ = fs::remove_file(&tmp); return Err(SinkError::Retry(format!("write {}: {err}", tmp.display()))); } @@ -435,7 +361,6 @@ impl FetchSink for AmazonSink { (written, digest) } None => { - // Zero-byte body: no piece ever arrived. Java would have created an empty tmp. if total_len != 0 { return Err(SinkError::Retry(format!( "finish without a stream for: {}", @@ -462,22 +387,16 @@ impl FetchSink for AmazonSink { } } -/// Outcome of one `run_download`. #[derive(Clone, Debug, Default)] pub struct RunResult { pub success: bool, pub cancelled: bool, pub error: String, - /// Bytes fetched by this run (skipped files excluded; failed attempts' pieces included, - /// like Java's `totalDownloaded`). pub bytes_written: u64, - /// Files committed by this run plus files skipped by the resume check. pub files_done: u64, pub files_total: u64, } -/// Throughput bookkeeping for the final summary line (avg over the run, peak over ≥500 ms -/// progress intervals — the same sampling window the Java speed label uses). struct SpeedMeter { started: Instant, last_at: Instant, @@ -516,10 +435,6 @@ impl SpeedMeter { } } -/// Blocking: plan → stream-fetch → verify/rename, calling `progress(bytes_done, bytes_total, -/// files_done, files_total)` after each written piece and each finished file (skipped files -/// are pre-credited) and `log` for every diagnostic line. `bytes_done` includes skipped bytes, -/// matching Java's `totalDownloaded` (which credits skipped files at `:251`). pub fn run_download( plan_json: &str, install_dir: &str, @@ -582,16 +497,12 @@ pub fn run_download( let opts = FetchOptions { max_workers, - // Every Amazon file comes from the one signed CDN base: give that host the whole - // ceiling (`per_host_cap_for`), or the core would clamp the window to hosts × cap. per_host_cap, timeout: REQUEST_TIMEOUT, headers: vec![("User-Agent".to_string(), DOWNLOAD_USER_AGENT.to_string())], ca_bundle_path: ca_bundle_path.to_string(), process_workers, label: LABEL.to_string(), - // Whole files, streamed into the tmp as they arrive: 8 wide like Java's pool, memory - // bounded to the pieces not yet written. stream: true, ..FetchOptions::default() }; @@ -753,10 +664,8 @@ mod tests { fn build_plan_skips_only_size_matched_files() { let dir = scratch_dir(); let entries = parse_plan(PLAN).unwrap(); - // Entry 1 present with the right size → skipped (no hash check even if wrong bytes). fs::create_dir_all(format!("{dir}/data")).unwrap(); fs::write(format!("{dir}/data/level1.pak"), b"zz").unwrap(); - // Entry 0 present with the wrong size → fetched. fs::create_dir_all(format!("{dir}/Binaries/Win64")).unwrap(); fs::write(format!("{dir}/Binaries/Win64/Game.exe"), b"tes").unwrap(); let plan = build_plan(entries, &dir); @@ -803,7 +712,6 @@ mod tests { let cancel = Arc::new(AtomicBool::new(false)); let sink = AmazonSink::new(&dir, vec![entry.clone()], Arc::clone(&cancel)); let item = item_for(&entry, 0); - // Mismatch → Retry, nothing on disk. match sink.process(&item, b"nope".to_vec()) { Err(SinkError::Retry(msg)) => assert!(msg.contains("SHA-256 mismatch")), Err(SinkError::Fatal(msg)) => panic!("expected Retry, got Fatal({msg})"), @@ -811,14 +719,11 @@ mod tests { } assert!(!dest_path(&dir, "sub/Game.exe").exists()); assert!(!tmp_path(&dir, "sub/Game.exe").exists()); - // Match → written, renamed, tmp gone, credited body length. assert_eq!(sink.process(&item, body.clone()).unwrap(), 4); assert_eq!(fs::read(dest_path(&dir, "sub/Game.exe")).unwrap(), body); assert!(!tmp_path(&dir, "sub/Game.exe").exists()); assert_eq!(sink.files_done(), 1); - // Existing dest is replaced (Java `:280`). assert_eq!(sink.process(&item, body.clone()).unwrap(), 4); - // Cancel → Fatal, no write. cancel.store(true, Ordering::Relaxed); assert!(matches!( sink.process(&item, body), @@ -863,7 +768,6 @@ mod tests { }; let sink = AmazonSink::new(&dir, vec![entry.clone()], Arc::new(AtomicBool::new(false))); let item = item_for(&entry, 0); - // Attempt 1 delivers garbage, then the core restarts at offset 0. sink.on_chunk(&item, 0, b"zzzz").unwrap(); sink.on_chunk(&item, 0, b"abc").unwrap(); sink.on_chunk(&item, 3, b"xyz").unwrap(); @@ -874,21 +778,17 @@ mod tests { } assert!(!tmp_path(&dir, "f.bin").exists()); assert!(!dest_path(&dir, "f.bin").exists()); - // Out-of-order piece → Retry. sink.on_chunk(&item, 0, b"abc").unwrap(); assert!(matches!( sink.on_chunk(&item, 5, b"f"), Err(SinkError::Retry(_)) )); - // Length mismatch at finish → Retry. sink.on_chunk(&item, 0, b"abc").unwrap(); assert!(matches!(sink.on_finish(&item, 6), Err(SinkError::Retry(_)))); - // Piece without a start → Retry. assert!(matches!( sink.on_chunk(&item, 3, b"def"), Err(SinkError::Retry(_)) )); - // Good attempt. sink.on_chunk(&item, 0, b"abc").unwrap(); sink.on_chunk(&item, 3, b"def").unwrap(); assert_eq!(sink.on_finish(&item, 6).unwrap(), 0); @@ -915,10 +815,8 @@ mod tests { let sink = AmazonSink::new(&dir, vec![big.clone(), empty.clone()], Arc::clone(&cancel)); sink.on_chunk(&item_for(&big, 0), 0, b"12345").unwrap(); assert!(tmp_path(&dir, "cut.bin").exists()); - // Zero-byte body: on_finish with no pieces creates and commits an empty file. assert_eq!(sink.on_finish(&item_for(&empty, 1), 0).unwrap(), 0); assert_eq!(fs::read(dest_path(&dir, "empty.bin")).unwrap(), b""); - // Cancel mid-stream → Fatal; cleanup deletes the partial tmp like Java does. cancel.store(true, Ordering::Relaxed); assert!(matches!( sink.on_chunk(&item_for(&big, 0), 5, b"6"), diff --git a/app/src/main/rust/stores/src/amazon/jni.rs b/app/src/main/rust/stores/src/amazon/jni.rs index 26797773b..32ce4c62c 100644 --- a/app/src/main/rust/stores/src/amazon/jni.rs +++ b/app/src/main/rust/stores/src/amazon/jni.rs @@ -1,12 +1,3 @@ -//! JNI facade for the Amazon download adapter. -//! -//! Kotlin side: `com.droiddeck.launcher.stores.amazon.AmazonNative` (`nativeStart` / -//! `nativeCancel` / `nativeRelease`) with an `AmazonNative.Listener` -//! (`onProgress(JJJJ)V`, `onLog(String)V`, `onComplete(ZString;J)V`). Mirrors the shape of -//! the GOG and Epic facades: the run happens on a spawned native -//! thread, callbacks attach the JavaVM as daemon threads and hold a `GlobalRef` to the -//! listener; cancel flips an `AtomicBool`. This module keeps its own `JavaVM` handle (taken -//! from the calling `JNIEnv` on first use) so it depends on nothing outside this module. use crate::amazon::{run_download, MAX_PARALLEL}; use jni::objects::{GlobalRef, JClass, JObject, JString, JValue}; @@ -16,10 +7,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, OnceLock}; use std::thread; -/// Logcat tag (`adb logcat -s AmazonNative`). Every engine line reaches logcat exactly ONCE: -/// this module only forwards lines to the listener (`onLog`), and the Java manager is the one -/// place that writes them to `android.util.Log` + its debug file. (Round 1 fix: the native side -/// used to also call `__android_log_write`, so each line appeared twice.) pub const LOG_TAG: &str = "AmazonNative"; static JVM: OnceLock<JavaVM> = OnceLock::new(); @@ -36,7 +23,6 @@ fn jstring_to_string(env: &mut JNIEnv, value: &JString) -> Option<String> { .map(|s| s.to_string_lossy().into_owned()) } -/// One live run; boxed and handed to Kotlin as an opaque `jlong`. struct AmazonRun { cancel: Arc<AtomicBool>, } @@ -115,7 +101,6 @@ fn call_complete( clear_pending_exception(env); } -/// Attach the current (native) thread and run `f` with the listener object. fn with_attached_env(listener: &GlobalRef, f: impl FnOnce(&mut JNIEnv, &JObject)) { let Some(vm) = JVM.get() else { return; @@ -126,9 +111,6 @@ fn with_attached_env(listener: &GlobalRef, f: impl FnOnce(&mut JNIEnv, &JObject) f(&mut env, listener.as_obj()); } -/// `AmazonNative.nativeStart(planJson, installDir, caBundlePath, maxWorkers, -/// processWorkers, listener) -> handle`. Returns 0 (and fires `onComplete(false, …)`) when the -/// inputs are unusable. The run itself happens on a spawned thread. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeStart( mut env: JNIEnv, @@ -200,8 +182,6 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_na to_handle(run) } -/// `AmazonNative.nativeCancel(handle)` — flips the run's cancel flag; the core stops -/// issuing requests, in-flight bodies are dropped, `onComplete(false, "cancelled", …)` follows. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeCancel( _env: JNIEnv, @@ -213,8 +193,6 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_na } } -/// `AmazonNative.nativeRelease(handle)` — frees the handle. Safe at any time: the worker -/// thread owns its own `Arc` of the cancel flag and the listener `GlobalRef`. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeRelease( _env: JNIEnv, diff --git a/app/src/main/rust/stores/src/epic.rs b/app/src/main/rust/stores/src/epic.rs index 258c273b9..41ac19767 100644 --- a/app/src/main/rust/stores/src/epic.rs +++ b/app/src/main/rust/stores/src/epic.rs @@ -1,29 +1,4 @@ -//! Epic Games Store download adapter for the shared fetch core (`crate::fetch_core`). -//! -//! Scope = the chunk-fetch inner loop of `EpicDownloadManager.java` (the fixed 8-thread pool that -//! fills `<installDir>/.chunks/<GUID>`), nothing more. The Java manager still parses the manifest -//! API JSON, downloads the manifest, selects files (install tags), runs the delta/verify pass, -//! assembles files from the chunk cache and does every post-install step. The adapter re-parses -//! the same manifest bytes, rebuilds the same chunk plan for the pending files Java hands it, -//! skips chunks already in the cache and writes verified chunks with the same `.part` + rename -//! protocol, so the on-disk state is identical whichever engine ran (see -//! Bannerlator's `docs/RUST_EPIC_PARITY.md` for the rule-by-rule table). -//! -//! Submodules: -//! - [`manifest`] — ChunksV4 binary manifest + legacy JSON manifest parsers (1:1 with the Java -//! `parseManifest` / `parseJsonManifest`). -//! - [`plan`] — install-tag selection, unique-chunk plan, chunk paths / CDN URLs. -//! - [`chunk`] — chunk header parse, zlib inflate, SHA-1 verify, cache write. -//! - [`driver`] — plan → `FetchItem`s → `fetch_core::run_fetch` with the chunk-cache sink. -//! - [`assemble`] — the files from the cache, one sequential write each, SHA-1 checked as they -//! are written; with a scratch cache (one the caller placed apart from the install) each chunk -//! is deleted as soon as the last file that uses it is done. -//! - [`jni`] — `Java_com_droiddeck_launcher_stores_epic_EpicNative_native*` exports. -//! -//! The selective-install-tag, delta/resume and chunk verification rules mirrored here were ported -//! into the Java manager from GameNative (`service/epic/EpicDownloadManager.kt`, -//! `manifest/ManifestUtils.kt`), itself derived from Legendary; both are GPL-3.0 and this crate -//! carries the same licence (see `Cargo.toml`). +//! Derived from GameNative's Epic manager (itself derived from Legendary); both are GPL-3.0 and this crate carries the same licence (see Cargo.toml). pub mod assemble; pub mod chunk; @@ -32,15 +7,11 @@ pub mod jni; pub mod manifest; pub mod plan; -/// User-Agent the Java manager sends on every chunk request (`EpicDownloadManager.UA`). pub const USER_AGENT: &str = "UELauncher/11.0.1-14907503+++Portal+Release-Live Windows/10.0.19041.1.256.64bit"; -/// Name of the chunk cache directory under the install dir (`new File(installDir, ".chunks")`). pub const CHUNK_CACHE_DIR: &str = ".chunks"; -/// Fixed pool width of the Java chunk downloader (`Executors.newFixedThreadPool(8)`). pub const JAVA_POOL_THREADS: usize = 8; -/// logcat tag for every engine line (the Kotlin manager mirrors it into its own debug log). pub const LOG_TAG: &str = "EpicNative"; diff --git a/app/src/main/rust/stores/src/epic/assemble.rs b/app/src/main/rust/stores/src/epic/assemble.rs index 580cb8c24..6881640b8 100644 --- a/app/src/main/rust/stores/src/epic/assemble.rs +++ b/app/src/main/rust/stores/src/epic/assemble.rs @@ -1,26 +1,3 @@ -//! Files from the chunk cache: the last stage of an Epic install, as a native run. -//! -//! The Kotlin manager's own loop does this too (`EpicDownloadManager.install`, after the chunk -//! fetch): for every pending file, open it, and for every part read the cached chunk and write -//! `chunk[offset..offset+size]`. This module does the same thing, in the same order, with the -//! same wording on a missing chunk, plus what the Kotlin loop cannot do without re-reading the -//! finished file from a slow card: -//! -//! - the file's SHA-1 is computed from the bytes as they are written (no extra I/O) and compared -//! with the manifest's, when the manifest has one; a mismatch deletes the file and fails the -//! run, so a bad assembly can never pass the next delta pass by size alone; -//! - with a scratch cache — one the caller placed apart from the install directory — every chunk -//! is reference-counted across the pending files and deleted the moment the last file that -//! needs it is written and verified, so the cache never holds more than what is still owed. -//! -//! The cache directory itself goes at the end of a successful run, scratch or not, which is what -//! the Kotlin loop does with `<installDir>/.chunks`. Cancellation and failure leave the cache -//! exactly as it stands, so the next run's delta pass skips the finished files and the fetch -//! skips the chunks still cached — on either engine. -//! -//! Why the cache may live elsewhere at all: an install to an SD card wrote every byte twice to -//! the card (chunk cache, then assembled file). With the cache on internal storage the card sees -//! each byte once, and the fetchers stop waiting on the card's write speed. use std::collections::HashMap; use std::fs::{self, File}; @@ -33,33 +10,25 @@ use sha1::{Digest, Sha1}; use super::manifest::{parse_manifest, FileInfo, Manifest}; use super::plan::resolve_cache_dir; -/// Everything the JNI layer hands over for one assembly run. pub struct AssembleRequest { pub manifest_bytes: Vec<u8>, pub install_dir: String, - /// `""` = `<installDir>/.chunks` (no scratch semantics); anything else = scratch cache. pub chunk_cache_dir: String, - /// Indices into `manifest.files` of the files to assemble, in order. pub file_indices: Vec<usize>, } -/// Terminal result of an assembly run. #[derive(Clone, Debug, Default)] pub struct AssembleOutcome { pub success: bool, pub cancelled: bool, pub error: String, - /// Bytes written into assembled files this run. pub bytes_written: u64, pub files_done: u64, pub files_total: u64, pub bytes_total: u64, - /// Chunk files deleted early because no remaining file needed them (scratch cache only). pub chunks_removed: u64, } -/// The parsed plan: manifest, the files to write, and how many of them still need each chunk. -/// Built up front so a bad index fails before anything is written. #[derive(Debug)] pub struct AssemblePlan { pub manifest: Manifest, @@ -68,12 +37,9 @@ pub struct AssemblePlan { pub scratch_cache: bool, pub files: Vec<usize>, pub bytes_total: u64, - /// GUID → number of files in `files` whose parts reference it (a file counts once per GUID, - /// however many of its parts use the chunk). pub refs: HashMap<String, u32>, } -/// Parse + plan. `Err` = nothing was written. pub fn build_assemble_plan(req: &AssembleRequest) -> Result<AssemblePlan, String> { if req.install_dir.is_empty() { return Err("assemble: empty install dir".to_string()); @@ -104,7 +70,6 @@ pub fn build_assemble_plan(req: &AssembleRequest) -> Result<AssemblePlan, String }) } -/// How many of `file_indices` reference each chunk GUID. pub fn chunk_refs(m: &Manifest, file_indices: &[usize]) -> HashMap<String, u32> { let mut refs: HashMap<String, u32> = HashMap::new(); for &fi in file_indices { @@ -118,7 +83,6 @@ pub fn chunk_refs(m: &Manifest, file_indices: &[usize]) -> HashMap<String, u32> refs } -/// The GUIDs a file's parts reference, each once, in first-seen order. fn distinct_guids(f: &FileInfo) -> Vec<String> { let mut out: Vec<String> = Vec::with_capacity(f.parts.len()); for p in &f.parts { @@ -130,19 +94,14 @@ fn distinct_guids(f: &FileInfo) -> Vec<String> { out } -/// The manifest's relative path with Windows separators normalised, as the Kotlin loop does -/// (`filename.replace("\\", "/")`). pub fn relative_path(f: &FileInfo) -> String { f.filename.replace('\\', "/") } -/// A manifest SHA-1 that is all zero means "none" (the Kotlin delta pass treats it the same way). fn expected_sha1(f: &FileInfo) -> Option<&[u8; 20]> { f.sha1.as_ref().filter(|h| h.iter().any(|&b| b != 0)) } -/// Chunk bytes, read whole, with the last chunk kept: consecutive parts of one file usually come -/// from the same chunk, and a file's parts are the only place a chunk is read. struct ChunkReader { cache_dir: PathBuf, last: Option<(String, Vec<u8>)>, @@ -161,7 +120,6 @@ impl ChunkReader { Ok(self.last.as_ref().map(|(_, d)| d.as_slice()).unwrap_or(&[])) } - /// Drop the kept copy of a chunk that is about to be deleted. fn forget(&mut self, guid: &str) { if self.last.as_ref().map(|(g, _)| g.as_str()) == Some(guid) { self.last = None; @@ -169,8 +127,6 @@ impl ChunkReader { } } -/// Write one file from the cache. Returns the bytes written. The output is removed on any error, -/// so a file only ever exists complete (and, when the manifest hashes it, verified) or not at all. fn write_file(reader: &mut ChunkReader, f: &FileInfo, out_path: &Path) -> Result<u64, String> { let rel_path = relative_path(f); if let Some(parent) = out_path.parent() { @@ -219,8 +175,6 @@ fn write_file(reader: &mut ChunkReader, f: &FileInfo, out_path: &Path) -> Result result } -/// Run the plan. `progress(bytes_done, bytes_total, files_done, files_total)` fires after every -/// file; `log` receives the start line, failures and the terminal summary. pub fn run_assemble( plan: &AssemblePlan, cancel: &AtomicBool, @@ -270,9 +224,6 @@ pub fn run_assemble( return outcome; } } - // Scratch cache: this file was the last user of a chunk → the chunk goes now, not at the - // end, so the cache holds only what is still owed to files not yet written. Released - // before the progress event, so "file done" also means "its chunks are freed". if plan.scratch_cache { for guid in distinct_guids(f) { let Some(count) = refs.get_mut(&guid) else { @@ -297,8 +248,6 @@ pub fn run_assemble( } outcome.success = true; - // Done with the cache: remove it whole (a scratch cache is already down to the chunks no - // pending file referenced; the default one is what the Kotlin loop deleted at this point). match fs::remove_dir_all(&plan.cache_dir) { Ok(()) => log(&format!("cache removed {}", plan.cache_dir.display())), Err(e) => log(&format!( @@ -337,7 +286,6 @@ mod tests { out } - /// file 0 = A[0..100] ++ B[10..60]; file 1 = A[100..300]. A is shared, B is file 0's alone. fn expected_files() -> (Vec<u8>, Vec<u8>) { let (a, b) = (chunk_a(), chunk_b()); let mut f0 = a[0..100].to_vec(); @@ -426,7 +374,6 @@ mod tests { fill_cache(&cache); let plan = build_assemble_plan(&request(&install, cache.to_str().unwrap())).unwrap(); assert!(plan.scratch_cache); - // After file 0: B (only file 0 used it) must be gone, A (file 1 still needs it) must stay. let seen = Mutex::new(Vec::new()); let progress = |_: u64, _: u64, files_done: u64, _: u64| { if files_done == 1 { diff --git a/app/src/main/rust/stores/src/epic/chunk.rs b/app/src/main/rust/stores/src/epic/chunk.rs index 22ebb864d..3e0e96c49 100644 --- a/app/src/main/rust/stores/src/epic/chunk.rs +++ b/app/src/main/rust/stores/src/epic/chunk.rs @@ -1,16 +1,3 @@ -//! One chunk: header parse → (zlib inflate | copy) → SHA-1 → `.part` → rename into the cache. -//! -//! Port of `EpicDownloadManager.downloadChunkStreaming` minus the HTTP part (the fetch core -//! hands us the whole response body). Java's protocol, kept exactly: -//! - 41-byte header: magic(4) headerVersion(4) headerSize(4) compressedSize(4) GUID(16) hash(8) -//! storedAs(1); payload starts at `max(headerSize, 41)` (Java never seeks back); -//! - payload = the next `compressedSize` bytes, or whatever the body still has (a short body -//! is NOT an error — the SHA-1 check decides); -//! - `storedAs & 1` → zlib; corrupt zlib = failure; a stream that ends early = partial output; -//! - the decompressed bytes go to `<final>.part`; the SHA-1 (when the manifest has one) must -//! match or the `.part` is deleted; then an atomic rename publishes `<final>`. -//! A cache file therefore exists under its final name ONLY if it fully downloaded and verified, -//! on either engine. use std::fs::{self, File}; use std::io::{self, BufWriter, Write}; @@ -18,10 +5,8 @@ use std::path::{Path, PathBuf}; use sha1::{Digest, Sha1}; -/// Chunk file magic (`0xB1FE3AA2`). pub const CHUNK_MAGIC: u32 = 0xB1FE_3AA2; -/// Bytes Java reads before looking at the header (`byte[] hdrBuf = new byte[41]`). pub const CHUNK_HEADER_PREFIX: usize = 41; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -37,7 +22,6 @@ impl ChunkHeader { (self.stored_as & 1) != 0 } - /// Where the payload begins in the body (`if (headerSize > 41) skipFully(headerSize - 41)`). pub fn payload_start(&self) -> usize { if self.header_size > CHUNK_HEADER_PREFIX as i32 { self.header_size as usize @@ -47,8 +31,6 @@ impl ChunkHeader { } } -/// `readFully(hdrBuf)` + header decode. `Err` = the Java `continue`/exception paths -/// (short stream, bad magic). pub fn parse_chunk_header(body: &[u8]) -> Result<ChunkHeader, String> { if body.len() < CHUNK_HEADER_PREFIX { return Err(format!( @@ -70,9 +52,6 @@ pub fn parse_chunk_header(body: &[u8]) -> Result<ChunkHeader, String> { }) } -/// The payload slice Java's read loop would consume: `compressedSize` bytes after the header, -/// truncated to what the body holds (`if (n <= 0) break;`). `Err` when the header skip runs -/// past the body (`skipFully` throws). pub fn chunk_payload<'a>(body: &'a [u8], hdr: &ChunkHeader) -> Result<&'a [u8], String> { let start = hdr.payload_start(); if start > body.len() { @@ -88,7 +67,6 @@ pub fn chunk_payload<'a>(body: &'a [u8], hdr: &ChunkHeader) -> Result<&'a [u8], Ok(&body[start..start + take]) } -/// `Writer` that mirrors `fos.write(...)` + `sha.update(...)` on every block. struct HashingWriter<W: Write> { inner: W, sha: Option<Sha1>, @@ -110,23 +88,18 @@ impl<W: Write> Write for HashingWriter<W> { } } -/// `new File(outFile.getPath() + ".part")`. pub fn part_path(final_path: &Path) -> PathBuf { let mut s = final_path.as_os_str().to_os_string(); s.push(".part"); PathBuf::from(s) } -/// Process one downloaded body into `final_path`. Returns the number of DECOMPRESSED bytes -/// written. `Err(reason)` = Java's "try the next CDN" outcome; the `.part` never survives an -/// error and `final_path` is only ever created by the rename after verification. pub fn write_verified_chunk( body: &[u8], expected_sha1: Option<&[u8; 20]>, final_path: &Path, ) -> Result<u64, String> { let tmp = part_path(final_path); - // `tmp.delete()` — clear any stale partial from a prior interrupted attempt. let _ = fs::remove_file(&tmp); let result = write_part(body, expected_sha1, &tmp, final_path); @@ -153,9 +126,6 @@ fn write_part( }; if hdr.is_compressed() { - // Java: inflate until the stream finishes or the input runs out; a corrupt stream throws - // (DataFormatException) → this CDN attempt fails. flate2 behaves the same way: EOF on a - // truncated stream is a clean end (partial output), corruption is an error. let mut dec = flate2::read::ZlibDecoder::new(payload); io::copy(&mut dec, &mut out).map_err(|e| format!("inflate: {e}"))?; } else { @@ -164,7 +134,7 @@ fn write_part( out.flush().map_err(|e| format!("flush: {e}"))?; let written = out.written; let digest = out.sha.take().map(|s| s.finalize()); - drop(out); // close the file before the rename + drop(out); if let (Some(expected), Some(actual)) = (expected_sha1, digest) { if actual.as_slice() != &expected[..] { @@ -172,7 +142,6 @@ fn write_part( } } - // Verified → publish atomically (same directory → same filesystem). fs::rename(tmp, final_path).map_err(|e| format!("Chunk rename failed: {e}"))?; Ok(written) } @@ -181,7 +150,6 @@ fn write_part( pub(crate) mod test_support { use super::*; - /// Build a chunk body the way the CDN serves it. pub fn build_chunk_body( decompressed: &[u8], compress: bool, @@ -199,15 +167,15 @@ pub(crate) mod test_support { let header_size = (CHUNK_HEADER_PREFIX + extra_header) as i32; let mut out = Vec::new(); out.extend_from_slice(&CHUNK_MAGIC.to_le_bytes()); - out.extend_from_slice(&3i32.to_le_bytes()); // headerVersion + out.extend_from_slice(&3i32.to_le_bytes()); out.extend_from_slice(&header_size.to_le_bytes()); out.extend_from_slice( &declared_compressed_size .unwrap_or(payload.len() as i32) .to_le_bytes(), ); - out.extend_from_slice(&[0xAB; 16]); // GUID (unused by the reader) - out.extend_from_slice(&[0xCD; 8]); // hash (unused by the reader) + out.extend_from_slice(&[0xAB; 16]); + out.extend_from_slice(&[0xCD; 8]); out.push(if compress { 1 } else { 0 }); out.extend(std::iter::repeat(0x5A).take(extra_header)); assert_eq!(out.len(), header_size as usize); @@ -280,7 +248,6 @@ mod tests { let neg = build_chunk_body(b"abcdef", false, 0, Some(-1)); let h = parse_chunk_header(&neg).unwrap(); assert_eq!(chunk_payload(&neg, &h).unwrap(), b""); - // headerSize past the body → skipFully throws. let mut far = build_chunk_body(b"abcdef", false, 0, None); far[8..12].copy_from_slice(&5000i32.to_le_bytes()); let h = parse_chunk_header(&far).unwrap(); @@ -345,12 +312,10 @@ mod tests { let data = sample_data(); let mut body = build_chunk_body(&data, true, 0, None); let final_path = dir.join("chunk"); - // Truncate the body: inflate ends early → partial output → SHA-1 mismatch. let cut = body.len() - 1000; let err = write_verified_chunk(&body[..cut], Some(&sha1_of(&data)), &final_path).unwrap_err(); assert!(err.contains("SHA-1 mismatch") || err.contains("inflate"), "{err}"); assert!(!final_path.exists()); - // Corrupt the deflate stream itself. for b in body.iter_mut().skip(60).take(64) { *b ^= 0x55; } @@ -366,7 +331,7 @@ mod tests { let dir = temp_dir("stale"); let final_path = dir.join("chunk"); fs::write(part_path(&final_path), b"stale").unwrap(); - let body = [0u8; 10]; // too short → header failure + let body = [0u8; 10]; assert!(write_verified_chunk(&body, None, &final_path).is_err()); assert!(!part_path(&final_path).exists()); assert!(!final_path.exists()); diff --git a/app/src/main/rust/stores/src/epic/driver.rs b/app/src/main/rust/stores/src/epic/driver.rs index 1c39f5ac6..f72118b87 100644 --- a/app/src/main/rust/stores/src/epic/driver.rs +++ b/app/src/main/rust/stores/src/epic/driver.rs @@ -1,12 +1,3 @@ -//! Plan → `FetchItem`s → `crate::fetch_core::run_fetch` with a sink that fills the chunk cache. -//! -//! This is the replacement for the body of the Java pool block in `EpicDownloadManager.install` -//! ("Download unique chunks — 8 parallel threads"). Inputs are exactly what that block sees: -//! the parsed manifest (re-parsed here from the same bytes), the pending file set (indices Java -//! computed after its delta/verify pass), the CDN prefixes (`baseUrl + cloudDir`, cloudflare -//! already skipped) and the chunk cache dir. Output = `<cache>/<GUID>` for every needed chunk, -//! where `<cache>` is `<installDir>/.chunks` unless the caller named another directory; the -//! files are assembled from it afterwards (by the Kotlin manager, or by [`super::assemble`]). use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; @@ -21,65 +12,44 @@ use super::plan::{ total_credit_bytes, unique_chunks_for_files, }; -/// Java `conn.setReadTimeout(60000)` — the longer of the two Java timeouts (connect was 30 s). pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); -/// Everything the JNI layer hands over for one run. pub struct EpicRequest { pub manifest_bytes: Vec<u8>, pub install_dir: String, - /// Chunk cache directory; `""` = `<installDir>/.chunks`. See [`resolve_cache_dir`]. pub chunk_cache_dir: String, - /// `cdn.baseUrl + cdn.cloudDir` per manifest-API CDN entry, in Java's order. pub cdn_prefixes: Vec<String>, - /// Indices into `manifest.files` of Java's `pendingFiles` (post delta/verify), in order. pub pending_file_indices: Vec<usize>, - /// Java's `neededChunks.size()` / `totalBytes` for the plan cross-check (`None` = skip). pub expected_chunks: Option<u64>, pub expected_bytes: Option<u64>, pub ca_bundle_path: String, - /// Window ceiling: the Steam speed tier's network window (Fast = 32) since improvements - /// round 1; the Java fallback pool keeps its fixed 8 (`super::JAVA_POOL_THREADS`). pub max_workers: usize, pub process_workers: usize, pub label: String, } -/// The chunk plan for one run. #[derive(Debug)] pub struct EpicPlan { pub manifest: Manifest, pub cache_dir: PathBuf, - /// The cache is a directory the caller named, apart from the install: scratch space that - /// the assembler empties as it goes and removes when it is done. pub scratch_cache: bool, - /// Chunk indices (into `manifest.unique_chunks`) in Java's submission order. pub needed: Vec<usize>, - /// `Σ max(fileSize, 1)` over `needed`. pub total_bytes: u64, - /// Distinct CDN prefixes = fetch-core host keys. pub hosts: Vec<String>, } -/// Terminal result of a run, in the shape Java's pool block needs to reproduce its own exit -/// paths (`CANCELLED during chunk download (n/total chunks)`, `N chunks failed`, `chunksOK=`). #[derive(Clone, Debug, Default)] pub struct EpicOutcome { pub success: bool, pub cancelled: bool, pub error: String, - /// Credited (manifest `max(fileSize,1)`) bytes: skipped-cached + fetched. pub bytes_credited: u64, - /// Chunks accounted for (skipped-cached + fetched), Java's `completedCount`. pub chunks_done: u64, pub chunks_total: u64, pub bytes_total: u64, - /// Decompressed bytes actually written to the cache this run. pub decompressed_written: u64, } -/// Parse + plan (no I/O beyond reading the cache dir). `Err` = "engine could not start"; Java -/// then runs its own pool, since nothing has been fetched yet. pub fn build_plan(req: &EpicRequest) -> Result<EpicPlan, String> { let manifest = parse_manifest(&req.manifest_bytes).map_err(|e| format!("plan: {e}"))?; for &i in &req.pending_file_indices { @@ -124,10 +94,8 @@ pub fn build_plan(req: &EpicRequest) -> Result<EpicPlan, String> { }) } -/// Sink: one downloaded body → verified cache file. `id` indexes `fetch_chunks`. struct ChunkCacheSink<'a> { plan: &'a EpicPlan, - /// Chunk indices (into `manifest.unique_chunks`) of the items actually being fetched. fetch_chunks: &'a [usize], decompressed: AtomicU64, } @@ -144,15 +112,11 @@ impl<'a> FetchSink for ChunkCacheSink<'a> { self.decompressed.fetch_add(written, Ordering::Relaxed); Ok(chunk.credit_bytes()) } - // Every per-attempt failure in Java is "try the next CDN"; the core's Retry rotates - // hosts and backs off the same way (bounded at its attempt cap). Err(reason) => Err(SinkError::Retry(format!("{} {reason}", chunk.guid_str()))), } } } -/// Rolling speed sampler for the end-of-run summary: samples at ≥500 ms like the Java pool's -/// `lastSpeedMs` logic, keeps the peak. struct SpeedMeter { start: Instant, last_at: Instant, @@ -197,9 +161,6 @@ impl SpeedMeter { } } -/// Run the plan. `progress(bytes_done, chunks_done)` fires once per accounted chunk (cached-skip -/// or fetched), exactly the cadence of the Java pool's per-task `progress(...)` call. `log` -/// receives engine lines (`fetch-window`, `summary`, failures). pub fn run_plan( plan: &EpicPlan, req: &EpicRequest, @@ -228,8 +189,6 @@ pub fn run_plan( req.process_workers )); - // Java: each pool task first checks the cancel flag, then `cachedFile.exists()` → credit - // without fetching. Account for the cached chunks up front (same credit, same callback). let mut fetch_chunks: Vec<usize> = Vec::with_capacity(plan.needed.len()); let mut pre_bytes: u64 = 0; let mut pre_chunks: u64 = 0; @@ -293,16 +252,12 @@ pub fn run_plan( let opts = FetchOptions { max_workers: req.max_workers.max(1), - // Improvements round 1: the tier ceiling split across the distinct CDNs (floor 6), so - // the whole window is reachable on 1 host or on all 3. per_host_cap: host_cap, timeout: REQUEST_TIMEOUT, headers: vec![("User-Agent".to_string(), super::USER_AGENT.to_string())], ca_bundle_path: req.ca_bundle_path.clone(), process_workers: req.process_workers.max(1), label: req.label.clone(), - // Whole-body mode (Epic chunks are ≤ ~1 MiB compressed; the core's byte budget scales - // with the window); `stream` and any future field keep the core's defaults. ..FetchOptions::default() }; diff --git a/app/src/main/rust/stores/src/epic/jni.rs b/app/src/main/rust/stores/src/epic/jni.rs index 937d6e42a..1c3c2a01e 100644 --- a/app/src/main/rust/stores/src/epic/jni.rs +++ b/app/src/main/rust/stores/src/epic/jni.rs @@ -1,25 +1,3 @@ -//! JNI exports for `com.droiddeck.launcher.stores.epic.EpicNative`. -//! -//! ```text -//! nativeStart(manifest: byte[], installDir: String, chunkCacheDir: String, cdnPrefixes: String[], -//! pendingFileIdx: int[], expectedChunks: int, expectedBytes: long, -//! caBundlePath: String, maxWorkers: int, processWorkers: int, -//! listener: EpicNative.Listener) -> long handle (0 = not started) -//! nativeAssemble(manifest: byte[], installDir: String, chunkCacheDir: String, fileIdx: int[], -//! listener: EpicNative.Listener) -> long handle (0 = not started) -//! nativeCancel(handle) — flips the run's AtomicBool; fetch or assembly stops promptly -//! nativeRelease(handle) — frees the handle (after onComplete) -//! ``` -//! `chunkCacheDir` = "" keeps the cache at `<installDir>/.chunks`; any other directory is a -//! scratch cache (see `assemble.rs`). Listener (all methods run on native threads): -//! `onPlan(int chunksTotal, long bytesTotal, String chunkDir)` once before any fetch, -//! `onProgress(long bytesDone, long bytesTotal, int done, int total)` per chunk fetched (or, in -//! an assembly run, per file written), `onLog(String line)`, -//! `onComplete(boolean success, String error, long bytes)`. -//! -//! Planning (manifest parse + cross-check) runs synchronously on the calling thread so a plan -//! failure returns 0 BEFORE any fetch and Java can fall back to its own pool; the fetch runs on -//! a new thread. JavaVM attach / GlobalRef handling is the same shape as the GOG facade. use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -59,8 +37,6 @@ fn android_log(message: &str) { } } -/// Per-run native state behind the `jlong` handle — the same type for a fetch run and an -/// assembly run, so one `nativeCancel` / `nativeRelease` pair serves both. pub struct EpicDownloadHandle { cancel: Arc<AtomicBool>, } @@ -200,7 +176,6 @@ fn call_on_complete(env: &mut JNIEnv, listener: &JObject, success: bool, error: clear_pending_exception(env); } -/// Log to logcat AND to the listener (the Kotlin manager mirrors it into its own debug log). fn log_both(env: &mut JNIEnv, listener: &JObject, line: &str) { android_log(line); call_on_log(env, listener, line); @@ -334,8 +309,6 @@ fn run_on_thread( let bytes_total = plan.total_bytes; let chunks_total = plan.needed.len() as u64; - // Progress fires from the core's process-pool threads; attach each as a daemon like the - // Steam engine's `dispatch_download_progress` does. let progress = |bytes_done: u64, chunks_done: u64| { let Ok(mut env) = vm.attach_current_thread_as_daemon() else { return; @@ -371,11 +344,6 @@ fn run_on_thread( ); } -/// `EpicNative.nativeAssemble(manifest, installDir, chunkCacheDir, fileIdx, listener)`: write -/// the files `fileIdx` names from the chunk cache (see `assemble.rs`). Parse + plan run on the -/// calling thread, so bad inputs return 0 (after `onComplete(false, reason, 0)`) before anything -/// is written; the assembly runs on a new thread and reports per file through `onProgress(bytes, -/// bytesTotal, filesDone, filesTotal)`, then `onComplete(success, error, bytesWritten)`. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeAssemble( mut env: JNIEnv, @@ -481,7 +449,5 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_native if handle == 0 { return; } - // The worker thread owns its own Arc clone of the cancel flag and its own GlobalRef, so - // freeing the handle is safe even if a (cancelled) run is still winding down. drop(unsafe { Box::from_raw(handle as *mut EpicDownloadHandle) }); } diff --git a/app/src/main/rust/stores/src/epic/manifest.rs b/app/src/main/rust/stores/src/epic/manifest.rs index 7fbe834ea..d0b38fdd2 100644 --- a/app/src/main/rust/stores/src/epic/manifest.rs +++ b/app/src/main/rust/stores/src/epic/manifest.rs @@ -1,43 +1,24 @@ -//! Epic build manifest parsers — a 1:1 port of `EpicDownloadManager.parseManifest` (binary, -//! magic `0x44BEC00C`) and `parseJsonManifest` (legacy JSON manifests). -//! -//! Every quirk of the Java reader is kept on purpose (the Java manager stays the reference and -//! the two engines must derive the same plan from the same bytes): section skips by declared -//! size, `readFString` UTF-16LE/ASCII decoding, duplicate-GUID handling (last value, first -//! position), the JSON blob decoding (`blobToNum`) and its fallbacks, and "any exception → -//! parse failed". use std::collections::HashMap; use std::io::Read; -/// Binary manifest magic (`EpicDownloadManager.parseManifest`). pub const MANIFEST_MAGIC: u32 = 0x44BE_C00C; -/// One entry of the ChunkDataList section (`EpicDownloadManager.ChunkInfo`). #[derive(Clone, Debug, PartialEq, Eq)] pub struct ChunkInfo { - /// Four uint32 in binary read order. pub guid: [u32; 4], - /// uint64 "hash" that forms the first half of the chunk file name. pub hash: u64, - /// SHA-1 of the DECOMPRESSED chunk; `None` for JSON manifests (Java: `sha1 == null`). pub sha1: Option<[u8; 20]>, - /// Subfolder number (DECIMAL `%02d`). `i32` so the JSON parser's `Integer.parseInt` range is - /// representable exactly (binary manifests always yield 0..=255). pub group_num: i32, - /// Uncompressed size (0 for JSON manifests). pub window_size: i32, - /// Compressed (wire) size. Signed like the Java `long`; JSON manifests may yield anything. pub file_size: i64, } impl ChunkInfo { - /// `String.format("%08X%08X%08X%08X", guid[0..4])`. pub fn guid_str(&self) -> String { guid_to_string(&self.guid) } - /// `ChunkInfo.getPath`: `"<chunkDir>/<%02d groupNum>/<%016X hash>_<GUID>.chunk"`. pub fn path(&self, chunk_dir: &str) -> String { format!( "{}/{:02}/{:016X}_{}.chunk", @@ -48,8 +29,6 @@ impl ChunkInfo { ) } - /// Java `chunk.sha1 != null && length == 20 && !allZero` — the condition under which the - /// streaming downloader verifies the decompressed chunk. pub fn verifiable_sha1(&self) -> Option<&[u8; 20]> { match &self.sha1 { Some(h) if h.iter().any(|b| *b != 0) => Some(h), @@ -57,7 +36,6 @@ impl ChunkInfo { } } - /// `Math.max(chunk.fileSize, 1)` — the byte credit the Java pool uses for totals/progress. pub fn credit_bytes(&self) -> u64 { if self.file_size < 1 { 1 @@ -67,13 +45,10 @@ impl ChunkInfo { } } -/// One file part referencing a chunk (`EpicDownloadManager.ChunkPart`). #[derive(Clone, Debug, PartialEq, Eq)] pub struct ChunkPart { pub guid: [u32; 4], - /// Offset into the DECOMPRESSED chunk (Java `int`). pub offset: i32, - /// Part length (Java `int`; summed as `size & 0xFFFFFFFFL`). pub size: i32, } @@ -83,26 +58,20 @@ impl ChunkPart { } } -/// One entry of the FileManifestList section (`EpicDownloadManager.FileInfo`). #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct FileInfo { pub filename: String, pub parts: Vec<ChunkPart>, - /// Install-tag names; empty = required/base file. pub install_tags: Vec<String>, - /// Full-file SHA-1; `None` for JSON manifests. pub sha1: Option<[u8; 20]>, } impl FileInfo { - /// `FileInfo.fileSize()` = Σ `(part.size & 0xFFFFFFFFL)`. pub fn file_size(&self) -> u64 { self.parts.iter().map(|p| p.size as u32 as u64).sum() } } -/// Parsed manifest (`EpicDownloadManager.EpicManifest.ParsedManifest`, minus the CDN list which -/// the Java side still owns). #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Manifest { pub chunk_dir: String, @@ -111,8 +80,6 @@ pub struct Manifest { } impl Manifest { - /// `byGuid` map as `uniqueChunksForFiles` builds it: `LinkedHashMap.put` per chunk, so a - /// duplicate GUID resolves to the LAST `ChunkInfo`. pub fn chunk_index_by_guid(&self) -> HashMap<String, usize> { let mut map = HashMap::with_capacity(self.unique_chunks.len()); for (i, c) in self.unique_chunks.iter().enumerate() { @@ -126,7 +93,6 @@ pub fn guid_to_string(guid: &[u32; 4]) -> String { format!("{:08X}{:08X}{:08X}{:08X}", guid[0], guid[1], guid[2], guid[3]) } -/// `chunkDir` from the binary header version (`parseManifest`). pub fn chunk_dir_for_binary_version(version: i32) -> &'static str { if version >= 15 { "ChunksV4" @@ -139,7 +105,6 @@ pub fn chunk_dir_for_binary_version(version: i32) -> &'static str { } } -/// `chunkDir` from `ManifestFileVersion` (`parseJsonManifest`; note the different `< 3` default). pub fn chunk_dir_for_json_version(version: i32) -> &'static str { if version >= 15 { "ChunksV4" @@ -152,10 +117,7 @@ pub fn chunk_dir_for_json_version(version: i32) -> &'static str { } } -// ── little-endian cursor with ByteBuffer-like failure semantics ───────────────────────────── -/// Mirrors the `ByteBuffer` calls the Java parser makes: every under-run or out-of-range -/// `position()` is an exception there, which the Java code turns into "parse failed" (null). struct Cursor<'a> { buf: &'a [u8], pos: usize, @@ -170,7 +132,6 @@ impl<'a> Cursor<'a> { self.pos } - /// `ByteBuffer.position(p)` — `p` must be within `0..=limit`. fn set_position(&mut self, p: i64) -> Result<(), String> { if p < 0 || p as u64 > self.buf.len() as u64 { return Err(format!("position {p} out of range (limit {})", self.buf.len())); @@ -223,7 +184,6 @@ impl<'a> Cursor<'a> { Ok(out) } - /// `ByteBuffer.position(position + n)` with the same range check. fn skip(&mut self, n: i64) -> Result<(), String> { let target = self.pos as i64 + n; self.set_position(target) @@ -233,23 +193,19 @@ impl<'a> Cursor<'a> { &self.buf[self.pos..] } - /// `EpicDownloadManager.readFString`: int32 length; `0` → ""; negative → `(-len)-1` UTF-16LE - /// code units + 2-byte terminator; positive → `len-1` US-ASCII bytes + 1-byte terminator. fn fstring(&mut self) -> Result<String, String> { let length = self.i32()?; if length == 0 { return Ok(String::new()); } if length < 0 { - // Java: `int chars = (-length) - 1;` — `-Integer.MIN_VALUE` wraps to itself, then -1 - // gives Integer.MAX_VALUE; `new byte[chars * 2]` overflows negative → exception. let chars = length.wrapping_neg().wrapping_sub(1); let byte_len = chars.checked_mul(2).ok_or("fstring length overflow")?; if byte_len < 0 { return Err("negative fstring length".to_string()); } let bytes = self.take(byte_len as usize)?; - self.take(2)?; // null terminator + self.take(2)?; let units: Vec<u16> = bytes .chunks_exact(2) .map(|p| u16::from_le_bytes([p[0], p[1]])) @@ -257,8 +213,8 @@ impl<'a> Cursor<'a> { Ok(String::from_utf16_lossy(&units)) } else { let bytes = self.take((length - 1) as usize)?; - self.take(1)?; // null terminator - // `new String(bytes, US_ASCII)`: every byte > 0x7F decodes to U+FFFD. + self.take(1)?; + Ok(bytes .iter() .map(|&b| if b < 0x80 { b as char } else { '\u{FFFD}' }) @@ -267,10 +223,7 @@ impl<'a> Cursor<'a> { } } -// ── binary manifest ────────────────────────────────────────────────────────────────────── -/// `EpicDownloadManager.parseManifest(bytes)`: binary when the magic matches, otherwise the JSON -/// parser. `Err` wherever Java returns `null`. pub fn parse_manifest(bytes: &[u8]) -> Result<Manifest, String> { let mut cur = Cursor::new(bytes); let magic = cur.u32()?; @@ -284,7 +237,7 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { let header_size = cur.i32()?; let size_uncompressed = cur.i32()?; let _size_compressed = cur.i32()?; - cur.skip(20)?; // SHA-1 of the body, not checked by the Java parser + cur.skip(20)?; let stored_as = cur.u8()?; let version = cur.i32()?; let chunk_dir = chunk_dir_for_binary_version(version); @@ -297,8 +250,6 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { if size_uncompressed < 0 { return Err("negative sizeUncompressed".to_string()); } - // Java: `inflater.inflate(new byte[sizeUncompressed])` — a single call that fills at most - // `sizeUncompressed` bytes; anything else (short, corrupt) is a size mismatch → null. let mut out = Vec::with_capacity(size_uncompressed as usize); let mut dec = flate2::read::ZlibDecoder::new(body_slice).take(size_uncompressed as u64); dec.read_to_end(&mut out) @@ -317,18 +268,16 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { let mut b = Cursor::new(body); - // ManifestMeta: skipped by its declared size (the size field counts itself). let meta_size = b.i32()?; let meta_end = b.position() as i64 - 4 + meta_size as i64; b.set_position(meta_end)?; - // ChunkDataList let cdl_start = b.position(); let cdl_size = b.i32()?; let _cdl_version = b.u8()?; let chunk_count = b.i32()?; if chunk_count < 0 { - return Err("negative chunkCount".to_string()); // `new ArrayList<>(negative)` throws + return Err("negative chunkCount".to_string()); } let chunk_count = chunk_count as usize; let mut chunks: Vec<ChunkInfo> = (0..chunk_count) @@ -361,7 +310,6 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { } b.set_position(cdl_start as i64 + cdl_size as i64)?; - // FileManifestList let fml_start = b.position(); let fml_size = b.i32()?; let _fml_version = b.u8()?; @@ -375,12 +323,12 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { f.filename = b.fstring()?; } for _ in 0..file_count { - b.fstring()?; // symlink targets + b.fstring()?; } for f in files.iter_mut() { f.sha1 = Some(b.sha1()?); } - b.skip(file_count as i64)?; // per-file flags byte + b.skip(file_count as i64)?; for f in files.iter_mut() { let tag_count = b.i32()?; let mut j = 0; @@ -415,8 +363,6 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { }) } -/// `LinkedHashMap<String, ChunkInfo>` keyed by GUID string: a repeated key keeps its FIRST -/// position but takes the LAST value. fn dedup_last_wins_first_position(chunks: Vec<ChunkInfo>) -> Vec<ChunkInfo> { let mut order: Vec<ChunkInfo> = Vec::with_capacity(chunks.len()); let mut index: HashMap<String, usize> = HashMap::with_capacity(chunks.len()); @@ -433,10 +379,7 @@ fn dedup_last_wins_first_position(chunks: Vec<ChunkInfo>) -> Vec<ChunkInfo> { order } -// ── JSON manifest (older games) ────────────────────────────────────────────────────────── -/// `org.json` value → string coercion as `optString` / `getString` do on Android (numbers and -/// booleans print as-is, JSON null prints "null", containers print their JSON). fn json_to_string(v: &serde_json::Value) -> String { match v { serde_json::Value::String(s) => s.clone(), @@ -445,9 +388,6 @@ fn json_to_string(v: &serde_json::Value) -> String { } } -/// `EpicDownloadManager.blobToNum`: a JSON manifest writes every number as a "blob", each byte -/// as three decimal digits, least significant first ("013000000000" = 13). Bits past 64 are -/// dropped; `None` when a group is not a number (Java: `NumberFormatException`). pub fn blob_to_num(s: &str) -> Option<u64> { let bytes = s.as_bytes(); let mut num: u64 = 0; @@ -464,7 +404,6 @@ pub fn blob_to_num(s: &str) -> Option<u64> { Some(num) } -/// `obj.optString(key, fallback)`. fn opt_string(obj: &serde_json::Map<String, serde_json::Value>, key: &str, fallback: &str) -> String { match obj.get(key) { Some(v) => json_to_string(v), @@ -472,16 +411,12 @@ fn opt_string(obj: &serde_json::Map<String, serde_json::Value>, key: &str, fallb } } -/// `(int) Long.parseLong(hex8, 16)` — the Java GUID quarter parse; invalid hex throws out of the -/// whole parse. fn parse_guid_quarter(s: &str) -> Result<u32, String> { let v = i64::from_str_radix(s, 16).map_err(|e| format!("bad guid hex '{s}': {e}"))?; Ok(v as i32 as u32) } fn parse_guid_from_hex(s: &str) -> Result<[u32; 4], String> { - // Java substrings by UTF-16 unit and `parseLong` throws on anything non-hex; a non-ASCII - // prefix can never parse, so refuse it before slicing (keeps the slice on char boundaries). if s.len() < 32 || !s.as_bytes()[..32].is_ascii() { return Err(format!("bad guid hex '{s}'")); } @@ -493,7 +428,6 @@ fn parse_guid_from_hex(s: &str) -> Result<[u32; 4], String> { ]) } -/// `EpicDownloadManager.parseJsonManifest`. pub fn parse_json_manifest(bytes: &[u8]) -> Result<Manifest, String> { let text = String::from_utf8_lossy(bytes); let root: serde_json::Value = @@ -513,7 +447,6 @@ pub fn parse_json_manifest(bytes: &[u8]) -> Result<Manifest, String> { let data_group_list = root.get("DataGroupList").and_then(|v| v.as_object()); let chunk_filesize_list = root.get("ChunkFilesizeList").and_then(|v| v.as_object()); - // `LinkedHashMap<String, ChunkInfo>` keyed by the RAW key string (not the normalized GUID). let mut chunk_order: Vec<ChunkInfo> = Vec::new(); let mut chunk_index: HashMap<String, usize> = HashMap::new(); for (guid_hex, hash_value) in chunk_hash_list.iter() { @@ -598,12 +531,9 @@ pub fn parse_json_manifest(bytes: &[u8]) -> Result<Manifest, String> { }) } -// ── tests ──────────────────────────────────────────────────────────────────────────────── #[cfg(test)] pub(crate) mod test_support { - //! Synthetic binary-manifest builder used by this module's tests and by `plan`/`driver` - //! tests. Emits exactly the layout `parseManifest` walks (meta / CDL / FML with sizes). use super::*; use std::io::Write; @@ -621,7 +551,6 @@ pub(crate) mod test_support { pub name: String, pub sha1: [u8; 20], pub tags: Vec<String>, - /// (guid, offset, size) pub parts: Vec<([u32; 4], i32, i32)>, } @@ -648,17 +577,14 @@ pub(crate) mod test_support { v } - /// Body (meta + CDL + FML) without the outer header. pub fn build_body(chunks: &[TestChunk], files: &[TestFile], meta_padding: usize) -> Vec<u8> { let mut body = Vec::new(); - // ManifestMeta: size field counts itself. let meta_size = 4 + meta_padding; body.extend_from_slice(&(meta_size as i32).to_le_bytes()); body.extend(std::iter::repeat(0xEEu8).take(meta_padding)); - // ChunkDataList let mut cdl = Vec::new(); - cdl.push(0u8); // version + cdl.push(0u8); cdl.extend_from_slice(&(chunks.len() as i32).to_le_bytes()); for c in chunks { for g in c.guid { @@ -680,14 +606,12 @@ pub(crate) mod test_support { for c in chunks { cdl.extend_from_slice(&c.file_size.to_le_bytes()); } - // Trailing padding the parser must skip via cdlSize. cdl.extend_from_slice(&[0xAA, 0xBB, 0xCC]); body.extend_from_slice(&((cdl.len() + 4) as i32).to_le_bytes()); body.extend_from_slice(&cdl); - // FileManifestList let mut fml = Vec::new(); - fml.push(0u8); // version + fml.push(0u8); fml.extend_from_slice(&(files.len() as i32).to_le_bytes()); for f in files { if f.name.is_ascii() { @@ -697,13 +621,13 @@ pub(crate) mod test_support { } } for _ in files { - fml.extend_from_slice(&fstring_ascii("")); // symlink target + fml.extend_from_slice(&fstring_ascii("")); } for f in files { fml.extend_from_slice(&f.sha1); } for _ in files { - fml.push(0); // flags + fml.push(0); } for f in files { fml.extend_from_slice(&(f.tags.len() as i32).to_le_bytes()); @@ -714,7 +638,6 @@ pub(crate) mod test_support { for f in files { fml.extend_from_slice(&(f.parts.len() as i32).to_le_bytes()); for (guid, offset, size) in &f.parts { - // struct size 4 + 16 + 4 + 4 = 28, plus 2 bytes of padding the parser skips. fml.extend_from_slice(&30i32.to_le_bytes()); for g in guid { fml.extend_from_slice(&g.to_le_bytes()); @@ -729,7 +652,6 @@ pub(crate) mod test_support { body } - /// Full manifest bytes: header + (optionally zlib-compressed) body. pub fn build_manifest( chunks: &[TestChunk], files: &[TestFile], @@ -745,7 +667,7 @@ pub(crate) mod test_support { } else { body.clone() }; - let header_size: i32 = 41 + 3; // 3 bytes of extra header the parser must jump over + let header_size: i32 = 41 + 3; let mut out = Vec::new(); out.extend_from_slice(&MANIFEST_MAGIC.to_le_bytes()); out.extend_from_slice(&header_size.to_le_bytes()); @@ -916,13 +838,9 @@ mod tests { fn bad_uncompressed_size_is_a_parse_failure() { let good = build_manifest(&sample_chunks(), &sample_files(), 21, true); let real = i32::from_le_bytes([good[8], good[9], good[10], good[11]]); - // Claim MORE than the body inflates to: Java's single `inflate()` returns fewer bytes - // than `sizeUncompressed` → "Decomp size mismatch" → null. let mut bigger = good.clone(); bigger[8..12].copy_from_slice(&(real + 1).to_le_bytes()); assert!(parse_manifest(&bigger).is_err()); - // Claim LESS: Java fills exactly `sizeUncompressed` bytes (silently truncating), then the - // FileManifestList end position lands past the limit → exception → null. let mut smaller = good; smaller[8..12].copy_from_slice(&(real - 1).to_le_bytes()); assert!(parse_manifest(&smaller).is_err()); @@ -933,7 +851,7 @@ mod tests { let mut v = fstring_ascii("abc"); v.extend_from_slice(&fstring_utf16("dé")); v.extend_from_slice(&fstring_ascii("")); - v.extend_from_slice(&[4, 0, 0, 0, b'a', 0xFF, b'b', 0]); // non-ASCII byte → U+FFFD + v.extend_from_slice(&[4, 0, 0, 0, b'a', 0xFF, b'b', 0]); let mut c = Cursor::new(&v); assert_eq!(c.fstring().unwrap(), "abc"); assert_eq!(c.fstring().unwrap(), "dé"); diff --git a/app/src/main/rust/stores/src/epic/plan.rs b/app/src/main/rust/stores/src/epic/plan.rs index 3dc2eeeb8..5f96e338c 100644 --- a/app/src/main/rust/stores/src/epic/plan.rs +++ b/app/src/main/rust/stores/src/epic/plan.rs @@ -1,19 +1,9 @@ -//! Chunk plan — the part of `EpicDownloadManager.install` between "manifest parsed" and "submit -//! to the pool", ported 1:1: install-tag file selection (`resolveInstallFiles` / -//! `getRequiredInstallFiles`), the unique-chunk list for a file set (`uniqueChunksForFiles`), -//! the byte total (`Σ max(fileSize, 1)`), chunk cache paths and CDN URLs. -//! -//! At runtime the Java manager passes the INDICES of its `pendingFiles` (post delta/verify), so -//! the file selection functions here are exercised by the unit tests (and available to a future -//! caller) while the driver only runs `unique_chunks_for_files` on Java's pending set — the same -//! function Java runs, on the same manifest, in the same order. use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use super::manifest::{ChunkInfo, Manifest}; -/// `getRequiredInstallFiles`: files with no install tags; if NONE carries a tag, every file. pub fn required_install_files(m: &Manifest) -> Vec<usize> { let required: Vec<usize> = m .files @@ -29,11 +19,6 @@ pub fn required_install_files(m: &Manifest) -> Vec<usize> { } } -/// `resolveInstallFiles(manifest, selectedTags)`: -/// - `None` → every file (legacy / DLC / overlay path); -/// - `Some([])` → required(base) files only; -/// - otherwise → base files + any file carrying at least one selected tag, falling back to -/// required-only when nothing matched. pub fn resolve_install_files(m: &Manifest, selected_tags: Option<&[String]>) -> Vec<usize> { let Some(tags) = selected_tags else { return (0..m.files.len()).collect(); @@ -59,8 +44,6 @@ pub fn resolve_install_files(m: &Manifest, selected_tags: Option<&[String]>) -> } } -/// `uniqueChunksForFiles(manifest, files)`: chunk indices (into `unique_chunks`) in first-seen -/// order over `files[*].parts`, each GUID once, GUIDs absent from the chunk list dropped. pub fn unique_chunks_for_files(m: &Manifest, file_indices: &[usize]) -> Vec<usize> { let by_guid: HashMap<String, usize> = m.chunk_index_by_guid(); let mut seen: HashSet<String> = HashSet::new(); @@ -81,7 +64,6 @@ pub fn unique_chunks_for_files(m: &Manifest, file_indices: &[usize]) -> Vec<usiz chunks } -/// `totalBytes += Math.max(chunk.fileSize, 1)` over the needed chunks. pub fn total_credit_bytes(m: &Manifest, chunk_indices: &[usize]) -> u64 { chunk_indices .iter() @@ -90,15 +72,10 @@ pub fn total_credit_bytes(m: &Manifest, chunk_indices: &[usize]) -> u64 { .sum() } -/// `<installDir>/.chunks`. pub fn chunk_cache_dir(install_dir: &str) -> PathBuf { Path::new(install_dir).join(super::CHUNK_CACHE_DIR) } -/// Where this run's chunks go: `cache_dir` when the caller named one, else the default -/// `<installDir>/.chunks`. A separate directory exists so the cache can sit on internal storage -/// while the game installs to an SD card: a chunk is written once into the cache and once into -/// the assembled file, and with both on a 26 MB/s card the second write starved the fetchers. pub fn resolve_cache_dir(install_dir: &str, cache_dir: &str) -> PathBuf { if cache_dir.is_empty() { chunk_cache_dir(install_dir) @@ -107,37 +84,23 @@ pub fn resolve_cache_dir(install_dir: &str, cache_dir: &str) -> PathBuf { } } -/// Final cache file for a chunk: `new File(chunkCacheDir, chunk.guidStr())`. pub fn cached_chunk_path(cache_dir: &Path, chunk: &ChunkInfo) -> PathBuf { cache_dir.join(chunk.guid_str()) } -/// The Java pool's skip rule: a chunk whose cache file EXISTS (any size) is not fetched. pub fn is_chunk_cached(cache_dir: &Path, chunk: &ChunkInfo) -> bool { cached_chunk_path(cache_dir, chunk).exists() } -/// One CDN entry as the Java manager passes it: `cdn.baseUrl + cdn.cloudDir` (auth params are -/// never used on chunk URLs). Chunk URL = `prefix + "/" + chunk.getPath(chunkDir)`. pub fn chunk_url(cdn_prefix: &str, chunk_dir: &str, chunk: &ChunkInfo) -> String { format!("{}/{}", cdn_prefix, chunk.path(chunk_dir)) } -/// Undo the JSON string escaping the Java CDN scanner passes through. `EpicApiClient` re-serializes -/// the manifest API response with Android's `org.json`, which escapes every `/` as `\/`, and -/// `parseCdnUrls` takes the raw substrings — so a base URL arrives as `https:\/\/host\` and a cloud -/// dir as `/Builds\/Org\/…\/default\`. Java's `HttpURLConnection` (OkHttp) treats `\` as `/` and -/// only ever reached the first CDN, so it never noticed; reqwest's URL parser does the same -/// mapping, which leaves empty `//` path segments that Fastly/Akamai tolerate but CloudFront -/// rejects outright (immediate 4xx). Produce the URL Java intended: `\/` → `/`, any other `\` -/// dropped, and no trailing slash (the chunk path is joined with one). pub fn normalize_prefix(raw: &str) -> String { let unescaped = raw.replace("\\/", "/").replace('\\', ""); unescaped.trim_end_matches('/').to_string() } -/// Distinct CDN prefixes in first-seen order, normalized (the fetch core wants one host key per -/// distinct endpoint; Java would simply try an identical URL twice, which changes nothing). pub fn distinct_prefixes(cdn_prefixes: &[String]) -> Vec<String> { let mut seen = HashSet::new(); let mut out = Vec::new(); @@ -153,9 +116,6 @@ pub fn distinct_prefixes(cdn_prefixes: &[String]) -> Vec<String> { out } -/// Improvements round 1: split the window across the distinct CDNs so the whole ceiling is -/// reachable whatever the host count — `max(6, ceil(max_workers / hosts))` (3 Epic CDNs at a -/// ceiling of 32 → 11 each; a single host gets all 32). pub fn per_host_cap(max_workers: usize, host_count: usize) -> usize { let hosts = host_count.max(1); let workers = max_workers.max(1); @@ -176,14 +136,10 @@ mod tests { #[test] fn install_tag_rules_match_java() { let m = manifest(); - // null → all assert_eq!(resolve_install_files(&m, None), vec![0, 1, 2]); - // empty → required only (file 0 has no tags) assert_eq!(resolve_install_files(&m, Some(&[])), vec![0]); - // German → base + the German file let de = vec!["German".to_string(), "de-DE".to_string()]; assert_eq!(resolve_install_files(&m, Some(&de)), vec![0, 1]); - // unknown tag → nothing extra matched, but base files matched, so base only let xx = vec!["Klingon".to_string()]; assert_eq!(resolve_install_files(&m, Some(&xx)), vec![0]); } @@ -204,7 +160,6 @@ mod tests { let mut files = sample_files(); files[0].tags = vec!["English".to_string()]; let m = parse_manifest(&build_manifest(&sample_chunks(), &files, 21, false)).unwrap(); - // No untagged file, unknown tag selected → matched nothing → required → (none) → all. let xx = vec!["Klingon".to_string()]; assert_eq!(resolve_install_files(&m, Some(&xx)), vec![0, 1, 2]); } @@ -212,12 +167,10 @@ mod tests { #[test] fn unique_chunks_follow_first_seen_part_order() { let m = manifest(); - // file 2 first: chunk c[2] then c[0]; then file 0: c[0] (dup), c[1]. assert_eq!(unique_chunks_for_files(&m, &[2, 0]), vec![2, 0, 1]); assert_eq!(unique_chunks_for_files(&m, &[1]), vec![1]); assert!(unique_chunks_for_files(&m, &[]).is_empty()); assert!(unique_chunks_for_files(&m, &[99]).is_empty(), "out-of-range index ignored"); - // total = 700000 + max(0,1) + 9 assert_eq!(total_credit_bytes(&m, &[0, 1, 2]), 700_000 + 1 + 9); } @@ -256,13 +209,11 @@ mod tests { #[test] fn prefixes_are_unescaped_from_the_java_json_scan() { - // Exactly what Bannerlator's Epic debug log shows its Java manager extracting for a real title. let raw = "https:\\/\\/egdownload.fastly-edge.com\\/Builds\\/Org\\/o-83e8\\/04b7\\/default\\"; assert_eq!( normalize_prefix(raw), "https://egdownload.fastly-edge.com/Builds/Org/o-83e8/04b7/default" ); - // Already-clean input is untouched (minus a trailing slash). assert_eq!( normalize_prefix("https://download.epicgames.com/Builds/x/default/"), "https://download.epicgames.com/Builds/x/default" diff --git a/app/src/main/rust/stores/src/fetch_core.rs b/app/src/main/rust/stores/src/fetch_core.rs index e700ee3a2..9dc96faa6 100644 --- a/app/src/main/rust/stores/src/fetch_core.rs +++ b/app/src/main/rust/stores/src/fetch_core.rs @@ -1,28 +1,3 @@ -//! Shared adaptive HTTP fetch core for the store download engines (Epic / GOG / Amazon). -//! -//! This is the generic half of the fetch layer Bannerlator's Steam depot writer grew: an -//! adaptive in-flight window (slow-start → hold at plateau → shrink on REAL errors only), a -//! speed-ranked per-host-capped scheduler, an in-flight RAW byte budget sized from the window -//! ceiling, per-item retry with exponential back-off + host rotation, one current-thread tokio -//! runtime driving every request through a [`FuturesUnordered`], and an mpsc hand-off to a sync -//! process pool that calls the adapter's [`FetchSink::process`] (inflate + hash + write). -//! -//! The tunables come from [`crate::fetch_tuning`], copied number for number from Bannerlator's -//! depot writer, so a `fetch-window` line from either app reads against the same tuning notes. -//! -//! What the core does NOT know: manifests, resume/skip rules, output layout, hashing. The adapter -//! builds the item list (already excluding present+verified items, exactly as its Java manager -//! does), maps host index → URL, and does everything with the body inside its sink. -//! -//! Log grammar (all lines go through the `log` callback; `label` is adapter-chosen, e.g. -//! `epic app=Fortnite`): -//! - `fetch-start label=… items=… bytes_reserved=… hosts=… ceiling=… budget=…MiB tier_max=… per_host_cap=… distinct_hosts=… window=… mode=body|stream` -//! - `fetch-window label=… window=N (min=… max=…) in_flight=… last=…MB/s ewma=…MB/s best=…MB/s reason=… cooldown=…ms err_rate=…% phase=… rtt=…ms budget_stalls=… host_stalls=…` -//! (same fields as the Steam engine's `fetch-window depot=…` line, so the two are A/B-comparable) -//! - `throughput label=… overall=…MB/s total=…MB elapsed=…s used=x/y servers: [host …MB/s …MB] …` every 5 s -//! - `fetch-end label=… items_ok=… bytes=… credited=… elapsed_ms=… avg_mbps=… peak_mbps=… result=ok|cancelled|error [error=…]` -//! -//! `MB/s` here means MiB/s (1024²), matching the Steam engine's lines. use crate::fetch_tuning::{ budget_admits, connect_timeout, inflight_budget_bytes, retry_backoff_millis, AsyncFetchError, @@ -46,95 +21,42 @@ use std::thread; use std::time::{Duration, Instant}; use tokio::sync::{Notify, Semaphore}; -/// Maximum attempts per item (fetch failures AND `SinkError::Retry` both count). Same as Steam. pub const MAX_ITEM_ATTEMPTS: u32 = MAX_CHUNK_ATTEMPTS; -/// How often the throughput reporter samples (and logs) — the samples feed `peak_mbps`. pub const THROUGHPUT_SAMPLE_INTERVAL_MS: u64 = 5_000; -/// Upper bound on how long the driver sits in one `await` before re-checking cancel/error/feedback. const DRIVER_POLL_MS: u64 = 250; -// ── Delay awareness (bufferbloat) ── -// On a 5G hotspot the window kept growing (31 → 84) while the per-request delay went from 3 s to -// 11 s and throughput stayed at 10-20 MB/s: every extra request only queued behind the others in -// the link's buffer. The signal used here is time-to-first-byte (request sent → response headers), -// which on a link with a deep buffer grows with the queue, while on a fast link it stays at a few -// round trips however wide the window gets. The baseline is the lowest per-probe mean seen. -/// A probe needs at least this many TTFB samples before its mean is trusted. pub const LATENCY_MIN_SAMPLES: u32 = 3; -/// Mean TTFB at or above `baseline ×` this counts as queueing delay… pub const LATENCY_HOLD_FACTOR: f64 = 2.5; -/// …and only when it is also this far above the baseline in absolute terms: a fast link whose -/// TTFB goes from 20 ms to 60 ms (new connections' TLS handshakes) is not bufferbloat. pub const LATENCY_MIN_EXCESS_MS: f64 = 250.0; -/// Throughput above `best × (1 + this)` is a gain that justifies the extra delay: hold, but never -/// shrink, while it lasts. pub const LATENCY_GAIN_EPS: f64 = 0.25; -/// Gentle proportional shrink while the delay keeps rising (gentler than the error shrink). pub const LATENCY_SHRINK_FACTOR: f64 = 0.85; -// ── Stalled hosts ── -// A CDN host can stop delivering while its connections stay open: on the device the Fastly host -// froze at 49 MB for the rest of the run while the other host carried everything, and its requests -// sat until their idle timeout. A host whose in-flight requests receive no byte for this long, -// while another host is receiving, has its requests cancelled and re-queued and is put to rest. -/// No byte from any of a host's in-flight requests for this long (while another host delivers). pub const HOST_STALL_MS: u64 = if cfg!(test) { 1_500 } else { 10_000 }; -/// First rest for a stalled host; doubles per further stall of the same host, reset on success. pub const HOST_STALL_COOLDOWN_MS: u64 = 30_000; pub const HOST_STALL_COOLDOWN_MAX_MS: u64 = 300_000; -/// How often the driver looks for stalled hosts. const STALL_CHECK_MS: u64 = 250; -/// Stream mode: the longest a request may sit with no byte arriving before it is failed (the -/// store's own timeout when it is shorter). A dead connection on a single-host run, where the -/// stall detector has no healthy host to compare with, never holds an item longer than this. pub const STREAM_IDLE_CAP: Duration = Duration::from_secs(30); -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Public contract -// ───────────────────────────────────────────────────────────────────────────────────────────── -/// One unit of work: a body to fetch from one of the hosts and hand to the sink. #[derive(Clone, Debug)] pub struct FetchItem { - /// Adapter-defined (index into the adapter's own table). Only echoed in errors / logs. pub id: u64, - /// `len == hosts.len()` → `urls[host_idx]`; `len == 1` → the same URL on every host. Any other - /// non-zero length falls back to `urls.get(host_idx)` then `urls[0]`. Empty = fatal. pub urls: Vec<String>, - /// Expected RAW (wire) bytes, for the in-flight byte budget; `0` → nominal 1 MiB. Ignored in - /// stream mode (streams are budgeted piece by piece as they arrive). pub reserve: u64, - /// Inclusive HTTP `Range` (`bytes=a-b`); `None` = whole body. pub range: Option<(u64, u64)>, } #[derive(Clone, Debug)] pub struct FetchOptions { - /// Window ceiling (tier); the core clamps it to `hosts × per_host_cap` and the hard cap. pub max_workers: usize, - /// Max concurrent requests per distinct host (6 unless the store needs otherwise). Honoured - /// as given: 1 host × `per_host_cap` 8 = a ceiling of 8. pub per_host_cap: usize, - /// Whole-body mode: the deadline for the entire request (connect + headers + body). Stream - /// mode: the deadline for the response headers, then an IDLE deadline per received piece. pub timeout: Duration, - /// Sent on EVERY request (applied on the request builder, so they override the client-level - /// defaults — e.g. a store's own `User-Agent`, or `Authorization: Bearer …`). Validated once - /// when the client is built. pub headers: Vec<(String, String)>, - /// PEM bundle path; `""` = the platform roots reqwest/rustls ships with. pub ca_bundle_path: String, - /// Sync process-pool threads (inflate + hash + write). pub process_workers: usize, - /// Log prefix, e.g. `"epic app=Fortnite"`. pub label: String, - /// `false` (default): each item's whole body is buffered and handed to - /// [`FetchSink::process`]. `true`: the body is streamed — pieces go to - /// [`FetchSink::on_chunk`] in order as they arrive, then [`FetchSink::on_finish`]; for - /// multi-GB files (Amazon) that keeps memory at "pieces not yet written", not whole files. - /// Applies to every item of the run. pub stream: bool, } @@ -153,36 +75,20 @@ impl Default for FetchOptions { } } -/// What the sink says about a body it was handed. #[derive(Clone, Debug)] pub enum SinkError { - /// Counts as a failed attempt for that item: host cooled + demoted, back-off, re-dispatch on a - /// different host, up to [`MAX_ITEM_ATTEMPTS`]. In stream mode the whole item is re-streamed - /// (`on_chunk` sees `offset == 0` again). Retry(String), - /// Abort the whole run; this becomes the run's first error. Fatal(String), } -/// Runs on a process-pool thread. Whole-body mode uses [`FetchSink::process`]; stream mode uses -/// [`FetchSink::on_chunk`] + [`FetchSink::on_finish`] (default impls refuse, so a whole-body sink -/// is unaffected by the stream option existing). pub trait FetchSink: Send + Sync { - /// Whole-body mode: the raw wire body. Do inflate + hash verify + write here. Returns the - /// number of bytes to credit to progress (adapter's choice, e.g. decompressed bytes). fn process(&self, item: &FetchItem, body: Vec<u8>) -> Result<u64, SinkError>; - /// Stream mode: one piece of the body, delivered in order (`offset` = bytes of this attempt - /// delivered before this piece). `offset == 0` marks the (re)start of an attempt — truncate any - /// partial output from a previous attempt. Each processed piece credits `data.len()` bytes. fn on_chunk(&self, item: &FetchItem, offset: u64, data: &[u8]) -> Result<(), SinkError> { let _ = (item, offset, data); Err(SinkError::Fatal("streaming not supported by this sink".to_string())) } - /// Stream mode: the body ended after `total_len` bytes. Finalize (rename `.tmp`, verify the - /// hash, …). Returns EXTRA bytes to credit (pieces were already credited as they were - /// written; return 0 for none). fn on_finish(&self, item: &FetchItem, total_len: u64) -> Result<u64, SinkError> { let _ = (item, total_len); Err(SinkError::Fatal("streaming not supported by this sink".to_string())) @@ -191,15 +97,9 @@ pub trait FetchSink: Send + Sync { #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct FetchOutcome { - /// Sum of what the sink returned for every successfully processed item (stream mode: every - /// piece written + every `on_finish` extra; pieces of an attempt that later failed stay - /// counted). pub bytes_credited: u64, - /// First error (fetch after max attempts, sink `Fatal`, client build failure). pub error: Option<String>, - /// `cancel` was observed set. pub cancelled: bool, - /// Items the sink accepted. pub items_ok: u64, } @@ -209,10 +109,6 @@ impl FetchOutcome { } } -/// Blocking: spawns its own current-thread tokio runtime + process pool and returns when every -/// item is processed, `cancel` is set, or the first fatal error is recorded. `progress` fires from -/// a process-pool thread after each successful `process` / each written piece + `on_finish` (the -/// caller throttles). pub fn run_fetch( items: Vec<FetchItem>, hosts: &[String], @@ -254,8 +150,6 @@ mode={}", let error_slot: Mutex<Option<String>> = Mutex::new(None); let reporter_done = AtomicBool::new(false); let meter = BandwidthMeter::new(&hosts); - // Stream mode: per-item "attempt N was rejected by the sink mid-stream" flags, so the driver's - // in-flight stream stops reading instead of feeding pieces nobody wants. let abandon: Vec<AtomicU32> = if stream { items.iter().map(|_| AtomicU32::new(NO_ATTEMPT)).collect() } else { @@ -268,11 +162,6 @@ mode={}", let abandon = &abandon; thread::scope(|scope| { - // Async(fetch) → sync(process): tokio unbounded mpsc drained with `blocking_recv`. Memory is - // bounded by the RAW byte budget enforced at DISPATCH (whole-body) / per piece (stream), not - // by channel capacity. Whole-body mode: ONE channel shared by every worker (any worker takes - // the next body). Stream mode: one channel PER worker and an item's pieces always go to the - // same worker (`idx % workers`), so pieces are written in order. let channel_count = if stream { proc_count } else { 1 }; let mut txs: Vec<tokio::sync::mpsc::UnboundedSender<ProcessMsg>> = Vec::with_capacity(channel_count); @@ -283,7 +172,6 @@ mode={}", txs.push(tx); rxs.push(Arc::new(Mutex::new(rx))); } - // Sync(process) → async(driver): sink verdicts, polled non-blocking by the driver. let (fb_tx, fb_rx) = std_mpsc::channel::<Feedback>(); let bytes_credited = &bytes_credited; let items_ok = &items_ok; @@ -291,8 +179,6 @@ mode={}", let error_slot = &error_slot; let reporter_done = &reporter_done; - // Periodic throughput reporter — atomics only, never on the fetch hot path. Its samples are - // also the `peak_mbps` source. let reporter = scope.spawn(move || { crate::priority::background(); loop { @@ -312,15 +198,12 @@ mode={}", } }); - // ── Process pool ── let mut proc_handles = Vec::with_capacity(proc_count); for worker in 0..proc_count { let rx = Arc::clone(&rxs[if stream { worker } else { 0 }]); let fb = fb_tx.clone(); proc_handles.push(scope.spawn(move || { crate::priority::background(); - // Stream mode: (item → attempt, message) whose pieces the sink rejected; the rest - // of that attempt's pieces are dropped and its Finish becomes a Retry verdict. let mut poisoned: HashMap<usize, (u32, String)> = HashMap::new(); loop { if cancel.load(Ordering::Relaxed) { @@ -344,8 +227,6 @@ mode={}", body, } => { let raw_len = body.len() as u64; - // A body that was already queued when cancel / the first error landed - // must not be written any more. if cancel.load(Ordering::Relaxed) || error_slot.lock().expect("err slot poisoned").is_some() { @@ -354,7 +235,6 @@ mode={}", } let item = &items[idx]; let res = sink.process(item, body); - // Free the budget on PROCESS-COMPLETE (actual raw bytes). in_flight.fetch_sub(raw_len, Ordering::Relaxed); let verdict = match res { Ok(credited) => { @@ -390,7 +270,7 @@ mode={}", let len = data.len() as u64; in_flight.fetch_sub(len, Ordering::Relaxed); if poisoned.get(&idx).is_some_and(|(a, _)| *a == attempts) { - continue; // rest of a rejected attempt + continue; } if cancel.load(Ordering::Relaxed) || error_slot.lock().expect("err slot poisoned").is_some() @@ -471,11 +351,6 @@ mode={}", } drop(fb_tx); - // ── Async fetch driver on one current-thread runtime. `txs` are moved in and dropped when - // the driver returns, which closes the channels so the pool's `blocking_recv` yields - // `None`. ── - // The runtime drives on this (already lowered) thread; `on_thread_start` covers the - // blocking pool it starts for DNS lookups. match tokio::runtime::Builder::new_current_thread() .enable_all() .on_thread_start(crate::priority::background) @@ -515,7 +390,6 @@ mode={}", let _ = reporter.join(); }); - // Final partial-interval sample so a short run still has a peak. meter.sample(); let elapsed = started.elapsed(); let elapsed_secs = elapsed.as_secs_f64().max(0.001); @@ -552,11 +426,7 @@ avg_mbps={avg_mbps:.2} peak_mbps={peak_mbps:.2} result={result}", outcome } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Pure helpers (unit-tested) -// ───────────────────────────────────────────────────────────────────────────────────────────── -/// Bytes reserved against the in-flight budget at DISPATCH (nominal when the adapter has none). pub fn reserve_bytes(item: &FetchItem) -> u64 { if item.reserve == 0 { NOMINAL_CHUNK_RESERVE_BYTES @@ -565,8 +435,6 @@ pub fn reserve_bytes(item: &FetchItem) -> u64 { } } -/// Window bounds: `max` = tier ceiling clamped to distinct-hosts × per-host-cap and the hard cap; -/// `min` = the floor; `bootstrap` = the start. Same arithmetic as the Steam depot writer. pub fn window_bounds( max_workers: usize, distinct_hosts: usize, @@ -579,7 +447,6 @@ pub fn window_bounds( (bootstrap, min, max) } -/// Distinct host count (string equality) — the multiplier for the window's host ceiling. pub fn distinct_host_count(hosts: &[String]) -> usize { let mut keys: Vec<&str> = Vec::new(); for h in hosts { @@ -590,8 +457,6 @@ pub fn distinct_host_count(hosts: &[String]) -> usize { keys.len().max(1) } -/// The URL to use for `item` on host `host_idx` (see [`FetchItem::urls`]). `None` when the item -/// carries no URL at all. pub fn url_for_host(item: &FetchItem, host_idx: usize) -> Option<&str> { if item.urls.is_empty() { return None; @@ -607,7 +472,6 @@ pub fn url_for_host(item: &FetchItem, host_idx: usize) -> Option<&str> { ) } -/// Byte count an inclusive range must deliver; `None` for an inverted range. pub fn range_len(range: (u64, u64)) -> Option<u64> { let (a, b) = range; if b < a { @@ -617,8 +481,6 @@ pub fn range_len(range: (u64, u64)) -> Option<u64> { } } -/// Validates a received body's length: for a range request it must be exactly `b-a+1`; otherwise -/// it must match `Content-Length` when the server sent one (as the Steam client does). pub fn validate_body_len( body_len: u64, content_length: Option<u64>, @@ -653,13 +515,10 @@ pub fn validate_body_len( Ok(()) } -/// Whether an HTTP status is acceptable for this request shape (200 always; 206 only when a range -/// was asked for). pub fn status_accepted(status: u16, ranged: bool) -> bool { status == 200 || (ranged && status == 206) } -/// Classifies a rejected HTTP status the same way the Steam async client does. pub fn classify_status(status: u16) -> FetchFailKind { if status == 429 { FetchFailKind::RateLimited @@ -670,10 +529,6 @@ pub fn classify_status(status: u16) -> FetchFailKind { } } -/// The HTTP status carried by a `send_checked` rejection message, if that is what the error was. -/// A 4xx other than 429 is a CLIENT rejection (bad/expired signed URL, wrong host for this path, -/// 404): it says nothing about congestion, so the driver demotes the host but does not shrink the -/// shared window for it — one CDN that refuses a path must not throttle the healthy ones. pub fn rejected_status(message: &str) -> Option<u16> { let rest = message.strip_prefix("unexpected HTTP status (")?; let digits = rest.strip_suffix(')')?; @@ -684,8 +539,6 @@ pub fn is_client_rejection(message: &str) -> bool { matches!(rejected_status(message), Some(s) if (400..500).contains(&s) && s != 429) } -/// How many individual fetch failures a run logs verbatim before going quiet (the window and -/// throughput lines keep reporting the aggregate). pub const FETCH_ERROR_LOG_LIMIT: u32 = 12; fn record_first_error(slot: &Mutex<Option<String>>, err: String) { @@ -695,11 +548,7 @@ fn record_first_error(slot: &Mutex<Option<String>>, err: String) { } } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// HTTP client (one pooled reqwest client + per-request headers + optional Range + streaming) -// ───────────────────────────────────────────────────────────────────────────────────────────── -/// The pooled client plus the pre-validated per-request headers. struct Http { client: reqwest::Client, headers: Vec<(reqwest::header::HeaderName, reqwest::header::HeaderValue)>, @@ -732,8 +581,6 @@ fn build_http(opts: &FetchOptions, pool_max_idle_per_host: usize) -> Result<Http } impl Http { - /// `GET url` with the run's headers (set per request, so they override the client defaults — - /// a store's own `User-Agent` wins over the Steam one) and the optional `Range`. fn request(&self, url: &str, range: Option<(u64, u64)>) -> reqwest::RequestBuilder { let mut request = self.client.get(url); for (name, value) in &self.headers { @@ -756,7 +603,6 @@ fn classify_reqwest_error(err: &reqwest::Error) -> FetchFailKind { } } -/// Sends the request and checks the status; shared by both modes. async fn send_checked( http: &Http, url: &str, @@ -786,9 +632,6 @@ async fn send_checked( Ok(response) } -/// Whole-body mode: one attempt for one item (the driver owns retry/rotation). `timeout` bounds the -/// whole request (headers + body). The body is read piece by piece so `host_rx` sees every byte as -/// it arrives (the stall detector's signal); `ttfb` is set when the response headers arrive. async fn fetch_once( http: &Http, url: &str, @@ -835,10 +678,6 @@ async fn fetch_once( } } -/// Stream mode: one attempt for one item. Pieces are pushed to the item's pool worker as they -/// arrive (each one added to the in-flight budget until the pool consumes it); the future waits -/// while the budget is full, and stops early on cancel or when the sink rejected this attempt. -/// Returns the total body length after the last piece was queued. #[allow(clippy::too_many_arguments)] async fn fetch_stream( http: &Http, @@ -871,7 +710,6 @@ async fn fetch_stream( let content_length = response.content_length(); let mut offset = 0u64; loop { - // Budget gate: don't read ahead of the pool by more than the budget. while in_flight.load(Ordering::Relaxed) >= budget { if cancel.load(Ordering::Relaxed) { return Err(AsyncFetchError { @@ -931,29 +769,22 @@ async fn fetch_stream( Ok(offset) } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Driver plumbing -// ───────────────────────────────────────────────────────────────────────────────────────────── -/// `abandon` sentinel: no attempt of this item has been rejected by the sink. const NO_ATTEMPT: u32 = u32::MAX; enum ProcessMsg { - /// Whole-body mode: the complete raw body. Body { idx: usize, attempts: u32, server_idx: usize, body: Vec<u8>, }, - /// Stream mode: one in-order piece of attempt `attempts` of item `idx`. Piece { idx: usize, attempts: u32, offset: u64, data: Vec<u8>, }, - /// Stream mode: the body ended; the pool finalizes and reports a verdict. Finish { idx: usize, attempts: u32, @@ -975,7 +806,6 @@ struct Feedback { verdict: SinkVerdict, } -/// An item awaiting (re)dispatch: `attempts` already spent; not eligible before `not_before`. #[derive(Clone, Copy, Debug)] struct PendingItem { idx: usize, @@ -988,18 +818,14 @@ enum Fetched { Streamed(u64), } -/// The result the driver awaits for each dispatched fetch. struct FetchDone { idx: usize, attempts: u32, reserve: u64, server_idx: usize, elapsed: Duration, - /// Request sent → response headers, when the headers arrived. ttfb: Option<Duration>, - /// Key in the driver's registry of running requests. req_id: u64, - /// The request was cancelled because its host stalled (not a failure of the item). stalled: bool, res: Result<Fetched, AsyncFetchError>, } @@ -1061,11 +887,7 @@ async fn run_driver(ctx: DriverCtx<'_>) { let mut retry: VecDeque<PendingItem> = VecDeque::new(); let mut next_item = 0usize; let mut errors_logged: u32 = 0; - // Bodies / finishes handed to the pool whose verdict has not come back yet. let mut outstanding = 0usize; - // Stall detection: bytes received per server (bumped by the request futures as pieces - // arrive), when that count last moved, since when the server has had requests to answer, - // how many requests it is running, and each running request's abort switch. let host_rx: Vec<AtomicU64> = hosts.iter().map(|_| AtomicU64::new(0)).collect(); let mut last_rx: Vec<u64> = vec![0; hosts.len()]; let mut last_rx_at: Vec<Option<Instant>> = vec![None; hosts.len()]; @@ -1075,7 +897,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { let mut aborts: HashMap<u64, (usize, Arc<Notify>)> = HashMap::new(); let mut next_req_id: u64 = 0; let mut last_stall_check = Instant::now(); - // Declared AFTER `txs`/`http` so it is dropped BEFORE them (its futures borrow both). let mut inflight: FuturesUnordered<_> = FuturesUnordered::new(); 'outer: loop { @@ -1085,8 +906,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { break; } - // ── Sink verdicts: Retry = counts as a failed attempt (cool + demote host, back-off, - // rotate); Fatal was already recorded in the error slot by the pool. ── while let Ok(fb) = fb_rx.try_recv() { outstanding = outstanding.saturating_sub(1); match fb.verdict { @@ -1123,7 +942,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { log(&window.summary_line(label, inflight.len(), probe_now)); } - // ── Stalled hosts: requests that receive nothing while another host delivers. ── if probe_now.duration_since(last_stall_check) >= Duration::from_millis(STALL_CHECK_MS) { last_stall_check = probe_now; for s in 0..hosts.len() { @@ -1163,12 +981,10 @@ async fn run_driver(ctx: DriverCtx<'_>) { hosts[s], probe_now.duration_since(silent_since).as_millis() )); - // Do not fire again for the same silence while the cancelled requests wind down. watch_from[s] = probe_now; } } - // ── Dispatch up to the current window ── while inflight.len() < window.current { if cancel.load(Ordering::Relaxed) || error_slot.lock().expect("err slot poisoned").is_some() @@ -1177,7 +993,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { } let now = Instant::now(); - // A due retry first, else the next fresh item. let use_retry = retry.front().is_some_and(|p| p.not_before <= now); let (idx, attempts) = if use_retry { let p = *retry.front().expect("retry nonempty"); @@ -1185,23 +1000,19 @@ async fn run_driver(ctx: DriverCtx<'_>) { } else if next_item < items.len() { (next_item, 0u32) } else { - break; // nothing dispatchable now (retry not due, or all dispatched) + break; }; let item = &items[idx]; - // Byte-budget gate (hard memory bound): reserve at DISPATCH. Always admits when nothing - // is in flight, so an item bigger than the whole budget can't deadlock. Streams reserve - // nothing here — their pieces are budgeted as they arrive. let reserve = if stream { 0 } else { reserve_bytes(item) }; if !budget_admits(in_flight.load(Ordering::Relaxed), reserve, budget) { window.note_budget_stall(); - break; // wait for a completion to free budget + break; } - // Speed-ranked, per-host-capped host pick. let Some(server_idx) = sched.pick(now) else { window.note_host_stall(); - break; // every host at cap or cooling → wait for a completion + break; }; let permit = match sched.host_sem(server_idx).clone().try_acquire_owned() { Ok(permit) => permit, @@ -1218,7 +1029,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { let url = url.to_string(); let range = item.range; - // Commit: pop the work item, reserve budget, launch the fetch future. if use_retry { retry.pop_front(); } else { @@ -1231,7 +1041,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { let rx_counter = &host_rx[server_idx]; let started = Instant::now(); if running[server_idx] == 0 { - // The host had nothing to answer until now: its silence is measured from here. watch_from[server_idx] = started; } running[server_idx] += 1; @@ -1268,7 +1077,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { .map(Fetched::Body) } }; - // The stall detector's switch: dropping the fetch future closes its connection. match select(pin!(fetch), pin!(abort.notified())).await { Either::Left((res, _)) => (res, false), Either::Right(((), _)) => ( @@ -1280,7 +1088,7 @@ async fn run_driver(ctx: DriverCtx<'_>) { ), } }; - drop(permit); // RAII per-host permit release (also on future drop / cancel-abort) + drop(permit); FetchDone { idx, attempts: attempts + 1, @@ -1295,17 +1103,14 @@ async fn run_driver(ctx: DriverCtx<'_>) { }); } - // ── Nothing in flight? finished, or waiting on retry-not-due / host cooldown / the pool. ── if inflight.is_empty() { if next_item >= items.len() && retry.is_empty() && outstanding == 0 { - break; // every item fetched and processed + break; } tokio::time::sleep(Duration::from_millis(5)).await; continue; } - // ── Await one completion (drives ALL in-flight requests cooperatively), bounded so - // cancel / a pool Fatal / a Retry verdict are noticed promptly even mid-fetch. ── let done = match tokio::time::timeout(Duration::from_millis(DRIVER_POLL_MS), inflight.next()) .await { @@ -1317,9 +1122,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { aborts.remove(&done.req_id); running[done.server_idx] = running[done.server_idx].saturating_sub(1); if done.stalled { - // Not the item's fault: straight back into the queue for a healthy host, no window - // or host penalty beyond the rest the detector already gave the host. It still counts - // as an attempt, which keeps each stream attempt's pieces distinguishable. in_flight.fetch_sub(done.reserve, Ordering::Relaxed); retry.push_front(PendingItem { idx: done.idx, @@ -1340,8 +1142,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { meter.record(done.server_idx, raw_len); sched.on_success(done.server_idx, raw_len, done.elapsed); window.record_ok(done.elapsed.as_secs_f64() * 1000.0); - // Reconcile the dispatch reservation to the actual raw size; the pool subtracts the - // actual size on process-complete, netting this item's budget to zero. if raw_len >= done.reserve { in_flight.fetch_add(raw_len - done.reserve, Ordering::Relaxed); } else { @@ -1357,7 +1157,7 @@ async fn run_driver(ctx: DriverCtx<'_>) { }) .is_err() { - break; // process side gone + break; } } Ok(Fetched::Streamed(total_len)) => { @@ -1374,7 +1174,7 @@ async fn run_driver(ctx: DriverCtx<'_>) { }) .is_err() { - break; // process side gone + break; } } Err(err) => { @@ -1423,14 +1223,10 @@ window_shrink={} msg={}", } } - // Dropping `inflight` aborts any still-running requests (the cancel / error / final-failure - // teardown path); dropping the senders then closes the channels so the pool drains and exits. drop(inflight); drop(txs); } -/// Since when a server has been silent: its last byte, or when it was last given work to do if -/// that came later. fn silent_since(last_rx_at: Option<Instant>, watch_from: Instant) -> Instant { match last_rx_at { Some(at) if at > watch_from => at, @@ -1438,16 +1234,12 @@ fn silent_since(last_rx_at: Option<Instant>, watch_from: Instant) -> Instant { } } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Bandwidth meter (per-host bytes + 5-s samples for peak) -// ───────────────────────────────────────────────────────────────────────────────────────────── pub struct BandwidthMeter { start: Instant, total_bytes: AtomicU64, per_server_bytes: Vec<AtomicU64>, hosts: Vec<String>, - /// (bytes, instant) at the last sample; peak = max sample rate in bytes/s. last_sample: Mutex<(u64, Instant)>, peak_bps: Mutex<f64>, } @@ -1478,8 +1270,6 @@ impl BandwidthMeter { self.total_bytes.load(Ordering::Relaxed) } - /// Take a throughput sample since the previous one (≥ 1 s apart to be counted) and fold it - /// into the peak. pub fn sample(&self) { let now = Instant::now(); let total = self.total_bytes(); @@ -1530,9 +1320,6 @@ impl BandwidthMeter { } } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Scheduler + adaptive window — a copy of the Steam depot writer's (keyed on host strings). -// ───────────────────────────────────────────────────────────────────────────────────────────── #[derive(Clone, Debug, Default)] struct ServerHealth { @@ -1542,10 +1329,6 @@ struct ServerHealth { cooldown_until: Option<Instant>, } -/// Speed-ranked, per-host-capped host picker. Cold hosts are probed first, then selection is -/// epsilon-greedy over the measured EWMA so a demoted-but-recovered host returns. Per-host caps key -/// on the host STRING (one `tokio::sync::Semaphore` each) so two entries that share a host still -/// share one cap. struct FetchScheduler { health: Vec<ServerHealth>, server_host: Vec<usize>, @@ -1635,8 +1418,6 @@ impl FetchScheduler { h.cooldown_until = None; } - /// A stalled host: out of the rotation for `rest`, and its speed estimate cut so it comes - /// back behind the hosts that kept delivering. fn rest(&mut self, server_idx: usize, now: Instant, rest: Duration) { let h = &mut self.health[server_idx]; h.cooldown_until = Some(now + rest); @@ -1710,8 +1491,6 @@ impl WindowReason { } } -/// Adaptive in-flight window: slow-start doubles every healthy probe until the first real back-off, -/// then grows additively; holds on plateau; shrinks proportionally only on real errors. #[derive(Clone, Debug)] struct AdaptiveWindow { current: usize, @@ -1733,14 +1512,10 @@ struct AdaptiveWindow { last_budget_stalls: u32, last_host_stalls: u32, latency_ewma_ms: f64, - /// TTFB samples of the current probe interval. ttfb_sum_ms: f64, ttfb_samples: u32, - /// Lowest per-probe mean TTFB seen this run (0 = none yet): the no-queue baseline. ttfb_base_ms: f64, - /// Mean TTFB of the last probe that had enough samples. ttfb_last_ms: f64, - /// Consecutive probes with queueing delay. latency_streak: u32, stalled_hosts: u32, cooldown_until: Option<Instant>, @@ -1803,7 +1578,6 @@ impl AdaptiveWindow { self.stalled_hosts = self.stalled_hosts.saturating_add(1); } - /// The latency brake's shrink: proportional and gentle, never below the floor, at least −1. fn shrink_latency(&mut self, now: Instant) { let scaled = (self.current as f64 * LATENCY_SHRINK_FACTOR).floor() as usize; self.current = scaled.min(self.current.saturating_sub(1)).max(self.min); @@ -1884,7 +1658,6 @@ impl AdaptiveWindow { let before = self.current; let cooling = self.cooldown_until.is_some_and(|t| now < t); - // Queueing delay: this probe's mean TTFB against the run's baseline. let probe_ttfb = (self.ttfb_samples >= LATENCY_MIN_SAMPLES) .then(|| self.ttfb_sum_ms / self.ttfb_samples as f64); let mut queueing = false; @@ -1909,9 +1682,6 @@ impl AdaptiveWindow { } else if cooling { self.last_reason = WindowReason::HoldCooldown; } else if queueing { - // More requests only made each one wait longer. Hold while a real gain still comes - // with the delay; once the delay keeps rising without one, give some of the window - // back, gently, and let the cooldown show whether that helped. let gain = self.bps_ewma > self.best_bps * (1.0 + LATENCY_GAIN_EPS); if gain { self.best_bps = self.bps_ewma; @@ -2012,9 +1782,6 @@ stalled_hosts={}", } } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Tests -// ───────────────────────────────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { @@ -2154,8 +1921,6 @@ mod tests { const MIB: u64 = 1024 * 1024; - /// Drive the window through probes 2.1 s apart, each with `samples` TTFB values and the - /// bytes that arrived in it; returns (reason, window) after every probe. fn run_probes(w: &mut AdaptiveWindow, t0: Instant, probes: &[(f64, u64)]) -> Vec<(WindowReason, usize)> { let mut total = 0u64; let mut out = Vec::new(); @@ -2174,7 +1939,6 @@ mod tests { fn rising_delay_without_more_throughput_holds_then_shrinks_gently() { let t0 = Instant::now(); let mut w = AdaptiveWindow::new(8, 2, 256, t0); - // Flat ~15 MB/s while the time to first byte climbs from 0.4 s to 12 s (the 5G log). let flat = 32 * MIB; let probes: Vec<(f64, u64)> = [400.0, 400.0, 450.0, 900.0, 1500.0, 2500.0, 4000.0, 6000.0, 9000.0, 12000.0] .iter() @@ -2186,7 +1950,6 @@ mod tests { assert!(first_hold < first_shrink, "{seen:?}"); let before = seen[first_shrink - 1].1; assert_eq!(seen[first_shrink].1, (before as f64 * LATENCY_SHRINK_FACTOR).floor() as usize, "{seen:?}"); - // From the first sign of queueing on, the window never grows again. for pair in seen[first_hold..].windows(2) { assert!(pair[1].1 <= pair[0].1, "grew under queueing delay: {seen:?}"); } @@ -2199,7 +1962,6 @@ mod tests { fn a_fast_link_keeps_growing_while_its_delay_stays_low() { let t0 = Instant::now(); let mut w = AdaptiveWindow::new(8, 2, 256, t0); - // Throughput doubles with the window; TTFB wobbles between 25 and 70 ms (new connections). let probes: Vec<(f64, u64)> = (0..6) .map(|k| ([25.0, 70.0, 40.0, 60.0, 30.0, 65.0][k], (8 * MIB) << k)) .collect(); @@ -2211,8 +1973,6 @@ mod tests { assert!(w.current >= 128, "{seen:?}"); } - /// Two-behaviour stub for the stall test: `freeze` answers the headers and 100 bytes, then - /// never sends another byte; otherwise every path gets 10 000 bytes in ten paced slices. fn start_paced_stub(freeze: bool) -> String { let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); let port = listener.local_addr().expect("addr").port(); @@ -2296,12 +2056,10 @@ mod tests { .unwrap_or_else(|| panic!("no host-stall line: {lines:?}")); assert!(stall.contains(&format!(" host={frozen} ")), "{stall}"); assert!(!stall.contains(" requeued=0 "), "{stall}"); - // Well inside the 10 s request timeout that would otherwise have freed the items. assert!(elapsed < Duration::from_secs(8), "took {elapsed:?}: {lines:?}"); assert!(!lines.iter().any(|l| l.contains(&format!("host={healthy}")) && l.starts_with("host-stall"))); } - // ── Minimal HTTP/1.1 stub: one thread per connection, `Connection: close`, Range-aware. ── struct Stub { base: String, hits: Arc<Mutex<HashMap<String, usize>>>, @@ -2444,11 +2202,9 @@ mod tests { } } - /// Stream-mode sink: appends pieces per item (offset 0 restarts), finalizes by length check. struct StreamSink { got: Mutex<HashMap<u64, Vec<u8>>>, finished: Mutex<HashMap<u64, u64>>, - /// Reject the first piece of this item once (exercises the mid-stream Retry path). retry_once_id: Option<u64>, retried: AtomicUsize, } @@ -2534,8 +2290,6 @@ mod tests { assert_eq!(got[&5], b"nile/0.1 Amazon".to_vec()); let finished = sink.finished.lock().unwrap(); assert_eq!(finished.len(), 5); - // Pieces credited as written + 7 extra per on_finish; the rejected attempt of /big - // contributed nothing (its first piece was refused before any write). let written: u64 = got.values().map(|b| b.len() as u64).sum(); assert_eq!(outcome.bytes_credited, written + 5 * 7); let hits = stub.hits.lock().unwrap(); @@ -2575,7 +2329,6 @@ mod tests { &sink, &cancel, &|bytes: u64, ok: u64| { - // Pool threads race on the callback order; keep the high-water mark. let mut cur = last_progress.lock().unwrap(); if bytes >= cur.0 { *cur = (bytes, ok); @@ -2595,7 +2348,6 @@ mod tests { let expected_credit: u64 = got.values().map(|b| b.len() as u64 * 2).sum(); assert_eq!(outcome.bytes_credited, expected_credit); assert_eq!(*last_progress.lock().unwrap(), (expected_credit, 4)); - // /c was served 503 once, then 200; /a was re-fetched once after the sink's Retry. let hits = stub.hits.lock().unwrap(); assert_eq!(hits["/c"], 2); assert_eq!(hits["/a"], 2); diff --git a/app/src/main/rust/stores/src/fetch_tuning.rs b/app/src/main/rust/stores/src/fetch_tuning.rs index abbe2d492..4f1f7074f 100644 --- a/app/src/main/rust/stores/src/fetch_tuning.rs +++ b/app/src/main/rust/stores/src/fetch_tuning.rs @@ -1,32 +1,12 @@ -//! The numbers and the two small types [`fetch_core`](crate::fetch_core) shares with the Steam -//! depot writer it was carved out of. -//! -//! In Bannerlator these live in `depot_writer.rs` and `cdn_client.rs`, next to the Steam CM and -//! depot code, so that the Steam engine and the store engines run on ONE set of tunables and their -//! `fetch-window` log lines stay A/B-comparable. DroidDeck ships no Steam depot engine (the Steam -//! client runs natively in the Linux runtime), so only this slice came across: the values are -//! copied verbatim and keep their names, which is what lets a GOG/Epic/Amazon log from either app -//! be read against the same tuning notes. -//! -//! The important behaviours are structural (slow-start ramp -> hold at plateau -> back off on REAL -//! errors, per-host cap, hard byte budget), not the exact numbers. The r3/r4 notes below record -//! why each number is what it is, because every one of them was arrived at by a device run that -//! went wrong the other way. use std::time::Duration; -/// Default `User-Agent` of the pooled client. Every store engine overrides it per request with -/// the agent its own launcher sends (`FetchOptions::headers`), so this value is only what a -/// request without an explicit header would carry. pub const USER_AGENT: &str = "Valve/Steam HTTP Client 1.0"; -/// TCP connect deadline for every request the pooled client makes. pub fn connect_timeout() -> Duration { Duration::from_secs(15) } -/// Classified fetch failure so the fetch driver can back off proportionally (429/5xx/timeout/reset -/// shrink the window + cool the host harder than a one-off protocol error). #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum FetchFailKind { Timeout, @@ -42,53 +22,17 @@ pub struct AsyncFetchError { pub kind: FetchFailKind, } -/// Attempts per item before the run gives up on it (fetch failures and sink retries both count). pub const MAX_CHUNK_ATTEMPTS: u32 = 5; -/// Floor for the in-flight byte budget — the pre-r4 fixed value, so no run ever gets *less* -/// buffered memory than it had before the budget started scaling. See [`inflight_budget_bytes`]. pub const FETCH_INFLIGHT_BUDGET_FLOOR_BYTES: u64 = 24 * 1024 * 1024; -/// Hard ceiling for the in-flight byte budget. This is THE OOM guard: whatever the tier, the host -/// count or the chunk size, the RAW bytes in memory for one run can never exceed this. 96 MiB = 4× -/// the old fixed budget and still a small fraction of the heap an Android game installer runs in, -/// and it is only ever reached by a fast-tier run whose CDN pool is large enough to support a 64+ -/// window. Raising this is the one change in the fetch layer that can reintroduce OOM, so it stays -/// fixed and small. pub const FETCH_INFLIGHT_BUDGET_HARD_CAP_BYTES: u64 = 96 * 1024 * 1024; -/// Assumed RAW (on-the-wire, still-compressed) size of one chunk when sizing the budget. Store -/// chunks are ~1 MiB; this is the same nominal [`NOMINAL_CHUNK_RESERVE_BYTES`] uses at dispatch. pub const TYPICAL_CHUNK_BYTES: u64 = NOMINAL_CHUNK_RESERVE_BYTES; -/// Headroom the budget carries over "one chunk per window slot" (3/2 = 1.5×), as a rational so the -/// arithmetic stays integer. The window is a count of concurrent *requests*; a slot's bytes are held -/// from DISPATCH until its WRITE completes, so at any instant some slots hold a fetched-but-not-yet- -/// written chunk while others hold a reservation for a fetch still on the wire. 1.5× covers that -/// overlap (and the occasional above-nominal chunk) without doubling the memory bound. pub const BUDGET_HEADROOM_NUM: u64 = 3; pub const BUDGET_HEADROOM_DEN: u64 = 2; -/// Byte budget for the *compressed* bytes buffered in the fetch→process channel, scaled to the -/// window ceiling this run can actually reach. -/// -/// The decouple channel is bounded by BYTES, not chunk count: a count cap makes in-flight memory -/// swing wildly with chunk size (a 4-chunk cap is ~4 MB for 1 MB chunks but ~120 MB for 30 MB -/// chunks). Budgeting bytes keeps the heap bounded regardless of game size while giving the fetch -/// pool bandwidth-delay-product headroom so the socket stays busy while process workers -/// decompress+write. -/// -/// r4: a device run proved a fixed 24 MiB budget becomes the binding constraint the moment the -/// adaptive window works — on a 1 Gbps line the window grew to 42 but the budget admitted only ~24 -/// chunks, so `in_flight` stalled at 23–28 with `budget_stalls` in the hundreds and -/// `host_stalls=0`. A budget that refuses slots the window has already decided are safe is not a -/// memory guard, it is a throttle. So the budget scales WITH the effective ceiling (tier ∧ -/// hosts×per-host-cap ∧ hard cap) and is clamped between the old fixed value and a hard cap. Low -/// tiers are unaffected; a fast link gets a budget that stops binding before the window does. -/// -/// Budget only the RAW bytes actually queued (reserved at DISPATCH, released on WRITE-COMPLETE); the -/// decoded expansion is transient inside a process worker and never accumulates. See -/// [`budget_admits`] for the single-chunk deadlock guard. pub fn inflight_budget_bytes(effective_ceiling: usize) -> u64 { (effective_ceiling as u64) .saturating_mul(TYPICAL_CHUNK_BYTES) @@ -100,71 +44,30 @@ pub fn inflight_budget_bytes(effective_ceiling: usize) -> u64 { ) } -// ─── Adaptive-window tuning ──────────────────────────────────────────────────────────────────── -// r3 retune: the first device run pinned the window at the floor on a 1 Gbps link. The controller -// shrank on throughput dips and latency jitter (normal CDN noise) as well as on errors, shrank a flat -// −6 on every single error, and only grew when throughput rose >10% in a 700 ms sample. The shrinks -// dominated, so the window ratcheted to the floor and stayed there. r3 makes growth eager and makes -// ERRORS the only shrink signal. -/// Max concurrent requests to any single CDN host. CDNs throttle/reset per host, so a big window -/// is reached by spreading across MORE hosts, never by piling onto one. pub const PER_HOST_CAP: usize = 6; -/// Initial in-flight window. Slow-start doubles from here, so this only sets how fast the first -/// couple of probes get useful — the ceiling still comes from the tier × hosts clamp. pub const BOOTSTRAP_WINDOW: usize = 8; -/// Hard safety ceiling on the window regardless of tier (also clamped to distinct-hosts × per-host). pub const WINDOW_HARD_CAP: usize = 256; -/// The window never shrinks below this (keeps a little pipelining even on a rough link). pub const WINDOW_MIN_FLOOR: usize = 2; -/// Additive growth step once slow-start has ended (after the first real back-off). pub const WINDOW_STEP_UP: usize = 4; -/// Slow-start multiplier: until the first back-off the window DOUBLES each healthy probe, so a fast -/// link reaches a useful window in seconds instead of never. pub const WINDOW_SLOW_START_FACTOR: usize = 2; -/// Proportional shrink on a real error signal (never a flat step: a flat −6 slammed a small window -/// straight to the floor). Always at least −1 while above the floor. pub const WINDOW_SHRINK_FACTOR: f64 = 0.75; -/// How often the adaptive window re-evaluates. Long enough that one jittery sample cannot drive a -/// decision; decisions use the EWMA, not the raw sample. pub const WINDOW_PROBE_INTERVAL_MS: u64 = 2_000; -/// After a back-off, hold the window (no growth) for this long — hysteresis against oscillation. pub const WINDOW_COOLDOWN_MS: u64 = 3_000; -/// EWMA smoothing for the measured throughput samples (higher = more responsive). pub const WINDOW_BPS_EWMA_ALPHA: f64 = 0.4; -/// Throughput above `best × (1 + this)` counts as a genuine improvement (keeps slow-start climbing). pub const WINDOW_IMPROVE_EPS: f64 = 0.05; -/// Throughput below `best × (1 − this)` counts as falling — growth STOPS (hold). It never shrinks: -/// only real errors shrink. pub const WINDOW_DECLINE_EPS: f64 = 0.15; -/// Error-rate thresholds over one probe window: above HIGH shrinks; growth needs at/below LOW -/// (in between = hold). pub const WINDOW_ERR_RATE_HIGH: f64 = 0.10; pub const WINDOW_ERR_RATE_LOW: f64 = 0.05; -/// Errors inside ONE probe interval that trigger an immediate (don't-wait-for-the-probe) shrink. A -/// single stray timeout no longer moves the window; a burst does. A 429 always shrinks immediately. pub const WINDOW_ERR_BURST_IMMEDIATE: u32 = 3; -/// Consecutive non-improving probes still allowed to grow before declaring a plateau — throughput -/// lags a window change by a probe or two, so one flat sample must not stop the ramp. pub const WINDOW_PLATEAU_PATIENCE: u32 = 2; -/// Once plateaued, re-arm the ramp this long later (doubling, capped) to re-probe for headroom if the -/// link improved. Bounded re-probing, not a creep: a weak link answers with errors and shrinks back. pub const WINDOW_PLATEAU_REARM_MS: u64 = 30_000; pub const WINDOW_PLATEAU_REARM_MAX_MS: u64 = 300_000; -/// Even when the window does not change, emit one `fetch-window` line every N probes so a stuck -/// window explains itself in the log (`reason=` says why it is not moving). pub const WINDOW_LOG_EVERY_PROBES: u32 = 5; -/// Every Nth exploit dispatch is instead an exploration pick (epsilon-greedy ≈ 1/N) so a -/// demoted-but-recovered host is retried rather than starved forever. pub const SERVER_EXPLORE_EVERY: u64 = 8; -/// Reservation for an item whose compressed size the manifest didn't carry (keeps the byte budget -/// honest at dispatch time). pub const NOMINAL_CHUNK_RESERVE_BYTES: u64 = 1024 * 1024; -/// Extra host cooldown floor when a host answers 429 (rate-limited) — back off harder than a -/// one-off timeout. pub const RATE_LIMIT_COOLDOWN_MS: u64 = 5_000; -/// Exponential back-off between attempts of one item: 0, 300, 600, 1200, 2400 ms … capped at 4 s. pub fn retry_backoff_millis(attempt: u32) -> u64 { if attempt == 0 { return 0; @@ -172,9 +75,6 @@ pub fn retry_backoff_millis(attempt: u32) -> u64 { (300u64 << (attempt - 1)).min(4000) } -/// Byte-budget admission test for the fetch→process channel. Always admits when nothing is in -/// flight (`in_flight == 0`) so a single item LARGER than the whole budget can never deadlock — it -/// simply waits until the channel drains, then goes through alone. #[inline] pub fn budget_admits(in_flight: u64, raw_len: u64, budget: u64) -> bool { in_flight == 0 || in_flight.saturating_add(raw_len) <= budget diff --git a/app/src/main/rust/stores/src/gog.rs b/app/src/main/rust/stores/src/gog.rs index 465d7346f..3aed66074 100644 --- a/app/src/main/rust/stores/src/gog.rs +++ b/app/src/main/rust/stores/src/gog.rs @@ -1,21 +1,3 @@ -//! GOG gen2 chunk download engine — the Rust replacement for the byte-fetching inner loop of -//! `GogDownloadManager.java` (`assembleDepotFile` / `fetchChunkVerified` driven from the -//! per-file thread pool). Everything around that loop — token/builds/manifest head, base-product -//! + language depot selection, secure-link resolution, disk guard, `_gog_manifest.json` marker, -//! prefs, exe resolution, DLC markers, redist orchestration — stays in Java and is untouched. -//! -//! Parity contract: Bannerlator's `docs/RUST_GOG_PARITY.md`. The adapter is fed the SAME inflated depot-manifest -//! JSON strings Java already parsed, the SAME resolved CDN base (secure link) and the SAME -//! install dir, and produces the SAME on-disk layout (`<file>.bhtmp` staging, atomic rename, -//! size+MD5-verified finals) so a download can be resumed by either engine. -//! -//! Layout: -//! - [`plan`] — depot-manifest JSON → file/chunk plan (mirrors `parseDepotManifest`), CDN path -//! and chunk-URL builders, largest-first ordering. -//! - [`engine`] — resume/skip pass (mirrors `fileVerified`), `FetchItem` list, the `FetchSink` -//! (compressed size/MD5 → inflate → decompressed size/MD5 → positioned write → -//! whole-file size/MD5 → rename), summary logging. -//! - [`jni`] — `GogNative` JNI exports (`nativeStart` / `nativeCancel` / `nativeRelease`). pub mod engine; pub mod jni; @@ -25,7 +7,6 @@ use std::fs::File; use std::io::Read; use std::path::Path; -/// Lowercase hex of `bytes` (matches `GogDownloadManager.toHex`). pub fn to_hex(bytes: &[u8]) -> String { const DIGITS: &[u8; 16] = b"0123456789abcdef"; let mut out = String::with_capacity(bytes.len() * 2); @@ -36,15 +17,12 @@ pub fn to_hex(bytes: &[u8]) -> String { out } -/// Lowercase hex MD5 of an in-memory buffer (`GogDownloadManager.md5Hex`). pub fn md5_hex(data: &[u8]) -> String { let mut hasher = crate::md5_small::Md5::new(); hasher.update(data); to_hex(&hasher.finalize()) } -/// Streaming lowercase hex MD5 of a file, never buffering the whole file -/// (`GogDownloadManager.md5HexFile`). `None` when the file cannot be read. pub fn md5_hex_file(path: &Path) -> Option<String> { let mut file = File::open(path).ok()?; let mut hasher = crate::md5_small::Md5::new(); @@ -59,11 +37,6 @@ pub fn md5_hex_file(path: &Path) -> Option<String> { Some(to_hex(&hasher.finalize())) } -/// zlib-inflate a chunk body (`GogDownloadManager.inflateZlib`): `None` when the body is not a -/// zlib stream (first byte != 0x78) or fails to inflate — the caller then treats the body as a -/// stored (non-compressed) chunk, exactly like Java's `if (inflated == null) inflated = raw`. -/// A truncated stream yields the partial output (Java's loop breaks on `n == 0` the same way); -/// the decompressed-size check that follows rejects it. pub fn inflate_zlib(data: &[u8]) -> Option<Vec<u8>> { if data.len() < 2 || data[0] != 0x78 { return None; @@ -96,16 +69,12 @@ mod tests { let compressed = enc.finish().unwrap(); assert_eq!(compressed[0], 0x78); assert_eq!(inflate_zlib(&compressed).unwrap(), payload); - // Not zlib → None (stored chunk path). assert!(inflate_zlib(b"\x1f\x8bnot zlib").is_none()); assert!(inflate_zlib(b"\x78").is_none()); - // Truncated zlib → partial output, like Java's `while (!inf.finished()) { if (n == 0) break; }` - // loop; the decompressed-size check downstream rejects it either way. let mut truncated = compressed.clone(); let mid = truncated.len() / 2; truncated.truncate(mid); assert!(inflate_zlib(&truncated).map(|out| out != payload).unwrap_or(true)); - // Corrupt zlib body → None (Java: DataFormatException → null → raw fallback). let mut corrupt = compressed.clone(); for b in corrupt.iter_mut().skip(2) { *b = !*b; diff --git a/app/src/main/rust/stores/src/gog/engine.rs b/app/src/main/rust/stores/src/gog/engine.rs index 0233110c3..9f2f59428 100644 --- a/app/src/main/rust/stores/src/gog/engine.rs +++ b/app/src/main/rust/stores/src/gog/engine.rs @@ -1,17 +1,3 @@ -//! The GOG gen2 fetch loop on top of the shared fetch core. One call = one Java pool run -//! (`runGen2` base loop, `doInstallDlc` loop, or one `assembleDependencyInstaller`). -//! -//! What this mirrors, and where (see Bannerlator's `docs/RUST_GOG_PARITY.md`): -//! - resume/skip: [`file_verified`] = `GogDownloadManager.fileVerified` (exists, non-empty, size -//! when known, MD5 when known) — a verified file is reported as "Verified…" progress with NO -//! bytes credited, exactly like the Java task; -//! - per chunk: compressed size → compressed MD5 → inflate (stored fallback) → decompressed size → -//! decompressed MD5 (`fetchChunkVerified`); a mismatch is a retryable failure (Java: hard fail -//! ≤3 with backoff; the core: ≤5 attempts with backoff); -//! - per file: write into `<file>.bhtmp` (positioned, so chunks may land out of order), then -//! whole-file size + MD5, delete any existing final, rename (`assembleDepotFile`); -//! - failure of any file aborts the run (`anyFailed`), cancel aborts the run; in both cases every -//! unfinished `.bhtmp` is deleted (Java deletes its tmp on the failing/cancelled thread). use std::collections::HashSet; use std::fs::{self, File, OpenOptions}; @@ -30,35 +16,25 @@ use super::plan::{ }; use super::{inflate_zlib, md5_hex, md5_hex_file}; -/// Per-request timeout on chunk fetches (Java: `TIMEOUT = 30_000` connect + read). pub const CHUNK_TIMEOUT: Duration = Duration::from_secs(30); -/// Java sends this on every content-system / CDN request (`fetchBytesEx`). pub const USER_AGENT: &str = "GOG Galaxy"; -/// Java staging suffix (`assembleDepotFile`). pub const TMP_SUFFIX: &str = ".bhtmp"; -/// Floor for the per-host cap (the core's default); a single-host store gets the whole ceiling. pub const PER_HOST_CAP_FLOOR: usize = 6; -/// `max(6, ceil(max_workers / distinct_hosts))` — one CDN host gets the whole ceiling, several -/// share it. pub fn per_host_cap_for(max_workers: usize, distinct_hosts: usize) -> usize { let hosts = distinct_hosts.max(1); let workers = max_workers.max(1); PER_HOST_CAP_FLOOR.max(workers.div_ceil(hosts)) } -/// Which Java loop this run replaces. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum PlanKind { - /// gen2: depot manifests → chunks → `assembleDepotFile` (base install, DLC, dependencies). #[default] Gen2Chunks, - /// gen1: build manifest → per-file HTTP Range GET streamed to disk (`runGen1`). Gen1Ranges, } impl PlanKind { - /// JNI mapping (`GogNative.KIND_*`): 0 = gen2 chunks, 1 = gen1 ranges, anything else → gen2. pub fn from_i32(v: i32) -> Self { if v == 1 { PlanKind::Gen1Ranges @@ -68,39 +44,21 @@ impl PlanKind { } } -/// Inputs — exactly what the Java manager has in hand after manifest + secure-link resolution. #[derive(Clone, Debug, Default)] pub struct GogRequest { pub kind: PlanKind, - /// gen2: inflated depot-manifest JSON strings, in the order Java fetched them (already filtered - /// by base-product / DLC-product and language in Java). gen1: the inflated build manifest. pub depot_manifests: Vec<String>, - /// gen2: the resolved CDN bases from the secure-link answer (every `urls[]` entry, query - /// strings kept, in the order GOG lists them), or the one unauthenticated dependency store - /// base. Several bases on distinct hosts let the fetch core spread the window across CDNs - /// with its per-host cap; a base on a host already listed is dropped (the first one wins). - /// gen1: unused (file URLs live in the manifest). pub cdn_bases: Vec<String>, pub install_dir: String, - /// Files already completed by an earlier run of this same download (secure-link refresh - /// re-run): counted as done WITHOUT re-hashing and WITHOUT a progress event. pub skip_paths: Vec<String>, pub ca_bundle_path: String, - /// Java pool size: `resolveDownloadThreads` (base), 8 (DLC), 1 (dependency). pub max_workers: usize, - /// Inflate/hash/write threads; Java did that on the same pool threads, so pass the same count. pub process_workers: usize, - /// Base install: largest-first (LPT) ordering; DLC / dependency: manifest order. pub sort_largest_first: bool, - /// Log prefix, e.g. `gog base=1207658930`. pub label: String, } -/// Callbacks out of the engine (JNI listener or a test recorder). Must be callable from the -/// process-pool threads. pub trait GogEvents: Sync { - /// One file reached its final state. `verified` = resume-skip (no bytes credited); otherwise a - /// freshly assembled + verified file (`file_bytes` credited). `files_done` counts both. #[allow(clippy::too_many_arguments)] fn on_file_done( &self, @@ -113,9 +71,6 @@ pub trait GogEvents: Sync { bytes_total: u64, ); fn on_log(&self, line: &str); - /// Periodic byte progress while files are in flight (~4/s, only when the value moved): - /// assembled bytes plus the bytes of files not yet finished. Same `bytes_total` as - /// `on_file_done`. Default: ignored (test recorders). fn on_bytes(&self, _bytes_done: u64, _bytes_total: u64) {} } @@ -123,20 +78,14 @@ pub trait GogEvents: Sync { pub struct GogRunResult { pub success: bool, pub cancelled: bool, - /// The run died on an HTTP code Java treats as an expired secure link (401/403/404/500) — - /// the Java caller refreshes the link (cap 5) and re-runs with `skip_paths` filled in. pub link_expiry: bool, pub error: String, - /// Decompressed bytes of files assembled by THIS run (Java `totalBytes`). pub bytes_written: u64, - /// Files done = verified-skipped + assembled (Java `doneCount`), excluding `skip_paths`. pub files_done: u32, pub files_verified: u32, pub files_total: u32, } -/// `GogDownloadManager.fileVerified`: true only if the file exists, is non-empty, matches the -/// expected size when known and the expected MD5 when known. pub fn file_verified(path: &Path, expected_size: u64, expected_md5: &str) -> bool { let Ok(meta) = fs::metadata(path) else { return false; @@ -156,8 +105,6 @@ pub fn file_verified(path: &Path, expected_size: u64, expected_md5: &str) -> boo true } -/// Finds an HTTP status in a fetch-core error string (`non-200 HTTP status (403)`, `status 403`, -/// `HTTP 403`, `http=403`, `code=403`). The link-expiry set is Java's `fetchChunkVerified` list. pub fn http_status_in(message: &str) -> Option<u16> { let lower = message.to_ascii_lowercase(); for marker in ["status (", "status ", "http ", "http=", "code=", "code "] { @@ -178,7 +125,6 @@ pub fn http_status_in(message: &str) -> Option<u16> { None } -/// Java: `code == 401 || code == 403 || code == 404 || code == 500` → refresh the secure link. pub fn is_link_expiry_status(code: u16) -> bool { matches!(code, 401 | 403 | 404 | 500) } @@ -186,8 +132,6 @@ pub fn is_link_expiry_status(code: u16) -> bool { struct FileState { handle: Option<Arc<File>>, chunks_done: usize, - /// Decompressed bytes of this file's finished chunks, credited to the sink's `partial` - /// counter until the file itself is finalized and moves into `bytes_done`. partial: u64, finalized: bool, failed: bool, @@ -240,11 +184,6 @@ impl SpeedSampler { } } -/// Positioned writer for ONE chunk's decompressed byte range inside the file's `.bhtmp`: hashes -/// everything it is given (the decompressed MD5) and counts it, but only writes bytes inside the -/// chunk's expected range when the size is known — an inflate overshoot then fails the size -/// check instead of clobbering the neighbouring chunk. An I/O error is remembered so the sink can -/// tell "disk failed" (fatal) from "stream failed" (retry). struct RangeWriter { file: Arc<File>, base: u64, @@ -294,13 +233,10 @@ enum ChunkMode { Stored(RangeWriter), } -/// Per-item streaming state for one attempt (reset whenever a piece arrives at `offset == 0`). struct ChunkStream { comp_md5: crate::md5_small::Md5, comp_len: u64, - /// Decided on the first non-empty piece (`0x78` → zlib, else stored — Java's `inflateZlib`). mode: Option<ChunkMode>, - /// The writer until the mode is decided (an empty body never decides). writer: Option<RangeWriter>, } @@ -315,17 +251,10 @@ impl ChunkStream { } } -/// The `FetchSink`. Stream mode (what `run_gen2` uses): pieces of a chunk are inflated as they -/// arrive straight into the chunk's byte range of the file, the compressed MD5 and the -/// decompressed size/MD5 accumulate on the fly, and `on_finish` runs the checks in Java's order -/// (compressed size → compressed MD5 → inflate → size → MD5). Body mode (`process`) is the same -/// pipeline on a whole buffer and stays available for tests / a body-mode fallback. struct GogSink<'a> { files: &'a [PlannedFile], - /// item id → (file index, chunk index) table: Vec<(usize, usize)>, states: Vec<Mutex<FileState>>, - /// item id → in-flight stream attempt (stream mode only) streams: Vec<Mutex<Option<ChunkStream>>>, files_total: u32, bytes_total: u64, @@ -333,8 +262,6 @@ struct GogSink<'a> { bytes_done: &'a AtomicU64, events: &'a dyn GogEvents, speed: Mutex<SpeedSampler>, - /// Bytes of files still in flight (finished chunks; for gen1, the current attempt's pieces). - /// `bytes_done + partial` is what the byte ticker reports between file completions. partial: AtomicU64, } @@ -343,8 +270,6 @@ impl<'a> GogSink<'a> { self.events.on_log(line); } - /// `assembleDepotFile` head: `parent.mkdirs(); tmpFile.delete(); new FileOutputStream(tmp)` — - /// once per file, on its first chunk. Returns a shared handle for positioned writes. fn file_handle(&self, file_idx: usize) -> Result<Arc<File>, SinkError> { let file = &self.files[file_idx]; let mut st = self @@ -385,9 +310,6 @@ impl<'a> GogSink<'a> { } } - /// One verified chunk landed: count it; on the file's last chunk run the whole-file verify + - /// rename and fire the per-file progress event (Java: doneCount++, totalBytes += df.totalSize, - /// "Downloading: <name> <speed>"). fn chunk_done(&self, file_idx: usize, chunk_bytes: u64) -> Result<(), SinkError> { let file = &self.files[file_idx]; let mut st = self @@ -431,7 +353,6 @@ impl<'a> GogSink<'a> { Ok(()) } - /// `assembleDepotFile` tail: whole-file size + MD5 on the tmp, delete existing final, rename. fn finalize_file(&self, file_idx: usize, st: &mut FileState) -> Result<(), String> { let file = &self.files[file_idx]; if let Some(handle) = st.handle.take() { @@ -469,7 +390,6 @@ impl<'a> GogSink<'a> { Ok(()) } - /// Delete every unfinished `.bhtmp` (Java: `tmpFile.delete()` on the failing/cancelled task). fn cleanup_partials(&self) { for st in &self.states { let Ok(mut st) = st.lock() else { @@ -496,8 +416,6 @@ impl<'a> GogSink<'a> { } impl<'a> FetchSink for GogSink<'a> { - /// Body mode: `fetchChunkVerified` on a whole buffer, then a positioned write of the inflated - /// bytes (used by tests and available as a non-stream fallback). fn process(&self, item: &FetchItem, body: Vec<u8>) -> Result<u64, SinkError> { let Some(&(file_idx, chunk_idx)) = self.table.get(item.id as usize) else { return Err(SinkError::Fatal(format!("unknown item id {}", item.id))); @@ -508,7 +426,6 @@ impl<'a> FetchSink for GogSink<'a> { speed.record(body.len() as u64); } - // fetchChunkVerified, in order: compressed size → compressed MD5 → inflate → size → MD5. if chunk.compressed_size > 0 && body.len() as u64 != chunk.compressed_size { let msg = format!( "chunk compressed-size mismatch chunk={} exp={} got={}", @@ -528,7 +445,7 @@ impl<'a> FetchSink for GogSink<'a> { } let inflated = match inflate_zlib(&body) { Some(out) => out, - None => body, // stored (non-zlib) chunk + None => body, }; if chunk.size > 0 && inflated.len() as u64 != chunk.size { let msg = format!( @@ -563,8 +480,6 @@ impl<'a> FetchSink for GogSink<'a> { Ok(credited) } - /// Stream mode: hash the compressed piece, inflate it straight into the chunk's byte range - /// (or pass it through for a stored chunk), hashing + counting the decompressed output. fn on_chunk(&self, item: &FetchItem, offset: u64, data: &[u8]) -> Result<(), SinkError> { let id = item.id as usize; let Some(&(file_idx, chunk_idx)) = self.table.get(id) else { @@ -581,7 +496,6 @@ impl<'a> FetchSink for GogSink<'a> { .and_then(|m| m.lock().ok()) .ok_or_else(|| SinkError::Fatal("chunk stream poisoned".to_string()))?; if offset == 0 || slot.is_none() { - // (Re)start of an attempt: fresh hashers + inflater; positioned writes re-cover the range. let handle = self.file_handle(file_idx)?; *slot = Some(ChunkStream::new(handle, chunk.offset, chunk.size)); } @@ -601,7 +515,7 @@ impl<'a> FetchSink for GogSink<'a> { stream.mode = Some(if data[0] == 0x78 { ChunkMode::Zlib(flate2::write::ZlibDecoder::new(writer)) } else { - ChunkMode::Stored(writer) // stored (non-zlib) chunk — Java's `inflated = raw` + ChunkMode::Stored(writer) }); } let result = match stream.mode.as_mut() { @@ -616,7 +530,6 @@ impl<'a> FetchSink for GogSink<'a> { match result { Ok(()) => Ok(()), Err((_, Some(io))) => { - // Disk, not stream: Java's `fos.write` exception → file fails → run aborts. if let Some(mut st) = self.states.get(file_idx).and_then(|m| m.lock().ok()) { st.failed = true; let _ = st.handle.take(); @@ -627,7 +540,6 @@ impl<'a> FetchSink for GogSink<'a> { Err(SinkError::Fatal(msg)) } Err((err, None)) => { - // Corrupt zlib body. Java: inflate → null → raw → decompressed-size mismatch → retry. let msg = format!("chunk inflate failed chunk={} err={err}", chunk.hash); self.log(&msg); *slot = None; @@ -636,7 +548,6 @@ impl<'a> FetchSink for GogSink<'a> { } } - /// Stream mode: the body ended — run Java's checks in order and count the chunk. fn on_finish(&self, item: &FetchItem, _total_len: u64) -> Result<u64, SinkError> { let id = item.id as usize; let Some(&(file_idx, chunk_idx)) = self.table.get(id) else { @@ -652,7 +563,6 @@ impl<'a> FetchSink for GogSink<'a> { .take(); let stream = match taken { Some(s) => s, - // Empty body with no pieces: same checks on zero bytes. None => ChunkStream::new(self.file_handle(file_idx)?, chunk.offset, chunk.size), }; @@ -717,11 +627,8 @@ impl<'a> FetchSink for GogSink<'a> { } } -/// How often the byte ticker reports while files are in flight. pub const BYTE_TICK: Duration = Duration::from_millis(250); -/// The CDN bases to fetch from: empty entries dropped, then one per distinct host (`host_key`), -/// the first base seen for a host kept, order preserved. pub fn distinct_bases(bases: &[String]) -> Vec<String> { let mut seen: HashSet<String> = HashSet::new(); bases @@ -732,10 +639,6 @@ pub fn distinct_bases(bases: &[String]) -> Vec<String> { .collect() } -/// Runs `fetch` with a reporter thread beside it that calls `on_bytes(bytes_done + partial, -/// bytes_total)` every [`BYTE_TICK`] when the value moved. `on_file_done` only fires when a whole -/// file is finished, so a game made of a few large files gave the UI a progress bar that sat -/// still and then jumped; this fills the gaps. One last report is made when `fetch` returns. fn with_byte_ticker<T>( events: &dyn GogEvents, bytes_done: &AtomicU64, @@ -765,9 +668,6 @@ fn with_byte_ticker<T>( }) } -/// Runs one download loop. Blocking; returns when every pending file is finalized, on the first -/// fatal file failure, or on cancel. Safe to call again with `skip_paths` extended by the files -/// this run reported done (that is how the Java side re-runs after a secure-link refresh). pub fn run(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> GogRunResult { match req.kind { PlanKind::Gen2Chunks => run_gen2(req, cancel, events), @@ -808,8 +708,6 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go let files_verified = AtomicU32::new(0); if files.is_empty() { - // Java: the caller already returned "no depot files collected" before the loop; reaching - // here means the two parsers disagree — surface it loudly rather than pretend success. return GogRunResult { success: false, error: "no files in plan (manifest parse mismatch)".to_string(), @@ -818,8 +716,6 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go }; } - // Resume / repair pass — Java does this per task on the pool threads; here it runs up front - // on `process_workers` threads so the fetch item list can exclude verified files. let pending: Vec<AtomicBool> = files.iter().map(|_| AtomicBool::new(false)).collect(); let next = AtomicUsize::new(0); let verify_threads = process_workers.min(files.len().max(1)); @@ -870,7 +766,6 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go }; } - // Fetch items: one per chunk of every pending file, in plan order (chunks in manifest order). let mut table: Vec<(usize, usize)> = Vec::new(); let mut items: Vec<FetchItem> = Vec::new(); let mut states: Vec<Mutex<FileState>> = Vec::new(); @@ -884,7 +779,6 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go handle: None, chunks_done: 0, partial: 0, - // Files not pending (verified or skip-listed) are already final for this run. finalized: !is_pending, failed: false, tmp_path, @@ -899,7 +793,6 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go let path = build_cdn_path(&chunk.hash); items.push(FetchItem { id: table.len() as u64, - // One URL per host, in `hosts` order: the core picks `urls[host_idx]`. urls: bases.iter().map(|b| build_chunk_url(b, &path)).collect(), reserve: chunk.compressed_size, range: None, @@ -943,15 +836,12 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go }; let opts = FetchOptions { max_workers, - // The ceiling split across the distinct CDN hosts (floor 6); one host gets all of it. per_host_cap, timeout: CHUNK_TIMEOUT, headers: vec![("User-Agent".to_string(), USER_AGENT.to_string())], ca_bundle_path: req.ca_bundle_path.clone(), process_workers, label: req.label.clone(), - // Stream mode: pieces inflate straight to disk; no whole-body byte budget (the 24 MiB - // body budget + largest-first big chunks left 1-3 requests in flight at window 16). stream: true, }; let log = |line: &str| events.on_log(line); @@ -1010,23 +900,14 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go } } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// gen1: per-file HTTP Range GET streamed to disk (`runGen1` / `downloadRange`) -// ───────────────────────────────────────────────────────────────────────────────────────────── struct Gen1State { handle: Option<File>, - /// Bytes this file's current attempt has written (credited to the sink's `partial`); a new - /// attempt starts over, so a retry never counts its bytes twice. attempt_bytes: u64, out_path: PathBuf, finalized: bool, } -/// Stream-mode sink for gen1. Java's `downloadRange`: `outFile.delete()` before EVERY attempt, -/// `new FileOutputStream(out)`, copy the Range body, no size/hash check; success → `doneG1++`, -/// `totalBytesG1 += size`, `"Downloading: <name> <speed>"`. Pieces arrive in order with their -/// offset; `offset == 0` marks the (re)start of an attempt, exactly where Java deletes the file. struct Gen1Sink<'a> { files: &'a [Gen1File], states: Vec<Mutex<Gen1State>>, @@ -1036,8 +917,6 @@ struct Gen1Sink<'a> { bytes_done: &'a AtomicU64, events: &'a dyn GogEvents, speed: Mutex<SpeedSampler>, - /// Bytes of files still in flight (finished chunks; for gen1, the current attempt's pieces). - /// `bytes_done + partial` is what the byte ticker reports between file completions. partial: AtomicU64, } @@ -1061,7 +940,6 @@ impl<'a> FetchSink for Gen1Sink<'a> { .lock() .map_err(|_| SinkError::Fatal("gen1 state poisoned".to_string()))?; if offset == 0 { - // New attempt: Java `outFile.delete()` + `new FileOutputStream(out)`. self.partial .fetch_sub(std::mem::take(&mut st.attempt_bytes), Ordering::Relaxed); st.handle = None; @@ -1109,7 +987,7 @@ impl<'a> FetchSink for Gen1Sink<'a> { let mut st = state .lock() .map_err(|_| SinkError::Fatal("gen1 state poisoned".to_string()))?; - st.handle = None; // close + st.handle = None; st.finalized = true; let attempt = std::mem::take(&mut st.attempt_bytes); self.bytes_done.fetch_add(file.size, Ordering::Relaxed); @@ -1130,10 +1008,6 @@ impl<'a> FetchSink for Gen1Sink<'a> { } } -/// gen1 loop. Resume = `exists && length == size` ("Resuming…", no bytes). Items = one Range GET -/// per pending file; retries/back-off by the core (Java: 3 attempts, 1 s / 2 s). Failed or -/// cancelled attempts leave whatever was written, like Java (a wrong-size partial fails the -/// size-match resume and is re-pulled next time; a base-install cancel deletes the whole dir). fn run_gen1(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> GogRunResult { let mut files: Vec<Gen1File> = Vec::new(); for manifest in &req.depot_manifests { @@ -1298,7 +1172,7 @@ fn run_gen1(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go GogRunResult { success, cancelled, - link_expiry: false, // gen1 has no secure-link refresh in Java + link_expiry: false, error: if cancelled && error.is_empty() { "cancelled".to_string() } else { error }, bytes_written: bytes_done.load(Ordering::Relaxed), files_done: files_done.load(Ordering::Relaxed), @@ -1412,8 +1286,6 @@ mod tests { assert!(peak >= 8.0, "2 MB across at most two buckets → ≥8 Mbps peak, got {peak}"); } - /// Whole-file finalize through the sink with a synthetic body (no network): verifies the - /// chunk → positioned write → whole-file MD5 → rename path and the progress event shape. #[test] fn sink_assembles_verifies_and_renames() { use flate2::write::ZlibEncoder; @@ -1468,10 +1340,8 @@ mod tests { }; let item = |id: u64| FetchItem { id, urls: vec!["x".into()], reserve: 0, range: None }; - // Second chunk first: positioned write must still produce the right bytes. assert_eq!(sink.process(&item(1), cb.clone()).unwrap(), part_b.len() as u64); assert!(tmp_path.exists() && !out_path.exists(), "still staging after 1 of 2 chunks"); - // A corrupt body is a retryable failure, not fatal. match sink.process(&item(0), b"\x78garbage".to_vec()) { Err(SinkError::Retry(msg)) => assert!(msg.contains("mismatch"), "{msg}"), Err(SinkError::Fatal(msg)) => panic!("expected Retry, got Fatal({msg})"), @@ -1580,16 +1450,14 @@ mod tests { assert_eq!(per_host_cap_for(0, 0), 6); } - /// Stream mode: zlib chunk split into pieces (with a poisoned first attempt restarting at - /// offset 0), a stored chunk, corrupt data → Retry, then the whole-file finalize. #[test] fn stream_sink_inflates_pieces_verifies_and_finalizes() { use flate2::write::ZlibEncoder; use flate2::Compression; let dir = temp_dir("stream"); - let part_a = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".repeat(50); // 2000 bytes, compresses well - let part_b = b"\x01\x02stored bytes".to_vec(); // first byte != 0x78 → stored + let part_a = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".repeat(50); + let part_b = b"\x01\x02stored bytes".to_vec(); let whole = [part_a.clone(), part_b.clone()].concat(); let mut enc = ZlibEncoder::new(Vec::new(), Compression::default()); enc.write_all(&part_a).unwrap(); @@ -1632,7 +1500,6 @@ mod tests { }; let item = |id: u64| FetchItem { id, urls: vec!["x".into()], reserve: 0, range: None }; - // Attempt 1 of the zlib chunk: corrupt tail → Retry from on_chunk. let mut corrupt = ca.clone(); for b in corrupt.iter_mut().skip(2) { *b = !*b; @@ -1642,7 +1509,6 @@ mod tests { Err(SinkError::Retry(m)) => assert!(m.contains("inflate failed"), "{m}"), Err(SinkError::Fatal(m)) => panic!("expected Retry, got Fatal({m})"), Ok(()) => { - // Some corruptions only surface at finish: that must also be a Retry. match sink.on_finish(&item(0), corrupt.len() as u64) { Err(SinkError::Retry(_)) => {} other => panic!("expected Retry at finish, got {}", match other { @@ -1653,7 +1519,6 @@ mod tests { } } } - // Attempt 2: restart at offset 0 in three pieces. let cut1 = ca.len() / 3; let cut2 = 2 * ca.len() / 3; sink.on_chunk(&item(0), 0, &ca[..cut1]).unwrap(); @@ -1661,7 +1526,6 @@ mod tests { sink.on_chunk(&item(0), cut2 as u64, &ca[cut2..]).unwrap(); assert_eq!(sink.on_finish(&item(0), ca.len() as u64).unwrap(), 0); assert!(tmp_path.exists() && !out_path.exists()); - // Stored chunk, one piece; wrong length first (→ Retry), then the real one. sink.on_chunk(&item(1), 0, &part_b[..3]).unwrap(); match sink.on_finish(&item(1), 3) { Err(SinkError::Retry(m)) => assert!(m.contains("compressed-size mismatch"), "{m}"), @@ -1705,7 +1569,6 @@ mod tests { partial: AtomicU64::new(0), }; let item = FetchItem { id: 0, urls: vec!["x".into()], reserve: 8, range: Some((100, 107)) }; - // Attempt 1 writes a partial, attempt 2 restarts from offset 0 and truncates it. sink.on_chunk(&item, 0, b"ZZZZZZZZZZZZ").unwrap(); sink.on_chunk(&item, 0, b"abcd").unwrap(); sink.on_chunk(&item, 4, b"efgh").unwrap(); @@ -1737,11 +1600,7 @@ mod tests { ..Default::default() }; let rec = Recorder { events: StdMutex::new(Vec::new()) }; - // Cancel before the fetch so no network is touched: the resume pass still runs. let cancel = AtomicBool::new(false); - // Use a pre-set cancel via a second request: run the resume pass, then expect the fetch - // to be attempted for short.bin only. We can't reach the network here, so assert on the - // plan log line instead of the outcome. cancel.store(true, Ordering::Relaxed); let res = run(&req, &cancel, &rec); assert!(res.cancelled, "{res:?}"); diff --git a/app/src/main/rust/stores/src/gog/jni.rs b/app/src/main/rust/stores/src/gog/jni.rs index eee602a6b..9fa1fbf01 100644 --- a/app/src/main/rust/stores/src/gog/jni.rs +++ b/app/src/main/rust/stores/src/gog/jni.rs @@ -1,15 +1,3 @@ -//! JNI facade for the GOG Rust download engine — the native half of -//! `com.droiddeck.launcher.stores.gog.GogNative` (Kotlin). Symbol names are bound to that -//! package/class; the `@JvmStatic external` declarations there are the other side of this file. -//! -//! Shape mirrors the Steam download JNI (`jni.rs` `nativeDownloadApp`): `nativeStart` validates -//! its inputs, takes a `GlobalRef` on the listener, spawns the worker thread and returns a handle -//! immediately; callbacks are delivered from the worker / process-pool threads through a -//! `JavaVM` attach; `nativeCancel` flips an `AtomicBool`; `nativeRelease` drops the Java side's -//! reference to the handle (the worker keeps its own until it exits). -//! -//! Log tag `GogNative`: every engine log line goes to logcat here AND to the listener's `onLog` -//! (which the Kotlin manager folds into its own debug log). use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -29,7 +17,6 @@ unsafe extern "C" { fn __android_log_write(prio: i32, tag: *const i8, text: *const i8) -> i32; } -/// `android.util.Log.i(LOG_TAG, message)`; a no-op on non-Android hosts (cargo test). pub fn android_log(message: &str) { #[cfg(target_os = "android")] { @@ -51,8 +38,6 @@ pub fn android_log(message: &str) { } } -/// One in-flight run. The Java side owns a `Box<Arc<..>>` (the `jlong`); the worker thread owns a -/// second `Arc` so `nativeRelease` is safe at any time after `nativeStart` returned. pub struct GogRunHandle { pub cancel: AtomicBool, } @@ -137,8 +122,6 @@ impl GogEvents for JniEvents { } fn on_bytes(&self, bytes_done: u64, bytes_total: u64) { - // A listener without `onBytes` (an app built against the older contract) makes this a - // NoSuchMethodError, which clear_exception drops: the run is unaffected. self.with_env(|env| { let _ = env.call_method( self.listener.as_obj(), @@ -209,7 +192,6 @@ unsafe fn handle_ref(handle: jlong) -> Option<&'static Arc<GogRunHandle>> { } } -/// Symbol-binding probe (see `GogNative.isAvailable`). Returns 1. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeProbe( _env: JNIEnv, @@ -218,9 +200,6 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativePr 1 } -/// Starts one download loop on a worker thread and returns a handle (0 = could not start; the -/// listener then receives NO callbacks). Inputs are exactly what `GogDownloadManager` holds after -/// manifest + secure-link resolution — see `GogRequest`. #[no_mangle] #[allow(clippy::too_many_arguments)] pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeStart( @@ -290,8 +269,6 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeSt Box::into_raw(Box::new(handle)) as jlong } -/// Requests cancellation; the run stops at its next scheduling point and `onComplete` fires with -/// `cancelled = true`. Idempotent. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeCancel( _env: JNIEnv, @@ -303,8 +280,6 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeCa } } -/// Drops the Java side's reference. Call once, after `onComplete` (or after cancelling — the worker -/// keeps the run alive until it exits either way). #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeRelease( _env: JNIEnv, diff --git a/app/src/main/rust/stores/src/gog/plan.rs b/app/src/main/rust/stores/src/gog/plan.rs index 830106dc1..50f833814 100644 --- a/app/src/main/rust/stores/src/gog/plan.rs +++ b/app/src/main/rust/stores/src/gog/plan.rs @@ -1,50 +1,24 @@ -//! Depot-manifest JSON → download plan. Mirrors `GogDownloadManager.parseDepotManifest` rule for -//! rule (see Bannerlator's `docs/RUST_GOG_PARITY.md` §3): -//! - root `depot.items[]`; `path` with `\` → `/` and one leading `/` stripped; -//! - a file is kept only when `path` is non-empty AND `chunks[]` is non-empty; -//! - per chunk: CDN hash = `compressedMd5` when non-empty, else `md5`; a chunk with neither is -//! dropped; `compressedSize` / `size` default 0; -//! - file `md5` kept when non-empty; `total_size` = Σ chunk `size` (decompressed); -//! - a file whose chunks all dropped is not added. -//! `org.json` coercion (`optString` turns numbers/bools into their text, `optLong` parses numeric -//! strings) is reproduced by [`opt_string`] / [`opt_u64`]. -//! -//! Additionally each chunk carries `offset` = prefix-sum of the preceding chunks' decompressed -//! sizes — Java appends sequentially so it never needs one; the Rust sink writes chunks at that -//! offset as they arrive so the final bytes are identical. use serde_json::Value; #[derive(Clone, Debug, PartialEq, Eq)] pub struct ChunkRef { - /// CDN-path hash (`compressedMd5` preferred, else `md5`). pub hash: String, - /// Verify compressed bytes before inflate (may be empty). pub compressed_md5: String, - /// Verify decompressed bytes after inflate (may be empty). pub md5: String, - /// Expected compressed size, 0 if absent. pub compressed_size: u64, - /// Expected decompressed size, 0 if absent. pub size: u64, - /// Decompressed offset inside the file (prefix-sum of preceding `size`s). pub offset: u64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct PlannedFile { - /// Install-dir-relative path, forward slashes, no leading slash. pub relative_path: String, - /// Whole-file MD5 from the manifest ("" when absent). pub md5: String, - /// Σ chunk decompressed sizes (0 if unknown). pub total_size: u64, pub chunks: Vec<ChunkRef>, } -/// `JSONObject.optString(key, "")` semantics: strings as-is, numbers/bools rendered, null/absent -/// → "". (`org.json` renders `JSONObject.NULL` as "null"; GOG manifests never carry JSON nulls in -/// these fields, and treating one as absent is the only safe reading.) pub fn opt_string(obj: &Value, key: &str) -> String { match obj.get(key) { Some(Value::String(s)) => s.clone(), @@ -54,8 +28,6 @@ pub fn opt_string(obj: &Value, key: &str) -> String { } } -/// `JSONObject.optLong(key, 0)` semantics for a size field (numbers, or numeric strings; a -/// negative/float value that Java would truncate is clamped to 0 — sizes are never negative). pub fn opt_u64(obj: &Value, key: &str) -> u64 { match obj.get(key) { Some(Value::Number(n)) => n @@ -68,8 +40,6 @@ pub fn opt_u64(obj: &Value, key: &str) -> u64 { } } -/// Parses one inflated gen2 depot manifest and appends its files to `out` -/// (`GogDownloadManager.parseDepotManifest`). Malformed JSON adds nothing, like Java's catch-all. pub fn parse_depot_manifest(json: &str, out: &mut Vec<PlannedFile>) { let Ok(root) = serde_json::from_str::<Value>(json) else { return; @@ -137,20 +107,14 @@ pub fn parse_depot_manifest(json: &str, out: &mut Vec<PlannedFile>) { } } -/// One gen1 file: a byte range of a (pre-signed) depot blob (`GogDownloadManager.Gen1File`). #[derive(Clone, Debug, PartialEq, Eq)] pub struct Gen1File { - /// Install-dir-relative path, used verbatim (Java's gen1 path does not normalise slashes). pub path: String, pub url: String, pub offset: u64, pub size: u64, } -/// Parses a gen1 build manifest (`runGen1`): every `depot[]` entry that is not `support: true`, -/// its `files[]` → (`path`, `url`, `offset` default 0, `size` default 0); an entry with -/// `size == 0` is dropped (Java's `path == null || url == null` checks never fire — `optString` -/// returns "" — so `size == 0` is the only effective filter, mirrored here). pub fn parse_gen1_manifest(json: &str, out: &mut Vec<Gen1File>) { let Ok(root) = serde_json::from_str::<Value>(json) else { return; @@ -184,10 +148,6 @@ pub fn parse_gen1_manifest(json: &str, out: &mut Vec<Gen1File>) { } } -/// Parses every manifest in order (the order Java fetched the depots) and optionally applies the -/// base-install largest-first (LPT) ordering — a STABLE sort by descending `total_size`, exactly -/// `Collections.sort(files, (a, b) -> Long.compare(b.totalSize, a.totalSize))`. The DLC and -/// dependency paths in Java do not sort, so they pass `sort_largest_first = false`. pub fn build_plan(manifests: &[String], sort_largest_first: bool) -> Vec<PlannedFile> { let mut files = Vec::new(); for manifest in manifests { @@ -199,8 +159,6 @@ pub fn build_plan(manifests: &[String], sort_largest_first: bool) -> Vec<Planned files } -/// `GogDownloadManager.buildCdnPath`: `"ab/cd/abcdef..."`. A hash shorter than 4 chars would -/// throw in Java (and fail that file); here it is returned unchanged and fails at fetch time. pub fn build_cdn_path(hash: &str) -> String { if !hash.is_char_boundary(2) || !hash.is_char_boundary(4) || hash.len() < 4 { return hash.to_string(); @@ -208,8 +166,6 @@ pub fn build_cdn_path(hash: &str) -> String { format!("{}/{}/{}", &hash[0..2], &hash[2..4], hash) } -/// `GogDownloadManager.buildChunkUrl`: appends the chunk path BEFORE any query string so the -/// secure-link token survives. pub fn build_chunk_url(base: &str, chunk_path: &str) -> String { match base.find('?') { Some(q) => format!("{}/{}{}", &base[..q], chunk_path, &base[q..]), @@ -217,8 +173,6 @@ pub fn build_chunk_url(base: &str, chunk_path: &str) -> String { } } -/// Per-host key for the fetch core's host semaphores: `scheme://host[:port]` of `url` (lowercased -/// host). Java has one resolved CDN base per run, so the host list has exactly one entry. pub fn host_key(url: &str) -> String { let (scheme, rest) = match url.find("://") { Some(i) => (&url[..i], &url[i + 3..]), diff --git a/app/src/main/rust/stores/src/lib.rs b/app/src/main/rust/stores/src/lib.rs index 963826c40..87d278ab0 100644 --- a/app/src/main/rust/stores/src/lib.rs +++ b/app/src/main/rust/stores/src/lib.rs @@ -1,21 +1,4 @@ -//! DroidDeck's native store download engines (`libdroiddeckstores.so`). -//! -//! The GOG, Epic Games Store and Amazon Games download loops, ported from Bannerlator's -//! `bl-steam-client` crate (GPL-3.0-or-later, itself derived from WinNative's `wn-steam-client`). -//! Only the store engines and the fetch core they run on came across: DroidDeck's Steam client is -//! the real one, running in the Linux runtime, so none of the Steam CM / depot code is here. -//! -//! Each store module owns its manifest → plan → `FetchItem` mapping, its `FetchSink` (inflate + -//! hash + write in the exact output layout of the Kotlin manager that drives it) and its JNI facade. -//! The JNI contract — classes, method names, signatures, threading — is written down in `JNI.md` -//! next to `Cargo.toml`; the Kotlin side mirrors it exactly. -//! -//! What a run looks like: Kotlin hands over a manifest it already parsed (or, for Amazon, the -//! plan it built from one), the resolved CDN base(s) and the install directory, calls `nativeStart` -//! and gets a handle back at once. Everything after that happens on native threads: the fetch core -//! keeps an adaptive window of HTTP requests in flight on one tokio runtime, hands bodies to a sync -//! process pool that inflates, hashes and writes them, and reports progress through the listener -//! object Kotlin passed in. `nativeCancel` flips a flag; `onComplete` fires exactly once. +//! Ported from Bannerlator's bl-steam-client crate (GPL-3.0-or-later), itself derived from WinNative's wn-steam-client. #![allow(clippy::missing_safety_doc, clippy::result_large_err)] diff --git a/app/src/main/rust/stores/src/md5_small.rs b/app/src/main/rust/stores/src/md5_small.rs index 8abd58bcc..943785b3f 100644 --- a/app/src/main/rust/stores/src/md5_small.rs +++ b/app/src/main/rust/stores/src/md5_small.rs @@ -1,17 +1,10 @@ -//! Minimal MD5 (RFC 1321) for the store download engines (GOG chunk / file verification). -//! -//! In-crate on purpose: the crate is built `--locked` in CI with no way to add a dependency from -//! this environment, and the algorithm is 100 lines. Not for anything security-sensitive — it only -//! mirrors the checksum the Java managers already compare against. -/// One-shot digest. pub fn md5(data: &[u8]) -> [u8; 16] { let mut h = Md5::new(); h.update(data); h.finalize() } -/// Lower-case hex of a 16-byte digest. pub fn md5_hex(digest: &[u8; 16]) -> String { const HEX: &[u8; 16] = b"0123456789abcdef"; let mut out = String::with_capacity(32); @@ -22,7 +15,6 @@ pub fn md5_hex(digest: &[u8; 16]) -> String { out } -/// Streaming MD5: `update` any number of times, then `finalize`. #[derive(Clone, Debug)] pub struct Md5 { state: [u32; 4], @@ -93,7 +85,6 @@ impl Md5 { let bit_len = self.total_len.wrapping_mul(8); let mut pad = [0u8; 72]; pad[0] = 0x80; - // Pad to 56 mod 64, then append the 64-bit little-endian bit length. let pad_len = if self.buffer_len < 56 { 56 - self.buffer_len } else { @@ -189,7 +180,6 @@ mod tests { #[test] fn padding_edge_lengths() { - // 55 / 56 / 64 bytes exercise the "pad fits in one block" vs "needs a second block" paths. assert_eq!(hex(&[b'a'; 55]), "ef1772b6dff9a122358552954ad0df65"); assert_eq!(hex(&[b'a'; 56]), "3b0c8ac703f828b04c6c197006d17218"); assert_eq!(hex(&[b'a'; 64]), "014842d480b571495a4a0363793f7367"); diff --git a/app/src/main/rust/stores/src/priority.rs b/app/src/main/rust/stores/src/priority.rs index 916008fac..7ea7bb2ea 100644 --- a/app/src/main/rust/stores/src/priority.rs +++ b/app/src/main/rust/stores/src/priority.rs @@ -1,22 +1,5 @@ -//! Background priority for every thread the store engines create. -//! -//! A download keeps several threads busy at once (the run's worker, the tokio runtime and its -//! blocking pool for DNS, the inflate/hash/write pool, the throughput reporter, GOG's verify pass, -//! the Epic assembler). At the default nice 0 they compete as equals with the app's UI and input -//! threads, and on the device the menus' animations and controller navigation slowed visibly for -//! as long as a store download ran. At nice +10 the scheduler hands them only what the UI leaves, -//! which for a download bound by network and storage costs next to nothing. -//! -//! Linux applies `setpriority(PRIO_PROCESS, tid, …)` to the one thread `tid`, not the process, and -//! a new thread inherits its creator's nice value; every thread is still lowered explicitly at its -//! start so the guarantee does not depend on who spawned it. Only threads this library creates are -//! touched: the JNI calling thread belongs to the app and keeps its priority. - -/// The nice value store-engine threads run at. pub const BACKGROUND_NICE: i32 = 10; -/// Lower the calling thread to [`BACKGROUND_NICE`]. Best effort: a refusal leaves the thread at -/// its current priority and the download runs as before. pub fn background() { #[cfg(any(target_os = "android", target_os = "linux"))] unsafe { @@ -25,7 +8,6 @@ pub fn background() { } } -/// The calling thread's nice value as a log field, e.g. `priority=nice10`. pub fn log_field() -> String { #[cfg(any(target_os = "android", target_os = "linux"))] { diff --git a/app/src/main/rust/stores/src/stores_jni.rs b/app/src/main/rust/stores/src/stores_jni.rs index e69b7f5bd..ea86fad3f 100644 --- a/app/src/main/rust/stores/src/stores_jni.rs +++ b/app/src/main/rust/stores/src/stores_jni.rs @@ -1,19 +1,10 @@ -//! The one export that is not a store engine: `com.droiddeck.launcher.stores.StoresNative`. -//! -//! `nativeVersion()` exists so the app can prove, at Setup time, that `libdroiddeckstores.so` -//! loaded and its JNI exports bind — before a user is mid-download and an -//! `UnsatisfiedLinkError` would surface as a failed install. It returns the crate version from -//! `Cargo.toml`, which the app can show next to the other component versions. use jni::objects::JClass; use jni::sys::jstring; use jni::JNIEnv; -/// The crate version, as `Cargo.toml` declares it. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -/// `StoresNative.nativeVersion(): String` — the crate version (`"0.1.0"`). A null return means the -/// JVM could not allocate the string, which the caller treats like a failed load. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_StoresNative_nativeVersion( env: JNIEnv, diff --git a/app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt b/app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt index b1c97962d..41f6f9d4e 100644 --- a/app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt @@ -88,7 +88,6 @@ class LogRedactorTest { } } - // The Steam UI's own login line (webhelper_js.txt), as the privacy scan found it. @Test fun loginStateLineLosesAnEmailAccount() { val line = "[2026-10-07 21:14:03] SteamUI: INFO: Login: OnLoginStateChange someone.masked@example.com 2 1 0 0" val out = LogRedactor.redact(line) diff --git a/app/src/test/java/com/droiddeck/launcher/core/SecretScrubTest.kt b/app/src/test/java/com/droiddeck/launcher/core/SecretScrubTest.kt index 86ff6ece8..61133f8a4 100644 --- a/app/src/test/java/com/droiddeck/launcher/core/SecretScrubTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/core/SecretScrubTest.kt @@ -13,7 +13,6 @@ class SecretScrubTest { "0024:warn:seh:dispatch_exception code=c0000005 flags=0 addr=0000000140012345", "0030:trace:seh:handle_syscall_fault code=80000003 flags=0", "exception code=0xC0000005 at 0x140012345", - // An exception code stays one even on a line that also carries OAuth keys. "client_id=abc code=c0000005", )) assertEquals(line, share(line)) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt index 528e97d42..2d0a29de4 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt @@ -13,7 +13,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** The Games tab's +: one picked .exe becomes a game, once, wherever the session can see it. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedExesTest { @@ -54,7 +53,6 @@ class AddedExesTest { assertEquals(Library.ADDED, game.source) assertTrue(File(app.filesDir, "session/added-games.json").readText().contains("Celeste.exe")) - // The same exe again: the game it already is, no second entry. val again = AddedGames.addExe(app, pick) assertTrue(again is AddedGames.AddResult.Existing) assertEquals(game.appId, (again as AddedGames.AddResult.Existing).game.appId) @@ -83,7 +81,6 @@ class AddedExesTest { val result = AddedGames.addExe(app, deep) assertTrue(result is AddedGames.AddResult.Switched) assertEquals(deep.path, AddedGames.scan(app).single().exe.path) - // The Games folder's game is an entry of its own (GamesFolderMigration); no second one. assertEquals(listOf(File(games, "Example").path), AddedExes.list(app).map { it.folder }) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameArtLookupTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameArtLookupTest.kt index 1c2978602..e0c7f3eb1 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameArtLookupTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameArtLookupTest.kt @@ -20,7 +20,6 @@ import java.io.File import java.security.SecureRandom import javax.crypto.spec.SecretKeySpec -/** Where an added game's art comes from: its own Steam appid, a matching name, SteamGridDB's key. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedGameArtLookupTest { @@ -39,7 +38,6 @@ class AddedGameArtLookupTest { val match = SteamMatch.resolve(app, dir.path, exe, "Palworld") { fail("searched"); null } assertEquals(1623730, match.appId) assertEquals(SteamMatch.Certainty.FILES, match.certainty) - // The same record for everyone who asks. assertEquals(1623730, SteamMatch.get(app, dir.path)!!.appId) } @@ -68,7 +66,6 @@ class AddedGameArtLookupTest { } @Test fun aStoreResultIsTakenOnlyWhenItsNameMatches() { - // The first result is no longer taken for being first. assertNull(AddedGameArt.pickStoreResult("My Obscure Game", listOf("10" to "Totally Different", "11" to "Another One"))) assertEquals("20", AddedGameArt.pickStoreResult("Hades", listOf("19" to "Hades II Soundtrack Bundle Extra", "20" to "Hades"))) assertEquals("30", AddedGameArt.pickStoreResult("Skyrim Special Edition Complete Edition", listOf("1" to "Unrelated", "30" to "Skyrim Special Edition"))) @@ -95,7 +92,6 @@ class AddedGameArtLookupTest { assertEquals("build", SteamGridDb.pick("", "build")) assertEquals("build", SteamGridDb.pick(" ", "build")) assertEquals("", SteamGridDb.pick("", "")) - // This test build carries no key. assertEquals("", SteamGridDb.builtInKey()) assertEquals("", SteamGridDb.key(app)) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameEditsTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameEditsTest.kt index f83a082d1..3697ef9a7 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameEditsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameEditsTest.kt @@ -18,7 +18,6 @@ import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config import java.io.File -/** "Add all games in this folder", the editor's writes to the shortcut listing, art choices. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedGameEditsTest { @@ -52,14 +51,11 @@ class AddedGameEditsTest { val alpha = result.added.first { it.name == "Alpha" } assertEquals("Alpha.exe", alpha.exe.name) assertFalse(alpha.exeUncertain) - // Two lookalikes: the best guess, marked. assertTrue(result.added.first { it.name == "Beta" }.exeUncertain) - // Entries, not a Games folder. assertEquals(emptyList<String>(), SessionPrefs.addedGamesDirs(app)) assertEquals(3, AddedExes.list(app).size) assertTrue(listing().map { it.getString("name") }.containsAll(listOf("Alpha", "Beta", "Celeste"))) - // Again: nothing new, all three there already. val again = AddedGames.addFolder(app, dir) assertEquals(emptyList<String>(), again.added.map { it.name }) assertEquals(3, again.already) @@ -84,7 +80,6 @@ class AddedGameEditsTest { assertEquals(game.appId, entry.getLong("appid")) assertEquals(game.appId, AddedGameEdits.game(app, folder)!!.appId) - // Back to automatic. AddedGameEdits.setName(app, folder, "") assertTrue(AddedGameEdits.setStartIn(app, folder, "")) val auto = listing().single() @@ -109,8 +104,6 @@ class AddedGameEditsTest { assertEquals("/g/Game/data", SessionPrefs.addedGameStartIn(app, folder.path)) assertEquals("-safe", SessionPrefs.addedGameLaunch(app, folder.path)) - // The user renames it in the app before Steam's shortcut is written again: the same Steam - // value is not taken a second time. SessionPrefs.setAddedGameName(app, folder.path, "Mine") AddedGames.adoptSteamEdits(app, folder, record) assertEquals("Mine", SessionPrefs.addedGameName(app, folder.path)) diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesCandidatesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesCandidatesTest.kt index 6771a4a18..f2881c712 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesCandidatesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesCandidatesTest.kt @@ -13,7 +13,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** The .exe picker below a game folder (five levels, past installer folders, ranked) and the names it finds. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedGamesCandidatesTest { @@ -65,7 +64,6 @@ class AddedGamesCandidatesTest { val game = tmp.newFolder("Huge") for (i in 0 until GameExePicker.WALK_BUDGET + 50) File(game, "data/d%04d".format(i)).mkdirs() exe(game, "zzz/Late.exe") - // data/ alone fills the budget before zzz/ is listed. assertEquals(emptyList<String>(), names(game)) val small = tmp.newFolder("Small") exe(small, "x/y/Found.exe") @@ -136,7 +134,6 @@ class AddedGamesCandidatesTest { game.setLastModified(game.lastModified() + 10_000) val after = AddedGames.scan(app).single() assertEquals("The Witcher 3 - Wild Hunt", after.name) - // Already added under the folder name: the shortcut keeps its id. assertEquals(before.appId, after.appId) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesRemoveTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesRemoveTest.kt index e98d08031..00da0980e 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesRemoveTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesRemoveTest.kt @@ -12,7 +12,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** Removing an added game stops listing it and keeps it off after a rescan, until it is added again; nothing on disk goes. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedGamesRemoveTest { @@ -47,7 +46,6 @@ class AddedGamesRemoveTest { SessionPrefs.setAddedGameRemoved(app, File(games, "Beta").path, true) assertEquals(listOf("Alpha", "Gamma"), AddedGames.scan(app).map { it.name }) - // A rescan does not bring it back. assertEquals(listOf("Alpha", "Gamma"), AddedGames.scan(app).map { it.name }) SessionPrefs.setAddedGameRemoved(app, File(games, "Beta").path, false) @@ -66,7 +64,6 @@ class AddedGamesRemoveTest { assertTrue(!listing.contains("\"Beta\"")) assertTrue(File(beta, "Beta.exe").isFile) - // Added again with the +: it comes back. assertTrue(AddedGames.addExe(app, File(beta, "Beta.exe")) is AddedGames.AddResult.Switched) assertEquals(emptyList<String>(), SessionPrefs.removedAddedGames(app)) assertTrue(File(app.filesDir, "session/added-games.json").readText().contains("\"Beta\"")) diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesStoreScanTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesStoreScanTest.kt index 6284086c7..38d3760c1 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesStoreScanTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesStoreScanTest.kt @@ -13,7 +13,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** A store install is one more folder the scan finds, named and aimed by its sidecar. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedGamesStoreScanTest { @@ -38,7 +37,6 @@ class AddedGamesStoreScanTest { assertEquals("HOMM3.exe", game.exe.name) assertEquals(Store.GOG.id, game.source) assertEquals("1207658924", game.storeId) - // Inside the runtime's tree the folder needs no bind: its guest path is its path from the tree's root. assertEquals("/root/Games/Stores/GOG/Heroes of Might and Magic 3 Complete/HOMM3.exe", game.guestExe) assertEquals("/root/Games/Stores/GOG/Heroes of Might and Magic 3 Complete", game.guestDir) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesTest.kt index 244f66373..b42748f18 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesTest.kt @@ -19,7 +19,6 @@ class AddedGamesTest { "\"AppState\"\n{\n\t\"appid\"\t\t\"17410\"\n\t\"installdir\"\t\t\"Mirrors Edge\"\n}\n", ) assertEquals(setOf("monster_hunter_stories_3_twisted_reflection", "mirrors edge"), AddedGames.steamInstallDirs(common)) - // Loose game folders at the library root are never Steam's. assertEquals(emptySet<String>(), AddedGames.steamInstallDirs(library)) } } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/GameFileSyncTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/GameFileSyncTest.kt index 09245502a..a4e3b0028 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/GameFileSyncTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/GameFileSyncTest.kt @@ -30,7 +30,6 @@ class GameFileSyncTest { assertEquals("8400", File(folder, "Geometry Wars.steam").readText()) assertEquals(folder.path, GameFileSync.folder(context)) assertEquals(1, folder.listFiles()!!.count { it.extension == GameFiles.EXTENSION }) - // An unavailable source must leave the existing export intact. val unavailable = File(steamapps.parentFile, "steamapps-unavailable") assertTrue(steamapps.renameTo(unavailable)) assertThrows(IllegalStateException::class.java) { GameFileSync.syncConfigured(context) } @@ -38,11 +37,9 @@ class GameFileSyncTest { assertTrue(unavailable.renameTo(steamapps)) GameFileSync.syncConfigured(context) assertTrue(File(folder, "Geometry Wars.steam").exists()) - // A genuine uninstall removes the owned export at the next tick. assertTrue(manifest.delete()) GameFileSync.syncConfigured(context) assertEquals(0, folder.listFiles()!!.count { it.extension == GameFiles.EXTENSION }) - // Disabled sync does not write files for new installs. GameFileSync.disable(context) manifest.writeText("\"name\" \"Geometry Wars\"\n\"StateFlags\" \"4\"") GameFileSync.syncConfigured(context) diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/GameFilesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/GameFilesTest.kt index 8bf61b338..d37689e29 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/GameFilesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/GameFilesTest.kt @@ -38,7 +38,6 @@ class GameFilesTest { val provider = LaunchFileProvider(file) provider.attachInfo(context, android.content.pm.ProviderInfo().apply { authority = "frontend" }) ShadowContentResolver.registerProviderInternal("frontend", provider) - // Providers often give opaque document IDs with no filename extension. val uri = Uri.parse("content://frontend/document/opaque%3A42") assertEquals("18446744073709551615", GameFiles.readIntent(context, Intent(Intent.ACTION_VIEW, uri))) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/GameIdentifierTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/GameIdentifierTest.kt index 9f6a6820b..f7ddc0212 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/GameIdentifierTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/GameIdentifierTest.kt @@ -9,18 +9,13 @@ import org.junit.Test import org.junit.rules.TemporaryFolder import java.io.File -/** - * Signal-chain coverage for [GameIdentifier]. Pure JVM: builds - * fake game layouts in a temp dir and asserts the right appId/name/source/confidence come back. - * The PE version-resource path is exercised on-device (real .exe), not here. - */ class GameIdentifierTest { @get:Rule val tmp = TemporaryFolder() private fun exeIn(dir: File, name: String = "game.exe"): File = - File(dir, name).apply { parentFile?.mkdirs(); writeBytes(byteArrayOf(0x4D, 0x5A)) } // "MZ" — not a real PE + File(dir, name).apply { parentFile?.mkdirs(); writeBytes(byteArrayOf(0x4D, 0x5A)) } @Test fun steamAppIdTxt_besideExe_givesHighConfidenceAppId() { @@ -51,7 +46,6 @@ class GameIdentifierTest { @Test fun steamManifestAcf_matchedByInstalldir_givesAppIdAndName() { - // steamapps/common/Palworld/Pal/Binaries/Win64/Palworld-Win64-Shipping.exe val steamapps = tmp.newFolder("steamapps") File(steamapps, "appmanifest_1623730.acf").writeText( """ @@ -63,7 +57,6 @@ class GameIdentifierTest { } """.trimIndent() ) - // a decoy manifest that must NOT win File(steamapps, "appmanifest_570.acf").writeText( """"AppState" { "appid" "570" "name" "Dota 2" "installdir" "dota 2 beta" }""" ) @@ -81,9 +74,6 @@ class GameIdentifierTest { """{"gameId":"1207658691","name":"The Witcher 3: Wild Hunt","version":1}""" ) val id = GameIdentifier.identify(exeIn(dir)) - // normalizeName() deliberately converts ":" to " - " (a colon is illegal in a Windows - // filename), so the identified name never carries one. The test asserted the raw GOG - // string and had never passed. assertEquals("The Witcher 3 - Wild Hunt", id.name) assertEquals("1207658691", id.gogId) assertNull(id.appId) @@ -103,7 +93,6 @@ class GameIdentifierTest { @Test fun noMarkers_fallsBackToCleanedFolderName_forEngineBinary() { - // UE shipping binary → the folder name is the real title, exe basename is noise. val dir = tmp.newFolder("Hades") val id = GameIdentifier.identify(exeIn(dir, "Hades-Win64-Shipping.exe")) assertEquals("Hades", id.name) @@ -115,18 +104,16 @@ class GameIdentifierTest { fun fallbackName_stripsRepackTagsAndVersionTokens() { val dir = tmp.newFolder("Stardew Valley [FitGirl Repack] v1.5.4") val id = GameIdentifier.identify(exeIn(dir, "Stardew.exe")) - assertEquals("Stardew", id.name) // exe basename wins (not an engine binary); folder tags irrelevant here - // and a repack-style exe basename is cleaned too: + assertEquals("Stardew", id.name) val id2 = GameIdentifier.identify(exeIn(tmp.newFolder("g2"), "Game.CODEX.exe")) assertEquals(GameIdentifier.Source.FILENAME, id2.source) } @Test fun fltIni_gameSettingsAppId_isParsed() { - // Real God of War (FLT release) layout: flt.ini holds the appid under [GameSettings]. val dir = tmp.newFolder("GodOfWar") File(dir, "flt.ini").writeText("[GameSettings]\nAppId=1593500\nUserName=X\nBuildId=7969425\n") - File(dir, "settings.ini").writeText("[Settings]\nVideoDevice=Wrapper\nMonitor=0\n") // no AppId -> ignored + File(dir, "settings.ini").writeText("[Settings]\nVideoDevice=Wrapper\nMonitor=0\n") val id = GameIdentifier.identify(exeIn(dir, "GoW.exe")) assertEquals(1593500, id.appId) assertEquals(GameIdentifier.Confidence.HIGH, id.confidence) @@ -141,12 +128,10 @@ class GameIdentifierTest { @Test fun bestName_prefersFileDescriptionWhenProductNameIsAbbreviation() { - // God of War ships ProductName "GoW" but FileDescription "God of War". assertEquals( "God of War", PeVersionInfo.bestName(mapOf("ProductName" to "GoW", "FileDescription" to "God of War")), ) - // Normal case: ProductName is the full title -> keep it. assertEquals( "Cyberpunk 2077", PeVersionInfo.bestName(mapOf("ProductName" to "Cyberpunk 2077", "FileDescription" to "Cyberpunk 2077")), @@ -156,18 +141,15 @@ class GameIdentifierTest { @Test fun name_isNormalizedToBeFilesystemSafeAndTidy() { - // Colon → " - " (would otherwise be sanitized to '_' by the shortcut writer). val d1 = tmp.newFolder("ds") File(d1, "goggame-1.info").writeText("""{"name":"Dark Souls: Remastered"}""") assertEquals("Dark Souls - Remastered", GameIdentifier.identify(exeIn(d1)).name) - // Trademark marks (symbol and text forms) stripped. val d2 = tmp.newFolder("gow2") File(d2, "goggame-2.info").writeText("""{"name":"God of War™"}""") assertEquals("God of War", GameIdentifier.identify(exeIn(d2)).name) val d3 = tmp.newFolder("ds3") File(d3, "goggame-3.info").writeText("""{"name":"DARK SOULS(TM): REMASTERED"}""") assertEquals("DARK SOULS - REMASTERED", GameIdentifier.identify(exeIn(d3)).name) - // Trailing PE descriptors dropped (DiRT 3's FileDescription is "DiRT 3 Executable"). val d4 = tmp.newFolder("dirt") File(d4, "goggame-4.info").writeText("""{"name":"DiRT 3 Executable"}""") assertEquals("DiRT 3", GameIdentifier.identify(exeIn(d4)).name) @@ -175,8 +157,6 @@ class GameIdentifierTest { @Test fun launcherExe_prefersFolderName_overLauncher() { - // GTA V Enhanced: user picks the Rockstar launcher PlayGTAV.exe (no Steam id, PE = junk on - // a real exe). Fallback must use the folder title, not "PlayGTAV". val dir = tmp.newFolder("Grand Theft Auto V Enhanced") val id = GameIdentifier.identify(exeIn(dir, "PlayGTAV.exe")) assertEquals("Grand Theft Auto V Enhanced", id.name) @@ -187,12 +167,11 @@ class GameIdentifierTest { fun launcherExe_withSeparator_prefersFolder_andStripsSiteTag() { val dir = tmp.newFolder("Grand-Theft-Auto-IV-AnkerGames") val id = GameIdentifier.identify(exeIn(dir, "Play - GTA IV.exe")) - assertEquals("Grand Theft Auto IV", id.name) // "AnkerGames" site tag stripped + assertEquals("Grand Theft Auto IV", id.name) } @Test fun nonLauncherExe_startingWithPlay_keepsItsOwnName() { - // "PlayerUnknown" must NOT be treated as a launcher (would wrongly use the folder name). val dir = tmp.newFolder("SomeInstallFolder") val id = GameIdentifier.identify(exeIn(dir, "PlayerUnknown.exe")) assertEquals("PlayerUnknown", id.name) @@ -211,8 +190,7 @@ class GameIdentifierTest { @Test fun emptyWhenNothingIdentifiable() { val dir = tmp.newFolder("x") - val id = GameIdentifier.identify(exeIn(dir, "Game.exe")) // engine-ish base → folder "x" - // "x" is a valid (if useless) fallback name; the point is it must not crash and appId is null. + val id = GameIdentifier.identify(exeIn(dir, "Game.exe")) assertNull(id.appId) assertTrue(id.confidence == GameIdentifier.Confidence.LOW) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/GamesFolderMigrationTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/GamesFolderMigrationTest.kt index 1d81b1af4..eb101d293 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/GamesFolderMigrationTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/GamesFolderMigrationTest.kt @@ -16,7 +16,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** Games folders from earlier builds become entries, every game keeping what it had. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class GamesFolderMigrationTest { @@ -40,7 +39,6 @@ class GamesFolderMigrationTest { StoreGameSidecar.file(store).writeText("""{"store":"gog","id":"1","title":"Store Game","exe":"Store.exe"}""") File(pc, "Notes").mkdirs() SessionPrefs.setAddedGamesDirs(app, listOf(pc.path)) - // What earlier scans had kept, by folder. SessionPrefs.setAddedGameExe(app, beta.path, pick.path) SessionPrefs.addedGameAppId(app, alpha.path, 0x8123_4567L) SessionPrefs.addedGameAppId(app, beta.path, 0x8765_4321L) @@ -63,11 +61,9 @@ class GamesFolderMigrationTest { assertEquals(0x8765_4321L, games.getValue(beta.path).appId) assertEquals("Alpha GOTY", games.getValue(alpha.path).name) assertEquals(pick.path, games.getValue(beta.path).exe.path) - // The same path inside the session as before: the shortcut keeps its target. assertEquals(AddedGames.GUEST_DIR + "/PC/Alpha/Alpha.exe", games.getValue(alpha.path).guestExe) assertEquals(AddedGames.GUEST_DIR + "/PC/Beta/bin/Beta64.exe", games.getValue(beta.path).guestExe) assertTrue(SessionPrefs.removedAddedGames(app).contains(gone.path)) - // Not a library any more. assertTrue(GameStorage.gamesFolderLibraries(app).isEmpty()) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/SteamPicsTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/SteamPicsTest.kt index 60c6b96c5..aa05b1b7a 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/SteamPicsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/SteamPicsTest.kt @@ -17,13 +17,11 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** Steam's app info asked anonymously: the answer read, batched, kept, and asked only when needed. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class SteamPicsTest { private val app get() = RuntimeEnvironment.getApplication() - /** Dragon's Dogma 2's app info as Steam's anonymous PICS answer carried it (recorded 2026-10-10). */ private val recorded: String get() = javaClass.classLoader!!.getResource("steam-pics/app_2054970.vdf")!!.readText() @After fun tearDown() { SteamAppInfo.pics = { SteamPics.lookup(it) } } @@ -32,7 +30,6 @@ class SteamPicsTest { val info = SteamPics.infoFromKeyValues(recorded)!! assertEquals("Dragon's Dogma 2", info.name) assertEquals(listOf("228988"), info.sharedDepots) - // VC++ 2019 for the Windows components. assertEquals(listOf("vcredist2019_dll" to "VC++ 2019"), WinCompSources.steamDepots(info.sharedDepots)) } @@ -66,11 +63,9 @@ class SteamPicsTest { val calls = ArrayList<List<Int>>() SteamAppInfo.pics = { ids -> calls.add(ids); mapOf(2054970 to SteamPics.infoFromKeyValues(recorded), 7 to null) } assertTrue(SteamAppInfo.lookup(app, listOf(2054970, 7))) - // One connection for both. assertEquals(listOf(listOf(2054970, 7)), calls) assertEquals(listOf("228988"), SteamAppInfo.info(app, 2054970)!!.sharedDepots) assertNull(SteamAppInfo.info(app, 7)) - // Kept: nothing asked again (the miss waits a week). assertFalse(SteamAppInfo.lookup(app, listOf(2054970, 7))) assertEquals(1, calls.size) assertEquals("hit", SteamAppInfo.state(app, 2054970)) @@ -78,7 +73,6 @@ class SteamPicsTest { } @Test fun noNetworkWhenTheClientsCacheHasTheApp() { - // An appinfo.vdf with the app in it (built as in WinCompSourcesTest). val file = SteamAppInfo.appinfoFile(app) WinCompSourcesTest.writeAppinfo(file, 1145360) SteamAppInfo.pics = { fail("asked Steam"); null } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt index 476428a90..78ac65445 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt @@ -20,7 +20,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** Each source's list of Windows components, how it maps to the catalog, and what turns on by itself. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class WinCompSourcesTest { @@ -35,7 +34,6 @@ class WinCompSourcesTest { private fun auto(findings: List<WinCompSources.Finding>) = findings.filter { it.auto }.map { it.component to it.origin } - // ---- mapping tables ---- @Test fun gogDependencyIdsMapToComponents() { assertEquals(listOf("vcredist2015"), WinCompSources.gog("MSVC2017_x64")) @@ -62,7 +60,6 @@ class WinCompSourcesTest { assertEquals(listOf("vcredist2015_dll" to "VC++ 2015", "d3dx9" to "DirectX (June 2010)"), WinCompSources.steamDepots(listOf("228986", "228990", "999"))) } - // ---- per source ---- @Test fun steamTakesItsSharedDepotsAndTurnsThemOn() { val steamapps = tmp.newFolder("steamapps") @@ -80,7 +77,6 @@ class WinCompSourcesTest { sidecar(folder, Store.GOG, mapOf(WinCompSources.GOG_DEPENDENCIES to "MSVC2017_x64,DirectX,ISI")) val findings = WinCompSources.forGame(app, game(folder, "gog")) assertEquals(listOf("vcredist2015" to WinCompSources.Origin.GOG, "d3dx9" to WinCompSources.Origin.GOG), auto(findings)) - // ISI is none of ours: a suggestion under its own name. assertEquals(listOf("ISI"), findings.filter { it.component == null }.map { it.original }) } @@ -105,12 +101,10 @@ class WinCompSourcesTest { assertEquals(listOf("Activate.exe"), findings.filter { it.component == null }.map { it.original }) } - // ---- Custom: certainty decides ---- private fun appinfo(file: File, appId: Int) = writeAppinfo(file, appId) companion object { - /** A version 29 appinfo.vdf with one app, whose depots point at two Steamworks Shared ones. */ fun writeAppinfo(file: File, appId: Int) { val strings = listOf("appinfo", "common", "name", "depots", "228986", "depotfromapp", "228990", "1001", "maxsize") fun key(s: String) = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(strings.indexOf(s)).array() @@ -173,7 +167,6 @@ class WinCompSourcesTest { assertEquals(listOf("vcredist2013" to WinCompSources.Origin.FOLDER), findings.map { it.component to it.origin }) } - // ---- what turns on, and the user's switches ---- private fun ready(name: String) = WinComponents.Component(name, "", "", "ready", emptyList(), listOf(WinComponents.Step("override_dll", JSONObject().put("dll", "x").put("type", "native")))) @@ -187,7 +180,6 @@ class WinCompSourcesTest { WinCompSources.Finding("d3dx9", "d3dx9_43.dll", WinCompSources.Origin.FOLDER, false), ) val picks = AutoComponents.autoPicks(findings, catalog) - // The detector's name installs as its DLL twin; physx cannot install; the folder scan only recommends. assertEquals(listOf("vcredist2015_dll"), picks.keys.toList()) assertEquals("GOG", picks.getValue("vcredist2015_dll").kind) } @@ -199,11 +191,9 @@ class WinCompSourcesTest { WinComponents.setUser(app, key, "d3dx9", false) WinComponents.setUser(app, key, "oalinst_dll", true) assertEquals(listOf("xact", "oalinst_dll"), WinComponents.picks(app, key)) - // A new automatic list keeps the user's switches. WinComponents.setAuto(app, key, mapOf("d3dx9" to WinComponents.AutoReason("GOG", listOf("DirectX")))) assertEquals(listOf("oalinst_dll"), WinComponents.picks(app, key)) assertEquals(mapOf("d3dx9" to false, "oalinst_dll" to true), WinComponents.selection(app, key).user) - // What the launch reads stays version 1 with the effective list. val text = File(app.filesDir, "wincomponents.json").readText() assertEquals(1, JSONObject(text).getInt("version")) assertEquals("oalinst_dll", JSONObject(text).getJSONObject("games").getJSONArray(key).getString(0)) @@ -219,10 +209,8 @@ class WinCompSourcesTest { val folder = tmp.newFolder("Shared") val exe = File(folder, "Shared.exe").apply { writeText("x") } val art = AddedGames.Game(folder, "Shared", exe, "/g/Shared.exe", "/g", 0x81234567L, 0L, emptyList()) - // The art fetch finds it by name ... val match = AddedGameArt.steamMatch(app, art) { 4242 } assertEquals(4242, match.appId) - // ... and the components read the same record. assertEquals(4242, SteamMatch.get(app, folder.path)!!.appId) assertEquals(SteamMatch.Certainty.NAME, SteamMatch.get(app, folder.path)!!.certainty) assertFalse(WinCompSources.forGame(app, game(folder, Library.ADDED)).any { it.auto }) @@ -268,7 +256,6 @@ class WinCompSourcesTest { @Test fun aSteamListedComponentNotYetDownloadedTurnsOnWhenItLands() = withFakeWorker { installedNow.add("d3dx9") val game = payback() - // The user had switched one off: it is never downloaded for an automatic pick. WinComponents.setUser(app, "1262580", "vcredist2019_dll", false) AutoComponents.queueGame(app, game, asked = true) assertEquals(listOf("vcredist2013_dll"), downloaded) @@ -276,7 +263,6 @@ class WinCompSourcesTest { assertEquals(setOf("d3dx9", "vcredist2013_dll", "vcredist2019_dll"), sel.auto.keys) assertEquals(setOf("d3dx9", "vcredist2013_dll"), WinComponents.picks(app, "1262580").toSet()) assertEquals("STEAM", sel.auto.getValue("vcredist2013_dll").kind) - // The list is kept for the page. assertEquals(3, AutoComponents.cachedFindings(app, "1262580")!!.count { it.auto }) } @@ -297,7 +283,6 @@ class WinCompSourcesTest { AutoComponents.unfavourable = { true } AutoComponents.queueGame(app, game, asked = false) assertTrue(downloaded.isEmpty()) - // The user opens the page: asked for, it goes. AutoComponents.queueGame(app, game, asked = true) assertEquals(3, downloaded.size) } @@ -311,7 +296,6 @@ class WinCompSourcesTest { assertEquals(listOf("g:1262580"), AutoComponents.queued()) session = false AutoComponents.drain(app) - // Each component once, though two games could want it. assertEquals(downloaded.distinct(), downloaded) } @@ -319,7 +303,6 @@ class WinCompSourcesTest { val game = payback() AutoComponents.online = { false } AutoComponents.queueGame(app, game, asked = true) - // The list is worked out offline; the downloads wait. assertTrue(downloaded.isEmpty()) assertTrue(AutoComponents.queued().all { it.startsWith("c:") }) AutoComponents.online = { true } @@ -327,7 +310,6 @@ class WinCompSourcesTest { assertEquals(3, downloaded.size) } - // ---- older store installs: the list read once from the store ---- @Test fun anOlderGogInstallGetsItsListFromTheStoreOnce() { val folder = tmp.newFolder("DOOM I Enhanced") @@ -338,7 +320,6 @@ class WinCompSourcesTest { try { assertTrue(StoreListBackfill.fill(app, folder)) assertEquals("MSVC2019_x64,DirectX", StoreGameSidecar.read(folder)!!.extra[WinCompSources.GOG_DEPENDENCIES]) - // Kept like a fresh install: not fetched again. assertFalse(StoreListBackfill.fill(app, folder)) assertEquals(1, fetches) assertEquals(listOf("vcredist2019" to WinCompSources.Origin.GOG, "d3dx9" to WinCompSources.Origin.GOG), auto(WinCompSources.forGame(app, game(folder, "gog")))) @@ -377,7 +358,6 @@ class WinCompSourcesTest { AutoComponents.online = { true } assertFalse(StoreListBackfill.fill(app, folder)) assertFalse(StoreListBackfill.fill(app, folder)) - // Signed out or the store refused: once, then a week's rest; the sidecar is untouched. assertEquals(1, fetches) assertTrue(StoreListBackfill.needs(StoreGameSidecar.read(folder)!!)) } finally { @@ -386,16 +366,13 @@ class WinCompSourcesTest { } } - // ---- apps the client never saw ---- @Test fun anAppTheClientLearnsLaterIsReadOnceItsCacheIsWritten() { val file = SteamAppInfo.appinfoFile(app) appinfo(file, 1) file.setLastModified(1_000_000_000_000L) assertEquals(null, SteamAppInfo.info(app, 1145360)) - // Unchanged file: the miss stands (no re-read). assertEquals(null, SteamAppInfo.info(app, 1145360)) - // The client wrote its cache with the app in it: read at once, not a week later. appinfo(file, 1145360) file.setLastModified(1_000_000_100_000L) assertEquals(listOf("228986", "228990"), SteamAppInfo.info(app, 1145360)!!.sharedDepots) @@ -418,7 +395,6 @@ class WinCompSourcesTest { assertTrue(StoreListBackfill.needs(StoreGameSidecar.read(older)!!)) } - // ---- a game installed during the session ---- private fun manifest(dir: File, appId: Int, state: Int, time: Long) = File(dir, "appmanifest_$appId.acf").apply { @@ -431,7 +407,6 @@ class WinCompSourcesTest { manifest(steamapps, 12900, 1026, 1_000_000L) manifest(steamapps, 570, 4, 1_000_000L) val watch = SessionInstalls.Watch { listOf(steamapps) } - // What was there when the session started is not new. assertTrue(watch.poll().isEmpty()) assertTrue(watch.poll().isEmpty()) manifest(steamapps, 12900, 4, 2_000_000L) @@ -448,14 +423,12 @@ class WinCompSourcesTest { gameUp = true AutoComponents.queueInSession(app, installed) assertTrue(downloaded.isEmpty()) - // Other work stays paused in the session. val other = game(tmp.newFolder("Other"), Library.ADDED) AutoComponents.queueGame(app, other, asked = false) gameUp = false AutoComponents.drain(app) assertEquals(3, downloaded.size) assertEquals(setOf("d3dx9", "vcredist2013_dll", "vcredist2019_dll"), WinComponents.picks(app, "1262580").toSet()) - // Nothing left pending for the launch to wait on; the other game still waits for the session's end. assertEquals(0, AutoComponents.Pending.read(app).length()) assertEquals(listOf("g:${AutoComponents.appKey(other)}"), AutoComponents.queued()) session = false diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/CardboardTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/CardboardTest.kt index c388569cb..a54bc216f 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/CardboardTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/CardboardTest.kt @@ -21,7 +21,6 @@ class CardboardTest { private fun assertVec(expected: FloatArray, actual: FloatArray) = assertArrayEquals(expected, actual, eps) - /** Rotation about a unit axis by an angle, as a quaternion (x, y, z, w). */ private fun quat(ax: Float, ay: Float, az: Float, deg: Double): FloatArray { val h = deg * PI / 360.0 val s = sin(h).toFloat() @@ -43,11 +42,8 @@ class CardboardTest { } @Test fun lensInsetLinesTheEyesUpWithTheLenses() { - // A 128 mm panel with 64 mm lenses: each viewport centre is 32 mm out, exactly under its lens. assertEquals(0f, Cardboard.lensInset(128f, 64f), eps) - // A wider 150 mm panel: viewport centres 37.5 mm out, lenses 32 mm: 5.5 mm of a 75 mm viewport inwards. assertEquals(5.5f / 75f, Cardboard.lensInset(150f, 64f), eps) - // A narrow panel puts the lens centres outside the viewport centres (negative inset). assertEquals(true, Cardboard.lensInset(110f, 64f) < 0f) assertEquals(0f, Cardboard.lensInset(0f, 64f), eps) assertEquals(0f, Cardboard.lensInset(150f, 0f), eps) @@ -62,7 +58,6 @@ class CardboardTest { assertEquals(SteamVr.Status.READY, SteamVr.parse(answer("ok" to true, "installed" to true, "driver" to true))) assertEquals(SteamVr.Status.RUNNING, SteamVr.parse(answer("ok" to true, "installed" to true, "driver" to true, "running" to true))) - // The helper itself failed: the agent still says ok. assertEquals(SteamVr.Status.UNAVAILABLE, SteamVr.parse(answer("ok" to true, "installed" to true, "driver" to true).put("exitCode", 1))) } @@ -75,7 +70,6 @@ class CardboardTest { } assertTrue(SteamVr.succeeded("start", answer(true, exitCode = 0))) assertTrue(SteamVr.succeeded("stop", answer(true, exitCode = 0))) - // The agent ran the request, but droiddeck-steamvr start failed (not installed, no driver). assertFalse(SteamVr.succeeded("start", answer(true, exitCode = 1))) assertFalse(SteamVr.succeeded("stop", answer(true, exitCode = 2))) assertFalse(SteamVr.succeeded("start", answer(true))) @@ -86,19 +80,14 @@ class CardboardTest { } @Test fun quaternionMatchesKnownRotations() { - // 90 degrees about z: x -> y. val m = matrix(quat(0f, 0f, 1f, 90.0)) assertVec(floatArrayOf(0f, 1f, 0f), HeadPose.apply(m, floatArrayOf(1f, 0f, 0f))) - // Rotation vector without w: computed from the other three. val q = HeadPose.quaternionFromRotationVector(floatArrayOf(0f, 0f, sin(PI / 4).toFloat())) assertEquals(cos(PI / 4).toFloat(), q[3], eps) } @Test fun recenterMakesTheCurrentViewStraightAhead() { - // Phone upright in landscape, screen facing the viewer, who faces north-east-ish. - // Upright device: screen up = world up; looking direction = the screen's -z. for (heading in listOf(0.0, 37.0, 140.0, -100.0)) { - // Start from "screen faces south, viewer looks north": device x = east, y = up, z = south. val upright = floatArrayOf(1f, 0f, 0f, 0f, 0f, -1f, 0f, 1f, 0f) val yaw = matrix(quat(0f, 0f, 1f, heading)) val screenToWorld = HeadPose.multiply(yaw, upright) @@ -108,7 +97,6 @@ class CardboardTest { @Test fun recenterKeepsPitchButDropsHeading() { val upright = floatArrayOf(1f, 0f, 0f, 0f, 0f, -1f, 0f, 1f, 0f) - // Looking 30 degrees down, then turning 50 degrees: after a recentre the view is still 30 degrees down. val pitched = HeadPose.multiply(upright, matrix(quat(1f, 0f, 0f, -30.0))) val turned = HeadPose.multiply(matrix(quat(0f, 0f, 1f, 50.0)), pitched) val pose = HeadPose.relative(HeadPose.recenter(turned), turned) @@ -121,23 +109,18 @@ class CardboardTest { @Test fun turningTheHeadTurnsTheView() { val upright = floatArrayOf(1f, 0f, 0f, 0f, 0f, -1f, 0f, 1f, 0f) val ref = HeadPose.recenter(upright) - // Turn the head 90 degrees to the left (counter-clockwise seen from above). val turned = HeadPose.multiply(matrix(quat(0f, 0f, 1f, 90.0)), upright) val forward = HeadPose.apply(HeadPose.relative(ref, turned), floatArrayOf(0f, 0f, -1f)) - // The view now points to the left of the recentred forward direction: -x. assertVec(floatArrayOf(-1f, 0f, 0f), forward) } @Test fun heldFlatTheViewStartsStraightAhead() { - // Screen facing up (device on a table, held like a tray, or lying back looking up at it). for (m in listOf(HeadPose.IDENTITY, HeadPose.multiply(matrix(quat(0f, 0f, 1f, 30.0)), matrix(quat(1f, 0f, 0f, 20.0))))) { assertVec(HeadPose.IDENTITY, HeadPose.relative(HeadPose.recenter(m), m)) } } @Test fun heldFlatTurningAtTheHipsPans() { - // Held flat, then the body turns 40 degrees to the left about the real vertical: the view - // turns left about its own up axis - it must not roll (the bug: yaw came out as a tilt). val flat = HeadPose.IDENTITY val ref = HeadPose.recenter(flat) val turned = HeadPose.multiply(matrix(quat(0f, 0f, 1f, 40.0)), flat) @@ -146,7 +129,6 @@ class CardboardTest { val up = HeadPose.apply(pose, floatArrayOf(0f, 1f, 0f)) assertVec(floatArrayOf(-sin(PI * 40 / 180).toFloat(), 0f, -cos(PI * 40 / 180).toFloat()), forward) assertVec(floatArrayOf(0f, 1f, 0f), up) - // Tipping the top edge down (pitching the tray towards you) looks down. val tipped = HeadPose.multiply(flat, matrix(quat(1f, 0f, 0f, -20.0))) val down = HeadPose.apply(HeadPose.relative(ref, tipped), floatArrayOf(0f, 0f, -1f)) assertEquals(-sin(PI / 9).toFloat(), down[1], eps) @@ -154,7 +136,6 @@ class CardboardTest { @Test fun fieldOfViewComesFromThePanelSeenThroughTheLens() { val v2 = Cardboard.Viewer.CARDBOARD_V2 - // A 74.7 mm tall panel 39.3 mm behind the lens: atan(37.35 / 39.3) = 43.5 degrees each way. assertEquals(87.07f, Cardboard.fovDeg(v2, 74.7f), 0.05f) assertEquals(v2.fallbackFovDeg, Cardboard.fovDeg(v2, 0f), eps) assertEquals(Cardboard.Viewer.NONE.fallbackFovDeg, Cardboard.fovDeg(Cardboard.Viewer.NONE, 74.7f), eps) @@ -165,11 +146,9 @@ class CardboardTest { val n = sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]); floatArrayOf(q[0] / n, q[1] / n, q[2] / n, q[3] / n) }) assertVec(r, HeadPose.toScreen(r, 0)) - // ROTATION_90 (as input/PadMotion maps it): screen x is the device's -y, screen y the device's x. val s90 = HeadPose.toScreen(r, 1) assertVec(HeadPose.apply(r, floatArrayOf(0f, -1f, 0f)), HeadPose.apply(s90, floatArrayOf(1f, 0f, 0f))) assertVec(HeadPose.apply(r, floatArrayOf(1f, 0f, 0f)), HeadPose.apply(s90, floatArrayOf(0f, 1f, 0f))) - // ROTATION_270 is the opposite turn; four quarter turns of 90 give the original back. val s270 = HeadPose.toScreen(r, 3) assertVec(HeadPose.apply(r, floatArrayOf(0f, 1f, 0f)), HeadPose.apply(s270, floatArrayOf(1f, 0f, 0f))) assertVec(r, HeadPose.toScreen(HeadPose.toScreen(HeadPose.toScreen(HeadPose.toScreen(r, 1), 1), 1), 1)) diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt index 434e87d09..6082d3ddf 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt @@ -34,7 +34,6 @@ class DriverBundleTest { SessionPrefs.setLinuxDriver(context, "") } - /** Enough of an AArch64 ELF for the importers: the header they read, then the libc they look for. */ private fun library(libc: String) = ByteArray(64).also { byteArrayOf(0x7f, 'E'.code.toByte(), 'L'.code.toByte(), 'F'.code.toByte(), 2, 1).copyInto(it) it[18] = 0xB7.toByte() diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt index b5d632fdc..ba2a53f4e 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt @@ -60,7 +60,6 @@ class DriverPairsTest { listOf( asset("Turnip-r4.zip", "r4", false, DriverPairs.BANNER), asset("Turnip-r4-Linux.zip", "r4", true, DriverPairs.BANNER), - // The A8xx set's newest display build is r4, but its Linux half is only in r3. asset("Turnip-r4-A8xx.zip", "r4", false, DriverPairs.BANNER_A8XX), asset("Turnip-r3-A8xx-Linux.zip", "r3", true, DriverPairs.BANNER_A8XX), asset("WN-Turnip-1.19-b_Axxx.zip", "v1.19", false, DriverPairs.WN_BALANCED), diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt index 10a0fd971..0ce3bdc6c 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt @@ -5,7 +5,6 @@ import org.junit.Test class GpuModelFallbackTest { @Test fun aPlatformCodeNameGivesItsAdreno() { - // AYANEO's Pocket FIT: KGSL says "Adreno33v2", the platform says pineapple. assertEquals(750, GpuInfo.platformModel("pineapple")) assertEquals(740, GpuInfo.platformModel("KALAMA")) assertEquals(830, GpuInfo.platformModel("sun")) diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt index d90e261d0..8919b79a7 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt @@ -8,7 +8,6 @@ class SocNamesTest { private fun props(vararg pairs: Pair<String, String>): (String) -> String = { pairs.toMap()[it].orEmpty() } @Test fun theModelIsFoundWhereVendorsPutIt() { - // AYANEO Pocket FIT: ro.soc.model blank, the vendor property set, ro.fota.platform a branch name. assertEquals("SG8350P", SocNames.model(props("ro.vendor.qti.soc_model" to "SG8350P", "ro.fota.platform" to "MSM_13.0"))) assertEquals("QCS8550", SocNames.model(props("ro.soc.model" to "QCS8550", "ro.vendor.qti.soc_model" to "other"))) assertEquals("", SocNames.model(props("ro.fota.platform" to "MSM_13.0"))) diff --git a/app/src/test/java/com/droiddeck/launcher/input/FakeInputRingPathTest.kt b/app/src/test/java/com/droiddeck/launcher/input/FakeInputRingPathTest.kt index d1d2b8d49..e0199e6ca 100644 --- a/app/src/test/java/com/droiddeck/launcher/input/FakeInputRingPathTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/input/FakeInputRingPathTest.kt @@ -7,12 +7,6 @@ import org.junit.Assert.* import org.junit.Before import org.junit.Test -/** - * The ring paths handed to the guest (FAKE_EVDEV_MEMFD_PATHS) must keep the spelling the session - * tree is bound under. On ColorOS 16 /data/user/0 resolves to /data/data, the guest only has the - * first bound, and a canonical export left every pad dead (#166). A symlinked directory stands in - * for that here. - */ class FakeInputRingPathTest { private lateinit var base: File private lateinit var real: File @@ -37,7 +31,6 @@ class FakeInputRingPathTest { @Test fun theExportKeepsTheBoundSpellingThroughALinkedDataDirectory() { val fakeInputDir = File(link, "files/session/dev/input").apply { mkdirs() } val ring = FakeInputRingPaths.ringFile(fakeInputDir, 0)!!.apply { parentFile!!.mkdirs(); writeText("") } - // The link really does resolve elsewhere, as /data/user/0 does on the affected devices. assertTrue(ring.canonicalPath.startsWith(real.canonicalPath + "/")) val exported = FakeInputRingPaths.exportPath(ring) assertEquals(File(link, "files/session/dev/fakeinput-rings/ring0").path, exported) diff --git a/app/src/test/java/com/droiddeck/launcher/input/PadBridgeRoutingTest.kt b/app/src/test/java/com/droiddeck/launcher/input/PadBridgeRoutingTest.kt index f3f0e64b0..8ff62e170 100644 --- a/app/src/test/java/com/droiddeck/launcher/input/PadBridgeRoutingTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/input/PadBridgeRoutingTest.kt @@ -148,8 +148,6 @@ class PadBridgeRoutingTest { private companion object { const val COMPOSITE = 0x1002313 } - // Robolectric's default InputEvent shadow has its own nullable device field. Use Android's - // original lookup so key and motion events resolve the registered device by id. @Implements(KeyEvent::class) class RegisteredKeyEvent : ShadowKeyEvent() { @RealObject private lateinit var event: KeyEvent diff --git a/app/src/test/java/com/droiddeck/launcher/input/SteamTouchConfigTest.kt b/app/src/test/java/com/droiddeck/launcher/input/SteamTouchConfigTest.kt index 51c6742cd..8addf24e0 100644 --- a/app/src/test/java/com/droiddeck/launcher/input/SteamTouchConfigTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/input/SteamTouchConfigTest.kt @@ -6,7 +6,6 @@ import org.junit.Assert.assertTrue import org.junit.Test class SteamTouchConfigTest { - // A layout DroidDeck saved for Geometry Wars on the Thor, as Steam keeps it. private val saved = "0aec0310012218080010011d0000703d255555d53d2d0000803f350000803f2218080110011d6666fe3e255555d53d2d0000803f350000803f2218080210011dba490c3e255c8f423f2d0000803f350000803f2218080410011d9a99593f255c8f423f2d0000803f350000803f2218080510011d8fc2353f25ae47613f2d0000803f350000803f2218080610011d2b87163e25d7a3f03e2d0000803f350000803f2218080710011dba49003f25f958053f2d293ab23f35293ab23f2218080810011d9a99683f25d227dd3e2d0000803f350000803f2218080910011d9a99483f25d227dd3e2d0000803f350000803f2218080a10011d9a99583f254444a43e2d0000803f350000803f2218080b10011d6666ce3e255555d53d2d0000803f350000803f2218080c10011d3333173f255555d53d2d0000803f350000803f2218080d10011d0ad7233e2585eb513e2d0000803f350000803f2218080e10011d3d0a573f2585eb513e2d0000803f350000803f2218080f10011d8fc2753d2585eb513e2d0000803f350000803f2218081010011dd7a3703f2585eb513e2d0000803f350000803f2218081c10011d0000713f255555d53d2d0000803f350000803f2218080310001de17a943e25ae47613f2d0000803f350000803f2a140d0000803f150000803f1d0000803f25cdcccc3e1002" @Test diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/AppImageManagerTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/AppImageManagerTest.kt index 5f753715f..ab620b385 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/AppImageManagerTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/AppImageManagerTest.kt @@ -16,7 +16,6 @@ class AppImageManagerTest { @Before fun setUp() { dir = Files.createTempDirectory("appimage").toFile() } @After fun tearDown() { dir.deleteRecursively() } - /** An ELF header with the AppImage magic at byte 8 and [machine] at byte 18. */ private fun image(machine: Int, type: Int = 2, magic: Boolean = true): File { val b = ByteArray(64) b[0] = 0x7f; b[1] = 'E'.code.toByte(); b[2] = 'L'.code.toByte(); b[3] = 'F'.code.toByte() diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt index e1e9fd9e9..e1e6827ab 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt @@ -68,7 +68,6 @@ class BwrapSpawnerTest { } } -/** The sandbox a Flatpak request describes, built on disk under a temporary directory. */ @RunWith(RobolectricTestRunner::class) class BwrapSpawnerPlanTest { private lateinit var dir: File @@ -76,7 +75,6 @@ class BwrapSpawnerPlanTest { @Before fun setUp() { dir = Files.createTempDirectory("bwrap-plan").toFile() - // Flatpak names the runtime by its path inside the rootfs. runtime = File(LinuxRuntime.rootDir(RuntimeEnvironment.getApplication()), "runtime-files") File(runtime, "lib").mkdirs() } @@ -107,18 +105,14 @@ class BwrapSpawnerPlanTest { op("t" to "bind", "src" to "/nonexistent-${System.nanoTime()}", "dest" to "/run/x", "try" to true), ), dir) - // The runtime is bound at /usr, not written into. assertTrue(plan.binds.any { it.guest == "/usr" && File(it.host).canonicalPath == runtime.canonicalPath }) assertFalse(File(runtime, "lib/GL").exists()) - // The tmpfs under /usr is a directory of ours, and the link Flatpak makes inside it lands there. val gl = plan.binds.single { it.guest == "/usr/lib/GL" } assertTrue(gl.host.startsWith(dir.path)) assertEquals("default/lib/libEGL.so", Files.readSymbolicLink(File(gl.host, "libEGL.so").toPath()).toString()) - // Top-level links, directories and files are made in the sandbox root itself. assertEquals("usr/lib", Files.readSymbolicLink(File(plan.root, "lib").toPath()).toString()) assertTrue(File(plan.root, "tmp").isDirectory) assertEquals("[Application]\nname=org.example.Game\n", File(plan.root, ".flatpak-info").readText()) - // A --bind-try whose source is missing is left out. assertFalse(plan.binds.any { it.guest == "/run/x" }) assertEquals(listOf("/app/bin/game"), plan.argv) assertEquals("/app", plan.cwd) diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/DeckyPluginInstallTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/DeckyPluginInstallTest.kt index 5edb3451d..080828ba9 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/DeckyPluginInstallTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/DeckyPluginInstallTest.kt @@ -69,7 +69,6 @@ class DeckyPluginInstallTest { val plugins = temporary.newFolder() val old = plugin(plugins, "Unifideck", "Unifideck") File(old, "old-version").writeText("keep") - // An empty staged directory makes the plugin activation fail after backing up the old one. val staged = plugin(temporary.newFolder(), "Unifideck", "Unifideck") staged.deleteRecursively() val settings = File(temporary.newFolder(), "loader.json").apply { writeText("{}") } diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt index ce129f7c0..5d4eb1d8e 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt @@ -9,7 +9,6 @@ class ProotFastPathTest { private val root = File("/data/data/com.droiddeck.launcher/files/linuxfs") @Test fun aSessionsUsualBindsGetAKey() { - // 87 on a device with two Games folders: past the old limit of 64, which dropped them. val binds = (1..87).map { "/host/$it:/guest/$it" } assertNotNull(ProotFastPath.key(root, binds)) } diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalTest.kt index 178e34d29..318e6b980 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalTest.kt @@ -107,7 +107,6 @@ class RuntimeRemovalTest { follower.join(5000) assertEquals(true, result.get()) assertEquals(true, followerResult.get()) - // The last stage is "Linux runtime removed", the only one reported at 100. assertEquals(100, percents.last()) assertFalse(root.exists()) assertFalse(File(dir, "linuxfs.removing").exists()) @@ -131,7 +130,6 @@ class RuntimeRemovalTest { val root = runtime() try { val removal = LinuxRuntimeInstaller.beginUninstall(null, root)!! - // The first stage names the removal, the second counts its first entry. var stages = 0 assertFalse(removal.run { _, _ -> if (++stages == 2) @@ -170,12 +168,10 @@ class RuntimeRemovalTest { assertNull(LinuxRuntimeInstaller.removalError()) assertFalse(root.exists()) assertFalse(File(dir, "linuxfs.removing").exists()) - // Only the undeletable entry and its parents remain, out of the way. val trash = File(dir, LinuxRuntimeInstaller.TRASH) assertEquals(listOf(".steampath"), trash.walk().filter { it.isFile }.map { it.name }.toList()) assertEquals(setOf(LinuxRuntimeInstaller.TRASH), dir.list()!!.toSet()) - // Once it can be deleted, the next removal clears the trash too. RuntimeFileTree.refuseForTest = null assertTrue(LinuxRuntimeInstaller.beginUninstall(null, root)!!.run(null)) assertFalse(trash.exists()) diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalUiTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalUiTest.kt index 748549560..8f690302d 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalUiTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalUiTest.kt @@ -35,7 +35,6 @@ class RuntimeRemovalUiTest { try { val activity = Robolectric.buildActivity(SessionActivity::class.java).create() assertTrue(activity.get().isFinishing) - // Even lifecycle callbacks delivered after finish must not touch uninitialised UI. activity.start().resume().pause().stop().destroy() assertFalse(SessionState.running) assertEquals(SessionPhase.IDLE, SessionState.phase) diff --git a/app/src/test/java/com/droiddeck/launcher/session/BatteryComponentTest.kt b/app/src/test/java/com/droiddeck/launcher/session/BatteryComponentTest.kt index fd820e22b..05c17988e 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/BatteryComponentTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/BatteryComponentTest.kt @@ -61,7 +61,6 @@ class BatteryComponentTest { val discovered = supplies.listFiles()!!.filter { it.name.contains("BAT") } assertEquals(listOf("BAT1"), discovered.map { it.name }) fun read(name: String) = File(discovered.single(), name).readText().trim().toLong() - // One 4 Ah battery at 4 V drawing 2 A: 8 W and two hours, regardless of vendor current sign. assertEquals(8_000_000L, read("power_now")) assertEquals(16_000_000L, read("energy_now")) assertEquals(7200L, read("time_to_empty_now")) diff --git a/app/src/test/java/com/droiddeck/launcher/session/GameEnvironmentStoreTest.kt b/app/src/test/java/com/droiddeck/launcher/session/GameEnvironmentStoreTest.kt index d499437e0..6645c6ee1 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/GameEnvironmentStoreTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/GameEnvironmentStoreTest.kt @@ -60,7 +60,6 @@ class GameEnvironmentStoreTest { "d3d9.samplerAnisotropy = 16; d3d11.samplerAnisotropy = 16; d3d9.samplerLodBias = -0.58; d3d11.samplerLodBias = -0.58", json.getString(GameEnvironmentStore.DXVK_CONFIG), ) - // The user's own entry is untouched; the launcher appends the options at launch. assertEquals("dxvk.tearFree = True", json.getJSONObject("shared").getString("DXVK_CONFIG")) assertEquals(GameEnvironment.Config(shared = mapOf("DXVK_CONFIG" to "dxvk.tearFree = True")), GameEnvironmentStore.read(context)) SessionPrefs.setTextureAnisotropy(context, 0) diff --git a/app/src/test/java/com/droiddeck/launcher/session/ProtonExtrasTest.kt b/app/src/test/java/com/droiddeck/launcher/session/ProtonExtrasTest.kt index e551d3052..a2a528f1e 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/ProtonExtrasTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/ProtonExtrasTest.kt @@ -102,7 +102,6 @@ class ProtonExtrasTest { assertTrue(ProtonExtras.compareVersions("DroidDeck-Proton 11-7", "DroidDeck-Proton 11-7.1") < 0) assertTrue(ProtonExtras.compareVersions("cachyos-11.0-20260703-slr", "cachyos-11.0-20261005-slr") < 0) assertTrue(ProtonExtras.compareVersions("cachyos-10.0-sunset-slr", "cachyos-11.0-20260601-slr") < 0) - // The architecture in a folder name is not part of its version. assertEquals(listOf(11L, 7L), ProtonExtras.versionKey("GE-Proton11-7-aarch64")) assertEquals(listOf(11L, 0L, 20261005L), ProtonExtras.versionKey("proton-cachyos-11.0-20261005-slr-arm64")) assertEquals(listOf(11L, 7L, 5L), ProtonExtras.versionKey("DroidDeck-Proton 11-7.5")) @@ -114,9 +113,8 @@ class ProtonExtrasTest { val tools = temp.newFolder("compatibilitytools.d") for (name in listOf("GE-Proton11-10", "GE-Proton11-9", "DroidDeck-Proton 11-7.10", "DroidDeck-Proton 11-7.5", "droiddeck-proton-arm64", "droiddeck-proton-11-arm64", "proton-cachyos-11.0-20261005-slr-arm64")) installTool(tools, name) - File(tools, "DroidDeck-Proton 11-6").mkdirs() // no manifest: half unpacked, not installed + File(tools, "DroidDeck-Proton 11-6").mkdirs() assertEquals(listOf("GE-Proton11-9", "GE-Proton11-10"), ProtonExtras.installedIn(tools, tool("ge"))) - // Ours, not DroidDeck's own droiddeck-proton-* tools: the prefix is matched with its case. assertEquals(listOf("DroidDeck-Proton 11-7.5", "DroidDeck-Proton 11-7.10"), ProtonExtras.installedIn(tools, tool("droiddeck"))) assertEquals(listOf("proton-cachyos-11.0-20261005-slr-arm64"), ProtonExtras.installedIn(tools, tool("cachyos"))) assertEquals(emptyList<String>(), ProtonExtras.installedIn(File(tools, "missing"), tool("ge"))) @@ -149,13 +147,11 @@ class ProtonExtrasTest { .put("cachyos", JSONObject().put("tag", "cachyos-11.0-20261005-slr").put("at", now - 60_000)) .put("droiddeck", JSONObject().put("tag", "DroidDeck-Proton-11-7.6").put("at", now - ProtonExtras.CHECK_EVERY_MS + 60_000)) File(context.filesDir, "proton-latest.json").writeText(latest.toString()) - // Every source was looked up within the last six hours: nothing goes to GitHub. assertFalse(ProtonExtras.checkLatest(context, now = now)) assertEquals("DroidDeck-Proton-11-7.6", ProtonExtras.update(context, tool("droiddeck"))) assertNull(ProtonExtras.update(context, tool("ge"))) assertNull(ProtonExtras.update(context, tool("cachyos"))) assertEquals(listOf("DroidDeck-Proton 11-7.5"), ProtonExtras.installedTags(context, tool("droiddeck"))) - // The tag recorded at install stands for the folder. File(context.filesDir, "proton-extras.json").writeText(JSONObject().put("DroidDeck-Proton 11-7.5", "DroidDeck-Proton-11-7.6").toString()) assertEquals(listOf("DroidDeck-Proton-11-7.6"), ProtonExtras.installedTags(context, tool("droiddeck"))) assertNull(ProtonExtras.update(context, tool("droiddeck"))) @@ -168,7 +164,6 @@ class ProtonExtrasTest { for (name in listOf("DroidDeck-Proton 11-7.5", "DroidDeck-Proton 11-7.6", "DroidDeck-Proton 11-7.10", "GE-Proton11-7")) installTool(tools, name) ProtonExtras.remove(context, tool("droiddeck"), "DroidDeck-Proton 11-7.6") assertEquals(listOf("DroidDeck-Proton 11-7.5", "DroidDeck-Proton 11-7.10"), ProtonExtras.installedAll(context, tool("droiddeck"))) - // A folder of another source is never removed through this one. ProtonExtras.remove(context, tool("droiddeck"), "GE-Proton11-7") assertTrue(File(tools, "GE-Proton11-7").isDirectory) assertEquals(listOf("DroidDeck-Proton 11-7.5", "DroidDeck-Proton 11-7.10"), ProtonExtras.installedAll(context, tool("droiddeck"))) diff --git a/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt b/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt index 3f19a2a2f..add675539 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt @@ -36,7 +36,6 @@ class QuestExperimentalTest { @Test fun quest2SelectsA6xxInsteadOfQuest3DriverAndPreservesLaterChoices() { ShadowBuild.setModel("Quest 2") - // A prior Quest 3 flag must not suppress Quest 2's own migration. context.getSharedPreferences("session", Context.MODE_PRIVATE).edit() .putBoolean("questDefaultsV1", true).commit() QuestExperimental.initialize(context) diff --git a/app/src/test/java/com/droiddeck/launcher/session/RumbleComponentTest.kt b/app/src/test/java/com/droiddeck/launcher/session/RumbleComponentTest.kt index 55bb4f0d1..b388c1a53 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/RumbleComponentTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/RumbleComponentTest.kt @@ -164,7 +164,6 @@ class RumbleComponentTest { } private fun effect(strong: Int = 65535, weak: Int = 65535, slot: Int = 0) { - // Inject a decoded force-feedback packet; transport is outside these tests. val int = Int::class.javaPrimitiveType RumbleComponent::class.java.getDeclaredMethod("buzz", int, int, int, int) .apply { isAccessible = true }.invoke(rumble, strong, weak, 5000, slot) diff --git a/app/src/test/java/com/droiddeck/launcher/session/SessionDisplayTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SessionDisplayTest.kt index 1c26f7814..a68458125 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SessionDisplayTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SessionDisplayTest.kt @@ -12,16 +12,12 @@ class SessionDisplayTest { } @Test fun heightPresetsKeepThePanelShapeWithASixteenByNineFloor() { - // 20:9 phone: edge to edge, no bars for the client's interface. assertEquals(1600 to 720, SessionDisplay.resolveChoice(2400 to 1080, SessionDisplay.DEFAULT_RESOLUTION)) assertEquals(1608 to 720, SessionDisplay.resolveChoice(2412 to 1080, "720p")) - // 1440p phone: bounded by the preset height, not the panel. assertEquals(1560 to 720, SessionDisplay.resolveChoice(3120 to 1440, "720p")) assertEquals(2340 to 1080, SessionDisplay.resolveChoice(3120 to 1440, "1080p")) - // Foldable inner panel and 4:3 handhelds: never narrower than 16:9. assertEquals(1280 to 720, SessionDisplay.resolveChoice(2208 to 1840, "720p")) assertEquals(1280 to 720, SessionDisplay.resolveChoice(1440 to 1080, "720p")) - // Never taller than the panel. assertEquals(1280 to 720, SessionDisplay.resolveChoice(1280 to 720, "1080p")) } @@ -39,10 +35,8 @@ class SessionDisplayTest { } @Test fun unfoldedControlsKeepTheGameSixteenByNineAcrossTheWidth() { - // Fold inner screen held as a tablet, and upright: 16:9 either way, the controls the rest. assertEquals(1376, SessionDisplay.unfoldedGameHeight(2448, 1848)) assertEquals(1038, SessionDisplay.unfoldedGameHeight(1848, 2448)) - // A window near 16:9 itself still leaves the controls a fifth of it. assertEquals(864, SessionDisplay.unfoldedGameHeight(1920, 1080)) } diff --git a/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareTest.kt index 1d58f179f..3e5a71d12 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareTest.kt @@ -27,7 +27,6 @@ class SessionLogShareTest { z.entries().toList().associate { it.name.substringAfter('/') to z.getInputStream(it).bufferedReader().readText() } } - /** A record as the session's ending writes it, listing [files] as they stand now. */ private fun record(folder: File, vararg files: String, rules: Int = LogRedactor.RULES_VERSION) { File(folder, SessionArtifacts.SCRUBBED_TREE_MARKER).writeText( "rules $rules\n" + files.joinToString("") { val f = File(folder, it); "${f.length()}\t${f.lastModified()}\t$it\n" }) @@ -35,8 +34,6 @@ class SessionLogShareTest { @Test fun aRecordedUnchangedFileIsScrubbedAgainOnTheWayIntoTheZip() { val folder = tmp.newFolder("2026-10-06-01-steam") - // A file the ending recorded as scrubbed still gets the share's final pass: the record - // may predate a rule, and a zip is the one way a log leaves the device. File(folder, "session.log").writeText("$secret\n") record(folder, "session.log") assertFalse(zip(folder).getValue("session.log").contains(leaked)) @@ -50,7 +47,6 @@ class SessionLogShareTest { File(folder, "steam/deep/nested.txt").writeText("$secret\n") File(folder, ".hidden.txt").writeText("$secret\n") record(folder, "app.log", "session.log") - // Appended to after the ending (the app's log capture), and touched with an older time. File(folder, "app.log").appendText("$secret\n") File(folder, "session.log").apply { writeText("$secret\n".padEnd(3, ' ')); setLastModified(1_000L) } File(folder, "events.jsonl").writeText("$secret\n") @@ -109,7 +105,6 @@ class SessionLogShareTest { @Test fun aFileThatCouldNotBeScrubbedLeavesTheFolderUnrecorded() { val folder = tmp.newFolder("2026-10-06-06-steam") File(folder, "session.log").writeText("$secret\n") - // A folder the scrubbed copy cannot be written into: the scrub fails, the original stays. folder.setWritable(false) try { SessionArtifacts.scrubAndMark(folder) diff --git a/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareZipTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareZipTest.kt index 81f230d66..6ee341bc8 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareZipTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareZipTest.kt @@ -12,7 +12,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** A shared zip is scrubbed on the way in, whatever the files on disk still hold. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class SessionLogShareZipTest { @@ -21,7 +20,6 @@ class SessionLogShareZipTest { @Test fun everyTextFileInTheZipIsScrubbedAndTheStoresAndToolsLogsComeAlong() { val logs = LinuxRuntime.logDir(app) val session = File(logs, "2026-10-08-01-steam").apply { mkdirs() } - // Written raw, as a file from before a rule existed would be. File(session, "session.log").writeText( "fetch https://gog-cdn-fastly.gog.com/token=nva=1791500000~dirs=/x~token=0a1b2c3d4e5f/content-system/v2/store/1/ab/cd/f\n" + "fetch https://epicgames-download1.akamaized.net/Builds/Org/a/ChunksV4/12/ABCD.chunk?__token__=exp=1~hmac=deadbeefcafe\n" + @@ -40,7 +38,6 @@ class SessionLogShareZipTest { for (secret in listOf("0a1b2c3d4e5f", "deadbeefcafe", "abcdef123456", "someone@example.com", "AUTHCODE123", "abc.def.ghi", "zzzzzzzz12", "eyabc")) { assertFalse("$secret survived", all.contains(secret)) } - // What makes a log worth reading stays. assertTrue(all.contains("kept: EResult 5, app 1086940, steamapps/common/Game")) assertTrue(all.contains("https://gog-cdn-fastly.gog.com/")) @@ -53,9 +50,6 @@ class SessionLogShareZipTest { assertEquals(line, com.droiddeck.launcher.core.LogRedactor.redactForShare(line)) } - // Folders an earlier build scrubbed and marked under older rules, their copied Steam UI log - // still naming the account: one app start (the move out of Download/, then the pass over - // older folders) leaves every one scrubbed on disk under the current rules. private fun olderFolder(parent: File, name: String, marker: String = ".scrubbed-2"): File { val dir = File(parent, name).apply { mkdirs() } File(dir, marker).writeText("scrubbed earlier\n") @@ -91,12 +85,10 @@ class SessionLogShareZipTest { val legacy = LinuxRuntime.legacyLogDir().apply { mkdirs() } val names = (1..9).map { "2026-10-0${it}-01-steam" } names.forEachIndexed { i, name -> - // Old markers of every kind an earlier build left, one even claiming the current rules. olderFolder(legacy, name, marker = listOf(".scrubbed-2", ".scrubbed-1", ".scrubbed-r${com.droiddeck.launcher.core.LogRedactor.RULES_VERSION}")[i % 3]) } File(legacy, "Saves").apply { mkdirs() }.resolve("keep.txt").writeText("save\n") - // What MainActivity runs at start. LogMigration.run(app) SessionArtifacts.scrubOlder(app) diff --git a/app/src/test/java/com/droiddeck/launcher/session/SteamDownloadMonitorTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SteamDownloadMonitorTest.kt index 86cf32009..28d5a7720 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SteamDownloadMonitorTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SteamDownloadMonitorTest.kt @@ -79,13 +79,11 @@ class SteamDownloadMonitorTest { var now = 1_000L assertTrue(monitor.poll(root, now, received)) - // Five minutes of a download Steam no longer logs, at 4 MB/s. while (now < 300_000L) { now += 2_000L received += 8_000_000L assertTrue(monitor.poll(root, now, received)) } - // Traffic stops: the session may suspend once the grace runs out. assertTrue(monitor.poll(root, now + 90_000L, received)) assertFalse(monitor.poll(root, now + 92_001L, received)) diff --git a/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt index 8046e7794..0c2a990d8 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt @@ -42,7 +42,6 @@ class SteamLanguageTest { assertEquals("latam", steam("es-MX")) assertEquals("latam", steam("es-419")) assertEquals("spanish", steam("es-ES")) - // The app's Spanish has no region; the system's says where the player is. assertEquals("latam", steam("es", region = "AR")) assertEquals("spanish", steam("es", region = "ES")) } diff --git a/app/src/test/java/com/droiddeck/launcher/session/WinComponentsSupportTest.kt b/app/src/test/java/com/droiddeck/launcher/session/WinComponentsSupportTest.kt index 31dad07df..656c56c9a 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/WinComponentsSupportTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/WinComponentsSupportTest.kt @@ -23,7 +23,6 @@ class WinComponentsSupportTest { @Test fun windowsInstallerPackagesInstallHere() { assertEquals(Support.READY, support(component("xna40", step("install_msi", "url" to "$base/xna40.msi")))) - // The catalog names some .msi packages install_exe. assertEquals(Support.READY, support(component("XLiveRedist", step("install_exe", "url" to "$base/XLiveRedist.msi", "file_name" to "XLiveRedist1.2.0238.msi")))) assertEquals(Support.READY, support(component("msxml3", step("delete_dlls", "dest" to "win32"), @@ -33,12 +32,10 @@ class WinComponentsSupportTest { } @Test fun installerWrappersInstallHere() { - // A WiX bundle, a self-extracting cabinet or 7-Zip archive: the packages inside are laid out. assertEquals(Support.READY, support(component("vcredist2022", step("install_exe", "url" to "$base/vcredist2022__VC_redist.x86.exe", "file_name" to "VC_redist.x86.exe"), step("install_exe", "url" to "$base/vcredist2022__VC_redist.x64.exe", "file_name" to "VC_redist.x64.exe"), step("override_dll", "dll" to "vcruntime140", "type" to "native,builtin")))) - // .NET Framework: "uninstall Wine Mono", the Windows version, the installer, a registry key. assertEquals(Support.READY, support(component("dotnet40", step("uninstall", "file_name" to "Wine Mono"), step("set_windows", "version" to "win7"), step("install_exe", "url" to "https://download.microsoft.com/x/dotNetFx40_Full_x86_x64.exe", "file_name" to "dotNetFx40_Full_x86_x64.exe"), @@ -48,7 +45,6 @@ class WinComponentsSupportTest { } @Test fun nestedCatalogKeysAreRead() { - // A few .NET entries keep url and file_name under "environment". val nested = WinComponents.Step("install_exe", JSONObject().put("environment", JSONObject() .put("WINEDLLOVERRIDES", "fusion=b").put("file_name", "ndp48-x86-x64-allos-enu.exe") .put("url", "https://download.visualstudio.microsoft.com/x/ndp48-x86-x64-allos-enu.exe"))) @@ -67,14 +63,11 @@ class WinComponentsSupportTest { @Test fun recordedInstallersInstallHere() { val snapshot = "https://github.com/Droid-Deck/DroidDeck-Components/releases/download/component-snapshots-v1/K-Lite.snapshot.json" - // A setup program with a recording of its result installs from the recording. val klite = WinComponents.Component("K-Lite", "", "", "ready", emptyList(), listOf(step("install_exe", "url" to "$base/K-Lite.exe", "file_name" to "K-Lite_1960.exe")), snapshot) assertEquals(Support.READY, WinComponents.support(klite, mapOf(klite.name to klite))) - // Only a recording on the release counts. val elsewhere = WinComponents.Component("K-Lite", "", "", "ready", emptyList(), klite.steps, "http://example.com/K-Lite.snapshot.json") assertEquals(Support.NEEDS_INSTALLER, WinComponents.support(elsewhere, mapOf(elsewhere.name to elsewhere))) - // Its prerequisites still have to be installable. val sp1 = WinComponents.Component("dotnet20sp1", "", "", "ready", listOf("dotnet20"), emptyList(), snapshot.replace("K-Lite", "dotnet20sp1")) val base20 = WinComponents.Component("dotnet20", "", "", "ready", emptyList(), listOf(step("install_exe", "url" to "$base/dotnet20__dotnetfx.exe", "file_name" to "dotnetfx.exe"))) @@ -82,7 +75,6 @@ class WinComponentsSupportTest { } @Test fun setupProgramsStillWait() { - // NSIS / InnoSetup installers hold no package to lay out. assertEquals(Support.NEEDS_INSTALLER, support(component("K-Lite", step("install_exe", "url" to "$base/K-Lite.exe", "file_name" to "K-Lite_1960.exe")))) assertEquals(Support.NEEDS_INSTALLER, support(component("plain", step("install_msi", "url" to "http://example.com/a.msi")))) diff --git a/app/src/test/java/com/droiddeck/launcher/store/FlathubApiTest.kt b/app/src/test/java/com/droiddeck/launcher/store/FlathubApiTest.kt index df2b3acac..2682c19fc 100644 --- a/app/src/test/java/com/droiddeck/launcher/store/FlathubApiTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/store/FlathubApiTest.kt @@ -12,7 +12,6 @@ class FlathubApiTest { } @Test fun refsReadAsWords() { - // The words come from string resources; stand in for them with their English. val english = mapOf( R.string.flatpakmgr_ref_runtime_branch to "%1\$s runtime %2\$s", R.string.flatpakmgr_ref_graphics_drivers to "graphics drivers", diff --git a/app/src/test/java/com/droiddeck/launcher/stores/CloudPlanTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/CloudPlanTest.kt index 854c04a9c..5338fd343 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/CloudPlanTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/CloudPlanTest.kt @@ -10,7 +10,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** The rules that keep a cloud sync from replacing real saves. (Robolectric for org.json.) */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class CloudPlanTest { @@ -42,20 +41,16 @@ class CloudPlanTest { @Test fun theSideThatChangedWins() { val base = mapOf("s" to Entry("a", "a", 1000)) - // Only local changed: up sends it, down leaves it. assertEquals(listOf("s"), CloudPlan.up(mapOf("s" to Local("b")), mapOf("s" to Remote("a", 1000)), base).transfer) assertTrue(CloudPlan.down(mapOf("s" to Local("b")), mapOf("s" to Remote("a", 1000)), base).transfer.isEmpty()) - // Only the cloud changed: down takes it, up leaves it. assertEquals(listOf("s"), CloudPlan.down(mapOf("s" to Local("a")), mapOf("s" to Remote("c", 9000)), base).transfer) assertTrue(CloudPlan.up(mapOf("s" to Local("a")), mapOf("s" to Remote("c", 9000)), base).transfer.isEmpty()) - // The same bytes on both sides: nothing moves. assertEquals(listOf("s"), CloudPlan.up(mapOf("s" to Local("c")), mapOf("s" to Remote("c", 9000)), base).same) } @Test fun timesAloneDecideWhenTheServiceGivesNoHash() { val base = mapOf("s" to Entry("a", null, 1000)) assertEquals(listOf("s"), CloudPlan.down(mapOf("s" to Local("a")), mapOf("s" to Remote(null, 60_000)), base).transfer) - // Within two seconds of the baseline: the same cloud copy. assertTrue(CloudPlan.down(mapOf("s" to Local("a")), mapOf("s" to Remote(null, 2500)), base).transfer.isEmpty()) } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/CloudSavePathsTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/CloudSavePathsTest.kt index 5258c19ad..a88c8b954 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/CloudSavePathsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/CloudSavePathsTest.kt @@ -6,7 +6,6 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test -/** A store's save-location template, made concrete inside the game's prefix (Bannerlator's cases). */ class CloudSavePathsTest { private val root = Files.createTempDirectory("cloud").toFile() private val prefix = File(root, "compatdata/3785150007/pfx").apply { mkdirs() } @@ -45,7 +44,6 @@ class CloudSavePathsTest { CloudSavePaths.epic("{UserSavedGames}/Studio/{EpicId}/{AppName}", prefix, install, "acc", "app")!!.path) assertEquals(File(profile, "Documents/G").path, CloudSavePaths.epic("{UserDir}/G", prefix, install, "acc", "app")!!.path) assertNull(CloudSavePaths.epic("{Nope}/G", prefix, install, "acc", "app")) - // Epic's folder names are matched by letters and digits alone when that names exactly one. File(profile, "AppData/Local/My-Game").mkdirs() assertEquals(File(profile, "AppData/Local/My-Game").path, CloudSavePaths.epic("{AppData}/MyGame", prefix, install, "acc", "app")!!.path) } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/CloudSavesTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/CloudSavesTest.kt index a85ee7c18..023013ed5 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/CloudSavesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/CloudSavesTest.kt @@ -10,7 +10,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** A sync against a cloud held in memory: the baseline gate, conflicts, backups and the no-prefix deferral. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class CloudSavesTest { @@ -59,7 +58,6 @@ class CloudSavesTest { assertEquals(1, up.conflicts) assertEquals("cloud v2", String(cloud.files.getValue("slot0.sav").first)) assertEquals(listOf("slot0.sav"), CloudSaves.conflicts(app, Store.GOG, game)) - // Keep local resolves it. CloudSaves.upWith(app, Store.GOG, game, dir, cloud, force = true) assertEquals("local v2", String(cloud.files.getValue("slot0.sav").first)) assertTrue(CloudSaves.conflicts(app, Store.GOG, game).isEmpty()) @@ -70,17 +68,14 @@ class CloudSavesTest { val folder = File(StoreInstallRoot.storeDir(StoreInstallRoot.internalRoot(app), Store.GOG), "ELDERBORN-mark").apply { mkdirs() } File(folder, "ELDERBORN.exe").writeText("MZ") StoreGameSidecar(Store.GOG, gameId, "ELDERBORN", "ELDERBORN.exe").write(folder) - // A save location the store gave, but no prefix yet: the upload cannot run, the mark stays. com.droiddeck.launcher.stores.gog.GogPrefs.get(app).edit().putString("cloud_template_$gameId", "<?SAVED_GAMES?>/ELDERBORN").apply() CloudSaves.markDirty(app, Store.GOG, gameId) assertTrue(CloudSaves.dirty(app).contains(Store.GOG to gameId)) val first = CloudSaves.uploadDirty(app, Store.GOG, gameId, "session-end")!! assertEquals("no-prefix", first.reason) assertTrue(CloudSaves.dirty(app).contains(Store.GOG to gameId)) - // Recovery waits while a session runs. CloudSaves.uploadAllDirty(app, "recovery", running = true) assertTrue(CloudSaves.dirty(app).contains(Store.GOG to gameId)) - // The store has no cloud saves for it: a final skip, the mark goes; a second trigger finds nothing. com.droiddeck.launcher.stores.gog.GogPrefs.get(app).edit().putString("cloud_template_$gameId", "").apply() assertEquals("no-cloud-saves", CloudSaves.uploadDirty(app, Store.GOG, gameId, "exit")!!.reason) assertTrue(CloudSaves.dirty(app).none { it.second == gameId }) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/GameFactsTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/GameFactsTest.kt index 5e2cc02f3..c8834783b 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/GameFactsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/GameFactsTest.kt @@ -7,7 +7,6 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test -/** The name match that finds a store game on Steam and HowLongToBeat, and how its facts read. */ class GameFactsTest { private val saved = Locale.getDefault() @@ -42,7 +41,6 @@ class GameFactsTest { val results = listOf("Hades II", "The Witcher 3: Wild Hunt — Songs of the Past", "Hades", "Hades") assertEquals(2, TitleMatch.pick("Hades", results.withIndex().toList()) { listOf(it.value) }?.index) assertNull(TitleMatch.pick("Hades", listOf("Hades II", "Hadesville")) { listOf(it) }) - // An alias counts as a name. assertEquals("x", TitleMatch.pick("The Witcher III: Wild Hunt", listOf("x" to "The Witcher 3: Wild Hunt, The Witcher III: Wild Hunt")) { it.second.split(',') }?.first) } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt index 090280f77..da3b873e1 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt @@ -4,18 +4,15 @@ import java.io.File import org.junit.Assert.assertEquals import org.junit.Test -/** The Installed tab draws what its count counts: the installs on disk. */ class InstalledCardsTest { private fun install(store: Store, id: String, title: String) = InstalledStoreGame(StoreGameSidecar(store, id, title, "game.exe", cover = "https://img/$id.jpg"), File("/x/$id")) @Test fun everyInstallIsACardWhetherOrNotTheLibraryHasLoaded() { val installed = listOf(install(Store.GOG, "1", "DOOM + DOOM II"), install(Store.GOG, "2", "DOOM I Enhanced"), install(Store.EPIC, "e", "DOOMBLADE")) - // Library not loaded: cards from the sidecars. val bare = installedCards(Store.GOG, installed, emptyList()) assertEquals(listOf("DOOM + DOOM II", "DOOM I Enhanced"), bare.map { it.title }) assertEquals("https://img/1.jpg", bare.first().tallImageUrl) - // Loaded: the library item wins, by id, or by title when the ids differ. val library = listOf( CatalogItem(Store.GOG, "1", "DOOM + DOOM II", imageUrl = "wide1", owned = true), CatalogItem(Store.GOG, "999", "DOOM I Enhanced", imageUrl = "wide2", owned = true), diff --git a/app/src/test/java/com/droiddeck/launcher/stores/MatureContentTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/MatureContentTest.kt index 2380b692e..ec6b2062b 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/MatureContentTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/MatureContentTest.kt @@ -9,7 +9,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** Mature means what the store's own ratings or tags say; a title without them stays visible. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class MatureContentTest { @@ -20,7 +19,6 @@ class MatureContentTest { assertTrue(GogStoreCatalog.isMature(gog("""{"name":"gogRating","ageRating":"18"}""", ""))) assertTrue(GogStoreCatalog.isMature(gog("""{"name":"pegiRating","ageRating":"12"}""", """{"name":"NSFW","slug":"nsfw"}"""))) assertFalse(GogStoreCatalog.isMature(gog("""{"name":"esrbRating","ageRating":"13"},{"name":"uskRating","ageRating":"16"}""", """{"name":"Puzzle","slug":"puzzle"}"""))) - // No rating data at all: visible. assertFalse(GogStoreCatalog.isMature(JSONObject("""{"id":"2","title":"U"}"""))) } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreAccountsCipherTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreAccountsCipherTest.kt index b4346c7ca..aed81f155 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreAccountsCipherTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreAccountsCipherTest.kt @@ -17,7 +17,6 @@ import java.security.SecureRandom import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec -/** The stores' sign-in files sealed at rest; a plain key stands in for the AndroidKeyStore. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class StoreAccountsCipherTest { @@ -44,7 +43,6 @@ class StoreAccountsCipherTest { assertEquals("AES/GCM", envelope.getString("alg")) assertFalse(envelope.toString().contains("secret")) assertEquals(creds.toString(), CredentialCipher.open(envelope, fake)) - // A fresh IV every time. assertFalse(envelope.getString("iv") == CredentialCipher.seal(creds.toString(), fake).getString("iv")) } @@ -87,7 +85,6 @@ class StoreAccountsCipherTest { @Test fun aFileSealedUnderALostKeyReadsAsSignedOutAndIsDeleted() { StoreAccounts.write(app, Store.AMAZON, creds) - // Data cleared, or a backup restored on another device: the Keystore key is a new one. val other = newKey() StoreAccounts.keys = CredentialCipher.KeyProvider { other } assertNull(StoreAccounts.read(app, Store.AMAZON)) @@ -108,7 +105,6 @@ class StoreAccountsCipherTest { assertEquals(creds.toString(), StoreAccounts.read(app, Store.EPIC).toString()) assertTrue(StoreAccounts.keystoreFailed) - // The next start, on a Keystore that works. StoreAccounts.keys = fake StoreAccounts.keystoreFailed = false StoreAccounts.encryptAll(app) @@ -123,7 +119,6 @@ class StoreAccountsCipherTest { assertNull(StoreAccounts.read(app, Store.GOG)) } - /** A Keystore whose key is there but which fails to hand it over [failures] times first. */ private class Flaky(private val key: SecretKey, var failures: Int, private val error: () -> Exception) : CredentialCipher.KeyProvider { override fun key(): SecretKey = key override fun existing(): SecretKey { if (failures > 0) { failures--; throw error() }; return key } @@ -142,10 +137,8 @@ class StoreAccountsCipherTest { assertNull(StoreAccounts.read(app, Store.EPIC)) assertTrue(StoreAccounts.isUnavailable(Store.EPIC)) assertEquals(sealed, file(Store.EPIC).readText()) - // Still signed in for the page: no sign-in card, and an action says why it failed. assertEquals("Epic sign-in could not be read: try again.", StoresState.notSignedInLine(Store.EPIC)) - // The next access, once the Keystore answers. assertEquals(creds.toString(), StoreAccounts.read(app, Store.EPIC).toString()) assertFalse(StoreAccounts.isUnavailable(Store.EPIC)) assertEquals("Someone", StoreAccounts.signedInAs(app, Store.EPIC)) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreCacheTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreCacheTest.kt index a8e4dd20b..c01e4202f 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreCacheTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreCacheTest.kt @@ -11,7 +11,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** Sizes kept per version, and a refresh that touches only the cards that changed. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class StoreCacheTest { @@ -20,7 +19,6 @@ class StoreCacheTest { @Test fun aSizeIsKeptForItsVersionAndAskedAgainForANewOne() { val cache = SizeCache.forStore(dir, Store.EPIC) cache.put("Metalstorm", "1.0.3", 4_400_000_000L) - // Read back from disk by a new instance, as the next app start does. val again = SizeCache.forStore(dir, Store.EPIC) assertEquals(4_400_000_000L, again.get("Metalstorm", "1.0.3")) assertNull(again.get("Metalstorm", "1.0.4")) @@ -36,14 +34,11 @@ class StoreCacheTest { val a = CatalogItem(Store.GOG, "1", "A", imageUrl = "x", owned = true) val b = CatalogItem(Store.GOG, "2", "B", imageUrl = "y", owned = true) val old = listOf(a, b) - // Nothing changed: the very list shown stays. assertSame(old, mergeItems(old, listOf(a.copy(), b.copy()))) - // One card changed: the other is the same object, the changed one is new. val merged = mergeItems(old, listOf(a.copy(), b.copy(title = "B2"))) assertSame(a, merged[0]) assertNotSame(b, merged[1]) assertEquals("B2", merged[1].title) - // A first load is the new list as it is. val fresh = listOf(a) assertSame(fresh, mergeItems(null, fresh)) } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt index ae966ca9b..d0f2de6ca 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt @@ -9,7 +9,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** Under Robolectric for a real org.json; the plain unit-test android.jar stubs it out. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class StoreGameSidecarTest { @@ -37,11 +36,9 @@ class StoreGameSidecarTest { @Test fun defaultsWhenFieldsAreAbsent() { val back = StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":"bin\\game.exe","addToSteam":false}""")!! - // An older sidecar's Steam switch is read past, not acted on. assertEquals("bin/game.exe", back.exe) assertNull(back.launcher) assertTrue(back.args.isEmpty()) - // No state field = an earlier build's finished install. assertTrue(back.isInstalled) } @@ -53,9 +50,7 @@ class StoreGameSidecarTest { val back = StoreGameSidecar.parse(set.toJson().toString())!! assertEquals(EpicOptions(eos = true, offline = true), back.epic) assertFalse(back.epic.wantsCode) - // A sidecar from before the choices existed reads as the defaults. assertEquals(EpicOptions(), StoreGameSidecar.parse("""{"store":"epic","id":"m","title":"M","exe":"M.exe"}""")!!.epic) - // An earlier build's overlay field is read past. assertEquals(EpicOptions(), StoreGameSidecar.parse("""{"store":"epic","id":"m","title":"M","exe":"M.exe","epic":{"v":2,"eos":true,"offline":false,"overlay":true}}""")!!.epic) } @@ -64,12 +59,10 @@ class StoreGameSidecarTest { StoreGameSidecar(Store.EPIC, "m", "Metalstorm", "Metalstorm.exe", launcher = ".droiddeck-launch.bat").write(folder) val written = StoreGameSidecar.updateEpic(folder) { it.copy(offline = true) }!! assertTrue(written.epic.offline) - // What the Steam-launch helper reads: the file itself. val onDisk = org.json.JSONObject(java.io.File(folder, StoreGameSidecar.FILE_NAME).readText()) assertTrue(onDisk.getJSONObject("epic").getBoolean("offline")) assertTrue(onDisk.getJSONObject("epic").getBoolean("eos")) assertFalse(onDisk.getJSONObject("epic").has("overlay")) - // Everything else in it stays. assertEquals(".droiddeck-launch.bat", StoreGameSidecar.read(folder)!!.launcher) assertEquals(null, StoreGameSidecar.updateEpic(java.io.File(folder, "missing")) { it }) } @@ -89,7 +82,6 @@ class StoreGameSidecarTest { val back = StoreGameSidecar.parse(started.toJson().toString())!! assertFalse(back.isInstalled) assertEquals("", back.exe) - // A finished one must still name its exe. assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":""}""")) assertTrue(StoreGameSidecar.parse(back.copy(exe = "Metalstorm.exe", state = StoreGameSidecar.STATE_INSTALLED).toJson().toString())!!.isInstalled) } @@ -98,7 +90,6 @@ class StoreGameSidecarTest { assertNull(StoreGameSidecar.parse("not json")) assertNull(StoreGameSidecar.parse("""{"store":"steam","id":"1","title":"T","exe":"a.exe"}""")) assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"","title":"T","exe":"a.exe"}""")) - // An exe outside the folder is not trusted: a sidecar must not point the shortcut anywhere. assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":"../other/a.exe"}""")) assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":"/abs/a.exe"}""")) assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":"C:\\a.exe"}""")) @@ -116,10 +107,7 @@ class StoreGameSidecarTest { assertTrue(text.contains("cd /d \"%~dp0Binaries\\Win64\"\r\n")) assertTrue(text.contains("set \"FOO=bar\"\r\n")) assertTrue(text.contains("\r\n\"%~dp0Binaries\\Win64\\Celeste.exe\" -EpicPortal -epicusername=\"Some One\"\r\n")) - // Epic reads the one-shot exchange code the app leaves beside the launcher, then deletes it. assertTrue(text.contains("set /p DD_X=<\"%~dp0.droiddeck-epic-code\"")) - // The code goes on the command line only: the variable is cleared on the very line that - // starts the game (cmd has already expanded it there), so the game inherits none of it. assertTrue(text.contains("set \"DD_X=\" & \"%~dp0Binaries\\Win64\\Celeste.exe\" -EpicPortal -epicusername=\"Some One\" -AUTH_LOGIN=unused -AUTH_PASSWORD=%DD_X% -AUTH_TYPE=exchangecode\r\n")) assertFalse(text.contains("AUTH=")) assertFalse(text.contains("DD_AUTH")) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt index 3856ed562..217bc7d5a 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt @@ -12,7 +12,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** Where a store game goes: internal storage unless the user picks a card, and back to its own folder on a rerun. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class StoreInstallRootTest { @@ -22,7 +21,6 @@ class StoreInstallRootTest { val card = File(app.filesDir, "fake-card/steam").apply { mkdirs() } SessionPrefs.setGameStorage(app, card.path, "Card") assertEquals(File(LinuxRuntime.rootDir(app), "root/Games/Stores").absolutePath, StoreInstallRoot.installRoot(app).absolutePath) - // The library's Games folder is still scanned, for installs an earlier build put there. assertTrue(StoreInstallRoot.roots(app).any { it.absolutePath == File(card, "Games").absolutePath }) assertFalse(StoreInstallRoot.targets(app).first().removable) } @@ -44,9 +42,7 @@ class StoreInstallRootTest { File(half, "Metalstorm.exe").writeText("MZ") assertFalse(StoreInstallRoot.isFinished(half)) assertTrue(StoreInstallRoot.unfinished(app).any { it.store == Store.EPIC && it.id == null && it.folder.absolutePath == half.absolutePath }) - // Install again, even with another root picked: the same folder, no duplicate. assertEquals(half.absolutePath, StoreInstallRoot.folderFor(app, Store.EPIC, "abc", "Metalstorm", File(app.filesDir, "card/Games")).absolutePath) - // Begun and finished: the folder becomes a game. StoreInstalls.begin(half, Store.EPIC, "abc", "Metalstorm", null, null) assertTrue(StoreInstallRoot.unfinished(app).any { it.id == "abc" }) StoreGameSidecar(Store.EPIC, "abc", "Metalstorm", "Metalstorm.exe").write(half) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreLogTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreLogTest.kt index 0b72b771b..4295cc557 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreLogTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreLogTest.kt @@ -4,7 +4,6 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Test -/** Store lines reach the Downloads log and logcat; nothing in them may carry a token. */ class StoreLogTest { @Test fun gogSecureLinkKeepsOnlyTheHost() { val line = "gog: window 32 in_flight 3 https://gog-cdn-fastly.gog.com/token=nva=1791500000~dirs=/content-system/v2/store/1207658924~token=0a1b2c3d4e5f/content-system/v2/store/1207658924/ab/cd/abcdef 12.1 MB/s" @@ -24,7 +23,6 @@ class StoreLogTest { assertEquals("refresh access_token=… refresh_token: '…' code=… hdnts=… __token__=…", out) assertEquals("Authorization: …", StoreLog.redactLine("Authorization: Bearer eyJhbGciOi")) assertEquals("""{"access_token":"…","expires_in":7200}""", StoreLog.redactLine("""{"access_token":"eyJ.abc","expires_in":7200}""")) - // Ordinary numbers stay: "HTTP code 403" has no separator to read as a secret. assertEquals("HTTP code 403 from https://api.gog.com/products/…", StoreLog.redactLine("HTTP code 403 from https://user:pw@api.gog.com/products/1?expand=x")) } } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadEntryTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadEntryTest.kt index 23c6b93db..46b38647c 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadEntryTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadEntryTest.kt @@ -6,23 +6,19 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test -/** The Downloads row reads the active stage's own progress, and which stages are behind it. */ class DownloadEntryTest { private val base = DownloadEntry(Store.EPIC, "doomblade", "DOOMBLADE", state = DownloadState.RUNNING) @Test fun downloadRunsOnBytesAndInstallOnItsOwnCount() { val dl = base.copy(stage = DownloadStage.DOWNLOAD, bytesDone = 50, bytesTotal = 200) assertEquals(0.25f, dl.stageFraction, 0.001f) - // Past Download the bytes no longer drive the bar; the stage's own count does. val inst = dl.copy(stage = DownloadStage.INSTALL, stageDone = 30, stageTotal = 40, stageItems = 3, stageItemsTotal = 4) assertEquals(0.75f, inst.stageFraction, 0.001f) assertEquals(75, inst.percent) - // Nothing counted yet: unknown, not empty and not full. assertEquals(-1f, base.copy(stage = DownloadStage.INSTALL).stageFraction, 0f) } @Test fun passedStagesComeFromTheBitsNotTheOrder() { - // Epic checks files before it fetches: Verify is behind Download there. val e = base.copy(stage = DownloadStage.DOWNLOAD, stagesPassed = (1 shl DownloadStage.MANIFEST.ordinal) or (1 shl DownloadStage.VERIFY.ordinal)) assertTrue(e.passed(DownloadStage.VERIFY)) assertFalse(e.passed(DownloadStage.INSTALL)) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadNotificationTextTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadNotificationTextTest.kt index 8bd9870ad..aca4c436d 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadNotificationTextTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadNotificationTextTest.kt @@ -4,7 +4,6 @@ import com.droiddeck.launcher.stores.Store import org.junit.Assert.assertEquals import org.junit.Test -/** The notification's line: what the Downloads row says, on one line. */ class DownloadNotificationTextTest { private val base = DownloadEntry(Store.EPIC, "m", "Metalstorm", state = DownloadState.RUNNING) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt index 4d085013f..669e0adc4 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt @@ -8,7 +8,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** The JSON manifest's numbers are blobs; the same values as the Rust engine's manifest tests. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class EpicJsonManifestTest { diff --git a/app/src/test/java/com/droiddeck/launcher/stores/gog/GogCdnTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/gog/GogCdnTest.kt index 22b418da6..324338e17 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/gog/GogCdnTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/gog/GogCdnTest.kt @@ -6,7 +6,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** Every CDN GOG's secure link offers is used, not only the first. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class GogCdnTest { diff --git a/app/src/test/java/com/droiddeck/launcher/ui/FlathubFocusTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/FlathubFocusTest.kt index b98cd7660..a268c46a8 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/FlathubFocusTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/FlathubFocusTest.kt @@ -79,14 +79,11 @@ class FlathubFocusTest { compose.waitForIdle() val field = editText(root)!! compose.runOnIdle { - // Keep a nonempty query, so clearing focus cannot simply dispose the search field. - // Whitespace skips the network search while retaining the field just like typed text. field.setText(" ") field.requestFocus() assertTrue(field.isFocused) } compose.waitForIdle() - // Touching a card does not implicitly focus a Compose control or release an AndroidView. compose.onNodeWithText("Datcord").performSemanticsAction(SemanticsActions.OnClick) { it() } compose.onNodeWithText(context.getString(R.string.store_install)).assertExists() compose.runOnIdle { diff --git a/app/src/test/java/com/droiddeck/launcher/ui/GamepadGeometryTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/GamepadGeometryTest.kt index 77a8d5b8e..e60f12f44 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/GamepadGeometryTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/GamepadGeometryTest.kt @@ -8,7 +8,6 @@ import org.junit.Assert.assertTrue import org.junit.Test class GamepadGeometryTest { - /** A foldable's unfolded panel (px) and density, as the gamepad gets it under a 16:9 game. */ private data class Panel(val name: String, val width: Int, val height: Int, val density: Float) { val controlsHeight get() = height - SessionDisplay.unfoldedGameHeight(width, height) } @@ -68,7 +67,6 @@ class GamepadGeometryTest { val right = b.getValue(Slot.RIGHT_MAIN) assertEquals(panel.name, left.cy, right.cy, 0.5f) assertEquals(panel.name, left.cx, panel.width - right.cx, 0.5f) - // In the bottom half of their side, nearer the corner than the second controls. assertTrue(panel.name, left.cx < b.getValue(Slot.LEFT_SECOND).cx) assertTrue(panel.name, left.cy > panel.controlsHeight / 2f) } @@ -77,8 +75,6 @@ class GamepadGeometryTest { @Test fun middleButtonsKeepTheDeckOrder() { for (panel in panels) { val b = boxes(panel) - // View before Menu and Steam before QAM: left of it in a row or two by two, above it - // in a single column. fun before(a: Slot, c: Slot) = b.getValue(a).let { x -> b.getValue(c).let { y -> x.cx < y.cx - 0.5f || (kotlin.math.abs(x.cx - y.cx) <= 0.5f && x.cy < y.cy) } } assertTrue(panel.name, before(Slot.VIEW, Slot.MENU)) diff --git a/app/src/test/java/com/droiddeck/launcher/ui/LiftSheetTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/LiftSheetTest.kt index 36c206f05..241230027 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/LiftSheetTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/LiftSheetTest.kt @@ -10,7 +10,6 @@ class LiftSheetTest { } @Test fun theButtonKeeps16dpAboveIt() { - // A short pane: the edge comes down so the button riding it still clears the top by 16. val top = sheetTop(150f, 56f, 16f, 3f) assertEquals(41f, top, 0.01f) assertEquals(16f, top + 3f - 56f / 2f, 0.01f) diff --git a/app/src/test/java/com/droiddeck/launcher/ui/MotionTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/MotionTest.kt index b16d89efd..669410a9c 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/MotionTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/MotionTest.kt @@ -31,7 +31,6 @@ class MotionTest { Motion.refresh(context) assertEquals(0f, Motion.scale, 0f) assertEquals(0, Motion.ms(300)) - // The app's toggle never writes Android's preference. assertEquals(1.5f, Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE), 0f) SessionPrefs.setAnimationsEnabled(context, true) Motion.refresh(context) diff --git a/app/src/test/java/com/droiddeck/launcher/ui/PageExitTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/PageExitTest.kt index b54248f40..ad1541685 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/PageExitTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/PageExitTest.kt @@ -13,7 +13,6 @@ class PageExitTest { } @Test fun anotherPageSinks() { - // The GPU drivers row in Steam's settings opens Components over the same rail item. assertEquals(PageExit.Sink, pageExit("steam", "components", PageLeave.Ordinary)) } diff --git a/app/src/test/java/com/droiddeck/launcher/ui/PickListingTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/PickListingTest.kt index af7b6f50d..9ed572671 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/PickListingTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/PickListingTest.kt @@ -12,7 +12,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import java.io.File -/** What the in-app picker lists in a folder: every folder A-Z, then only the files asked for. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class PickListingTest { diff --git a/app/src/test/java/com/droiddeck/launcher/ui/PresetScaleTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/PresetScaleTest.kt index d498d7f44..244918291 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/PresetScaleTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/PresetScaleTest.kt @@ -21,7 +21,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) -// Render the component without starting runtime services or installing the app crash handler. @Config(sdk = [28], application = android.app.Application::class) class PresetScaleTest { @get:Rule val compose = createRobolectricComposeRule() @@ -39,7 +38,6 @@ class PresetScaleTest { } } val originalWidth = compose.onNodeWithTag("page-size").fetchSemanticsNode().boundsInRoot.width - // Notify changes driven by the test before asking Compose to become idle again. compose.runOnIdle { scale.intValue = 75 Snapshot.sendApplyNotifications() diff --git a/app/src/test/java/com/droiddeck/launcher/ui/RobolectricComposeRule.kt b/app/src/test/java/com/droiddeck/launcher/ui/RobolectricComposeRule.kt index e6efbb130..b21ffa60d 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/RobolectricComposeRule.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/RobolectricComposeRule.kt @@ -9,11 +9,8 @@ import org.robolectric.Shadows.shadowOf fun createRobolectricComposeRule() = createComposeRule().also { val context = RuntimeEnvironment.getApplication() - // Register only in Robolectric, without adding a host activity to either APK variant. shadowOf(context.packageManager).addActivityIfNotPresent( ComponentName(context, ComponentActivity::class.java), ) - // Pure state tests can leave unapplied snapshots before a Compose host starts its snapshot - // manager. Apply them before the rule's initial idle check, which runs before the test body. Snapshot.sendApplyNotifications() } diff --git a/app/src/test/java/com/droiddeck/launcher/ui/StoreTabsTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/StoreTabsTest.kt index b8af29326..2be67a4b6 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/StoreTabsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/StoreTabsTest.kt @@ -4,13 +4,11 @@ import com.droiddeck.launcher.stores.Store import org.junit.Assert.assertEquals import org.junit.Test -/** A store's modes are Store, Library and Installed; Amazon has no catalog of its own, so no Store. */ class StoreTabsTest { @Test fun storeLibraryInstalledAmazonWithoutStore() { assertEquals(listOf("store", "library", "installed"), tabsFor(Store.GOG)) assertEquals(listOf("library", "installed"), tabsFor(Store.AMAZON)) assertEquals("library", tabFor(Store.AMAZON, "store")) - // An old saved All tab opens Library; Installed is a mode again. assertEquals("library", tabFor(Store.GOG, "all")) assertEquals("installed", tabFor(Store.EPIC, "installed")) assertEquals("store", tabFor(Store.GOG, "store")) diff --git a/artwork/boot-animation-source/droiddeck-boot.html b/artwork/boot-animation-source/droiddeck-boot.html index 353ebc0a9..55c1d9582 100644 --- a/artwork/boot-animation-source/droiddeck-boot.html +++ b/artwork/boot-animation-source/droiddeck-boot.html @@ -151,13 +151,6 @@ body.render main{display:none} </main> <script> -/* ===================================================================== - DroidDeck boot animation engine - Every frame is a pure function of time t, so the live preview and the - offline video render draw exactly the same thing. - Units: logo units from droiddeck-mark.svg. World origin = frame centre, - y points down. The frame is always 690 units tall. - ===================================================================== */ const DD = (() => { 'use strict'; const deg = Math.PI / 180; @@ -167,7 +160,6 @@ const DD = (() => { const eInQ = t => { t = clamp(t); return t * t; }; const eOutC = t => { t = clamp(t); return 1 - Math.pow(1 - t, 3); }; - // ---- 2D affine helpers. m = [a,b,c,d,e,f] (canvas setTransform order) const mul = (m, n) => [ m[0]*n[0] + m[2]*n[1], m[1]*n[0] + m[3]*n[1], m[0]*n[2] + m[2]*n[3], m[1]*n[2] + m[3]*n[3], @@ -179,20 +171,18 @@ const DD = (() => { const ap = (m, p) => ({ x: m[0]*p.x + m[2]*p.y + m[4], y: m[1]*p.x + m[3]*p.y + m[5] }); const rv = (a, x, y) => { const c = Math.cos(a), s = Math.sin(a); return { x: c*x - s*y, y: s*x + c*y }; }; - // ---- Geometry straight from the SVG const G = { L: -31.98, B: 223.72, slotL: 56.055, slotR: 71.885, cy: 111.86, dcx: 63.97, dR: 111.86, hcx: 63.98, hR: 71.81, ocx: 63.98, oR: 55.98 }; const ORB = '#1A9FFF'; const THEMES = { light: { ink: '#1b1b1b', bg: ['#EDF3F8', '#DCE6EE', '#B6C7D6'], shadow: '22,40,58', shadowK: 1, ringA: 1, ringGlow: 0, glow: 0, noise: 3 }, - // Keep the shipped Steam render black between and around the animated pieces. dark: { ink: '#FFFFFF', bg: ['#000000', '#000000', '#000000'], shadow: '0,0,0', shadowK: 2.3, ringA: 1.5, ringGlow: 14, glow: .34, noise: 0 }, }; let TH = THEMES.light, THN = 'light'; const setTheme = n => { THN = THEMES[n] ? n : 'light'; TH = THEMES[THN]; }; const ORB_C = { x: G.ocx, y: G.cy }; - function archPath() { // left piece: the "bridge" + function archPath() { const p = new Path2D(); const dx = G.slotL - G.hcx, hy = Math.sqrt(G.hR*G.hR - dx*dx); p.moveTo(G.L, 0); p.lineTo(G.slotL, 0); p.lineTo(G.slotL, G.cy - hy); @@ -200,7 +190,7 @@ const DD = (() => { p.lineTo(G.slotL, G.B); p.lineTo(G.L, G.B); p.closePath(); return p; } - function domePath() { // right piece: the curve of the D + function domePath() { const dxo = G.slotR - G.dcx, hyo = Math.sqrt(G.dR*G.dR - dxo*dxo); const dxi = G.slotR - G.hcx, hyi = Math.sqrt(G.hR*G.hR - dxi*dxi); const p = new Path2D(); @@ -215,7 +205,6 @@ const DD = (() => { const PATH = { arch: archPath(), dome: domePath(), orb: orbPath() }; - // centroids (numerical, symmetric in y) function centroid(path, x0, x1) { const c = document.createElement('canvas').getContext('2d'); let n = 0, sx = 0; @@ -225,23 +214,21 @@ const DD = (() => { } const CEN = { arch: centroid(PATH.arch, G.L, G.slotL), dome: centroid(PATH.dome, G.slotR, 176) }; - // anchors: the point of each piece that touches whatever it stands on const ANC = { arch: { x: G.slotL, y: G.cy }, ball: { x: G.ocx, y: G.cy + G.oR }, dome: { x: G.hcx + G.hR, y: G.cy } }; - const HA = G.slotL - G.L; // arch height when standing (88.035) - const WA = G.cy; // arch half width (111.86) - const FOOT_HALF = (G.cy - (G.cy - Math.sqrt(G.hR*G.hR - (G.slotL-G.hcx)**2))) ; // unused helper - const FEET = [{ x: G.slotL, y: 20.24 }, { x: G.slotL, y: G.B - 20.24 }]; // foot centres (piece coords) + const HA = G.slotL - G.L; + const WA = G.cy; + const FOOT_HALF = (G.cy - (G.cy - Math.sqrt(G.hR*G.hR - (G.slotL-G.hcx)**2))) ; + const FEET = [{ x: G.slotL, y: 20.24 }, { x: G.slotL, y: G.B - 20.24 }]; - // ---- Scene constants - const VIEW_H = 690; // world units visible vertically + const VIEW_H = 690; const FLOOR = 150; const GRAV = 6500; const DUR = 6.30; - const T_L = 4.00; // lift-off + const T_L = 4.00; + + + - // ------------------------------------------------------------------ - // Drops: exact ballistic bounces (height above whatever you land on) - // ------------------------------------------------------------------ function drop(o) { const g = GRAV, segs = [], impacts = []; let t = o.t0, v = o.v0; @@ -267,10 +254,6 @@ const DD = (() => { const ballDrop = drop({ t0: 0.50, h0: 440, v0: 250, e: 0.45, minV: 170 }); const domeDrop = drop({ t0: 1.15, h0: 330, v0: 250, e: 0.24, minV: 250 }); - // ------------------------------------------------------------------ - // The balancing act: one master sway curve, everything else follows it - // through springs so the parts lag, overshoot and settle on their own. - // ------------------------------------------------------------------ const SWAY = [[1.60, 0], [1.90, .42], [2.24, -.70], [2.62, .95], [2.82, 1.10], [3.08, -.44], [3.30, .16], [3.50, 0]]; function sway(t) { @@ -281,11 +264,9 @@ const DD = (() => { } return 0; } - // legs hop sideways to get back under the bowl on the big wobble const HOP = { t: 2.70, d: .19, dx: 26, h: 16 }; const hopX = t => HOP.dx*eio((t - HOP.t)/HOP.d); const hopH = t => (t > HOP.t && t < HOP.t + HOP.d) ? HOP.h*Math.sin(Math.PI*(t - HOP.t)/HOP.d) : 0; - // crouch before the jump (added straight onto squash) function crouch(t, amt, a, b, hold) { if (t < a || t >= T_L) return 0; if (t < b) return amt * eio((t - a)/(b - a)); @@ -297,12 +278,12 @@ const DD = (() => { const crouchD = t => crouch(t, .09, 3.64, 3.88, 3.91); const PHI_FALL = 5*deg, PSI_FALL = -6*deg, U0 = 12; - const ROLL = G.oR/G.dR; // bowl radius is 2x the ball: it turns 1.5x as it rolls + const ROLL = G.oR/G.dR; const DT = 1/1200, N = Math.ceil((DUR + .2)/DT); const SIG = { phi: new Float32Array(N), u: new Float32Array(N), psi: new Float32Array(N), qA: new Float32Array(N), qB: new Float32Array(N), qD: new Float32Array(N) }; - function peakFactor(w, z) { // peak of a unit-velocity kick + function peakFactor(w, z) { const wd = w*Math.sqrt(1 - z*z), ts = Math.atan(wd/(z*w))/wd; return Math.exp(-z*w*ts)*Math.sin(wd*ts)/wd; } @@ -333,7 +314,6 @@ const DD = (() => { if (k.k === 'phi') phiV += k.dv; else q[k.k][1] += k.dv; } const s = sway(t); - // arch rocking on its feet if (t < archDrop.land) { phi = PHI_FALL; phiV = 0; phiA = 0; } else { const w = 20, z = .38, tgt = 5.6*deg*sway(t - .10); @@ -342,13 +322,10 @@ const DD = (() => { if (sg !== lastSign && crossings < 2 && t < 1.0) { q.A[1] += kick('A', .035); crossings++; } lastSign = sg; } - // ball rolling along the arch top if (t < ballDrop.rest) { u = U0; uV = 0; } else { const w = 13, z = .5, tgt = 24*sway(t - .12); const a = -w*w*(u - tgt) - 2*z*w*uV; uV += a*DT; u += uV*DT; } - // dome: floppy hat with inertia (it lags the body) if (t < domeDrop.land) { psi = PSI_FALL; psiV = 0; } else { const w = 13, z = .22, tgt = 11*deg*s; const a = -w*w*(psi - tgt) - 2*z*w*psiV - .6*phiA; psiV += a*DT; psi += psiV*DT; } - // squash springs; falling pieces stretch along their velocity const tgA = archDrop.air(t) ? -Math.min(.12, Math.abs(archDrop.v(t))*4.6e-5) : 0; const tgB = ballDrop.air(t) ? -Math.min(.14, Math.abs(ballDrop.v(t))*6.5e-5) : 0; const tgD = domeDrop.air(t) ? -Math.min(.10, Math.abs(domeDrop.v(t))*4.0e-5) : 0; @@ -365,16 +342,13 @@ const DD = (() => { return a[i] + (a[i+1] - a[i])*f; }; - // ------------------------------------------------------------------ - // Standing pose: a kinematic chain floor → arch → ball → dome - // ------------------------------------------------------------------ function standPose(t) { const hA = archDrop.h(t) + hopH(t), hx = hopX(t), phi = sig('phi', t), u = sig('u', t), psi = sig('psi', t); const qA = sig('qA', t) + crouchA(t), qB = sig('qB', t) + crouchB(t), qD = sig('qD', t) + crouchD(t); const hB = ballDrop.h(t), hD = domeDrop.h(t); const sxA = 1 + .55*qA, syA = 1 - qA; - const side = phi >= 0 ? 1 : -1; // rock on the outer corner of one foot + const side = phi >= 0 ? 1 : -1; const piv = { x: hx + side*WA*sxA, y: FLOOR - hA }; const off = rv(phi, -side*WA*sxA, 0); const A = { x: piv.x + off.x, y: piv.y + off.y }; @@ -386,7 +360,6 @@ const DD = (() => { const mB = chain(T(cB.x, cB.y), R(phi), S(sxB, syB), T(-ANC.ball.x, -ANC.ball.y)); const bc = rv(phi, 0, -G.oR*syB), O = { x: cB.x + bc.x, y: cB.y + bc.y }; - // bowl: outer curve resting on the ball, rolling without slipping const ang = phi + psi, dd = rv(ang, 0, -G.oR*syB); const cD = { x: O.x + dd.x, y: O.y + dd.y - hD }; const sxD = 1 + .6*qD, syD = 1 - qD; @@ -394,9 +367,6 @@ const DD = (() => { return { arch: mA, ball: mB, dome: mD, O, hA }; } - // ------------------------------------------------------------------ - // Flight: lift-off, unfold around the orb, magnetic clack, hover settle - // ------------------------------------------------------------------ const FL = { TR: .34, APEX: -108, TC: .44, X: 48, ROT: .40 }; const H0 = (() => { const p = standPose(T_L), O = p.O, h = { O }; @@ -420,7 +390,7 @@ const DD = (() => { let Ox = H0.O.x*(1 - eio(tau/.4)); if (done) { Ox = 0; Oy = 0; } const O = { x: Ox, y: Oy }; - const st = -Math.sin(Math.PI*clamp(tau/.30))*.085; // stretch on the way up + const st = -Math.sin(Math.PI*clamp(tau/.30))*.085; const gs = sC > 0 && !done ? 1 + .05*Math.exp(-6*sC)*Math.sin(20*sC) : 1; const G_ = chain(T(O.x, O.y), S(gs, gs), T(-O.x, -O.y)); @@ -429,7 +399,7 @@ const DD = (() => { const a = h.a0 + (h.aF - h.a0)*eio(tau/FL.ROT); let r = h.rF + (h.r0 - h.rF)*(1 - smooth(0, .30, tau)); r += FL.X*smooth(0, .22, tau)*(1 - eInQ((tau - .30)/(FL.TC - .30))); - if (sC > 0 && !done) r += 3.6*Math.exp(-14*sC)*Math.abs(Math.sin(38*sC)); // hard stop, tiny rebound + if (sC > 0 && !done) r += 3.6*Math.exp(-14*sC)*Math.abs(Math.sin(38*sC)); let th = h.th0 + (0 - h.th0)*eio((tau - .02)/FL.ROT); if (done) { r = h.rF; th = 0; } const q = st*kq, c = CEN[k]; @@ -444,7 +414,6 @@ const DD = (() => { function pose(t) { t = clamp(t, 0, DUR); const p = t < T_L ? standPose(t) : flightPose(t); - // contact shadows const fade = t < T_L ? 1 : 1 - smooth(0, .2, t - T_L); const sh = []; if (fade > 0) { @@ -457,16 +426,12 @@ const DD = (() => { sh.unshift({ x: cx, rx: 150 + lowest*.35, ry: 15 + lowest*.03, a: .17*Math.exp(-lowest/210)*fade }); } p.shadows = sh; - // shock ring when the pieces lock const sC = t - (T_L + FL.TC); p.sC = sC; p.ring = sC > 0 && sC < .8 ? { O: p.O, r: 118 + 210*eOutC(sC/.8), a: .34*(1 - sC/.8)**2, w: 4*(1 - .7*sC/.8) } : null; return p; } - // ------------------------------------------------------------------ - // Drawing - // ------------------------------------------------------------------ function makeBG(W, H, noise) { const c = document.createElement('canvas'); c.width = W; c.height = H; const g = c.getContext('2d'); @@ -474,7 +439,7 @@ const DD = (() => { const gr = g.createRadialGradient(cx, cy, 0, cx, cy, R); gr.addColorStop(0, TH.bg[0]); gr.addColorStop(.42, TH.bg[1]); gr.addColorStop(1, TH.bg[2]); g.fillStyle = gr; g.fillRect(0, 0, W, H); - if (noise) { // static dither: kills banding in the video + if (noise) { const id = g.getImageData(0, 0, W, H), d = id.data; let s = 1337; for (let i = 0; i < d.length; i += 4) { s = (s*1664525 + 1013904223) >>> 0; const n = ((s >>> 24)/255 - .5)*noise; @@ -505,7 +470,7 @@ const DD = (() => { ctx.beginPath(); ctx.arc(p.ring.O.x, p.ring.O.y, p.ring.r, 0, Math.PI*2); ctx.stroke(); ctx.shadowBlur = 0; ctx.shadowColor = 'transparent'; } - if (TH.glow && p.sC > 0 && p.sC < 1.4) { // dark only: the orb flashes on at lock, light leaks through the halo gap + if (TH.glow && p.sC > 0 && p.sC < 1.4) { const ga = TH.glow*Math.min(1, p.sC/.03)*Math.exp(-p.sC/.32)*(1 - smooth(1.0, 1.4, p.sC)); ctx.setTransform(...base); const g = ctx.createRadialGradient(p.O.x, p.O.y, G.oR*.9, p.O.x, p.O.y, G.oR*2.6); @@ -520,7 +485,6 @@ const DD = (() => { ctx.setTransform(1, 0, 0, 1, 0, 0); } - // how far (px) things move during an exposure: drives motion-blur sample count const PROBE = { arch: [{x:G.L,y:0},{x:G.L,y:G.B},{x:G.slotL,y:0},{x:G.slotL,y:G.B}], dome: [{x:G.slotR,y:0},{x:G.slotR,y:G.B},{x:176,y:G.cy}], ball: [{x:G.ocx-G.oR,y:G.cy},{x:G.ocx+G.oR,y:G.cy},{x:G.ocx,y:G.cy-G.oR},{x:G.ocx,y:G.cy+G.oR}] }; @@ -533,7 +497,6 @@ const DD = (() => { return m*px; } - // Offline render with real motion blur (180° shutter, adaptive samples) let RC = null; function renderFrame(i, fps = 60, shutter = .5) { const cv = document.getElementById('cv'), W = cv.width, H = cv.height; @@ -564,9 +527,6 @@ const DD = (() => { frameCount: fps => Math.round(DUR*fps) + 1, CEN, H0 }; })(); -/* ===================================================================== - Preview player (or bare canvas when ?render=1) - ===================================================================== */ (() => { const qs = new URLSearchParams(location.search); const cv = document.getElementById('cv'); @@ -586,11 +546,10 @@ const DD = (() => { const sndBtn = document.getElementById('snd'), sndMark = document.getElementById('sndMark'), hint = document.getElementById('hint'); let W = 0, H = 0, bg = null, t = reduce ? DD.DUR : 0, playing = !reduce, speed = 1, last = 0, raf = 0; - // Soundtrack through Web Audio. When it's playing, the audio clock drives the picture. const Sound = (() => { let ctx = null, buf = null, src = null, on = false, pending = null, t0 = 0, c0 = 0, rate = 1; const bytes = () => { const b = atob(DD_SOUND), u = new Uint8Array(b.length); for (let i = 0; i < b.length; i++) u[i] = b.charCodeAt(i); return u.buffer; }; - async function ensure() { // call from a tap: browsers only allow sound after one + async function ensure() { if (!ctx) { const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return false; ctx = new AC(); } if (ctx.state !== 'running') { try { await ctx.resume(); } catch (e) {} } if (!buf) { pending = pending || new Promise((ok, no) => ctx.decodeAudioData(bytes(), ok, no)); @@ -607,7 +566,6 @@ const DD = (() => { return { ensure, start, stop, clock, get on() { return on; }, set on(v) { on = v; if (!v) stop(); } }; })(); - // version: remembered per viewer, otherwise follows the system theme let ver = null; try { ver = localStorage.getItem('dd-version'); } catch (e) {} if (ver !== 'light' && ver !== 'dark') ver = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; function setVersion(v) { @@ -662,7 +620,7 @@ const DD = (() => { if (Sound.on) { Sound.on = false; ui(); return; } Sound.on = true; ui(); if (!(await Sound.ensure())) { Sound.on = false; ui(); return; } - t = 0; play(); // replay from the top so it's heard in sync + t = 0; play(); }; playBtn.onclick = () => playing ? pause() : play(); cv.onclick = () => playing ? pause() : play(); @@ -693,8 +651,8 @@ const DD = (() => { if (playing) play(); })(); </script> -<script id="dd-sound">/* soundtrack: MP3, base64. Rebuild with embed_sound.py */ -const DD_SOUND = /*SOUND*/"SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYwLjE2LjEwMAAAAAAAAAAAAAAA//uUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAEJAAGPAAADBggLDhATFhgaHR8iJScqLS8yNDY5Oz5BQ0ZJS01QUlVYWl1gYmVnaWxucXR2eXx+gIOFiIuNkJOVmJqcn6Gkp6msr7Gztri7vsDDxsjLzc/S1Nfa3N/i5Obp6+7x8/b5+/4AAAAATGF2YzYwLjMxAAAAAAAAAAAAAAAAJAXgAAAAAAABjwC+eaeUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uUZAAP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAETEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVWu1SCQSiMSBsJhIEglv5aaJeRvmMLGAx41a1odhWJUK7SbMt+F3DJARa8xHUNPMPZF7ncrmCygIIQAMGCxgOZgWgKDgbqSgGCCmBhJpgcke/AzWKwUMgGPTCBo3RAbG1oHzjLwMriEDHQgACCgKCoCQNACD/wxgBgMAgDAML7gY6FgFBIBhIUg4CAYYH4ChD/AwQAgwoKURccYe6BgYGh84bA//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEFwYWXfygTiRMGhosMBCgxBYc0PKQMXD/plwuMThoyBfKRiRExMSZOf/l83dNN2qSYsF5aLo//+pqmfQ6RsDFTnVvZ6yUWiUWCQSBwNBIJNDlpprZQCY28bHHv1syR43YqA47FzM8hpwyiUZpML1CLzCjQ+xQSVmBygEJfgwZMCDMDQBbcDhiqAwYYQMOPEDpkb4GPQmCwwAw+NwMqw4DPUVA5YOeBmcTgZEFAGEAECIbANBUDCoD+KUC0cLnxH4GHAoCQOAwEEQ7AGBhCBIN/gYMAwW8CdDVMcYb6BgoIhlsL2A3eBEBfygThsRQqGiwtCEFw1cLlDDkDE6/5uXC4xOFxkEzJZqiiYnP/UxfN3TTdqkmLBPLRdH//1NUz6HSNkTJIzOcTQAAAAACUADchlzEz4U/4OTYNYzTJgb47Osu5N5uM3TGAgacwSADhMPqBizAjgM4wqQHkMBK//uURP+AAAAAaQUAAAgAAA0goAABHI1nLbn6gBuVrea3P1ADAzzATQCAHAC4OAJkSDAIQE4SANQSQeXMKBqUnZCc7AhYSlcHxMzkAVBJAjM+zxWaWln4rYQaiqtqxU0aagv9p6/N1n8bW5S2Ydq09vn5fnW/DPtWHYzagGrzuX/+/1///dVcq1bLn1P///X//6/+/8ajQGAx5llvRhISrDRZT19f+z//r//9QAEAAAAoixpdnoGUIUyb7YZJpNjqmMsaMbEZGhh1gUGAqAUYzoL5gDAJmXKAqYAYRCOMNKKKwGA4Ae1yNTIKGnWNwNbncjOFM69wgAzAtxHceYUmC3KSRVfrU1bljHOxM75OORl/9/8P73Lss6QSIioUqtFzBERWpm+obAro3V5d63DNjWEtyTMaUct5JABmWrRWJ0187ydA6haHbdy4pnviSKYQ2VpGJogXRhqgbIYOKB1GAoBEpgqgFMYB8AUGAuAFhgIABcYBcANGAQAEQkA2mACg//uUZMiABeM9RSZ/AACNxHiWz2AAFgCZCD38gAHTkuMLvPAAAQidDBQJWAoF8J4JAhQUeRCDAYEX2JV1frGcNh7T3tp+4ZZ36t+9G5DnIJnOkitHCoxA9JKL0m1hhrtW9r9dywtKDQFNFiQ8eCJLF3iIcpSEEWFHy5lk6mBlhcBoZSGkFWWxsJJOKL/Lie35kQuCmEmGQZRQM5qCDuGHuDUYRYQBgCAdGEACcYD4JAjAtMAEBswDQBS6sArzZml6jkXAhx5EBJCDlmKkZyoMFlEWNA4VmidtfD9/XWq/ebfG9Pk4qmFZ1ikKfWtb375zl7OKnlEu3bp//7rm1Y9uv/utAEKjHbnT40oemoNQXEwjk0EtQw4sJlM+6A5zCJgFIxEgAuPnzDax05MFMuIChlKw4FEiDhcwiElcGEDRUAkFDBgIwsPS2ZsnO7qAclAVMEIXciLfPeRF1xqcaag3GxGGWJGjgInkxYePDVSjHlhodc4sehhzlpruuz9Ptpfr//uUZD8M1PJgwxP7OfBlhMiReyZMDkR7EBX3gAE+EKPOuvABNXp9GmnnWdFu251as81V2qq3W+qrdDlzGnfPbslHaUoCAxYrIzP5P9MzwK04DxGTFQDRMPoWQxZxLDBfCbKAkjCuAQONYqFoJHnVjTeFARGIj4RLJcggQGhjgKfSAmgbZfdMmuv1GOzU2QWxSt/g6Dsx4aEgCNnPu/tn9T87G4KWo0///6DJMoD42ZeYYMsiF2DYNxO0yBYfWMNvFIzBmQN0wE8FXFgfYwJ8AbMBzAHBICKMAVABB4ASLPiQASjShgCzAKR6Q4AfgGojhjAE4uzUJKcyeJqxODJ4uYcS9davWes2Jt3s1637zAw1T2rdd+xvfT///9QjUN9LVOrd1MbTdNxESJpoMnhFCgyGIgGhwNGAgBu4im4Mojk20WsVS6NWAjTdUh0qLSHp8/XcJwxWtNbeW/vekuc/P9Jfr5x87g71vBH3igAFsWuagAAAADPi1s8x9oYOMcYN//uUZAoAA0Qcx8594ABDYpq/zDwAy1xzFj33gAFLD2MLvPAAwjMKAmowFUlEMoeFwTAYwK8wAYAhMJ6A7gMAxGCwAKyeYCAFk6GXlqVYGIiaAXgUTwnoPiOdRPwQRijeBrlwQ9GuKxWbNdTbg7tTD7WcyWrWA/fhUAARKHd4h3eIj/gAAAAAAALim21fdOmflFrPq652SgZ7TYTI31cDQVpYQE8egxyDQz3ai3xE6dR9buqb3i2kO1lo74RwGFfjjRpPDlCZVCAoGipg8xioAiSYbUFfGB5gRBgGIDUNAPo8ABGARADIYAFoKsKXvZTWWBGSjENUqQQgOIv6ScFUe5M2xsi6+vjNv/vO953jdr4vHEYqiy68mQAMGQyQ1DVRjJCBoMF8MkwJAAjHDBoCgSxhtgPGAqCUxQwDAA2aMIrv9C1DUJAhhNidnO1kWj1IXy6uevELlrCxTWNXz/JnWcf/FNzSVkxJHJoJFWMc3H8TYokfoz7sFwNHKJ2zBhQS//uUZAsNkwQbxRP7WXBHo0kKd2wWCyB9EjX3gAFTD6JKvPAAIxGIG3NU2jW342ISAggXTIARyn25QK/RoaWtyBJO0hTZebdIactxYkBEKjV0LOnfM+yXtNLtrHmwZAIwRH//ev0f/9vT/o6rdv6gAIAG0YAANDuCOM4nNrgdM7lSO0YTYkcw45K0RC1mg0GSEhuxnxVKSw4NmTAJWFjswrWt/p8tUY1decs+x3v7ADALgsk2HPWYxJNLZMLHjdDGVhX4zts2aMTaFsDCiwbswMkDrMCbBMwoAslQBLKAJMqgA7rN2bs1BA19gihrLxVlaRwsaNSqsJwnDPTCxB1Pq3zvf9NfdN4t853W3+Y6NQMGkyKYZuEGZjdg4GGyzuYAgm5jLAdGD2BiYcAGw4A0BgODAHAGTHZu7cpdlMVkEsqpNIXAXMhJ/TLs60NLkayXe6zuH8Rd/5vu0Hf1SkkN5e+KxNkAAAAIJEggxR1R9NCYCDjW+igoyW4DmNvVNajD//uUZA4AAwsURiZ/AABQoojozrwADJhRFLn3gAEgC+f3NPACrREcwSkCGMAPABzCHAOARACBgaQDgFAA+DFZ4w0h6lmGkAyKPxMHHe2HU6mdrmZ0w6CVwt5P3s5/nbw0HUjcGdilan79YAB0vrNAAB7pUR92rRz/XJpcDx79bZ1MVBjcN5EFxgCCgOJIGgaDgWXG19SQUAtpLD0seAjCuFjjltL42EKHEbb9D1Vq2HmqxFKYcOFoocfpAAoAAANV+R+TR1RYczCJpyMtrD7DT+aBUxcwYyAQTMYACAIGFTg8o8A2GBOgMwOAD3Cfp/GctXYEIGLmL5fD/Ow7iqPIiTCUpQHICv3SFbXcxzwE2uLvbWOrR///7Kf8vsiv3aQAi7bbdrbLKAAAAAAADd1AFQMvFAgE3HA0JVAWxZIjhFfSRVy8ieLIoekQpQyiVFykLopiVhgzoeTp2no8/3Pfd//nXi4oLE0JAAMZEB5TNHDmQ2HsmEMbQLrzD5QaIwGs//uUZAyN0vYcRBd94ABPAwii7zwASpxhEE/phUFEDiHF7aC4J3MEoAvTBAgF0wE8B8DgJwwAEAgAQB8Xqr00voU0AOgp65iD1qZCk+5IQ5K+WRmj7xavzfP/peud63NjwA64+5//6///oLgADEVEtMvUdY0nhxTBGI6MLUOkwcRUDCfBuMG0CoSCSMAcAIMAJUiuiX4v68svJGIAfRMVSXg4Vy3uTeyrt7Zs3jGtfH+6bhafw0uOHjykMGrQ7DLUb3QxHIYGMpHNpjAbALwwkcAhOXePPRMkWN0HboPGnRqxN3loRBzVlLpNxIEMGQdJAtbPDeBb/baLLuWpPxawfKngmTd9f////////99bDJUKdMku78ygiETFyINMMcDQynA/zOAw80lHhMWHgMbA4ZVxEJe8iwagjgr0jczDclhTXn+hyklILQA0juJ7S74ta/h1nMHtv///+r0I97dKKUlTDnC1A0XEFFNM8BoTFYDSM5FCTQFeNIBEFVJnZjoC//uUZBCAgq8VRJP8SOBSwohge48WCXhDJ69lgqE9DiKZ7aRwGDAGW0mJ2HaR737XU772MCUNhxQTIBUdYK01lMV0n4UDQLtwszpz5T0fmr/R/1Nb6P7v99ZhwMRG0Yz2a/IgpitrAGrjyeTrZnMgmyRQYgFYNHLgFATBzsRyFvciGC8Hwu2sZJ+ptYui2JnOQ8VBW8t9+V14vA4oo5WcPWJ+Mt9Hd/o/v7prQAAQ3IHdpG5RQdYw7rIzCSFXMVM7p9z8JICUimtpJhePe9i50lMnzaY/PL061OaXaIUvAMpANQMmTYcIO/b91+n6V9v9H1xf8Vv/qSg6pZMXMXg0fIzjHRDmMZBCMDNZ+poa2gmcgQGO06E10HWnchhyY9DbLKslXOBcjGE4rFzgmO3tMyz1/nr5u/+ePH1+yzs93+z+jvYj/699MYtENjepjRk27UGvMcmSmQH0zSYSOTl8y8cjD5MVAVUKC2t3YYdGIMQmlD2vuOytoDk1oW88SjXw//uUZByNws0RQwP8yJBLopiCe4wWCkhFDC/zQkE5CaIJ7jBYd9UF0iZljFKLFg6ZkB66k2atP+tfpt/r5LZ9lfa26K9RISqYUAE5j3v+GtcEQYYqtBqhBnJp4NAAIQ4YPEai71gwWNtrQkXDUFz5MaKi9TqBHC2+stldznXWmrp/3bO65/7/kU79/2f+l//6yGHvhSBmnHbAZEKQFGAaIShzOWGbiiZTdRnI+mGhAbKCkRhgqa2U4wiYWw7rOH7ljvMttXJVIqr7v/SVK5UffOCKhYodK/i/6n93f/2/+6z3/vWVVjBxPBMpu9E0TBlTFzRFM0HY6ApRIVmFwK47hFvYqgsstpMEszPRWIpdiaWsZGnpWLLQWWufHkVNeJV2t71/6FUVbv9t9f+rd2bX66k9AAMYpME9D8nD+aEqMsma8xSALgYBaA3hgk5niBkASa77Oddo4AmYJAxqwMwabQZ9jcX/JVsmg442DZhaxav///////awBgwqrmqxgea4//uUZCeN8ikUxRPaSTBKwohxe4wWCJRDEE/tIsESCGHB7TBYoTJljGMHb1ke9VRlMPGTQMVAY+yxkf0mFxsKwBwAw+8fTVBFSRGuOy0cpqLJy9JgeD5ITX0bMUTU//d/7P//b9BSGEmFjpkHte2ZHoKnmDvGHAUqTS7oy4jMCKCIeRuLYCQcSxLxOqA4mtZxVASNwWeTSe8oHEATwdIiayz///////f//oMM8Qs0SL2DXHFSMvEIU+gEe2GRTlaIxxkDIQEDjwj9roPDsdxhHYKTiiutXL8lbaDSm3sj1NW5Pup/7/X6vuZ0//7/6aAMGtGpzSEGOU1ZkNZMOzLwTnJBMBpg2Zc6Qs1JkxI0wwhl7l0+NR/n+erOYrvTTxeavdqWb17C8O7HMQu32e5ADD+BrNc3bA0yBsjNOIvPOGj8Ik3IkGi0v+gYLGawpRhXoaGNZmWjpMPRMNi7Rzj5HzEeqhG2j0b/93+zc7q/6v+z/14MIpFrDIDLewzLsZOM//uUZEkNwgARQ4v80IBEojhxe2wWCKRDDi/xgsETCCIJ7ZhYgYJyjtMSBKtM8mYABMFFoIAKQjNQZXw4WD2cF/bvL1xysY7bKnTbWw9s2nP9v0erV/+7////V6hRWExAheTYDhZNdgLUyuRQDCAcNrhwtQ+KgAQA7kLswuD1CEzxIwLJRjMXOgoZeQVoK7xf0/yX/3WR2+/Sp61f/u6v/oUoVAMKKFxjUMXVY0+oOMMlzJyzBigEoOA+jhPjBnzLLjDGkV3UUuAigBRIqVciQGUSBZORtcVfa2Fwqgm92judHO1tJfzlv/3fr//9/rGAADCWBYMkzIczCRmzIiEdEUM2c4zKsHCB0KoA3z2jkZqmyUbiY0wdnmedLIHQQ0bgY2cIf9v/p///RUyzGFrA0plRsA+ZaAIfmKbDWhj1KYRRGUF5MHxhrAkAr+Et2lxGU8SbduXrOjCARIGjjhTpvsp9a9P/Ff9sV/bV3/9X/SQxXTqjZEaeNuEOAxdxujeC//uUZG+M0mwQQ5P6SSA7IiiCe0kWCLg/Ek/tIsDwCGHF7SRYjeaxKWZIIm4YIOg85KHONTQnRWfIfAS1sDckCUG+g6Zo//3f//////lf/Wp/zBqQcIzt5zFM+PDtTGFxKs8IUCKjHszcDjNnTHjRIJIg2mlkiRiwMvgii7FBm7s2xzw5a2pmRoPW3/+n9Ov7fouV6PVm8AMFo2ow+epjHoGcMDwnM1EVMolhEAkxwTAIKEVYpnU4F8DIW860qQROb00w3rHMHoV4t2f9v/zPp9Pd/3fSu4WvMJ9DgDJOFkQyX0LyMW8B1jvVwxgzNDHDHgFV76v00kNMIDAvIjFDNyMILsWbF/f/s/7Prs7f1fFH//Qmf1b1EMOIpg1+UHzd6B9MF0rQ2iKNMjQFAiUQCARXKok+h5WYlZXEKjdjolmzy0kKDH+pDzptn/VytH/9/+7o/i/RoVhj39kAAK3HFGmzAkAIoxyFKWMgyCVjE5wdswLYAwaCauGspkEBAMvf//uUZJgI0jINxAv6MLA+ohhxe2kWCEg/Fs/sYsEPCGGF7aRYx++7sphEOClHJG3zBSec38d9ej1qT/Tv//03/7HIs0ABWBVTAfDFMkzKE2zhTV/hMGicMyYcETEgpSrmVyU7eTkx8DVyKTyENBk5OTeEmrLk32bW9ftx37GfZ1e+9Wz/Of6NQZhaYwl4UxMupZezNFA2cxUIH2PMZTHxIzgjMpDBIJegvS41ybKqRgWr0gsBJjy39nX7/+n5J398c977Pr/d14CrRUylUkQMT0XM21gqDZ7AkMbIgo0Ks1XYv8PHRZeXnhh24TY7KYWrSExNG91YNCgavczR9aKejPaKv1+ni+n76///+n2KACs0qmAril5lZ0K2ZfaIWmMPg0Bkc+CGQxdPMaIASIl2wcAMPCo1ybvLLoxGHKkPieWen3ADT6dhayGlrRYzVar5W//76Pv99CNVaEGDiAKYvH6hovBRqG6ZhIPhoELZg4DAqBqgSRIDjA/7WWA5yDJR//uUZMCIgiURRlP4MSBDwgiGe4YICIw9Es/sYsEGiCHJ7RhgVfeJiCRoAAsSapm79Vtz/6no0GBsh2fs/2Vfu+zahIzDYw/A0Zh+0NRpEPjKXg688+0DahRM9jAzEEzBoLYGj0tUTRkRoTSCoK9dKlIMCWPUJT5EO2IRTXsmjZ/nrdCdaqHti1nlrdUfdehjRn7ulx/YAJhvEznAylAcgQFxgmi0Gnigc9OJioOKqCADKmiMVZciAlAkEpjAsgeLQiAIS6tUhp4+qjTvcqHC7DTz+hDJdP1pqZHTElsMrYx41jdzF0NakmxetCteYjAch2I0GHosNB2HqjLvQ9AwYAF2AIICczEYu6a50ZAYHP1qBtGzioWbaLdVysmhcFAsZvtL0O7nNgUUoHmd8WLVvnEOWmlqA9pHn17bvU1XTZt0X2P6VGDQBg5hfqTyenSqYqquY2FiZ8jsYHB+YBAWpqqswVm6CieHMaciODpUehyUlCAERadv0g+vmEKU/6ar//uUZOcIwlsOxDP7MLBFIchhe6kGCmw7DC/xIsFfiKEF7hhY0uux5GhVTBgslPsTSvRb3NST/0btaDDoDRNc97M2ShSTG2ENOgTTrxs1RiACmRYZsqCY4jGJCYcdmCASibQnEuQU3qm60GYt8wQJysWjQSxwLpaUPzV9YnXIl7FYo1jcDr9L+zBGIwOQsKLb/X/yMgv//tGRWP+dkw9RCDlKm9OlEPMxWhzjO6LPkBNSowsCAUD0tUeG5HvzqpOHUYsSFj5b3122SHHCqjhgw1cKhilioRk3XlwkAUmVFbQM5xxrioGtQNL3meaFB8JUmDjzzT9cVGOdUhbWuvQ81QApLJbbbI0gYrEsZ/VWZDH0Y2DUY7iyZ7K4awKSaHqecI3MbE0sbfNkbPHSahvkZSFWZTBsRMxfhLowUwECiY4gHNkZ0CSa/HCsaaYiALqN9ZDUn0w1mNXMXBMVgAqnEMd7ZttmsgByxt03UApGDEha8NROpU2WgQipoBkjCLJD//uUZPcO8rcNwgP6SSBQgghAf6YIDGRrHi9tg9mMiGCB7iRYgCKJCF+UJSKpdIHCgIoACg0AkDGR09Fzs0YMwFnjrtqzl6n/kGU9DMNRGZ1Wu0UKfl9oJicCSKMyq7rtmlmpuUS+ipquF6/qrWu4LIqv///kyIksVAFlgAldNttgAAAwIERkRmynComJPAgDY+UPX/qQAEhwAekhD9GZ7RyOG+KgApJKyukAMowBOcEcPVUxA1WmRhPHXjLmJIrmAsHmgAImCIOGNY5mJwSDoCGFwGsnTvjFdtL8dACiAuKwALDAJgEURA/1fHzhmbX9c4jYbdO4z/e3uWaTP1vWMyRYti7gnQE///oIKxp20HTFEYwJhkhbGngaZrA5ggEIDQUBFqF7FutDgmer6gKEYHgMOKtTJ0EACow9an11MMXLyzTa2iYwxLxfOLzYoTFmKcQ2LltVM+WF/DEtAxcyg4XUMZ5B3DIWy1UxfgCWMDnAGZwwIcDEAQFYYHsAxmAF//uUZO4AFro5zeu7yzYpIgodaSYZzLB7M5XXgDjOh6TCuJABgAgVqew+x0BBkTmEWieqJbwYgDs1cJwBYKBXdf2+3jnzSS7+RB4nskVNbfWecie7R0lNS2rFH3tXkCUl27bvTWE3aGmCwKAEIhx7mOsS5Ao1zADAHV7HKsHtuTC4/PbUMaF7nOEX//mv/+8AJYAAgxqnUTE3SAMmVuoyFT4zOxAxM6oi0yshSjA5AnMHcIswvwZjJpBTMAQIcwJQEFXmBcAY4I0D66jwjxhGZEQzKNzCUKwK3BEJayYZAR3o219vnkT1jrJZTcvS+W36+VLK7eWcqj1XtTuOs8Ma2HPsYX8q5Fm59KW6r9NK2fJ/y2h136tX0gBIGRWq+RheKdYcRVfuGG7wOhzGt9YaHkS1mUijaRiPoMeYSYIsGB7BBxngxCmYOMBWGBAgFioDAcQDUwAgAAMAzALjAAwEtBMBGikg0RDPGbiGFIlvqImAQg/4RAHQpqMYp5lrrrP+//uUZM8ABWgkw4Z/IACI5Iilz2AAFsyVDjn8gAIGDiKjPYAAxeWz7hPvNSKmfJ+sO5xKWXbdim1Ws51It23S47uVL26qgswWNSr3AQCLEmWOlyocPOJbGJWZCIscba4dSSfMuIJhEJJTpeKjDv////9KQAAEIBQAJDdUFNMgJQo2zzVjL8P0BMTZkWIymTmRMYEQJBiVBAmFMBOY1wW5iVAoDQJqw5goADN+SgSoNEwi1awxrUXEMagtUCRGgt8qhFFvOxPWuUsTcN9qOjlz2X8L/YKllnfaWjxpXCAiIAkI1kksQt/obuup7eO1Xf1XXs/RrQU21YzC+P7NZPds8Ce5zWMNJM94V07ThyDEwBHMJgUIyiwjgACqYVAVLIhEAi/aqi+oYjByvPxzFg9mB2JixAVu0luKtM1f1bTlrX613Y87+6dq7QcYeEEdv/9/s+5pNBDWv//6QAAVSQnLQAAYbXnZfBkHeb6NmkS50x8WmCwSDmJBt3muvI0x570n//uUZEYMEyofxx95gABEgxlt7ZgBDkRrDg/7QsEMjuRZ3Yi7qEzgM9RIwFLIH7Sb7UpbGYfkcktHnKsNCEDBHExIwr4MwMemDTTSFCTsylceHMdbC4w78aTD2H4MMlQA0SRAjALBEMdsSowOAJjqvzRCQMRQ7M4UCS1MADBRijC4RtwxoqxdCXyq0NuDK6Whpaem+htTOqHkcoo1Tfj+FOaMjSCyQxE8nVqTX2/////9P+zeAgWjCJFzPmcjP7kDE8dgAOhz0LYXGwCqBNGOg5MFudF4FhcLiMdjEpj0btxupqNVv6MnsHUtTI4zdd1q4QiGPyT//TBOFBwxx3wwN7H/DjL4DEAxkMF1OVhF1DFsQHUwXIgwMLXBcgIBnmBFA+pgR4EuFHBzBo0YMIDAAFGVWIHMkBywylwONq/AoVDuoyLAkRkVJ6VzsYqk7w2JVm0bSZZ20q+cdhOFoUU/kmGzLSKXNxAQjiYRUmYhO0B2DIta5i67ry8WSU1PcWeh//uUZESP1IshQgP6StBjQziBe0lKD0SBCg/0x8EVC6MF7aCziniqZRLpGkDAOYK5w5natimdm9eYvQzJgZGQGkufKYUgDhhZlMGRqG8BgTDosAFGMERCgFpD5QBLvZylElakWhIZy5Cd8aTmfQQXcxFIu2uw9NOjlPjfc0PjSI00BUq2f0f0f7v/3p+Kf09pghCJMaf2SyGUhdCJiLRZ2YLEXTmV2G7hhZgHqYIuPHHyiRGQB/GUK+GVo3FY4GHgTlp0n2WtEZqWUYGIQDLxLcAQCrAMhLcpytJdGal1PUqhmkimjJFyBDSMYjOd/bZjumApIyHmquJbFbn6tzH6/p6fx3c9vvbf1bzGI4GWYGiQJmuEymO2BUYBIVpnTgnG/iZkHAZCNBBYoAgBRCae2kP247aj0xOypkznoFz2EQw1x7lWWtJPZAyIIq3eC6QpzB1BLIwvWd7OOSWWDMSSEoxKALyNZMH7TDMANgwh8LmMWWADzAaATw1vcD4DPvmg//uUZBiIk7kYQwv6MlBFovkad0knTMhfG1X0gAD2jCRquoAEFn33ZVDSwDNV8xyAUU1LWcQHASVimYoQcMGBR5AwNVpCyEqTEix0qYBy0wksnTf2LbZvdV3xGt6lntyQwEN+5+1/mn9I5YAFopOIAAGJ5pGbTdG5fMGJgKA1EzF5BiAQYKw0Qw48y4xdKVbJn8LLiiKBUQsE2jUmCdMzsYw/Y/vJf0x9AhfqnefrTqCABbTjbMDRBMDIuB+U0lUq8MVYBWjBYA4Axh0LxMGHBCzBAwREwYIACMAzAFTAcwCECgA4YARwE6deYfhAfLEgoSKgqyTGW03XD3Jr5qlWhmiJXnKzwb1q/0Xbe+6U/6naP/U5H+okC5AYih+ZgwYZd6GLP6KhuEYaCggAwVmCQYBAZKKqqT00/F/C+JAluhovRJK3Rt1FSShsK3qWo8yo5FrBEgKMgEARKEDAbCPkzRgUoMrbmkzDvSAYzRgbrNBHFiTE+QsswWAB+MMwCwjA//uUZBYAA64YRbZ94ABNIqm/zbwADdhrFrn3gAEDiOh3NPACMwMUxFAIMMD7AdjAKgAliRMAZO6YA+ADNoYiEC7GSZyAFKM98T0vaMQMGt8brZ/PnNc4v5loC5ifIUN+33WN6Cn9ci1+aV9rNSQAAAHeHiIiYaOMAAAAAAADK+U78fOEaDu30zP2NJMDFg9dhfZL0WYk1iZEqGAgdcwUMds/WUghMSxBpMrhMiTloW4nj7blilLVksYZ0gQEAAAGPxibhnPJIuaAEarGbVinhlbMiYYXWVDGCRhipgbgBEYSGD7GAggTJgDIBYYCWBWGAkAALjKTcUoAF5YLST8nx+F8Vp7qrZoIOhKSKdQLZgRJo0bHmxn51j7hBO613//////3//+d7v//WAGjLbprrQLAAAAAAAAZSQSrDD+iayZNGcxGYoCz0OmMOL7wlKGkL0w8vSH3zOBIhAZOoz9mUk+iMtjbGwMO+HKqYgwUUG7NBvYvTYYSHc01YVFMJmFO//uUZAqMEz4Uw4994AA74umK7hgBy2xREDX2AADzCuOmumAFDFqhw8wfQCzMAwDVTBcAE8wDYBgMBWAEBoBTMAtAD0s0JEurWTmFOWRcbFuN+xXrK09ccUrNeaNWaoOi8Ug4PU4dJLTX3N/6LF+3/q+27/4W1awAbvcjAAAC4xMrCA1vgyqMTFglMXDotEnOUAKKsYSBU1jOst/JRLBwIRSscyzju3glAOPtVaWsOpOd39wMYZ8FyGYdI8JlcUFqZl6UOmK9grZlyIDUYJ+ANmBMBKpgDwBOYCAABoFGAMACxgEQAAlE79NdzNR1EVcEJIJ1VIqJRk07SaxX/oxE469iPs3/X/T/8l/+n//pAAEJ8DRw2DFP4j5TQDeMczCYcDiwyzAAIB4GUJacpggEaV6Bz6U9nuNiCsE5ZqQSkSWp3eypITy/RkfKOgAIDIoKlA03sUaMq1UXjAtjAw09ySqM0eEdDCiQNkwB4DjMHVB+jBLwFgw64KRMCGAIUynF//uUZBqAA80axA594ABL4jjVzyQACpQ7Ft3mAAD7i2RruoAFd+BC3rjHMSNEIGMXQsCGOAk5O4rOUrnD14V97riuv/bHxeYGB4rNsRGXCy5Jx5FyOhHdt3D5h72FKpJvoG+tzE/s//9IAlwAABm5rVGLQOUZOyJRnHEUmX+CmaqwpJhcAxFAHxguhMmBIACYpYM4QBBGtJ00rM6cUAIDZIDuggIquMCQ3vcEQzu////////+kBi3F8YvbCxxA/VHPzwKbICzpmCA6mskIeYLAJJgzA0mF6BgYGwCJgPAJFyhIA5l03FO1VoWmRMjLNL+WZLwmI1Tcy4ee2vZ7v/3f8Xbf9f6eilPxgAF1O0AAAGEQbGJskmF/gmGhAmJxmmtIyGFoAAUIDAcBmApquq5MFRqm/iwHoTixgteo6+6Ypy6J0LQywiL/WQAAwjgD6MoXVGza/kU8xc8DNMGaCfjEmQ1szJkNDMj6T4MXQE1mFCZgIW6UvDrP4qJpACRIGEJ//uUZBwNkncUxBP7STBD4liBe0kmClQ9DA/7ggDai6Nl3BidGmTEv8arZNMTWFA07Rt/////7P///9dAGBmNcAmJTOGiGMwkQwwjiSTQmJaNpFNiyFnauguJVoTpVfHoYktHgiRkQgcPFDzeytaTUQVbcBV2f///9/f/////oMCJB1zLUm7k03uhQMFfFGjGROhN/U6w562TPS2OKgsGnUyUFhCGQ4OShWiX2d0L6Q+z9zcez0By6bpxUKiUFjA86526X7+n//+n7/69n07PH7WJJ2BjCUhnN8R/CtRsMI5guSRqAiJ+Ia0FiCAxBM1t04VDPzQQOFhexWdz2SOc63q/rXVlzCZnJQEDDSw8gzOhEsNbBgGzKIhEcwZ8H7MZcC3Dah4zSMPRLAIImOnymRAADwK8i0IxBlNMiOLGVqWRR/2WoVvviqETDXLmm3f/6f/b/9vs/1f/6+ABwSpk9jbGyBGqZAIIYWDUM78M885ACFT4JzCERg2XrQnPtgjm//uUZDwM0pAVw5P7STA7QeiBe0wmCixJEk/tJMDqh+IF7SSdzUh86Pq0wTLL8+sEQYCQEcM7Nnn7KmVUwJADsMWtVWDU4h+YyUcFeMHYBqDEKAik+QwNzKDHS4woYMJHQ4AMJBGaWJVSbKGAykZKXkPLL8mBOKwuET7hf32qxhCju+7v/0f/9ux3QwwfhszCnZ1M2aPQxGQKjBkF5MLYfI8VcCijTPx4qZIUAQaCj5O5HZKGxxwgFZLeNOgyMB1dwpNoXvyqJAADBzhIUymM1vMk1Y7jIfANEwUsDyMW5B7z9S0zDEOlOzDwkZCFH1pq0RW3WmIRQpqRtlNE7ZfzzzezQghgtb2Ut2f7f02mP9X1//pAAiZuAAAAxBIM2geM/NjMz4EIxCKj2BbQoBIEQ+eMuquQBnAub3JiFk83J8zjy8NlNW/MlPW9qghuaeQoFDmHJKB5hzK+mYOmCgGAugpJhP4PQeMeYbAElkYDMhFMUmamy8/RgQYXpI9y+pZM//uUZF0AkmoUQ5P7STA3ovjad4YXScg/FM/oxMDJBqKZ7ZhlWEYFIXktm706k+Y30f729qUlj7Xd/9P/9VCAMCsAUwxEtDMqNnMS8PcybOP32TIwYtEGCA8UFrkLI828sEZytBQOKB+WxmaShK1oVSyMFPAfTLpjaAyNpqsMRvDNzBSAh8xqAHsP2CTc1AykuBxIYcUl6goAW5QT1OYswCN11t1kl2sOSipJtGgglhPc1GmtM7Vu7ea/szx82xzOV+gan3LAGQAkFIZSqCRiEOrGCQJADAbzGFCIOk8ZoNIZFkDOsFgKFWFxzjzgZkSTQDJiTCXxfvq098wtKrJgXoWcYQSrFH13H8aVxgxhyjuGM0R6OMDESw4EjkBRDME+43aw7/7yw7Q9zqAALyAAj0yaD79ifWn9/+b+j/7sm7XT//vgSBgaBEmP6rSY1bSpiOB1mg55jVsYIEhcYCIFPkBAEMugtaZA35brLSMSw0QAX/MEOs1fSjCbAgAz1Env//uUZIgIkqoOwwv7STA1gdiVeyYnSRA3Es/7QEDQhuJZ7ZhhNqQlVzE1R8cwSIGEMffApTrzIx2ZQ6BgQYWUlgHFAOll4CzfQSSYYUamTBAKmz54qPcU+7b1c2Y2kNCshtTn2pVkna+lAvqyObSt25jfSBFgDAaAMMUJCM86LY2lLAx2BE3yB8MIBBGJCA4DBFnNLceWG4scGU4MSwpg/jnak8A2C13NUYHsEemAXImZjDSTgYayJHGBJA/4VCCDGYwwsZEbeBQxJJesCVJztYSSUSSaUQIFHvE0UGWV+wj0buv1/939v0el/9HdcYJIsRj6K6GqGzUYuQOps+EbD8rkMVjy4K3QUCOo1iD+h/qI4EBQJQYEzx7HhaPATET2v////t////+uQAwqUG+M2+DlTdnzUMyZoXTMGxA2jGFgWM3vjOtPNFNNJNlqScTr39c8DMK0mnBRqNf5n79exBn7+uhfu/31fq7Ebv7e1e//Z0jQQMwkhgQiSGMShIYl//uUZLIIsrUOQoP7MTAyQdiVe6ILSRg5FS/oxMDsh6IB7YxiTQYsIiBTKfnIwsJzACPByHi6ulyhVHcNv4hHHCQkLXCCtztvs2/7f//732en//7QAA7pG5G0QAWeKkANMJsxGBAIFk90H2Js7ZYjQtM0K82a81606sE1ZNBECQJgwKV9M7DuUsbl9WG4vADOJBK3bh+cwsTbK1Ttu6ADEfNnb7CxyaTe6+zCxyd9YcHnX/Dgw5e/PSJYzoYyHnwH/+fAGEoD2ZSqVJtFJkA5Ns0g+T4lGMmDEAmkOi4hBphQHp1ORPcmVHPEIFYRRQPqmXrQjevqt///dZ/Gf////9dlAAc2sttsiQCKBlxxDqUFC5gxw1sRcM0C4ywkOLO5MXEMRsVAMdI01jTeLWssQlP1Kpt/YdeJQF8mBFvm7oOsRdVpTXZa+rWWuxBOZK4HwHNntDonbVay7VlIIz0ZJj4yVZCTaXOnoTFS17LpytgXa7Xvlk96gKjS/Nl/9dBy//uUZNmAQlQWQwv5MTA8AViWe4kWDcyfP65pifj/B2HF7gxgbNhed/nBQEAkAAwZwnTGcSxNMMrAx1ggjKpwPnGc0YMkiTMoGRXSugiHaJYcD4lq7kgRElW9vL7xV7HEC4fD7si2kdAUMBHDwDBqVAUwrljnMF0CTDBpQdgzeUElMHXAMDA3hfc6wvTNadO0WExEEwcMjLoEFgGzVGVR1XzDUEiM6esoQgZURi6rpPjnnsuAqMNTtOW4xMxC0hW6j+73f/1///t/6rY3LQHgMAoL6tFpwF+8MXQut6AN4xQvSRE1ItnRzC81jRmsCCTtQPCfaJ6nTMk3Sh0KoyOlkDv0TjOC7sI4p0szHZXVMPx5IyjBODHWK1MX8GcIAjMFMYAwagcAwJgwEQGmXQMy5WJYGUuW6AyMFHatYlQpWS2FPiLq3g2hubTqNi9oMynxIHiFNX3ed6qO/96VP0aWRmjTenmBsXrFEAAApqNuNAAACEhi7wZZAIAiUlMrJAwg//uUZO0AlCMvUutZYn48IciWe4wYDHBpEk/xJwC+i6f1l5jmRwQrYq9TrNBg+JS9r7TyWjTIiurPEh6hw0Ew1HzGjrpl12766KmOCM9q3fMgH5ENVeTTTZB2XU6qAubM2cPITiVEpAxbALgMHNEKTOmwjkwbsRgMURE5TDiguMwGMAaFgAMwEkBVBQBGYCUAQmAigBYGWIKHA44C4MCVA+URiDgCZibQueFqDhiRC0jJFQsi2FEgY5xsTguYqLaREuqNkD5knWYOo+i6CKloJqXRroIIJMmksxTXTR0mQT16nSpUmToLTd1a216LLVSpKp1rM1tEzlgjCTIQLBkMEywhtEZECCgSef//7v/+xhrzpimqE0CZbxfZpcBiGha5eYaDyBicg3FYk5mfiSmCABcYfgR5g2AxGBkAgPAPGCWA+KgMEwDoAAZNAvQA8FELYeBWHhUhosRPwqCmKZDILnBet7BMwk5ZaatGxT4hTAYLtOh2OIhppdgjYEImUKFT//uUZPEAA3caRJV54ABDgwmdrbABl9kbCBn5gAJCDaGDPPAACHTChZA1rqCQwgXWdrcm1GWAdKC60XOxwhVGVJ//6X0ZAAMDDBozGVR8Yz7pcVMbzDpTAzg54xwkYHMFUApzBEAVAwwEFEMDQA9TATgLIwB8AmMA/ADlSiQAE366l+k4HKLGqqp1b6hepyMyPbPabp9z1kn1nVK+3k5swz/////////QAAA043JAAAE/TIazgDjChAqiMwPL4qCoxQ7lep2vVYjbwEEwlNjWNtNHVMDZuv9bospaLxnmFggLJg4gruYrec4mVXMjhgyoiUYBCGTmRdh5xnWMph1KBvaARg0OohK0umhq3BWNThtllRJsjPrdLGY4QQIhA2zqe2z9q5vI7Z7f9z///Xu//jb/uZ/9KRBxAwdB01QMI5bgwx0AYxbJY5bIkx0BMKSB+GBgSm45MEPK/b0O3TPHNTS4RqVchzTf73bedgdx6xVp5WgqIAwO8MFMekQgzaB0//uUZJ4Mkx8aRJd94AA3Avm97RgBiuRdEk/0xYD3i6Od3ZisQ8xngKNMETDlTKDA8YwQAB5MAwDQDo0gwwHNWjgiDDEBN9CUonDL0ZJmxVAoGRGTjTIjDQMpqbW37jeI5HhWzp2f/6/9Vn//q///6QAOuvwAFXCC4MM4QUElUzMnWAgow4g55yYZnom5bEorHXhi0xbjNSmnMVuiBtGdZcJh8LyleSWRAgOJtNMwOR9DIKj+OECbUyiQrTDGH0M8Yp8yQmDKOmN4FgECIBIMcAQsAkwmTw8zl0XtkdKHEjgdwJ7qXc2zGiBJ4PCjffdb/TrT/3fdkv/s+r9ZYGIRRgaRjpC2DMEIDGgazdQIAkYCbi0YyMCEuJxz4w40RmwIcCQhhN4SIORL81VCvRJVJAADBBA6Ax69M2MF+h8DCYwbIwMMGMMm/BXzQENTCVtDnATQQLxZowCBduoCA5Dm1hLJrqYMWh11BlcgIFw0RjhmLWdyuq2gUeetc1GJn/tl//uUZLQIksAWQ4v7ScA4AulpbyMtinRbGU9wRUDKCCNJ3QylvR+c06//Z20AAAAxqKAAABGgQdU8+XTQwJAJGNMJVdgAoeQKBEobBQKVYTIh0lAGrGr1LyV1g4HkYoIgjQTFFIDBFglwxlNFFMlTclzHfgfEwSkH5MEyExjB8lTDUJTmgGjAUbigWAUCYCAhfqCFdDDlgW1jsPEl0YXZBWkaSOW5WV8puhJh9m5SbdIx+dYxGv+rsxlE4QR2+r/T6b/vwuMaQTmkb48lioYaSBqkck0AVwuSUUrJWmQNAsDsN3D6QGLMExJRA5kZekJAkZfASWkFJRVTALQBsxn5RvNQqA0zLcwUYwlYA2MgaCpRbCDHUzzn4EzBolDAEPiqBANAZHWwrl0Wf217smEqa6JRYygVBFRXN/9UmSzbOMvShhWdWr3o72LJoz1l23GP6qqX/StS1tcCsSPlGChlckkgBACcYRixQ4/kYYQFxgrhFmViGQYYdmPQJ2B+FwAK//uUZNaMkssWw5P9SVA14gkNcyklC9hXDk/1JUDSC2VlvAzmEoQChQAaewWHm1p2fM0mAsilpV6tTmOYMN1hoNm7v1//9X///////262YRqBkGU1IThrpLuwZKEBjG6NkHXs5mXYWGNDmmzAigICgMLIKEEiAJkM5GYgu1NZiUGGFIwRwDlyY3cenv+Y0ukbdL31Iy9FC0XovpawT2OOVf6nV0SqnI5e+6QU1ayVgpUhTAGAADcY1Ynxl7rKgYugy2qzuTyGiyZAQJikHLeMHApHRWxMFpdG5cWrSZ7SoQCDiIpAxcLzclkMZu3////7//+3/0IwVUMYMZzcRDBvMpEwrcNDMEdBHjI1wYg0HBwwo1k1+HcFAkIRMMBQXVkQCggBFxp5MtUggnbUSoNEqYeAMSHk2U/imeWScSBJUmbQJH2qYHOEmDlULtNqi1J68NhdRwy9CZposmxgvGtYAb1HArOwkKiLNsf0tWAADAADtMQlO01ojkzIDBtOwyD0//uUZPMM01gWQxP9SVBHoiiBe2YqDJhZDC/1I4ENCyIJ7gxwMszgUIHcy8ULLmLnpMYL3YVHocnYFhdaWEosmxMzEv9KqaOZjhBD/////////pf/3fGIoQuaianp6qVBGbyBSb0qJ86SGWBWYhdYBDIkHjCAPRCTphFwWYAtG8zWvu7T6VGpAoGlLBQgNUv3fpuumX6d3tS5bLSxn/6e0WeO/d9PAoISY4A5pnUwZGJoIMYPoRRkBgLGOicy53xgoMyikc1AGvOKVBgyEjZA5bYrzWhSzaXFrwABOL///6f////+qkZGBbcxy59BOHPUKjJqBgowfUBTMVACvTEwTTGiAzVUDwAFpjYCAjA8dANezHAFJFpTFZRWHZ8yydNjlfIuZ2Z7ctAkWwtPODodBBAVaYTpfnkuDLORaczwDJuYsmR3K5ipYjDryzFKSQgQYkcgks2wCCxtBogDB7DUMT5QI1WZczJUFZMhsE7yijBBFMHDYHSpHIHBcMAqTKFC//uUZPOI07YVQYP9SVBCwtiCe2McCkRDFs9xIwD/iKIF7KSYesIfWHnShp9xiBjFccdsKIr5oXIvY06RtN5Hb/os/d7N1d6O/20U//91ffGDihCRkwAu0YBzG4mBFDBJjO/x8inJgyFhhcKpoeBYWC0wzCZsjVKRXK5Yuz1+mGQMNuKtEc5RwnFxIlRdotDC3xyYuhDmOJSJ1q76zazCZX9rZcwKnmIKKASJhDYkUt2+5tiBTMbIYMQFxk8moBnMBlXhOGEkDKZaAPxSGmLmgkPCwCFwNsDqLOjncH1gWh1FNq+dTtuYt4/fKU/1VN2J/2+n2fTVz27/sv/9dQEMCKCRjEcWnQzyaG0MdFFZDh2dOZ9QqBcxufANowQBTDQpFACkxIGVuUyeHRwJBRs5MVKRcpNQ1Ppf3drC5uv7cc1Gx/X2Gsdo/QRRY8XtV7px42nSuZWZMEYDExMzhzLJaQGh3DFpKM6VEoGgiBw8Z0IS7ioIk5saVNODKjo5aUfd//uUZPuMw6oWwYv9YTBQIjhhe4McDOg7Ci/0w4EbCyHF7ZigTTvpEBSWIMijvOe1f2a6mY/m9lPRZz1znPjXJBfU/UuxdAqyyYcwThwToxnufCGb7x1phwggmXsBGVmAJUPqhYiOmBISzlPWdBMU2iH1ZTrO1QDBIFFDohPklqTuOs2MFnOAGtZYQNPRXB8znTSu8XSg4bqaGFQvrUlE4zuh2iFJykgDBrB2MhJOEzRZlx4okwaQGjHJCUOWcMXAMQaQzLTpPwWp5wRPBAXDiYOvdf+3GYSe9i4fBhDBtmrVF60I96u2n+X0acZ2/3K/ivU+pKowWYIPMFxgxzX4IgEyccPqNwI2MdoHNGNjUag2U6DDQx0bKAUMB0gn8cOGnjmIXL87uf1d7yuEwVQoWDJIEu0mbaKixBicWCugw1tyLeNU4Wj7lmpwrYMZeWPDnNmh9Ax4uo/QxAzRiQYCBpJlCo6Gl3WHqHfG1okmvG54lUYS5gjBKJihImEwTNX///uUZO+MwrQQQov8MNBOoiiCe4kYDAQ5DE9pJME3h2GF7TCYzys2KWao3pYGQIXhISDDYHmWdF57Rx+23Szyv772/f/dq6a9bndZgiYAgZOkGAGWDwsRgggN2ZYi6cFicdleYj0cMWYc6NEFD16MHnM8IxTQ/n9YkGSbgeAxo2q13as1qemSbgYzoT+jTVp71PEdOj1KWqR5yLz/WupiqsqQg7GI6h4Z6KVRjqBIGNqh9lqYCWKHCMVULGi9U5CDYU5g0hQqbef1HfbB9o/kbB9Lmyenn9cts9zl2htWrR3bNq+r3M0vfXVRCwwicQqMP8Y1DUn2IsyEAJkPloYw6mjavjGPiOUj6FyiFrIHedWQRaF2qeVflrHdM00KzRuePPpVgGi5QgJC7ka7pyJp0uMGKeNcg88IrQTHMaaHMFH6ChFFbmsjL2qASZHzYEGBoBaZBqgJnzTmmH2JeVWUeBD4WQK8TMBTGxNUfOGYZr353UttEAZAcCBQ8KbScakn//uUZPOPQ0kPQYP92IBIgViGd3kQCoAvCg/3QgEtCCHJ7aRYOTfbmro3mj9LmBJVKOxCU7aEUp/Up6DalmPr202AUgBhfALmkGVYd6yB5n+jxmGUBSYRwDh3Fp22QfQP0YO7FNk4MOtGTRghIYORhS7SLUMYax9dDK2yOC5zJ2uQGNEJo8fMIEakZMtMnzZhuAtDoJiF6hVg4W1NMmpQWdfprYbAKZqqHAkTDMLzMchGQwuwmDJITG3DAAQCOLKvIFQbOHjsYiyOi4LvLwukyB9FiUdZYxz0mfqqRm57cnqt+1ErjNtNyUjsa/bu6/T3KgAFbbbbbY2gYsnG9Qppv8YIkG3TAZqkTWHQpkxl+mSwCaZl5EhkADgmR2B0ZuCfxm3B+mOKF0YjQaZjojxhB4ph1gbGGEB+NCnGC0BoVgOGACAwCPTTeHTiJcBmhOwHhN8IHSjwazw5cWQFjwwBVdA43ADjXNqIyHDHXACANLEQCc6Yim6dC1F/tRac01zo//uUZPkMgxsNwgv80IBQAVhRe5gQDFxjFk9pJcEviCJl7Rhgchl1l1KRVqX5A92guzEcbKypdK6lcus6sKi0ZlVDKI3GJfRTs9S0FDc3l/bFvC3zLG1ax1nh3nec1vv71vWq9QMPfUWv/1AAra7bbUAACaL+Ez75RCQz0gAI6JJxrGcWTJjUSAiyl0y0u+VjF/NqERYcaKa60qRcPpaUx3fcfcbeLBPEgkGZwngiDtq7AANId5h7dYYl6cIyaOq10QW2PK4MGNMQRCUymgFjHjE9BwtRiODWHDAoGYewMRiqEYyAJr0j58sWJg4RgXDUwNFU1IFMFAICQAdYoAVwYS06PyaV0Ft/ZuzdsU7+yKvIJRF4pRzPLQQZFQEVEYJbHVNSW/8qSAAMctskAPwIACZYlDKwMjNY2mepQW4DuN1Yu0uG1ApGYLiW//QPdlFlXw/d3OcHgAMV2TUyNF1jSKQiMYMP02V51jCJBbMGQG4ChPmZIL6b/wZBnWWRh4EZ//uUZPWBFq1Azut+ydZGYyotZ0xVzrhpS+17prC5CGm0/DDfkObB+qeJjaFBg4C7ODBVOjCwES6ZcILgICgJlV2GmvUmNCAaxo+5n7lnNIVRZCDbzJDv1WAAAAuRy6AABCeNNFET4N5jZnyrMq06SN+TNV5naZlAK9AlYzVCe5974/xPD9EOJhA1CfiMFWGI4qaZv5XBkpoeGIsHMcNzNJiVAXGBGAeYIYSRigLuGlAF+ZZBRi4UGCXuY+rBg0Plv3ZMZigMMyEDdWtJR82maGqByOM+U/fsilSaGlzkntK6rRpv/2gqAmC6brJaTMTM0toI0xZhlggrGRdKmXq/oYdebVBwmRaKEeGhth1n5leSiTBKTwu59Q8DAAMIuC5zHvR/8w8YtTMA9AgzR3xoAwaUBvMAIAKzAFQL4wNkuOMMoBkTOsbgUEphAGZ4sopmIEgQNQsBBgcQYkJQQFjREEaY1NZpXkvU8eKBGdbs237M+JqDTdf1SnLAAEMNuSTQ//uUZMONEv0YxxPdMlA0IvpNYeYdi0RlIG9wyQjIi+gplI2OAAFScCqAsvOpZRCVzFhq6o8vahTVxbRyY+uqkkZ6m4c37CDjPv6MQo4m1AzKmxzBGHAEZhi7o5uYjUX0GGSgIJt6gfMYSIAeGJyGGYYQuxlH6EGmSOiRBLGCwAuYK4RxoIjhgoGYxQmJQox23ASgQgZiQWOBIkKxiNy6xqVyePyuxTWu2t52tYzeMqiWdKK3jG45n6P7wAA2bmhhkGcPZGc+BlQicklxj4FoIEGBifiy1QANPTvAw+h5yqUkB6irN5CFy3LIbbCTDkv8t/3MqDmBQ84e5SGOA3gGARFjAID4gwSwvmM2/C7DFBgoA3es6nMH5BGDAQgH0wFwBmMO5FWAUmRGaQZmMoRGIKgmeF8GQQYmFQKFszF4FTIIEREABZZlSPsAUTMq1yUNIDF/V163+/WyH1rdHaDligLA6esNdp0DokzLCf4tv+3TZv9fTr3qUpLInvrAACZN//uUZOGNEwYYRhP9MlI1wuo9YMKDjNhhEi/7ZMESC+Vpvhh05AAAQk816ETALzJRQa/6JjMBBQNMoETDes8QHctJIWAAdOFYUp59QEKM4Zu+Bo99cH3GOWzf3r55NY5LlT5fyrrW2YP2WFGApEZBkWBiKYaWG9mqFJXRhGAJKYDGAHmBMA95hOI9CY4KB6GbAtmKYnGPQaHxDxmRAHAobwECRhgWgEBtrrX15l691AR1fyYtQKt/4u7putdts885w052f+j////+sAIElyy2gsoCQ4sUXmYuo50oZeqUaImEqRZgk7xj4nQq2Vwoa9rA5K2FCjdumk+cu0U0VytdqV2qMKTEWDFPhtUxCI8YMCkFRDIYWucwT8EIMAIGcwsQdzGakfMAIbIICdMDoC0wZQVDQfDrMIsBkaBDNgA1FCAcENAaMYWFQgGlkGPlR26rxPZKrVq//aL+fN1InfmcL1Tfu0e6v97lCn//////3+kwFgZTKQYlNwSngw4TKj5F//uUZO6OE+oYxBP9YlBGYtlKc2YrTFxjEg/1iQDgDCg1p5kn9NMmUO4wUgFjAqCPMRSY0zWwtDDIRKwaYEeRv3DmCxawswEAjE5GAwrKwOvNIRj0PXD6BcVS+MSRfpm/9T81KfOOBkybR///1/0///osYMSIkmOFDapkEI9yYOcFymPNqShgCIG+YBCBPGAugRZgoIvmYjYBHG8BGMlAwbRTBX2CpVLrJymDYmCj0EAZIhDFEiE1gPGK73SqelmGb/aeysDLq28QGDqj8Yl39JDEjM3NkqOM31TnTHeGtNij1Aw4AFTAKBMMCgGUxwGITAuCaMDlswWJTE5xOJqwxiOxYMBwhKwQYzAinn4nm/oerGAV4CET8/bd7u16UeS3R//////pBSoTBxx6UxUQUOMSqGKzBMQj4y7BPTMF6AgiYA2MA/AyzCBAy8SJ/DDAGiYIzCQPzjgbxIjQgAWHGBAkhwNPAgo0tZuEyKh8c7gtDbUvz+Mz45hNg31f/T/+//uURO0N03AYw4P+2TBdQviAe4xIC9xhEi/xiQFXjGJF7hkg392r///0AAAnJsAAAZh5y9GXRZGB4WHeRjiwINda8YoBKZSBWPBSsClKYaBeiM/7/pqtcmn9Fl/oX8o8vEM8JQ/azaYcStUvEdVu8IAweQn/MU7NYDA+Ug4wnsGvNslYsjCgwVkwMoBCEgdYwfIVOMYcB7THUOjE4NTAMrDkc7TB0EAgMWZGFAxGGIIBAFqAJpLhyoQSDEoIFE3Q2lWqzzVSQIFn0/U2ff9/+1/9P//+f7PSBSkAMCxlMCTvMvxOBQeH9pKmEIDBxCY+HGFGR4wWXMT4CwecSbJxyukIQYKwZH81C67TZlxaavdbXdRcHDRLW1RVoAwaQSzMhnH1zCUzzExJoBKODuRBTCmgKowNcBVMBaA0DA8CWAwGwFnNsnYwaBzIKFNz2hDRb4MCRhkgBAWL+FrraEfL4CnCvPlzm4173wqrVmzAkPFv/sQ29rH/9f/+2z/6gBJw//uUZNsMkwEVxRP9QkBFoyk6Z6YPTQRnDi/1KQEFC+Rp3aylAAACEJccCnMAwXcGgamiIRoYNQBtcAgIAQo1MGR4HiwdAATIkSNAl2oipguOWy4DFXVZTYNvlk79SiRSi7ZjCGAHwxPkzTMn8BDjGHwdI42sWCMMtARjAxAC0KAYBgWpHUYsqBchiCMTBIx0hj7BZJBUCQIouYnKoQWi6Sk+qmq4EhDEwHRNG8v1fUi5ACDkUrdGdnN+yz/v0f//9X73gW5UDFwmjDdDjDhdDCcTD5PATE0BhIBRGABh01RQF4GOV2CijUtIg2cwNJX+oqZMR6eJeRQ2c+w6E7UlclRAx9qqLmaTAHhIIwXAO4Mg3IvDDeAac1zobMMImAczAoQA4wHwBIMGbCkjELQHo4gZMHNwtBHavokVJGtzBwDIoMeCZU47kWHY2trX/zN3OYNKhZ9RGmRPL7PV//Z/+j9T+2ugAARjYAAADBVMjIsqDMsiTFwZzsT/DAoIwqBg//uUZOENkxoXw4v8SkBAotkHe4MrTChZDi/xCQEFDKQp3JkdUAYxBXYSdcaCAAKpoclICjcMONB6uJXEkD40D/xtqm7vsXWSahiSaspWGBsjlxhHJkQAT38wbMM+N3BSvzC+wLgwJoAiMAuApTBTxQ0xYUA8M9BMyOMDGp/Ortox6GzCQEZgYKFQ0H0R1LGcM1o86CF6kk244vjrKERd5EVJ/Z5QfR+qrZ3drvYzov6/215pcDA+IAMNcj0ykCuzDbC4MouZEwVgSjAkBTAgBDNFWgkzkvy5QIAYykGmGnphwwQDRfMMMxbixe3iwvu4dByPSnYggg+QIu/lAAALtYRMXaY+uezWESMMT8gs59rJDDABjAIHGAwgmK/PmoI4AYMUE4ECsMNSHVfMQGQORLd2o+z95br1b/0klqfUepAASDIHc17/6o1CaF0Uav9XXu+///6mgAAwUROjGcFjMQM0IwKAmTJjgEMHUJMhAQXIYPZDJjSgeByonyFQM/MY//uUZOuMkv4YxRP7MkBEIwj6d0ZHTNxXDi/xCQEfi+JJ7oyxBIM6i0DCQ4eE3nfpfLbft5KQfOrhVUsGyhQx7goLSlSYCeAWGFMEvJjoIdMYTQIvGPvqlZgBYHkQAKZeMwGIXvMEiAiBs4YMCYSIArAGer3Z+ISIYKZLIm5rWz9BZuiVi7coQIBIusAU8xf02dHo6jvUm+5Gz92NZ3O/r1BmAwnQOzHWHIMmsrkwwAyjZ2hHMEgHowKwHzBABBMI020wlwNjVAxZuZJSey8IjK2EGzI0U+3/gdw62Oic6cRQFUvLbf97PkrNJXf/96oAbULSYLSCnmLvBSpgtZfiYH+KWmPEsBxgdYLGYAgBKGAfgSpgYgmyYdAAgBh0giCB440ThpHp4wQEs8kctUNahzgOhk4n2z7JX8cuGCIOs01/X36fvciRWjt87dG6XWp17WM2X2MTNgAASsAABY3zAZwDmAFTNsiTfbWyMGcAcxGCIwdEMy8lkzaFwBA6heIR//uUZPAIkswRRsue6BhHgtiie2NJS/RZEs/oaQE2DCJZ7RkgCMjA9Wop9lxgECqLLxvwtFWDv1Mvx1Y3c1Mat+UrMqrS00YI5GZjGTvGZ5TEYeaABnrCWOYKoB5GNYdmAggGgBen0gvmHYECAAzBEDTKAbnSXc5QyBi7o9KYdW5Yys2z4GAJ4qbaLhI6wkhn/usVeoftmcchpX62u2bHJ69iaEQA+gwKBjzAfGPMLUtAwpAZjoSPrBw+JjsRjoKNxJ46wNQgriILmBQwPYNSpnLT00kkX+f2nZrrwTeQUIFIsrE4cFgG4s9VaVMCMDwzFKw5IxasRBMIuEjTe4VQwwocD1MDKAZjARQDAwYAVKMQUAvTeU4ywNRkOumVnNcVwYSIsifZ5IKhnDIUO3ACX4ZmedN4Ofc1E6ogpUG5QqE2tPNW6Klmh+cqm2XLxiGVUaqVWP0/a5stJwAYC45Zh1GQmLaiiYFAIx6ogtCRURhQAYoEAN0Qx2A4z0iMcAAY//uUZPaMsz8WRDP7MkBJAiipd90RTEw3Ek9/ogEKi2JB7gy1gmvTha+WxIKCC144/jhPyHo4UAIZZTYdU33/0atf/6u3///6P+MMF0EMzEP0ZszFg8xMHxDgjtgjuMIIaDEBCBMGIRgwUZwDB9GtNtSjOikhPj22IibkBreqACwhOtwVkc/uNLP37lSrvdZ6klBKLlj7Ul3KDRDQLlrskrs4udbl0KNWMPWvcuWT2Mi4tqZtLu8qly0eMALUzBjDpH2NIqc6vD/CYeUODswsCgxXiQ2JGNKwrA4wGEIy2DEICBnkSLnsTlkPzbn/r8v7vVgPtY5RZtCb2B6j+hVADASga0xEQVtMaBRADCkwHQ6BkXFMQFAiDDCBrMAYOoxr2kjTlCPMaFQHPMeRsmYNC3ZAoMSIQqagOsARgRQPDougLKDYUe4rite3xSpG00OdQf7WdEUWpfq97/sTVtdV9MoAAYRGIay00bVvcZEHMckLWpg/gMGDQRhAaGVIwHFo//uUZPmM04cXwxP7GkBMYXhxe7siDeRFCA/7ZEEPiCIFz3RAYJjuYYBiIZZiOkEzauKAmrRC5BjLt7/ndbrVA+ccJ27pn9P///0AEYdaHRlyduHkHmKbPxUZ0fCj8YgYB3mCmgOBgXwDIYRCA7mMHgSRvhpiRgMOm9liAM5DbGARKhcp8OflZygsbZhtsKCcDqQLn9JyittAHmbacW9LO1jlTDss+9d2bU9qVy2TsRckdSZWAwsLY3pVM2g00xPUM2iIejB8BtMOhgxAGTE/PPmmB8oGBIRMinYFAt4YUg6zSS4U87/Ms/3/26VOXOBFd2z/T/3/+v/3//tVCgw7DVjXliCPjpGM2IymjcOWaswnMEcMDMAYwgCjMDdF3zDcQOo4RHATMYwig8OQ9LdKlApQhHJKKJdri4osGwrYtRKbHob1D0kXte7S73KGX6A7VXK//pa9CUiE1/6VxaUAAMPEKMfZPNOKSMUWCMmWXEwUwiAAAiYBYCpgglCmEyDE//uUZO8M0xELwov+0RBGQgiCd90QDRw1CC9/QoEgCCIJ33BAXJDGxghYmlHiKKz5DIJYtFRTeWtZayz+eCzDrKrK3ev//pMCIAVTCyTVsw69AyMQQDOTTEIiwwRIHNMEkHIhC9MFh90wkAkjMCUEiQUPTgA4aGHcXiYCEpBM8dOCJZr6t7eGOWrQVWxhBQum96xVjllmn7Upsm2tHvozD4sOTNvFRzmxhTF4rdQnRlrBWHgFSj7zDN6jYAzj6cJTMBoTT3weBAPY4BUicYMBXZh6AGAYkyQASjCK09GXuCWigp1YTXr3v/PfN9ytjDyWpX7cRR3/92n8x/V9qP1/b/YxSnvMLcH00Zn/TZRwBMJAoYweZfvEYJwYIjKDBLM2PROnAyLUFv2WCwKu9P8EAHubKMoclvSgEA7rlxARTVMPQxjXf0a78gmzFu0Xfob/RUjXl/aIlaK2AAAxDPIxsno2KmswLMY3KIRTCYA0MJgUcCBkBvjaFGg+om9Qs63f//uUZPAM0woMQovf2KBFQfiCd9oUDgBDCA/7ZEEviGGB32hQchw072o9lfL6g4scHS5kuoHL//t////0f//0xcQQgIOMWGOdDIbibQwysTpNfLlYDB/wdUwtA3jB7D9MboGo1VAOTXQoyQtMbHDpXFnSdKA8CAiSNPKbMP0eMRYDMNUbmdhJrvXfnUuXLCtKd3W/jvX7SR7W5R7mtc+35d4m/Bax+f/jaX7ilFm7vYW/hykzHVWafvkgDAUkwvxi/HLGGqFWdWLnBiIgamKAUYDABliBC5hD3SgGtNHoJfOmAGoZiF2hpAo465jig5V3eIln7v9z/Zf///+n7zvrAABbv77GAXTGJT2MpFEgw+hdjy8qxMT4GQ/YwcoZY9B0QlioEUFTvEIcduHadLaNZ17lGgds0OclzSb2Sly99jqP+m+u+f0faO2r9Nn/79PAGE5CmOlIm2MGmigfHU4cGYegFxi8EhgMMS1A0oGUqk/UqTEAHepocCFqkoozhHoR//uUZOyM0qwMQ4vf6IBDwXiCd9wQDxwzBi/7ZEkXheHF7mCIYsSitbrp1zf/s////u6f+/9v+wTJVMYZg0ZoJx7mtxnWYNZHpurpqEYYuBOmFsBgYJIFhgeKtGBKEOEA5EwBA4AETDQKqPfDLAlRyuvSy4Im4nDzja0JF/tWmU+7r+VvVV/F9a83H+7eVdb3Z/V8PjgH0bGuf3nOffdRr/5q01slPZYk5Q7A9gL+giBPWZz+vk988OYMQswuQBAhUC4IYj5GXjL2viyAMU3tprZCAwrCtSWVkTptnq16L/Uz1ej/7v1//9X9z//MAuCMTDZjAUxF4wcMMjBjDn5A0Ew8QB+MDxAPTAkAIwwUsTuMN9AXTFSAsqOHR1rmmg7jwDoTIp6mCJmlsWIoWKIyIRVbrfrKZt1eq7u5lN1kPffIzZ2q3s7TOXtIh9DSuxO5HM7UVHdWRO12q6LMiotc0zK0+UaRUEDx6sVJAsMSssNh+8M+jzPMpiwxGATzPQYD//uUZO6Ign8LxcvZ4QBEYXiBd9wQDqAvDM9/wgj7BeJVv2xAGBkNwH+44GF/BCHiok/MASkkB4zFqeJSN62Cc5KyVD6vV6dvTZkuv//V0U4p/6U5MPUUY1Uo3D2JIkNJwbw5X043MLzAuDAsgD0EgHBgi4IYYgWAMGoUmHAA1mfXgpmxKAyAStWG+z8mKhgRAFjQWMiEc0VWI6DGMjrQsJbVjEM2mXFRomhZ76iVFLJ1z1qWSUBXNveaXkQisqaYaeLvMi1MuwACgZ2lQYWUyYUoMb47fJhYglGHCJKDGYmIfRKEsOcsDBq4ItMM5ytWs777naWUJXX7P///+///3U3UzArImM4HFI4msAzMvPbN68XojCfgXcyjJYxCCczx3k8LIwwwAdxgwGTOIDVCWhvImg8jfyuUXvx7+9dq1BS8sAhOxRdIAMrNLSRvePvWpR5MVULsCupWfWn1ThVOkqRkd5wato0hPSgDUrSfYq4AIMJKSSNopIumK2pzBkYq//uUZPiM0/pZwgv7KjBEgXhgd9sQDnwvCC9/QoDoBeIF32xAumyVsGBwEoclUyGSHOluxWCXWtz2F+qDbZlTWDSCVCqE9Of76fX/Yj/Ur/7Lupsr1vQYpwl5rYq9m/pvUYfqLBqDrnqYQ+DYGA2AQ5gGoBoYCWKmmBOgWRrhqY+CAQgOBQ10M5iTAm2j3ZReEI8VhRQaLgwBAyXeD4aFgKKcYaKMMhMw1+9sV0AAypww02nOJQhCC7NaK7FJRJrpoZcEFk1FZFL2AJRDM4kHOS3WMmGMME6dEwEAZgaE40YKmKJiXquqEOKmeN6WSj7+6LcQp4nCgdc1cU6uv/uV//o/9v+hFGFjA1CRMozAc4n3CzIcMnMRUa8jAtgUEkAgjAAQGEwJwQtMI0AaB0K3IQlTvuFA+YKowrK1cFgieoEFQmGkQabLHzzpd97g2ggRSITR1Axr+owgNj6KkTMlgQgXMxk+eZWhkfPNNlS4q8sxh1tq0AMAxsMhIQMc6xMB//uUZO2MA3gQQgvf6IBA4TkNb7oRDnQvCC9/YoDxBiHF33BA2bNYGTgwiwaTCAUAoRC0bAzUQBsoZEYBAcKjdI8VitYzDUjZziz/b/10/6v/1df/9nySAGGuK8bBiaBo7RMmLOXkaFtCjmDmAsRgJoD2CQAIwSAJdCCC0wcAGg8iAQmwij/PoqpFr1ucteGQeDQ1ATApmetKIOumX9o69L5hKBOanKZYkNnd6nMewyQFSLo0kJK59uiBXUhp14u1UGxyiiUgEADRhVi7GN0OIYGIhhvSNSmE4CEYGBpyGQPY2Gq6oHoEIFyzjMAVkHvqqyp6/4rjQw13u////q///9X6PQpAjAWwXAwQU26MJVOaDBdwW02SNi8MLHBMzIgeDFMOzSa8DygYTFcCgwLQCGxhWKKYzizSx2bzvA48irPqtLnZ8E9LB50tWGm3ZM4ivYLOe4+y62ucYzMmYycYVFLqSyAiNWadYqjHpDZ9l9MAAASzGGAzSsiDAwsTqPT4//uUZOuM03oLQpPf0KA+YWhxd9wQDcQxBi9/YoD6i6IJ7ZSgMN8CkywGAxOaFFDY6TBbqLrZPepsZVnrLl/1MTOCLMzX1//+uv/d1f9f9H41qzBkCpNOMu0wELxzNoDFN9iRVTC+ANwwMUA/MBOAWjAXhNwwOgCjNK0ABUu2eFMjW1CNoeyClDjECBySUHzqxAcUPIse9kaZaMS0lDyihpZpRUu06RfPGBVd9bR6I+TlWqIkGMVSEn34EusENsVYE3KEIJmcTvmDU+gYoDsiEZFh5R5iMMChHzEgeWSdGhBxY5NTKNSa9zd9gZ9E1Kf///1U6d//6+3/91UhzA1gmswK05HMokAaDFPwoQ5nEe0AQ3eYiABpgrgTGCCssZL4LoYAWzBJcwoAHEmKSy7jqy7gCNseUq2OnV7XgyjTdUvBC7NW/87dwy/7x+/utPu51GTnkdKHyafxXIwv9zTjeLZtK2wSBevgcf/qfFPDv8kAG6mqAoLM0UAK6wzSUDcX//uUZOwM818Vwgv9EVA84Yhxd9sQDjApCC9/QoDfheHB32xAJ1MJsAoHEKVxDLBDu/VPBCUu7v2QqeYtsu0YlAQfc5rer1U6f9P/vo/vY5qOv/9dFXRmGGBWaiwKRz20PGeUVmcEQR9GGNAQ4Q6ZkEDhofOB7SG5gUAi8ywDgqHbkVaRRqvuzje/zRnAuVTV5hnN95K5ezt33L9Cy2tO58n/ff75+g3/L5fV9G99g21FdhjuYqtu+LJqm3O6nl35X5AfJ+6mlpAOY7kRsFAmikaa9CIBg+ANhhZACZdsNxopenkDpHnaLnjCwtWgc3RmGvd6KLuv0r7P5D/xXX/3/+4AAAvq/swDwtDCZSeMTFlEwORoD6Vk1MV0GQxsC1LzNr7PQiQyBVNk5TvAcvDla3kTruaQiNAaA0IbKRkBqffFrqSAomITyxdcVYHxG2kWDOM3s2/TH1IRULPsStlNLAGEAsmcafnCDWmZKHGv2/qYN4KBhga6RiM2Bw4ta7S4//uUZPCMw6kXQYv+EVJBASimc9sQDfQxCC9/ogjxBGKJz2hATDgLGjsRuyQFEJUt5+5AtuSv///1Zv/T///2/9/SYGIAMmJlgnhlcRTEYbOKhm00PEBhIwLaYCmBDGAJASZgYYuCYQiBoGaD4FAgQDnJFSfLTrDP5PKtNZa5nCM59iQkJX8nv3//uVwh7OzelkLTxtTmZXmK+PuayAw36mH1b9K+Z10lPqNxuf7WkhtESlnZedH/NVRL7r2CgNMSINMqbKMMk/MjqdAdBpC4sQh474mKhy7ZCVQAwIFilavEKK9kB1HjC0tAlvfr/mPVRbpt3f//R9+5PdT9SklBoFmYRIK6mQMiVRhpApcZUK6qmALgtxgAQEKQABZgCQsSYMkBFG2HhDIwBwGTXEjVyYn7F1RKKbSQ8iPRCucj6Z5yKRU/55G8JyNKf3+flSCWptfD9flyrdR17QOc11D9qsBCotd0K3J/9LSLv/IZ5P/x+7oW2UFECjwzoWjnt9Mu//uUZO2I0vMKRMvcyRA+gThxd9sQD0irBA/saMkBBWGB32xAMwyxGXTA2A6AoCrgklSZEe2D26EwVJ+fBVdBYoyY0mp69Gn/3f1/+zo/t0fo/+tUjMqiPNToww4qTMGVEeTIgWXIwS0FpMOiWMQR3M4L5PFAuFgfQGFvzIEGGT4X5J9v9Y1OEZsx3HXnnHpnjhxc1cLki4uLoZn2NF4kONduYLu7TIsNnHXkutirMFR60HU0NMIDAFagZJhhm4Heg0avgJtguOGEaCKZwcCi5orYTsfOMQSnpLbHKe0J6ECj3VRjTVX7blM6vt7TCHf2f61f//nvk8vVAAAKBDATAG8waURoMUsLijA9RDo1XRmJMHYBYDHEeTBIQDO5UD34azMliqBCwU9BxtYXYlNN3us7nHPaiW52Zp0AnnI07CcPqMs3f/s+0KU3f2at37O7OGGe5mSqVL8L7q/+mP6WN0b+t6Nv3/M09f+P59CABUGmMzIaH6xoZdHFUoyYYADw//uUZO+Ow7oswYv6GjI6wTiSc9sQDLAzCE5/oED+hOHFz2hAChgQAIlwa5Z5BbVAgrS75ESdw2lwudEhpaF/6v///37v70/2W/+796QDWaVRwAHMCpCRzFSQqUwXAITNYhOwjB1wJ0x6DABDsZJb4bHDCYagEGAiFQMMJgpgqvTTNGFkO0pSeRWlzvL0iGx2DSbC8PpOxZ4lQQYLhlBMMELFGYHd2i0INUbfrNOPv3GJmXYQDNh5IdtW2RNMgKIWG5heXivpMUj04TR9gcL+PGCBMAQAcctvRQ0X7pv7KOAFaEwQGqMidwobV7+j/s/3d33L9O3///S66hVjhgKwmqY3YUNmCcgWRyXRb6YbgASGGyBSYMgThgxryGMGFmZWWGCBIcBEZkRBTyz0timdJQhUGPmIFSfDCv0PzjUTbU8uK109DryDN676N/zzv99tJt8h9BqPu4/AylrDGXKzUkJb2wX+22/DTcv02sw1ungBUYmTGia8jBg8cnJ4QUHD//uUZPGIw58MQcP90RI74Shxc9oQDfRnCy/0ZQD3hOIJz2xAMkyMBgQCyAKR25Sluaq1n9x696kMBgTPhiNgO1P/b+v/////u2EkYBcAumCAA5xhDJsQYO6EEHJOi3Zh0oEMYYwHpgVg8GG6hgZy4FZf1CcMAKmCgB0oLW2zCezoQ40t1zK/SKZtxUralDUiyB96vG8j2fz9mWtIXgz8jf0VSO7/9mdMyv7Avk7Q9rfUlhvtq75eln3B/X02X17P0GPp8c08y9PDuMAMYK44D0rzCTAmMoBC4IE3BtvLZPGUabPfrdMTWbUaQGBVr3lmIR//f3f9Kvd//u/7P/qVMnT8NTLyMeHJHzD2Ayk4JRDcMMoAyzNsQDHwGDTUuz+sOhoUVLxAChiMGa4ZPi4T34ZZXpLaUTlbWR+YfhmdF9mnqYHd9HsGth23//5XJa6rVY1w+S/t9mv9NtvfrLVVv6ephHP7gZxX/ezp3Jum+2SASGEaH0YaBDYWElNtRzgw//uUZPCM86ILQQP+2RI2YShxc9sQD1j3BC/4ZUjnhOHBz2hAeAUzHjkhTMlw3NLZ2VIVz47tHPGQtzFTaLdOS7Jyz/9//+z6KO13+tUZVMgBc6DZjGGgpEwqMJUND3QHzBYwL4xkDkoC8xxv82xGYwsBUBAczkeLN18KWDQqMUIhq2RpI4fmGOTW91raEvNJWVZgCHb1LU1wTPODLFxVtUclSiQCFrVlE2WPIWOGrMnJRyb2ghuquwQCmUspzG4ZPNGNuhuYCIEiMsGDP4s86silMe7apGxyqKU3dud/q/cj0dnsPdSvd9X9/T31qhABdmA/hYZi4J48YVUJTmlTR7BgpQPCYCeBYCAC5MA+HOTBFgNo2iEs8FlZwMSKLlRmCd3Luc9rB+KGBAQrT+k4Lq/KYRsGYD1oHP6cih27jAjY5/lmzhg++l+VDvWiHStuWZLmTIWaQUU6eef/yMxHRWv54/iB8+Kcla4RR80io8nAQQBjOTdPEg0zdIzIZgoM//uUZO6MA4MLwQO/6BI44Xhge0MmDPwlCk5/oEDkA+LZv2RIAsFMKCAhCQpNgIkhucia9e/9TYMkUVJRQlTL1Mp6Uabf7KNn3N6N35j30IGavbUERgGoAQYKwE2mH5mBZg0gcsYe04gmAhAsRgDYC+FwDswNQQ9MMSAPCIqTCwAQO0rX5L7NUSSA2ZXiwjjf3/iXPNvKPyZf59tpodQ/OJ6T7E/+HUJqU0petPvnmUuXOe/PxAu0+Yr84QuoxurDqL77m/9M3xmjWu3LLKruMDGYz5IzAKOM8BOcwSAHi47yCCCTVJZL+PeSEV73Um3CZb+lNVr3dFm1W3tUj9sn7Hf/9f32dFUhjAHAB4wHEJaMZ2EyDC6wxUzyla6MFpBBzHwbDFccTQoLTzwBxYYhIEQcARjSAaZ0Zo6gxWiGLsL6+Un51zG9uLQYU07guj6UlXudT2mT2tX53/u38Py+6bCPSoenO7T+6I39Oee6zV/+iLBR3c3/a78ehIBKExum//uUZPcKxBFMwAP6GkI/AThhc9sQDwETBK/oaIjsA6JJz2hIDLHkMon821U7jChAhASgDAxI4RnHXlldrkBjARuoJtmdyAfCYx9f//7f+vVV//aFUVYGQAEwG0DWMRVEtDCIgg40Uw4JMHAAjjGkNzB4DDGibjbQYTA8EkTUywYDkjZwqpjDsm7jSpv4bN0YMqX3Z3fIfvGYHv3n6a3e31CvKPaR35Na8s32Vfy0/e5DIPz6tP/fE7vy/3/U3/vPfEExATTasfNHCY30C0TCuAKDnZEAHKAKktKjscZLlMFySB0wBZskNMuvMt0t/+ir//Sz2f//QhUAAAmRBULAEBgFYVaYMgRamCjgzJrqJQgYS2A5mQQPBwvGEH3CEWwwShoAVpGJwHvwBggNhijzQEdZyeHKyTL2O8S6ecI/Oz2nCK9n3+WTnIf60uEUyn0ued+5eUzzyzyWbF6E3/nWyfpnnkQyoLUXdNp79MRZPnIACAEw+jTpzLNViA5CwtQ4//uUZOqI054awYv9GUI0YOhxc9oQjag/Cs/0pMjbg+HFz2hAXoeSR4KEUYiO/c51KO048oOIa4f0VjyofaBfZ7s/Zb66uc2vU36lf//jdv0lUARMEGBKjFHihAwPcBCOPOBpzDTAD4wNcArMB+AaDA/xLwwwMB/LBYtSDRx7TqfUtypccrf0eWJlIUR22bEdOwnLsIiOHT7FKf6dThcOxVf8k8j8fzWZkVOzB78DLMLN+4coOEKrafo1/LviDueMWqv2fRjoF3mulGUQFAzDA5Na54yoYTc0NKMJABcOXI0GJrB3OapY+7HO/d4VYi0G6UPOFCoehwn7en7/9ZKnQe/uq//b7fm/QkBMAPABDAVAgwxgkDaMGCBzDZpSjEwgsCAMdgtMAw0M20EO9QQNgks8KImDU8NTCSfnhv633VrAM4wdDFq3gRyBQZjQM14qlsVTGrCji21rgC8g9FbDYw2FFOirOlhhpYz2GClw6XQSDBEJHnpqqXG7RjSgIWF4//uUZPIIw7xTwkv9GTI84Phhc9sQDyy/Ag/oaQj7hOHJz2hA/V00xDBRFF0k6iITj16IW9OJT3M2CNZtb811+zoX/9qNP+z6r/p6evpBKrUwKIwxsZZjUGBmB2JCcLDQxhQAomLhgkAme/57oojjD0YFh2Q5ctDmhCIEoS0X62vbdiRdRRuLJPKDbxtr2NdEGpbVzi7tCTzHmgw1iakIHYpWilD2JOvBcBAK65QWAguyPIwMfNNhqeMCAHVWgEENgpVlF+p652xTFM6Grrb069lemq/R2/1/5FzOp7P6v9MwIwEPMCpAWzDjjnIwh0XsNbphqzCTQeowHICpMAfAJDAbRvEwNsDSNmLzKwYwIGODMWz3qJsPQ0EgS9J5AoSoQJEg54ykvABgVePXaFBdB5UVQZm5kP2ghUNFrmCnI1BoJECxHCRJZRmT5oWFT3MjhoPS65s4MqHqJKwQGyKyxkrFihm1S6fNYAEnoYlVUDiCBY05yjWscMkPgwAm9TAG//uUZOmIA2ANQYv9yRA0QPiiY6wGC2glFY17YGDdg6NlrshMBAFQC0jAaECnMx2D6Vaz1vJDTLCaCCAXNqYJ68f0++j4xstTR6HqFpFTLU+h0rS535n/W3aQ5kcdG0qOYreItmEFBqphdSpAYCwCBkg2GBoomP3Nm8goJlsoHQHMGwgidz71j7NzO0BhjnJOW9f1ZHT335W08I/3a269WvubyzfJuOt/3rx1YIpq+un8sTQdWM7W15dqcG4NiqLL7bJ5fc1PymHGKYS1phdzmeyz4YLIH5gBhdcwGsM4Q/SV2gWcPu2HRtEgODsMttQ/r1L/o56MLP936N+ijTQvt///aigAsMBBBCDGehhUwhUOSM18Z+zBYAR4wrFkDAEaCQefAAKYAgAkWnQPHasyX5XP5b5P6xurGT6/WViuu9LUyMj0MHn/gpU+kUPf6eh0gTQ6RIxFaUVvi+5X/E5KJ9HdHS/U2koX++0X2ayjTwOktGZ9YcjJLdDaxCEKOhhA//uUZP4M5H9UP4P7GiJFQOhhZ54ADawvBi5/oEjwhKGBz2hAcR6AmWNUpKUwagJgMTWIDDo+BlErmIDYhynnGlbbhhcgjb3/f/svs5nX/+pP/1VmAJADZgLIOIYPOTFGA9hH5p7KZYYQqBumOgkGJIMmeU1HWAmnyAGKgUw4aWJRG1hfxqV+wRzK49wxAthoSB4a/Ka0UM14Oz1z+uD2Wktn1CfCmTf153kns9nYNtfKWfafHmd2PTUw/djb+D/f73bx7dhpAVfYLJh4akBDs1mypjBpAFDlFJmJYPqwuxXga+5iUoZHLC0ChObW4YB7fu2WWei3+j7//nv7/9tdMZjgxVbTD1SBswnAFINigNVTCVgJgwJ8AvMBCAQDAOhBQwOYCLN+ACAorny01e9bu5fj+Fy2lpajucUs9aK9eD/VCdMRnXXP28Vf/LfqPce3+2hMo7bmJO905rxv/5F7vC0lhPsNO3yDbWXHSTN7nl0wCqxgcAGfp6YSBZtfBaiw//uUZOyPw8E7wIP9GWI0QOhxb9oQDjQvBA/3JEjmg+IJv2RAiJQnT4LGRCS+0pyeRhZY2qWk3tDwzybNCfcyjT6O7/X/6LtfT/1BaGUTANgCEwGsCzMU6A9DCtQCc1zcRaAQhONJCYWgsYQ8Qa1h2UAcvRooQTj2386EiYikIfKkLs/COOI62qo1I3aR8zPP2xfBi96eZ0Y4+Tqx8BkZykR/vsujHE+tlncyOlnxPncs/JsV8zIvtpzRTW33qnf9+g+mJdXAABPdTCEFsMGUG43PydTChASCEqVwzuGqd2thH9fqvzWf3vdQTucapordYlWz9/2d9Or8d+3t+n5C1f0KFgIMwOQE4MYnJJDDOATM4QwlyMMeAhSZvzIALjSOsj58ITBIAlgyEGDAsN6YLAASwCBOCLKr7W0Hp1T+diMmww2dX6528d9BWW/NVPQ1Puj06b86Wfab5xPOsU6IpYzGZNqan0BMnnSLmRFn7hDT0OA39tvUaugOqe6ArSAA//uUZO6I04kMQQOfyDI2QOhxc9oQD21/Bs/0ZQjuhiGBj2gIA1S3KqYwohONQTPlM+In4w+AdCptQagp/cxznk3XLipcRY5Yu7vd17/7rbUlGDn+j8Z+z/7fVAqgDxgDYCuYEePVmBHgyRplJiqYOiBJGKgTlADGUK6nH4hGEAGlpkGTD8DH/lG487ni0qj6p/s62PKlU2MhkRz0OzKxr7EejE5NXsrWZUe7obuiPvRfZvTsbtV3bTe3SuRmtu793cpAmG2SN5IWFkAeQRHB5Hqa3ZGgurGYH4FgABd4wdQ/ppsPUsCcy5chJ7qUshxwBBqzdt/0f/0e6n/9/7/t43+2ACX4kBOLaM5JEAxDR+zwU4QMQAJcwHwMAABqYJZYJhxgnGcCqZOU7gm1prNt7gogqSFpdKJUQKWl7Gn4xhtAsqyWRqDgo0VDMeFxcZeWCSSyLAXePNvXFjQWALlGVxbetrporMhBdjqgAACGWZJoAi4wHJgshzEER4HAI0nl//uUZO0Og+NaQAP9GTI4QOipb7gSDe1lBi/0RQDfBKGFv2RAGx2amFV5qZTe94wYabxrVhch+l7B/q3T9VXf/fVHt2Wfi7N937NghALzAcgHgxpUPSMJWEGDI/WkMwCkE+MAEAawuAOmAYCgZgywDkdg400ADApm/nbEF8yzwnKn92rkI86gOwVBBEkqGV9CieRHkq7ZcHZXf2hzZINY2szDK37fC26abg6+Znw5SkzMVeonIX5GaWv2GTQnPNaf6qPLGfQ+Rzw7pBywrLBZKVm08ZlkAY6CKJgMAQIJ4cJJo8ckF/kKrQEWaL1PGukSA+eV7E/0botv/Vbrue2hq9K3//zX2TBpGMCzUwowdPMEBDOTG6U/IwOcERMLhsMLxdMso/OdgaFgjTMTrARPNXvdsTvcvs4ZNkv/VQGIhs5Zehy7ayq0+WM/irTwKnqYa1faNT+Dr73J35NKv54s3fP/Lzpdpf5+1+i8vt03Xn8w93Pnh0kFR9BNJbT3fsjD//uUZO0Ig0QJQiteyDA8IOiJc9oQEAl/AA/kaQjsA6IJv2hA0FQcAmOAER9iL2M5YSKue50VkopWmhrX+n9HT/b+3/2e7VR6vvsrVQGTAUA9MA8NgyIF9jB9JiPRr18xHAxjA/A9AACxhDBtGQ4BuY4i2V8ngVJr+E0RiYcLL0ZaSFlu/plnXkOZoTytabZXMi9pxdJT41T872/kdkTJXnCiUS8N7lzLIjhdUEcOmT0ck+qcJ+b3yB7Xz7glgI6bj34LeDD4MKDhfA0pWNR4wcwaACAxNTQQMBqsSu3IzpwRUhFppwueIizXDI7QRDitO1mjX///9//Tu9P01J1KMANAEjAMgE0xIUKLMDUBxTT5zRgwZUB7McAqAwxGPmYGswrgYWhYCRCAxgwEMETda+KI3HBSlZTWdf9yKkNbnyS3U4/w/zmEk1lWmq8fZkw/+8/RBznb/viWaXadT6v0SyS8n+bZ//DmSfHpd8/MbnUIA3QAg8wzQTD4fNm8YgME//uUZOsMw2YMQQOf6BI0QOiBb7kQDwVTBk9kaIjtA+HFv2RAXGjirgZiEisLsW3vBlUWeE8w1YHDygaUKvqp//9213t0/2jde+v3K/9H2lQFwAgwBEBPMTED+waBCGywD1JEJomEeAqYIQLBgioGGIGDuYCIChZ1HIaC2XFO24iCVnhO1Y3Bfb5NxGS6eea516XbULc9mTvvy1CJ0K8KnGyI8janTKL3KO1P5rvE7sjXnzMyMprqnctS42SZzSZdpkaNicBad70/GWmemFEE0IwLTZKFaCBASI0lYFbwYriVVQA53vHoYMcARyhy2DbBoS7Kn+/1b/obs/9dNKX+zaUSdo/xJm4xYpMNyjCMRYgwLUEINYrDqTCbwCoxkBUwLDoyBnI4jA1YVfRAAphADD+5fjz8e3sNCNy3t9NGCJSR3p79bqgDhlDHyPufrJM4Bsy61unfkxb5qvzCu46srR93mt85hcLJ+e9/uZfN9t+WEa6AgEXf3VKU3DktCowO//uUZO6J04sWwQP9GUI6gOhhc9oQD01vAq/4ZQj2BeGF7RSQz2bBQpI4uEKoK345cs1ud1gila6bGXUUp/bf9vz7Pb/Ff/RT+q4ITAMwEwwDgDjMGLGXTBSAik1nAu6MIoAMzIEGjEQCDNEizq0IBYKW3EQAGFwNt5anKAPGCCZzLIhV3po28TjeyGZ3g7TdZBB5nZz0e+hUN8UMJ6CUhm1B+qpecJe/QlhLTj1Al19PtE4tsFjbY7hTfAOncLBDDeUoACEosqtxUATKqkwh1PFNKMKATQefkKij43OZSOq12lm8Ex5axb9kLv8d93++zdMGbPr09dX7nHf2JjAPAEMwHMBaMP0D5DBxwrQ0SlCVMDDAtjGoQQMExjvjJtuNRhcCoCA5posWcDgChtyFC0Nt8s96yFKezFz+gttHOEU8rfcwfAhi14XMa2Mk6vjP9WMtoEETm9wO4/oY9OO08mpT/8hzvGLxa9ax4oj+zKPCokAiGJdYyysMZrDH5TbM//uUZOqKA2EMQQN/6BIy4Oi5a7gQDyh1Aq/0ZQjmA6IZvuRIAMCxBC9QhphyZ1Z6lmCYGNDnOWSIii7u7urYto9lrZrztGmv5ZuiWYlTDlKv2f0If1X6YFGZMQcYWZaI6BiTDvmzZnwYBgZJgKgbDgFoAKFMFYDpALARUOZNP7Wxr8/er8ot77TyogHVNmDSR5Uc02hpza8CnWgKtazptZVAy1zC7z78cbMhClVjSQZAGTHJ5+ouBFHTu8QGdAwFNnNwRSmJym4YAIFoiAJ7iCuGEJXYyipgvc90gkVujsymv0eu72kdGp9/t3bL+L//6LOlltdAQYAKArmEfDmJgS4XsYdEshmBNgpRguJ4JBEyg005gCxElFssoYkAuySfsUqMgU00IMxhTTpmsZrLSmbf0kmz+YwYs1Uzg5lH7PomVdcxDzvLoTKbEXiD+sUZM5wklMylyTOrG7e917+xHmeTrYVU/Onvr3U8oE+jUwJSVQY1w1AkMZqiKpgUAMFx//uUZO+Iw6skwIP9GTJA4Ohhb9oSDIAvCK17AMDmg6GFv2hA3UAWbgxUXC+gO+1bLz6L3vdSkqt4WStp6+ptX1IS7/p376E1v1fee/6kovs5CBDAKAJMEEJQyDFJjE3H3Oq34gwXwgTFIOjDMNzLIEzjAARoMkgUxgEO7bY5TFqqIZ0WUqU8/rt1pW5e5lPfvS/Cpl96rdlamR/L9Lf6pkZe6fGNipEWTK2WYWh59fBd+A//+fP/UP7vAtcWSM/TNpNbnRUSYxmeRk5JoJlyGCmACHAKbloCuqR2K9BKOFWKwWJKNvA6gg8UCUtRkFd9r7v9iV/+3/b6f3WKBFZRICYwTAIzNzF5McwZY/nsVTHCAoMVQhMFgKMYIyNghTMDwMUBT3GAUl6nGg8Eplxq59T2OPzK3kbq/KnPEkVCG+bwszPytenk6G7Tj2HkXFXhZuSzEidsj/maQPCI1YjcijuDB9Phw51LecM34TNYVyhudYPDGCdLCKEPDgcw+U0Q//uUZPOMw8xiwAP9GUA/AXhhb9QSDj0HBC90ZQjgA6HFr2RAo0FhaQgLRC9tByYSRlV3UhYq11aEtWYskEsOa+v+z/9aP/v9f/+tBQUAKGAjgTphoAiCYOqCZmp1kZRhUIDWY0AkGCkYQcECRPCA6QNXkYiAO/FfU2Kqo2bGWx8akiVGxYvrV0JZTciNL3pwjOMp1oo2sqv5xLnySH1unf/5NzlJCLzjmXrLf5KxQ5Wn25QzSJmTauVHSG14TtmTJCMWj0IRvWif2jOBgBbHlYZkwxlALnEEjr28Pt/aogipa8kwUIkAuE4qyY0/7/rpZ//c9Cfo/7P8XgAQlelAEywACcYD5fRg3ABHZ0CeYfQFwGNZhQFGO7UbhCasjosyBwfnhJqW+PlLnFEU9EIOGFOJfBhnvAL0aJ6F5ZHSPppkxVYelfdsln1CqMx5cMiGqfTLgIE/6teSwhf6kZzpHzwNqHueYPrEWwyFuy/1KwBVSVT/JeHaYa0Z20pQOEFS//uUZPAMA9NewRPdGTIwYOiCa9kSDzV/Ai/0ZQjVg+KZnuRAT2CORFO4TPSSCPecU80KxdfZ36Pt/5RN/2dHXt29/eiv7gCpEkrpAAC0yJzEzA4DkP2dkEwxwMDJgLAoIM6pA92BAUBVhkPQCDqEQ4QoCrTaGlgmu35zPIq8onuxd0zekiMR1Ye8d9dqxGVPedh5GRH5yXMihkR8TRGB+4k/OpkOexehzet6l3Ur3PqF3l3/Nv98OD3QIVIzNDqlxnKJkYR0jIJhOBCxYaamRBUE1IYyi1+aDQoKNHUbGf7/9Eo5306s181or//V/0UbhCAHGAPgKJhsALKYF6ELmghGgxgjQHaYmhsYPAiZJwmbjiEBglYk8gsPrUb9mtnhS50VzIL9OJoxLumvlUN5wxEOmTLayBKLN85BoZtauRkuP7m2wXaPL4F7+REZ0SCAX6UbwZnCivKEWZq9KAEmE5CeqamHUZvwV2UHFx4pnneSMADaWpbSjZgswWnmwVVG//uUZPEAg6g9Q0vcGTozALimZ7gQDn15Bs9wZMDTA6JlrphQCwDsiiIrcaVj+VPVFeskxrQsgihqW9vsqfSLZW0ds+v9uSpJez79P3fSwZgEAVjgTRjOoOGBsSAcmu6ZidhoGAMBqy4wISxTBFBEPARYSwh9TLiJzzxLUuTBel7wM3RRb4TVz7mvaWlree6lZXbMjz3Q5yEd1r+pxD6VLN0bPkhwlOdZuw8sz+ka07zjFea25GVxItIWXp5tgagtp3WMm1YTo0THcDAznQsBiy4gOh/AA61Ems4lZRGbGaTxG4WnvR7/9X2UKrR5b1f0+rWqARSuuu7MAEBAwBATzDDJjMEQVEw+oljBGBDBAqAIhMRWM1sEG5vowswSBKdurkEsyaTnO8Pmcmh7BnhMXxbMyXanv9Lx6/Mq2mtQtfuRS/EQjhG0F6nJB+oakWfllT9EvHtspl/89C7Dm+ZOvCWUz8FHoUBAALQNNJIWbRWZeWbD0gYPgOWoS/EVx6FP//uUZPaMhBdSwIv9GWI4AOiWa7gSDn17BC9gaEjBg+HFrqRQh85Xe7Sja8Z3kH9P9Pv693/yv/q0flK7Lu2tyzBQHTDsXDx5mDO+RDha+jMSgHYwCQK0vjCIElByDqYzO3sKAfHsn8MN/dyo8unqxscVKskiETA8MKCPpzbFW0jeXEZ/ttDpcY3MyNXPwbHdzbkqmaiPPHS8nTzXJ4v6W3tEYsipwkpyIY6FWMIqxDUrL8XhGRGkdBPCAWAENXBJVBgDPGLKR5RXph0CCKbkJyj+bmdyv4q5BFFYzLNIYFa1HsJTCv3d+cqbou/H26EIr7BbwC+lFX1V795zAGAWMgcpIwThPzqnoeMVkH8xIHgUKzNlAOtB0OESXKVRgEEv9WnWVpuX6PxZEaF5rL7pEK83p/yama9O60vkbLM7kJuFaiJ6+tHRvPnFLjRXmzRQkxFaSdKs5Dd3WPVYyyMcXDn2Pu77cZEWbiUps7oqfPWWGBQro0N+FrEWZ/DOe7Yl//uUZPSAA5teQ0vcGTA0YOiZa7gSD6GDAi74Y8jyg+HVvuBIOpQ9ooPEIuRFd23+3+ux7I72dtH9eb+vorALBgDgGGAgDqYGiI5hVBdH30/uYngQRgpgLmA0BIYCRNxg6AkmcymLlnsT3jCq2qiIZ7FFYzmzFXX7C39DpfGJbSb2Z/l34suCRyvFwZXnEVUizhOewMk1PnpCuRQzzRATyxrlEfhklMU5Un3WHqzcd0U2pm5JoGOvrq4xgJU3U7jNkJpMAvhwTJQOwQl+Z7DVLlIPNGygmRvC7hohABxA0kOI7G62v77epP6n0f+ub/2+rb+OtHoAIMgoYPA+YjjUcycYbEg4aSGGSCwScHAcJgDYA0YA6FcGCQgE6bLTHpDAGiBXw92igvVSfMvISzwiWqgOGOb5HmIMyQYnc7x26blaVy5SLk0yzXJHQ7I9jHsbrDqHSdS8ypoZpJxnNv7xEJZSGIp1bZVZPhlTD9Q3VbJXF6AQCJqqmhlRYwNgAlDo//uUZPGJw2YvQYvcGTIzwPiCa7gSD4l3BQ9gaEjrg+GFr2RAU1wgWlYXeJZItiy62XbU73jEOK2q0v1N9GgLZjQWv7f7vR/+r7uoSFFMBcAQeCIMqMMQxbgwz9aScMV4G0MWRioEGd5MC6OpZAadwhBtwTpoW7LIZ18i3b/hF45zNZmhg7a6TY4xH/zSTIIlOn6nstc9tP2fQi3y/PdP4qa/+cmrEX6kWYuj/FdCoG9XHkcJjgK9hlpRRsgAX/a0kaaXCZp+dcPoBhlWtAqGRWCcTo6gylzn08fdKzlV6F2e+iz7ae/v+dZ/oZq6rO/7UR4AZMA5ATzCNBIcwUEGSNEBLMTCDwJYyAJS3xoeun+x0YaA6AZHUxyBofll25+q+p/DOR87vykrffI8z3aZIXxCRY0IiFMjrzPKS1H6pWMQ6ZXqMW2lkNYT5ytShoeRMrc7IxIczjqynXM4DNQwGygNKw02JCgr6OfVEBC901P64w26ZeR1/vlrnJphkZEa//uUZPOIA+xaQLO/GMI0QKipa7gQDiUzBE9wZMjUA6IVrphQW2sbFVXSneRCb5Rj9n+3V9akfd1JXtqd/W6zYBf63P6AtzC4KhiCJuDI/Z4OctGJ4DKYQhYYBBmYi2uZuigAghUGSpMLgEZqKARbeDDBnYwBLSNqrn0ttLSg2Jp7kdofOIUM3I7G8zTfQsdCnrvJDlMiddjvlFUluS3KGjGvSN+Ldi2NVUkNvs7b7nBvlp7sWuUlMeDz8yJQQUkjT5Gz4GAtmTWGA0D19NlJa2dVeaqtVe9zJ08Lio0YlSK+vZ9TUturf/0MVijUJUwfQli0fTT/1fIqVOYFACZllk2mH4SIaj/65g6BIGBQWg0DzCK9zPgOgcDKkkJgWBWMU+F3erO6K5auquQnhq0yyzhkjkxkhJahsVVieZYMj8oIVPB6lf0cToZZNQkwxqe6E0tJLKagn+MR41BNwzyfzSfYzSIZELIkhPD9xdIiiLejmgXtJLKDcMgWdMEYDXTX//uUZPWLA9ZPwAP8GWI0wOimZ5gSDwl3AK90ZNjug6HJruRITAwGFM5YQZkRlFf5IjS1rY6JGsMmRqrw4JGC93UoUQoj+b/75FVn2/qqR7271/QtVWPlUAgwEQRTIGHLMNcck0T8CjDJCvMGDQwiMTMlFO7AJEt7GnhxGg8qS04Jhk0zMoZtvqv/OPS+E5Ull0/qEsKSqMmYQvKuZnsELvMFh6ixncUTLkYJhqTVlq0Kv/+umExHFK8kySUi5GO0i45fSuSh8IxKbYPMAgQKyLLZeY8OjX1OFYsMDwBQUXegJJggH5aHc5aK4ucgMVbQOQPT0ra3/3EPR+3fsb0dWiHLf+/zNQoA+BApjDhQ6MK4hI7ztszDEDUMhjUwIBjOTUPvDEwCAl8sxMRAaiHwxax0RLE4IkyvLdr9TsN7akQ4pFZt10SUxRRH2dRZd/ndQtBmv64PdCEHESnOn5tahdfS3pkMmuZUoRdZbMzBmhSEz1aUJFmhgmpQ/TwkANUs//uUZPGMA+RdQAPdGWA7oOhhb7kSDtlvBC9wZMjmA+IlnphQzbgQ7SjUUOWFVFg8XvDiSg8BIZUE9N7fuowumL9zTE/V7/875zbmP+n+R28+2/alzwuAMFgVzFbKfLAip4HvkGFqByZABxQKjH3oNjCkODyvU9jAgHmHBZm9HP0irFMs0LGJjhvZAzFfFmy7ISJqpWkvxJtb5N70i5u/ELhtnxQUOd+vyqRdBrI8+FemZxZmtpWP6+fGzmjJf2h7PZ9DKZDVDtzjAzMYwMzPXy4IiPVY+YpMs2j52fqjHuqMNx73gCjNXadW+aX939CPRv69n8Xv0u/2qgAQipqqtrXMXwAkwHQMjpeL5MPQAMIKZgoIGD8sZfD5cJYV+isJycmoR5TblS4JzXUnX2hQ+x5tdTyZc5Lirn6+sz+Q0b57n5mjo5Ekh0eZkefvqbRT+MbmRFxJ545zr2Z1FYjZCSMaUG61DONtHw9hPMAtLIMd0MQNPwjOJiNSyaYKhYNA//uUZOsMg7ZcwAPcGTIzAgiWZ6IUDlV7BC9wZMDLA6HFruRAHNYqAJr49qrZBle9obDi0que1rqR33f/94C//bqWn93/ci7UIBgBAA2FQHgwgUOkMBtA7DTzw3Ewj0A4MUgPMBQuMdYeN6QHWFZkQACYKAk+uH75lzss/dUlMMbgkHlNTUvGS1E4zC0IyNjM1Ncp1BVOQ6lAE3VCqUw5McZNadCtsavhqRqZJh+z4RXN1zNYCTw9rQiUKKJvMIzHvTDZyZCpkJJj5zx3Y0SEZpjkpxWIfcxaAhiQpa0IZFfaXLCWpFzAqeijAZbXqvQlGvk+LWlmttiZMWlWzd71OivfmdzJKY+Rp91Z1i9LqjARAYMBoFEwNysTDuE/PXpyoxOAIzIYEBw8M6ls8eCB4AyNeBhAByb+YZ/+E7r9EsM+kXSpVCYyLnYp+RrDzsMy3uGXKnq+wv1kbIrn+fnCnd5ezyfLm6nULWlUL/vlO67SkRUyMv/kR6V7mmfO8MOO//uUZPEAw5BWQkvcGTI34hhha6IWELGA/i/0ZYkDg6FFruBANIiQAjXPsn6aRySxzhKQjAoCV3RVG8iBqfQeq51qWY0aPRYSUxCvzHzn9RVOrY3fdX9Ox9//7t7BhFEzJPejDyCyMMEVI5AY1zCFBSMCQBNJIwMRnTDVAqAwCqYU6PAfz6WPkCPh3Y9vkMvZDdbJRXCNOuRF+mOjHNqN+0yyt19zPy27grGMr33ufOParHPZ3bDw9LTbpemXJsvyZdh7lbeZvebFgtwvSjmynk7KTxJcEdAz3oswFAxnskGQfTOGpSY8OvRtGDuWpQPXuytGv2odGfR/fzH9fs6qv6OjXQAgAYSGqJh7hzGBsPSbOGnRgsBVmAKBsOgYmAAUaYMgHhd14kpQoAdDokxUS4bU+IjGMyqE2KiKhGPbdXV2MkpNs4dM1RTnHqRioc77EYhcUC1xVhNtO8jR1jh8dp7yATqjaV0BtXeciQIefcmwuAfnuaNHkDdanAKADhKq//uUZOaMg55gwIPcGWI1wOh1a6YUDkF3BEx4YUjQg+HVrphQmzbmE0mbunFgDVVGligZoGV8IZ6o9D+ciMhdImljb9tFdky11X0tumE/RyimITpdtl6dqNbdr3wKb21F5SQBQwdAYTBWHsMmDWkwEgngKJU9zKXtO0ApqLQ1hAEQYEHw/mFDRKC+7/T3xTNYh67FCSzCf0SURFTpeQKT4c28/eO2pjldTI0PrRCU9XqepqVyYvKZ7GVL03C58vk3YnCXM0OOzKDU6dKKGSsHhbdWXGFgclgUDky7zCwAljw4KbK6WL+ovpZWdY75WbHjY59sM9HqnrFu2Zfdsd01PTfci0p1fdd0U+x5LtVBoQgHmC+AWYJY2pxF4XGGeEOYsFxhsQkUEA9hGg8s1UwKI8KvX/w53c5zCAwdtr7WMcueReEnU6VeseM3g+8SFVI9S473PhJQRaPtced+5A6G9PSHHpPnFc7ltfKm8XPpmHJKvzJck0MkI2SEX9ORx6vA//uUZOyLs74vwCseKFJAoghVa6cWDjFlAA9wZMjuiCFB3AiYGlwglrvEtQzLM8zj4xLAdKxl5dwoDEHHHCXTZTdGao+MK32EmLxzI+tvLOf6a0S/p0RV9mLue+Z5tPnit3VEcx7kBf9UhgYA1mCUHccwjtphyAjGJwaAAIYsqBtoTltnBeNF6fI4ZLBPYnwGpfJyZOUM81KsxdfNGLmTc5MvB9v9a0jPyPNMj9oXfIz0fIpn59O39iPLq0jz+v/b6J5XWVOEL270cG9DnlNV1MlzemiKcAh5giYKFxM91CQGkDQGVAVsz0Ud33V7czDDzDEGVIrUt9qU1b71/37f+j6u7/o96gACkqaqmWpnGC2HbnnEabkYXICIkB4XSBArwNAPTOhM0LAK0ipWTYX+bW1SJbCII+4xam8HjmbsmuZ1sdZpFKq5wG0NmP5+Zw1YguRak7n54xLiNif0JF2Q+HpJeqhFnl0MxN1t1Qub7F6Z0w9Jo2W6H/2KveZlYC6P//uUZOeM06ZewAPcGWI/wghRa6UUDU1ZBC9wZMjWCiGFnpRQsRREhaRZeYkDBk4uQwPevIKpENb7IipKXOchttW9bbPH8km9H7bl67Kq7idLv1E++wW2BCyqqlvgMA6YMoHZhVAYnoUAkEEchyLMOBIyPpDh4ZW06K8gMI4tz9/bvdnv1AejEM5lNnhM7C4pL5kiOLa0oKXa6k1c8M4WB1SZWnldzPtGprrUDR41IoDfpqosnbkIhBF+0J0co5ECQTOv6liA31FydCeItJQWDfanZTxxKWl0gFKDEhO6FaMMABV9CBGCREC8Vw3OlyOfz8GWtWwGyT2RTqFvi3n7vs81F3rX6Y8wJzL9KbvYRIL6VQCBKKiRIKBjRD82mWOT89swyQJgEB6gKMDkEAHEBJ0u0z0v9XmhGwX7ZmqFPdP0+FkppnMzlyqyZgmmiZRYnXPI1KUvLzfp01pOdvIz5q+8IqxXbeqT5n/0vVAQUboGoeRUq5yEWDF0sAJAAgBR//uUZOsIg5haQkteGMI6IXhQZ6UUD7FrAy9wZYj1CCGVnoxQmAjHDC/RyLAZhWADOYk25MD1LaYMZ6K2P2Ld2bV0fY36tqv716kfp+MevRm0ysL3/T0+1IWlkUuUWhMCEIwwVBBTiCbiML4EUw2CAUCTHsVNwhQvy/UJIiDAt/HLHmHzn6M4isuZp9CVMOMPSMjDgjWeepgzMw7VSQ6f5RqUorDUjuWDw0g0Iy8vQX602NYZqcL/Mo7SOwIqSSLaUvoh+lSJ9UrseZRCOnsYMy8RoRoCgxFasqekuaVBObaySKL7TJCNZy0F9Fupu3UZureRT1+23/dRoZW2XSf/7P/kHf/SAQJJlFaS9QFGACDiYCYhRpfulmCiCEYICaxwTbTBoWL8tOhgt/0MJD2WCfZfZC8nI4ZLBUkKKn91BTkIi0uTYbP982M17u5J68B54I0xJsKyOk8JTpzpkedTMjylh0yRfe1+pluOZDWErvS1OkWJTvwTjxHlbgAAQLHD//uUZOUIg1ZKwbN+GMA2wXhlZ6UWD5GBBS9wZYjDiCJxzAiILPhAgQ+GBkTggAHxqEoNxgS5QLvsrd/b9AVbrMV76/p/q/dr/b10aNNFSE0XpfPbVgEFwACQBcwSAWjARGlMR62owGQgRALACKTHXZOAB5pcML7MFgKXp2wIGA6kDd93TDEhFEJiMilFJjRZrIZVTZLU3M/JkaSiqdaAlNwimXBZi4uIJmLvxCYpdCIhTnM469azquqxHi0a0aC1PjCySEcLM7k6B3t4OxKkgsAFqajoYMVMzmv8MEgNrD7gU0FkTaWq3XLyFabr7Kb996j/TQt/m//6lM9CUO5CypnYh1KaAANAVIZWSMAGGBWAIYGAkRo6QkmDMCwYDBtIY4QoeFXalc6TCCiu7z1hYznv+HK+/766KZo8pKDpw+7EayP96rAzmXhACXEBAbqzHHMuzSDn625oZXmWeud2oJlU9tqal+VgdEj0juELNtmmbgiUVpA5q7sH4gEITOdE//uUZOuAo7FewcvcGTI1woh4Z6IUD3ltAC9wZMjPg6HhnkhIGJgYHPUNmDgBKXw4w0jDczuV/cNiNmLLJZCUqAL9uMxtBJfdhVn1oe9u5yBZNVR/FGzOro5YwmqzHZshAAaIFwCzAGA3MFcQs4ZX3jCzBTMSBIBB8ydADhAPFgkv1W4LgGmEzixZWOqjbSLCqRUClNusfB+mgpKwuxwUgwJ6pT+MNPPDUAO3mS4/bny4Bzju6HiFvQE+y4RrZ6j/EmIC8dr07kvnEemle1cC3mEEtgYzePLzv1UhIW1IPOA6QOaVzRMntSVTOf32S9ru2iK2atCkK0dEUSubkJRvaP6+b2a1Maj+h/RV7N50VAQAQYKgQhyJJXmGAA4YlAJgQEAy4GNBChKazFw4FUdn8cu4cov85TPZqsm7kRAiJFpqXMtp5sTYHo6PTNZZkmbtXVoU9ilrnlS7m5laqGRX05YZHqX6Nz3ZfKsdfQmLypseZlAiFPoMjipwhHe1UufI//uUZOuIg69VwUvcGWJAIPhVZ7gSDiyRAk9wZMjnBiFFjsxI2mH4nkQYkQ4q1uoShFBtNVQO57Oiv16F326Eet/ZtT4ZZWWz22wxWK2Vi9vn6WPmKEogHqd29VztoxDUOEjBSgHlZ8wknDRUQ8ECYA4BhgOkMGHKAyuJ+HxDAQKL+OQIT0gbOIM9aIUs0RoyV0ev0jgfKIU4Il6PZizhEJpMJSna8QUL6ii2X2Urax8JVQjGMl9KePm0ilGz2bMtTVsgm9ByG/kdjHTnEI1JVlCA1FnBjliMigtLRilQCg49L8WEJtZcSgYnmtA/VNm6dB23vMMsQ0H3b36BRdvV6B3RV036v9j9r/fsbV6KoooBAAF4jAgEwU3NuhzqQMuMPQBUaBaMBAAAwSwug4jNtKFqiT9bndoBm9QuUyQjIiq/wn43WPO3bpZ1jSm6dTbvC31szpSa3JyVflDwA8Yb8T8OP7/6YOe/iTiG1e7SWfrHP/uLJLSk3PREwAAgQCNR//uUZOiMg5NWQQvcGWI84phQY6cWDxGC/g34YwjiimHZjohQEvIFgnOGmGCApadJlhitKYN2vQVvfaP/nFXa+yj3z1/qqv10ym/HNf6/2293f9VnsWADKSzVKoCW6AQEhgbhJGwKgCYO4DQGEWVGPuJ0YEXxa83EMLpelyc6PorG2/O97S9j30Qivg9KaF5kJapm5KeZHqa6rYjHqcpP0NbiI7usC4tjh4L0dMLoiR5pTsHc59sT3z4X1ytZM5h1tW3iRIRGjMBMqEYsOa60tk7M0MgFEpGpGdKbp69R1prdOPPH0ftvf0/KpV3L6Uf/3Ee/RQPWwCPZrqPrEmSqqoSgYAKYE4DxgZB8msE10YLQIZhEMAwHmD6OZTCydMPNJBwYh0UurwOz50mIKX7mEJ0NxkzwamzqZmelR1cgxFTeQ2lwZoHhfKZkWUIJ+h6FrWhzyTV7SLKOHPG8yp74JXQj7Z5MFOUmd9yhktIGDQWqTRVGGq9rcFNAAACMJqq6//uUZOYAg1I2wKt+GMI3IqhodwUiDbDVCS9sZMjkiCHZwwkIrR2XkFzFEwWSvNYJN9m2RpqQt/JRXt2N/r77DHO/9wn8UQny/sS+xSu71gABAwAMAIVBxiQ8mDcoZpdepgOg/BQBtL8wFCATCoAgEgAWLLkCgAd6/qvvLHsgwxwAZTUnLQWeCJHCAKPvVNj/D4QLi3dVNxxLJ10IgZjMTCmelnTNCpEVHjuHcebZ2IcSTKMRk8FlGzBtKcmdJCZwaezIcL4RF2u2jIdbxl5O+zn0BEMKq1ZIoaMCg19u0Ejt0hI1bg5yg3V9TdU1/xriRRiIt7nbdn2fT+Ae3xapCPo1K/+c1QAQDGU/dXRJsZACxgCPJq32xgOC4MAFuzGURuJI71tKK/NaeJ57Ss+7nbdDjrsqIryavV0ux5DuiPY1T27y9zaL6Kkq1u8tyUdkonclUdTorjmnIFe8JMASjSySmj21lcWhoAAkRCQTCnR0BvTtd0DAu4j/q2lAEHZR//uUZPAAg+tdQTPcGTIxgLisY48QEGlw/w54Y8jPCiHlzBSIHZk29dW/gxhHbQ/u+AOi1b7037P/7fXqpu3+tiYAACMC8EkwihRDk2qPMNYEwxaDyyhlhaHZhGWadFsYCDl7dAzSFASdIKlvNZP5rJLt0X0Zu4ZrutGNTZ81++p63yTkbQPa9tffWbmVrzORmEtyr01zfV/hea8dBLu2KNb9p+9fYu5T6Kc6dPffeNPznGCZj/WRLvaaLLK6OvT6e5mxiKoXMoQIwivZxY4AQfSgYsLG3liiiTIZUC9LbdOo0BV/aXq07yHSMOjX7Vf8khPi7O3a67N3IrRov6VqAAKFACQZankGAUjQU5sypADQm5MWIDjAvo2YFWdFYkkTomOm+KvQhE8K10y8c9mrlDn2f/wQZkv9Jnid3M93/TT1JxDWpCOAhYTpSuOcpplVt+7rNYWXlO6bWcnw3OU78aGXPzGXc6Zm2zuS2RJWlANHepRDwxrgg8YB5fr8mPfM//uUZOsAgxRHQ+O6ETAzAoh5Y4IUEPWG/A9wxMjdCCHljghQbQ761oMxdd+Gah5yza1K1PuYqh1aJycSwT9swtFVvCtKmVV2RJpX+9aAkIkUW1MBECgwVAxTduNNMLQBsMA6BQBhgTiJGASAC1p/XaJgDrUtaGba3+nNphQzP3Tbyvv5mExHu50TXhbG/MM7maFTM0tO5kTJa7nws7iEddlIn45ZHubGHyiIe5Qpe+mfXhmZ+5dKXO+e9ITDnErJ45HJq9hyj2prq0UvwAtObDAiAL3TiTzzmxHd+K219dDOnaUb7t138bZ15O5/u+nrVPN/43uHVQAAi992WVEAUweo8RIUwpApMV4gCEBMH0VtVZzEjLV1b2eGDeABc2ZckBahR46nvYwkLIGsOzTIpuRE0BWlXSy0MoVlt+5DnPbYSvVacGhAdWAIQKKsrSx5O8hHhtmoA4BQDUMMlJTtqZfztrPgrnq1DEdaLxHorlZNjv8nb6p3ysV+k/l7MW7V//uUZO6IA1tZQUvbGTI/IghAdSJCDwVdAK8YbYjCBiKY/ghQdn/SUgIe6mApQ/2fFOua17rj+M9td3+teFuos+zny/vrLGyhpuqZBVF84eUgwIA1kUAiE43IFSB3SRkaCtRa0mujIivalmGqshHOTUMK2gVt0DnRgq5iH3JWxgoKHWH0JRLmYUUu88adFxZwkvLTSmtW2w8vUrZIAQ2Cfire+jpt4hYdtgGL4l89EFzVTbpMK6ZbCQqFz4sVS2n6XY/NF6T5H0pmZPhZkZSefMuskhvIdJ2x8prOUsz/o/gzmIy7GISs0YAvmRgCslwp1Vy89rH2tpUBjzqGFglADgYbQTRHJguAxwWam0LgMj314+bU/TX7wque01BDZxxfxaZdEdj5+cPcvnf/OWU9TL3LOw+IKPPoO/V47+LZLXmO3rqfi2YhDlehI/xH4gCASUwUvsYDY0pmERgA8coCsMWJRW1VdjZ8tN9Yyl9FFOeVFMJPz1RTThrDjoRB3Pwg//uURPKIAosPwsNdEMBZAqhJcwImSmxlCK7gpEF6IiBBxY0xO/3fPVl3IpQNdw7hLjl6LtkAmHT8t5etUTdjENv7ajTLn3Ux9PhpO5MIC+ASHYE0JD15rxVBTQWosG0RT3U2BKKi0SqOnmpeIgF3FTK/tE9DZxc6S3cqfpbf2/efN9DoGPYKnzZjwx7d/po/2nRKkpVrvz4afqZt+4XGaPHhq1KrYhxAJsxJUGdyiVlUVp8FoAPTah3pkUmTWIiqhcVWcJnyLoLrBxrLUoMWKsFRaVbNtM7DDNR5dtrc8tfQ8aQAqXzyUkCB8UF0qgKf/VlTUBGCDTfXU2lNOKB0iGBXiRHZnZIYljOuGUIMRcMagQsoTbEVRMvJJdbZPKNci7ucQqx5MzYWNS3KO8yIlJsqXJTlJqXMuqfrJenI9dsvdU+ZfnLqqKPXpXUX5P3cXQCkwOOh2YiH7ZokFysPGPvOLgs484/LKetM8xo6mw69hm1HyKGewfMp+QCZkhEd//uUZPIIgq8lwakcGDJZwpgld0MmSrhDAgfxAokvCmGk/hRQ4ucK7ak7xZHs9VkI1iZrFzRUwqGSmvTznDKUyTrQFEgaRpWrS4UBqaoVqZXIs+SmlKCaBBoghrrSAIjdpDqMWETOJjyFS9snXSlWmyuWauxT4srPDq0+P2pQK4V1p2+lrE0WWudSu5ruoULLQRT6PBWZG07aNKwKFywt0UIzpUF5UBEdYlnmdulAZOVG6+6zMjlFl1x/4u2aby+F9unaFKsyf+mfB5oP4atnbHvP6mEfvtN+76vnpafGF+4liLMm/xqnoB0CiIcGRu2gkgOYRee/G1VdrboVfniP1AZaI7a6qGm9jaWIqG7sRZiyllKq9F93lKtn2VvmRfTk3MebTPzyt9FNGLYjBMhnzZjNSP+Ue60y9MmO+fDJJa5mGb1gkqXM42VfUAACTNVT+m4jgCNRR1JCN1BFgbSBLAvwGXNspEfcfhP0pPJSLTuyUuf1c+N6p8PPNKfdkth6//uURPcAgwhaQauFGnJl6OgAcMNoRvgXFSDuIGFTiWDVtIkJeefvgsuVqp8lJTLZk+nt+ly9PxMgpbywSDwrXXUZJLnBQuw/mE6lbBASETe9oDAe/EvTsc97aBmpr5pMwmcu3FnMCIkhGVF9oI0O9hTmsHxLiidrZ3G37vCJ5T/+5SmbXSj3vXRsT2v+jXd9taNl3WM1Go7+sr7Lk/AysfRGohJ9p0PAaFTutYDgu6kaJCoX5EzrIpUid1VaZwuA2mpT0FyGK/BCDueffkm5zbu1OANj69Lr0pbyOpfwR9PX7rJ0dLbif/lXfyuclsP3x7DJia6q9rk9dIfPYjUBBCdsN8j4c4gY8FHnkDclvmOUCceO2nBFe2aYLOu6JSFC/mtfr7ZZX9s7Xfy+5qa5/XaBrlmlp8hVCi/VeZcIrSNSZjTCmV5t0kMqrsGyofffkXJ9/dDt7p7Cf+84AAQgDanUTwAAD5TdFhQzdyAr4NJ1t5QnEtD5pnENnpcP1mO///uUZPsPgyVXwAOIG3JbCUhZcwMiCxxlAg4saQlrjOCVxI0JWM5kr4972/+9d88cm/kTkJhbek3rHJH9jW9f+9TMtY/zmMbT3ObGRp9eQz+Lut+euSuZ/AAIUD3C4FUCPnaEWpD2u1dWob8FoTW179suQKHgvbBy4doqzkg2cpuz5rsnkU6kPrmVl6fOlyHpI18/45aavhJqa0jOv3OtNnbu5S4yRBAOhWN4XUMbF8tYB7JpAMZ5cCER+EaFAqc2mCpCN1LrUD5vczsGpPnzP4+ZTLjaTmZia5Q8sfDbaaZv/y1//EWv1v/FIr0dmfvTTM/XqwTbbw5+fDS2V5FdO+dQdyT789bzuszqtUq7gsEn7DIsAxalY21JVYCvu6owtd0uqjkVlOSFPKeKRAzbwqmcSUm07g8s1X6hGrwlqGOf/XBZShXjP15i2phF+WpKvl2CsX9kQ77Ys6r47l7Rf+5Dynf0EkWaqyss6R5OSEBEmRWRVJEoSdBXYr4duc6+//uUZO8Aou1CQKuIGmJYopgocyMmS3E5BwDsYMlakGCVzAyZzH6GR/f9UKi3NOBP/s2V7Ccc5L+/kvP/MTo94n7elVmz2M9TTve936SFYj/ux9E50xgkGmspf9E3WWSOhj+m1dHLQOjeqI1wuj5Poj6WUOE/DsMecV0gst1FBBX2JX90Bb9yd59pvr+4Y6KorXgzebL1Cv0vhTHUKXeqi/v3937mq0L133SAAEgslmmS1EYMOerlCqW05IE5UUzAWjFaSDUnXUvIMJMoNzE7m1hR3lBilqii0yRcOzkzB0esKLD7MtXGAKEZdbRU6yaRYx0Rgg0Walp24jWgWte5K3jqADAAg9YsITbNbJ2bmMFwecQ7lo30dUyec/Sw3+ghQgLJKISLlI2KtUQ8isdpaeFX3SfQyLMefJtCjZ8jOMEUi94EPLFTAaIHxiEIjA6yVTKShAJTh0uT1gqPiIDowPqhndcM7MVNnXLJLK6kozLLmpYI9drIo75i7soxEhwi//uUZOiLAs8twINlGnJSwPhWcwMiSdgnBK0wRElcj+Dhwo0oIdCOxD5UDyXk+Oc1XQZ+Hl9Jf7f7bNbtNZ892qPfbztzpkPODqvafzeIBBgVn8GtqKG5rEK/s/izWDZ1zIJvke6Dbuiq8BxzsQMZwGMVeFlxO8znzXIm9snMmM4lTt4adItLPuRnoTyDOxxZHu7Q4hF5kLvMrvDJi7dnlzpWH/Ac6nwJhE0/kppmSLnFA8ZHlqVEToFVEmUQRATvRzgjSM+4kzIr28RiOn3I983bbP3tPcyLHE3X/O7w+chwGSm8EKDmt5z/fa1WOMsI7SC7zPRRToig+i6rtL/zchgGlTVLTF7CnaCmmcCgNPpKcJ0wpojYR71FkkF8i0BkmeZGFuXCeL4M3YqRKrKiucctPrn1NEosiEL5RXOpmdy/BisHtL1QoDmGIiW3Zywu857AxsERzJ53TffU7NKMz37VAZj+q8CioKabfK7llskBKW/v4rikTbhxMJAjI27F//uUZO0A8m4YwcMhGlBZ48gQcWNMS+VhBsyUacloGOBBwo0hG/mB/ODQ3qj7ui3W6XIVbXsVGYrnTQt9mWW/KpmTqjvL5WQlX63e7oO8DNW1pgkOFKllrQzLIBRx+8H2hwDxXyv8UdkzwMEFFhGq4CvfHFiUdcdoepw5dV+/c+vNuWF2c4RWTk3YvnZmZ8558Onvn588QtYvx3at8OXoMMoJsq8hr4Fzr+wOvRO0E/3d2s6R9kBB5V2LAD9xgZBFX5cuRf6dmKYvt+AxTSuWUO8axQlnOqW6XzZkUlSvVHdCOejp9Jxzu1zP4H/VuSc/o3X+Rqt5aCCT4UpY3b33blnYHW5jvYwI3PJVAACBEOdfF9j3i3zO4Khy5karptmoISWZ3gck9Ft4apyncsKxEMyXFb/nJInrSeWd7w5XZK/OHRVvLVUeRN9X/295Dxl+jfEj5NLC/q5oUlBPyXMyU/oD12/tdr52qQq+c7LlxLK3UPMuA4cyajzptQKR1O6y//uUZOqAgtgrQANxGdJUybg1bCJuCujxBq0gaYlgmOBBsZWpnblH+mtsufyYCgkY83lH25KI4MxI/1/f/qecSznO+IV/ZB59ZP3Y5+ru/fMfJrL3+5JL7fcZQo6eJdDtb2ytWoNDhWc+cKA64hsDG7TvocPSr/cqs2sgSfjpgV50E3mCHi1caU4mt6i79gesUFL0G87QiV8AW3xEp+/d/v7EXEOg2dPGy6bh9QBAtWarJQX2lADJ7KS7HmsHKlLoc7vOqkVes0KW8WliLc64xtXdWkZbtq60f2Kh1yC117J0d0eLFjFR/Kf6JPtxRrJjjOAOoi46zFqm/S9v5hbd+UcA1QQABu5KlZDdxZq7psqtHeBFbhZtLC2tv3QgjpvSJxJ3cfbTtpmin7Tp2KcL+/mbwypZL3f9W8LSNCK3Moae450zdjP51dSs1MEiuvnQlaO3o5tynGtze/k6uY53mdmwqAEKPeUNSKVHfn6op2+nbPDGGA8+lpWycRnon3jW//uUZOiIgq8awUNLGTJTQ4g1bGVMSoB/Ag0UaUlYnCBRs5UJl9SFYLzBHxL/c5IWrVKGXN1eHbeO5lmbNCUxvEGdKJnySwtjRvIvcHbDjKmS/djmb7bX/IzCv5htK819lP03ZMyATW6FsYUqles4YajZBd7pZlH7MkoWOW5Z47WilZAnZ2xnJDZQ6GkPffQ8dWma7Kbw4TOiIug69AnFjqEyQRaZ4xZqFDqg2UMAJCp8odsQAQAGC2kdTuUnFe0U9OXNkocLuRkGtfGd/TK7p2yWKqzlsksaSV5mQFEOdlY6UemkVkJFa38iul8l4DWAAAW8pYnYaiu0ynqDg5aslDoSNxvKfQfE0cg3UbD68rk9e7D7vKdQ1E80vN2OnktMdzQp9vveySq3P+/YQdkv7f3Xlv3uMLiLnI2M3SmZOKK36wmGimlqM9Rp+5F0zG5iRJaN1iDmxmxpE854XrLnMNQU9WBEH8IuYehugiJVCDJaF0lWC+vRHRUz84Z+hxxl//uUZOyDAv1cQCtIGnJeiogoD2MGSdh1BKyUSYDqg6EUPTAIU26WftW9UW50pfQavbc5Fqmp/Js+531XR2Wou5DPotq3Mz7NrZljv5ffTWPd3wAFSQm7yPyynae7Aax/w03c4p517FwlDPf3cAQiJm+iQgWUmuxKZDTX0SlZkyuJVvus8E57mUUuAkIWsdHJOGDWIzyEjtllt/Q/mkN5iuGy2tBqYs9bA6e9Puf3RnEFzQ8JTkQMShB8zIjUS/tIJEr1jKuGMWAEEUiKzO5AZj6U5FC2NithqdXosO/T1P7P8d51ORUrob09fHmdESIaVsM4RvLIu7VAEU1lZbeMlpB+mEX9Ps1KiYDu+ES2oPb6Il0JCtekyBp2scHBCTgKQvUYlM4Z03ZiuF3N4tUigyCxfvRvwJpxTCmyMgjSaFXVWfE5bueJfQjhoFimClzY94UuGbQilCNu6E/hAy643BJBHzQAmsq1Ld5uMHBjIHYQlqyV9vkv3Ps7baRjyJHE//uUZPaPA9liPqtJMnBRCPgQbWUmTQmK/g0ga8jBg6HkHJQIXE6j6ppMXaa1rAbA4lIdNLPgU+KBaOecXkp1aaupZ9BKG61UCjbq3oAihAABjnSPscsa+e/X230ZLgllZVSgcPzpAjqoNTJpEji8tG8FFB061yNva5ZzoTwVMqFCPvU4UOk2ZmIm2syIiGuRob7tPBO0p/ARoaFd8zUEsKPmgIa1lNGhPnYPR1AFeFm9Mk+24AABnnhFjw0Gz2OwTB3FK91NzUL8SkJhqf1nm3XXhiLkIRwcy3I8JmUlZ9GTsjkyJq9Ka1uZ2qaW7Hz2dmfdkl1ih7Tva8q/asbQfSUgPP+Be3V4xxh0yucpminlFLbVDXtq9DFg6AjTDs7JDSQZWMzyCjlIDZtk5Sk1NChRI1ydFLJC52mdm9q6P1R1dRTkcMjIohEpy3hO+pnuZPbtrzu4/CM6YgdtXDUX9x+lGNy8v5nltABNG0szOyZdA0AkqpMxaVTZ9aPPtR+H//uUZPIAA11dwUsoGmJGoqhpZOMoDMFdAK0kaQlcoyBVpAk5opEcpKq5dUjbL6vWg6yyCj90jKrgMkS4KZUQuOkQ48gtbmZJa1Dn3EjyxSIYUQuhoccpK0Qx57O+s9kNwUAcfatc1bmNQ2vxMmYqQiOG6CPBn+yk7C9scTIT5wkS1yPpm0PCA7S1FF87w8AcUutY6sLNZKjdP4OBLJTa3f2ljqsfv5qEDCL749bllh3Iak2CAAP1ecI44R5smDh74kZZqGZ4HSbONLxuHj3whnpR7Q5fqP8QjqqWS5uxHGKmTeDyz3JY680yIvy9VyxIpy3XLS0DnB0+GhacMkHIotBU5In4BaAE0JeiBICANfjWbGakUusohOzjw9Di96Ce9bZxmZkfDaso0gBcG6PRsK65uU4Y//ilyt4Idr9CnKG5WpPlYDn3OfAv9lvfU/KYftr46xaUVtjMxBZLBIs3EAgEQGudxpgWqRZQkMeHFfKuYK+SKR40ji1WxC2Wm/6h//uUZOgOAudOQAMvGTJHh1hpUCKeC9kNACygaYlenSBVpIywabBA/SgtgTWIIbLLAtyhWI1FQnTIxSiKj0vWuMFSTrEBNMuMvGgvYfMFzrEUtKhr2oABgAAVb5ViA4e5cAcfEF2tL1NiVmYytg1afwb4gJoXUINq/An+O+z+/kK2Sot93ZfqXVu+/WnXydQqlKl5JQI8Xeoayfg+wYvCm9n3zOTbX0PAGWV5VAI/SQW9gxNsqDHQpAlDmJbAo2mgtNtZLvRJHZMy6dGbcvKwpT/5veCu1XrfFiLIkhp8iJQ0QvL2AAzJzKWiZyFta9te4RLWlR++JHxd4Vas/SArBaq1ypTHOEj5nEoexyglq3zvjl2RvvpErNvMi88QhfzppGtAiVl5ubgqqOXSFKwTyqQUMK2I14yCEvSpQuAzU0NVWQ2hqeRlmeYJXMPx0FrdIVBX61mEsL8F9NO2d6/JT/c+8cqA87jE5o0jlzbwaRNOJCLxmrSTSOTN2B39sHb3//uUROiAQqckwTMHElJRA/g2YMMiCkx/BSwgRYlkEyAFhJiYmzy81ygvN5rVrsxfvdYrfNFvL6sP+7SvfBUtXT7fH53vLLHr6MrRVyzT7AsRbt3e7JX7o+L57vDVWutWz+v9cK1pmIBzSiIzf0oB79xRHtlSXdQEK8+lCMLL6XKJNnHCIJCgyDAqEZFx5N4FQPSl4HadalbI6crIoZ0UM0eS7UAFPH9p6sAEIFRWtFSUPzLwqrmA8kaYqTcpUoDqt0O42hIe2SNc5LHMUKOUfFTabo2lFjHl0KWiwv9SNkPxdttTfKUC1kw1CmGLCJEAbB1Sn6JKZV9G8PUXJmXJBTSVWzcqiSziWvMsSutkiRyu5JI0iAQCixKe1ORRw6YzNV2/+7MFXYMK+KFEwGiobEoK6eCx6DRUGisse4NeOeJQ0rgriKd8GagHxykUQEEbDuIjTmDoJfezFtddueWRNOQEgyN2EysqwQSoe62LK114zdOjh9cTw4pcLWg2Gfuf//uURO2OAxk8PwMJG2JbZEgBYYYmSLhdACgkRcEHDOFkF5QIRXtOrm4JkZesTn/WaZW5gToUtZgbfBq2Ri9mgKkDBSIN/d0g+tyW8rr+KBCQMCgwkCAxAOQQ2Wf9pNONKLMPMPLZyiyjzD6qqoldNNMr//+VVVVU0000A1VQZTTb///////ppppqqqqr/////////+000VVVVU0000VKOkBOgc8T7WkcDXDCjGt70ZDMGc916p6yfbFYdohnHWlQqUEtSz2UlLMjoEaap2P2ZEhZhJOaPt6qekjWraK/6q28duKb9LHCoiv39YspT+18qvcZDm8fk+j92s9/PxoAISlJ3Z6lIaPDhk1bLRk2PmG2J5rnmzCOF1GWZubkotDg4PmG81l4wwsQdPZ0+7////s7toUww44zs7OMOOIBzSNYITRl0yYNaDLAoQCABJTTkqVCcs+nOB41tFBetKN23ExxgVAT1irrbEveP2rEFRAXTrUhSPWBkNe7onxYLoXa//uURPiM4u4vPAHmHFBhZifxYSguSghIfEMYwAl8nB9BJiD5hahi9Q1sy5w+LFUOIkscGA+Qc6DiKQuD548XZO3KI1Ggt1jggxiAtILg84I8FrJlIpmFpEOekhwRDfcptzAgtO0HP0zh+iJuf8CJXcRQ/Cf//qSRJdG0jeknST5yZwPn/83tf8f4DxHSzv+cz+vwdx3dAQiAALPqvb9eAYrwr6Vd7wSFd2QJ6iBtC2Nc5Elpu4WqCSJ9TWlRLwLKcZcia+jNREMm03KRmaO7W9aCwe79q2PHkMlQWlLIiI5lkSHfjR1blM45lHcVZW1Nm5bDaKWTqeCBUK/wVpQAJPcXEeYBMxuRizWCuSdX2TZlygprNQ/s9J56f/kPlsj9fVlSa81x1VOnm6E+jFDyLFOUqCTxsIi5QFD5S/bTUyiJdXBkwo1jBXQIfWu+eaul5DpGe7mZ7JFRqZOimOe+UCUEfaBAAA11SBhUSoSEKtXiopM2qhNhQFF0oVRBSYK4//uURO8IAlc7rbDJE2BEAphdNGU0DKi48geYcUmbrV/k8w2hFDQp/wsnxSQ38RWR3gvwsm3fwK+/hRwLl8LFgKNOFBQTC64K4+K7/6C//xXf/+FcChvnDcn82A6vW4AX234c/toLCLenJkGo2N+wPHfKa8WMjqYYm/G31iBb3HTaRHVzcnQcoD3+YcZMjo04/r/QmQ62J8TbzfMhypw8TP3/sdz5RMnv33bm7NMU/oFYQVgoIGCBhAYOBQQMEDCAwcUEDBAwgOxZZUtlllQODBBQwMEDCBwcUMDBAwgexWoIGEeyywGED2KwMEDCF6StNN+VVX/6af/lVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRAAALR6cKwIZO6L1jMNPL5p7pmVcFaa8UGLl50b//uURO+EQzRcv7HmG2JTIXe2DSIaSpSY/CC9AUkollAIMY0xP2qg737anoanYe9+J277bOUwmveR/usyQzbnvecO2XXOpCBEVSQp2sRsS4MzHLGK4ydegk77QqtyapK5QZDzl7Knv1a0aJp26+aXyMa8aikf4EslONh/TB0pAYb0y/k1iueqTqBoNcx1qlXS66hB0P+rUnEDCNVZiDM6ZHJfKM3IUm7vLEq0573E8Y10lsXkILeD91zTVQYHUmxW1aOF0lVKKKSfMNfGcYma6ZUYnhWypD24LRz4o9VMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQAgAAAVr2VinDlX+ZQXl958WlXfQqauhkcuHYqbBV8oUX+sjM9jx+GWr9bTxK6Q0vqWcGl3//uURLyG8ptHv7GnG3IAAA0gAAABED1+9swwzQgAADSAAAAEJGMXjF2QzDFFORihopIwY3tJTzNnOB/FfkN7As8Ei2x4W1o5aQPlTQsh/RpfhxA3NDiYN3a4UJ0bkhCIklJNw9Lx4KljdyVPMbHgusatH3BKOBBONm5oj1n052Iv8w/9TWgh6WwheKM6tvccylH19oPQVhKNqU3Rwx9M5ekOQujnngWz4Xs5tvCD6iaeybYAfz7K2tRbd1obtmmXzIDHZsfPg3ezmYXOn1CRDDZHTuAyLw6QzO0ltDBMQU1FMy4xMDCqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgCBAQIKoVVzAG+7bW0KCfIVnGBi5JN1xWRhjoRQx8SJKNQy71TO5FaQ4xvNXMOoaykcr//uURMiA83BWPssJG3IAAA0gAAABD/2A/Uikz8AAADSAAAAEm/19vshU3jZ5euKPyvrMohdTUwxiIDZ82Op5z4F3IzPMsG9WVbPWVO55wDzyfxeapGdyjCPQdTw0vgFOvW9MqlUoia0nxw4hD2W2ioE4wXKnjUswvKC0uWnJmMa6gMMCjfPI6ikNCnyTi0QUQLJXkvzAsJsaW2USnQXwsrZ5hibYlSUkrR+YdZ16duHW2vukW2LxGTY0jSBNCjkrgvUb12NnDeReLR/OeTkjTFsbG3KNSWhXnSr7JVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uURMeA8upVQeFlGvoAAA0gAAABEcWK8Cww0oAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUAAHNCQsXMCYhYJiyMlFR1hJ0SIomNBi0EjbInWidaJIpZEqwWB0kBSLvCpJ6iQUArAESCQFIjCYSApEFiJ2kX/27AERCQFIjUiwFFaWmmPTH+siEg9CoZGIVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uURH6P8swUuJDJMFAAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUBgABAAAgD/RrBgAIZtqjFHDUPGX+HAAieJjXwIkY+4K2f6a7XDEYgaYwQYCO04Gih8Bg0X+8BofgZ5LYGHwp+BigGAYxCYAgs/8DGgODJgMDAQMKAYTB/934GDQKJOBkUYgKBoAIOAFA///E3BdQDY+NNQbeNb//xSgGAgEFjg54XABqwZAQQ///yISsISDIBYYI0IOLgIp////mg4CmbmxFG//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpBQAACAAADSCgAAEYoE4cJxv//////////zchhEKBQEBAEBAIAwCAQAAADuTUvIyMVeJMvgCcTO2Rcs0IQNTMY7AEjQZATkxT4VnMA4BGjAowDIwU0IfAoCGYJsAnBcDHAoAkGACSctUDG6pAz8oAQAcDCAXKZFQAQMBhIKgZdCQBwVLIfCWncAoqgYeIQGOxsKoXCPkyUsEBJAxWKgMDAkAYjgHDsZEqybIDSSWoCAWAeFgMKAQDFYQABBYyxoOUM8PROmKlVYesQ8fA3BlBlC8XS6yJDkV2+IzHcJ5DpHLhAUSitjI6a6/65ElEaXhyScWi5itS1//5mianzJZ48bHUk0P+j//6SR3/MAzACUVDVmy48xkA0G/1TiwAUZd0LKGG4g1v+CgBBd5gA4DMYEcAmZJmgGmSKAsKOm6gMYAwDFITACAftCxcDCYRAOB/38pAYPAoi4nQPj//DLYyYW8EXIoQP////uUZP+ABaSBQKZ+oAAAAA0gwAAAHamBG7n6gAJ7QFzDP1AAFzjjHWLjK5IkX//9QWjiCY4yCEAHALj///yfUbEAIgUBlBwDvM////8nyfTppoETKBEBZY55P///////////kENFAADUtMBbmYyZ2NzfgEWPjBHIzhh4jNLKGML0UgxEQtTCPBFBgMxg0AKAgA4rASZc4rX12QUrwEg5VIjIjE7f3PbczDmY2YYNBazfiwAjoYFIGOPjfnLf77hvlyGHTUxAQeDBoFGiOZhhQBkAs+5T5zlvn8/9///+8rztupE4vADiXZdOZ7//7////3+Z6sbugRrPTShtibbg84mTmTASnzZl827JNssrBACAEo4cPS4dBnkaXCIZDVEaRAMZik0YSAmYtBqYSgyFwCYKzJFZtWsNfcpqL+NdW02Y49A30dEuC94kB95VRSOQWs+8z1lq1rD6WNvA6ltpDqSs2A+swyJA+CCnKZt7VdiNzNKLj7i+rGo/aPe1VKIx//uUZI0MRUAyRp97YAByY1i37uAAFHjbHG9g9QF8k+NB7CE5gAIAEpmfwSG2ZaWYj49J1Px/mH0JycnAHJjVEyiwlhgbA1mEcB6IwO1kAYChrLT1p3nkeZsJdRFYwPgmQEE2PAWmAaB0YcYbRQCYhku9ci9TGMQQMAwe83zJIE3TescUPFLpvpOzylJqyiTVGUIDmnQzXltN3f//6/69t9IBe+HIej0HMLiTiALGxBP///uhpYHA2ETX/rUlLM+mzi2lVrbHGwsVVBDGzCCMO4KgxkCxTCaAUMewKQxBgVTBRAUJAHwSBeX0RmXO90qijyQI1iKPCh8A6A5DJFDDWxXDuYVIU/Dpu0zRhj1vcziA5FFKe3HACrhMUe////104yi93FFPRVUAAG0gJpuMAGGxCbME4CcZs5AGDCecVUZuHnKqZkRbg2dwc8aX5nPhoCYTCxIaDE8pfc381IuZurzmn+YWXpMJRjSCUxkQQoMdGDDx066oMvHTIAEvM0Ol//uUZCwIBPA6UOuZ2SxQpRpdYexPjgUlVayabpjuGiu085o/tzWesabWest95nreufnhKYMZcKAZgwcZqUiwOv1vjAhUxsqMYCUECBzgz348///f/////+//91aCRKYoAWVQtrLMEAbFMgAAXECkq4gAOWbIxAIMgKGiP8rA/9OShZKpKTpNEOMTe/JX5k1r+A5F1H8odsrAGrZGavmm/M98ss2ifpubb+mdrAZgcce4rCCTm3bf/wawy4ioxHcKBGuZWRKUfI5CiKsA0MYPQMXWmk6xEQZhgOETBd1a5AycZPHHenZSw6wCrh6TUgHaME7VFxDWZl8YQoMky4ymT2/SS6y6z//pl8csUGLeQAnC4QMQnBv4NgsPXHPJ83TSf/////WmXEHfAAwAALY22KHAApkai1lmQbGuN5f8gBbCSJKG3+bEeIooSvozmg9Zrhh2/+MiWTM6pb////6nrjPZ4DAaY9UAABOtolpmMFRUuVKSNhuAP9ToOElvplDh//uUZAyIA51MVmshVww9ZnstPMpnzbUNX0xB9bD0ISspACfOJE6hwgaZSpZKmBEB0NCGGojKpfLZ6ap7NNdwp6XX93S/j+5Sodn/5bxZgHRMpjNWdrVcL+NUhhEaYTuL9f5zw8ASHxxxpCFyFELR3/oe5qWdf//1NKkpucPhsAAG3XE5YIAOyQIsOBpytoJJSMvJrJhAHNo+eWc1vXOe8w/mhKPe9RIAiGJrGurOymOqaf///iGOepo9GQ2aigWBA03QaVBUbswo1hdIFjAw0mC+9pTRrzD9YtfXpbzlFmfr26tjWCkKJ5ulZBC7fMklLNyGEEW+oc0Mtg3oAeDkkuVE6CBggh///+3/8CC0EHDuAECGpeJDhLYcBLQM6FIyPQEwf///7TmscAAKSJQAgBFxWg+hNTYPqIzB8RQi4uc7MmM1GBupdeozTdborR70//zI/Uxmb2+tP/8rUB0E2s2vhG0KItoEgOGpOAw87MdVjvi0xcNOlQjBycJfzF28//uUZAyMg3FBUhtmbLQ85Do6ZYJmDmT1Om7tScD1GimllKmee1DMBiHnTNVFkL4zdKABUMuppIv6e0SBDW8VhyLKsjZ7bFCZ7zzSTKdEQo2pekYhJSik/+iy1stFkWRRpa0TaldFqnpe1fWYmv6iSNvwVBp///rdyQAAGlECZgDCbDyoWD1FjG2SQ4ke5CQPDEwtdy1mydu5LRu8eixqmOd3FeopruJe62N8N4J/0Dfr/8n/kesAAXwAAF0YWm8ZnnObUqIZLG2cPFQYLISbmj4YSkEaVASeaQlYcYMPnRNg0fumtAKuCD7UmrMPLtM3btBrvONFYGij9xSnmqGUyqMxait0lOYaahpzOPRx7gDSOvuGR7f/W9a7qyzR8zsxxATmL0/8hJfIgsAXQOAkehARTtVAUxDCLNIA0mkrpbHVnbj1dfKQ6nUXvZiy1b/yjftVf80Au7mTfNt/uj9DXa3JW/qfw3UAp2wAwZ+Mxih8oPI7zR4g6SXClmd6xGB///uUZAyMk1UzUJtvHGRBpnqnYKtvjjDNNG7pTUDykSrlhB3uRrxGakUq5JBIyAzL5PvYAwWLA0In1OThjjKxSun8jg/tEhz3gazjV8ViTbxnea13+J0zVvjWpZS6LF8Ztf/Ne6UnhPCeGlcLVyJww3RXkA1qcAIJOWMCmpVjK4biNAKqZQMlaGunWbNOGUYaxDv/7OzvPV40VXuUqYFH/RRDgnSll1cSp0+nV9f/FZjU3W1Rqf/lwAJuwDBtCAqcphe7RvzfxpcGR8iPJizDZo6vhh3YxiSGJmqbAKG0BA0aOgQRDMuFBGZjeRBWwv8mALLJ5rcWUWkUw0x3KeVxKSWdQ5SZ162uKK10SoqEi0EstflB6FcOH1a3ezJu7a6SQrZJhCDwW/++6775HFU4W2BXpAFTxUHKH25HFjvxLYpVEapsLMj1nnsQlPB4+DN9FZKn9G5T+UCA4+zYs00EEjmzLImAygAAA1G3G3GBDxgjoVXmgnmTKgE8SORUcSDi//uURA0Igskz1GtMPLxZpFn3c2geikiLT00lcHlWEOhZzJkmYOICvIigAhDxepSuC/1WaKPFNnI4qr4hIVWSapftAs1ms1B1AerO2FQcY6CKe79BUAGLa////nC5u6C5/871gMBKdgACAWZOXZmeCmpJGcjLmnWhBemQswW7hNKMST8hEDmhED9Pu3cvIRBFqEvBO5wWE4OmDjELLJdjDMhxvbPcz6IP7gm+q9iQCCccNxMsHjpsLyKxIdxQe2m5WAEtw4wDEBndBnhAapEDcABmSixoRGqJ4C7ryVfT0XLUnSICnKvWM00XkUxgJEdlwyojoP5+Xz58hNzkDs/7icASszUr6OWGxb/fiA/7JiCpyBCBTHQaMjEoy28jPREOFC8wEdTAgsLSAaVFjaBBgQ3I2cROaSOXLMQ9DzZ/uCiTB0o5IlSIU1p00lzdd5/KS/NHHfP++oBXyp2bQ2gu6dEP+jlaABliAMEcP0whC9jJDIcMW5VE3xEc7Xf8wrqc//uUZA2Mg3Yiy5vdYPBAxfqaYYdNjjC/LG9wqYD9kimdga4e4ADwz+sk2VGAydSUHFKYOgEblgEAgMAwAonGAYlBgdLHayqIaBpxU3jIIBWOgdOkwlFeTZYuuvjb/XG2py2TkV+85itMznuLmeN48ceWvWoQM/+gAAGik3WAGitlKhVgVBRKjGU+EjE8Q/pCSN+peN2vUljVvLsq7eGtv4qahplsq+2eWRWG7/oBIxdv3W31Xd5R+XAAeYAMCIScxWQKjISRZMJpe4wyBsjR3G4MEUrkzDhFzF0PnMD4Kw6sojBYEBCPNtQIyQBUuWcmKTWGCZQR01cpwrNeddqr3qaa4EhdCs+j8RSBhYQCY5mIFB9zq5zB/YJiv5zh8y12u7qImqy95cQdkBlJSAANhWUFpF9i2wCCqqTsIcRVVl2YdSL9sxOex1EoQ1x1OsFle1bTOzh50X2Yy6oWj/2bA4dY1JwRBQIejZUAIKTiAAUETG08ziANR5zIn82QyMIQ//uURAsNwrkv0Tt4QjxUBPoHc2geyeSTRE3lCLlkkSaN3iB4ToBsxRBDGQ/ueVLQ/Hc67fFEuZX0ho4tR0ieIIqycaPHsRF2PBwXearcNrTQJp/yxUApEyqzfCcz7NHv/cQLH80AQhOYAAGBUwumzRSsM+wkwq1P6UDFlU1ovM+rQFSBBU3peQzYNHhCitsRkEM2p2BbFWxNvC5rqt3ER/Ba3HGUVWNEGOL+hAA462v668e6ZxOxwKyF7cgQdm8kZxHAaS0GuFoKBTvDEOLQ6FNY2ZZUTxpRTs8kYBEWQ5xyiwNpgo6VVoXZuVrKFThm6Dg+VSkWvjQgHJVWGdC8WhyYnQ0mQ2/nQjL6AAMDZj8WZv6QRtFKhpX1HSJyYsMpxBkmaiuLkExwI2kmAQQajBKLK/IUMgCA36lkkemKUkgzYjEY1RgsyHMBM02rZYu9cQDNhwknr+DgAaDqbQ6p7fKKBKlgAFRYDEgZqXJynEmyhmbXDpVIR1wqkPwDtTS8//uURA8N0rojTxuaQdZfBDlje4Y8CjyLPG4ZMVlJESXJ3aUgtCoEzSJJuALQXDKcxWrCpfSyTB1xMQcSotZpZpQ9UaaNHNPx9Nl3/7kAuEA1AsqixG1mv/bukf/vAAHQAASDMYKRJxiSHbmXifeY5gvZonhHmDaDKZDQlpjz1HhCaZFaA8IDAZfMItIRgVbVHCgwBMdkDlCwLqwRMN5G72ki9Ah6KJnIo1uQ9N817QzWB1///kTQK1upH1qBTg4LDE6sMQTw51ZjfIjMrlYURBywamG10IwSIia3UvoY6D6fUkiCh7Fr8XPsinYqPCTtObprPG5RdwvGnnhnqQQPG/7ywRK3vVInIlKaSf/5BeYECMZqvUbReEdQYgcoiWYiukYBAIbWEqY2ygYJiaWHUBAJiQUfUQkw0q5HRMtr8Ny+CVYoJfERnVJPRLHBqNYxCMfkf/932qeqKR03//+XUfUEgAMBgJgxQT2zMVUDNvNjE2KgeDamCtMMwzg0VgxT//uUZA+Mkz8jSZPcWmY6RDq9YSY5jQiLJm9taYDHkOt09hz+HtQEMsUF80BsA5xmGx0flU5pEHBYBKBFU2mEAEuiFKUlYQTlYTB0NTMspnKiUutxW9atHGboNyJSLJcmgr2o93/qSBovX8bPLgAAFRxzWUACgehUqgbYh4yfytLLAONWDbDNgqmSjqB7eRKSVNPZKkH8/PlkiWctpZb+AZv//LJN//UAREAAYCYBJihJgmfYa+bwSXBrVgRmTWXWAD4jLqBcMg0QYzQQET99UxMwMnnzPegHP4VAiUHGkwHCqqpEAsILwrVUOS+bm16non2h6GZ21fNl6JPLd9psc9sFXGw0pv/DxiZ7xdpr/WXJLLrhuFOIIOUdBAFhiqueHVQt5gWGT5766/QO49ilrYXTTZf5UurI6oxlSL/VAWO6VQREAABoD8xeCIDKLUWNZMhU8Chw+fOoxhrAzkIg0PhE2VB4zECAxPAUwwA40wG8DBAydJEqCEsOgCZm1hQe//uUZB8Nkv4iSxvdYPo4pEp6YYg5ivCJNE7oyXjcjafdpI2UBHGBUECyToSkvUn0LHLvat8/Xf+s/0d7cdLqZNbWmU32yAACpuSAACXsJC7yy4JICmkDpWDjwtVXJ5pXEI/WnRYocsTasWOYkZLTCVFyMlpad7hORb/9oBelAtmBIEGBrpGd9fn2w3GncemsxnGDzAGLowGGrZmCoVHZHGLBCpA+7tmkmgQeBiwZprXoZc69TxiLz9i+QPgsy3pMimivN6W/Yc9HywXjfNs0cEAIgDWbghckqiYUCi2g1h42gE8YEK3CKeypqYOBSOnUAXAQlhc+9zQ69w6I6AYh0uh7DUjRBRHPxEl0Ai8AAEilGAAAoHTKIbOBJA/GFAb5HLF4WxjmxAwChDoAxAxTyWWBQpfs5LyQJaZXljiJMFwinLUOa1HK6XXi1x/yIs6sI8Jv0YCRrCrawEDeAACAJmEdg9AHpRoZyhxro3mEMRjowYVKh0oWRc9LcxEMeMQB//uURDmJkmIiz1ObQOxK5Gnmc2kniJyHQ05pIzEgESelzSyvgAKp8kLrMLyYhesXKE6r2l9y9bq9y9ZH08pmF/J3K1HumxzEAMnLCoBxRDmeTAc6JZztpwyoByAf0YxKRgACOdaTExiEoEYMCVKLRjJqQy9dWwhG32xdZkqdLZfgLKTqED2Kl6StGymQJJrxEAQY3TW8sPCH8ZXZvcSmLlGzJGWVk+EAhIGewa6qep3EeKNSCqfMmZBkdMzYc07OUc1U1pftW6n2KEebSOOvj2UChYTyAADAWgAMC8C4xJAnzFnS7NPktI6VfQ2bSYwvTg5VD4zERw1wBEwDKlBkQhIY6iu6SsEACMFi86T4GDdcanE3Zze63MrbxPp19MzrnUc27pbQaf5LepTJn6AC49lbuNEAAF9OS2gACAW2LIEVkS2ZsaK23tPThQvBI45gm8NpWyluYrrnn2pC6zS7gVVXUVKuh/rChZlBITxgIhHGAOucaBi7huYMomuobOad//uUZFWJEvUjS8vdYO42xFqNYGWFi8yJJu9tCUjhESj1l5T2gWRgtCNAKE4xEDpDEABjMCuAEZGLmp9NsCi8wYALmKTKCJt5fFXOjEMRCDJXT04uLnnH3buXMy483/8Q+4gHLSI+WAdZQQnXrAAACdjjctszNkYHumQYThOMU1JJHpxzkOFLNh+G8E+0xiUk+M43IixPvKqqmudR93zP639/2C1pAAACbid1AAEFGIghEpHCyQO5gYFsZGr8YMhoHLdwidSwo4OlqT7EGgN46sUpboDB4dO7fPia6aI/PIAkVS1lgUdOzE5qoJVmOJTI9/rAAAFtt11AAEwYkYa1wcfcdlYOmXwFkYwqTPa3exIgk7GmSssWY4khiK/4jHS6ctfdevhZG/Kz4nFItJIJlBoyblwyo7EItZvNHVM6U1HLrsLoJkiiahAnIKEzIVFh3NBoiPKety5lcid14WkhEG7cOq2gUZMqzRFpP3TSI55xchBQlVTc7rA0jyrNPrFU//uURG2Jkmoh0mtmTL5LpEpdaYeVyQyJTa3pI3kgESh1zLCntfBknyZ3A3JJbGYmB1OZGg5oJ3j1QA0aLRg8cL0E8asFdoxe/TWYy0Yn3qDWhzDA0tUrYY2ucjtGmFC7d6IyZd2rmpv2eZNZma5iaz3alQAAbbtoAAkRhQSYC5Gr1RoqKQAwzOJ1iMkmia/AnWbX0OAcSpNNErKyJVq0NCqZCYWZk1OMJaKSYUslmlWRg11pxhLY5cVHgoVGSogAAQ7LLdQAA+Jhw2YVIGn1BsZ2YgOIeiQ2XnIi5ndJNoDp+E0rMhmIA7GYQxJjo/OpParnoSzhryYZHMUHyfK7WqHLBwGora8uC891gucwfDQxmXI17UM2Y083NnjdQnAMTNwjcwq8SIxmNBLXLWhw8hSKLPkfkum6uo70ljEuBSlJGwlLXMxstuM7y15T4u2Zr/KysTEhuMIQ8MrH6OkypNsunPycw/IjzCBdNDjEw3FzMoAMRBtW4KAEzEDPuzrW//uURIaJEl4h0tN5ST5MZDpNbYh5iUxtNG7ww8klkSYJ3hh23moZBRplG96lGnxKbVWZT5Yq4p25hWbyUP/nY8baFgFeAACABDJE5DcYzzSvWCDAmgSeAZGZzDJgFymSgQasQk4kEIjV3I+sSIA3JaU2F9H/jkflFPnhXsBgYPVVVVWWW8aicqqlpS6a2HJAAAAAHDDlGYMqYskxJUFDru7PYsswygzM4vANZMnAgAh5mo6EgoNy+TcaJnr8wFG5BPzVCIAobiRS5YEFQHrKRcpXhIaBS4CkQU4CAzMW1PMIX+MOtTM610MrQ8KqXspCo5g5BDG7StYUNJqoJqsXZK0hhrTharTHpsopWNy9u/rRPiUh4YWRiekaLo6qYPxfqb8wSDcyBTEycpAxbq8QJ8aAkQYW2hguKQ4l4Dgqx15xJ2U2gmYoJqIxIDOSIkjX+jRytflHlH+jTjdfc2L1oLqlf/9tBhrPsboG3gAAwGwPTFCNLM29E8yVmujKoNrM//uURJ6N0lUPzRu8yKJKo7lze4MfCRBPNG7liNklEWaJ3ZivmIKwxl0TXAmBKIOUCcw4diIAGCAGZMAxhgFIJUmkJiE1CpAxY8ojbKG7xqXV6yludmtK83LaQMg/syQ8sgFsanFP5ZmQACrnLqAAM26CQQqwSYRHKpVuKaGkJpBxqBYV6IlneAItAivnph2EPkBhC6whQnnFHEqYQUxp0nwPLcAm4VA8MQotYykAXTNtabM40uAwyhIzBGF7MTwFQCjJGFcCqdtKJVAUOMcvNAOUVDg4wAQTFAFHtZy/HjgJyILnbdIo6B2RK6ZpmHBE6UyNXwroXIgMCJu988AAW1bmtCsoL8FMm9KVRUWRMMoQFuNgePBIMy0Mh8PU9ztn/yoNTKqD/H+eWi/l8IML1GWzWpB6T3T9kgfAAwEgcjELRAMc1Xg0inGjSmFBMm0AkwJRcTHyCIMEEgAwXQEzfMhkmNEj3mDHCIsYcGWTQzDgafSYCkHraXEngkcxLHAw//uUZLkNEtYaSxvcMXI6xEp6YeM5y0iJLG9oaUjsEKlphhjnwQS6O+hU99Nz0cJXNhFfvic/ugAAlKcgAAuCokCxwQ8N6AnVFExti1DSwlmqvyWSjUTsSt5xcGrtfzOISI4fmZ04m5kkRQ5p06LiIPowzwALpg4ACGRaf6YpkEJsdw9myuP2YPY2hgfCzmV0GQYL45RjGgVHd2GoJGqLn+yCAMYMQgeQGwEAFghMYEhC7JU8anbxvxINDwcHJrXqZM96bFa/b5n2GYrL7sVrpu28YyDoGHIXGrez0fJV4nUEwplIeC4tUwKFKhyhL+xg3ccRDr08mDubIIEAnkSJ9QhmTl28y9UBgAMCyBQTCYDC0yJQkgM3sQjj0ub8N6AS8wfhLTYyEoMJEFMDPXn8UZpp+ZmnHZSJqacZ2nGHqAoqDJSYWCkQmWWAoMOhBVBCwAKKNbhbrOtFtSGDoFg+Rz9erS2b1inu3RepYXNvpPr1gAjOU5AABBQICG9gGysA//uUZNGNktAiSpPaGlY8hCoaayMdy1h3Jm9oyUDfkOlphgzvFwEcn7FyiJT4LVMwR/KpELg3FC4pV00TUFAw1UMzRmVRxxUZil28zmIELLenama01pLX9cPrEYbITxr7M8Hv/HEfmezp0sxHmeiYKYK4rxsQAxAxBEyZwgDMNaMHDQxiLD3YINHjwxWSAaJASHCTgXcexAPxnMndNNbZm1pkzbOU4T7Q9J79uejUSlVq1ay33WtZdyw/nN301H8AAJtJyPQQigZtNjTNFHM0KHABGEWjKEjpU8HhaLwi4ZHyo6NT05Lpiqjs0199dFAQaH+TMzMzQS0YWbMGhIVNg0WcLQdAAwE0GYMGbMTDFNkB80hYoQMVMLIjFxwWQwFMLiMV6AsTA6BTswKYD6GLs0s7NikzorI0UiMrHjBikwQzARGYYEiw6WaAQE3EvazVsDBYFjTtS6OxmpGgk4BHmo5RK0qecZrOqyCPOjeoE7wAAWRAxfN1Tg2NLDdruNcF//uUZOqNE1EYRpP+2KBE5En6aeY7zKh3Gi9zCJkOEefppg1vcwkmgjunB4CYOzVTUEC5S6UlWFgl63tmnetwzZBARbSqUNgJlUoezaltqxqJVOjJQY7+uGg8YNoDqmNvhOJmrCFga+ePimHAHZZizQZgYL4IBGKCApJgzwlSYZgBNGgKsZpIxgUdHYGeIyAAi2YuDIyAQUJDCYCBALQcLpFuWGJ1NFe93aQNFTAqKmBKWeyo1eIpvjWxqs8aNx+389rq3pEYpxqdhZiecEdXOfYqcseOxg0azlqIwCdZTlMol9oZzE0QYeROTIvjbLaiyiVzWyyzk5q5qgmVHF4UQk35JpuVGAwBhDzFKmBN3lKU/0DyDdBfyMgQWEwekRDLCEbMP4BkycgQTtW0x8zMHNDkVUUHhGIggIjxVAS8CC6aica3WbOq70qu3C0FqHBJE4uLx82vTtf1WDHe5tsqDAAGAMAv5ggRuWZieD3GoJAsxigpioYqYH5mUFHG9yZm//uUZOyNk1ceRpP7MmJGw6ljcwM6TXyHGE/xKUEBDmcpp5lPlhGnqQfmMoMkIPmDYqGwY0mC4EIAy/CcSQSIqVzTWXMpeIDxgo25pWpsNtsvjKMrpDNi8RcWg/QOGCmDUZwAfhmCOFn8eHCctipZkjhcmCmjYYoAjZg9FDGNUBufzseEAYZCOqjRBCoDQeCxhYBNNlTHWDs4gsNBEwmiQrRmwjnWN2GLkSOogxN99BIYICB5GN9CPZh8SpSbByHiGXKFwplEoQKYKmG4GDuAspgXQiEYZgBUmQPg8dGBz5jv8mmkOn9DKtKl60IJRKeR3woVMm0F/0/yYXUT3J1U7/yKl3+dyugHAAMAbBHjACS3gwVszyNDGDiTMMQlIwgcFuMApE6jBSQMswGUPAMBBAaT6RgBLZao4AvM4BGjoZgoDHgtylcSFd8QfSGYH+N1hjsrG4RaadBnwPDbSAAYCeC0GCwIZRh8yrIaXgOEmbUi6RhbojYYAqFUGAOASJgN//uUROuN0s8bxwvbMlZZAviyf6ksSmBRHE9pKQlgjuJJ/aUgYo8IgJ00qFDDYBMNBw8QbzDwAQBxlVF7ohBztvfMWRaR6yGH00/5eFZ766mwm5HqCwwHwCQDCt8wqcxNNP9IbDP4gyQBE8BgEYv4YeyBmmClh9BhRoDyet5hIEIRQ6cZBRwvBd4IKRIETVdx813vDL3ilk/G6RA4Iupw+PSvRY1PLf1z9zlLtxGhhgPwHsZDuNhGX0nspsRp68a2MLBmcrAaZgogh8YPiBLmDnjNBh5YFyaKPZEjzCqMPgvMwmEFqwe3inojGn3hGMSFID0UDMy6feyONH5j2uPy5G+fX/zf+hvv1+vfWhoAAwOAHHMH9N/zK6hCs0iAvPM4FHGTBUwc0wBUT2MVjAGzY02DykBjJsnjEcEBAHJgKPRdlMphwUA5dLTn6aE39WAQNFXURVLaJfdDVuN0HhRYDP2I0ygABgswUGYkKoimkcIKJpryLmabSAyGJqis5UDk//uUROqN0qUXxpP7GlBUYviif4ZICzh5Fk/syUF3i6HF/hkgDCxQBIweUHWARFUaueGRjhjcOeT/DgKm5EywBVpdWrNBs7jWKyDBJAQ/GilT8xW/Sxv//XQQwTkC3MjIFUzQSVOY2bVYQP9b6w1GQNDGTV6OUcIYyGUiTOnBOMJgnEBCHBg4f8FzqZzaAkmVfKp552hQVBUSlUVh2K0szM8ws5d7W0YA9oTBU/Z0aeGByg4pkiBMSZxO9GGsfr0hprxNsZZ+GSGQSVkZzoexkOotmhODQYRQhJWC6DghTIOAXKwSkImosqYvMYvhT457KylVCKnTg+eEY+K6n9/9fX///76//6P1fpoIBUMJZEXDHCmv40ZWFZOLyjozRT2Tkx4IXCBoZQZV6C6GCykk5gmYI4a6dxn0aGIDcdpa4QQqGKgkIjQClOdl5sNNOqmKo/J15A3r6aXuefBjFtTqf8n7ONX1puyWr/9yfu/SsAAGBkAAYx5OhsrAAmysgOaP//uUROgN0rcXxZP9QdBSQoiCf2VICrxTEC/7YtFqCqHF/wywCXZnAjXmFmI4aloCRn9STRplzgAhohKzCFYSN5dHXGXFuqMnsqnfMpJ/jb7tDkpeZZ2rb0sQYQYCfGZKosBvnBrwcM7LeGNuNeRkvArCYKABpGWRgoRgqQ/6Yi0BsGgsvmGQZGE5Bmk6HGFoHrQaWIAbV5uklNNZ0gtlFRmZYKpiIuRVZ5YbIJqT/1a9/v+63/Yz/7vdsBSkbQMCg9MaVtNn+kPQbMPQWeNrz4MOyDOkwyApMDVTQ7VPMsNgLXtL7Ky0RmRkIx7l3xliKKwl+WPfN1tJ/QACBvVQjB2FVMhWZ81tVljsM8zNfyNUxwQiTChILNLcKwyWSE4UAcIQhJh7QEeMnafGGgi1+jvOQ2cYvaj8HR5nNuSFIBaTFneg0/v839S/93T/Yr/zH6dkTGDVBupjdqkGZb+0NGnLPqphRicaZdyGFmDqgrZijYJEYNkGimKIADZj8GQF//uUZOkMkx8aw5P8MkBGwviSe2k4DERrDi/0aQD9CKO13STdAUwQCk4GDQOEFPJ1CACgRWw6QMS3Pu5Wd+e9bxUetJCT1vR9f+2xpdZjos/69u//Shft7CQAAwQsBxMeAOXTE91qczwd7uNH1NYDEnQcQwIcWbMfjBTjY7Yj4YgTV0lQUnpgeGxkGPIOBpWVnQoCzfuBFA2BsUCQ6J1UdKdfcg4NB8JOYbYvZ/9P///////+gSqrIYHKBJmL9HZ5kPK7KY2msVGSylqhiPodwZEt6eWkwarUUeEDCcuPmUBZAsGqz4OGmkx5lTzUX15ZYxJ1ViVfS9FxZOz16+v+7f/u21//VWAMEPB9DGKlaQxkBZUMkwkyzYZCmMxyYIeMDGBs2nRRDEmZCMSAM05xHyaWCofNKmBXajqD6UTNnxcda7L4/EHggSF09FSXDB4DiYYCAEBAfCVc/9lf//7n81T3/rWBUDAuwWgxehA6MqMQYjBt2hk/ssDTpkGqMP4t//uURPEIgs8UxsvdYbhgIohgf6lEC5BLEE/1JwFEhmJZ/uyIczEwfDEaU+MUQJMx8QwwsGCxGcdPAkX2rO+VADKLve0QIAiYchOH6RRj0VdP+rTtu19H/XsHZ7///sGAgBvjGRTuoybcHQMGqhJDWVQcYwDkKVMBeHazDTQKIwD4VAMJpAZzu/4nVAEXjdqTEKz3fJA91mzs3ctrbd3jgOnkFidG9uO4M3pmxc5mE74wE8IwMWbTsjOZzswyEZ4+NSdKfDBoxZkEGJmSWBuYGi/Bk2g8mfzETLgweMTsBOQhaHKUtWOVMbl2z/aZwwsKBtYRW08zyGjDA30Udl9X6LN+mz91+76FK4wwqoNFM4PeETbvK/s0E/o/OW+KJjNDQbgyMa5TBkAjMY9Qw3JwRTgXXNxCMLpIzHLxUEIjBcGAwMF4ULUmVNWiunqGJRdnaWlxqcr5BM+AiKOxFlwuilX6BVH7QFu/OJXQ/FP6EtcZ6GvEoAAARkbiAAAAAZmG//uUROsNwuwQQ4v+4RBXAXiCf9wUCghrEE/saUFtBuHF/3CISxmnGfGXnZmvlSG3QvmEDTmIwKAg0iKMDcagZ2Y4lUBQLKCyRTUJRPP0xUTOmq1mJ4aXCKE19fR/7CssSmA5Bcpi2CU+YQEzVGVYRMhprJwiYSMI1GBaDNZhYABqaN2uf0k8ZTIoYiAEYHBmZ6iwgPC4BMSZkkUx+OrVhmGn2EcJQ+OVd9ENVih7h16XSSev3t/e3b/2NX1d7un//02AK/bQGBIDGNA1G0YlmJEcBVXTJUUQCa4QhA4tpQW0r2MlDJyNZYyqfpWSyl2oaC4KhLSV0tizJmK1PTcWJ2m9yjAMFtB2jGFigozcUpJMjHYuThO0uNEQUkwXkmzRYB7MF89ExFQXjK5s1gIAwWcsCDQ4Bg4LALipGq2J9Mpg5nUGxWGqSexw7zLtmqimlRf6UoTFvf8rIgIYOqDtGRBE9xqy4zyZNKxMnudjwcVxkphKFUmPkGqYZ6WpjNhE//uUZOkIk1MSQoP+4RBFYfj9dyw1TJRREs/1B0EACuT13CTsGEO5kwQOFZ9TADjlY0pRfk8tuX4zepiQkWECqiqBIzhyzp/8K//R/2f1zf/6DAJgbwxE03WMz4GLTL5lowxQdDzKFFpMGY6M1UAhTAmPNMXAEw7TSMiGREYGJpIYEEwkGBKZyApO1VRY7JGpNq70Mymgu3Ltyvhbt3rWITTRVEHJY1V77KBZTAIQpQxBtI4M9wRaTN9FaI5D8WzczMtMGcisyqAwzALTxMaYHY2piBwyOnBn0eNBkDQywlzL8tlUpuZ2AeDbdx4UEhsNqJVbd1ftutvbR2foNS1jHf9tX+hcWRUwUoHXMbHIkTDaEdw0o9crN83ig0FxcjCULDNq4A0wmCbTLqA0PZfjOAgQEJzpw+oOFzBAILg4oCoCkfU8k+FjsofGGoGkFPcxt0lPWtXrdIHC2P7BW2s1UmC7A1JjmIjKZBUsIGiYoZpg0RbwYo6GUGFrmnXgCGLV//uUROyPwq0SxQv+2KZUoZiCf9sUCvBRFA/7Ypl2hqIJ/2xQYgtjDSRwWHR0DPjIBYDfy66EtpK8xdrWb5g+0I0svHV2+j8hTzva3rcidrTqTvf3POaXp7PKgwYESDFmH7I/Zk8KQGa0ophHoRl6aSA0RgxBkm3cBAYQwxhlcAhnXJAVLTCQM4w8YaYiFjgMBggGACsJcdhSSCmcPNYhl+JLJ7FS7bqcqXqe5Y3nhyp+/1zP7Eh6P+MAYCqEOmHCLwJodaq2bVwnIGQfIQpk1wUUZ1KyfwEUZVM4fVDWYbBU3UwCCszQH4t68zkKLCyawgHWEXrrQPh6BQKOeHzgeW/1ey32f/q/b92z9rfyKg4AAwLcCcMW4GljGEhHo1ek+dPjt/gwch1TDQHYNbIDowYjVDFIBjMK8RpNwKRHRuNJGOeusgGL5riiSCi6GJsrzl0NwupRaqVa9/D6fPWsOX7Hg+Y4yhl+hgAAwNsC8MdiDADKQh4c2PE6PO/vTQxV//uUROqNwrMURIP+2KJYwaiSf7siC9BtEk/7YolXB6HF/pigERTDYDeM0gHwwjEQjHkB1OTngUemFnBwDqvBgbdKd3rOUUlcvlHO26k5T5cz1ypsLAugHyBb/3f+3+swHEFEMD3MRDGHyig0ZAmIO8oXkx2QaTB3JSNAwEIwKSqjCzBJGZpkzK3wlUUGi2YXBoc38bVfi6IMe+H7strSyKWN5XBEofWPSABkxDvH1jIYGKEDGBlKghks6/sbFEV7H3U/OboINRimiQGbwHgYMKVBiWhNoBwSA2CgChYd4mAvZi47KRXIMGBWAeRAwgJOTtqMRbYmjKODx86OPHzdM//9P7P9v//21RAMFAAXTIbxmUyWhYRN0TLjTTaRzkwnEIwMDuFFDFSgEoxpxA7JEAwYWgxvBwwgc6i5SYcfLNkoNrEWd5TZFJOpp0DOs3rmQ5Sbm5unz1fv9oAHMUKefSpWWGAADA4AJQx+ME3MLKXlTWwRqs0J0ChMB2DojLpx//uUROaP0soXxRP+yLJSwmiCf9sUCfxFGA/7QollCeIJ/yRoDqgsDHHRh9lyQlgMGxggGpjQNSc7J19xFtJ2qEw2AcByM0y8fJyNduXbe3NTBpJAPPjft/////////9I0GBhBC5gA54qZC4VlGp6CMxmjRIAYhEBLmAiCQwsK8GAtgTJhIoBIbLAmmgYWADIjWGA4CCoAKhCs7/sWS6R+UydKpFXub6BgKEghHzM7VEvQYNsVSmLnFUph/EHmRH1qeODRR/cAgnmwjobEwpZhNDtGL8BsYLoNJkrgaGFciTYgLHogxxm+oaHxcJh0Tk7cWT5s223cWWTZtsDiXYfL0/6utck/V///u/6P9QtqpAQVGZE8FummZlrxro4xgZpaXdmHxhL5gKokoYKAA3mGPHm846GUAlAIyn6MtQBVrLvF7l3RKDn5kSul3QEkTDiQNCZYsifs56tVSafOcaSIp3cr/9rqL13dn6/d/6P9gAA1X4AAXBhhBeEPNPNp80Z//uUROmNws4URAv90ZJY4oiCf6kqCvBjFE/saUFXCeKJ7SUQUTPgwCygVXOQmogAvnpNHRzJ6MipILMaSe0R2CQUSVN2iHeK/Wku3eKW/h1mygyrCYMiHmGGNL4BiRDaacCEewmRRJYBi/IMsYg7BRjDiSmFAtGYhgWwHhxbWMQRzUmoIETAQZvxEFsSdtUq1Vhl1QC7MVkri1H+yuym9Ws45U2eN7G0EhEJTjBQu5O7r+x1n4yftqX20//5On9PQcBgeADmMCQoaLSqRq/CnGfWkoYxwmpgXFJGKCBeYBoCkAumKF7MDBCWaKmja0R8hDJkARoBWVEJkiNIY+tk/JWQZ0s3KhkAAwTQCnMaXFKjKaj3I1f0viNfzS8yUBojBIQEMzcLswrCbzHaAsN0owdVCAKM8Ix4IR+VVbxXLMnXYi8UDuLAUjf6Ozkuq8rb5ZhalBEQaNN//YIAwb4BXMldL2DT5Fgo2v9E0MIZQqTGqAyA08rg3LOU0k9U7cGQ//uUZOeMkxEZRRP9SdA/AzlJceY7zdBbEE/7ZEESieKJ7STZ15dAUUYoPHeJo0QM2pXkazGItEZZLpTYl1a3La1+ludskjwSW/QM1q/X3SP//9n/1/v/ewwHgHvMG9RfzKQyIE2FZAdPDHQwyOhyyFAo0wQ5SIX0yfgHz94ha8j0G47rbNNUqZ0yluTYXofeGIfhyQSednqWmxrC4ZhI0GwWE3////////7BSFUwKoIrMNpRzzNKwZI1lhDnMZPObDD1g30w0ow4TOUzsDc70Bs2E4X+YmJHUlbLpDfaNTXbmdi3eBkFhYWCwGJiZqrk/fVd/2avH/4orqV27fqu/qogDA2QEEx3AkpMUaNXzZH1PQ0doo5MMBA7zA+wzAxi0DDMWMGN+CNMFUvGgrCoLmBwhPGH4III1jsRBLHAunxoTzuBajcXwBsHBZNweFnM/X//2//////6vW4Bgc4FCY9YdPmJcL6pqxKpUZkcZbmPIgi5pfER2eX5kJvhzIS5//uUROsNwqQSxRP+2KJZIkhxf7siClxHEi/7QoFYBmIJ/uyImf4JCgVaYHUymMW6eeQeCz7RVgo4yxW8fYvPWW/d//c5dXd/6v/0BIgpgcYPQYgYnrGMEqmBqvC5iagWIFGFRg/xgzicG0WB+YPJ/Rgqg+misBhouoeovDzlzEFw1IKe3GOTdaxlYzpwQJmQyOcOadk0JqYv/c7//T6v9qO6rf6nu9IacbjQJKMCQmMtLOAPfHIGVnIaDGF45EoQPXYXkkQ2xXycBVPF9Af7k3PcjeysKCpWAlfvV//Vt2Jtq3/6P1+uMBCEZDP7XHg0RWB/OEd56js3AmY0CoGvMCcRk8FwIjKDhuNXUQs0FhUDFgYSioaikGDheXTA6VLUaOeq0VzOv2z+deqDUxE6wmamwsxW8a1ahcaooKNLDDzxqyWlyGvS6mOYZrOl00PvTZadYKiszRSpHva8KgBa8wTAUzGxPfNfhQs32VQDUyUaMusHgwew+wM38ZWepSa///uURO2MgssRRAv9YaRMwThxf7oiC3g/EE/7RED/hOW13cCWuPDEPTECG/eQPHXc1GTbh81OLft7PT/f//f2f0M/o8Ytd2r0GHFCfZmz8emcrgsJmmibKpyApqSY2qJ+mCch+dBYehlEj7GwuBQdOUphQcGCzWZnWhgUAs+g1ECE37IBFqTZWHBz1hpKGGU6xorJa3jX8JMcVZPqqcid7Wsjr0LFE8gUWtiMzMKUF6S7XdD6AMAi1NhObMwGWg3WGiTMjeLMhwcgQkjmceCEZuuCkJ5F4cARUNAPj2fJZZQg8HFLKJZtrdHqr+SvFv/9H/3/91UwUcQJM5McxzZy7DQz0fkuN1qW5jJVQrgygWeTgXG0MoNds03QhDRa3BTMEiCc+D48FVJPkjc34dSocs2KHXGwCaEggKizSdQbVCMgsVUxhsWY/S5WRsc/eueJ9gbHN1au7Cnu6l+gqVVMXSENkosNFNesz/VrjEdW8Mh4RkwBh+zIxAYfofJPEcXE//uUZPqPg68QQYP+6RBHQbiFe0g0DWwrCA/7hEEChOHF32gQViCO6a9nJNl/YKgMCSZNnV/o2/7v/7vTd9X/+4w4UUyM5odRDa56rkz0TmJNRpenzHaBWwwZH+zZ7GmMQGHUw4w4Dq1bMpBQwsODa6JDgNnZSPh4AAsPJVE542BJDVP0NuodR2oCPCSYtiqzew0y4USnrpbAdVjC4Ef8XXl2lT8i0wIgSjFhQuFsJDMudpNBhT0yZwrzFGwTZ0RASiMYBNxPIoKaYgCObhu3aooqatOgA6Nvle5rf02/9XR/s/17Mv8YMHmEGjPSZ5Q6jZUCMtp/ZjHPJZwyVoZPMHCI8zE0wUgwaQhJMTEBGjv9Y+Y2MxVTw6gtmzCA0bWrZ4u57ORps7or7PNbROc2j/3vXWSqtd1fuuDefVqrACiakBe9LqX2TalHlpUQ4rZ0mWvIEVCrKABEiqph0NZsjAZlJuDGWHAoa+Z4xjoiiGAKXiYPYLANgIbgmIUUHIAz//uUZPOP0zYJwgP+4RBAgSiSd9kEDJQnCA/7hEEIBOIF7uiA1cHNSVyrH3Ck2BAat/0JTX/T21/3/0bC3+a/+j3mGdCmZn97jWZ4XDSGUX+iZoq0QOZVSDXmNLReYaRDxkSHVmy0CmZQp5rgWv6JsUaIDrw4oY9tzP+55XDxQwhCVBx1DQbedLkh6Zhz1DFhxzgITfUV5Y8AiI26mc1lwXSpN7jbkcyt49jTcdEzxCMZQOSA98oCDQDTBuJ7MQ1HkxXkTT5BWTZYNDApbzGwGh1Q0sYmTNgJCOw4FKYpi7S7z1LLe77Wfd/M/1bGf47Z/qZ9FTBOQ2sy5a0rMuh1KzaSOzA3yNvIMdrE2jAWlJNKAVIyhFQDWICtIeYbWD4FLhotYiEBKAu0k838pw5fyyzx1nhSicsk+kXHFRQ2L3LqUhzk9d1ZZrSZxjjCFsFlDFpGsFE3/XXSpfWprnLHzHIqKrGmAYEgYbdTsaGi4hlbzIHACGQYPov5gnFsmQsB//uUZPqPg4AuwYP7EjBGYSiGd9oEDjg1Bg/7hEEBA+Nl7uBMkCARtwBotQ9QQlio5+b12CK/XB2Jh1VN3AfFf+hCv+v+3t/RT//9f6zB6QhAzqBgIMrChTDhPcZE4qBK/MxCCbjFEeHNnAT4xeHoDGIDtPTNTsBMxMRPJbiIDVDC3ThrvbOVNdw1ZIhoRuCbDSFl23Quvuc22sWRbUhbyrVUfaVYduzxg539bk3dYsccMaXL3vQK8MDUIIwF1XDWHSMMUqXM3RjNjLLA+MFcmky8gOgvLKK4KlAqLAA9FnLtiNCoZQ7UKHcMFwCXc1Ejf260aEf19l3/9PZ//q/U/owJ0KAMQZTqDLWQhI1ZRvANXBBSjA6QikwKAOHDC5kwu7Q2pCkyGLQIHFWMDHA5lDUUfYSGCoOMzmrVDU1olDmniP2eQ6vr7+1Nav+yuL79jNlmj/qMErDPDCf2u40sVB6NqEazjWkyjYwlUShMA4lkzJgMQIpYZGYMpm0IEOJh//uUZPQP04MPwYP+4RBEYUhxd9oEDUw5CA/7ZEEfCGHF7QzYIOeQPEwrR4wDO6LFlx7n1rq1fT2t7GWlhN2/de99+24U9Vb0IVWjcOq7UdxUd1VcYcQX5qWtonP1X6fIPwJ4GKDAJBsGpyfkAqYXoACsCN/aCDKHIAj36q5Tk70BgmIwQaLBuQe/Zt9Ngr/4v7/1f/qZuX+77vkyGDZARplp5OKaVlAFm15siJrcwIgZAWC8GfsDn6gkmZtSB+bm9nGcFjioE512TNBBAaDBG9IzDNil2WsHSOtNqaLrZhT9b6vSyiT02KNfUxpj391o9CU1mZVjBchHkwzJy7Mcgfojf6npQ1/9EVMJqEbDBKAV0xzYCJNB7JPwSADnVBwvGBwLmWYfF+4xXVmHbNRfx33l2afIMOPFmgih5MKw43QbC8XSojUhnpQ9B+8lc00fph2kaU2CuKref3vfcRUpYuPkGmFEyiIj6whNZsUs1HmdDC3SnMY0MUwCQdDK0AJA//uURO6I4rITQ4v9GbBXgUhQf9siCVgpFs93REFbhKFF/uiIxpGiCoFT3B6GXZ/JDXeaHhGAXtlWOYmvubYP7f/Q30/7P/Z//1hAgBiAoYsahARFHX9q7J2/cnWbNM9sGX0CYZggweUZT0DzmCRj/JhNIJAZL3Ri8EBQTnBjADgqrE7q6xmouw5c+vzyhe7VIPYGtUI6msiLfd+yLG/Z1MnpEE0LHzP3/NHaGvtr/+p/tM3tuu/23zc9uyv/3+I5m/VP/IwYBgHhVGA2mMaTzYpuDv/mauugZLIdxggxB5oFpjyMENBjMALBbIDBwV1t5U1+6IAJbR1n54BJHtaFG9m7s/+5Hq/V//sV4cwPMSSMLfh8DWqFV85p5QsMl/X/THNwyMwGUgDcUFlMB5k0yYgpDCoc1EJKpcYU5tyh+wone/6wxyHCoeS6OcsVM4yjiiVGLLE9Ck2Go/M2o1MDhIDsXxCjfNOdD7nP22RcmcZuUwBioIpwvABlHSeG1Q5c//uUZPSMw3oQwxP9MbBDYSiSc9kED3BFBk/xaIkUhOHF7uyAa3yt5l2BwGCkQsZVgD5gsy7QCRGmbkg4dPYdgsQvov+pbIms/2r+l////+n/9bLmFRBBJnQYtsZmtMaHXamG5jp6+EY2aGiGC0rwa0QcphpHcmiEBwYWmw05wYCTaoeIg3Ak+7kJAg+dbShJcEB5dKmAZKJ3YKIxZw1blpFLnKnxqa6Y0XVuPC60BmiZixlW1bjTGvSopeBjAsZbagQA4mCuxGamropu1tim/6kYYYA1ZilihxoLoM+A6MYDQRVo2RQWgX+SmywxpV1uFlbl//3fS38X1/nbI7+r7P/tAC/5gPAZCYEC/NmefuW50iQYIZ94oImQsBKRgMwrQBhtEyodA+oIE7c1AVmYADnFHDe2sGjU7wq+YeyKoeecftFULxpBBdOsheNfQFkYodGj7A7j5vUqxFkw65acY6QxrEyz9zI0AAAm5aYIAPMSYXMy2iFTSaLoNQAGkxqw//uUZOiM0zULQov+2RA8gShxd9oEDdAnCi/7hEEFhOGF7uiAHzGvgNVBsCkH/gu41uCEe5Zv5jGjZXStkrVdb3fV9e3+9X/u/Rb+//rIAwXcCVM6wKYTQDg2M9cso2NvZU9jAJRVsxYIDDEIA5MNpo4yvAszHHQO/zETw2SDQEPjHH7hVhBeHSbDwswQn7Do4wcQTfLKrEhZasSXF2OuxdY3pUiOUbQZJPYTike56PaTZ5B7gWA2LUMAYDoJxh/KqGcLD4b0ZSpuXmQmCSLAYIV8Y7heYt4fENqjcymoKaiv7mrVC3Et8tFnikZM/p/3/+7p/W7/0/9mjoUwqUT8MfMtejaRawg+pBXdOPwLfTIlwCgysYajIlEsMQuAAyVxIzm50NCiseDQeLiYHMVg9O2UUowamXYYFIuhFojsESDBIQ6jzmirz55z9IvPrc5rCiGGHLrRIFmCoaHoz1VLDTq2tc18x0MSogDAEjDemSjGrlHOdwJg26lOjN6DRMA4//uUZO0Igy4Jwyv92YBBgTipe5ggDWwpCC/7ZEEHhOHF7uSAx4xxwSwrnTKJxYdGhkIaNB7jE7fH0tGqIZWwIBl0/66O+qyO9Tf//+7ZTi2ujZ/qhkwZIJwM83NODL6KQE7VtpCOCXHTTCCxEww9GxjU5CIMNdOw0VwFTP0RNLhIw8OTQRqDAS/1WKvbaSeePDTVBq5KZF0K3HTrFPGLG0IPWC6GPCeKOQhw/Yt3MAdi1WRcsssonFb1NGmHgNErRacQPMIIMExy07Tc0MiOp8bY0iV3zIJEzMEaeOpw1FM4tYMtpHpzckEc5v47Y5cjT2Mlxlz/N/v9PW7o/+Z9zhPT/Vt/xdWTjCbxQEwslxPOUIaLjlcpls1jAaeMUoCwTDPh1YxisBLMIFBxQETBGAogPxgHYBAYAGAFGBCAOKjrjQhRIYMiI8wuE0B9wdXIyMmhqVHXFVn16cXoUsoedSZF5WTuoStAcc+UwNCTbKnvCjgHFFsRXzSmAHglDH0L//uUZPCM82wKQYP+4RBHgUhhd9oEDfgpCC/7hEEKBSGB7uiA/Neteo42jhzEbVAMZsgwxGCNDKnBJMFUE0wKAHTAPAQQ2X2BgGr9jsoEubKRy920YA60++vWn+j+v/+yv//0J7EaDTZq/Q+vtQRNozN/DIc2WoyjLe/NluPZTJQQ1EweEFgMTaG0zAoQOcxUsnLMISADTAfQCAeACwcD2v8YGmAqyMJoDLIYCnluuMmMgfDiQ6A5UccEBQ0HGhMQMPnSIiUwPrVxQUHODJ8G1pUQXFhND1gpcsGkVMnzYTEbTdYjLONiYaGf/+oZ//wk4AGAwrEHsMXpNszFTUY8zyMsMMZUNwTGlQ/sxasKyFAJQw24DqMAAAwzBLwoEaBMDARgAyVGAFgDrxGANgCj5BaQJ0FiSUzH0GvcU9vVNMMfUcxNTF/cVv1L08f1DP3xB33ccL1P+rRe1/PV6yl9Cj7aaxPETxpd1H1Xcdddy1XVXVPWN3uCjwEEXHTMHgZf//uUZOqAA5QJwg18IABIwUhxrwgAEUgvChn1gAJLKeEHPoAA//oFVTAGwkMz8hwYMLljwTc5LBszXpiUMb1D4DCwwZ4x9EB1MFxFoTCXgJsHA9oWAIzANgEkwCwCUAwA8iM7KsoWJpxW7iEZyrRBfTTppPzDTS79GhW7Q+lVd8Ct9DGLaxz2dnqJlIlbHKdj0iDCbKANppkg1lD5zWDXMMQwPswKAdTL5AUMF8LIwQgNyID2aVWSnl+OMrFOgXjvrVYzr7fZZ////2fo/7um7qAAlVc0YfwShs55FGY7IlBnbqvoYY4ZrmGqA3xjAaJ/OIZktJZuADxhORalBbAxCBGBLfKWycfh69PIZxnZ6Wepyp/H/VX2bE3+xCv997LPQgwIEMz1rszdRNDQxSmNMQr0xpQOzBhDNMjUA4zeDVBM3NLGHyIqD8M5kYn7JTHKGMwWgL5Me/XyzOhR8c1TiXPM4JT8DDkg2EwHikTYHC5MQ4BEzgANTak0SZDFwg5U//uUZKwI00IMQgd9IABAQUhw7wgACgQnFY9/oEC7BKJF32QUdGgqBZ5ugDisUxR3e2iXGuvU2+pd/apP6bWURs50V8dK9mZ1C3SplRxaiAC4HZiVJFmT0+iaqjZ5uOiEGIOJkYDuOdLg8Y1AJhzZKRYQuUMEby+pZ307U+iYIder9v/8z/////Vs6TA7QnsyzR6HMr0gSTS8cZg2d5MlMezBRzGvUzNmMD0wok1DMsDdPVEzigUw8tEDUiPAkndKRHRMKhcJB5ItQQVcydUhZ4AalUbdlZeT7Bb0s3/LqvSyZTzWe2hDU/3RjDB0HDZrnDB2huNW1143LhdBJcEwAiLzIBAuNGcp6A0Yto3ItjLP+PG1M31QlHyliv+O/q+zr/6f2/0/s6YAABl6qkwNcEkMWMNfDFI0rgwuBaJM4BEcDBpQgogvk4nDAwzuAw5Fs2lMaipeHMEv5X1H8kptP1+hCBzHXM0f+62zq+tf/pTVp/oXwBg2OhlH1xn5mxml//uUZMYM0tIJQwv+2RA8wUhxe7ogC+wnCA/7ZED1hKHF32QQM1uazpVJhvh1mAGQ2Yh4DQBpkRozDEoWOeTE+OD5hjKlLmETWy//6v9n/////+sQq6u4wHkJEMShPjjIKTCMxpplENBmBQjFkgJsk2s0rEQy5rY3HDky9YnEAkMccuzaK34ZvVbHubpeWlw/mh6lJpYKa9kvFAohaS83N36I3XQpBAzWysc48jXvdQ6cSAzcaTTZg0BZoCsp4xNZx79hzpOBo6ExgaoYJAYq0p8FgooFkrOJ3Buv3Il21izXa/9mrd+/S++P/+j0f+z7kdMCDFoyEM0B1YfOyCO3zWy9s84b081MpVI5TE5Sb0wEsE4MH9BLDE2wDwLA0JgEYDCYCKAomAggRhgDgAMgMjrVBKbgCrJZpIIDBAHK2MIQ2E0GTtZLsky9y0XEI9bElDQHuStR9wY6Ghs6s4JmTFD5oaKXIQMYkLta0Pl0ABoSZh2INmRbBAZpjWJkJtMm//uUZNoIgl8JxUv90RA7IPiBd9oEC9QnEy/3REEBBKMp3IjQCSCuYY5WpiVBYGDkAeYHgEBgPALjwGhaQIANpafVgZaWHV10gH+rfs1f/3aev//3dns//QaSEGvmxvs6hkDLpoZh4DkGH3pO5+th2YYkIIvmFIgYJgfZAiYG+AyGCxAgJgJoKYYEYAsqOGBXgBjfGBlACTvkaoy8bj2rCc61DiQoMBXBK96sm0wtbr0nmi3FLGvehr7HMqElTFtUdMlyLEJuS/E74gUCMu9Fb//9Jf//pMEUBXzIiydIxs1JHMiSJUzGQEeUzCYvCMUOBCTAigOcxKcL/MDIAXjCVQ8IwBECuCADNoiAZvAgAFX+dQXHgy7vHyOJRjKs3hrp6R+rnml+9Rw3WG5aIeepe6tJ5ZKTmFIFY3rqJbu7iK441VGSahO+Y675euutBnY3eXX2TiKqVNmUkcTAgRJtAIkLg4wuf//yh///iJUy/CHgORsfqza91WMxyU+ANMIk//uUZPUAA9wKwY19IABG4ThxrwgAD6wvDBn2AAJaKmDDPoAAlT7LVtkxsIQHMDmBrTIpRgsw4AGOMPRBfDCigFIwGsBvT6MAkAalymAdAOokABhtUGBTIB0XDCy4bBEXMknABUAxw9QViRo5CQqBrbCbUn0PQKuQtxNZYFHhas6HySWZODblHgyfYIzQ44kPyp02otSYf//pE3//JAACAAAGM2FGdKURZlVDyHtCiYZgHXB6Y0lmvkCsYAoi5luh6mE0GsY3JdQ0OejpDhgSgCtdMCwBBXYboR4Y6riWs5rjm5Whk3cI7T3EVP3Uz1PXLRDLKLD3pS/cXMwjTTL1jJj55jmXa7425Sfa2nu7uI+blop4S+iFD7RIyLxMKE0gu0UhZhL//nj3/+gBj+YBECSGC1KmRt+AvgaqE+8GMfsuhiIYZYYTWEPmKKAWZg7QliYRMA6GBbAHRgJQAuYA4AfmAigIyIy6pt0RAOhqHsUbF++pbHq9YcWKGPRWtKTt//uUZLeABF8MwoZ9AACPSYhFzyAADHQnDD3xAADUBOJXvBAEplbCWxHT75fUzsPdWvdTqAgABgdhkGQOAkYaaFBndLMGieYAYxosRhpgVmOMAUYIwA5gfgLGAcAgX5Y6HAIxmz2uvuWtACABhDgTiZhSSrmOqrUBuDLrWZfIqpmMkgfpg4k/m0sGsYqQ8ok6icrpGRCN0PUEYJfqGJacKrIyvfY8lUbHIb1afT2i1rOLWWNIIb6em5q9qpLatzaq1bd5fACQD8wKkhTE/dNMwVqE1viHzJIBAMOhNBZyCmg2kafTawUNBwMWMX03KImRv7ltVcxgBgLqYOciLmDDpIZouRpEZTySAGEZgrgCEg9qAQy4ZA2WCEAMQQlQxv270prQVfxnU6dPXt06/oXOavcrX7ftWj/9en/kHliAUDnahsZdSEhmPMXGueDEYOIeZhwThy4A6oAcKwcADN3bX1R9+gbVX+y3u////+n///v1/+kwU0AOMwEHsDGdGIA5//uUZIuI0tQJwqv+2RAuQPiBe7kgCWApFM/3BEDZBKIFz3QIetR7NlMNGzIKAX4w9h+jZgAkMPZRIw6geTNcyBQLLrmcRoTAeET0B027UYwOCrvfRRNaO3fYmS+9VZZ2l/T3nfXv+7VV7ofNpN8AYGINBhGIaGjKbMZ1Ky5qjEmGN6AoYWpMccAWKvwxcF5gsDigsB083bm1zj1LQ9tvv1f/1//+7SDEAmCqhhRgsi/kaE+RFG+nF/RqkYmsYKSFxmCAYMaLwP5gFHgGNOCCas6AkaV57gEJt8ek4OaW2U0aGsnDecZ++s45VzH7v27+z6qq/19td70IMRC4/GZjOCeTNYBYY08UfTCqEEMgcY/8JTBRdAwIAIBIhG1wunTfyhQssk5cKwiuE//+jqUweYBsM0hBMzW7nd05+8sHNpmH0DGXQEYwoUxDSUD7MKcnQzcQFDQOgDWYgGDDU1nU3yUS3RimfivaE38ijdUtmUtvf++rUfsO1I+lDaxncux1//uUZLSM0rUKQoP+4RA4wPiBe7ogCkAlDk/7REDUhKHFz3AIe/j4uSAAGCIEqYXZxZmMLvGxSgyZfCQRjkBOmOvEcuF5lXgtQGTEU0fXdehpX00Pc44n//////////1CijJgwQcOYUesCmQvLfxt3wbMaJSY5mC+BhBk92xxQRZlm75ziJBmXoLXGCCnjKL3qYwY90UdXelbll7Fsdpu3baW2IbjPwjNqaz8J1WuUBSyd4YGMxf51u0iCmMgYgM2KRoTfcPwMNlQMxgwuTDeSjKILWah+omONV6HiYRnyYUWZrcd+aEjxA3/07//3f+Q/s//9v61MCcC0DNzEJM5d1StOUxNaDWAmNgynoCOMMCHFjA2wzMwfsVxMIDATTAPABwwEkAmC4BeYD4ANIWMAaZbEzAYOj7BhSKj1BCViiHMXS4ercuccNdWRUhj8VDLyy2n6n8LtV6vkxikrWsag9mVVIaowrBRjEsR2M35x85jUYTNbXMMMsgAxFC3DDVC//uUZNcM8q0KQoP+2RA3wPiCe5kgCzQlDk/3REDyBKHB7uSCpMD0DwwNADxgAESBHXWWvxw5WHj7/anTq3Uao2jRRr/7+/t/T/+5Pp+7WYEvAPnCsIt5gV9s+dsAlXG2jcdBqUNOKYVqLhmIAA/BlMAiYYjkBFmO4C3piwgH+YCWBCqLmA1ARQYAFmAWAS6OBZQM0DGzXKU0YfR9rXqdxvibh1X1VbLY61dsK1T+O5bbpiqmbuT0TEPpszw+Irmf6n/4a72c8xue25m99XTvuJfK3Vbt0y1e99S96LlAyVWGIeSLCIFAVOf/6B3//NGFohQBjxA4CYyYUBGTKEghhw6XgaRcLymJwgRZgJIIYYT4F+GBUgTJhyQQ6YBOA0mACgDjgmAdADLLzARQAB3SVqsYVRFrospK8fNK8UvxmPea7zG//szf3itaJf5mYX5ldmS7ZrJ7GM+I26uzS3z3u67NW5Xd5106l8jt35bfa3XjctrztrN57aqvle/ncspn//uUZPMAA3YKQgV9IABF4UhgrwwAFAFPBhn1gAKBLaDDPmAAyLZQwQhMDhL//gZn/+kmATU1VGIKaAb98UhsdfKH8PEAYAk/hoqncGTyZ8ZDYUJhODOmNUCKYPAUZgFAOokgIIZg0/GZ0ami8Y1Yxi0ex/7fTze9vT6f9TGye3d//+8wMwuDIbI/ND9CE1ER0TOzHSML4NkwFRkDGlAjMBcGIOAXHANSgAZnSanN8qjHP////+3///////ZQYI0GEmLCudZljbfKcRgzRmeJJXZjswR8YBp3hqBgQGDANEZkgOJ27QZ+AAc9TJM38mqW+jQpFXt4u11MsYgsu5euz+16V6TNVXYY+n2Ldd6Nmn0dqeCEKzR6tzM+J1NVYhM1UAgTGhAHMrxoF/cIFQsZ2XItUDO7+tTH51iTPiq//////Rp+1dVmZmMBTA/TF6ij0x4AGTNM5RUjMAyNAkCHTE5xD4cGjDCkjP4RzDUkYGBJDtt/KuVu+5Pc2ldz6fTs//uUZKaI0oAJxTd4QAA54TiA7wgACqQpCg/7REDMhKIF33AIGf2WWU9vdZdo1CX/Xb9q/RqMCcDYxUkBDNdcuNjc9Q13SgTAZEOApEBqJGRekVsZqprOSgOkeuVuo7Nv3///Wn+r3f/////330GD/hehkPDY4aeO21G4sQB5rbg5qYoaAoGOuGUa6IgRhgpUGL2FeZghAIDVeGjyWcItthpK0P3qJHHop7qUWx7dG/Tnr+/oTT1f/fiiWeqv2MVQgwNIA0nuYxd22TagTjNOo7gyVggQaA+JKLmBus6MQFbhXHgFvuNc87Z7SSWf3dVf+9S9VX/9VKowMARQM6ha7DKUq7U3snKKOMTHlTCJRFAwJQIHMkrBSjA/RM4xGsApNd1UxcHDBg3M7GpeL7VZBFdYvVjMPDM+kWN55VMdDg5rJ+NppW1b6RsUaRXTLtHi0WUlhA0ppAOIaBTrjTyzVT87BtOskpjWzKWSwuABBNdV0YsHZvfdnoyyH++cG7s5//uUZMsM0loJxJP9wSA4YThwe7ogCkgnCg/7ZEDdBKHF32gQmZYygUhwVYxUnJjAKqvx3llz0lqXqO0eLso/2PTXZ/6mdttH/e+p32/9axGFyCHJlk7KybdciYGaIUzJsqxogZBkCcmD0bGbNIdBhqiVmdSCsehLmCCAWFDYjttIPljdlGAu24GkKFBDAjGBdACGMSmhilyxwSKRJyx+6nbKXV2B2t5Uv14j0WcXILRQ9dSblugYCIMpisG1mmOlga+y75heJRGM2E4ZTwp/8PBeEFum82VhRFC6j/lRzyQVTVrMkk7Hrveru9UZ6LrP93/r/t//9NUBzBFQoUybxYFNiTPFDE/ZuY0Vc+DMQ1CTzC/P9NIcJAwdT+TH/ByMnKwE8DQGDxtEukuyqTjh6mS18XS3Qm5mrYmpUxeMtlW1No9PMXOxRKhXh9pwU73rLZ40ZZEZxh02YrkgbhTAYwsF5p4uaGTotOZAwbRgAESmOSBgFqg2UWbGtJSNCTm///uUZPINg6MYQQP8GjA+oPi5c7kEDLwlCC/7ZEEJBKHF7mSAuAftASVOcu//fTR0b5pbtK1bop2UXp//9l/o5L2sD/5hGYbkZNslJGKeuyphp0/CZHslMGJ8gthh7pEmaKBWYMyVZhzA8HCXY9EAwfMYQHRjUzAYYiQeHwKVWXG46HD4xzWknz25q3vTHqaeQMFlJsJERa3UK3mz08YhClBly3kd5JcetCiYxoeGdTDAoA4MU1Nw00GWDLoe/NF9QwxEBBTCqSzQ8IgBoL0mKZkzhiSessx7dypYo65t4FKjxcolg77WC8z0Iqrqp/9S7aa7NPXad/1qMEwTYJaMlzdgDUmIB4wGCyAMT4W3TF8gzQw5F/TITAHMSY/UzGAXBySNeAjBA44AzVgl9JEZ3fPzqbARGLTCGR7oLhEnmKCOZUNbTUHhiN7qybmuY6lm5K25kVR4QQenaUKKEpkg8tPT9AAAAAsUbbbTBgFmU6Am55bGSvUHSC5maYGmF4WY//uUZPII0wcJwov+2RBFIShgd9kEDaQlCq/7ZEElBOGF7uiAIBQhsLHHDIs22+44lSpngeHTLe+/i6//1L/b3bCOn/+v2dH6TCCgvkzJtZDM7/JEzJxcHc0F9fyMQYD9zDkZbMSYIExcAGDQaA5MxwQGcmTih0paXqa7LmkWMv/PC9mNIrAlU4JniZDa5yFCFzCzHyaTihCefGGDp2/GFC5Mkw80UMYVspJPaxc6YICh3FjyWuCtz9SAOvurwsBCYHhSpi2HymCEiCerjQY4kEYAJWZBAUQgK6oFow0rLLyzj7btE61J/WM9KNrrt9/q//+zv9qXf/qx9NVpqqMAGB9TEVFN0yDBKlMr2bDjOBCncxA0CnMo+dOWAFMYKQNmBbMenAFAg0HKhNQ2b8eoxaUS9K3m3SJwHz5JCKbHNpbPgdZhxEyk8fXaNDITtebkBlyHCxQSwZkVnUuE9mbFjgukXkVIfYt54mA+Mc4jowC4CjBtg8NpIWoHKamGUBm5//uUZPGMg1EMwgv+2RBBIPjNd5ggDig1Bg/7ZEECg+Ll7uBIwOBWkJ2NrYodbEpre780URcTYMWsXs6GvPKOfc3u6Lau3kOr/+07f6vb7FWtCmqu/MC1AgDFhB/YxT0+cMr3QuTLNw4IwX0GeMNovOJgiMC5AMLQ5CVg7lgAHpdS7lN5NncfWZd3Yld7iX3Xm5Ydck3t8XV697pN95QaOpQvkrNrmKel6DUMCsIAKn/GguZuCG9zUdKDMNsNUwMVM4AAIYFEIAI1oHqvmTCoyIfJkrWd+kuxXX7f+3X6v319t37P0f6FAAAJqerMDlCiDCMUV4yb0uPNK4TkjO2gU4xTQCJMHnnA+EGSs0GzYVnEnBikLhjcj36s9lNhz1k9ikRl4Ui5qXaARjwxO3tLlGHhxlzDLXUzb67WNsqcpdg98a90ZbO7x5BndPHRWVUOBdMZcOU0gjaTEuWcPTtqNhw0CpZk34gSGGGDGEmCxFdlvL7oy6YPWeY7Vb2ZVq0t//uUZPCIw2wJw5P92RBEgShge7kgCwAnFS/3JED2A+HF7uCIalNv9/67vs9HuYJ9V3WS7jDkApk0dk3ZOdASfzuaZr83U0vvMYPD7TBrAmcyBAB4MFlBVjEbwHA5PcMwBjCTU0FoYXBGa+AEXmpcQeEpAy6nK3ihuSCer3ayzbf5nIZirnFft2zP2/X0/4hr/5647Zst8av2e9wc7+d5WKv7XzKq7W8ndbB0gTFVTRgNA6iIskw41BjJ7RvNUfHM3i2MKAxBXEABsa4Asld/GC2uuuc0eq27DjvY/md6UqQlte5936Ni//T1blY127aYAE0tVGBRBmphXi0CZOOuXG17KBx8DXWGg6JsYYoBBqQAxGHOVCZBYHQ/lAzMoEC81bsDTkav6w3ks5yTHTblnSPpf8rX1aM77pxwB+dE0z1v1OjWz3tHaguK3Tv7wBjlra2Nwe5lv+Ihi138l+nTTretsxlE44QSc04WRzV6YgMYVZQxrhAzA4DcMo4B8yyI//uUZPqIgywJxEv90RBDgSiCe7oSDuxDBA/saIkShCJl7uRIjAJ6MTa8NB5F+phpsRrS7S6ur1s29Wi5nd/2D031fFvu/7f6RgMJnB8jNAgJw1U14lOBbUETL3U+0xBYMcMNAi42AgRjA3TLMDEJoK/wc3GEBxq5mza1YgO2EnMh1rHgJpTHlQEagoLBRBoqRKlmkxdZKM4+lrlPzGxbhdJliugVUjsvFEwum+hjBNehQ9h4M1TMVAPjAFMSMzoBg0Nz4ToK/TTUSxCbxsEAII0KxBDYqObgDObooMCiUOf35X72eXT4q3/01//9n/mr/fQqMC3EBjCB4po1qQ4qOQ6OQjBcmB4xcoPVMEuEnjFrgG0wRYSNMLQAkDq8xrMYUmd2ImvMTMAhSG4hY+57fKbEIsbGOaVRJNQ5CjKn7wRIrEc2UM2rQ+FWn2NMnSiVFUJi4bwFcis6LKHHAWj05Bwog0TwuQBiQGnpeEZELLBshKaGpuZsYm4bQE2h8wIi//uUZPYIg5QKQzP+0KJBwShgd9oEDXAnCC/7ZED5g+JZ7uRIIxCwLEgWNB91EKb2saGu5qWXP4ulgVb/3f/MK+z1xvbWhNzf/T1L/3iEYPwCTmamjoRjjLIAcvmKamZlKpJjIYLqYEuJ+ERIYanNOT6IYQIAY0gKrCPJwsyLzkcClnMgaVK3MGbctV6uuP0bMf7Xz/j2p71C+t/7uu058v6l/e+UXfM+fbtBlwtL0A5ite0nS9O6ae70rK0fT260ZbjjaJLFAOYbzJg/TZwCvJwMVYKXgEUAKrSqBJZJaMehhA9wq6vZqTRzCbOWW5a7j3jFf7P9G9n6Lv//PVUwRQNVMLjiWTFLJL857QanNR/REjEmwswwAlljKOBDMVwyQzDAYz1Qk0kLFCAy1NUGdmJPVfGjmhseWc06IBhNBZU4P6LBrjR2Glw6mEy5xLVOe+cLAFAttbvXFzCGsgYkhi8IMa6m+JndKjIBKpkklBszwQs3+ro+ojo/tCwxBJkw//uUZPSMg5USQYP6QiBDAShhc9wCDmBJBi/0Zsj4A6O1zuwMAWQwmBGGxdwtMmFE4Cs4JiulDizWakFkHq6Bf07EV//f+v/8jYn20fzYBFTrJgSYGiY5oKdmHhHUhsG5A4Z2YPAmJdAXhiVnQBCcxmt4xyEM5CCetu55mPnO369oN3EFmWUqAYOunGrTs/S+oM5EPii1HdW6USzC3TKNPopNOvCWKOzLZGhD09SQCOm2jBsNDJKYDtGEDy1OTwZUAM9RiWSGSwWFelBkcSY9E0Gi9J832twsQfZQtdX6ew59dXRhL/Z791id/9vrMJ7EejHCInY3AUuyO2mUzTd/xpAwVcP/MEdkwywQyzA/UoMmEGs8lCHqICgJzwYTANrbdaLP7w4uvbQLk/i3D3K+aipWb/vdfteO6/moPt//+XDNdou5ptNG//V914bd40Ha/6sYe5oR8ml/PV4/fPtstjAw6Oq5gymFxzU4B+PndNMhi6BDCGkYIlQRUrCgExoE//uUZO8Ig18JwYP+2RA/wSiWdwI0C/wnDy/3JED+BCKl3mCAoEXve193Vy73MVz6pgs1n28UtR0f/pO/2/3pN/9kcgXzBMAagyrcrINMKYVTfMUlw1VYbEMU7AMTDlQ4M28NcwpR2CJXY0atFocUCgKau5/0lnCsM44w/BFa4v6vESPj+dfqLwk/uLedZHy/+Z05XiAbtCB/Ju3eIHP1JlHc93ccre8f88i22//zm9XM8prWtoAYJQQ5iNmPmaUt8bUYhhlcIdGNEEyYOvB8ENhS8jMEHI0VE1ht63je60SrVL5I6MGCmX3f3I30/ov/b/d9f6G/9CoAJKKqYOIGpmKWpn5lNipUatckqmeJlu5gkAXYYmS8fqiWZauOcXh0ZggcEYYAGdUpAco3BQfWUUHFtLIOPYFTVxfUgiTJVGEoVLXKY9MYTYL4aa6JHc5qF5XQAKUSL3LcWkx1RaiXs2gCKVpjC4JN/b8zRw+zR4E9MT6vMvxJMKSgFqHaeJBD//uUZPeMw44LQQP+2RJAgThxc90ADjQpCC/7ZEkJBSGF7mSAEmLZtQJqjqqN8beZrbSxvq3UOR+wj+OSvsT+r7vo0N7kbruuowQ0BzMauKcTO2x5EzSFA4MfKKNDDygX8wdY48pHsxbmUyhCoLEmDgCVRMLgIZLKr1OK3+r6vxBBY0BNawKGo42KMeLvQBBqSVb3BiksXENJgVccEzjqXq7hz4bQqlhN7SjHorWRoEyNAUaggDAhBoMN4qAybEZzXfKKOL+4NSSAMGxwFwTJdAi0C801Iy+fUHRiUB4daiMqAECiqP091Tada9/+593ur9RQZQS/b/9ReiWJYxxi8jPCxANwI1s7Vu+zLHq5M8UokxbQPzM/AtMDsbcMDlDAcAEAOicFgB2Xxfsyhc8vvf5koQWlqHMdqSXfNNILPPr04vpdajtTf0Gbza7v11He4U7Xx1YEGE+CNhjZB6MZH0bimVdJ75kKpeWYyKHBmEFgIZhQ4EaYBOFjkwLIYBgA//uUZPIMg0UJQrP90RBBYOiWc90ADbhPDk/0RQEXA+GF7uRIQFryUAKMAGAJ0w3HuUoaD6jS7VSKWEa6WC2PaswLjjVKkInnona0A2KvnVDReVFkEvQlTkI7E/vIlWKtrFzaI4sA/xtyyNMuPaTWbykkyvFJ4OUigYDJYRHwwwQBIMisEwTEhAcExncTlMPoBYjAAgE4MABzASAD1AIYF2AIQ6OCaGpQaEslkw8aWcXNzCkC0tebnkG7x+hNybjZrki8wVhSnolR1labUD1tQq4Yzdr5IOHXUkUujOKztT7lbgqtpsw6ghRug0refPr6RieNj7jY1Vo3RM7rNTyS6CDXnz72oQgf01dKHH1GKIaq0MRRRPsk130aqxaF0k7/////////9SDv/////////9SgAogAIpQoiaYa+XHbxr2xnzge2aviHymIZwWBxKS/4Y4uASGHNABxiUwP+YgYBPmDkBc5h3wBUYCyAgKxmBRAGKChWBNwMeeNmoIaaOMO//uURPEAAtcVw5V4QABo4ShBr4gAGgYE/hn1gALqL6AbPmAAOKwmtZ5K6yBjY52p0W7a9li0d+xXciaX4vmMcmSdJxUlHBCBAkayrC81zrskoVwst+gWQQfG0+AJA2G5gWdqyZrSpkrZoLohhzsac6TAkk1wnpkJVqdJtL3Z0vqBl761yyz9pY3//iE9//qElTItWak+9BgpMw2TADStCXI1jVQLNPvUUTB/hl4wqwAEMRmCbDCQATUxqEQaMIyA3jAmgDCKGA4gJ7MDACwGZdKhgHOPuo9HO4i53qIlHjYqIFkIpS2NFWVEIRWD4xZ4wc7kOdZhcimQ5bnJQUUWGXE3UxXY9CI5DiTi58iGMeQgiyJOqFc9iVKWejkjCkGucxXYgqdyWV2OawoxHe6ujIpF/////QQd//lRKaG0l51oAwGCansaYo0JiMJcGXWcWY1RCxg+gBmZ8J2YGYM5legekgQAOAJgYu1DjD70DNAdpq5QPXV1FCFVziFhrNmE//uUZHeABVVwQQZ8oACHqygwzwwADgApCl3xgAEEhGILuhAA0+VzB+ovIpBmWEMbDQvSOe9Feu5HEi/mlT4pwjBiJ7J+RnKnNselNJM2NxXxf5FLGpQ2N/4DNgwYPEwFR//x7//8usVVlMGCD2TGqjr8yGQFTMmxW7DM0SewwlsBXMCFCBzDBQPggCUjA4gHshAOTAWABUuoYAcALu6/tjIU4dM1h6hRsDBafecfM7Awavic2sMSOxQfLN203uBxxEVlGMJIAQteF1ElFZB2cvqbERwVysWZVIDAcDDLRtDfvpjf3iDudQTBwZgsdZmeIQkJ6C7oiwCsRi3a2RFDqiWmJRA+2tm6v1+LL7r/+jFF3ImWX////WptTBuQf4yMBKMNLPWpjHmoBM01AYlMRZAYjBQxB8wj0EkMmesNHyHMUB3MQgLL9ExpItQLIoJQPKAThAqkX6Qo82NogbH1aeXCY3Lkf7feGfOpy/lSI/2uZn6+TeTfMydvNvlpG7V8//uUZDOMg/dcQQv9GbJIYPhle7kSELUnAA/0Z0jyA6KlvvAAr6ZLmeh3zVUY/89c71bMQl0rhLawqDskAHiDAJBUMIMlwxD0XzK2TFPDo0NZBLMAlFMawGJM3+BCJQRRsUshRp4s+VxKI5Ao5Q89d7PsrTWX49WpVHpO/1L66v3qb39KTAhw8QyCliyMOod5zHOpq42JUSJME1DDjAdhU8wTUEiMqcIG3CMCyQAxgAAGTD4NnRmpudotWJ+tc4Al0iIq8q01JARhmhohn5itaRxS2emeS7i3pbdJUBZ/tPIzNDzhmTX64iniV+9OXZjrmVwmxkrg0tcshZBfD5fsGvB0EXL4udKM3TAe1VTZhwec/TnARqGsdTHEa+ZpFo4sQUEBQFqQUUU9aoXLiqyWmpGK3yTbvUyn9Po1d3/q+d1f9CEdNCEVACABg/wSqZFOYAmnMFuxmaLeeaFQP6GKBAepmd6JnwThiGY5xuIhgkQIcNhYAIw2B9mlHqCqYzML//uUZByIw70ZwSv9GUI6gOiCd5sQDslhBi/0RQkBBKGJvuAQwTnOg6cvhXOA485CGEqI8Pzqkbq//WsxpNn103+8l1e8jy8V27nu/0QC+GP+bHd33tTx9Dne+RRCd/ZlfvzhKsoVApMYG5N54ANGMxMmeo1eOgrsm6ACNpEGt3VDuBnC10W6aEE7NF6NPfrvr9WrfiDlKfq/dr/+pkymAfg95iaSB6ZtqR/GZoLSxlSZIiYRSCemHd5nDQiGBstmlIfGKAxEQUsuFiLl9Bh0VdAylYjugsHYZ9XRr3Td09yKkzIxEXd2Z22t3cjsf0Z3Vn/NlO/nIjR2Yuh3d5Xk+sy012d/Wr1I7oV2GaQodoZzzp9CQABj5UfMpGXPrmFHuGglGmdgmGBB0gpwAYomiDFLd42mHcKzzNjVXpOBJq2QnKo21019v9f+nq20e3sT3bfsSmWMEPBYjFfBYAwxM7lMp9RcTEgiQgwsECkME3nOPApMVorMXA0IhDGgtQ4g//uUZBYMw08Swgv9GUA5oOhxd5gSDmBVBC/0ZMkJBCFBvuAQwDn9mt0NpLKU+dI+qKBF5gTE4mYYp2qlS6s8lN59BtbIu0gdTaxguTcsRkbXHnn3libqnSJ8vaXm1I3sqinIhQDDE2ADY6YDSPGzw/aMjFcwshSbYAzTNEATHZVKRGI2VvToltVdikI/1eltVX/6aPR3+lW3196UAMAmBjDDqUagyBhC+M0UStDCLC90w1IGSMdH3PJBFMpnlNegYMSA2UtRMMMgJbwWJBjMSrY8IQRgj/PyQWTMkO4G3OsbMRLds++eu/Z94D7+R7//ll5D5J7W5zZZd9431T7Vs96df9vrJ/+jp/cBano+tCSd2bm55qG83OHOi8mZYFmBIykU2kPESwQB2et9froa8aOeu+YDOxnVZvYEv6Urfezeru03EpLt56GGVr/bie+7uidMIYIEz/zRzOBGtOSmes0YW0jD0EpMYBxOBhHMZAiHolO/RC1UwZfIrOrmzJWZ//uUZBeMwtIJwovdyRA0IOhxd5gSC4ApCC90ZMCyA6HFrvAAKrofPvgmxNSqjU1vs581qc5jgKOeKNyRS4YOlWPUfSyg1XZsZzPeRY5x6rBAoB5A2ZpZN5rTL5/EoGQyIAgKLaFRRC4Qk7ZhwDzkZrErjSv7af9Xp9X//0/7O/+6tqmgIYGgjZikW3mvJOydekJBtbKpmO2B2YhgYceBqYNKAZgBGgEUbZGSgJLyjMLYMEwifS+hqzHsb5cCISSsexts+tDWKRRwMK3LUIY4CvcIidjnPU5+1taEuRJUPUwCpz/DDDGSjTh9j4g2AVlAgkCLKo2nFFJHUlIN3oCDuyoUahLt3v3f/v/2UQAzPVRMTj/UIMilXw16yuTOxFcMI0Hwwo8AeKDbZNMLjDzyxCFzCm2aaFHVZJLRer1JT1r0sVWgnXPU6NheijN2JtXprV7f2XpAAACqjjaIIKqwgajdUU1+LP6rMCBJBw1JFpsLl9fNq0K3CPQ1NrNGv6+j//uUZDoIgkoHRLOe2BAygPjtb0sTCwAhCA9zBEDGA6GBzTCI/v/3ej//06zB/EuBsSBrHq+nb4WIbPgNJk3AmisAO1BkyLADToFLvq/Y0ETnqqxV7LgiXa8/tCiHRi3pVtbc+ZZWtaK7D6kdoik7VsIvc5lrbQhQqRFXSiULdVjErQ+1HA0GNDyk3OsS43tFDPYCEO4DzRwG3gvGdViR9LP/t+m44i1fQn//b/pySbf1+ohZ+xUCDIQET2s5DeGMROl0Ks1JRkzD1C5MTR42YFzF4cNZgAEBQaBzMy71UBhh4SMpME6C8NaGXLixuKOc6q8rrE78W3v7UGbaC232abr6G2SLjkX3fJ8ADAmYDZ5h/JHV0+YziZjYPClgSgtxU80hzlBOMtYvigcqh9w1Kv9urb6v/f//6PvWQBggB1mAMyCY8bHhweDBnzVpGmotGFqADRiGFJVg4+kryYT8kyZD+6YPOeJSLzYiF5jTnmNA8OyyEUR3XOYei19Gur0T//uUZGUM0p0Hwgu+4BAxQPhxc0YkCpQlCC93AoDPA+GBrhggCndwHSkl3k+Rd7HvYw/StCo73s3DUTyRoMsw4yEIyQ0MZbAuFqcKofhB5czH/3/mXzj3OkP3+r0U+309C9nt/0+pWpUAABmlmkxGBjnEnNx7DPM1qN77cy0KiU1JNjpdV0xVq8sd+tM9uTSsJLKizKbOvd93Tkv9EUF8w+XfpZt0RRZP/6SAHQAFlsCrQddF501uAnMKqQsVidI1QpesgCdIPYGS1qy8fJCpsiyh2v071/9P/0+//9KP9/vbcwDFQKccbfIgfoU+IZWMeBRAEoPT5gr4lQTicaXsuzCDhhtoeMNHLLCWePD2mQFRbYvYt+vS/bo/M/Yy5xtOUnswxNuv2aXuqK6pi01GXXQRhg79uMdEBk2KFYczOtLXdBubvtJJZym566mfS2x/93R7pH8Z+hj/v9Xf/1IABR++utSBp1sGibUfbwBpJ8g4rgm4jOS9HpRSf6MbZLSV//uUZIwI0g4HRMud4AA2gOhhc28SCTAfDC7vRADPg6GFzbxIODGTNBsWcHK1OSKdLrxju2pDdaAMbI3dHd0+ds9nLzz9LqOBgEGtZiV+dGKHJF48skrRXKSjcUxhkeswkUagZb7+91H5D1a/J/f+77P/fo8rstp3AQYHB+Yg3ia4cSezYSe9wxnchDsgH9gQ0ErrOoeyf8GDoOAUKBYqHWNE6TIOZJAo9RpJkeeoVUy1K0nlLKkMtyaBY43SZTaLgAXWRd9gk8pi76HN9S2JeExCk2DY9isF1lKyURH5GLwPB2ypmMeyxNGhXXT67P/Z7+//+/0b+ulHVckwFBM1OfEyMpo/b7g5eXUzgAEKnwLLhluCBUlAYzPONaGQWhoDkKskMREy1crSM5C9joxSc9FMlqyjOc0jJXdz+uRTBJbRc5nmfL8PXczvmZnqcJMGxasRltIRnWrEp8hT9OP4xkvmoBp9GuxtpQkv7tAxMc9oc9cfRA8UuYiiEbw+lLbx//uUZL8IwiYHRUuZYRAxYMhxbywgCrQdCC7zQgC1AuHFrCSARCLlzd239juliXd6f/Io2dWn7PX3tpq9CuMMGRAMZLKO7zcPSuPONzEMHQjIAMMiwAMARUGgfZrBT+sVlAwE9q9xFRmXoRFOTYWKG/1yVwyUaG5mqK46XXOmSKbamTnFzgcH5Ez2m89dEiTJu0Gbsg9cmO0qCIWLzsdbCPJqYIg5NIZyDJMzNqFKx6doyEaP7WmRkzAkGxAw6A1XM80c6a0eK41ML1ybS2AQ0EiAInnhFjKZ0MJfm0LRQLHNKilzx2bKPSNPoCTHmUmCO+6wjtstZvXIbG2K83UhdSAJGsYMcguJ2n9G9BAUGZH4fqROKz3hisnoagmomLcZzn2iBMkNEjqpBbEp1PjMXa8izrHpKlM0R6ll0bbKK5E+kY5SKQ5F0PVzYpZ+UbWwpoRvEQ7QTFk1P8jK8NmfNUThQHR8HTgdxqMAhkRnMQ0OGsC1kfZSiVVvB4PEjY88//uUZPCPw3lVwIO7GTIwILhxaykSD5WI/g6YbQkSA6EBrKxIgLWBTStLExV3ek+G3htzWs778oLrFP32121dG7aLa35NowVbYh6rkorCMrKgwFCqyZk2GlVpvyQgXKkEqiv4BgSIJB9QsRn1Vxkz240LMjL8kOLC5EpKZctJW39655chnkXJTbdTlJqfKcmEbM8jNU9pprpmslP03OK5eeicntgsabX1unCUvSZbntXS5ocM9VBhZEQw+RAqDrMiyg8Co5w9cOPCSkkWFUswMgJxZKhQohVljrENzaXyLSqDwqLiB2NfcslRlbmOAipx73GhemPFiyUUWqkRlLukbdwHIfse5yvloOhFJ2EKspvv7dKz+IBjMR+YLrtxKWI3ZUj/3TWWuofqrvtmsjLMrqbjvUZDrArov/so8lVvTOr6N4478eHTOwXisAHKrCtoNEXGdYFBkIONQOFqbHMbVKtg/mF4FHc3XL32r0RdC6ViBh/1Njpq2bUm/74w3X9N//uUZO4I81hZwAOZGTI+QKhAY1gADE1NBs3gYokgg2DBhgyAd7qcX3Alf+Fa8zD+ecU4FTc4NcF+9qX/Mippe9YC+f+lAAatfRaIRNOh1UEFgqXEpgyXRrXWjJc7AZLrPXZMY66pUqtVSAjVSUqpBhXrszUTD/qs1X/bX1h0mP2bUul9I/qxVjdJm/gYVRJFON6qf69WeXVIMKKHSjbEzWH/8Y1LNhWIqUEEQWBoAo6tqqTPSZxMDBSawCb2ZtYBUBbVSh1WagJawCL9Yx+ucY71Ywo1EjCrp1q+CuIoi/iIqd/U8NfbJEZUt/1hpUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZPIMYpcZwZMvGKJWY+gBYMMsTIFs8kwwZckRFB5UkY0yVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"/*END*/; +<script id="dd-sound"> +const DD_SOUND = "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYwLjE2LjEwMAAAAAAAAAAAAAAA//uUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAEJAAGPAAADBggLDhATFhgaHR8iJScqLS8yNDY5Oz5BQ0ZJS01QUlVYWl1gYmVnaWxucXR2eXx+gIOFiIuNkJOVmJqcn6Gkp6msr7Gztri7vsDDxsjLzc/S1Nfa3N/i5Obp6+7x8/b5+/4AAAAATGF2YzYwLjMxAAAAAAAAAAAAAAAAJAXgAAAAAAABjwC+eaeUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uUZAAP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAETEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVWu1SCQSiMSBsJhIEglv5aaJeRvmMLGAx41a1odhWJUK7SbMt+F3DJARa8xHUNPMPZF7ncrmCygIIQAMGCxgOZgWgKDgbqSgGCCmBhJpgcke/AzWKwUMgGPTCBo3RAbG1oHzjLwMriEDHQgACCgKCoCQNACD/wxgBgMAgDAML7gY6FgFBIBhIUg4CAYYH4ChD/AwQAgwoKURccYe6BgYGh84bA//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEFwYWXfygTiRMGhosMBCgxBYc0PKQMXD/plwuMThoyBfKRiRExMSZOf/l83dNN2qSYsF5aLo//+pqmfQ6RsDFTnVvZ6yUWiUWCQSBwNBIJNDlpprZQCY28bHHv1syR43YqA47FzM8hpwyiUZpML1CLzCjQ+xQSVmBygEJfgwZMCDMDQBbcDhiqAwYYQMOPEDpkb4GPQmCwwAw+NwMqw4DPUVA5YOeBmcTgZEFAGEAECIbANBUDCoD+KUC0cLnxH4GHAoCQOAwEEQ7AGBhCBIN/gYMAwW8CdDVMcYb6BgoIhlsL2A3eBEBfygThsRQqGiwtCEFw1cLlDDkDE6/5uXC4xOFxkEzJZqiiYnP/UxfN3TTdqkmLBPLRdH//1NUz6HSNkTJIzOcTQAAAAACUADchlzEz4U/4OTYNYzTJgb47Osu5N5uM3TGAgacwSADhMPqBizAjgM4wqQHkMBK//uURP+AAAAAaQUAAAgAAA0goAABHI1nLbn6gBuVrea3P1ADAzzATQCAHAC4OAJkSDAIQE4SANQSQeXMKBqUnZCc7AhYSlcHxMzkAVBJAjM+zxWaWln4rYQaiqtqxU0aagv9p6/N1n8bW5S2Ydq09vn5fnW/DPtWHYzagGrzuX/+/1///dVcq1bLn1P///X//6/+/8ajQGAx5llvRhISrDRZT19f+z//r//9QAEAAAAoixpdnoGUIUyb7YZJpNjqmMsaMbEZGhh1gUGAqAUYzoL5gDAJmXKAqYAYRCOMNKKKwGA4Ae1yNTIKGnWNwNbncjOFM69wgAzAtxHceYUmC3KSRVfrU1bljHOxM75OORl/9/8P73Lss6QSIioUqtFzBERWpm+obAro3V5d63DNjWEtyTMaUct5JABmWrRWJ0187ydA6haHbdy4pnviSKYQ2VpGJogXRhqgbIYOKB1GAoBEpgqgFMYB8AUGAuAFhgIABcYBcANGAQAEQkA2mACg//uUZMiABeM9RSZ/AACNxHiWz2AAFgCZCD38gAHTkuMLvPAAAQidDBQJWAoF8J4JAhQUeRCDAYEX2JV1frGcNh7T3tp+4ZZ36t+9G5DnIJnOkitHCoxA9JKL0m1hhrtW9r9dywtKDQFNFiQ8eCJLF3iIcpSEEWFHy5lk6mBlhcBoZSGkFWWxsJJOKL/Lie35kQuCmEmGQZRQM5qCDuGHuDUYRYQBgCAdGEACcYD4JAjAtMAEBswDQBS6sArzZml6jkXAhx5EBJCDlmKkZyoMFlEWNA4VmidtfD9/XWq/ebfG9Pk4qmFZ1ikKfWtb375zl7OKnlEu3bp//7rm1Y9uv/utAEKjHbnT40oemoNQXEwjk0EtQw4sJlM+6A5zCJgFIxEgAuPnzDax05MFMuIChlKw4FEiDhcwiElcGEDRUAkFDBgIwsPS2ZsnO7qAclAVMEIXciLfPeRF1xqcaag3GxGGWJGjgInkxYePDVSjHlhodc4sehhzlpruuz9Ptpfr//uUZD8M1PJgwxP7OfBlhMiReyZMDkR7EBX3gAE+EKPOuvABNXp9GmnnWdFu251as81V2qq3W+qrdDlzGnfPbslHaUoCAxYrIzP5P9MzwK04DxGTFQDRMPoWQxZxLDBfCbKAkjCuAQONYqFoJHnVjTeFARGIj4RLJcggQGhjgKfSAmgbZfdMmuv1GOzU2QWxSt/g6Dsx4aEgCNnPu/tn9T87G4KWo0///6DJMoD42ZeYYMsiF2DYNxO0yBYfWMNvFIzBmQN0wE8FXFgfYwJ8AbMBzAHBICKMAVABB4ASLPiQASjShgCzAKR6Q4AfgGojhjAE4uzUJKcyeJqxODJ4uYcS9davWes2Jt3s1637zAw1T2rdd+xvfT///9QjUN9LVOrd1MbTdNxESJpoMnhFCgyGIgGhwNGAgBu4im4Mojk20WsVS6NWAjTdUh0qLSHp8/XcJwxWtNbeW/vekuc/P9Jfr5x87g71vBH3igAFsWuagAAAADPi1s8x9oYOMcYN//uUZAoAA0Qcx8594ABDYpq/zDwAy1xzFj33gAFLD2MLvPAAwjMKAmowFUlEMoeFwTAYwK8wAYAhMJ6A7gMAxGCwAKyeYCAFk6GXlqVYGIiaAXgUTwnoPiOdRPwQRijeBrlwQ9GuKxWbNdTbg7tTD7WcyWrWA/fhUAARKHd4h3eIj/gAAAAAAALim21fdOmflFrPq652SgZ7TYTI31cDQVpYQE8egxyDQz3ai3xE6dR9buqb3i2kO1lo74RwGFfjjRpPDlCZVCAoGipg8xioAiSYbUFfGB5gRBgGIDUNAPo8ABGARADIYAFoKsKXvZTWWBGSjENUqQQgOIv6ScFUe5M2xsi6+vjNv/vO953jdr4vHEYqiy68mQAMGQyQ1DVRjJCBoMF8MkwJAAjHDBoCgSxhtgPGAqCUxQwDAA2aMIrv9C1DUJAhhNidnO1kWj1IXy6uevELlrCxTWNXz/JnWcf/FNzSVkxJHJoJFWMc3H8TYokfoz7sFwNHKJ2zBhQS//uUZAsNkwQbxRP7WXBHo0kKd2wWCyB9EjX3gAFTD6JKvPAAIxGIG3NU2jW342ISAggXTIARyn25QK/RoaWtyBJO0hTZebdIactxYkBEKjV0LOnfM+yXtNLtrHmwZAIwRH//ev0f/9vT/o6rdv6gAIAG0YAANDuCOM4nNrgdM7lSO0YTYkcw45K0RC1mg0GSEhuxnxVKSw4NmTAJWFjswrWt/p8tUY1decs+x3v7ADALgsk2HPWYxJNLZMLHjdDGVhX4zts2aMTaFsDCiwbswMkDrMCbBMwoAslQBLKAJMqgA7rN2bs1BA19gihrLxVlaRwsaNSqsJwnDPTCxB1Pq3zvf9NfdN4t853W3+Y6NQMGkyKYZuEGZjdg4GGyzuYAgm5jLAdGD2BiYcAGw4A0BgODAHAGTHZu7cpdlMVkEsqpNIXAXMhJ/TLs60NLkayXe6zuH8Rd/5vu0Hf1SkkN5e+KxNkAAAAIJEggxR1R9NCYCDjW+igoyW4DmNvVNajD//uUZA4AAwsURiZ/AABQoojozrwADJhRFLn3gAEgC+f3NPACrREcwSkCGMAPABzCHAOARACBgaQDgFAA+DFZ4w0h6lmGkAyKPxMHHe2HU6mdrmZ0w6CVwt5P3s5/nbw0HUjcGdilan79YAB0vrNAAB7pUR92rRz/XJpcDx79bZ1MVBjcN5EFxgCCgOJIGgaDgWXG19SQUAtpLD0seAjCuFjjltL42EKHEbb9D1Vq2HmqxFKYcOFoocfpAAoAAANV+R+TR1RYczCJpyMtrD7DT+aBUxcwYyAQTMYACAIGFTg8o8A2GBOgMwOAD3Cfp/GctXYEIGLmL5fD/Ow7iqPIiTCUpQHICv3SFbXcxzwE2uLvbWOrR///7Kf8vsiv3aQAi7bbdrbLKAAAAAAADd1AFQMvFAgE3HA0JVAWxZIjhFfSRVy8ieLIoekQpQyiVFykLopiVhgzoeTp2no8/3Pfd//nXi4oLE0JAAMZEB5TNHDmQ2HsmEMbQLrzD5QaIwGs//uUZAyN0vYcRBd94ABPAwii7zwASpxhEE/phUFEDiHF7aC4J3MEoAvTBAgF0wE8B8DgJwwAEAgAQB8Xqr00voU0AOgp65iD1qZCk+5IQ5K+WRmj7xavzfP/peud63NjwA64+5//6///oLgADEVEtMvUdY0nhxTBGI6MLUOkwcRUDCfBuMG0CoSCSMAcAIMAJUiuiX4v68svJGIAfRMVSXg4Vy3uTeyrt7Zs3jGtfH+6bhafw0uOHjykMGrQ7DLUb3QxHIYGMpHNpjAbALwwkcAhOXePPRMkWN0HboPGnRqxN3loRBzVlLpNxIEMGQdJAtbPDeBb/baLLuWpPxawfKngmTd9f////////99bDJUKdMku78ygiETFyINMMcDQynA/zOAw80lHhMWHgMbA4ZVxEJe8iwagjgr0jczDclhTXn+hyklILQA0juJ7S74ta/h1nMHtv///+r0I97dKKUlTDnC1A0XEFFNM8BoTFYDSM5FCTQFeNIBEFVJnZjoC//uUZBCAgq8VRJP8SOBSwohge48WCXhDJ69lgqE9DiKZ7aRwGDAGW0mJ2HaR737XU772MCUNhxQTIBUdYK01lMV0n4UDQLtwszpz5T0fmr/R/1Nb6P7v99ZhwMRG0Yz2a/IgpitrAGrjyeTrZnMgmyRQYgFYNHLgFATBzsRyFvciGC8Hwu2sZJ+ptYui2JnOQ8VBW8t9+V14vA4oo5WcPWJ+Mt9Hd/o/v7prQAAQ3IHdpG5RQdYw7rIzCSFXMVM7p9z8JICUimtpJhePe9i50lMnzaY/PL061OaXaIUvAMpANQMmTYcIO/b91+n6V9v9H1xf8Vv/qSg6pZMXMXg0fIzjHRDmMZBCMDNZ+poa2gmcgQGO06E10HWnchhyY9DbLKslXOBcjGE4rFzgmO3tMyz1/nr5u/+ePH1+yzs93+z+jvYj/699MYtENjepjRk27UGvMcmSmQH0zSYSOTl8y8cjD5MVAVUKC2t3YYdGIMQmlD2vuOytoDk1oW88SjXw//uUZByNws0RQwP8yJBLopiCe4wWCkhFDC/zQkE5CaIJ7jBYd9UF0iZljFKLFg6ZkB66k2atP+tfpt/r5LZ9lfa26K9RISqYUAE5j3v+GtcEQYYqtBqhBnJp4NAAIQ4YPEai71gwWNtrQkXDUFz5MaKi9TqBHC2+stldznXWmrp/3bO65/7/kU79/2f+l//6yGHvhSBmnHbAZEKQFGAaIShzOWGbiiZTdRnI+mGhAbKCkRhgqa2U4wiYWw7rOH7ljvMttXJVIqr7v/SVK5UffOCKhYodK/i/6n93f/2/+6z3/vWVVjBxPBMpu9E0TBlTFzRFM0HY6ApRIVmFwK47hFvYqgsstpMEszPRWIpdiaWsZGnpWLLQWWufHkVNeJV2t71/6FUVbv9t9f+rd2bX66k9AAMYpME9D8nD+aEqMsma8xSALgYBaA3hgk5niBkASa77Oddo4AmYJAxqwMwabQZ9jcX/JVsmg442DZhaxav///////awBgwqrmqxgea4//uUZCeN8ikUxRPaSTBKwohxe4wWCJRDEE/tIsESCGHB7TBYoTJljGMHb1ke9VRlMPGTQMVAY+yxkf0mFxsKwBwAw+8fTVBFSRGuOy0cpqLJy9JgeD5ITX0bMUTU//d/7P//b9BSGEmFjpkHte2ZHoKnmDvGHAUqTS7oy4jMCKCIeRuLYCQcSxLxOqA4mtZxVASNwWeTSe8oHEATwdIiayz///////f//oMM8Qs0SL2DXHFSMvEIU+gEe2GRTlaIxxkDIQEDjwj9roPDsdxhHYKTiiutXL8lbaDSm3sj1NW5Pup/7/X6vuZ0//7/6aAMGtGpzSEGOU1ZkNZMOzLwTnJBMBpg2Zc6Qs1JkxI0wwhl7l0+NR/n+erOYrvTTxeavdqWb17C8O7HMQu32e5ADD+BrNc3bA0yBsjNOIvPOGj8Ik3IkGi0v+gYLGawpRhXoaGNZmWjpMPRMNi7Rzj5HzEeqhG2j0b/93+zc7q/6v+z/14MIpFrDIDLewzLsZOM//uUZEkNwgARQ4v80IBEojhxe2wWCKRDDi/xgsETCCIJ7ZhYgYJyjtMSBKtM8mYABMFFoIAKQjNQZXw4WD2cF/bvL1xysY7bKnTbWw9s2nP9v0erV/+7////V6hRWExAheTYDhZNdgLUyuRQDCAcNrhwtQ+KgAQA7kLswuD1CEzxIwLJRjMXOgoZeQVoK7xf0/yX/3WR2+/Sp61f/u6v/oUoVAMKKFxjUMXVY0+oOMMlzJyzBigEoOA+jhPjBnzLLjDGkV3UUuAigBRIqVciQGUSBZORtcVfa2Fwqgm92judHO1tJfzlv/3fr//9/rGAADCWBYMkzIczCRmzIiEdEUM2c4zKsHCB0KoA3z2jkZqmyUbiY0wdnmedLIHQQ0bgY2cIf9v/p///RUyzGFrA0plRsA+ZaAIfmKbDWhj1KYRRGUF5MHxhrAkAr+Et2lxGU8SbduXrOjCARIGjjhTpvsp9a9P/Ff9sV/bV3/9X/SQxXTqjZEaeNuEOAxdxujeC//uUZG+M0mwQQ5P6SSA7IiiCe0kWCLg/Ek/tIsDwCGHF7SRYjeaxKWZIIm4YIOg85KHONTQnRWfIfAS1sDckCUG+g6Zo//3f//////lf/Wp/zBqQcIzt5zFM+PDtTGFxKs8IUCKjHszcDjNnTHjRIJIg2mlkiRiwMvgii7FBm7s2xzw5a2pmRoPW3/+n9Ov7fouV6PVm8AMFo2ow+epjHoGcMDwnM1EVMolhEAkxwTAIKEVYpnU4F8DIW860qQROb00w3rHMHoV4t2f9v/zPp9Pd/3fSu4WvMJ9DgDJOFkQyX0LyMW8B1jvVwxgzNDHDHgFV76v00kNMIDAvIjFDNyMILsWbF/f/s/7Prs7f1fFH//Qmf1b1EMOIpg1+UHzd6B9MF0rQ2iKNMjQFAiUQCARXKok+h5WYlZXEKjdjolmzy0kKDH+pDzptn/VytH/9/+7o/i/RoVhj39kAAK3HFGmzAkAIoxyFKWMgyCVjE5wdswLYAwaCauGspkEBAMvf//uUZJgI0jINxAv6MLA+ohhxe2kWCEg/Fs/sYsEPCGGF7aRYx++7sphEOClHJG3zBSec38d9ej1qT/Tv//03/7HIs0ABWBVTAfDFMkzKE2zhTV/hMGicMyYcETEgpSrmVyU7eTkx8DVyKTyENBk5OTeEmrLk32bW9ftx37GfZ1e+9Wz/Of6NQZhaYwl4UxMupZezNFA2cxUIH2PMZTHxIzgjMpDBIJegvS41ybKqRgWr0gsBJjy39nX7/+n5J398c977Pr/d14CrRUylUkQMT0XM21gqDZ7AkMbIgo0Ks1XYv8PHRZeXnhh24TY7KYWrSExNG91YNCgavczR9aKejPaKv1+ni+n76///+n2KACs0qmAril5lZ0K2ZfaIWmMPg0Bkc+CGQxdPMaIASIl2wcAMPCo1ybvLLoxGHKkPieWen3ADT6dhayGlrRYzVar5W//76Pv99CNVaEGDiAKYvH6hovBRqG6ZhIPhoELZg4DAqBqgSRIDjA/7WWA5yDJR//uUZMCIgiURRlP4MSBDwgiGe4YICIw9Es/sYsEGiCHJ7RhgVfeJiCRoAAsSapm79Vtz/6no0GBsh2fs/2Vfu+zahIzDYw/A0Zh+0NRpEPjKXg688+0DahRM9jAzEEzBoLYGj0tUTRkRoTSCoK9dKlIMCWPUJT5EO2IRTXsmjZ/nrdCdaqHti1nlrdUfdehjRn7ulx/YAJhvEznAylAcgQFxgmi0Gnigc9OJioOKqCADKmiMVZciAlAkEpjAsgeLQiAIS6tUhp4+qjTvcqHC7DTz+hDJdP1pqZHTElsMrYx41jdzF0NakmxetCteYjAch2I0GHosNB2HqjLvQ9AwYAF2AIICczEYu6a50ZAYHP1qBtGzioWbaLdVysmhcFAsZvtL0O7nNgUUoHmd8WLVvnEOWmlqA9pHn17bvU1XTZt0X2P6VGDQBg5hfqTyenSqYqquY2FiZ8jsYHB+YBAWpqqswVm6CieHMaciODpUehyUlCAERadv0g+vmEKU/6ar//uUZOcIwlsOxDP7MLBFIchhe6kGCmw7DC/xIsFfiKEF7hhY0uux5GhVTBgslPsTSvRb3NST/0btaDDoDRNc97M2ShSTG2ENOgTTrxs1RiACmRYZsqCY4jGJCYcdmCASibQnEuQU3qm60GYt8wQJysWjQSxwLpaUPzV9YnXIl7FYo1jcDr9L+zBGIwOQsKLb/X/yMgv//tGRWP+dkw9RCDlKm9OlEPMxWhzjO6LPkBNSowsCAUD0tUeG5HvzqpOHUYsSFj5b3122SHHCqjhgw1cKhilioRk3XlwkAUmVFbQM5xxrioGtQNL3meaFB8JUmDjzzT9cVGOdUhbWuvQ81QApLJbbbI0gYrEsZ/VWZDH0Y2DUY7iyZ7K4awKSaHqecI3MbE0sbfNkbPHSahvkZSFWZTBsRMxfhLowUwECiY4gHNkZ0CSa/HCsaaYiALqN9ZDUn0w1mNXMXBMVgAqnEMd7ZttmsgByxt03UApGDEha8NROpU2WgQipoBkjCLJD//uUZPcO8rcNwgP6SSBQgghAf6YIDGRrHi9tg9mMiGCB7iRYgCKJCF+UJSKpdIHCgIoACg0AkDGR09Fzs0YMwFnjrtqzl6n/kGU9DMNRGZ1Wu0UKfl9oJicCSKMyq7rtmlmpuUS+ipquF6/qrWu4LIqv///kyIksVAFlgAldNttgAAAwIERkRmynComJPAgDY+UPX/qQAEhwAekhD9GZ7RyOG+KgApJKyukAMowBOcEcPVUxA1WmRhPHXjLmJIrmAsHmgAImCIOGNY5mJwSDoCGFwGsnTvjFdtL8dACiAuKwALDAJgEURA/1fHzhmbX9c4jYbdO4z/e3uWaTP1vWMyRYti7gnQE///oIKxp20HTFEYwJhkhbGngaZrA5ggEIDQUBFqF7FutDgmer6gKEYHgMOKtTJ0EACow9an11MMXLyzTa2iYwxLxfOLzYoTFmKcQ2LltVM+WF/DEtAxcyg4XUMZ5B3DIWy1UxfgCWMDnAGZwwIcDEAQFYYHsAxmAF//uUZO4AFro5zeu7yzYpIgodaSYZzLB7M5XXgDjOh6TCuJABgAgVqew+x0BBkTmEWieqJbwYgDs1cJwBYKBXdf2+3jnzSS7+RB4nskVNbfWecie7R0lNS2rFH3tXkCUl27bvTWE3aGmCwKAEIhx7mOsS5Ao1zADAHV7HKsHtuTC4/PbUMaF7nOEX//mv/+8AJYAAgxqnUTE3SAMmVuoyFT4zOxAxM6oi0yshSjA5AnMHcIswvwZjJpBTMAQIcwJQEFXmBcAY4I0D66jwjxhGZEQzKNzCUKwK3BEJayYZAR3o219vnkT1jrJZTcvS+W36+VLK7eWcqj1XtTuOs8Ma2HPsYX8q5Fm59KW6r9NK2fJ/y2h136tX0gBIGRWq+RheKdYcRVfuGG7wOhzGt9YaHkS1mUijaRiPoMeYSYIsGB7BBxngxCmYOMBWGBAgFioDAcQDUwAgAAMAzALjAAwEtBMBGikg0RDPGbiGFIlvqImAQg/4RAHQpqMYp5lrrrP+//uUZM8ABWgkw4Z/IACI5Iilz2AAFsyVDjn8gAIGDiKjPYAAxeWz7hPvNSKmfJ+sO5xKWXbdim1Ws51It23S47uVL26qgswWNSr3AQCLEmWOlyocPOJbGJWZCIscba4dSSfMuIJhEJJTpeKjDv////9KQAAEIBQAJDdUFNMgJQo2zzVjL8P0BMTZkWIymTmRMYEQJBiVBAmFMBOY1wW5iVAoDQJqw5goADN+SgSoNEwi1awxrUXEMagtUCRGgt8qhFFvOxPWuUsTcN9qOjlz2X8L/YKllnfaWjxpXCAiIAkI1kksQt/obuup7eO1Xf1XXs/RrQU21YzC+P7NZPds8Ce5zWMNJM94V07ThyDEwBHMJgUIyiwjgACqYVAVLIhEAi/aqi+oYjByvPxzFg9mB2JixAVu0luKtM1f1bTlrX613Y87+6dq7QcYeEEdv/9/s+5pNBDWv//6QAAVSQnLQAAYbXnZfBkHeb6NmkS50x8WmCwSDmJBt3muvI0x570n//uUZEYMEyofxx95gABEgxlt7ZgBDkRrDg/7QsEMjuRZ3Yi7qEzgM9RIwFLIH7Sb7UpbGYfkcktHnKsNCEDBHExIwr4MwMemDTTSFCTsylceHMdbC4w78aTD2H4MMlQA0SRAjALBEMdsSowOAJjqvzRCQMRQ7M4UCS1MADBRijC4RtwxoqxdCXyq0NuDK6Whpaem+htTOqHkcoo1Tfj+FOaMjSCyQxE8nVqTX2/////9P+zeAgWjCJFzPmcjP7kDE8dgAOhz0LYXGwCqBNGOg5MFudF4FhcLiMdjEpj0btxupqNVv6MnsHUtTI4zdd1q4QiGPyT//TBOFBwxx3wwN7H/DjL4DEAxkMF1OVhF1DFsQHUwXIgwMLXBcgIBnmBFA+pgR4EuFHBzBo0YMIDAAFGVWIHMkBywylwONq/AoVDuoyLAkRkVJ6VzsYqk7w2JVm0bSZZ20q+cdhOFoUU/kmGzLSKXNxAQjiYRUmYhO0B2DIta5i67ry8WSU1PcWeh//uUZESP1IshQgP6StBjQziBe0lKD0SBCg/0x8EVC6MF7aCziniqZRLpGkDAOYK5w5natimdm9eYvQzJgZGQGkufKYUgDhhZlMGRqG8BgTDosAFGMERCgFpD5QBLvZylElakWhIZy5Cd8aTmfQQXcxFIu2uw9NOjlPjfc0PjSI00BUq2f0f0f7v/3p+Kf09pghCJMaf2SyGUhdCJiLRZ2YLEXTmV2G7hhZgHqYIuPHHyiRGQB/GUK+GVo3FY4GHgTlp0n2WtEZqWUYGIQDLxLcAQCrAMhLcpytJdGal1PUqhmkimjJFyBDSMYjOd/bZjumApIyHmquJbFbn6tzH6/p6fx3c9vvbf1bzGI4GWYGiQJmuEymO2BUYBIVpnTgnG/iZkHAZCNBBYoAgBRCae2kP247aj0xOypkznoFz2EQw1x7lWWtJPZAyIIq3eC6QpzB1BLIwvWd7OOSWWDMSSEoxKALyNZMH7TDMANgwh8LmMWWADzAaATw1vcD4DPvmg//uUZBiIk7kYQwv6MlBFovkad0knTMhfG1X0gAD2jCRquoAEFn33ZVDSwDNV8xyAUU1LWcQHASVimYoQcMGBR5AwNVpCyEqTEix0qYBy0wksnTf2LbZvdV3xGt6lntyQwEN+5+1/mn9I5YAFopOIAAGJ5pGbTdG5fMGJgKA1EzF5BiAQYKw0Qw48y4xdKVbJn8LLiiKBUQsE2jUmCdMzsYw/Y/vJf0x9AhfqnefrTqCABbTjbMDRBMDIuB+U0lUq8MVYBWjBYA4Axh0LxMGHBCzBAwREwYIACMAzAFTAcwCECgA4YARwE6deYfhAfLEgoSKgqyTGW03XD3Jr5qlWhmiJXnKzwb1q/0Xbe+6U/6naP/U5H+okC5AYih+ZgwYZd6GLP6KhuEYaCggAwVmCQYBAZKKqqT00/F/C+JAluhovRJK3Rt1FSShsK3qWo8yo5FrBEgKMgEARKEDAbCPkzRgUoMrbmkzDvSAYzRgbrNBHFiTE+QsswWAB+MMwCwjA//uUZBYAA64YRbZ94ABNIqm/zbwADdhrFrn3gAEDiOh3NPACMwMUxFAIMMD7AdjAKgAliRMAZO6YA+ADNoYiEC7GSZyAFKM98T0vaMQMGt8brZ/PnNc4v5loC5ifIUN+33WN6Cn9ci1+aV9rNSQAAAHeHiIiYaOMAAAAAAADK+U78fOEaDu30zP2NJMDFg9dhfZL0WYk1iZEqGAgdcwUMds/WUghMSxBpMrhMiTloW4nj7blilLVksYZ0gQEAAAGPxibhnPJIuaAEarGbVinhlbMiYYXWVDGCRhipgbgBEYSGD7GAggTJgDIBYYCWBWGAkAALjKTcUoAF5YLST8nx+F8Vp7qrZoIOhKSKdQLZgRJo0bHmxn51j7hBO613//////3//+d7v//WAGjLbprrQLAAAAAAAAZSQSrDD+iayZNGcxGYoCz0OmMOL7wlKGkL0w8vSH3zOBIhAZOoz9mUk+iMtjbGwMO+HKqYgwUUG7NBvYvTYYSHc01YVFMJmFO//uUZAqMEz4Uw4994AA74umK7hgBy2xREDX2AADzCuOmumAFDFqhw8wfQCzMAwDVTBcAE8wDYBgMBWAEBoBTMAtAD0s0JEurWTmFOWRcbFuN+xXrK09ccUrNeaNWaoOi8Ug4PU4dJLTX3N/6LF+3/q+27/4W1awAbvcjAAAC4xMrCA1vgyqMTFglMXDotEnOUAKKsYSBU1jOst/JRLBwIRSscyzju3glAOPtVaWsOpOd39wMYZ8FyGYdI8JlcUFqZl6UOmK9grZlyIDUYJ+ANmBMBKpgDwBOYCAABoFGAMACxgEQAAlE79NdzNR1EVcEJIJ1VIqJRk07SaxX/oxE469iPs3/X/T/8l/+n//pAAEJ8DRw2DFP4j5TQDeMczCYcDiwyzAAIB4GUJacpggEaV6Bz6U9nuNiCsE5ZqQSkSWp3eypITy/RkfKOgAIDIoKlA03sUaMq1UXjAtjAw09ySqM0eEdDCiQNkwB4DjMHVB+jBLwFgw64KRMCGAIUynF//uUZBqAA80axA594ABL4jjVzyQACpQ7Ft3mAAD7i2RruoAFd+BC3rjHMSNEIGMXQsCGOAk5O4rOUrnD14V97riuv/bHxeYGB4rNsRGXCy5Jx5FyOhHdt3D5h72FKpJvoG+tzE/s//9IAlwAABm5rVGLQOUZOyJRnHEUmX+CmaqwpJhcAxFAHxguhMmBIACYpYM4QBBGtJ00rM6cUAIDZIDuggIquMCQ3vcEQzu////////+kBi3F8YvbCxxA/VHPzwKbICzpmCA6mskIeYLAJJgzA0mF6BgYGwCJgPAJFyhIA5l03FO1VoWmRMjLNL+WZLwmI1Tcy4ee2vZ7v/3f8Xbf9f6eilPxgAF1O0AAAGEQbGJskmF/gmGhAmJxmmtIyGFoAAUIDAcBmApquq5MFRqm/iwHoTixgteo6+6Ypy6J0LQywiL/WQAAwjgD6MoXVGza/kU8xc8DNMGaCfjEmQ1szJkNDMj6T4MXQE1mFCZgIW6UvDrP4qJpACRIGEJ//uUZBwNkncUxBP7STBD4liBe0kmClQ9DA/7ggDai6Nl3BidGmTEv8arZNMTWFA07Rt/////7P///9dAGBmNcAmJTOGiGMwkQwwjiSTQmJaNpFNiyFnauguJVoTpVfHoYktHgiRkQgcPFDzeytaTUQVbcBV2f///9/f/////oMCJB1zLUm7k03uhQMFfFGjGROhN/U6w562TPS2OKgsGnUyUFhCGQ4OShWiX2d0L6Q+z9zcez0By6bpxUKiUFjA86526X7+n//+n7/69n07PH7WJJ2BjCUhnN8R/CtRsMI5guSRqAiJ+Ia0FiCAxBM1t04VDPzQQOFhexWdz2SOc63q/rXVlzCZnJQEDDSw8gzOhEsNbBgGzKIhEcwZ8H7MZcC3Dah4zSMPRLAIImOnymRAADwK8i0IxBlNMiOLGVqWRR/2WoVvviqETDXLmm3f/6f/b/9vs/1f/6+ABwSpk9jbGyBGqZAIIYWDUM78M885ACFT4JzCERg2XrQnPtgjm//uUZDwM0pAVw5P7STA7QeiBe0wmCixJEk/tJMDqh+IF7SSdzUh86Pq0wTLL8+sEQYCQEcM7Nnn7KmVUwJADsMWtVWDU4h+YyUcFeMHYBqDEKAik+QwNzKDHS4woYMJHQ4AMJBGaWJVSbKGAykZKXkPLL8mBOKwuET7hf32qxhCju+7v/0f/9ux3QwwfhszCnZ1M2aPQxGQKjBkF5MLYfI8VcCijTPx4qZIUAQaCj5O5HZKGxxwgFZLeNOgyMB1dwpNoXvyqJAADBzhIUymM1vMk1Y7jIfANEwUsDyMW5B7z9S0zDEOlOzDwkZCFH1pq0RW3WmIRQpqRtlNE7ZfzzzezQghgtb2Ut2f7f02mP9X1//pAAiZuAAAAxBIM2geM/NjMz4EIxCKj2BbQoBIEQ+eMuquQBnAub3JiFk83J8zjy8NlNW/MlPW9qghuaeQoFDmHJKB5hzK+mYOmCgGAugpJhP4PQeMeYbAElkYDMhFMUmamy8/RgQYXpI9y+pZM//uUZF0AkmoUQ5P7STA3ovjad4YXScg/FM/oxMDJBqKZ7ZhlWEYFIXktm706k+Y30f729qUlj7Xd/9P/9VCAMCsAUwxEtDMqNnMS8PcybOP32TIwYtEGCA8UFrkLI828sEZytBQOKB+WxmaShK1oVSyMFPAfTLpjaAyNpqsMRvDNzBSAh8xqAHsP2CTc1AykuBxIYcUl6goAW5QT1OYswCN11t1kl2sOSipJtGgglhPc1GmtM7Vu7ea/szx82xzOV+gan3LAGQAkFIZSqCRiEOrGCQJADAbzGFCIOk8ZoNIZFkDOsFgKFWFxzjzgZkSTQDJiTCXxfvq098wtKrJgXoWcYQSrFH13H8aVxgxhyjuGM0R6OMDESw4EjkBRDME+43aw7/7yw7Q9zqAALyAAj0yaD79ifWn9/+b+j/7sm7XT//vgSBgaBEmP6rSY1bSpiOB1mg55jVsYIEhcYCIFPkBAEMugtaZA35brLSMSw0QAX/MEOs1fSjCbAgAz1Env//uUZIgIkqoOwwv7STA1gdiVeyYnSRA3Es/7QEDQhuJZ7ZhhNqQlVzE1R8cwSIGEMffApTrzIx2ZQ6BgQYWUlgHFAOll4CzfQSSYYUamTBAKmz54qPcU+7b1c2Y2kNCshtTn2pVkna+lAvqyObSt25jfSBFgDAaAMMUJCM86LY2lLAx2BE3yB8MIBBGJCA4DBFnNLceWG4scGU4MSwpg/jnak8A2C13NUYHsEemAXImZjDSTgYayJHGBJA/4VCCDGYwwsZEbeBQxJJesCVJztYSSUSSaUQIFHvE0UGWV+wj0buv1/939v0el/9HdcYJIsRj6K6GqGzUYuQOps+EbD8rkMVjy4K3QUCOo1iD+h/qI4EBQJQYEzx7HhaPATET2v////t////+uQAwqUG+M2+DlTdnzUMyZoXTMGxA2jGFgWM3vjOtPNFNNJNlqScTr39c8DMK0mnBRqNf5n79exBn7+uhfu/31fq7Ebv7e1e//Z0jQQMwkhgQiSGMShIYl//uUZLIIsrUOQoP7MTAyQdiVe6ILSRg5FS/oxMDsh6IB7YxiTQYsIiBTKfnIwsJzACPByHi6ulyhVHcNv4hHHCQkLXCCtztvs2/7f//732en//7QAA7pG5G0QAWeKkANMJsxGBAIFk90H2Js7ZYjQtM0K82a81606sE1ZNBECQJgwKV9M7DuUsbl9WG4vADOJBK3bh+cwsTbK1Ttu6ADEfNnb7CxyaTe6+zCxyd9YcHnX/Dgw5e/PSJYzoYyHnwH/+fAGEoD2ZSqVJtFJkA5Ns0g+T4lGMmDEAmkOi4hBphQHp1ORPcmVHPEIFYRRQPqmXrQjevqt///dZ/Gf////9dlAAc2sttsiQCKBlxxDqUFC5gxw1sRcM0C4ywkOLO5MXEMRsVAMdI01jTeLWssQlP1Kpt/YdeJQF8mBFvm7oOsRdVpTXZa+rWWuxBOZK4HwHNntDonbVay7VlIIz0ZJj4yVZCTaXOnoTFS17LpytgXa7Xvlk96gKjS/Nl/9dBy//uUZNmAQlQWQwv5MTA8AViWe4kWDcyfP65pifj/B2HF7gxgbNhed/nBQEAkAAwZwnTGcSxNMMrAx1ggjKpwPnGc0YMkiTMoGRXSugiHaJYcD4lq7kgRElW9vL7xV7HEC4fD7si2kdAUMBHDwDBqVAUwrljnMF0CTDBpQdgzeUElMHXAMDA3hfc6wvTNadO0WExEEwcMjLoEFgGzVGVR1XzDUEiM6esoQgZURi6rpPjnnsuAqMNTtOW4xMxC0hW6j+73f/1///t/6rY3LQHgMAoL6tFpwF+8MXQut6AN4xQvSRE1ItnRzC81jRmsCCTtQPCfaJ6nTMk3Sh0KoyOlkDv0TjOC7sI4p0szHZXVMPx5IyjBODHWK1MX8GcIAjMFMYAwagcAwJgwEQGmXQMy5WJYGUuW6AyMFHatYlQpWS2FPiLq3g2hubTqNi9oMynxIHiFNX3ed6qO/96VP0aWRmjTenmBsXrFEAAApqNuNAAACEhi7wZZAIAiUlMrJAwg//uUZO0AlCMvUutZYn48IciWe4wYDHBpEk/xJwC+i6f1l5jmRwQrYq9TrNBg+JS9r7TyWjTIiurPEh6hw0Ew1HzGjrpl12766KmOCM9q3fMgH5ENVeTTTZB2XU6qAubM2cPITiVEpAxbALgMHNEKTOmwjkwbsRgMURE5TDiguMwGMAaFgAMwEkBVBQBGYCUAQmAigBYGWIKHA44C4MCVA+URiDgCZibQueFqDhiRC0jJFQsi2FEgY5xsTguYqLaREuqNkD5knWYOo+i6CKloJqXRroIIJMmksxTXTR0mQT16nSpUmToLTd1a216LLVSpKp1rM1tEzlgjCTIQLBkMEywhtEZECCgSef//7v/+xhrzpimqE0CZbxfZpcBiGha5eYaDyBicg3FYk5mfiSmCABcYfgR5g2AxGBkAgPAPGCWA+KgMEwDoAAZNAvQA8FELYeBWHhUhosRPwqCmKZDILnBet7BMwk5ZaatGxT4hTAYLtOh2OIhppdgjYEImUKFT//uUZPEAA3caRJV54ABDgwmdrbABl9kbCBn5gAJCDaGDPPAACHTChZA1rqCQwgXWdrcm1GWAdKC60XOxwhVGVJ//6X0ZAAMDDBozGVR8Yz7pcVMbzDpTAzg54xwkYHMFUApzBEAVAwwEFEMDQA9TATgLIwB8AmMA/ADlSiQAE366l+k4HKLGqqp1b6hepyMyPbPabp9z1kn1nVK+3k5swz/////////QAAA043JAAAE/TIazgDjChAqiMwPL4qCoxQ7lep2vVYjbwEEwlNjWNtNHVMDZuv9bospaLxnmFggLJg4gruYrec4mVXMjhgyoiUYBCGTmRdh5xnWMph1KBvaARg0OohK0umhq3BWNThtllRJsjPrdLGY4QQIhA2zqe2z9q5vI7Z7f9z///Xu//jb/uZ/9KRBxAwdB01QMI5bgwx0AYxbJY5bIkx0BMKSB+GBgSm45MEPK/b0O3TPHNTS4RqVchzTf73bedgdx6xVp5WgqIAwO8MFMekQgzaB0//uUZJ4Mkx8aRJd94AA3Avm97RgBiuRdEk/0xYD3i6Od3ZisQ8xngKNMETDlTKDA8YwQAB5MAwDQDo0gwwHNWjgiDDEBN9CUonDL0ZJmxVAoGRGTjTIjDQMpqbW37jeI5HhWzp2f/6/9Vn//q///6QAOuvwAFXCC4MM4QUElUzMnWAgow4g55yYZnom5bEorHXhi0xbjNSmnMVuiBtGdZcJh8LyleSWRAgOJtNMwOR9DIKj+OECbUyiQrTDGH0M8Yp8yQmDKOmN4FgECIBIMcAQsAkwmTw8zl0XtkdKHEjgdwJ7qXc2zGiBJ4PCjffdb/TrT/3fdkv/s+r9ZYGIRRgaRjpC2DMEIDGgazdQIAkYCbi0YyMCEuJxz4w40RmwIcCQhhN4SIORL81VCvRJVJAADBBA6Ax69M2MF+h8DCYwbIwMMGMMm/BXzQENTCVtDnATQQLxZowCBduoCA5Dm1hLJrqYMWh11BlcgIFw0RjhmLWdyuq2gUeetc1GJn/tl//uUZLQIksAWQ4v7ScA4AulpbyMtinRbGU9wRUDKCCNJ3QylvR+c06//Z20AAAAxqKAAABGgQdU8+XTQwJAJGNMJVdgAoeQKBEobBQKVYTIh0lAGrGr1LyV1g4HkYoIgjQTFFIDBFglwxlNFFMlTclzHfgfEwSkH5MEyExjB8lTDUJTmgGjAUbigWAUCYCAhfqCFdDDlgW1jsPEl0YXZBWkaSOW5WV8puhJh9m5SbdIx+dYxGv+rsxlE4QR2+r/T6b/vwuMaQTmkb48lioYaSBqkck0AVwuSUUrJWmQNAsDsN3D6QGLMExJRA5kZekJAkZfASWkFJRVTALQBsxn5RvNQqA0zLcwUYwlYA2MgaCpRbCDHUzzn4EzBolDAEPiqBANAZHWwrl0Wf217smEqa6JRYygVBFRXN/9UmSzbOMvShhWdWr3o72LJoz1l23GP6qqX/StS1tcCsSPlGChlckkgBACcYRixQ4/kYYQFxgrhFmViGQYYdmPQJ2B+FwAK//uUZNaMkssWw5P9SVA14gkNcyklC9hXDk/1JUDSC2VlvAzmEoQChQAaewWHm1p2fM0mAsilpV6tTmOYMN1hoNm7v1//9X///////262YRqBkGU1IThrpLuwZKEBjG6NkHXs5mXYWGNDmmzAigICgMLIKEEiAJkM5GYgu1NZiUGGFIwRwDlyY3cenv+Y0ukbdL31Iy9FC0XovpawT2OOVf6nV0SqnI5e+6QU1ayVgpUhTAGAADcY1Ynxl7rKgYugy2qzuTyGiyZAQJikHLeMHApHRWxMFpdG5cWrSZ7SoQCDiIpAxcLzclkMZu3////7//+3/0IwVUMYMZzcRDBvMpEwrcNDMEdBHjI1wYg0HBwwo1k1+HcFAkIRMMBQXVkQCggBFxp5MtUggnbUSoNEqYeAMSHk2U/imeWScSBJUmbQJH2qYHOEmDlULtNqi1J68NhdRwy9CZposmxgvGtYAb1HArOwkKiLNsf0tWAADAADtMQlO01ojkzIDBtOwyD0//uUZPMM01gWQxP9SVBHoiiBe2YqDJhZDC/1I4ENCyIJ7gxwMszgUIHcy8ULLmLnpMYL3YVHocnYFhdaWEosmxMzEv9KqaOZjhBD/////////pf/3fGIoQuaianp6qVBGbyBSb0qJ86SGWBWYhdYBDIkHjCAPRCTphFwWYAtG8zWvu7T6VGpAoGlLBQgNUv3fpuumX6d3tS5bLSxn/6e0WeO/d9PAoISY4A5pnUwZGJoIMYPoRRkBgLGOicy53xgoMyikc1AGvOKVBgyEjZA5bYrzWhSzaXFrwABOL///6f////+qkZGBbcxy59BOHPUKjJqBgowfUBTMVACvTEwTTGiAzVUDwAFpjYCAjA8dANezHAFJFpTFZRWHZ8yydNjlfIuZ2Z7ctAkWwtPODodBBAVaYTpfnkuDLORaczwDJuYsmR3K5ipYjDryzFKSQgQYkcgks2wCCxtBogDB7DUMT5QI1WZczJUFZMhsE7yijBBFMHDYHSpHIHBcMAqTKFC//uUZPOI07YVQYP9SVBCwtiCe2McCkRDFs9xIwD/iKIF7KSYesIfWHnShp9xiBjFccdsKIr5oXIvY06RtN5Hb/os/d7N1d6O/20U//91ffGDihCRkwAu0YBzG4mBFDBJjO/x8inJgyFhhcKpoeBYWC0wzCZsjVKRXK5Yuz1+mGQMNuKtEc5RwnFxIlRdotDC3xyYuhDmOJSJ1q76zazCZX9rZcwKnmIKKASJhDYkUt2+5tiBTMbIYMQFxk8moBnMBlXhOGEkDKZaAPxSGmLmgkPCwCFwNsDqLOjncH1gWh1FNq+dTtuYt4/fKU/1VN2J/2+n2fTVz27/sv/9dQEMCKCRjEcWnQzyaG0MdFFZDh2dOZ9QqBcxufANowQBTDQpFACkxIGVuUyeHRwJBRs5MVKRcpNQ1Ppf3drC5uv7cc1Gx/X2Gsdo/QRRY8XtV7px42nSuZWZMEYDExMzhzLJaQGh3DFpKM6VEoGgiBw8Z0IS7ioIk5saVNODKjo5aUfd//uUZPuMw6oWwYv9YTBQIjhhe4McDOg7Ci/0w4EbCyHF7ZigTTvpEBSWIMijvOe1f2a6mY/m9lPRZz1znPjXJBfU/UuxdAqyyYcwThwToxnufCGb7x1phwggmXsBGVmAJUPqhYiOmBISzlPWdBMU2iH1ZTrO1QDBIFFDohPklqTuOs2MFnOAGtZYQNPRXB8znTSu8XSg4bqaGFQvrUlE4zuh2iFJykgDBrB2MhJOEzRZlx4okwaQGjHJCUOWcMXAMQaQzLTpPwWp5wRPBAXDiYOvdf+3GYSe9i4fBhDBtmrVF60I96u2n+X0acZ2/3K/ivU+pKowWYIPMFxgxzX4IgEyccPqNwI2MdoHNGNjUag2U6DDQx0bKAUMB0gn8cOGnjmIXL87uf1d7yuEwVQoWDJIEu0mbaKixBicWCugw1tyLeNU4Wj7lmpwrYMZeWPDnNmh9Ax4uo/QxAzRiQYCBpJlCo6Gl3WHqHfG1okmvG54lUYS5gjBKJihImEwTNX///uUZO+MwrQQQov8MNBOoiiCe4kYDAQ5DE9pJME3h2GF7TCYzys2KWao3pYGQIXhISDDYHmWdF57Rx+23Szyv772/f/dq6a9bndZgiYAgZOkGAGWDwsRgggN2ZYi6cFicdleYj0cMWYc6NEFD16MHnM8IxTQ/n9YkGSbgeAxo2q13as1qemSbgYzoT+jTVp71PEdOj1KWqR5yLz/WupiqsqQg7GI6h4Z6KVRjqBIGNqh9lqYCWKHCMVULGi9U5CDYU5g0hQqbef1HfbB9o/kbB9Lmyenn9cts9zl2htWrR3bNq+r3M0vfXVRCwwicQqMP8Y1DUn2IsyEAJkPloYw6mjavjGPiOUj6FyiFrIHedWQRaF2qeVflrHdM00KzRuePPpVgGi5QgJC7ka7pyJp0uMGKeNcg88IrQTHMaaHMFH6ChFFbmsjL2qASZHzYEGBoBaZBqgJnzTmmH2JeVWUeBD4WQK8TMBTGxNUfOGYZr353UttEAZAcCBQ8KbScakn//uUZPOPQ0kPQYP92IBIgViGd3kQCoAvCg/3QgEtCCHJ7aRYOTfbmro3mj9LmBJVKOxCU7aEUp/Up6DalmPr202AUgBhfALmkGVYd6yB5n+jxmGUBSYRwDh3Fp22QfQP0YO7FNk4MOtGTRghIYORhS7SLUMYax9dDK2yOC5zJ2uQGNEJo8fMIEakZMtMnzZhuAtDoJiF6hVg4W1NMmpQWdfprYbAKZqqHAkTDMLzMchGQwuwmDJITG3DAAQCOLKvIFQbOHjsYiyOi4LvLwukyB9FiUdZYxz0mfqqRm57cnqt+1ErjNtNyUjsa/bu6/T3KgAFbbbbbY2gYsnG9Qppv8YIkG3TAZqkTWHQpkxl+mSwCaZl5EhkADgmR2B0ZuCfxm3B+mOKF0YjQaZjojxhB4ph1gbGGEB+NCnGC0BoVgOGACAwCPTTeHTiJcBmhOwHhN8IHSjwazw5cWQFjwwBVdA43ADjXNqIyHDHXACANLEQCc6Yim6dC1F/tRac01zo//uUZPkMgxsNwgv80IBQAVhRe5gQDFxjFk9pJcEviCJl7Rhgchl1l1KRVqX5A92guzEcbKypdK6lcus6sKi0ZlVDKI3GJfRTs9S0FDc3l/bFvC3zLG1ax1nh3nec1vv71vWq9QMPfUWv/1AAra7bbUAACaL+Ez75RCQz0gAI6JJxrGcWTJjUSAiyl0y0u+VjF/NqERYcaKa60qRcPpaUx3fcfcbeLBPEgkGZwngiDtq7AANId5h7dYYl6cIyaOq10QW2PK4MGNMQRCUymgFjHjE9BwtRiODWHDAoGYewMRiqEYyAJr0j58sWJg4RgXDUwNFU1IFMFAICQAdYoAVwYS06PyaV0Ft/ZuzdsU7+yKvIJRF4pRzPLQQZFQEVEYJbHVNSW/8qSAAMctskAPwIACZYlDKwMjNY2mepQW4DuN1Yu0uG1ApGYLiW//QPdlFlXw/d3OcHgAMV2TUyNF1jSKQiMYMP02V51jCJBbMGQG4ChPmZIL6b/wZBnWWRh4EZ//uUZPWBFq1Azut+ydZGYyotZ0xVzrhpS+17prC5CGm0/DDfkObB+qeJjaFBg4C7ODBVOjCwES6ZcILgICgJlV2GmvUmNCAaxo+5n7lnNIVRZCDbzJDv1WAAAAuRy6AABCeNNFET4N5jZnyrMq06SN+TNV5naZlAK9AlYzVCe5974/xPD9EOJhA1CfiMFWGI4qaZv5XBkpoeGIsHMcNzNJiVAXGBGAeYIYSRigLuGlAF+ZZBRi4UGCXuY+rBg0Plv3ZMZigMMyEDdWtJR82maGqByOM+U/fsilSaGlzkntK6rRpv/2gqAmC6brJaTMTM0toI0xZhlggrGRdKmXq/oYdebVBwmRaKEeGhth1n5leSiTBKTwu59Q8DAAMIuC5zHvR/8w8YtTMA9AgzR3xoAwaUBvMAIAKzAFQL4wNkuOMMoBkTOsbgUEphAGZ4sopmIEgQNQsBBgcQYkJQQFjREEaY1NZpXkvU8eKBGdbs237M+JqDTdf1SnLAAEMNuSTQ//uUZMONEv0YxxPdMlA0IvpNYeYdi0RlIG9wyQjIi+gplI2OAAFScCqAsvOpZRCVzFhq6o8vahTVxbRyY+uqkkZ6m4c37CDjPv6MQo4m1AzKmxzBGHAEZhi7o5uYjUX0GGSgIJt6gfMYSIAeGJyGGYYQuxlH6EGmSOiRBLGCwAuYK4RxoIjhgoGYxQmJQox23ASgQgZiQWOBIkKxiNy6xqVyePyuxTWu2t52tYzeMqiWdKK3jG45n6P7wAA2bmhhkGcPZGc+BlQicklxj4FoIEGBifiy1QANPTvAw+h5yqUkB6irN5CFy3LIbbCTDkv8t/3MqDmBQ84e5SGOA3gGARFjAID4gwSwvmM2/C7DFBgoA3es6nMH5BGDAQgH0wFwBmMO5FWAUmRGaQZmMoRGIKgmeF8GQQYmFQKFszF4FTIIEREABZZlSPsAUTMq1yUNIDF/V163+/WyH1rdHaDligLA6esNdp0DokzLCf4tv+3TZv9fTr3qUpLInvrAACZN//uUZOGNEwYYRhP9MlI1wuo9YMKDjNhhEi/7ZMESC+Vpvhh05AAAQk816ETALzJRQa/6JjMBBQNMoETDes8QHctJIWAAdOFYUp59QEKM4Zu+Bo99cH3GOWzf3r55NY5LlT5fyrrW2YP2WFGApEZBkWBiKYaWG9mqFJXRhGAJKYDGAHmBMA95hOI9CY4KB6GbAtmKYnGPQaHxDxmRAHAobwECRhgWgEBtrrX15l691AR1fyYtQKt/4u7putdts885w052f+j////+sAIElyy2gsoCQ4sUXmYuo50oZeqUaImEqRZgk7xj4nQq2Vwoa9rA5K2FCjdumk+cu0U0VytdqV2qMKTEWDFPhtUxCI8YMCkFRDIYWucwT8EIMAIGcwsQdzGakfMAIbIICdMDoC0wZQVDQfDrMIsBkaBDNgA1FCAcENAaMYWFQgGlkGPlR26rxPZKrVq//aL+fN1InfmcL1Tfu0e6v97lCn//////3+kwFgZTKQYlNwSngw4TKj5F//uUZO6OE+oYxBP9YlBGYtlKc2YrTFxjEg/1iQDgDCg1p5kn9NMmUO4wUgFjAqCPMRSY0zWwtDDIRKwaYEeRv3DmCxawswEAjE5GAwrKwOvNIRj0PXD6BcVS+MSRfpm/9T81KfOOBkybR///1/0///osYMSIkmOFDapkEI9yYOcFymPNqShgCIG+YBCBPGAugRZgoIvmYjYBHG8BGMlAwbRTBX2CpVLrJymDYmCj0EAZIhDFEiE1gPGK73SqelmGb/aeysDLq28QGDqj8Yl39JDEjM3NkqOM31TnTHeGtNij1Aw4AFTAKBMMCgGUxwGITAuCaMDlswWJTE5xOJqwxiOxYMBwhKwQYzAinn4nm/oerGAV4CET8/bd7u16UeS3R//////pBSoTBxx6UxUQUOMSqGKzBMQj4y7BPTMF6AgiYA2MA/AyzCBAy8SJ/DDAGiYIzCQPzjgbxIjQgAWHGBAkhwNPAgo0tZuEyKh8c7gtDbUvz+Mz45hNg31f/T/+//uURO0N03AYw4P+2TBdQviAe4xIC9xhEi/xiQFXjGJF7hkg392r///0AAAnJsAAAZh5y9GXRZGB4WHeRjiwINda8YoBKZSBWPBSsClKYaBeiM/7/pqtcmn9Fl/oX8o8vEM8JQ/azaYcStUvEdVu8IAweQn/MU7NYDA+Ug4wnsGvNslYsjCgwVkwMoBCEgdYwfIVOMYcB7THUOjE4NTAMrDkc7TB0EAgMWZGFAxGGIIBAFqAJpLhyoQSDEoIFE3Q2lWqzzVSQIFn0/U2ff9/+1/9P//+f7PSBSkAMCxlMCTvMvxOBQeH9pKmEIDBxCY+HGFGR4wWXMT4CwecSbJxyukIQYKwZH81C67TZlxaavdbXdRcHDRLW1RVoAwaQSzMhnH1zCUzzExJoBKODuRBTCmgKowNcBVMBaA0DA8CWAwGwFnNsnYwaBzIKFNz2hDRb4MCRhkgBAWL+FrraEfL4CnCvPlzm4173wqrVmzAkPFv/sQ29rH/9f/+2z/6gBJw//uUZNsMkwEVxRP9QkBFoyk6Z6YPTQRnDi/1KQEFC+Rp3aylAAACEJccCnMAwXcGgamiIRoYNQBtcAgIAQo1MGR4HiwdAATIkSNAl2oipguOWy4DFXVZTYNvlk79SiRSi7ZjCGAHwxPkzTMn8BDjGHwdI42sWCMMtARjAxAC0KAYBgWpHUYsqBchiCMTBIx0hj7BZJBUCQIouYnKoQWi6Sk+qmq4EhDEwHRNG8v1fUi5ACDkUrdGdnN+yz/v0f//9X73gW5UDFwmjDdDjDhdDCcTD5PATE0BhIBRGABh01RQF4GOV2CijUtIg2cwNJX+oqZMR6eJeRQ2c+w6E7UlclRAx9qqLmaTAHhIIwXAO4Mg3IvDDeAac1zobMMImAczAoQA4wHwBIMGbCkjELQHo4gZMHNwtBHavokVJGtzBwDIoMeCZU47kWHY2trX/zN3OYNKhZ9RGmRPL7PV//Z/+j9T+2ugAARjYAAADBVMjIsqDMsiTFwZzsT/DAoIwqBg//uUZOENkxoXw4v8SkBAotkHe4MrTChZDi/xCQEFDKQp3JkdUAYxBXYSdcaCAAKpoclICjcMONB6uJXEkD40D/xtqm7vsXWSahiSaspWGBsjlxhHJkQAT38wbMM+N3BSvzC+wLgwJoAiMAuApTBTxQ0xYUA8M9BMyOMDGp/Ortox6GzCQEZgYKFQ0H0R1LGcM1o86CF6kk244vjrKERd5EVJ/Z5QfR+qrZ3drvYzov6/215pcDA+IAMNcj0ykCuzDbC4MouZEwVgSjAkBTAgBDNFWgkzkvy5QIAYykGmGnphwwQDRfMMMxbixe3iwvu4dByPSnYggg+QIu/lAAALtYRMXaY+uezWESMMT8gs59rJDDABjAIHGAwgmK/PmoI4AYMUE4ECsMNSHVfMQGQORLd2o+z95br1b/0klqfUepAASDIHc17/6o1CaF0Uav9XXu+///6mgAAwUROjGcFjMQM0IwKAmTJjgEMHUJMhAQXIYPZDJjSgeByonyFQM/MY//uUZOuMkv4YxRP7MkBEIwj6d0ZHTNxXDi/xCQEfi+JJ7oyxBIM6i0DCQ4eE3nfpfLbft5KQfOrhVUsGyhQx7goLSlSYCeAWGFMEvJjoIdMYTQIvGPvqlZgBYHkQAKZeMwGIXvMEiAiBs4YMCYSIArAGer3Z+ISIYKZLIm5rWz9BZuiVi7coQIBIusAU8xf02dHo6jvUm+5Gz92NZ3O/r1BmAwnQOzHWHIMmsrkwwAyjZ2hHMEgHowKwHzBABBMI020wlwNjVAxZuZJSey8IjK2EGzI0U+3/gdw62Oic6cRQFUvLbf97PkrNJXf/96oAbULSYLSCnmLvBSpgtZfiYH+KWmPEsBxgdYLGYAgBKGAfgSpgYgmyYdAAgBh0giCB440ThpHp4wQEs8kctUNahzgOhk4n2z7JX8cuGCIOs01/X36fvciRWjt87dG6XWp17WM2X2MTNgAASsAABY3zAZwDmAFTNsiTfbWyMGcAcxGCIwdEMy8lkzaFwBA6heIR//uUZPAIkswRRsue6BhHgtiie2NJS/RZEs/oaQE2DCJZ7RkgCMjA9Wop9lxgECqLLxvwtFWDv1Mvx1Y3c1Mat+UrMqrS00YI5GZjGTvGZ5TEYeaABnrCWOYKoB5GNYdmAggGgBen0gvmHYECAAzBEDTKAbnSXc5QyBi7o9KYdW5Yys2z4GAJ4qbaLhI6wkhn/usVeoftmcchpX62u2bHJ69iaEQA+gwKBjzAfGPMLUtAwpAZjoSPrBw+JjsRjoKNxJ46wNQgriILmBQwPYNSpnLT00kkX+f2nZrrwTeQUIFIsrE4cFgG4s9VaVMCMDwzFKw5IxasRBMIuEjTe4VQwwocD1MDKAZjARQDAwYAVKMQUAvTeU4ywNRkOumVnNcVwYSIsifZ5IKhnDIUO3ACX4ZmedN4Ofc1E6ogpUG5QqE2tPNW6Klmh+cqm2XLxiGVUaqVWP0/a5stJwAYC45Zh1GQmLaiiYFAIx6ogtCRURhQAYoEAN0Qx2A4z0iMcAAY//uUZPaMsz8WRDP7MkBJAiipd90RTEw3Ek9/ogEKi2JB7gy1gmvTha+WxIKCC144/jhPyHo4UAIZZTYdU33/0atf/6u3///6P+MMF0EMzEP0ZszFg8xMHxDgjtgjuMIIaDEBCBMGIRgwUZwDB9GtNtSjOikhPj22IibkBreqACwhOtwVkc/uNLP37lSrvdZ6klBKLlj7Ul3KDRDQLlrskrs4udbl0KNWMPWvcuWT2Mi4tqZtLu8qly0eMALUzBjDpH2NIqc6vD/CYeUODswsCgxXiQ2JGNKwrA4wGEIy2DEICBnkSLnsTlkPzbn/r8v7vVgPtY5RZtCb2B6j+hVADASga0xEQVtMaBRADCkwHQ6BkXFMQFAiDDCBrMAYOoxr2kjTlCPMaFQHPMeRsmYNC3ZAoMSIQqagOsARgRQPDougLKDYUe4rite3xSpG00OdQf7WdEUWpfq97/sTVtdV9MoAAYRGIay00bVvcZEHMckLWpg/gMGDQRhAaGVIwHFo//uUZPmM04cXwxP7GkBMYXhxe7siDeRFCA/7ZEEPiCIFz3RAYJjuYYBiIZZiOkEzauKAmrRC5BjLt7/ndbrVA+ccJ27pn9P///0AEYdaHRlyduHkHmKbPxUZ0fCj8YgYB3mCmgOBgXwDIYRCA7mMHgSRvhpiRgMOm9liAM5DbGARKhcp8OflZygsbZhtsKCcDqQLn9JyittAHmbacW9LO1jlTDss+9d2bU9qVy2TsRckdSZWAwsLY3pVM2g00xPUM2iIejB8BtMOhgxAGTE/PPmmB8oGBIRMinYFAt4YUg6zSS4U87/Ms/3/26VOXOBFd2z/T/3/+v/3//tVCgw7DVjXliCPjpGM2IymjcOWaswnMEcMDMAYwgCjMDdF3zDcQOo4RHATMYwig8OQ9LdKlApQhHJKKJdri4osGwrYtRKbHob1D0kXte7S73KGX6A7VXK//pa9CUiE1/6VxaUAAMPEKMfZPNOKSMUWCMmWXEwUwiAAAiYBYCpgglCmEyDE//uUZO8M0xELwov+0RBGQgiCd90QDRw1CC9/QoEgCCIJ33BAXJDGxghYmlHiKKz5DIJYtFRTeWtZayz+eCzDrKrK3ev//pMCIAVTCyTVsw69AyMQQDOTTEIiwwRIHNMEkHIhC9MFh90wkAkjMCUEiQUPTgA4aGHcXiYCEpBM8dOCJZr6t7eGOWrQVWxhBQum96xVjllmn7Upsm2tHvozD4sOTNvFRzmxhTF4rdQnRlrBWHgFSj7zDN6jYAzj6cJTMBoTT3weBAPY4BUicYMBXZh6AGAYkyQASjCK09GXuCWigp1YTXr3v/PfN9ytjDyWpX7cRR3/92n8x/V9qP1/b/YxSnvMLcH00Zn/TZRwBMJAoYweZfvEYJwYIjKDBLM2PROnAyLUFv2WCwKu9P8EAHubKMoclvSgEA7rlxARTVMPQxjXf0a78gmzFu0Xfob/RUjXl/aIlaK2AAAxDPIxsno2KmswLMY3KIRTCYA0MJgUcCBkBvjaFGg+om9Qs63f//uUZPAM0woMQovf2KBFQfiCd9oUDgBDCA/7ZEEviGGB32hQchw072o9lfL6g4scHS5kuoHL//t////0f//0xcQQgIOMWGOdDIbibQwysTpNfLlYDB/wdUwtA3jB7D9MboGo1VAOTXQoyQtMbHDpXFnSdKA8CAiSNPKbMP0eMRYDMNUbmdhJrvXfnUuXLCtKd3W/jvX7SR7W5R7mtc+35d4m/Bax+f/jaX7ilFm7vYW/hykzHVWafvkgDAUkwvxi/HLGGqFWdWLnBiIgamKAUYDABliBC5hD3SgGtNHoJfOmAGoZiF2hpAo465jig5V3eIln7v9z/Zf///+n7zvrAABbv77GAXTGJT2MpFEgw+hdjy8qxMT4GQ/YwcoZY9B0QlioEUFTvEIcduHadLaNZ17lGgds0OclzSb2Sly99jqP+m+u+f0faO2r9Nn/79PAGE5CmOlIm2MGmigfHU4cGYegFxi8EhgMMS1A0oGUqk/UqTEAHepocCFqkoozhHoR//uUZOyM0qwMQ4vf6IBDwXiCd9wQDxwzBi/7ZEkXheHF7mCIYsSitbrp1zf/s////u6f+/9v+wTJVMYZg0ZoJx7mtxnWYNZHpurpqEYYuBOmFsBgYJIFhgeKtGBKEOEA5EwBA4AETDQKqPfDLAlRyuvSy4Im4nDzja0JF/tWmU+7r+VvVV/F9a83H+7eVdb3Z/V8PjgH0bGuf3nOffdRr/5q01slPZYk5Q7A9gL+giBPWZz+vk988OYMQswuQBAhUC4IYj5GXjL2viyAMU3tprZCAwrCtSWVkTptnq16L/Uz1ej/7v1//9X9z//MAuCMTDZjAUxF4wcMMjBjDn5A0Ew8QB+MDxAPTAkAIwwUsTuMN9AXTFSAsqOHR1rmmg7jwDoTIp6mCJmlsWIoWKIyIRVbrfrKZt1eq7u5lN1kPffIzZ2q3s7TOXtIh9DSuxO5HM7UVHdWRO12q6LMiotc0zK0+UaRUEDx6sVJAsMSssNh+8M+jzPMpiwxGATzPQYD//uUZO6Ign8LxcvZ4QBEYXiBd9wQDqAvDM9/wgj7BeJVv2xAGBkNwH+44GF/BCHiok/MASkkB4zFqeJSN62Cc5KyVD6vV6dvTZkuv//V0U4p/6U5MPUUY1Uo3D2JIkNJwbw5X043MLzAuDAsgD0EgHBgi4IYYgWAMGoUmHAA1mfXgpmxKAyAStWG+z8mKhgRAFjQWMiEc0VWI6DGMjrQsJbVjEM2mXFRomhZ76iVFLJ1z1qWSUBXNveaXkQisqaYaeLvMi1MuwACgZ2lQYWUyYUoMb47fJhYglGHCJKDGYmIfRKEsOcsDBq4ItMM5ytWs777naWUJXX7P///+///3U3UzArImM4HFI4msAzMvPbN68XojCfgXcyjJYxCCczx3k8LIwwwAdxgwGTOIDVCWhvImg8jfyuUXvx7+9dq1BS8sAhOxRdIAMrNLSRvePvWpR5MVULsCupWfWn1ThVOkqRkd5wato0hPSgDUrSfYq4AIMJKSSNopIumK2pzBkYq//uUZPiM0/pZwgv7KjBEgXhgd9sQDnwvCC9/QoDoBeIF32xAumyVsGBwEoclUyGSHOluxWCXWtz2F+qDbZlTWDSCVCqE9Of76fX/Yj/Ur/7Lupsr1vQYpwl5rYq9m/pvUYfqLBqDrnqYQ+DYGA2AQ5gGoBoYCWKmmBOgWRrhqY+CAQgOBQ10M5iTAm2j3ZReEI8VhRQaLgwBAyXeD4aFgKKcYaKMMhMw1+9sV0AAypww02nOJQhCC7NaK7FJRJrpoZcEFk1FZFL2AJRDM4kHOS3WMmGMME6dEwEAZgaE40YKmKJiXquqEOKmeN6WSj7+6LcQp4nCgdc1cU6uv/uV//o/9v+hFGFjA1CRMozAc4n3CzIcMnMRUa8jAtgUEkAgjAAQGEwJwQtMI0AaB0K3IQlTvuFA+YKowrK1cFgieoEFQmGkQabLHzzpd97g2ggRSITR1Axr+owgNj6KkTMlgQgXMxk+eZWhkfPNNlS4q8sxh1tq0AMAxsMhIQMc6xMB//uUZO2MA3gQQgvf6IBA4TkNb7oRDnQvCC9/YoDxBiHF33BA2bNYGTgwiwaTCAUAoRC0bAzUQBsoZEYBAcKjdI8VitYzDUjZziz/b/10/6v/1df/9nySAGGuK8bBiaBo7RMmLOXkaFtCjmDmAsRgJoD2CQAIwSAJdCCC0wcAGg8iAQmwij/PoqpFr1ucteGQeDQ1ATApmetKIOumX9o69L5hKBOanKZYkNnd6nMewyQFSLo0kJK59uiBXUhp14u1UGxyiiUgEADRhVi7GN0OIYGIhhvSNSmE4CEYGBpyGQPY2Gq6oHoEIFyzjMAVkHvqqyp6/4rjQw13u////q///9X6PQpAjAWwXAwQU26MJVOaDBdwW02SNi8MLHBMzIgeDFMOzSa8DygYTFcCgwLQCGxhWKKYzizSx2bzvA48irPqtLnZ8E9LB50tWGm3ZM4ivYLOe4+y62ucYzMmYycYVFLqSyAiNWadYqjHpDZ9l9MAAASzGGAzSsiDAwsTqPT4//uUZOuM03oLQpPf0KA+YWhxd9wQDcQxBi9/YoD6i6IJ7ZSgMN8CkywGAxOaFFDY6TBbqLrZPepsZVnrLl/1MTOCLMzX1//+uv/d1f9f9H41qzBkCpNOMu0wELxzNoDFN9iRVTC+ANwwMUA/MBOAWjAXhNwwOgCjNK0ABUu2eFMjW1CNoeyClDjECBySUHzqxAcUPIse9kaZaMS0lDyihpZpRUu06RfPGBVd9bR6I+TlWqIkGMVSEn34EusENsVYE3KEIJmcTvmDU+gYoDsiEZFh5R5iMMChHzEgeWSdGhBxY5NTKNSa9zd9gZ9E1Kf///1U6d//6+3/91UhzA1gmswK05HMokAaDFPwoQ5nEe0AQ3eYiABpgrgTGCCssZL4LoYAWzBJcwoAHEmKSy7jqy7gCNseUq2OnV7XgyjTdUvBC7NW/87dwy/7x+/utPu51GTnkdKHyafxXIwv9zTjeLZtK2wSBevgcf/qfFPDv8kAG6mqAoLM0UAK6wzSUDcX//uUZOwM818Vwgv9EVA84Yhxd9sQDjApCC9/QoDfheHB32xAJ1MJsAoHEKVxDLBDu/VPBCUu7v2QqeYtsu0YlAQfc5rer1U6f9P/vo/vY5qOv/9dFXRmGGBWaiwKRz20PGeUVmcEQR9GGNAQ4Q6ZkEDhofOB7SG5gUAi8ywDgqHbkVaRRqvuzje/zRnAuVTV5hnN95K5ezt33L9Cy2tO58n/ff75+g3/L5fV9G99g21FdhjuYqtu+LJqm3O6nl35X5AfJ+6mlpAOY7kRsFAmikaa9CIBg+ANhhZACZdsNxopenkDpHnaLnjCwtWgc3RmGvd6KLuv0r7P5D/xXX/3/+4AAAvq/swDwtDCZSeMTFlEwORoD6Vk1MV0GQxsC1LzNr7PQiQyBVNk5TvAcvDla3kTruaQiNAaA0IbKRkBqffFrqSAomITyxdcVYHxG2kWDOM3s2/TH1IRULPsStlNLAGEAsmcafnCDWmZKHGv2/qYN4KBhga6RiM2Bw4ta7S4//uUZPCMw6kXQYv+EVJBASimc9sQDfQxCC9/ogjxBGKJz2hATDgLGjsRuyQFEJUt5+5AtuSv///1Zv/T///2/9/SYGIAMmJlgnhlcRTEYbOKhm00PEBhIwLaYCmBDGAJASZgYYuCYQiBoGaD4FAgQDnJFSfLTrDP5PKtNZa5nCM59iQkJX8nv3//uVwh7OzelkLTxtTmZXmK+PuayAw36mH1b9K+Z10lPqNxuf7WkhtESlnZedH/NVRL7r2CgNMSINMqbKMMk/MjqdAdBpC4sQh474mKhy7ZCVQAwIFilavEKK9kB1HjC0tAlvfr/mPVRbpt3f//R9+5PdT9SklBoFmYRIK6mQMiVRhpApcZUK6qmALgtxgAQEKQABZgCQsSYMkBFG2HhDIwBwGTXEjVyYn7F1RKKbSQ8iPRCucj6Z5yKRU/55G8JyNKf3+flSCWptfD9flyrdR17QOc11D9qsBCotd0K3J/9LSLv/IZ5P/x+7oW2UFECjwzoWjnt9Mu//uUZO2I0vMKRMvcyRA+gThxd9sQD0irBA/saMkBBWGB32xAMwyxGXTA2A6AoCrgklSZEe2D26EwVJ+fBVdBYoyY0mp69Gn/3f1/+zo/t0fo/+tUjMqiPNToww4qTMGVEeTIgWXIwS0FpMOiWMQR3M4L5PFAuFgfQGFvzIEGGT4X5J9v9Y1OEZsx3HXnnHpnjhxc1cLki4uLoZn2NF4kONduYLu7TIsNnHXkutirMFR60HU0NMIDAFagZJhhm4Heg0avgJtguOGEaCKZwcCi5orYTsfOMQSnpLbHKe0J6ECj3VRjTVX7blM6vt7TCHf2f61f//nvk8vVAAAKBDATAG8waURoMUsLijA9RDo1XRmJMHYBYDHEeTBIQDO5UD34azMliqBCwU9BxtYXYlNN3us7nHPaiW52Zp0AnnI07CcPqMs3f/s+0KU3f2at37O7OGGe5mSqVL8L7q/+mP6WN0b+t6Nv3/M09f+P59CABUGmMzIaH6xoZdHFUoyYYADw//uUZO+Ow7oswYv6GjI6wTiSc9sQDLAzCE5/oED+hOHFz2hAChgQAIlwa5Z5BbVAgrS75ESdw2lwudEhpaF/6v///37v70/2W/+796QDWaVRwAHMCpCRzFSQqUwXAITNYhOwjB1wJ0x6DABDsZJb4bHDCYagEGAiFQMMJgpgqvTTNGFkO0pSeRWlzvL0iGx2DSbC8PpOxZ4lQQYLhlBMMELFGYHd2i0INUbfrNOPv3GJmXYQDNh5IdtW2RNMgKIWG5heXivpMUj04TR9gcL+PGCBMAQAcctvRQ0X7pv7KOAFaEwQGqMidwobV7+j/s/3d33L9O3///S66hVjhgKwmqY3YUNmCcgWRyXRb6YbgASGGyBSYMgThgxryGMGFmZWWGCBIcBEZkRBTyz0timdJQhUGPmIFSfDCv0PzjUTbU8uK109DryDN676N/zzv99tJt8h9BqPu4/AylrDGXKzUkJb2wX+22/DTcv02sw1ungBUYmTGia8jBg8cnJ4QUHD//uUZPGIw58MQcP90RI74Shxc9oQDfRnCy/0ZQD3hOIJz2xAMkyMBgQCyAKR25Sluaq1n9x696kMBgTPhiNgO1P/b+v/////u2EkYBcAumCAA5xhDJsQYO6EEHJOi3Zh0oEMYYwHpgVg8GG6hgZy4FZf1CcMAKmCgB0oLW2zCezoQ40t1zK/SKZtxUralDUiyB96vG8j2fz9mWtIXgz8jf0VSO7/9mdMyv7Avk7Q9rfUlhvtq75eln3B/X02X17P0GPp8c08y9PDuMAMYK44D0rzCTAmMoBC4IE3BtvLZPGUabPfrdMTWbUaQGBVr3lmIR//f3f9Kvd//u/7P/qVMnT8NTLyMeHJHzD2Ayk4JRDcMMoAyzNsQDHwGDTUuz+sOhoUVLxAChiMGa4ZPi4T34ZZXpLaUTlbWR+YfhmdF9mnqYHd9HsGth23//5XJa6rVY1w+S/t9mv9NtvfrLVVv6ephHP7gZxX/ezp3Jum+2SASGEaH0YaBDYWElNtRzgw//uUZPCM86ILQQP+2RI2YShxc9sQD1j3BC/4ZUjnhOHBz2hAeAUzHjkhTMlw3NLZ2VIVz47tHPGQtzFTaLdOS7Jyz/9//+z6KO13+tUZVMgBc6DZjGGgpEwqMJUND3QHzBYwL4xkDkoC8xxv82xGYwsBUBAczkeLN18KWDQqMUIhq2RpI4fmGOTW91raEvNJWVZgCHb1LU1wTPODLFxVtUclSiQCFrVlE2WPIWOGrMnJRyb2ghuquwQCmUspzG4ZPNGNuhuYCIEiMsGDP4s86silMe7apGxyqKU3dud/q/cj0dnsPdSvd9X9/T31qhABdmA/hYZi4J48YVUJTmlTR7BgpQPCYCeBYCAC5MA+HOTBFgNo2iEs8FlZwMSKLlRmCd3Luc9rB+KGBAQrT+k4Lq/KYRsGYD1oHP6cih27jAjY5/lmzhg++l+VDvWiHStuWZLmTIWaQUU6eef/yMxHRWv54/iB8+Kcla4RR80io8nAQQBjOTdPEg0zdIzIZgoM//uUZO6MA4MLwQO/6BI44Xhge0MmDPwlCk5/oEDkA+LZv2RIAsFMKCAhCQpNgIkhucia9e/9TYMkUVJRQlTL1Mp6Uabf7KNn3N6N35j30IGavbUERgGoAQYKwE2mH5mBZg0gcsYe04gmAhAsRgDYC+FwDswNQQ9MMSAPCIqTCwAQO0rX5L7NUSSA2ZXiwjjf3/iXPNvKPyZf59tpodQ/OJ6T7E/+HUJqU0petPvnmUuXOe/PxAu0+Yr84QuoxurDqL77m/9M3xmjWu3LLKruMDGYz5IzAKOM8BOcwSAHi47yCCCTVJZL+PeSEV73Um3CZb+lNVr3dFm1W3tUj9sn7Hf/9f32dFUhjAHAB4wHEJaMZ2EyDC6wxUzyla6MFpBBzHwbDFccTQoLTzwBxYYhIEQcARjSAaZ0Zo6gxWiGLsL6+Un51zG9uLQYU07guj6UlXudT2mT2tX53/u38Py+6bCPSoenO7T+6I39Oee6zV/+iLBR3c3/a78ehIBKExum//uUZPcKxBFMwAP6GkI/AThhc9sQDwETBK/oaIjsA6JJz2hIDLHkMon821U7jChAhASgDAxI4RnHXlldrkBjARuoJtmdyAfCYx9f//7f+vVV//aFUVYGQAEwG0DWMRVEtDCIgg40Uw4JMHAAjjGkNzB4DDGibjbQYTA8EkTUywYDkjZwqpjDsm7jSpv4bN0YMqX3Z3fIfvGYHv3n6a3e31CvKPaR35Na8s32Vfy0/e5DIPz6tP/fE7vy/3/U3/vPfEExATTasfNHCY30C0TCuAKDnZEAHKAKktKjscZLlMFySB0wBZskNMuvMt0t/+ir//Sz2f//QhUAAAmRBULAEBgFYVaYMgRamCjgzJrqJQgYS2A5mQQPBwvGEH3CEWwwShoAVpGJwHvwBggNhijzQEdZyeHKyTL2O8S6ecI/Oz2nCK9n3+WTnIf60uEUyn0ued+5eUzzyzyWbF6E3/nWyfpnnkQyoLUXdNp79MRZPnIACAEw+jTpzLNViA5CwtQ4//uUZOqI054awYv9GUI0YOhxc9oQjag/Cs/0pMjbg+HFz2hAXoeSR4KEUYiO/c51KO048oOIa4f0VjyofaBfZ7s/Zb66uc2vU36lf//jdv0lUARMEGBKjFHihAwPcBCOPOBpzDTAD4wNcArMB+AaDA/xLwwwMB/LBYtSDRx7TqfUtypccrf0eWJlIUR22bEdOwnLsIiOHT7FKf6dThcOxVf8k8j8fzWZkVOzB78DLMLN+4coOEKrafo1/LviDueMWqv2fRjoF3mulGUQFAzDA5Na54yoYTc0NKMJABcOXI0GJrB3OapY+7HO/d4VYi0G6UPOFCoehwn7en7/9ZKnQe/uq//b7fm/QkBMAPABDAVAgwxgkDaMGCBzDZpSjEwgsCAMdgtMAw0M20EO9QQNgks8KImDU8NTCSfnhv633VrAM4wdDFq3gRyBQZjQM14qlsVTGrCji21rgC8g9FbDYw2FFOirOlhhpYz2GClw6XQSDBEJHnpqqXG7RjSgIWF4//uUZPIIw7xTwkv9GTI84Phhc9sQDyy/Ag/oaQj7hOHJz2hA/V00xDBRFF0k6iITj16IW9OJT3M2CNZtb811+zoX/9qNP+z6r/p6evpBKrUwKIwxsZZjUGBmB2JCcLDQxhQAomLhgkAme/57oojjD0YFh2Q5ctDmhCIEoS0X62vbdiRdRRuLJPKDbxtr2NdEGpbVzi7tCTzHmgw1iakIHYpWilD2JOvBcBAK65QWAguyPIwMfNNhqeMCAHVWgEENgpVlF+p652xTFM6Grrb069lemq/R2/1/5FzOp7P6v9MwIwEPMCpAWzDjjnIwh0XsNbphqzCTQeowHICpMAfAJDAbRvEwNsDSNmLzKwYwIGODMWz3qJsPQ0EgS9J5AoSoQJEg54ykvABgVePXaFBdB5UVQZm5kP2ghUNFrmCnI1BoJECxHCRJZRmT5oWFT3MjhoPS65s4MqHqJKwQGyKyxkrFihm1S6fNYAEnoYlVUDiCBY05yjWscMkPgwAm9TAG//uUZOmIA2ANQYv9yRA0QPiiY6wGC2glFY17YGDdg6NlrshMBAFQC0jAaECnMx2D6Vaz1vJDTLCaCCAXNqYJ68f0++j4xstTR6HqFpFTLU+h0rS535n/W3aQ5kcdG0qOYreItmEFBqphdSpAYCwCBkg2GBoomP3Nm8goJlsoHQHMGwgidz71j7NzO0BhjnJOW9f1ZHT335W08I/3a269WvubyzfJuOt/3rx1YIpq+un8sTQdWM7W15dqcG4NiqLL7bJ5fc1PymHGKYS1phdzmeyz4YLIH5gBhdcwGsM4Q/SV2gWcPu2HRtEgODsMttQ/r1L/o56MLP936N+ijTQvt///aigAsMBBBCDGehhUwhUOSM18Z+zBYAR4wrFkDAEaCQefAAKYAgAkWnQPHasyX5XP5b5P6xurGT6/WViuu9LUyMj0MHn/gpU+kUPf6eh0gTQ6RIxFaUVvi+5X/E5KJ9HdHS/U2koX++0X2ayjTwOktGZ9YcjJLdDaxCEKOhhA//uUZP4M5H9UP4P7GiJFQOhhZ54ADawvBi5/oEjwhKGBz2hAcR6AmWNUpKUwagJgMTWIDDo+BlErmIDYhynnGlbbhhcgjb3/f/svs5nX/+pP/1VmAJADZgLIOIYPOTFGA9hH5p7KZYYQqBumOgkGJIMmeU1HWAmnyAGKgUw4aWJRG1hfxqV+wRzK49wxAthoSB4a/Ka0UM14Oz1z+uD2Wktn1CfCmTf153kns9nYNtfKWfafHmd2PTUw/djb+D/f73bx7dhpAVfYLJh4akBDs1mypjBpAFDlFJmJYPqwuxXga+5iUoZHLC0ChObW4YB7fu2WWei3+j7//nv7/9tdMZjgxVbTD1SBswnAFINigNVTCVgJgwJ8AvMBCAQDAOhBQwOYCLN+ACAorny01e9bu5fj+Fy2lpajucUs9aK9eD/VCdMRnXXP28Vf/LfqPce3+2hMo7bmJO905rxv/5F7vC0lhPsNO3yDbWXHSTN7nl0wCqxgcAGfp6YSBZtfBaiw//uUZOyPw8E7wIP9GWI0QOhxb9oQDjQvBA/3JEjmg+IJv2RAiJQnT4LGRCS+0pyeRhZY2qWk3tDwzybNCfcyjT6O7/X/6LtfT/1BaGUTANgCEwGsCzMU6A9DCtQCc1zcRaAQhONJCYWgsYQ8Qa1h2UAcvRooQTj2386EiYikIfKkLs/COOI62qo1I3aR8zPP2xfBi96eZ0Y4+Tqx8BkZykR/vsujHE+tlncyOlnxPncs/JsV8zIvtpzRTW33qnf9+g+mJdXAABPdTCEFsMGUG43PydTChASCEqVwzuGqd2thH9fqvzWf3vdQTucapordYlWz9/2d9Or8d+3t+n5C1f0KFgIMwOQE4MYnJJDDOATM4QwlyMMeAhSZvzIALjSOsj58ITBIAlgyEGDAsN6YLAASwCBOCLKr7W0Hp1T+diMmww2dX6528d9BWW/NVPQ1Puj06b86Wfab5xPOsU6IpYzGZNqan0BMnnSLmRFn7hDT0OA39tvUaugOqe6ArSAA//uUZO6I04kMQQOfyDI2QOhxc9oQD21/Bs/0ZQjuhiGBj2gIA1S3KqYwohONQTPlM+In4w+AdCptQagp/cxznk3XLipcRY5Yu7vd17/7rbUlGDn+j8Z+z/7fVAqgDxgDYCuYEePVmBHgyRplJiqYOiBJGKgTlADGUK6nH4hGEAGlpkGTD8DH/lG487ni0qj6p/s62PKlU2MhkRz0OzKxr7EejE5NXsrWZUe7obuiPvRfZvTsbtV3bTe3SuRmtu793cpAmG2SN5IWFkAeQRHB5Hqa3ZGgurGYH4FgABd4wdQ/ppsPUsCcy5chJ7qUshxwBBqzdt/0f/0e6n/9/7/t43+2ACX4kBOLaM5JEAxDR+zwU4QMQAJcwHwMAABqYJZYJhxgnGcCqZOU7gm1prNt7gogqSFpdKJUQKWl7Gn4xhtAsqyWRqDgo0VDMeFxcZeWCSSyLAXePNvXFjQWALlGVxbetrporMhBdjqgAACGWZJoAi4wHJgshzEER4HAI0nl//uUZO0Og+NaQAP9GTI4QOipb7gSDe1lBi/0RQDfBKGFv2RAGx2amFV5qZTe94wYabxrVhch+l7B/q3T9VXf/fVHt2Wfi7N937NghALzAcgHgxpUPSMJWEGDI/WkMwCkE+MAEAawuAOmAYCgZgywDkdg400ADApm/nbEF8yzwnKn92rkI86gOwVBBEkqGV9CieRHkq7ZcHZXf2hzZINY2szDK37fC26abg6+Znw5SkzMVeonIX5GaWv2GTQnPNaf6qPLGfQ+Rzw7pBywrLBZKVm08ZlkAY6CKJgMAQIJ4cJJo8ckF/kKrQEWaL1PGukSA+eV7E/0botv/Vbrue2hq9K3//zX2TBpGMCzUwowdPMEBDOTG6U/IwOcERMLhsMLxdMso/OdgaFgjTMTrARPNXvdsTvcvs4ZNkv/VQGIhs5Zehy7ayq0+WM/irTwKnqYa1faNT+Dr73J35NKv54s3fP/Lzpdpf5+1+i8vt03Xn8w93Pnh0kFR9BNJbT3fsjD//uUZO0Ig0QJQiteyDA8IOiJc9oQEAl/AA/kaQjsA6IJv2hA0FQcAmOAER9iL2M5YSKue50VkopWmhrX+n9HT/b+3/2e7VR6vvsrVQGTAUA9MA8NgyIF9jB9JiPRr18xHAxjA/A9AACxhDBtGQ4BuY4i2V8ngVJr+E0RiYcLL0ZaSFlu/plnXkOZoTytabZXMi9pxdJT41T872/kdkTJXnCiUS8N7lzLIjhdUEcOmT0ck+qcJ+b3yB7Xz7glgI6bj34LeDD4MKDhfA0pWNR4wcwaACAxNTQQMBqsSu3IzpwRUhFppwueIizXDI7QRDitO1mjX///9//Tu9P01J1KMANAEjAMgE0xIUKLMDUBxTT5zRgwZUB7McAqAwxGPmYGswrgYWhYCRCAxgwEMETda+KI3HBSlZTWdf9yKkNbnyS3U4/w/zmEk1lWmq8fZkw/+8/RBznb/viWaXadT6v0SyS8n+bZ//DmSfHpd8/MbnUIA3QAg8wzQTD4fNm8YgME//uUZOsMw2YMQQOf6BI0QOiBb7kQDwVTBk9kaIjtA+HFv2RAXGjirgZiEisLsW3vBlUWeE8w1YHDygaUKvqp//9213t0/2jde+v3K/9H2lQFwAgwBEBPMTED+waBCGywD1JEJomEeAqYIQLBgioGGIGDuYCIChZ1HIaC2XFO24iCVnhO1Y3Bfb5NxGS6eea516XbULc9mTvvy1CJ0K8KnGyI8janTKL3KO1P5rvE7sjXnzMyMprqnctS42SZzSZdpkaNicBad70/GWmemFEE0IwLTZKFaCBASI0lYFbwYriVVQA53vHoYMcARyhy2DbBoS7Kn+/1b/obs/9dNKX+zaUSdo/xJm4xYpMNyjCMRYgwLUEINYrDqTCbwCoxkBUwLDoyBnI4jA1YVfRAAphADD+5fjz8e3sNCNy3t9NGCJSR3p79bqgDhlDHyPufrJM4Bsy61unfkxb5qvzCu46srR93mt85hcLJ+e9/uZfN9t+WEa6AgEXf3VKU3DktCowO//uUZO6J04sWwQP9GUI6gOhhc9oQD01vAq/4ZQj2BeGF7RSQz2bBQpI4uEKoK345cs1ud1gila6bGXUUp/bf9vz7Pb/Ff/RT+q4ITAMwEwwDgDjMGLGXTBSAik1nAu6MIoAMzIEGjEQCDNEizq0IBYKW3EQAGFwNt5anKAPGCCZzLIhV3po28TjeyGZ3g7TdZBB5nZz0e+hUN8UMJ6CUhm1B+qpecJe/QlhLTj1Al19PtE4tsFjbY7hTfAOncLBDDeUoACEosqtxUATKqkwh1PFNKMKATQefkKij43OZSOq12lm8Ex5axb9kLv8d93++zdMGbPr09dX7nHf2JjAPAEMwHMBaMP0D5DBxwrQ0SlCVMDDAtjGoQQMExjvjJtuNRhcCoCA5posWcDgChtyFC0Nt8s96yFKezFz+gttHOEU8rfcwfAhi14XMa2Mk6vjP9WMtoEETm9wO4/oY9OO08mpT/8hzvGLxa9ax4oj+zKPCokAiGJdYyysMZrDH5TbM//uUZOqKA2EMQQN/6BIy4Oi5a7gQDyh1Aq/0ZQjmA6IZvuRIAMCxBC9QhphyZ1Z6lmCYGNDnOWSIii7u7urYto9lrZrztGmv5ZuiWYlTDlKv2f0If1X6YFGZMQcYWZaI6BiTDvmzZnwYBgZJgKgbDgFoAKFMFYDpALARUOZNP7Wxr8/er8ot77TyogHVNmDSR5Uc02hpza8CnWgKtazptZVAy1zC7z78cbMhClVjSQZAGTHJ5+ouBFHTu8QGdAwFNnNwRSmJym4YAIFoiAJ7iCuGEJXYyipgvc90gkVujsymv0eu72kdGp9/t3bL+L//6LOlltdAQYAKArmEfDmJgS4XsYdEshmBNgpRguJ4JBEyg005gCxElFssoYkAuySfsUqMgU00IMxhTTpmsZrLSmbf0kmz+YwYs1Uzg5lH7PomVdcxDzvLoTKbEXiD+sUZM5wklMylyTOrG7e917+xHmeTrYVU/Onvr3U8oE+jUwJSVQY1w1AkMZqiKpgUAMFx//uUZO+Iw6skwIP9GTJA4Ohhb9oSDIAvCK17AMDmg6GFv2hA3UAWbgxUXC+gO+1bLz6L3vdSkqt4WStp6+ptX1IS7/p376E1v1fee/6kovs5CBDAKAJMEEJQyDFJjE3H3Oq34gwXwgTFIOjDMNzLIEzjAARoMkgUxgEO7bY5TFqqIZ0WUqU8/rt1pW5e5lPfvS/Cpl96rdlamR/L9Lf6pkZe6fGNipEWTK2WYWh59fBd+A//+fP/UP7vAtcWSM/TNpNbnRUSYxmeRk5JoJlyGCmACHAKbloCuqR2K9BKOFWKwWJKNvA6gg8UCUtRkFd9r7v9iV/+3/b6f3WKBFZRICYwTAIzNzF5McwZY/nsVTHCAoMVQhMFgKMYIyNghTMDwMUBT3GAUl6nGg8Eplxq59T2OPzK3kbq/KnPEkVCG+bwszPytenk6G7Tj2HkXFXhZuSzEidsj/maQPCI1YjcijuDB9Phw51LecM34TNYVyhudYPDGCdLCKEPDgcw+U0Q//uUZPOMw8xiwAP9GUA/AXhhb9QSDj0HBC90ZQjgA6HFr2RAo0FhaQgLRC9tByYSRlV3UhYq11aEtWYskEsOa+v+z/9aP/v9f/+tBQUAKGAjgTphoAiCYOqCZmp1kZRhUIDWY0AkGCkYQcECRPCA6QNXkYiAO/FfU2Kqo2bGWx8akiVGxYvrV0JZTciNL3pwjOMp1oo2sqv5xLnySH1unf/5NzlJCLzjmXrLf5KxQ5Wn25QzSJmTauVHSG14TtmTJCMWj0IRvWif2jOBgBbHlYZkwxlALnEEjr28Pt/aogipa8kwUIkAuE4qyY0/7/rpZ//c9Cfo/7P8XgAQlelAEywACcYD5fRg3ABHZ0CeYfQFwGNZhQFGO7UbhCasjosyBwfnhJqW+PlLnFEU9EIOGFOJfBhnvAL0aJ6F5ZHSPppkxVYelfdsln1CqMx5cMiGqfTLgIE/6teSwhf6kZzpHzwNqHueYPrEWwyFuy/1KwBVSVT/JeHaYa0Z20pQOEFS//uUZPAMA9NewRPdGTIwYOiCa9kSDzV/Ai/0ZQjVg+KZnuRAT2CORFO4TPSSCPecU80KxdfZ36Pt/5RN/2dHXt29/eiv7gCpEkrpAAC0yJzEzA4DkP2dkEwxwMDJgLAoIM6pA92BAUBVhkPQCDqEQ4QoCrTaGlgmu35zPIq8onuxd0zekiMR1Ye8d9dqxGVPedh5GRH5yXMihkR8TRGB+4k/OpkOexehzet6l3Ur3PqF3l3/Nv98OD3QIVIzNDqlxnKJkYR0jIJhOBCxYaamRBUE1IYyi1+aDQoKNHUbGf7/9Eo5306s181or//V/0UbhCAHGAPgKJhsALKYF6ELmghGgxgjQHaYmhsYPAiZJwmbjiEBglYk8gsPrUb9mtnhS50VzIL9OJoxLumvlUN5wxEOmTLayBKLN85BoZtauRkuP7m2wXaPL4F7+REZ0SCAX6UbwZnCivKEWZq9KAEmE5CeqamHUZvwV2UHFx4pnneSMADaWpbSjZgswWnmwVVG//uUZPEAg6g9Q0vcGTozALimZ7gQDn15Bs9wZMDTA6JlrphQCwDsiiIrcaVj+VPVFeskxrQsgihqW9vsqfSLZW0ds+v9uSpJez79P3fSwZgEAVjgTRjOoOGBsSAcmu6ZidhoGAMBqy4wISxTBFBEPARYSwh9TLiJzzxLUuTBel7wM3RRb4TVz7mvaWlree6lZXbMjz3Q5yEd1r+pxD6VLN0bPkhwlOdZuw8sz+ka07zjFea25GVxItIWXp5tgagtp3WMm1YTo0THcDAznQsBiy4gOh/AA61Ems4lZRGbGaTxG4WnvR7/9X2UKrR5b1f0+rWqARSuuu7MAEBAwBATzDDJjMEQVEw+oljBGBDBAqAIhMRWM1sEG5vowswSBKdurkEsyaTnO8Pmcmh7BnhMXxbMyXanv9Lx6/Mq2mtQtfuRS/EQjhG0F6nJB+oakWfllT9EvHtspl/89C7Dm+ZOvCWUz8FHoUBAALQNNJIWbRWZeWbD0gYPgOWoS/EVx6FP//uUZPaMhBdSwIv9GWI4AOiWa7gSDn17BC9gaEjBg+HFrqRQh85Xe7Sja8Z3kH9P9Pv693/yv/q0flK7Lu2tyzBQHTDsXDx5mDO+RDha+jMSgHYwCQK0vjCIElByDqYzO3sKAfHsn8MN/dyo8unqxscVKskiETA8MKCPpzbFW0jeXEZ/ttDpcY3MyNXPwbHdzbkqmaiPPHS8nTzXJ4v6W3tEYsipwkpyIY6FWMIqxDUrL8XhGRGkdBPCAWAENXBJVBgDPGLKR5RXph0CCKbkJyj+bmdyv4q5BFFYzLNIYFa1HsJTCv3d+cqbou/H26EIr7BbwC+lFX1V795zAGAWMgcpIwThPzqnoeMVkH8xIHgUKzNlAOtB0OESXKVRgEEv9WnWVpuX6PxZEaF5rL7pEK83p/yama9O60vkbLM7kJuFaiJ6+tHRvPnFLjRXmzRQkxFaSdKs5Dd3WPVYyyMcXDn2Pu77cZEWbiUps7oqfPWWGBQro0N+FrEWZ/DOe7Yl//uUZPSAA5teQ0vcGTA0YOiZa7gSD6GDAi74Y8jyg+HVvuBIOpQ9ooPEIuRFd23+3+ux7I72dtH9eb+vorALBgDgGGAgDqYGiI5hVBdH30/uYngQRgpgLmA0BIYCRNxg6AkmcymLlnsT3jCq2qiIZ7FFYzmzFXX7C39DpfGJbSb2Z/l34suCRyvFwZXnEVUizhOewMk1PnpCuRQzzRATyxrlEfhklMU5Un3WHqzcd0U2pm5JoGOvrq4xgJU3U7jNkJpMAvhwTJQOwQl+Z7DVLlIPNGygmRvC7hohABxA0kOI7G62v77epP6n0f+ub/2+rb+OtHoAIMgoYPA+YjjUcycYbEg4aSGGSCwScHAcJgDYA0YA6FcGCQgE6bLTHpDAGiBXw92igvVSfMvISzwiWqgOGOb5HmIMyQYnc7x26blaVy5SLk0yzXJHQ7I9jHsbrDqHSdS8ypoZpJxnNv7xEJZSGIp1bZVZPhlTD9Q3VbJXF6AQCJqqmhlRYwNgAlDo//uUZPGJw2YvQYvcGTIzwPiCa7gSD4l3BQ9gaEjrg+GFr2RAU1wgWlYXeJZItiy62XbU73jEOK2q0v1N9GgLZjQWv7f7vR/+r7uoSFFMBcAQeCIMqMMQxbgwz9aScMV4G0MWRioEGd5MC6OpZAadwhBtwTpoW7LIZ18i3b/hF45zNZmhg7a6TY4xH/zSTIIlOn6nstc9tP2fQi3y/PdP4qa/+cmrEX6kWYuj/FdCoG9XHkcJjgK9hlpRRsgAX/a0kaaXCZp+dcPoBhlWtAqGRWCcTo6gylzn08fdKzlV6F2e+iz7ae/v+dZ/oZq6rO/7UR4AZMA5ATzCNBIcwUEGSNEBLMTCDwJYyAJS3xoeun+x0YaA6AZHUxyBofll25+q+p/DOR87vykrffI8z3aZIXxCRY0IiFMjrzPKS1H6pWMQ6ZXqMW2lkNYT5ytShoeRMrc7IxIczjqynXM4DNQwGygNKw02JCgr6OfVEBC901P64w26ZeR1/vlrnJphkZEa//uUZPOIA+xaQLO/GMI0QKipa7gQDiUzBE9wZMjUA6IVrphQW2sbFVXSneRCb5Rj9n+3V9akfd1JXtqd/W6zYBf63P6AtzC4KhiCJuDI/Z4OctGJ4DKYQhYYBBmYi2uZuigAghUGSpMLgEZqKARbeDDBnYwBLSNqrn0ttLSg2Jp7kdofOIUM3I7G8zTfQsdCnrvJDlMiddjvlFUluS3KGjGvSN+Ldi2NVUkNvs7b7nBvlp7sWuUlMeDz8yJQQUkjT5Gz4GAtmTWGA0D19NlJa2dVeaqtVe9zJ08Lio0YlSK+vZ9TUturf/0MVijUJUwfQli0fTT/1fIqVOYFACZllk2mH4SIaj/65g6BIGBQWg0DzCK9zPgOgcDKkkJgWBWMU+F3erO6K5auquQnhq0yyzhkjkxkhJahsVVieZYMj8oIVPB6lf0cToZZNQkwxqe6E0tJLKagn+MR41BNwzyfzSfYzSIZELIkhPD9xdIiiLejmgXtJLKDcMgWdMEYDXTX//uUZPWLA9ZPwAP8GWI0wOimZ5gSDwl3AK90ZNjug6HJruRITAwGFM5YQZkRlFf5IjS1rY6JGsMmRqrw4JGC93UoUQoj+b/75FVn2/qqR7271/QtVWPlUAgwEQRTIGHLMNcck0T8CjDJCvMGDQwiMTMlFO7AJEt7GnhxGg8qS04Jhk0zMoZtvqv/OPS+E5Ull0/qEsKSqMmYQvKuZnsELvMFh6ixncUTLkYJhqTVlq0Kv/+umExHFK8kySUi5GO0i45fSuSh8IxKbYPMAgQKyLLZeY8OjX1OFYsMDwBQUXegJJggH5aHc5aK4ucgMVbQOQPT0ra3/3EPR+3fsb0dWiHLf+/zNQoA+BApjDhQ6MK4hI7ztszDEDUMhjUwIBjOTUPvDEwCAl8sxMRAaiHwxax0RLE4IkyvLdr9TsN7akQ4pFZt10SUxRRH2dRZd/ndQtBmv64PdCEHESnOn5tahdfS3pkMmuZUoRdZbMzBmhSEz1aUJFmhgmpQ/TwkANUs//uUZPGMA+RdQAPdGWA7oOhhb7kSDtlvBC9wZMjmA+IlnphQzbgQ7SjUUOWFVFg8XvDiSg8BIZUE9N7fuowumL9zTE/V7/875zbmP+n+R28+2/alzwuAMFgVzFbKfLAip4HvkGFqByZABxQKjH3oNjCkODyvU9jAgHmHBZm9HP0irFMs0LGJjhvZAzFfFmy7ISJqpWkvxJtb5N70i5u/ELhtnxQUOd+vyqRdBrI8+FemZxZmtpWP6+fGzmjJf2h7PZ9DKZDVDtzjAzMYwMzPXy4IiPVY+YpMs2j52fqjHuqMNx73gCjNXadW+aX939CPRv69n8Xv0u/2qgAQipqqtrXMXwAkwHQMjpeL5MPQAMIKZgoIGD8sZfD5cJYV+isJycmoR5TblS4JzXUnX2hQ+x5tdTyZc5Lirn6+sz+Q0b57n5mjo5Ekh0eZkefvqbRT+MbmRFxJ545zr2Z1FYjZCSMaUG61DONtHw9hPMAtLIMd0MQNPwjOJiNSyaYKhYNA//uUZOsMg7ZcwAPcGTIzAgiWZ6IUDlV7BC9wZMDLA6HFruRAHNYqAJr49qrZBle9obDi0que1rqR33f/94C//bqWn93/ci7UIBgBAA2FQHgwgUOkMBtA7DTzw3Ewj0A4MUgPMBQuMdYeN6QHWFZkQACYKAk+uH75lzss/dUlMMbgkHlNTUvGS1E4zC0IyNjM1Ncp1BVOQ6lAE3VCqUw5McZNadCtsavhqRqZJh+z4RXN1zNYCTw9rQiUKKJvMIzHvTDZyZCpkJJj5zx3Y0SEZpjkpxWIfcxaAhiQpa0IZFfaXLCWpFzAqeijAZbXqvQlGvk+LWlmttiZMWlWzd71OivfmdzJKY+Rp91Z1i9LqjARAYMBoFEwNysTDuE/PXpyoxOAIzIYEBw8M6ls8eCB4AyNeBhAByb+YZ/+E7r9EsM+kXSpVCYyLnYp+RrDzsMy3uGXKnq+wv1kbIrn+fnCnd5ezyfLm6nULWlUL/vlO67SkRUyMv/kR6V7mmfO8MOO//uUZPEAw5BWQkvcGTI34hhha6IWELGA/i/0ZYkDg6FFruBANIiQAjXPsn6aRySxzhKQjAoCV3RVG8iBqfQeq51qWY0aPRYSUxCvzHzn9RVOrY3fdX9Ox9//7t7BhFEzJPejDyCyMMEVI5AY1zCFBSMCQBNJIwMRnTDVAqAwCqYU6PAfz6WPkCPh3Y9vkMvZDdbJRXCNOuRF+mOjHNqN+0yyt19zPy27grGMr33ufOParHPZ3bDw9LTbpemXJsvyZdh7lbeZvebFgtwvSjmynk7KTxJcEdAz3oswFAxnskGQfTOGpSY8OvRtGDuWpQPXuytGv2odGfR/fzH9fs6qv6OjXQAgAYSGqJh7hzGBsPSbOGnRgsBVmAKBsOgYmAAUaYMgHhd14kpQoAdDokxUS4bU+IjGMyqE2KiKhGPbdXV2MkpNs4dM1RTnHqRioc77EYhcUC1xVhNtO8jR1jh8dp7yATqjaV0BtXeciQIefcmwuAfnuaNHkDdanAKADhKq//uUZOaMg55gwIPcGWI1wOh1a6YUDkF3BEx4YUjQg+HVrphQmzbmE0mbunFgDVVGligZoGV8IZ6o9D+ciMhdImljb9tFdky11X0tumE/RyimITpdtl6dqNbdr3wKb21F5SQBQwdAYTBWHsMmDWkwEgngKJU9zKXtO0ApqLQ1hAEQYEHw/mFDRKC+7/T3xTNYh67FCSzCf0SURFTpeQKT4c28/eO2pjldTI0PrRCU9XqepqVyYvKZ7GVL03C58vk3YnCXM0OOzKDU6dKKGSsHhbdWXGFgclgUDky7zCwAljw4KbK6WL+ovpZWdY75WbHjY59sM9HqnrFu2Zfdsd01PTfci0p1fdd0U+x5LtVBoQgHmC+AWYJY2pxF4XGGeEOYsFxhsQkUEA9hGg8s1UwKI8KvX/w53c5zCAwdtr7WMcueReEnU6VeseM3g+8SFVI9S473PhJQRaPtced+5A6G9PSHHpPnFc7ltfKm8XPpmHJKvzJck0MkI2SEX9ORx6vA//uUZOyLs74vwCseKFJAoghVa6cWDjFlAA9wZMjuiCFB3AiYGlwglrvEtQzLM8zj4xLAdKxl5dwoDEHHHCXTZTdGao+MK32EmLxzI+tvLOf6a0S/p0RV9mLue+Z5tPnit3VEcx7kBf9UhgYA1mCUHccwjtphyAjGJwaAAIYsqBtoTltnBeNF6fI4ZLBPYnwGpfJyZOUM81KsxdfNGLmTc5MvB9v9a0jPyPNMj9oXfIz0fIpn59O39iPLq0jz+v/b6J5XWVOEL270cG9DnlNV1MlzemiKcAh5giYKFxM91CQGkDQGVAVsz0Ud33V7czDDzDEGVIrUt9qU1b71/37f+j6u7/o96gACkqaqmWpnGC2HbnnEabkYXICIkB4XSBArwNAPTOhM0LAK0ipWTYX+bW1SJbCII+4xam8HjmbsmuZ1sdZpFKq5wG0NmP5+Zw1YguRak7n54xLiNif0JF2Q+HpJeqhFnl0MxN1t1Qub7F6Z0w9Jo2W6H/2KveZlYC6P//uUZOeM06ZewAPcGWI/wghRa6UUDU1ZBC9wZMjWCiGFnpRQsRREhaRZeYkDBk4uQwPevIKpENb7IipKXOchttW9bbPH8km9H7bl67Kq7idLv1E++wW2BCyqqlvgMA6YMoHZhVAYnoUAkEEchyLMOBIyPpDh4ZW06K8gMI4tz9/bvdnv1AejEM5lNnhM7C4pL5kiOLa0oKXa6k1c8M4WB1SZWnldzPtGprrUDR41IoDfpqosnbkIhBF+0J0co5ECQTOv6liA31FydCeItJQWDfanZTxxKWl0gFKDEhO6FaMMABV9CBGCREC8Vw3OlyOfz8GWtWwGyT2RTqFvi3n7vs81F3rX6Y8wJzL9KbvYRIL6VQCBKKiRIKBjRD82mWOT89swyQJgEB6gKMDkEAHEBJ0u0z0v9XmhGwX7ZmqFPdP0+FkppnMzlyqyZgmmiZRYnXPI1KUvLzfp01pOdvIz5q+8IqxXbeqT5n/0vVAQUboGoeRUq5yEWDF0sAJAAgBR//uUZOsIg5haQkteGMI6IXhQZ6UUD7FrAy9wZYj1CCGVnoxQmAjHDC/RyLAZhWADOYk25MD1LaYMZ6K2P2Ld2bV0fY36tqv716kfp+MevRm0ysL3/T0+1IWlkUuUWhMCEIwwVBBTiCbiML4EUw2CAUCTHsVNwhQvy/UJIiDAt/HLHmHzn6M4isuZp9CVMOMPSMjDgjWeepgzMw7VSQ6f5RqUorDUjuWDw0g0Iy8vQX602NYZqcL/Mo7SOwIqSSLaUvoh+lSJ9UrseZRCOnsYMy8RoRoCgxFasqekuaVBObaySKL7TJCNZy0F9Fupu3UZureRT1+23/dRoZW2XSf/7P/kHf/SAQJJlFaS9QFGACDiYCYhRpfulmCiCEYICaxwTbTBoWL8tOhgt/0MJD2WCfZfZC8nI4ZLBUkKKn91BTkIi0uTYbP982M17u5J68B54I0xJsKyOk8JTpzpkedTMjylh0yRfe1+pluOZDWErvS1OkWJTvwTjxHlbgAAQLHD//uUZOUIg1ZKwbN+GMA2wXhlZ6UWD5GBBS9wZYjDiCJxzAiILPhAgQ+GBkTggAHxqEoNxgS5QLvsrd/b9AVbrMV76/p/q/dr/b10aNNFSE0XpfPbVgEFwACQBcwSAWjARGlMR62owGQgRALACKTHXZOAB5pcML7MFgKXp2wIGA6kDd93TDEhFEJiMilFJjRZrIZVTZLU3M/JkaSiqdaAlNwimXBZi4uIJmLvxCYpdCIhTnM469azquqxHi0a0aC1PjCySEcLM7k6B3t4OxKkgsAFqajoYMVMzmv8MEgNrD7gU0FkTaWq3XLyFabr7Kb996j/TQt/m//6lM9CUO5CypnYh1KaAANAVIZWSMAGGBWAIYGAkRo6QkmDMCwYDBtIY4QoeFXalc6TCCiu7z1hYznv+HK+/766KZo8pKDpw+7EayP96rAzmXhACXEBAbqzHHMuzSDn625oZXmWeud2oJlU9tqal+VgdEj0juELNtmmbgiUVpA5q7sH4gEITOdE//uUZOuAo7FewcvcGTI1woh4Z6IUD3ltAC9wZMjPg6HhnkhIGJgYHPUNmDgBKXw4w0jDczuV/cNiNmLLJZCUqAL9uMxtBJfdhVn1oe9u5yBZNVR/FGzOro5YwmqzHZshAAaIFwCzAGA3MFcQs4ZX3jCzBTMSBIBB8ydADhAPFgkv1W4LgGmEzixZWOqjbSLCqRUClNusfB+mgpKwuxwUgwJ6pT+MNPPDUAO3mS4/bny4Bzju6HiFvQE+y4RrZ6j/EmIC8dr07kvnEemle1cC3mEEtgYzePLzv1UhIW1IPOA6QOaVzRMntSVTOf32S9ru2iK2atCkK0dEUSubkJRvaP6+b2a1Maj+h/RV7N50VAQAQYKgQhyJJXmGAA4YlAJgQEAy4GNBChKazFw4FUdn8cu4cov85TPZqsm7kRAiJFpqXMtp5sTYHo6PTNZZkmbtXVoU9ilrnlS7m5laqGRX05YZHqX6Nz3ZfKsdfQmLypseZlAiFPoMjipwhHe1UufI//uUZOuIg69VwUvcGWJAIPhVZ7gSDiyRAk9wZMjnBiFFjsxI2mH4nkQYkQ4q1uoShFBtNVQO57Oiv16F326Eet/ZtT4ZZWWz22wxWK2Vi9vn6WPmKEogHqd29VztoxDUOEjBSgHlZ8wknDRUQ8ECYA4BhgOkMGHKAyuJ+HxDAQKL+OQIT0gbOIM9aIUs0RoyV0ev0jgfKIU4Il6PZizhEJpMJSna8QUL6ii2X2Urax8JVQjGMl9KePm0ilGz2bMtTVsgm9ByG/kdjHTnEI1JVlCA1FnBjliMigtLRilQCg49L8WEJtZcSgYnmtA/VNm6dB23vMMsQ0H3b36BRdvV6B3RV036v9j9r/fsbV6KoooBAAF4jAgEwU3NuhzqQMuMPQBUaBaMBAAAwSwug4jNtKFqiT9bndoBm9QuUyQjIiq/wn43WPO3bpZ1jSm6dTbvC31szpSa3JyVflDwA8Yb8T8OP7/6YOe/iTiG1e7SWfrHP/uLJLSk3PREwAAgQCNR//uUZOiMg5NWQQvcGWI84phQY6cWDxGC/g34YwjiimHZjohQEvIFgnOGmGCApadJlhitKYN2vQVvfaP/nFXa+yj3z1/qqv10ym/HNf6/2293f9VnsWADKSzVKoCW6AQEhgbhJGwKgCYO4DQGEWVGPuJ0YEXxa83EMLpelyc6PorG2/O97S9j30Qivg9KaF5kJapm5KeZHqa6rYjHqcpP0NbiI7usC4tjh4L0dMLoiR5pTsHc59sT3z4X1ytZM5h1tW3iRIRGjMBMqEYsOa60tk7M0MgFEpGpGdKbp69R1prdOPPH0ftvf0/KpV3L6Uf/3Ee/RQPWwCPZrqPrEmSqqoSgYAKYE4DxgZB8msE10YLQIZhEMAwHmD6OZTCydMPNJBwYh0UurwOz50mIKX7mEJ0NxkzwamzqZmelR1cgxFTeQ2lwZoHhfKZkWUIJ+h6FrWhzyTV7SLKOHPG8yp74JXQj7Z5MFOUmd9yhktIGDQWqTRVGGq9rcFNAAACMJqq6//uUZOYAg1I2wKt+GMI3IqhodwUiDbDVCS9sZMjkiCHZwwkIrR2XkFzFEwWSvNYJN9m2RpqQt/JRXt2N/r77DHO/9wn8UQny/sS+xSu71gABAwAMAIVBxiQ8mDcoZpdepgOg/BQBtL8wFCATCoAgEgAWLLkCgAd6/qvvLHsgwxwAZTUnLQWeCJHCAKPvVNj/D4QLi3dVNxxLJ10IgZjMTCmelnTNCpEVHjuHcebZ2IcSTKMRk8FlGzBtKcmdJCZwaezIcL4RF2u2jIdbxl5O+zn0BEMKq1ZIoaMCg19u0Ejt0hI1bg5yg3V9TdU1/xriRRiIt7nbdn2fT+Ae3xapCPo1K/+c1QAQDGU/dXRJsZACxgCPJq32xgOC4MAFuzGURuJI71tKK/NaeJ57Ss+7nbdDjrsqIryavV0ux5DuiPY1T27y9zaL6Kkq1u8tyUdkonclUdTorjmnIFe8JMASjSySmj21lcWhoAAkRCQTCnR0BvTtd0DAu4j/q2lAEHZR//uUZPAAg+tdQTPcGTIxgLisY48QEGlw/w54Y8jPCiHlzBSIHZk29dW/gxhHbQ/u+AOi1b7037P/7fXqpu3+tiYAACMC8EkwihRDk2qPMNYEwxaDyyhlhaHZhGWadFsYCDl7dAzSFASdIKlvNZP5rJLt0X0Zu4ZrutGNTZ81++p63yTkbQPa9tffWbmVrzORmEtyr01zfV/hea8dBLu2KNb9p+9fYu5T6Kc6dPffeNPznGCZj/WRLvaaLLK6OvT6e5mxiKoXMoQIwivZxY4AQfSgYsLG3liiiTIZUC9LbdOo0BV/aXq07yHSMOjX7Vf8khPi7O3a67N3IrRov6VqAAKFACQZankGAUjQU5sypADQm5MWIDjAvo2YFWdFYkkTomOm+KvQhE8K10y8c9mrlDn2f/wQZkv9Jnid3M93/TT1JxDWpCOAhYTpSuOcpplVt+7rNYWXlO6bWcnw3OU78aGXPzGXc6Zm2zuS2RJWlANHepRDwxrgg8YB5fr8mPfM//uUZOsAgxRHQ+O6ETAzAoh5Y4IUEPWG/A9wxMjdCCHljghQbQ761oMxdd+Gah5yza1K1PuYqh1aJycSwT9swtFVvCtKmVV2RJpX+9aAkIkUW1MBECgwVAxTduNNMLQBsMA6BQBhgTiJGASAC1p/XaJgDrUtaGba3+nNphQzP3Tbyvv5mExHu50TXhbG/MM7maFTM0tO5kTJa7nws7iEddlIn45ZHubGHyiIe5Qpe+mfXhmZ+5dKXO+e9ITDnErJ45HJq9hyj2prq0UvwAtObDAiAL3TiTzzmxHd+K219dDOnaUb7t138bZ15O5/u+nrVPN/43uHVQAAi992WVEAUweo8RIUwpApMV4gCEBMH0VtVZzEjLV1b2eGDeABc2ZckBahR46nvYwkLIGsOzTIpuRE0BWlXSy0MoVlt+5DnPbYSvVacGhAdWAIQKKsrSx5O8hHhtmoA4BQDUMMlJTtqZfztrPgrnq1DEdaLxHorlZNjv8nb6p3ysV+k/l7MW7V//uUZO6IA1tZQUvbGTI/IghAdSJCDwVdAK8YbYjCBiKY/ghQdn/SUgIe6mApQ/2fFOua17rj+M9td3+teFuos+zny/vrLGyhpuqZBVF84eUgwIA1kUAiE43IFSB3SRkaCtRa0mujIivalmGqshHOTUMK2gVt0DnRgq5iH3JWxgoKHWH0JRLmYUUu88adFxZwkvLTSmtW2w8vUrZIAQ2Cfire+jpt4hYdtgGL4l89EFzVTbpMK6ZbCQqFz4sVS2n6XY/NF6T5H0pmZPhZkZSefMuskhvIdJ2x8prOUsz/o/gzmIy7GISs0YAvmRgCslwp1Vy89rH2tpUBjzqGFglADgYbQTRHJguAxwWam0LgMj314+bU/TX7wque01BDZxxfxaZdEdj5+cPcvnf/OWU9TL3LOw+IKPPoO/V47+LZLXmO3rqfi2YhDlehI/xH4gCASUwUvsYDY0pmERgA8coCsMWJRW1VdjZ8tN9Yyl9FFOeVFMJPz1RTThrDjoRB3Pwg//uURPKIAosPwsNdEMBZAqhJcwImSmxlCK7gpEF6IiBBxY0xO/3fPVl3IpQNdw7hLjl6LtkAmHT8t5etUTdjENv7ajTLn3Ux9PhpO5MIC+ASHYE0JD15rxVBTQWosG0RT3U2BKKi0SqOnmpeIgF3FTK/tE9DZxc6S3cqfpbf2/efN9DoGPYKnzZjwx7d/po/2nRKkpVrvz4afqZt+4XGaPHhq1KrYhxAJsxJUGdyiVlUVp8FoAPTah3pkUmTWIiqhcVWcJnyLoLrBxrLUoMWKsFRaVbNtM7DDNR5dtrc8tfQ8aQAqXzyUkCB8UF0qgKf/VlTUBGCDTfXU2lNOKB0iGBXiRHZnZIYljOuGUIMRcMagQsoTbEVRMvJJdbZPKNci7ucQqx5MzYWNS3KO8yIlJsqXJTlJqXMuqfrJenI9dsvdU+ZfnLqqKPXpXUX5P3cXQCkwOOh2YiH7ZokFysPGPvOLgs484/LKetM8xo6mw69hm1HyKGewfMp+QCZkhEd//uUZPIIgq8lwakcGDJZwpgld0MmSrhDAgfxAokvCmGk/hRQ4ucK7ak7xZHs9VkI1iZrFzRUwqGSmvTznDKUyTrQFEgaRpWrS4UBqaoVqZXIs+SmlKCaBBoghrrSAIjdpDqMWETOJjyFS9snXSlWmyuWauxT4srPDq0+P2pQK4V1p2+lrE0WWudSu5ruoULLQRT6PBWZG07aNKwKFywt0UIzpUF5UBEdYlnmdulAZOVG6+6zMjlFl1x/4u2aby+F9unaFKsyf+mfB5oP4atnbHvP6mEfvtN+76vnpafGF+4liLMm/xqnoB0CiIcGRu2gkgOYRee/G1VdrboVfniP1AZaI7a6qGm9jaWIqG7sRZiyllKq9F93lKtn2VvmRfTk3MebTPzyt9FNGLYjBMhnzZjNSP+Ue60y9MmO+fDJJa5mGb1gkqXM42VfUAACTNVT+m4jgCNRR1JCN1BFgbSBLAvwGXNspEfcfhP0pPJSLTuyUuf1c+N6p8PPNKfdkth6//uURPcAgwhaQauFGnJl6OgAcMNoRvgXFSDuIGFTiWDVtIkJeefvgsuVqp8lJTLZk+nt+ly9PxMgpbywSDwrXXUZJLnBQuw/mE6lbBASETe9oDAe/EvTsc97aBmpr5pMwmcu3FnMCIkhGVF9oI0O9hTmsHxLiidrZ3G37vCJ5T/+5SmbXSj3vXRsT2v+jXd9taNl3WM1Go7+sr7Lk/AysfRGohJ9p0PAaFTutYDgu6kaJCoX5EzrIpUid1VaZwuA2mpT0FyGK/BCDueffkm5zbu1OANj69Lr0pbyOpfwR9PX7rJ0dLbif/lXfyuclsP3x7DJia6q9rk9dIfPYjUBBCdsN8j4c4gY8FHnkDclvmOUCceO2nBFe2aYLOu6JSFC/mtfr7ZZX9s7Xfy+5qa5/XaBrlmlp8hVCi/VeZcIrSNSZjTCmV5t0kMqrsGyofffkXJ9/dDt7p7Cf+84AAQgDanUTwAAD5TdFhQzdyAr4NJ1t5QnEtD5pnENnpcP1mO///uUZPsPgyVXwAOIG3JbCUhZcwMiCxxlAg4saQlrjOCVxI0JWM5kr4972/+9d88cm/kTkJhbek3rHJH9jW9f+9TMtY/zmMbT3ObGRp9eQz+Lut+euSuZ/AAIUD3C4FUCPnaEWpD2u1dWob8FoTW179suQKHgvbBy4doqzkg2cpuz5rsnkU6kPrmVl6fOlyHpI18/45aavhJqa0jOv3OtNnbu5S4yRBAOhWN4XUMbF8tYB7JpAMZ5cCER+EaFAqc2mCpCN1LrUD5vczsGpPnzP4+ZTLjaTmZia5Q8sfDbaaZv/y1//EWv1v/FIr0dmfvTTM/XqwTbbw5+fDS2V5FdO+dQdyT789bzuszqtUq7gsEn7DIsAxalY21JVYCvu6owtd0uqjkVlOSFPKeKRAzbwqmcSUm07g8s1X6hGrwlqGOf/XBZShXjP15i2phF+WpKvl2CsX9kQ77Ys6r47l7Rf+5Dynf0EkWaqyss6R5OSEBEmRWRVJEoSdBXYr4duc6+//uUZO8Aou1CQKuIGmJYopgocyMmS3E5BwDsYMlakGCVzAyZzH6GR/f9UKi3NOBP/s2V7Ccc5L+/kvP/MTo94n7elVmz2M9TTve936SFYj/ux9E50xgkGmspf9E3WWSOhj+m1dHLQOjeqI1wuj5Poj6WUOE/DsMecV0gst1FBBX2JX90Bb9yd59pvr+4Y6KorXgzebL1Cv0vhTHUKXeqi/v3937mq0L133SAAEgslmmS1EYMOerlCqW05IE5UUzAWjFaSDUnXUvIMJMoNzE7m1hR3lBilqii0yRcOzkzB0esKLD7MtXGAKEZdbRU6yaRYx0Rgg0Walp24jWgWte5K3jqADAAg9YsITbNbJ2bmMFwecQ7lo30dUyec/Sw3+ghQgLJKISLlI2KtUQ8isdpaeFX3SfQyLMefJtCjZ8jOMEUi94EPLFTAaIHxiEIjA6yVTKShAJTh0uT1gqPiIDowPqhndcM7MVNnXLJLK6kozLLmpYI9drIo75i7soxEhwi//uUZOiLAs8twINlGnJSwPhWcwMiSdgnBK0wRElcj+Dhwo0oIdCOxD5UDyXk+Oc1XQZ+Hl9Jf7f7bNbtNZ892qPfbztzpkPODqvafzeIBBgVn8GtqKG5rEK/s/izWDZ1zIJvke6Dbuiq8BxzsQMZwGMVeFlxO8znzXIm9snMmM4lTt4adItLPuRnoTyDOxxZHu7Q4hF5kLvMrvDJi7dnlzpWH/Ac6nwJhE0/kppmSLnFA8ZHlqVEToFVEmUQRATvRzgjSM+4kzIr28RiOn3I983bbP3tPcyLHE3X/O7w+chwGSm8EKDmt5z/fa1WOMsI7SC7zPRRToig+i6rtL/zchgGlTVLTF7CnaCmmcCgNPpKcJ0wpojYR71FkkF8i0BkmeZGFuXCeL4M3YqRKrKiucctPrn1NEosiEL5RXOpmdy/BisHtL1QoDmGIiW3Zywu857AxsERzJ53TffU7NKMz37VAZj+q8CioKabfK7llskBKW/v4rikTbhxMJAjI27F//uUZO0A8m4YwcMhGlBZ48gQcWNMS+VhBsyUacloGOBBwo0hG/mB/ODQ3qj7ui3W6XIVbXsVGYrnTQt9mWW/KpmTqjvL5WQlX63e7oO8DNW1pgkOFKllrQzLIBRx+8H2hwDxXyv8UdkzwMEFFhGq4CvfHFiUdcdoepw5dV+/c+vNuWF2c4RWTk3YvnZmZ8558Onvn588QtYvx3at8OXoMMoJsq8hr4Fzr+wOvRO0E/3d2s6R9kBB5V2LAD9xgZBFX5cuRf6dmKYvt+AxTSuWUO8axQlnOqW6XzZkUlSvVHdCOejp9Jxzu1zP4H/VuSc/o3X+Rqt5aCCT4UpY3b33blnYHW5jvYwI3PJVAACBEOdfF9j3i3zO4Khy5karptmoISWZ3gck9Ft4apyncsKxEMyXFb/nJInrSeWd7w5XZK/OHRVvLVUeRN9X/295Dxl+jfEj5NLC/q5oUlBPyXMyU/oD12/tdr52qQq+c7LlxLK3UPMuA4cyajzptQKR1O6y//uUZOqAgtgrQANxGdJUybg1bCJuCujxBq0gaYlgmOBBsZWpnblH+mtsufyYCgkY83lH25KI4MxI/1/f/qecSznO+IV/ZB59ZP3Y5+ru/fMfJrL3+5JL7fcZQo6eJdDtb2ytWoNDhWc+cKA64hsDG7TvocPSr/cqs2sgSfjpgV50E3mCHi1caU4mt6i79gesUFL0G87QiV8AW3xEp+/d/v7EXEOg2dPGy6bh9QBAtWarJQX2lADJ7KS7HmsHKlLoc7vOqkVes0KW8WliLc64xtXdWkZbtq60f2Kh1yC117J0d0eLFjFR/Kf6JPtxRrJjjOAOoi46zFqm/S9v5hbd+UcA1QQABu5KlZDdxZq7psqtHeBFbhZtLC2tv3QgjpvSJxJ3cfbTtpmin7Tp2KcL+/mbwypZL3f9W8LSNCK3Moae450zdjP51dSs1MEiuvnQlaO3o5tynGtze/k6uY53mdmwqAEKPeUNSKVHfn6op2+nbPDGGA8+lpWycRnon3jW//uUZOiIgq8awUNLGTJTQ4g1bGVMSoB/Ag0UaUlYnCBRs5UJl9SFYLzBHxL/c5IWrVKGXN1eHbeO5lmbNCUxvEGdKJnySwtjRvIvcHbDjKmS/djmb7bX/IzCv5htK819lP03ZMyATW6FsYUqles4YajZBd7pZlH7MkoWOW5Z47WilZAnZ2xnJDZQ6GkPffQ8dWma7Kbw4TOiIug69AnFjqEyQRaZ4xZqFDqg2UMAJCp8odsQAQAGC2kdTuUnFe0U9OXNkocLuRkGtfGd/TK7p2yWKqzlsksaSV5mQFEOdlY6UemkVkJFa38iul8l4DWAAAW8pYnYaiu0ynqDg5aslDoSNxvKfQfE0cg3UbD68rk9e7D7vKdQ1E80vN2OnktMdzQp9vveySq3P+/YQdkv7f3Xlv3uMLiLnI2M3SmZOKK36wmGimlqM9Rp+5F0zG5iRJaN1iDmxmxpE854XrLnMNQU9WBEH8IuYehugiJVCDJaF0lWC+vRHRUz84Z+hxxl//uUZOyDAv1cQCtIGnJeiogoD2MGSdh1BKyUSYDqg6EUPTAIU26WftW9UW50pfQavbc5Fqmp/Js+531XR2Wou5DPotq3Mz7NrZljv5ffTWPd3wAFSQm7yPyynae7Aax/w03c4p517FwlDPf3cAQiJm+iQgWUmuxKZDTX0SlZkyuJVvus8E57mUUuAkIWsdHJOGDWIzyEjtllt/Q/mkN5iuGy2tBqYs9bA6e9Puf3RnEFzQ8JTkQMShB8zIjUS/tIJEr1jKuGMWAEEUiKzO5AZj6U5FC2NithqdXosO/T1P7P8d51ORUrob09fHmdESIaVsM4RvLIu7VAEU1lZbeMlpB+mEX9Ps1KiYDu+ES2oPb6Il0JCtekyBp2scHBCTgKQvUYlM4Z03ZiuF3N4tUigyCxfvRvwJpxTCmyMgjSaFXVWfE5bueJfQjhoFimClzY94UuGbQilCNu6E/hAy643BJBHzQAmsq1Ld5uMHBjIHYQlqyV9vkv3Ps7baRjyJHE//uUZPaPA9liPqtJMnBRCPgQbWUmTQmK/g0ga8jBg6HkHJQIXE6j6ppMXaa1rAbA4lIdNLPgU+KBaOecXkp1aaupZ9BKG61UCjbq3oAihAABjnSPscsa+e/X230ZLgllZVSgcPzpAjqoNTJpEji8tG8FFB061yNva5ZzoTwVMqFCPvU4UOk2ZmIm2syIiGuRob7tPBO0p/ARoaFd8zUEsKPmgIa1lNGhPnYPR1AFeFm9Mk+24AABnnhFjw0Gz2OwTB3FK91NzUL8SkJhqf1nm3XXhiLkIRwcy3I8JmUlZ9GTsjkyJq9Ka1uZ2qaW7Hz2dmfdkl1ih7Tva8q/asbQfSUgPP+Be3V4xxh0yucpminlFLbVDXtq9DFg6AjTDs7JDSQZWMzyCjlIDZtk5Sk1NChRI1ydFLJC52mdm9q6P1R1dRTkcMjIohEpy3hO+pnuZPbtrzu4/CM6YgdtXDUX9x+lGNy8v5nltABNG0szOyZdA0AkqpMxaVTZ9aPPtR+H//uUZPIAA11dwUsoGmJGoqhpZOMoDMFdAK0kaQlcoyBVpAk5opEcpKq5dUjbL6vWg6yyCj90jKrgMkS4KZUQuOkQ48gtbmZJa1Dn3EjyxSIYUQuhoccpK0Qx57O+s9kNwUAcfatc1bmNQ2vxMmYqQiOG6CPBn+yk7C9scTIT5wkS1yPpm0PCA7S1FF87w8AcUutY6sLNZKjdP4OBLJTa3f2ljqsfv5qEDCL749bllh3Iak2CAAP1ecI44R5smDh74kZZqGZ4HSbONLxuHj3whnpR7Q5fqP8QjqqWS5uxHGKmTeDyz3JY680yIvy9VyxIpy3XLS0DnB0+GhacMkHIotBU5In4BaAE0JeiBICANfjWbGakUusohOzjw9Di96Ce9bZxmZkfDaso0gBcG6PRsK65uU4Y//ilyt4Idr9CnKG5WpPlYDn3OfAv9lvfU/KYftr46xaUVtjMxBZLBIs3EAgEQGudxpgWqRZQkMeHFfKuYK+SKR40ji1WxC2Wm/6h//uUZOgOAudOQAMvGTJHh1hpUCKeC9kNACygaYlenSBVpIywabBA/SgtgTWIIbLLAtyhWI1FQnTIxSiKj0vWuMFSTrEBNMuMvGgvYfMFzrEUtKhr2oABgAAVb5ViA4e5cAcfEF2tL1NiVmYytg1afwb4gJoXUINq/An+O+z+/kK2Sot93ZfqXVu+/WnXydQqlKl5JQI8Xeoayfg+wYvCm9n3zOTbX0PAGWV5VAI/SQW9gxNsqDHQpAlDmJbAo2mgtNtZLvRJHZMy6dGbcvKwpT/5veCu1XrfFiLIkhp8iJQ0QvL2AAzJzKWiZyFta9te4RLWlR++JHxd4Vas/SArBaq1ypTHOEj5nEoexyglq3zvjl2RvvpErNvMi88QhfzppGtAiVl5ubgqqOXSFKwTyqQUMK2I14yCEvSpQuAzU0NVWQ2hqeRlmeYJXMPx0FrdIVBX61mEsL8F9NO2d6/JT/c+8cqA87jE5o0jlzbwaRNOJCLxmrSTSOTN2B39sHb3//uUROiAQqckwTMHElJRA/g2YMMiCkx/BSwgRYlkEyAFhJiYmzy81ygvN5rVrsxfvdYrfNFvL6sP+7SvfBUtXT7fH53vLLHr6MrRVyzT7AsRbt3e7JX7o+L57vDVWutWz+v9cK1pmIBzSiIzf0oB79xRHtlSXdQEK8+lCMLL6XKJNnHCIJCgyDAqEZFx5N4FQPSl4HadalbI6crIoZ0UM0eS7UAFPH9p6sAEIFRWtFSUPzLwqrmA8kaYqTcpUoDqt0O42hIe2SNc5LHMUKOUfFTabo2lFjHl0KWiwv9SNkPxdttTfKUC1kw1CmGLCJEAbB1Sn6JKZV9G8PUXJmXJBTSVWzcqiSziWvMsSutkiRyu5JI0iAQCixKe1ORRw6YzNV2/+7MFXYMK+KFEwGiobEoK6eCx6DRUGisse4NeOeJQ0rgriKd8GagHxykUQEEbDuIjTmDoJfezFtddueWRNOQEgyN2EysqwQSoe62LK114zdOjh9cTw4pcLWg2Gfuf//uURO2OAxk8PwMJG2JbZEgBYYYmSLhdACgkRcEHDOFkF5QIRXtOrm4JkZesTn/WaZW5gToUtZgbfBq2Ri9mgKkDBSIN/d0g+tyW8rr+KBCQMCgwkCAxAOQQ2Wf9pNONKLMPMPLZyiyjzD6qqoldNNMr//+VVVVU0000A1VQZTTb///////ppppqqqqr/////////+000VVVVU0000VKOkBOgc8T7WkcDXDCjGt70ZDMGc916p6yfbFYdohnHWlQqUEtSz2UlLMjoEaap2P2ZEhZhJOaPt6qekjWraK/6q28duKb9LHCoiv39YspT+18qvcZDm8fk+j92s9/PxoAISlJ3Z6lIaPDhk1bLRk2PmG2J5rnmzCOF1GWZubkotDg4PmG81l4wwsQdPZ0+7////s7toUww44zs7OMOOIBzSNYITRl0yYNaDLAoQCABJTTkqVCcs+nOB41tFBetKN23ExxgVAT1irrbEveP2rEFRAXTrUhSPWBkNe7onxYLoXa//uURPiM4u4vPAHmHFBhZifxYSguSghIfEMYwAl8nB9BJiD5hahi9Q1sy5w+LFUOIkscGA+Qc6DiKQuD548XZO3KI1Ggt1jggxiAtILg84I8FrJlIpmFpEOekhwRDfcptzAgtO0HP0zh+iJuf8CJXcRQ/Cf//qSRJdG0jeknST5yZwPn/83tf8f4DxHSzv+cz+vwdx3dAQiAALPqvb9eAYrwr6Vd7wSFd2QJ6iBtC2Nc5Elpu4WqCSJ9TWlRLwLKcZcia+jNREMm03KRmaO7W9aCwe79q2PHkMlQWlLIiI5lkSHfjR1blM45lHcVZW1Nm5bDaKWTqeCBUK/wVpQAJPcXEeYBMxuRizWCuSdX2TZlygprNQ/s9J56f/kPlsj9fVlSa81x1VOnm6E+jFDyLFOUqCTxsIi5QFD5S/bTUyiJdXBkwo1jBXQIfWu+eaul5DpGe7mZ7JFRqZOimOe+UCUEfaBAAA11SBhUSoSEKtXiopM2qhNhQFF0oVRBSYK4//uURO8IAlc7rbDJE2BEAphdNGU0DKi48geYcUmbrV/k8w2hFDQp/wsnxSQ38RWR3gvwsm3fwK+/hRwLl8LFgKNOFBQTC64K4+K7/6C//xXf/+FcChvnDcn82A6vW4AX234c/toLCLenJkGo2N+wPHfKa8WMjqYYm/G31iBb3HTaRHVzcnQcoD3+YcZMjo04/r/QmQ62J8TbzfMhypw8TP3/sdz5RMnv33bm7NMU/oFYQVgoIGCBhAYOBQQMEDCAwcUEDBAwgOxZZUtlllQODBBQwMEDCBwcUMDBAwgexWoIGEeyywGED2KwMEDCF6StNN+VVX/6af/lVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRAAALR6cKwIZO6L1jMNPL5p7pmVcFaa8UGLl50b//uURO+EQzRcv7HmG2JTIXe2DSIaSpSY/CC9AUkollAIMY0xP2qg737anoanYe9+J277bOUwmveR/usyQzbnvecO2XXOpCBEVSQp2sRsS4MzHLGK4ydegk77QqtyapK5QZDzl7Knv1a0aJp26+aXyMa8aikf4EslONh/TB0pAYb0y/k1iueqTqBoNcx1qlXS66hB0P+rUnEDCNVZiDM6ZHJfKM3IUm7vLEq0573E8Y10lsXkILeD91zTVQYHUmxW1aOF0lVKKKSfMNfGcYma6ZUYnhWypD24LRz4o9VMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQAgAAAVr2VinDlX+ZQXl958WlXfQqauhkcuHYqbBV8oUX+sjM9jx+GWr9bTxK6Q0vqWcGl3//uURLyG8ptHv7GnG3IAAA0gAAABED1+9swwzQgAADSAAAAEJGMXjF2QzDFFORihopIwY3tJTzNnOB/FfkN7As8Ei2x4W1o5aQPlTQsh/RpfhxA3NDiYN3a4UJ0bkhCIklJNw9Lx4KljdyVPMbHgusatH3BKOBBONm5oj1n052Iv8w/9TWgh6WwheKM6tvccylH19oPQVhKNqU3Rwx9M5ekOQujnngWz4Xs5tvCD6iaeybYAfz7K2tRbd1obtmmXzIDHZsfPg3ezmYXOn1CRDDZHTuAyLw6QzO0ltDBMQU1FMy4xMDCqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgCBAQIKoVVzAG+7bW0KCfIVnGBi5JN1xWRhjoRQx8SJKNQy71TO5FaQ4xvNXMOoaykcr//uURMiA83BWPssJG3IAAA0gAAABD/2A/Uikz8AAADSAAAAEm/19vshU3jZ5euKPyvrMohdTUwxiIDZ82Op5z4F3IzPMsG9WVbPWVO55wDzyfxeapGdyjCPQdTw0vgFOvW9MqlUoia0nxw4hD2W2ioE4wXKnjUswvKC0uWnJmMa6gMMCjfPI6ikNCnyTi0QUQLJXkvzAsJsaW2USnQXwsrZ5hibYlSUkrR+YdZ16duHW2vukW2LxGTY0jSBNCjkrgvUb12NnDeReLR/OeTkjTFsbG3KNSWhXnSr7JVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uURMeA8upVQeFlGvoAAA0gAAABEcWK8Cww0oAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUAAHNCQsXMCYhYJiyMlFR1hJ0SIomNBi0EjbInWidaJIpZEqwWB0kBSLvCpJ6iQUArAESCQFIjCYSApEFiJ2kX/27AERCQFIjUiwFFaWmmPTH+siEg9CoZGIVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uURH6P8swUuJDJMFAAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUBgABAAAgD/RrBgAIZtqjFHDUPGX+HAAieJjXwIkY+4K2f6a7XDEYgaYwQYCO04Gih8Bg0X+8BofgZ5LYGHwp+BigGAYxCYAgs/8DGgODJgMDAQMKAYTB/934GDQKJOBkUYgKBoAIOAFA///E3BdQDY+NNQbeNb//xSgGAgEFjg54XABqwZAQQ///yISsISDIBYYI0IOLgIp////mg4CmbmxFG//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpBQAACAAADSCgAAEYoE4cJxv//////////zchhEKBQEBAEBAIAwCAQAAADuTUvIyMVeJMvgCcTO2Rcs0IQNTMY7AEjQZATkxT4VnMA4BGjAowDIwU0IfAoCGYJsAnBcDHAoAkGACSctUDG6pAz8oAQAcDCAXKZFQAQMBhIKgZdCQBwVLIfCWncAoqgYeIQGOxsKoXCPkyUsEBJAxWKgMDAkAYjgHDsZEqybIDSSWoCAWAeFgMKAQDFYQABBYyxoOUM8PROmKlVYesQ8fA3BlBlC8XS6yJDkV2+IzHcJ5DpHLhAUSitjI6a6/65ElEaXhyScWi5itS1//5mianzJZ48bHUk0P+j//6SR3/MAzACUVDVmy48xkA0G/1TiwAUZd0LKGG4g1v+CgBBd5gA4DMYEcAmZJmgGmSKAsKOm6gMYAwDFITACAftCxcDCYRAOB/38pAYPAoi4nQPj//DLYyYW8EXIoQP////uUZP+ABaSBQKZ+oAAAAA0gwAAAHamBG7n6gAJ7QFzDP1AAFzjjHWLjK5IkX//9QWjiCY4yCEAHALj///yfUbEAIgUBlBwDvM////8nyfTppoETKBEBZY55P///////////kENFAADUtMBbmYyZ2NzfgEWPjBHIzhh4jNLKGML0UgxEQtTCPBFBgMxg0AKAgA4rASZc4rX12QUrwEg5VIjIjE7f3PbczDmY2YYNBazfiwAjoYFIGOPjfnLf77hvlyGHTUxAQeDBoFGiOZhhQBkAs+5T5zlvn8/9///+8rztupE4vADiXZdOZ7//7////3+Z6sbugRrPTShtibbg84mTmTASnzZl827JNssrBACAEo4cPS4dBnkaXCIZDVEaRAMZik0YSAmYtBqYSgyFwCYKzJFZtWsNfcpqL+NdW02Y49A30dEuC94kB95VRSOQWs+8z1lq1rD6WNvA6ltpDqSs2A+swyJA+CCnKZt7VdiNzNKLj7i+rGo/aPe1VKIx//uUZI0MRUAyRp97YAByY1i37uAAFHjbHG9g9QF8k+NB7CE5gAIAEpmfwSG2ZaWYj49J1Px/mH0JycnAHJjVEyiwlhgbA1mEcB6IwO1kAYChrLT1p3nkeZsJdRFYwPgmQEE2PAWmAaB0YcYbRQCYhku9ci9TGMQQMAwe83zJIE3TescUPFLpvpOzylJqyiTVGUIDmnQzXltN3f//6/69t9IBe+HIej0HMLiTiALGxBP///uhpYHA2ETX/rUlLM+mzi2lVrbHGwsVVBDGzCCMO4KgxkCxTCaAUMewKQxBgVTBRAUJAHwSBeX0RmXO90qijyQI1iKPCh8A6A5DJFDDWxXDuYVIU/Dpu0zRhj1vcziA5FFKe3HACrhMUe////104yi93FFPRVUAAG0gJpuMAGGxCbME4CcZs5AGDCecVUZuHnKqZkRbg2dwc8aX5nPhoCYTCxIaDE8pfc381IuZurzmn+YWXpMJRjSCUxkQQoMdGDDx066oMvHTIAEvM0Ol//uUZCwIBPA6UOuZ2SxQpRpdYexPjgUlVayabpjuGiu085o/tzWesabWest95nreufnhKYMZcKAZgwcZqUiwOv1vjAhUxsqMYCUECBzgz348///f/////+//91aCRKYoAWVQtrLMEAbFMgAAXECkq4gAOWbIxAIMgKGiP8rA/9OShZKpKTpNEOMTe/JX5k1r+A5F1H8odsrAGrZGavmm/M98ss2ifpubb+mdrAZgcce4rCCTm3bf/wawy4ioxHcKBGuZWRKUfI5CiKsA0MYPQMXWmk6xEQZhgOETBd1a5AycZPHHenZSw6wCrh6TUgHaME7VFxDWZl8YQoMky4ymT2/SS6y6z//pl8csUGLeQAnC4QMQnBv4NgsPXHPJ83TSf/////WmXEHfAAwAALY22KHAApkai1lmQbGuN5f8gBbCSJKG3+bEeIooSvozmg9Zrhh2/+MiWTM6pb////6nrjPZ4DAaY9UAABOtolpmMFRUuVKSNhuAP9ToOElvplDh//uUZAyIA51MVmshVww9ZnstPMpnzbUNX0xB9bD0ISspACfOJE6hwgaZSpZKmBEB0NCGGojKpfLZ6ap7NNdwp6XX93S/j+5Sodn/5bxZgHRMpjNWdrVcL+NUhhEaYTuL9f5zw8ASHxxxpCFyFELR3/oe5qWdf//1NKkpucPhsAAG3XE5YIAOyQIsOBpytoJJSMvJrJhAHNo+eWc1vXOe8w/mhKPe9RIAiGJrGurOymOqaf///iGOepo9GQ2aigWBA03QaVBUbswo1hdIFjAw0mC+9pTRrzD9YtfXpbzlFmfr26tjWCkKJ5ulZBC7fMklLNyGEEW+oc0Mtg3oAeDkkuVE6CBggh///+3/8CC0EHDuAECGpeJDhLYcBLQM6FIyPQEwf///7TmscAAKSJQAgBFxWg+hNTYPqIzB8RQi4uc7MmM1GBupdeozTdborR70//zI/Uxmb2+tP/8rUB0E2s2vhG0KItoEgOGpOAw87MdVjvi0xcNOlQjBycJfzF28//uUZAyMg3FBUhtmbLQ85Do6ZYJmDmT1Om7tScD1GimllKmee1DMBiHnTNVFkL4zdKABUMuppIv6e0SBDW8VhyLKsjZ7bFCZ7zzSTKdEQo2pekYhJSik/+iy1stFkWRRpa0TaldFqnpe1fWYmv6iSNvwVBp///rdyQAAGlECZgDCbDyoWD1FjG2SQ4ke5CQPDEwtdy1mydu5LRu8eixqmOd3FeopruJe62N8N4J/0Dfr/8n/kesAAXwAAF0YWm8ZnnObUqIZLG2cPFQYLISbmj4YSkEaVASeaQlYcYMPnRNg0fumtAKuCD7UmrMPLtM3btBrvONFYGij9xSnmqGUyqMxait0lOYaahpzOPRx7gDSOvuGR7f/W9a7qyzR8zsxxATmL0/8hJfIgsAXQOAkehARTtVAUxDCLNIA0mkrpbHVnbj1dfKQ6nUXvZiy1b/yjftVf80Au7mTfNt/uj9DXa3JW/qfw3UAp2wAwZ+Mxih8oPI7zR4g6SXClmd6xGB///uUZAyMk1UzUJtvHGRBpnqnYKtvjjDNNG7pTUDykSrlhB3uRrxGakUq5JBIyAzL5PvYAwWLA0In1OThjjKxSun8jg/tEhz3gazjV8ViTbxnea13+J0zVvjWpZS6LF8Ztf/Ne6UnhPCeGlcLVyJww3RXkA1qcAIJOWMCmpVjK4biNAKqZQMlaGunWbNOGUYaxDv/7OzvPV40VXuUqYFH/RRDgnSll1cSp0+nV9f/FZjU3W1Rqf/lwAJuwDBtCAqcphe7RvzfxpcGR8iPJizDZo6vhh3YxiSGJmqbAKG0BA0aOgQRDMuFBGZjeRBWwv8mALLJ5rcWUWkUw0x3KeVxKSWdQ5SZ162uKK10SoqEi0EstflB6FcOH1a3ezJu7a6SQrZJhCDwW/++6775HFU4W2BXpAFTxUHKH25HFjvxLYpVEapsLMj1nnsQlPB4+DN9FZKn9G5T+UCA4+zYs00EEjmzLImAygAAA1G3G3GBDxgjoVXmgnmTKgE8SORUcSDi//uURA0Igskz1GtMPLxZpFn3c2geikiLT00lcHlWEOhZzJkmYOICvIigAhDxepSuC/1WaKPFNnI4qr4hIVWSapftAs1ms1B1AerO2FQcY6CKe79BUAGLa////nC5u6C5/871gMBKdgACAWZOXZmeCmpJGcjLmnWhBemQswW7hNKMST8hEDmhED9Pu3cvIRBFqEvBO5wWE4OmDjELLJdjDMhxvbPcz6IP7gm+q9iQCCccNxMsHjpsLyKxIdxQe2m5WAEtw4wDEBndBnhAapEDcABmSixoRGqJ4C7ryVfT0XLUnSICnKvWM00XkUxgJEdlwyojoP5+Xz58hNzkDs/7icASszUr6OWGxb/fiA/7JiCpyBCBTHQaMjEoy28jPREOFC8wEdTAgsLSAaVFjaBBgQ3I2cROaSOXLMQ9DzZ/uCiTB0o5IlSIU1p00lzdd5/KS/NHHfP++oBXyp2bQ2gu6dEP+jlaABliAMEcP0whC9jJDIcMW5VE3xEc7Xf8wrqc//uUZA2Mg3Yiy5vdYPBAxfqaYYdNjjC/LG9wqYD9kimdga4e4ADwz+sk2VGAydSUHFKYOgEblgEAgMAwAonGAYlBgdLHayqIaBpxU3jIIBWOgdOkwlFeTZYuuvjb/XG2py2TkV+85itMznuLmeN48ceWvWoQM/+gAAGik3WAGitlKhVgVBRKjGU+EjE8Q/pCSN+peN2vUljVvLsq7eGtv4qahplsq+2eWRWG7/oBIxdv3W31Xd5R+XAAeYAMCIScxWQKjISRZMJpe4wyBsjR3G4MEUrkzDhFzF0PnMD4Kw6sojBYEBCPNtQIyQBUuWcmKTWGCZQR01cpwrNeddqr3qaa4EhdCs+j8RSBhYQCY5mIFB9zq5zB/YJiv5zh8y12u7qImqy95cQdkBlJSAANhWUFpF9i2wCCqqTsIcRVVl2YdSL9sxOex1EoQ1x1OsFle1bTOzh50X2Yy6oWj/2bA4dY1JwRBQIejZUAIKTiAAUETG08ziANR5zIn82QyMIQ//uURAsNwrkv0Tt4QjxUBPoHc2geyeSTRE3lCLlkkSaN3iB4ToBsxRBDGQ/ueVLQ/Hc67fFEuZX0ho4tR0ieIIqycaPHsRF2PBwXearcNrTQJp/yxUApEyqzfCcz7NHv/cQLH80AQhOYAAGBUwumzRSsM+wkwq1P6UDFlU1ovM+rQFSBBU3peQzYNHhCitsRkEM2p2BbFWxNvC5rqt3ER/Ba3HGUVWNEGOL+hAA462v668e6ZxOxwKyF7cgQdm8kZxHAaS0GuFoKBTvDEOLQ6FNY2ZZUTxpRTs8kYBEWQ5xyiwNpgo6VVoXZuVrKFThm6Dg+VSkWvjQgHJVWGdC8WhyYnQ0mQ2/nQjL6AAMDZj8WZv6QRtFKhpX1HSJyYsMpxBkmaiuLkExwI2kmAQQajBKLK/IUMgCA36lkkemKUkgzYjEY1RgsyHMBM02rZYu9cQDNhwknr+DgAaDqbQ6p7fKKBKlgAFRYDEgZqXJynEmyhmbXDpVIR1wqkPwDtTS8//uURA8N0rojTxuaQdZfBDlje4Y8CjyLPG4ZMVlJESXJ3aUgtCoEzSJJuALQXDKcxWrCpfSyTB1xMQcSotZpZpQ9UaaNHNPx9Nl3/7kAuEA1AsqixG1mv/bukf/vAAHQAASDMYKRJxiSHbmXifeY5gvZonhHmDaDKZDQlpjz1HhCaZFaA8IDAZfMItIRgVbVHCgwBMdkDlCwLqwRMN5G72ki9Ah6KJnIo1uQ9N817QzWB1///kTQK1upH1qBTg4LDE6sMQTw51ZjfIjMrlYURBywamG10IwSIia3UvoY6D6fUkiCh7Fr8XPsinYqPCTtObprPG5RdwvGnnhnqQQPG/7ywRK3vVInIlKaSf/5BeYECMZqvUbReEdQYgcoiWYiukYBAIbWEqY2ygYJiaWHUBAJiQUfUQkw0q5HRMtr8Ny+CVYoJfERnVJPRLHBqNYxCMfkf/932qeqKR03//+XUfUEgAMBgJgxQT2zMVUDNvNjE2KgeDamCtMMwzg0VgxT//uUZA+Mkz8jSZPcWmY6RDq9YSY5jQiLJm9taYDHkOt09hz+HtQEMsUF80BsA5xmGx0flU5pEHBYBKBFU2mEAEuiFKUlYQTlYTB0NTMspnKiUutxW9atHGboNyJSLJcmgr2o93/qSBovX8bPLgAAFRxzWUACgehUqgbYh4yfytLLAONWDbDNgqmSjqB7eRKSVNPZKkH8/PlkiWctpZb+AZv//LJN//UAREAAYCYBJihJgmfYa+bwSXBrVgRmTWXWAD4jLqBcMg0QYzQQET99UxMwMnnzPegHP4VAiUHGkwHCqqpEAsILwrVUOS+bm16non2h6GZ21fNl6JPLd9psc9sFXGw0pv/DxiZ7xdpr/WXJLLrhuFOIIOUdBAFhiqueHVQt5gWGT5766/QO49ilrYXTTZf5UurI6oxlSL/VAWO6VQREAABoD8xeCIDKLUWNZMhU8Chw+fOoxhrAzkIg0PhE2VB4zECAxPAUwwA40wG8DBAydJEqCEsOgCZm1hQe//uUZB8Nkv4iSxvdYPo4pEp6YYg5ivCJNE7oyXjcjafdpI2UBHGBUECyToSkvUn0LHLvat8/Xf+s/0d7cdLqZNbWmU32yAACpuSAACXsJC7yy4JICmkDpWDjwtVXJ5pXEI/WnRYocsTasWOYkZLTCVFyMlpad7hORb/9oBelAtmBIEGBrpGd9fn2w3GncemsxnGDzAGLowGGrZmCoVHZHGLBCpA+7tmkmgQeBiwZprXoZc69TxiLz9i+QPgsy3pMimivN6W/Yc9HywXjfNs0cEAIgDWbghckqiYUCi2g1h42gE8YEK3CKeypqYOBSOnUAXAQlhc+9zQ69w6I6AYh0uh7DUjRBRHPxEl0Ai8AAEilGAAAoHTKIbOBJA/GFAb5HLF4WxjmxAwChDoAxAxTyWWBQpfs5LyQJaZXljiJMFwinLUOa1HK6XXi1x/yIs6sI8Jv0YCRrCrawEDeAACAJmEdg9AHpRoZyhxro3mEMRjowYVKh0oWRc9LcxEMeMQB//uURDmJkmIiz1ObQOxK5Gnmc2kniJyHQ05pIzEgESelzSyvgAKp8kLrMLyYhesXKE6r2l9y9bq9y9ZH08pmF/J3K1HumxzEAMnLCoBxRDmeTAc6JZztpwyoByAf0YxKRgACOdaTExiEoEYMCVKLRjJqQy9dWwhG32xdZkqdLZfgLKTqED2Kl6StGymQJJrxEAQY3TW8sPCH8ZXZvcSmLlGzJGWVk+EAhIGewa6qep3EeKNSCqfMmZBkdMzYc07OUc1U1pftW6n2KEebSOOvj2UChYTyAADAWgAMC8C4xJAnzFnS7NPktI6VfQ2bSYwvTg5VD4zERw1wBEwDKlBkQhIY6iu6SsEACMFi86T4GDdcanE3Zze63MrbxPp19MzrnUc27pbQaf5LepTJn6AC49lbuNEAAF9OS2gACAW2LIEVkS2ZsaK23tPThQvBI45gm8NpWyluYrrnn2pC6zS7gVVXUVKuh/rChZlBITxgIhHGAOucaBi7huYMomuobOad//uUZFWJEvUjS8vdYO42xFqNYGWFi8yJJu9tCUjhESj1l5T2gWRgtCNAKE4xEDpDEABjMCuAEZGLmp9NsCi8wYALmKTKCJt5fFXOjEMRCDJXT04uLnnH3buXMy483/8Q+4gHLSI+WAdZQQnXrAAACdjjctszNkYHumQYThOMU1JJHpxzkOFLNh+G8E+0xiUk+M43IixPvKqqmudR93zP639/2C1pAAACbid1AAEFGIghEpHCyQO5gYFsZGr8YMhoHLdwidSwo4OlqT7EGgN46sUpboDB4dO7fPia6aI/PIAkVS1lgUdOzE5qoJVmOJTI9/rAAAFtt11AAEwYkYa1wcfcdlYOmXwFkYwqTPa3exIgk7GmSssWY4khiK/4jHS6ctfdevhZG/Kz4nFItJIJlBoyblwyo7EItZvNHVM6U1HLrsLoJkiiahAnIKEzIVFh3NBoiPKety5lcid14WkhEG7cOq2gUZMqzRFpP3TSI55xchBQlVTc7rA0jyrNPrFU//uURG2Jkmoh0mtmTL5LpEpdaYeVyQyJTa3pI3kgESh1zLCntfBknyZ3A3JJbGYmB1OZGg5oJ3j1QA0aLRg8cL0E8asFdoxe/TWYy0Yn3qDWhzDA0tUrYY2ucjtGmFC7d6IyZd2rmpv2eZNZma5iaz3alQAAbbtoAAkRhQSYC5Gr1RoqKQAwzOJ1iMkmia/AnWbX0OAcSpNNErKyJVq0NCqZCYWZk1OMJaKSYUslmlWRg11pxhLY5cVHgoVGSogAAQ7LLdQAA+Jhw2YVIGn1BsZ2YgOIeiQ2XnIi5ndJNoDp+E0rMhmIA7GYQxJjo/OpParnoSzhryYZHMUHyfK7WqHLBwGora8uC891gucwfDQxmXI17UM2Y083NnjdQnAMTNwjcwq8SIxmNBLXLWhw8hSKLPkfkum6uo70ljEuBSlJGwlLXMxstuM7y15T4u2Zr/KysTEhuMIQ8MrH6OkypNsunPycw/IjzCBdNDjEw3FzMoAMRBtW4KAEzEDPuzrW//uURIaJEl4h0tN5ST5MZDpNbYh5iUxtNG7ww8klkSYJ3hh23moZBRplG96lGnxKbVWZT5Yq4p25hWbyUP/nY8baFgFeAACABDJE5DcYzzSvWCDAmgSeAZGZzDJgFymSgQasQk4kEIjV3I+sSIA3JaU2F9H/jkflFPnhXsBgYPVVVVWWW8aicqqlpS6a2HJAAAAAHDDlGYMqYskxJUFDru7PYsswygzM4vANZMnAgAh5mo6EgoNy+TcaJnr8wFG5BPzVCIAobiRS5YEFQHrKRcpXhIaBS4CkQU4CAzMW1PMIX+MOtTM610MrQ8KqXspCo5g5BDG7StYUNJqoJqsXZK0hhrTharTHpsopWNy9u/rRPiUh4YWRiekaLo6qYPxfqb8wSDcyBTEycpAxbq8QJ8aAkQYW2hguKQ4l4Dgqx15xJ2U2gmYoJqIxIDOSIkjX+jRytflHlH+jTjdfc2L1oLqlf/9tBhrPsboG3gAAwGwPTFCNLM29E8yVmujKoNrM//uURJ6N0lUPzRu8yKJKo7lze4MfCRBPNG7liNklEWaJ3ZivmIKwxl0TXAmBKIOUCcw4diIAGCAGZMAxhgFIJUmkJiE1CpAxY8ojbKG7xqXV6yludmtK83LaQMg/syQ8sgFsanFP5ZmQACrnLqAAM26CQQqwSYRHKpVuKaGkJpBxqBYV6IlneAItAivnph2EPkBhC6whQnnFHEqYQUxp0nwPLcAm4VA8MQotYykAXTNtabM40uAwyhIzBGF7MTwFQCjJGFcCqdtKJVAUOMcvNAOUVDg4wAQTFAFHtZy/HjgJyILnbdIo6B2RK6ZpmHBE6UyNXwroXIgMCJu988AAW1bmtCsoL8FMm9KVRUWRMMoQFuNgePBIMy0Mh8PU9ztn/yoNTKqD/H+eWi/l8IML1GWzWpB6T3T9kgfAAwEgcjELRAMc1Xg0inGjSmFBMm0AkwJRcTHyCIMEEgAwXQEzfMhkmNEj3mDHCIsYcGWTQzDgafSYCkHraXEngkcxLHAw//uUZLkNEtYaSxvcMXI6xEp6YeM5y0iJLG9oaUjsEKlphhjnwQS6O+hU99Nz0cJXNhFfvic/ugAAlKcgAAuCokCxwQ8N6AnVFExti1DSwlmqvyWSjUTsSt5xcGrtfzOISI4fmZ04m5kkRQ5p06LiIPowzwALpg4ACGRaf6YpkEJsdw9myuP2YPY2hgfCzmV0GQYL45RjGgVHd2GoJGqLn+yCAMYMQgeQGwEAFghMYEhC7JU8anbxvxINDwcHJrXqZM96bFa/b5n2GYrL7sVrpu28YyDoGHIXGrez0fJV4nUEwplIeC4tUwKFKhyhL+xg3ccRDr08mDubIIEAnkSJ9QhmTl28y9UBgAMCyBQTCYDC0yJQkgM3sQjj0ub8N6AS8wfhLTYyEoMJEFMDPXn8UZpp+ZmnHZSJqacZ2nGHqAoqDJSYWCkQmWWAoMOhBVBCwAKKNbhbrOtFtSGDoFg+Rz9erS2b1inu3RepYXNvpPr1gAjOU5AABBQICG9gGysA//uUZNGNktAiSpPaGlY8hCoaayMdy1h3Jm9oyUDfkOlphgzvFwEcn7FyiJT4LVMwR/KpELg3FC4pV00TUFAw1UMzRmVRxxUZil28zmIELLenama01pLX9cPrEYbITxr7M8Hv/HEfmezp0sxHmeiYKYK4rxsQAxAxBEyZwgDMNaMHDQxiLD3YINHjwxWSAaJASHCTgXcexAPxnMndNNbZm1pkzbOU4T7Q9J79uejUSlVq1ay33WtZdyw/nN301H8AAJtJyPQQigZtNjTNFHM0KHABGEWjKEjpU8HhaLwi4ZHyo6NT05Lpiqjs0199dFAQaH+TMzMzQS0YWbMGhIVNg0WcLQdAAwE0GYMGbMTDFNkB80hYoQMVMLIjFxwWQwFMLiMV6AsTA6BTswKYD6GLs0s7NikzorI0UiMrHjBikwQzARGYYEiw6WaAQE3EvazVsDBYFjTtS6OxmpGgk4BHmo5RK0qecZrOqyCPOjeoE7wAAWRAxfN1Tg2NLDdruNcF//uUZOqNE1EYRpP+2KBE5En6aeY7zKh3Gi9zCJkOEefppg1vcwkmgjunB4CYOzVTUEC5S6UlWFgl63tmnetwzZBARbSqUNgJlUoezaltqxqJVOjJQY7+uGg8YNoDqmNvhOJmrCFga+ePimHAHZZizQZgYL4IBGKCApJgzwlSYZgBNGgKsZpIxgUdHYGeIyAAi2YuDIyAQUJDCYCBALQcLpFuWGJ1NFe93aQNFTAqKmBKWeyo1eIpvjWxqs8aNx+389rq3pEYpxqdhZiecEdXOfYqcseOxg0azlqIwCdZTlMol9oZzE0QYeROTIvjbLaiyiVzWyyzk5q5qgmVHF4UQk35JpuVGAwBhDzFKmBN3lKU/0DyDdBfyMgQWEwekRDLCEbMP4BkycgQTtW0x8zMHNDkVUUHhGIggIjxVAS8CC6aica3WbOq70qu3C0FqHBJE4uLx82vTtf1WDHe5tsqDAAGAMAv5ggRuWZieD3GoJAsxigpioYqYH5mUFHG9yZm//uUZOyNk1ceRpP7MmJGw6ljcwM6TXyHGE/xKUEBDmcpp5lPlhGnqQfmMoMkIPmDYqGwY0mC4EIAy/CcSQSIqVzTWXMpeIDxgo25pWpsNtsvjKMrpDNi8RcWg/QOGCmDUZwAfhmCOFn8eHCctipZkjhcmCmjYYoAjZg9FDGNUBufzseEAYZCOqjRBCoDQeCxhYBNNlTHWDs4gsNBEwmiQrRmwjnWN2GLkSOogxN99BIYICB5GN9CPZh8SpSbByHiGXKFwplEoQKYKmG4GDuAspgXQiEYZgBUmQPg8dGBz5jv8mmkOn9DKtKl60IJRKeR3woVMm0F/0/yYXUT3J1U7/yKl3+dyugHAAMAbBHjACS3gwVszyNDGDiTMMQlIwgcFuMApE6jBSQMswGUPAMBBAaT6RgBLZao4AvM4BGjoZgoDHgtylcSFd8QfSGYH+N1hjsrG4RaadBnwPDbSAAYCeC0GCwIZRh8yrIaXgOEmbUi6RhbojYYAqFUGAOASJgN//uUROuN0s8bxwvbMlZZAviyf6ksSmBRHE9pKQlgjuJJ/aUgYo8IgJ00qFDDYBMNBw8QbzDwAQBxlVF7ohBztvfMWRaR6yGH00/5eFZ766mwm5HqCwwHwCQDCt8wqcxNNP9IbDP4gyQBE8BgEYv4YeyBmmClh9BhRoDyet5hIEIRQ6cZBRwvBd4IKRIETVdx813vDL3ilk/G6RA4Iupw+PSvRY1PLf1z9zlLtxGhhgPwHsZDuNhGX0nspsRp68a2MLBmcrAaZgogh8YPiBLmDnjNBh5YFyaKPZEjzCqMPgvMwmEFqwe3inojGn3hGMSFID0UDMy6feyONH5j2uPy5G+fX/zf+hvv1+vfWhoAAwOAHHMH9N/zK6hCs0iAvPM4FHGTBUwc0wBUT2MVjAGzY02DykBjJsnjEcEBAHJgKPRdlMphwUA5dLTn6aE39WAQNFXURVLaJfdDVuN0HhRYDP2I0ygABgswUGYkKoimkcIKJpryLmabSAyGJqis5UDk//uUROqN0qUXxpP7GlBUYviif4ZICzh5Fk/syUF3i6HF/hkgDCxQBIweUHWARFUaueGRjhjcOeT/DgKm5EywBVpdWrNBs7jWKyDBJAQ/GilT8xW/Sxv//XQQwTkC3MjIFUzQSVOY2bVYQP9b6w1GQNDGTV6OUcIYyGUiTOnBOMJgnEBCHBg4f8FzqZzaAkmVfKp552hQVBUSlUVh2K0szM8ws5d7W0YA9oTBU/Z0aeGByg4pkiBMSZxO9GGsfr0hprxNsZZ+GSGQSVkZzoexkOotmhODQYRQhJWC6DghTIOAXKwSkImosqYvMYvhT457KylVCKnTg+eEY+K6n9/9fX///76//6P1fpoIBUMJZEXDHCmv40ZWFZOLyjozRT2Tkx4IXCBoZQZV6C6GCykk5gmYI4a6dxn0aGIDcdpa4QQqGKgkIjQClOdl5sNNOqmKo/J15A3r6aXuefBjFtTqf8n7ONX1puyWr/9yfu/SsAAGBkAAYx5OhsrAAmysgOaP//uUROgN0rcXxZP9QdBSQoiCf2VICrxTEC/7YtFqCqHF/wywCXZnAjXmFmI4aloCRn9STRplzgAhohKzCFYSN5dHXGXFuqMnsqnfMpJ/jb7tDkpeZZ2rb0sQYQYCfGZKosBvnBrwcM7LeGNuNeRkvArCYKABpGWRgoRgqQ/6Yi0BsGgsvmGQZGE5Bmk6HGFoHrQaWIAbV5uklNNZ0gtlFRmZYKpiIuRVZ5YbIJqT/1a9/v+63/Yz/7vdsBSkbQMCg9MaVtNn+kPQbMPQWeNrz4MOyDOkwyApMDVTQ7VPMsNgLXtL7Ky0RmRkIx7l3xliKKwl+WPfN1tJ/QACBvVQjB2FVMhWZ81tVljsM8zNfyNUxwQiTChILNLcKwyWSE4UAcIQhJh7QEeMnafGGgi1+jvOQ2cYvaj8HR5nNuSFIBaTFneg0/v839S/93T/Yr/zH6dkTGDVBupjdqkGZb+0NGnLPqphRicaZdyGFmDqgrZijYJEYNkGimKIADZj8GQF//uUZOkMkx8aw5P8MkBGwviSe2k4DERrDi/0aQD9CKO13STdAUwQCk4GDQOEFPJ1CACgRWw6QMS3Pu5Wd+e9bxUetJCT1vR9f+2xpdZjos/69u//Shft7CQAAwQsBxMeAOXTE91qczwd7uNH1NYDEnQcQwIcWbMfjBTjY7Yj4YgTV0lQUnpgeGxkGPIOBpWVnQoCzfuBFA2BsUCQ6J1UdKdfcg4NB8JOYbYvZ/9P///////+gSqrIYHKBJmL9HZ5kPK7KY2msVGSylqhiPodwZEt6eWkwarUUeEDCcuPmUBZAsGqz4OGmkx5lTzUX15ZYxJ1ViVfS9FxZOz16+v+7f/u21//VWAMEPB9DGKlaQxkBZUMkwkyzYZCmMxyYIeMDGBs2nRRDEmZCMSAM05xHyaWCofNKmBXajqD6UTNnxcda7L4/EHggSF09FSXDB4DiYYCAEBAfCVc/9lf//7n81T3/rWBUDAuwWgxehA6MqMQYjBt2hk/ssDTpkGqMP4t//uURPEIgs8UxsvdYbhgIohgf6lEC5BLEE/1JwFEhmJZ/uyIczEwfDEaU+MUQJMx8QwwsGCxGcdPAkX2rO+VADKLve0QIAiYchOH6RRj0VdP+rTtu19H/XsHZ7///sGAgBvjGRTuoybcHQMGqhJDWVQcYwDkKVMBeHazDTQKIwD4VAMJpAZzu/4nVAEXjdqTEKz3fJA91mzs3ctrbd3jgOnkFidG9uO4M3pmxc5mE74wE8IwMWbTsjOZzswyEZ4+NSdKfDBoxZkEGJmSWBuYGi/Bk2g8mfzETLgweMTsBOQhaHKUtWOVMbl2z/aZwwsKBtYRW08zyGjDA30Udl9X6LN+mz91+76FK4wwqoNFM4PeETbvK/s0E/o/OW+KJjNDQbgyMa5TBkAjMY9Qw3JwRTgXXNxCMLpIzHLxUEIjBcGAwMF4ULUmVNWiunqGJRdnaWlxqcr5BM+AiKOxFlwuilX6BVH7QFu/OJXQ/FP6EtcZ6GvEoAAARkbiAAAAAZmG//uUROsNwuwQQ4v+4RBXAXiCf9wUCghrEE/saUFtBuHF/3CISxmnGfGXnZmvlSG3QvmEDTmIwKAg0iKMDcagZ2Y4lUBQLKCyRTUJRPP0xUTOmq1mJ4aXCKE19fR/7CssSmA5Bcpi2CU+YQEzVGVYRMhprJwiYSMI1GBaDNZhYABqaN2uf0k8ZTIoYiAEYHBmZ6iwgPC4BMSZkkUx+OrVhmGn2EcJQ+OVd9ENVih7h16XSSev3t/e3b/2NX1d7un//02AK/bQGBIDGNA1G0YlmJEcBVXTJUUQCa4QhA4tpQW0r2MlDJyNZYyqfpWSyl2oaC4KhLSV0tizJmK1PTcWJ2m9yjAMFtB2jGFigozcUpJMjHYuThO0uNEQUkwXkmzRYB7MF89ExFQXjK5s1gIAwWcsCDQ4Bg4LALipGq2J9Mpg5nUGxWGqSexw7zLtmqimlRf6UoTFvf8rIgIYOqDtGRBE9xqy4zyZNKxMnudjwcVxkphKFUmPkGqYZ6WpjNhE//uUZOkIk1MSQoP+4RBFYfj9dyw1TJRREs/1B0EACuT13CTsGEO5kwQOFZ9TADjlY0pRfk8tuX4zepiQkWECqiqBIzhyzp/8K//R/2f1zf/6DAJgbwxE03WMz4GLTL5lowxQdDzKFFpMGY6M1UAhTAmPNMXAEw7TSMiGREYGJpIYEEwkGBKZyApO1VRY7JGpNq70Mymgu3Ltyvhbt3rWITTRVEHJY1V77KBZTAIQpQxBtI4M9wRaTN9FaI5D8WzczMtMGcisyqAwzALTxMaYHY2piBwyOnBn0eNBkDQywlzL8tlUpuZ2AeDbdx4UEhsNqJVbd1ftutvbR2foNS1jHf9tX+hcWRUwUoHXMbHIkTDaEdw0o9crN83ig0FxcjCULDNq4A0wmCbTLqA0PZfjOAgQEJzpw+oOFzBAILg4oCoCkfU8k+FjsofGGoGkFPcxt0lPWtXrdIHC2P7BW2s1UmC7A1JjmIjKZBUsIGiYoZpg0RbwYo6GUGFrmnXgCGLV//uUROyPwq0SxQv+2KZUoZiCf9sUCvBRFA/7Ypl2hqIJ/2xQYgtjDSRwWHR0DPjIBYDfy66EtpK8xdrWb5g+0I0svHV2+j8hTzva3rcidrTqTvf3POaXp7PKgwYESDFmH7I/Zk8KQGa0ophHoRl6aSA0RgxBkm3cBAYQwxhlcAhnXJAVLTCQM4w8YaYiFjgMBggGACsJcdhSSCmcPNYhl+JLJ7FS7bqcqXqe5Y3nhyp+/1zP7Eh6P+MAYCqEOmHCLwJodaq2bVwnIGQfIQpk1wUUZ1KyfwEUZVM4fVDWYbBU3UwCCszQH4t68zkKLCyawgHWEXrrQPh6BQKOeHzgeW/1ey32f/q/b92z9rfyKg4AAwLcCcMW4GljGEhHo1ek+dPjt/gwch1TDQHYNbIDowYjVDFIBjMK8RpNwKRHRuNJGOeusgGL5riiSCi6GJsrzl0NwupRaqVa9/D6fPWsOX7Hg+Y4yhl+hgAAwNsC8MdiDADKQh4c2PE6PO/vTQxV//uUROqNwrMURIP+2KJYwaiSf7siC9BtEk/7YolXB6HF/pigERTDYDeM0gHwwjEQjHkB1OTngUemFnBwDqvBgbdKd3rOUUlcvlHO26k5T5cz1ypsLAugHyBb/3f+3+swHEFEMD3MRDGHyig0ZAmIO8oXkx2QaTB3JSNAwEIwKSqjCzBJGZpkzK3wlUUGi2YXBoc38bVfi6IMe+H7strSyKWN5XBEofWPSABkxDvH1jIYGKEDGBlKghks6/sbFEV7H3U/OboINRimiQGbwHgYMKVBiWhNoBwSA2CgChYd4mAvZi47KRXIMGBWAeRAwgJOTtqMRbYmjKODx86OPHzdM//9P7P9v//21RAMFAAXTIbxmUyWhYRN0TLjTTaRzkwnEIwMDuFFDFSgEoxpxA7JEAwYWgxvBwwgc6i5SYcfLNkoNrEWd5TZFJOpp0DOs3rmQ5Sbm5unz1fv9oAHMUKefSpWWGAADA4AJQx+ME3MLKXlTWwRqs0J0ChMB2DojLpx//uUROaP0soXxRP+yLJSwmiCf9sUCfxFGA/7QollCeIJ/yRoDqgsDHHRh9lyQlgMGxggGpjQNSc7J19xFtJ2qEw2AcByM0y8fJyNduXbe3NTBpJAPPjft/////////9I0GBhBC5gA54qZC4VlGp6CMxmjRIAYhEBLmAiCQwsK8GAtgTJhIoBIbLAmmgYWADIjWGA4CCoAKhCs7/sWS6R+UydKpFXub6BgKEghHzM7VEvQYNsVSmLnFUph/EHmRH1qeODRR/cAgnmwjobEwpZhNDtGL8BsYLoNJkrgaGFciTYgLHogxxm+oaHxcJh0Tk7cWT5s223cWWTZtsDiXYfL0/6utck/V///u/6P9QtqpAQVGZE8FummZlrxro4xgZpaXdmHxhL5gKokoYKAA3mGPHm846GUAlAIyn6MtQBVrLvF7l3RKDn5kSul3QEkTDiQNCZYsifs56tVSafOcaSIp3cr/9rqL13dn6/d/6P9gAA1X4AAXBhhBeEPNPNp80Z//uUROmNws4URAv90ZJY4oiCf6kqCvBjFE/saUFXCeKJ7SUQUTPgwCygVXOQmogAvnpNHRzJ6MipILMaSe0R2CQUSVN2iHeK/Wku3eKW/h1mygyrCYMiHmGGNL4BiRDaacCEewmRRJYBi/IMsYg7BRjDiSmFAtGYhgWwHhxbWMQRzUmoIETAQZvxEFsSdtUq1Vhl1QC7MVkri1H+yuym9Ws45U2eN7G0EhEJTjBQu5O7r+x1n4yftqX20//5On9PQcBgeADmMCQoaLSqRq/CnGfWkoYxwmpgXFJGKCBeYBoCkAumKF7MDBCWaKmja0R8hDJkARoBWVEJkiNIY+tk/JWQZ0s3KhkAAwTQCnMaXFKjKaj3I1f0viNfzS8yUBojBIQEMzcLswrCbzHaAsN0owdVCAKM8Ix4IR+VVbxXLMnXYi8UDuLAUjf6Ozkuq8rb5ZhalBEQaNN//YIAwb4BXMldL2DT5Fgo2v9E0MIZQqTGqAyA08rg3LOU0k9U7cGQ//uUZOeMkxEZRRP9SdA/AzlJceY7zdBbEE/7ZEESieKJ7STZ15dAUUYoPHeJo0QM2pXkazGItEZZLpTYl1a3La1+ludskjwSW/QM1q/X3SP//9n/1/v/ewwHgHvMG9RfzKQyIE2FZAdPDHQwyOhyyFAo0wQ5SIX0yfgHz94ha8j0G47rbNNUqZ0yluTYXofeGIfhyQSednqWmxrC4ZhI0GwWE3////////7BSFUwKoIrMNpRzzNKwZI1lhDnMZPObDD1g30w0ow4TOUzsDc70Bs2E4X+YmJHUlbLpDfaNTXbmdi3eBkFhYWCwGJiZqrk/fVd/2avH/4orqV27fqu/qogDA2QEEx3AkpMUaNXzZH1PQ0doo5MMBA7zA+wzAxi0DDMWMGN+CNMFUvGgrCoLmBwhPGH4III1jsRBLHAunxoTzuBajcXwBsHBZNweFnM/X//2//////6vW4Bgc4FCY9YdPmJcL6pqxKpUZkcZbmPIgi5pfER2eX5kJvhzIS5//uUROsNwqQSxRP+2KJZIkhxf7siClxHEi/7QoFYBmIJ/uyImf4JCgVaYHUymMW6eeQeCz7RVgo4yxW8fYvPWW/d//c5dXd/6v/0BIgpgcYPQYgYnrGMEqmBqvC5iagWIFGFRg/xgzicG0WB+YPJ/Rgqg+misBhouoeovDzlzEFw1IKe3GOTdaxlYzpwQJmQyOcOadk0JqYv/c7//T6v9qO6rf6nu9IacbjQJKMCQmMtLOAPfHIGVnIaDGF45EoQPXYXkkQ2xXycBVPF9Af7k3PcjeysKCpWAlfvV//Vt2Jtq3/6P1+uMBCEZDP7XHg0RWB/OEd56js3AmY0CoGvMCcRk8FwIjKDhuNXUQs0FhUDFgYSioaikGDheXTA6VLUaOeq0VzOv2z+deqDUxE6wmamwsxW8a1ahcaooKNLDDzxqyWlyGvS6mOYZrOl00PvTZadYKiszRSpHva8KgBa8wTAUzGxPfNfhQs32VQDUyUaMusHgwew+wM38ZWepSa///uURO2MgssRRAv9YaRMwThxf7oiC3g/EE/7RED/hOW13cCWuPDEPTECG/eQPHXc1GTbh81OLft7PT/f//f2f0M/o8Ytd2r0GHFCfZmz8emcrgsJmmibKpyApqSY2qJ+mCch+dBYehlEj7GwuBQdOUphQcGCzWZnWhgUAs+g1ECE37IBFqTZWHBz1hpKGGU6xorJa3jX8JMcVZPqqcid7Wsjr0LFE8gUWtiMzMKUF6S7XdD6AMAi1NhObMwGWg3WGiTMjeLMhwcgQkjmceCEZuuCkJ5F4cARUNAPj2fJZZQg8HFLKJZtrdHqr+SvFv/9H/3/91UwUcQJM5McxzZy7DQz0fkuN1qW5jJVQrgygWeTgXG0MoNds03QhDRa3BTMEiCc+D48FVJPkjc34dSocs2KHXGwCaEggKizSdQbVCMgsVUxhsWY/S5WRsc/eueJ9gbHN1au7Cnu6l+gqVVMXSENkosNFNesz/VrjEdW8Mh4RkwBh+zIxAYfofJPEcXE//uUZPqPg68QQYP+6RBHQbiFe0g0DWwrCA/7hEEChOHF32gQViCO6a9nJNl/YKgMCSZNnV/o2/7v/7vTd9X/+4w4UUyM5odRDa56rkz0TmJNRpenzHaBWwwZH+zZ7GmMQGHUw4w4Dq1bMpBQwsODa6JDgNnZSPh4AAsPJVE542BJDVP0NuodR2oCPCSYtiqzew0y4USnrpbAdVjC4Ef8XXl2lT8i0wIgSjFhQuFsJDMudpNBhT0yZwrzFGwTZ0RASiMYBNxPIoKaYgCObhu3aooqatOgA6Nvle5rf02/9XR/s/17Mv8YMHmEGjPSZ5Q6jZUCMtp/ZjHPJZwyVoZPMHCI8zE0wUgwaQhJMTEBGjv9Y+Y2MxVTw6gtmzCA0bWrZ4u57ORps7or7PNbROc2j/3vXWSqtd1fuuDefVqrACiakBe9LqX2TalHlpUQ4rZ0mWvIEVCrKABEiqph0NZsjAZlJuDGWHAoa+Z4xjoiiGAKXiYPYLANgIbgmIUUHIAz//uUZPOP0zYJwgP+4RBAgSiSd9kEDJQnCA/7hEEIBOIF7uiA1cHNSVyrH3Ck2BAat/0JTX/T21/3/0bC3+a/+j3mGdCmZn97jWZ4XDSGUX+iZoq0QOZVSDXmNLReYaRDxkSHVmy0CmZQp5rgWv6JsUaIDrw4oY9tzP+55XDxQwhCVBx1DQbedLkh6Zhz1DFhxzgITfUV5Y8AiI26mc1lwXSpN7jbkcyt49jTcdEzxCMZQOSA98oCDQDTBuJ7MQ1HkxXkTT5BWTZYNDApbzGwGh1Q0sYmTNgJCOw4FKYpi7S7z1LLe77Wfd/M/1bGf47Z/qZ9FTBOQ2sy5a0rMuh1KzaSOzA3yNvIMdrE2jAWlJNKAVIyhFQDWICtIeYbWD4FLhotYiEBKAu0k838pw5fyyzx1nhSicsk+kXHFRQ2L3LqUhzk9d1ZZrSZxjjCFsFlDFpGsFE3/XXSpfWprnLHzHIqKrGmAYEgYbdTsaGi4hlbzIHACGQYPov5gnFsmQsB//uUZPqPg4AuwYP7EjBGYSiGd9oEDjg1Bg/7hEEBA+Nl7uBMkCARtwBotQ9QQlio5+b12CK/XB2Jh1VN3AfFf+hCv+v+3t/RT//9f6zB6QhAzqBgIMrChTDhPcZE4qBK/MxCCbjFEeHNnAT4xeHoDGIDtPTNTsBMxMRPJbiIDVDC3ThrvbOVNdw1ZIhoRuCbDSFl23Quvuc22sWRbUhbyrVUfaVYduzxg539bk3dYsccMaXL3vQK8MDUIIwF1XDWHSMMUqXM3RjNjLLA+MFcmky8gOgvLKK4KlAqLAA9FnLtiNCoZQ7UKHcMFwCXc1Ejf260aEf19l3/9PZ//q/U/owJ0KAMQZTqDLWQhI1ZRvANXBBSjA6QikwKAOHDC5kwu7Q2pCkyGLQIHFWMDHA5lDUUfYSGCoOMzmrVDU1olDmniP2eQ6vr7+1Nav+yuL79jNlmj/qMErDPDCf2u40sVB6NqEazjWkyjYwlUShMA4lkzJgMQIpYZGYMpm0IEOJh//uUZPQP04MPwYP+4RBEYUhxd9oEDUw5CA/7ZEEfCGHF7QzYIOeQPEwrR4wDO6LFlx7n1rq1fT2t7GWlhN2/de99+24U9Vb0IVWjcOq7UdxUd1VcYcQX5qWtonP1X6fIPwJ4GKDAJBsGpyfkAqYXoACsCN/aCDKHIAj36q5Tk70BgmIwQaLBuQe/Zt9Ngr/4v7/1f/qZuX+77vkyGDZARplp5OKaVlAFm15siJrcwIgZAWC8GfsDn6gkmZtSB+bm9nGcFjioE512TNBBAaDBG9IzDNil2WsHSOtNqaLrZhT9b6vSyiT02KNfUxpj391o9CU1mZVjBchHkwzJy7Mcgfojf6npQ1/9EVMJqEbDBKAV0xzYCJNB7JPwSADnVBwvGBwLmWYfF+4xXVmHbNRfx33l2afIMOPFmgih5MKw43QbC8XSojUhnpQ9B+8lc00fph2kaU2CuKref3vfcRUpYuPkGmFEyiIj6whNZsUs1HmdDC3SnMY0MUwCQdDK0AJA//uURO6I4rITQ4v9GbBXgUhQf9siCVgpFs93REFbhKFF/uiIxpGiCoFT3B6GXZ/JDXeaHhGAXtlWOYmvubYP7f/Q30/7P/Z//1hAgBiAoYsahARFHX9q7J2/cnWbNM9sGX0CYZggweUZT0DzmCRj/JhNIJAZL3Ri8EBQTnBjADgqrE7q6xmouw5c+vzyhe7VIPYGtUI6msiLfd+yLG/Z1MnpEE0LHzP3/NHaGvtr/+p/tM3tuu/23zc9uyv/3+I5m/VP/IwYBgHhVGA2mMaTzYpuDv/mauugZLIdxggxB5oFpjyMENBjMALBbIDBwV1t5U1+6IAJbR1n54BJHtaFG9m7s/+5Hq/V//sV4cwPMSSMLfh8DWqFV85p5QsMl/X/THNwyMwGUgDcUFlMB5k0yYgpDCoc1EJKpcYU5tyh+wone/6wxyHCoeS6OcsVM4yjiiVGLLE9Ck2Go/M2o1MDhIDsXxCjfNOdD7nP22RcmcZuUwBioIpwvABlHSeG1Q5c//uUZPSMw3oQwxP9MbBDYSiSc9kED3BFBk/xaIkUhOHF7uyAa3yt5l2BwGCkQsZVgD5gsy7QCRGmbkg4dPYdgsQvov+pbIms/2r+l////+n/9bLmFRBBJnQYtsZmtMaHXamG5jp6+EY2aGiGC0rwa0QcphpHcmiEBwYWmw05wYCTaoeIg3Ak+7kJAg+dbShJcEB5dKmAZKJ3YKIxZw1blpFLnKnxqa6Y0XVuPC60BmiZixlW1bjTGvSopeBjAsZbagQA4mCuxGamropu1tim/6kYYYA1ZilihxoLoM+A6MYDQRVo2RQWgX+SmywxpV1uFlbl//3fS38X1/nbI7+r7P/tAC/5gPAZCYEC/NmefuW50iQYIZ94oImQsBKRgMwrQBhtEyodA+oIE7c1AVmYADnFHDe2sGjU7wq+YeyKoeecftFULxpBBdOsheNfQFkYodGj7A7j5vUqxFkw65acY6QxrEyz9zI0AAAm5aYIAPMSYXMy2iFTSaLoNQAGkxqw//uUZOiM0zULQov+2RA8gShxd9oEDdAnCi/7hEEFhOGF7uiAHzGvgNVBsCkH/gu41uCEe5Zv5jGjZXStkrVdb3fV9e3+9X/u/Rb+//rIAwXcCVM6wKYTQDg2M9cso2NvZU9jAJRVsxYIDDEIA5MNpo4yvAszHHQO/zETw2SDQEPjHH7hVhBeHSbDwswQn7Do4wcQTfLKrEhZasSXF2OuxdY3pUiOUbQZJPYTike56PaTZ5B7gWA2LUMAYDoJxh/KqGcLD4b0ZSpuXmQmCSLAYIV8Y7heYt4fENqjcymoKaiv7mrVC3Et8tFnikZM/p/3/+7p/W7/0/9mjoUwqUT8MfMtejaRawg+pBXdOPwLfTIlwCgysYajIlEsMQuAAyVxIzm50NCiseDQeLiYHMVg9O2UUowamXYYFIuhFojsESDBIQ6jzmirz55z9IvPrc5rCiGGHLrRIFmCoaHoz1VLDTq2tc18x0MSogDAEjDemSjGrlHOdwJg26lOjN6DRMA4//uUZO0Igy4Jwyv92YBBgTipe5ggDWwpCC/7ZEEHhOHF7uSAx4xxwSwrnTKJxYdGhkIaNB7jE7fH0tGqIZWwIBl0/66O+qyO9Tf//+7ZTi2ujZ/qhkwZIJwM83NODL6KQE7VtpCOCXHTTCCxEww9GxjU5CIMNdOw0VwFTP0RNLhIw8OTQRqDAS/1WKvbaSeePDTVBq5KZF0K3HTrFPGLG0IPWC6GPCeKOQhw/Yt3MAdi1WRcsssonFb1NGmHgNErRacQPMIIMExy07Tc0MiOp8bY0iV3zIJEzMEaeOpw1FM4tYMtpHpzckEc5v47Y5cjT2Mlxlz/N/v9PW7o/+Z9zhPT/Vt/xdWTjCbxQEwslxPOUIaLjlcpls1jAaeMUoCwTDPh1YxisBLMIFBxQETBGAogPxgHYBAYAGAFGBCAOKjrjQhRIYMiI8wuE0B9wdXIyMmhqVHXFVn16cXoUsoedSZF5WTuoStAcc+UwNCTbKnvCjgHFFsRXzSmAHglDH0L//uUZPCM82wKQYP+4RBHgUhhd9oEDfgpCC/7hEEKBSGB7uiA/Neteo42jhzEbVAMZsgwxGCNDKnBJMFUE0wKAHTAPAQQ2X2BgGr9jsoEubKRy920YA60++vWn+j+v/+yv//0J7EaDTZq/Q+vtQRNozN/DIc2WoyjLe/NluPZTJQQ1EweEFgMTaG0zAoQOcxUsnLMISADTAfQCAeACwcD2v8YGmAqyMJoDLIYCnluuMmMgfDiQ6A5UccEBQ0HGhMQMPnSIiUwPrVxQUHODJ8G1pUQXFhND1gpcsGkVMnzYTEbTdYjLONiYaGf/+oZ//wk4AGAwrEHsMXpNszFTUY8zyMsMMZUNwTGlQ/sxasKyFAJQw24DqMAAAwzBLwoEaBMDARgAyVGAFgDrxGANgCj5BaQJ0FiSUzH0GvcU9vVNMMfUcxNTF/cVv1L08f1DP3xB33ccL1P+rRe1/PV6yl9Cj7aaxPETxpd1H1Xcdddy1XVXVPWN3uCjwEEXHTMHgZf//uUZOqAA5QJwg18IABIwUhxrwgAEUgvChn1gAJLKeEHPoAA//oFVTAGwkMz8hwYMLljwTc5LBszXpiUMb1D4DCwwZ4x9EB1MFxFoTCXgJsHA9oWAIzANgEkwCwCUAwA8iM7KsoWJpxW7iEZyrRBfTTppPzDTS79GhW7Q+lVd8Ct9DGLaxz2dnqJlIlbHKdj0iDCbKANppkg1lD5zWDXMMQwPswKAdTL5AUMF8LIwQgNyID2aVWSnl+OMrFOgXjvrVYzr7fZZ////2fo/7um7qAAlVc0YfwShs55FGY7IlBnbqvoYY4ZrmGqA3xjAaJ/OIZktJZuADxhORalBbAxCBGBLfKWycfh69PIZxnZ6Wepyp/H/VX2bE3+xCv997LPQgwIEMz1rszdRNDQxSmNMQr0xpQOzBhDNMjUA4zeDVBM3NLGHyIqD8M5kYn7JTHKGMwWgL5Me/XyzOhR8c1TiXPM4JT8DDkg2EwHikTYHC5MQ4BEzgANTak0SZDFwg5U//uUZKwI00IMQgd9IABAQUhw7wgACgQnFY9/oEC7BKJF32QUdGgqBZ5ugDisUxR3e2iXGuvU2+pd/apP6bWURs50V8dK9mZ1C3SplRxaiAC4HZiVJFmT0+iaqjZ5uOiEGIOJkYDuOdLg8Y1AJhzZKRYQuUMEby+pZ307U+iYIder9v/8z/////Vs6TA7QnsyzR6HMr0gSTS8cZg2d5MlMezBRzGvUzNmMD0wok1DMsDdPVEzigUw8tEDUiPAkndKRHRMKhcJB5ItQQVcydUhZ4AalUbdlZeT7Bb0s3/LqvSyZTzWe2hDU/3RjDB0HDZrnDB2huNW1143LhdBJcEwAiLzIBAuNGcp6A0Yto3ItjLP+PG1M31QlHyliv+O/q+zr/6f2/0/s6YAABl6qkwNcEkMWMNfDFI0rgwuBaJM4BEcDBpQgogvk4nDAwzuAw5Fs2lMaipeHMEv5X1H8kptP1+hCBzHXM0f+62zq+tf/pTVp/oXwBg2OhlH1xn5mxml//uUZMYM0tIJQwv+2RA8wUhxe7ogC+wnCA/7ZED1hKHF32QQM1uazpVJhvh1mAGQ2Yh4DQBpkRozDEoWOeTE+OD5hjKlLmETWy//6v9n/////+sQq6u4wHkJEMShPjjIKTCMxpplENBmBQjFkgJsk2s0rEQy5rY3HDky9YnEAkMccuzaK34ZvVbHubpeWlw/mh6lJpYKa9kvFAohaS83N36I3XQpBAzWysc48jXvdQ6cSAzcaTTZg0BZoCsp4xNZx79hzpOBo6ExgaoYJAYq0p8FgooFkrOJ3Buv3Il21izXa/9mrd+/S++P/+j0f+z7kdMCDFoyEM0B1YfOyCO3zWy9s84b081MpVI5TE5Sb0wEsE4MH9BLDE2wDwLA0JgEYDCYCKAomAggRhgDgAMgMjrVBKbgCrJZpIIDBAHK2MIQ2E0GTtZLsky9y0XEI9bElDQHuStR9wY6Ghs6s4JmTFD5oaKXIQMYkLta0Pl0ABoSZh2INmRbBAZpjWJkJtMm//uUZNoIgl8JxUv90RA7IPiBd9oEC9QnEy/3REEBBKMp3IjQCSCuYY5WpiVBYGDkAeYHgEBgPALjwGhaQIANpafVgZaWHV10gH+rfs1f/3aev//3dns//QaSEGvmxvs6hkDLpoZh4DkGH3pO5+th2YYkIIvmFIgYJgfZAiYG+AyGCxAgJgJoKYYEYAsqOGBXgBjfGBlACTvkaoy8bj2rCc61DiQoMBXBK96sm0wtbr0nmi3FLGvehr7HMqElTFtUdMlyLEJuS/E74gUCMu9Fb//9Jf//pMEUBXzIiydIxs1JHMiSJUzGQEeUzCYvCMUOBCTAigOcxKcL/MDIAXjCVQ8IwBECuCADNoiAZvAgAFX+dQXHgy7vHyOJRjKs3hrp6R+rnml+9Rw3WG5aIeepe6tJ5ZKTmFIFY3rqJbu7iK441VGSahO+Y675euutBnY3eXX2TiKqVNmUkcTAgRJtAIkLg4wuf//yh///iJUy/CHgORsfqza91WMxyU+ANMIk//uUZPUAA9wKwY19IABG4ThxrwgAD6wvDBn2AAJaKmDDPoAAlT7LVtkxsIQHMDmBrTIpRgsw4AGOMPRBfDCigFIwGsBvT6MAkAalymAdAOokABhtUGBTIB0XDCy4bBEXMknABUAxw9QViRo5CQqBrbCbUn0PQKuQtxNZYFHhas6HySWZODblHgyfYIzQ44kPyp02otSYf//pE3//JAACAAAGM2FGdKURZlVDyHtCiYZgHXB6Y0lmvkCsYAoi5luh6mE0GsY3JdQ0OejpDhgSgCtdMCwBBXYboR4Y6riWs5rjm5Whk3cI7T3EVP3Uz1PXLRDLKLD3pS/cXMwjTTL1jJj55jmXa7425Sfa2nu7uI+blop4S+iFD7RIyLxMKE0gu0UhZhL//nj3/+gBj+YBECSGC1KmRt+AvgaqE+8GMfsuhiIYZYYTWEPmKKAWZg7QliYRMA6GBbAHRgJQAuYA4AfmAigIyIy6pt0RAOhqHsUbF++pbHq9YcWKGPRWtKTt//uUZLeABF8MwoZ9AACPSYhFzyAADHQnDD3xAADUBOJXvBAEplbCWxHT75fUzsPdWvdTqAgABgdhkGQOAkYaaFBndLMGieYAYxosRhpgVmOMAUYIwA5gfgLGAcAgX5Y6HAIxmz2uvuWtACABhDgTiZhSSrmOqrUBuDLrWZfIqpmMkgfpg4k/m0sGsYqQ8ok6icrpGRCN0PUEYJfqGJacKrIyvfY8lUbHIb1afT2i1rOLWWNIIb6em5q9qpLatzaq1bd5fACQD8wKkhTE/dNMwVqE1viHzJIBAMOhNBZyCmg2kafTawUNBwMWMX03KImRv7ltVcxgBgLqYOciLmDDpIZouRpEZTySAGEZgrgCEg9qAQy4ZA2WCEAMQQlQxv270prQVfxnU6dPXt06/oXOavcrX7ftWj/9en/kHliAUDnahsZdSEhmPMXGueDEYOIeZhwThy4A6oAcKwcADN3bX1R9+gbVX+y3u////+n///v1/+kwU0AOMwEHsDGdGIA5//uUZIuI0tQJwqv+2RAuQPiBe7kgCWApFM/3BEDZBKIFz3QIetR7NlMNGzIKAX4w9h+jZgAkMPZRIw6geTNcyBQLLrmcRoTAeET0B027UYwOCrvfRRNaO3fYmS+9VZZ2l/T3nfXv+7VV7ofNpN8AYGINBhGIaGjKbMZ1Ky5qjEmGN6AoYWpMccAWKvwxcF5gsDigsB083bm1zj1LQ9tvv1f/1//+7SDEAmCqhhRgsi/kaE+RFG+nF/RqkYmsYKSFxmCAYMaLwP5gFHgGNOCCas6AkaV57gEJt8ek4OaW2U0aGsnDecZ++s45VzH7v27+z6qq/19td70IMRC4/GZjOCeTNYBYY08UfTCqEEMgcY/8JTBRdAwIAIBIhG1wunTfyhQssk5cKwiuE//+jqUweYBsM0hBMzW7nd05+8sHNpmH0DGXQEYwoUxDSUD7MKcnQzcQFDQOgDWYgGDDU1nU3yUS3RimfivaE38ijdUtmUtvf++rUfsO1I+lDaxncux1//uUZLSM0rUKQoP+4RA4wPiBe7ogCkAlDk/7REDUhKHFz3AIe/j4uSAAGCIEqYXZxZmMLvGxSgyZfCQRjkBOmOvEcuF5lXgtQGTEU0fXdehpX00Pc44n//////////1CijJgwQcOYUesCmQvLfxt3wbMaJSY5mC+BhBk92xxQRZlm75ziJBmXoLXGCCnjKL3qYwY90UdXelbll7Fsdpu3baW2IbjPwjNqaz8J1WuUBSyd4YGMxf51u0iCmMgYgM2KRoTfcPwMNlQMxgwuTDeSjKILWah+omONV6HiYRnyYUWZrcd+aEjxA3/07//3f+Q/s//9v61MCcC0DNzEJM5d1StOUxNaDWAmNgynoCOMMCHFjA2wzMwfsVxMIDATTAPABwwEkAmC4BeYD4ANIWMAaZbEzAYOj7BhSKj1BCViiHMXS4ercuccNdWRUhj8VDLyy2n6n8LtV6vkxikrWsag9mVVIaowrBRjEsR2M35x85jUYTNbXMMMsgAxFC3DDVC//uUZNcM8q0KQoP+2RA3wPiCe5kgCzQlDk/3REDyBKHB7uSCpMD0DwwNADxgAESBHXWWvxw5WHj7/anTq3Uao2jRRr/7+/t/T/+5Pp+7WYEvAPnCsIt5gV9s+dsAlXG2jcdBqUNOKYVqLhmIAA/BlMAiYYjkBFmO4C3piwgH+YCWBCqLmA1ARQYAFmAWAS6OBZQM0DGzXKU0YfR9rXqdxvibh1X1VbLY61dsK1T+O5bbpiqmbuT0TEPpszw+Irmf6n/4a72c8xue25m99XTvuJfK3Vbt0y1e99S96LlAyVWGIeSLCIFAVOf/6B3//NGFohQBjxA4CYyYUBGTKEghhw6XgaRcLymJwgRZgJIIYYT4F+GBUgTJhyQQ6YBOA0mACgDjgmAdADLLzARQAB3SVqsYVRFrospK8fNK8UvxmPea7zG//szf3itaJf5mYX5ldmS7ZrJ7GM+I26uzS3z3u67NW5Xd5106l8jt35bfa3XjctrztrN57aqvle/ncspn//uUZPMAA3YKQgV9IABF4UhgrwwAFAFPBhn1gAKBLaDDPmAAyLZQwQhMDhL//gZn/+kmATU1VGIKaAb98UhsdfKH8PEAYAk/hoqncGTyZ8ZDYUJhODOmNUCKYPAUZgFAOokgIIZg0/GZ0ami8Y1Yxi0ex/7fTze9vT6f9TGye3d//+8wMwuDIbI/ND9CE1ER0TOzHSML4NkwFRkDGlAjMBcGIOAXHANSgAZnSanN8qjHP////+3///////ZQYI0GEmLCudZljbfKcRgzRmeJJXZjswR8YBp3hqBgQGDANEZkgOJ27QZ+AAc9TJM38mqW+jQpFXt4u11MsYgsu5euz+16V6TNVXYY+n2Ldd6Nmn0dqeCEKzR6tzM+J1NVYhM1UAgTGhAHMrxoF/cIFQsZ2XItUDO7+tTH51iTPiq//////Rp+1dVmZmMBTA/TF6ij0x4AGTNM5RUjMAyNAkCHTE5xD4cGjDCkjP4RzDUkYGBJDtt/KuVu+5Pc2ldz6fTs//uUZKaI0oAJxTd4QAA54TiA7wgACqQpCg/7REDMhKIF33AIGf2WWU9vdZdo1CX/Xb9q/RqMCcDYxUkBDNdcuNjc9Q13SgTAZEOApEBqJGRekVsZqprOSgOkeuVuo7Nv3///Wn+r3f/////330GD/hehkPDY4aeO21G4sQB5rbg5qYoaAoGOuGUa6IgRhgpUGL2FeZghAIDVeGjyWcItthpK0P3qJHHop7qUWx7dG/Tnr+/oTT1f/fiiWeqv2MVQgwNIA0nuYxd22TagTjNOo7gyVggQaA+JKLmBus6MQFbhXHgFvuNc87Z7SSWf3dVf+9S9VX/9VKowMARQM6ha7DKUq7U3snKKOMTHlTCJRFAwJQIHMkrBSjA/RM4xGsApNd1UxcHDBg3M7GpeL7VZBFdYvVjMPDM+kWN55VMdDg5rJ+NppW1b6RsUaRXTLtHi0WUlhA0ppAOIaBTrjTyzVT87BtOskpjWzKWSwuABBNdV0YsHZvfdnoyyH++cG7s5//uUZMsM0loJxJP9wSA4YThwe7ogCkgnCg/7ZEDdBKHF32gQmZYygUhwVYxUnJjAKqvx3llz0lqXqO0eLso/2PTXZ/6mdttH/e+p32/9axGFyCHJlk7KybdciYGaIUzJsqxogZBkCcmD0bGbNIdBhqiVmdSCsehLmCCAWFDYjttIPljdlGAu24GkKFBDAjGBdACGMSmhilyxwSKRJyx+6nbKXV2B2t5Uv14j0WcXILRQ9dSblugYCIMpisG1mmOlga+y75heJRGM2E4ZTwp/8PBeEFum82VhRFC6j/lRzyQVTVrMkk7Hrveru9UZ6LrP93/r/t//9NUBzBFQoUybxYFNiTPFDE/ZuY0Vc+DMQ1CTzC/P9NIcJAwdT+TH/ByMnKwE8DQGDxtEukuyqTjh6mS18XS3Qm5mrYmpUxeMtlW1No9PMXOxRKhXh9pwU73rLZ40ZZEZxh02YrkgbhTAYwsF5p4uaGTotOZAwbRgAESmOSBgFqg2UWbGtJSNCTm///uUZPINg6MYQQP8GjA+oPi5c7kEDLwlCC/7ZEEJBKHF7mSAuAftASVOcu//fTR0b5pbtK1bop2UXp//9l/o5L2sD/5hGYbkZNslJGKeuyphp0/CZHslMGJ8gthh7pEmaKBWYMyVZhzA8HCXY9EAwfMYQHRjUzAYYiQeHwKVWXG46HD4xzWknz25q3vTHqaeQMFlJsJERa3UK3mz08YhClBly3kd5JcetCiYxoeGdTDAoA4MU1Nw00GWDLoe/NF9QwxEBBTCqSzQ8IgBoL0mKZkzhiSessx7dypYo65t4FKjxcolg77WC8z0Iqrqp/9S7aa7NPXad/1qMEwTYJaMlzdgDUmIB4wGCyAMT4W3TF8gzQw5F/TITAHMSY/UzGAXBySNeAjBA44AzVgl9JEZ3fPzqbARGLTCGR7oLhEnmKCOZUNbTUHhiN7qybmuY6lm5K25kVR4QQenaUKKEpkg8tPT9AAAAAsUbbbTBgFmU6Am55bGSvUHSC5maYGmF4WY//uUZPII0wcJwov+2RBFIShgd9kEDaQlCq/7ZEElBOGF7uiAIBQhsLHHDIs22+44lSpngeHTLe+/i6//1L/b3bCOn/+v2dH6TCCgvkzJtZDM7/JEzJxcHc0F9fyMQYD9zDkZbMSYIExcAGDQaA5MxwQGcmTih0paXqa7LmkWMv/PC9mNIrAlU4JniZDa5yFCFzCzHyaTihCefGGDp2/GFC5Mkw80UMYVspJPaxc6YICh3FjyWuCtz9SAOvurwsBCYHhSpi2HymCEiCerjQY4kEYAJWZBAUQgK6oFow0rLLyzj7btE61J/WM9KNrrt9/q//+zv9qXf/qx9NVpqqMAGB9TEVFN0yDBKlMr2bDjOBCncxA0CnMo+dOWAFMYKQNmBbMenAFAg0HKhNQ2b8eoxaUS9K3m3SJwHz5JCKbHNpbPgdZhxEyk8fXaNDITtebkBlyHCxQSwZkVnUuE9mbFjgukXkVIfYt54mA+Mc4jowC4CjBtg8NpIWoHKamGUBm5//uUZPGMg1EMwgv+2RBBIPjNd5ggDig1Bg/7ZEECg+Ll7uBIwOBWkJ2NrYodbEpre780URcTYMWsXs6GvPKOfc3u6Lau3kOr/+07f6vb7FWtCmqu/MC1AgDFhB/YxT0+cMr3QuTLNw4IwX0GeMNovOJgiMC5AMLQ5CVg7lgAHpdS7lN5NncfWZd3Yld7iX3Xm5Ydck3t8XV697pN95QaOpQvkrNrmKel6DUMCsIAKn/GguZuCG9zUdKDMNsNUwMVM4AAIYFEIAI1oHqvmTCoyIfJkrWd+kuxXX7f+3X6v319t37P0f6FAAAJqerMDlCiDCMUV4yb0uPNK4TkjO2gU4xTQCJMHnnA+EGSs0GzYVnEnBikLhjcj36s9lNhz1k9ikRl4Ui5qXaARjwxO3tLlGHhxlzDLXUzb67WNsqcpdg98a90ZbO7x5BndPHRWVUOBdMZcOU0gjaTEuWcPTtqNhw0CpZk34gSGGGDGEmCxFdlvL7oy6YPWeY7Vb2ZVq0t//uUZPCIw2wJw5P92RBEgShge7kgCwAnFS/3JED2A+HF7uCIalNv9/67vs9HuYJ9V3WS7jDkApk0dk3ZOdASfzuaZr83U0vvMYPD7TBrAmcyBAB4MFlBVjEbwHA5PcMwBjCTU0FoYXBGa+AEXmpcQeEpAy6nK3ihuSCer3ayzbf5nIZirnFft2zP2/X0/4hr/5647Zst8av2e9wc7+d5WKv7XzKq7W8ndbB0gTFVTRgNA6iIskw41BjJ7RvNUfHM3i2MKAxBXEABsa4Asld/GC2uuuc0eq27DjvY/md6UqQlte5936Ni//T1blY127aYAE0tVGBRBmphXi0CZOOuXG17KBx8DXWGg6JsYYoBBqQAxGHOVCZBYHQ/lAzMoEC81bsDTkav6w3ks5yTHTblnSPpf8rX1aM77pxwB+dE0z1v1OjWz3tHaguK3Tv7wBjlra2Nwe5lv+Ihi138l+nTTretsxlE44QSc04WRzV6YgMYVZQxrhAzA4DcMo4B8yyI//uUZPqIgywJxEv90RBDgSiCe7oSDuxDBA/saIkShCJl7uRIjAJ6MTa8NB5F+phpsRrS7S6ur1s29Wi5nd/2D031fFvu/7f6RgMJnB8jNAgJw1U14lOBbUETL3U+0xBYMcMNAi42AgRjA3TLMDEJoK/wc3GEBxq5mza1YgO2EnMh1rHgJpTHlQEagoLBRBoqRKlmkxdZKM4+lrlPzGxbhdJliugVUjsvFEwum+hjBNehQ9h4M1TMVAPjAFMSMzoBg0Nz4ToK/TTUSxCbxsEAII0KxBDYqObgDObooMCiUOf35X72eXT4q3/01//9n/mr/fQqMC3EBjCB4po1qQ4qOQ6OQjBcmB4xcoPVMEuEnjFrgG0wRYSNMLQAkDq8xrMYUmd2ImvMTMAhSG4hY+57fKbEIsbGOaVRJNQ5CjKn7wRIrEc2UM2rQ+FWn2NMnSiVFUJi4bwFcis6LKHHAWj05Bwog0TwuQBiQGnpeEZELLBshKaGpuZsYm4bQE2h8wIi//uUZPYIg5QKQzP+0KJBwShgd9oEDXAnCC/7ZED5g+JZ7uRIIxCwLEgWNB91EKb2saGu5qWXP4ulgVb/3f/MK+z1xvbWhNzf/T1L/3iEYPwCTmamjoRjjLIAcvmKamZlKpJjIYLqYEuJ+ERIYanNOT6IYQIAY0gKrCPJwsyLzkcClnMgaVK3MGbctV6uuP0bMf7Xz/j2p71C+t/7uu058v6l/e+UXfM+fbtBlwtL0A5ite0nS9O6ae70rK0fT260ZbjjaJLFAOYbzJg/TZwCvJwMVYKXgEUAKrSqBJZJaMehhA9wq6vZqTRzCbOWW5a7j3jFf7P9G9n6Lv//PVUwRQNVMLjiWTFLJL857QanNR/REjEmwswwAlljKOBDMVwyQzDAYz1Qk0kLFCAy1NUGdmJPVfGjmhseWc06IBhNBZU4P6LBrjR2Glw6mEy5xLVOe+cLAFAttbvXFzCGsgYkhi8IMa6m+JndKjIBKpkklBszwQs3+ro+ojo/tCwxBJkw//uUZPSMg5USQYP6QiBDAShhc9wCDmBJBi/0Zsj4A6O1zuwMAWQwmBGGxdwtMmFE4Cs4JiulDizWakFkHq6Bf07EV//f+v/8jYn20fzYBFTrJgSYGiY5oKdmHhHUhsG5A4Z2YPAmJdAXhiVnQBCcxmt4xyEM5CCetu55mPnO369oN3EFmWUqAYOunGrTs/S+oM5EPii1HdW6USzC3TKNPopNOvCWKOzLZGhD09SQCOm2jBsNDJKYDtGEDy1OTwZUAM9RiWSGSwWFelBkcSY9E0Gi9J832twsQfZQtdX6ew59dXRhL/Z791id/9vrMJ7EejHCInY3AUuyO2mUzTd/xpAwVcP/MEdkwywQyzA/UoMmEGs8lCHqICgJzwYTANrbdaLP7w4uvbQLk/i3D3K+aipWb/vdfteO6/moPt//+XDNdou5ptNG//V914bd40Ha/6sYe5oR8ml/PV4/fPtstjAw6Oq5gymFxzU4B+PndNMhi6BDCGkYIlQRUrCgExoE//uUZO8Ig18JwYP+2RA/wSiWdwI0C/wnDy/3JED+BCKl3mCAoEXve193Vy73MVz6pgs1n28UtR0f/pO/2/3pN/9kcgXzBMAagyrcrINMKYVTfMUlw1VYbEMU7AMTDlQ4M28NcwpR2CJXY0atFocUCgKau5/0lnCsM44w/BFa4v6vESPj+dfqLwk/uLedZHy/+Z05XiAbtCB/Ju3eIHP1JlHc93ccre8f88i22//zm9XM8prWtoAYJQQ5iNmPmaUt8bUYhhlcIdGNEEyYOvB8ENhS8jMEHI0VE1ht63je60SrVL5I6MGCmX3f3I30/ov/b/d9f6G/9CoAJKKqYOIGpmKWpn5lNipUatckqmeJlu5gkAXYYmS8fqiWZauOcXh0ZggcEYYAGdUpAco3BQfWUUHFtLIOPYFTVxfUgiTJVGEoVLXKY9MYTYL4aa6JHc5qF5XQAKUSL3LcWkx1RaiXs2gCKVpjC4JN/b8zRw+zR4E9MT6vMvxJMKSgFqHaeJBD//uUZPeMw44LQQP+2RJAgThxc90ADjQpCC/7ZEkJBSGF7mSAEmLZtQJqjqqN8beZrbSxvq3UOR+wj+OSvsT+r7vo0N7kbruuowQ0BzMauKcTO2x5EzSFA4MfKKNDDygX8wdY48pHsxbmUyhCoLEmDgCVRMLgIZLKr1OK3+r6vxBBY0BNawKGo42KMeLvQBBqSVb3BiksXENJgVccEzjqXq7hz4bQqlhN7SjHorWRoEyNAUaggDAhBoMN4qAybEZzXfKKOL+4NSSAMGxwFwTJdAi0C801Iy+fUHRiUB4daiMqAECiqP091Tada9/+593ur9RQZQS/b/9ReiWJYxxi8jPCxANwI1s7Vu+zLHq5M8UokxbQPzM/AtMDsbcMDlDAcAEAOicFgB2Xxfsyhc8vvf5koQWlqHMdqSXfNNILPPr04vpdajtTf0Gbza7v11He4U7Xx1YEGE+CNhjZB6MZH0bimVdJ75kKpeWYyKHBmEFgIZhQ4EaYBOFjkwLIYBgA//uUZPIMg0UJQrP90RBBYOiWc90ADbhPDk/0RQEXA+GF7uRIQFryUAKMAGAJ0w3HuUoaD6jS7VSKWEa6WC2PaswLjjVKkInnona0A2KvnVDReVFkEvQlTkI7E/vIlWKtrFzaI4sA/xtyyNMuPaTWbykkyvFJ4OUigYDJYRHwwwQBIMisEwTEhAcExncTlMPoBYjAAgE4MABzASAD1AIYF2AIQ6OCaGpQaEslkw8aWcXNzCkC0tebnkG7x+hNybjZrki8wVhSnolR1labUD1tQq4Yzdr5IOHXUkUujOKztT7lbgqtpsw6ghRug0refPr6RieNj7jY1Vo3RM7rNTyS6CDXnz72oQgf01dKHH1GKIaq0MRRRPsk130aqxaF0k7/////////9SDv/////////9SgAogAIpQoiaYa+XHbxr2xnzge2aviHymIZwWBxKS/4Y4uASGHNABxiUwP+YgYBPmDkBc5h3wBUYCyAgKxmBRAGKChWBNwMeeNmoIaaOMO//uURPEAAtcVw5V4QABo4ShBr4gAGgYE/hn1gALqL6AbPmAAOKwmtZ5K6yBjY52p0W7a9li0d+xXciaX4vmMcmSdJxUlHBCBAkayrC81zrskoVwst+gWQQfG0+AJA2G5gWdqyZrSpkrZoLohhzsac6TAkk1wnpkJVqdJtL3Z0vqBl761yyz9pY3//iE9//qElTItWak+9BgpMw2TADStCXI1jVQLNPvUUTB/hl4wqwAEMRmCbDCQATUxqEQaMIyA3jAmgDCKGA4gJ7MDACwGZdKhgHOPuo9HO4i53qIlHjYqIFkIpS2NFWVEIRWD4xZ4wc7kOdZhcimQ5bnJQUUWGXE3UxXY9CI5DiTi58iGMeQgiyJOqFc9iVKWejkjCkGucxXYgqdyWV2OawoxHe6ujIpF/////QQd//lRKaG0l51oAwGCansaYo0JiMJcGXWcWY1RCxg+gBmZ8J2YGYM5legekgQAOAJgYu1DjD70DNAdpq5QPXV1FCFVziFhrNmE//uUZHeABVVwQQZ8oACHqygwzwwADgApCl3xgAEEhGILuhAA0+VzB+ovIpBmWEMbDQvSOe9Feu5HEi/mlT4pwjBiJ7J+RnKnNselNJM2NxXxf5FLGpQ2N/4DNgwYPEwFR//x7//8usVVlMGCD2TGqjr8yGQFTMmxW7DM0SewwlsBXMCFCBzDBQPggCUjA4gHshAOTAWABUuoYAcALu6/tjIU4dM1h6hRsDBafecfM7Awavic2sMSOxQfLN203uBxxEVlGMJIAQteF1ElFZB2cvqbERwVysWZVIDAcDDLRtDfvpjf3iDudQTBwZgsdZmeIQkJ6C7oiwCsRi3a2RFDqiWmJRA+2tm6v1+LL7r/+jFF3ImWX////WptTBuQf4yMBKMNLPWpjHmoBM01AYlMRZAYjBQxB8wj0EkMmesNHyHMUB3MQgLL9ExpItQLIoJQPKAThAqkX6Qo82NogbH1aeXCY3Lkf7feGfOpy/lSI/2uZn6+TeTfMydvNvlpG7V8//uUZDOMg/dcQQv9GbJIYPhle7kSELUnAA/0Z0jyA6KlvvAAr6ZLmeh3zVUY/89c71bMQl0rhLawqDskAHiDAJBUMIMlwxD0XzK2TFPDo0NZBLMAlFMawGJM3+BCJQRRsUshRp4s+VxKI5Ao5Q89d7PsrTWX49WpVHpO/1L66v3qb39KTAhw8QyCliyMOod5zHOpq42JUSJME1DDjAdhU8wTUEiMqcIG3CMCyQAxgAAGTD4NnRmpudotWJ+tc4Al0iIq8q01JARhmhohn5itaRxS2emeS7i3pbdJUBZ/tPIzNDzhmTX64iniV+9OXZjrmVwmxkrg0tcshZBfD5fsGvB0EXL4udKM3TAe1VTZhwec/TnARqGsdTHEa+ZpFo4sQUEBQFqQUUU9aoXLiqyWmpGK3yTbvUyn9Po1d3/q+d1f9CEdNCEVACABg/wSqZFOYAmnMFuxmaLeeaFQP6GKBAepmd6JnwThiGY5xuIhgkQIcNhYAIw2B9mlHqCqYzML//uUZByIw70ZwSv9GUI6gOiCd5sQDslhBi/0RQkBBKGJvuAQwTnOg6cvhXOA485CGEqI8Pzqkbq//WsxpNn103+8l1e8jy8V27nu/0QC+GP+bHd33tTx9Dne+RRCd/ZlfvzhKsoVApMYG5N54ANGMxMmeo1eOgrsm6ACNpEGt3VDuBnC10W6aEE7NF6NPfrvr9WrfiDlKfq/dr/+pkymAfg95iaSB6ZtqR/GZoLSxlSZIiYRSCemHd5nDQiGBstmlIfGKAxEQUsuFiLl9Bh0VdAylYjugsHYZ9XRr3Td09yKkzIxEXd2Z22t3cjsf0Z3Vn/NlO/nIjR2Yuh3d5Xk+sy012d/Wr1I7oV2GaQodoZzzp9CQABj5UfMpGXPrmFHuGglGmdgmGBB0gpwAYomiDFLd42mHcKzzNjVXpOBJq2QnKo21019v9f+nq20e3sT3bfsSmWMEPBYjFfBYAwxM7lMp9RcTEgiQgwsECkME3nOPApMVorMXA0IhDGgtQ4g//uUZBYMw08Swgv9GUA5oOhxd5gSDmBVBC/0ZMkJBCFBvuAQwDn9mt0NpLKU+dI+qKBF5gTE4mYYp2qlS6s8lN59BtbIu0gdTaxguTcsRkbXHnn3libqnSJ8vaXm1I3sqinIhQDDE2ADY6YDSPGzw/aMjFcwshSbYAzTNEATHZVKRGI2VvToltVdikI/1eltVX/6aPR3+lW3196UAMAmBjDDqUagyBhC+M0UStDCLC90w1IGSMdH3PJBFMpnlNegYMSA2UtRMMMgJbwWJBjMSrY8IQRgj/PyQWTMkO4G3OsbMRLds++eu/Z94D7+R7//ll5D5J7W5zZZd9431T7Vs96df9vrJ/+jp/cBano+tCSd2bm55qG83OHOi8mZYFmBIykU2kPESwQB2et9froa8aOeu+YDOxnVZvYEv6Urfezeru03EpLt56GGVr/bie+7uidMIYIEz/zRzOBGtOSmes0YW0jD0EpMYBxOBhHMZAiHolO/RC1UwZfIrOrmzJWZ//uUZBeMwtIJwovdyRA0IOhxd5gSC4ApCC90ZMCyA6HFrvAAKrofPvgmxNSqjU1vs581qc5jgKOeKNyRS4YOlWPUfSyg1XZsZzPeRY5x6rBAoB5A2ZpZN5rTL5/EoGQyIAgKLaFRRC4Qk7ZhwDzkZrErjSv7af9Xp9X//0/7O/+6tqmgIYGgjZikW3mvJOydekJBtbKpmO2B2YhgYceBqYNKAZgBGgEUbZGSgJLyjMLYMEwifS+hqzHsb5cCISSsexts+tDWKRRwMK3LUIY4CvcIidjnPU5+1taEuRJUPUwCpz/DDDGSjTh9j4g2AVlAgkCLKo2nFFJHUlIN3oCDuyoUahLt3v3f/v/2UQAzPVRMTj/UIMilXw16yuTOxFcMI0Hwwo8AeKDbZNMLjDzyxCFzCm2aaFHVZJLRer1JT1r0sVWgnXPU6NheijN2JtXprV7f2XpAAACqjjaIIKqwgajdUU1+LP6rMCBJBw1JFpsLl9fNq0K3CPQ1NrNGv6+j//uUZDoIgkoHRLOe2BAygPjtb0sTCwAhCA9zBEDGA6GBzTCI/v/3ej//06zB/EuBsSBrHq+nb4WIbPgNJk3AmisAO1BkyLADToFLvq/Y0ETnqqxV7LgiXa8/tCiHRi3pVtbc+ZZWtaK7D6kdoik7VsIvc5lrbQhQqRFXSiULdVjErQ+1HA0GNDyk3OsS43tFDPYCEO4DzRwG3gvGdViR9LP/t+m44i1fQn//b/pySbf1+ohZ+xUCDIQET2s5DeGMROl0Ks1JRkzD1C5MTR42YFzF4cNZgAEBQaBzMy71UBhh4SMpME6C8NaGXLixuKOc6q8rrE78W3v7UGbaC232abr6G2SLjkX3fJ8ADAmYDZ5h/JHV0+YziZjYPClgSgtxU80hzlBOMtYvigcqh9w1Kv9urb6v/f//6PvWQBggB1mAMyCY8bHhweDBnzVpGmotGFqADRiGFJVg4+kryYT8kyZD+6YPOeJSLzYiF5jTnmNA8OyyEUR3XOYei19Gur0T//uUZGUM0p0Hwgu+4BAxQPhxc0YkCpQlCC93AoDPA+GBrhggCndwHSkl3k+Rd7HvYw/StCo73s3DUTyRoMsw4yEIyQ0MZbAuFqcKofhB5czH/3/mXzj3OkP3+r0U+309C9nt/0+pWpUAABmlmkxGBjnEnNx7DPM1qN77cy0KiU1JNjpdV0xVq8sd+tM9uTSsJLKizKbOvd93Tkv9EUF8w+XfpZt0RRZP/6SAHQAFlsCrQddF501uAnMKqQsVidI1QpesgCdIPYGS1qy8fJCpsiyh2v071/9P/0+//9KP9/vbcwDFQKccbfIgfoU+IZWMeBRAEoPT5gr4lQTicaXsuzCDhhtoeMNHLLCWePD2mQFRbYvYt+vS/bo/M/Yy5xtOUnswxNuv2aXuqK6pi01GXXQRhg79uMdEBk2KFYczOtLXdBubvtJJZym566mfS2x/93R7pH8Z+hj/v9Xf/1IABR++utSBp1sGibUfbwBpJ8g4rgm4jOS9HpRSf6MbZLSV//uUZIwI0g4HRMud4AA2gOhhc28SCTAfDC7vRADPg6GFzbxIODGTNBsWcHK1OSKdLrxju2pDdaAMbI3dHd0+ds9nLzz9LqOBgEGtZiV+dGKHJF48skrRXKSjcUxhkeswkUagZb7+91H5D1a/J/f+77P/fo8rstp3AQYHB+Yg3ia4cSezYSe9wxnchDsgH9gQ0ErrOoeyf8GDoOAUKBYqHWNE6TIOZJAo9RpJkeeoVUy1K0nlLKkMtyaBY43SZTaLgAXWRd9gk8pi76HN9S2JeExCk2DY9isF1lKyURH5GLwPB2ypmMeyxNGhXXT67P/Z7+//+/0b+ulHVckwFBM1OfEyMpo/b7g5eXUzgAEKnwLLhluCBUlAYzPONaGQWhoDkKskMREy1crSM5C9joxSc9FMlqyjOc0jJXdz+uRTBJbRc5nmfL8PXczvmZnqcJMGxasRltIRnWrEp8hT9OP4xkvmoBp9GuxtpQkv7tAxMc9oc9cfRA8UuYiiEbw+lLbx//uUZL8IwiYHRUuZYRAxYMhxbywgCrQdCC7zQgC1AuHFrCSARCLlzd239juliXd6f/Io2dWn7PX3tpq9CuMMGRAMZLKO7zcPSuPONzEMHQjIAMMiwAMARUGgfZrBT+sVlAwE9q9xFRmXoRFOTYWKG/1yVwyUaG5mqK46XXOmSKbamTnFzgcH5Ez2m89dEiTJu0Gbsg9cmO0qCIWLzsdbCPJqYIg5NIZyDJMzNqFKx6doyEaP7WmRkzAkGxAw6A1XM80c6a0eK41ML1ybS2AQ0EiAInnhFjKZ0MJfm0LRQLHNKilzx2bKPSNPoCTHmUmCO+6wjtstZvXIbG2K83UhdSAJGsYMcguJ2n9G9BAUGZH4fqROKz3hisnoagmomLcZzn2iBMkNEjqpBbEp1PjMXa8izrHpKlM0R6ll0bbKK5E+kY5SKQ5F0PVzYpZ+UbWwpoRvEQ7QTFk1P8jK8NmfNUThQHR8HTgdxqMAhkRnMQ0OGsC1kfZSiVVvB4PEjY88//uUZPCPw3lVwIO7GTIwILhxaykSD5WI/g6YbQkSA6EBrKxIgLWBTStLExV3ek+G3htzWs778oLrFP32121dG7aLa35NowVbYh6rkorCMrKgwFCqyZk2GlVpvyQgXKkEqiv4BgSIJB9QsRn1Vxkz240LMjL8kOLC5EpKZctJW39655chnkXJTbdTlJqfKcmEbM8jNU9pprpmslP03OK5eeicntgsabX1unCUvSZbntXS5ocM9VBhZEQw+RAqDrMiyg8Co5w9cOPCSkkWFUswMgJxZKhQohVljrENzaXyLSqDwqLiB2NfcslRlbmOAipx73GhemPFiyUUWqkRlLukbdwHIfse5yvloOhFJ2EKspvv7dKz+IBjMR+YLrtxKWI3ZUj/3TWWuofqrvtmsjLMrqbjvUZDrArov/so8lVvTOr6N4478eHTOwXisAHKrCtoNEXGdYFBkIONQOFqbHMbVKtg/mF4FHc3XL32r0RdC6ViBh/1Njpq2bUm/74w3X9N//uUZO4I81hZwAOZGTI+QKhAY1gADE1NBs3gYokgg2DBhgyAd7qcX3Alf+Fa8zD+ecU4FTc4NcF+9qX/Mippe9YC+f+lAAatfRaIRNOh1UEFgqXEpgyXRrXWjJc7AZLrPXZMY66pUqtVSAjVSUqpBhXrszUTD/qs1X/bX1h0mP2bUul9I/qxVjdJm/gYVRJFON6qf69WeXVIMKKHSjbEzWH/8Y1LNhWIqUEEQWBoAo6tqqTPSZxMDBSawCb2ZtYBUBbVSh1WagJawCL9Yx+ucY71Ywo1EjCrp1q+CuIoi/iIqd/U8NfbJEZUt/1hpUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZPIMYpcZwZMvGKJWY+gBYMMsTIFs8kwwZckRFB5UkY0yVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" ; </script> </body> </html> diff --git a/artwork/boot-animation-source/render.py b/artwork/boot-animation-source/render.py index ee85ea06a..66a32dbcd 100644 --- a/artwork/boot-animation-source/render.py +++ b/artwork/boot-animation-source/render.py @@ -18,12 +18,12 @@ W = int(sys.argv[1]) if len(sys.argv) > 1 else 1280 H = int(sys.argv[2]) if len(sys.argv) > 2 else 720 FPS = int(sys.argv[3]) if len(sys.argv) > 3 else 30 OUT = sys.argv[4] if len(sys.argv) > 4 else f"droiddeck-boot-{W}x{H}.webm" -THEME = sys.argv[5] if len(sys.argv) > 5 else "light" # light | dark -ONLY = [int(x) for x in os.environ.get("FRAMES", "").split(",") if x] # debug: render some frames only +THEME = sys.argv[5] if len(sys.argv) > 5 else "light" +ONLY = [int(x) for x in os.environ.get("FRAMES", "").split(",") if x] html = Path(__file__).with_name("droiddeck-boot.html").resolve() tmp = Path(os.environ["FRAMEDIR"]) if os.environ.get("FRAMEDIR") else Path(tempfile.mkdtemp(prefix="ddframes_")) -tmp.mkdir(parents=True, exist_ok=True) # FRAMEDIR=... makes a long render resumable +tmp.mkdir(parents=True, exist_ok=True) t0 = time.time() with sync_playwright() as p: browser = p.chromium.launch() @@ -45,16 +45,16 @@ if ONLY: print("frames left in", tmp); sys.exit(0) COLOR = ["-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709", "-color_range", "tv"] -SOUND = Path(__file__).with_name("droiddeck-boot-sound.wav") # made by sound.py; muxed in if present +SOUND = Path(__file__).with_name("droiddeck-boot-sound.wav") cmd = ["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(FPS), "-i", str(tmp / "f%05d.png")] if SOUND.exists(): cmd += ["-i", str(SOUND), "-map", "0:v", "-map", "1:a"] cmd += ["-c:a", "aac", "-b:a", "192k"] if OUT.lower().endswith(".mp4") else ["-c:a", "libopus", "-b:a", "160k"] cmd += ["-vf", "scale=out_color_matrix=bt709:out_range=tv:flags=accurate_rnd+full_chroma_int,format=yuv420p"] -if OUT.lower().endswith(".mp4"): # H.264 for sharing, previews and anything that won't play webm +if OUT.lower().endswith(".mp4"): cmd += ["-c:v", "libx264", "-preset", "slow", "-crf", "15", "-profile:v", "high", "-tune", "animation", "-g", str(FPS * 2), "-movflags", "+faststart"] -else: # VP8 keeps software decoding inexpensive during Steam startup +else: cmd += ["-c:v", "libvpx", "-b:v", "2M", "-crf", "10", "-deadline", "good", "-cpu-used", "2", "-g", str(FPS * 2)] cmd += COLOR + ([] if SOUND.exists() else ["-an"]) + [OUT] diff --git a/artwork/boot-animation-source/sound.py b/artwork/boot-animation-source/sound.py index f0f7eb1b8..6919dfbc9 100644 --- a/artwork/boot-animation-source/sound.py +++ b/artwork/boot-animation-source/sound.py @@ -22,15 +22,13 @@ FPS = 60 ev = json.load(open(sys.argv[1] if len(sys.argv) > 1 else 'events.json')) OUT = sys.argv[2] if len(sys.argv) > 2 else 'droiddeck-boot-sound.wav' FRAMES = round(ev['DUR'] * FPS) + 1 -N = int(np.ceil(FRAMES / FPS * SR)) # exactly the video length +N = int(np.ceil(FRAMES / FPS * SR)) rng = np.random.default_rng(20260925) -# note frequencies def hz(n): return 440.0 * 2 ** ((n - 69) / 12) D2 = 440.0 * 2 ** ((38 - 69) / 12) D3, A3, D4, E4, Fs4, A4, B4, Cs5, D5, E5, Fs5, A5, D6 = [hz(n) for n in (50, 57, 62, 64, 66, 69, 71, 73, 74, 76, 78, 81, 86)] -# ------------------------------------------------------------------ building blocks def tt(dur): return np.arange(int(dur * SR)) / SR def edge(x, a_ms=1.0, r_ms=4.0): @@ -70,7 +68,6 @@ def put(t0, st, gain=1.0): if i >= N: return j = min(N, i + st.shape[1]); MIX[:, i:j] += gain * st[:, :j - i] -# ------------------------------------------------------------------ instruments def wood(amp, f0=520, size=1.0, thump=0.0, dur=.35): """Hard wood / toy-block clack. thump adds body for heavy landings.""" r = [1, 1.93, 2.81, 4.13, 5.62] @@ -142,24 +139,19 @@ def bell(f, amp, dur=1.6): y = np.sin(2 * np.pi * f * t + 1.1 * np.exp(-t / .3) * np.sin(2 * np.pi * 3.5 * f * t)) * np.exp(-t / .7) return edge(y * amp, a_ms=2, r_ms=40) -# ------------------------------------------------------------------ tracks from the engine tr = {k: np.array(v) for k, v in ev['tr'].items()} TT = tr['t']; dT = TT[1] - TT[0] def at(name, t): return np.interp(t, TT, tr[name]) def speed(xk, yk): return np.hypot(np.gradient(tr[xk], dT), np.gradient(tr[yk], dT)) -X2PAN = 1 / 330 # world units to stereo pan +X2PAN = 1 / 330 drops = ev['drops']; T_L = ev['T_L']; LOCK = ev['LOCK']; HOP = ev['HOP'] events_log = [] def log(t, what): events_log.append((round(t, 3), what)) -# ------------------------------------------------------------------ cue sheet -# Kept deliberately sparse: one sound per story beat, with silence between them. tsamp = np.arange(N) / SR -# 1. the stack: each piece lands on a muted note of the final chord (D, A, A-echo, F#); -# the lock then plays them all together ai = drops['archDrop']['impacts'] put(ai[0]['t'], pan(tock(D3, .40, decay=.5, sub=.8), .1)); log(ai[0]['t'], 'legs land: D') @@ -171,21 +163,18 @@ for k, imp in enumerate(bi[:2]): di = drops['domeDrop']['impacts'] put(di[0]['t'], pan(tock(Fs4, .28, decay=.45, sub=.45), -.05)); log(di[0]['t'], 'bowl lands: F#') -# 2-3. the balancing act and the jump play in silence: the stack and the lock carry it -# 4. lock: one snap, then a four-note chord that rings out put(LOCK, pan(tock(D3, .24, decay=.28, sub=.8), 0)); log(LOCK, 'lock: soft landing in the stack voice') for f, a, p, off in ((D3, .24, -.1, 0), (A3, .28, .2, .02), (D4, .30, -.2, .04), (Fs4, .26, .15, .06)): put(LOCK + .018 + off, pan(epiano(f, a, decay=1.3 if f < 300 else 1.1), p)) log(LOCK + .018, 'D chord') -# ------------------------------------------------------------------ room, master def room_ir(dur=1.1, rt60=.75): t = tt(dur); tau = rt60 / 6.9; irs = [] for ch in range(2): n = rng.standard_normal(len(t)) ir = lp(n, 7000) * np.exp(-t / (tau * .6)) * .5 + lp(n, 2200) * np.exp(-t / tau) - ir[:int(.011 * SR)] = 0 # pre-delay + ir[:int(.011 * SR)] = 0 for d_ms, g in ((13, .5), (19, .35), (27, .3), (38, .2)): ir[int((d_ms + ch * 1.3) * SR / 1000)] += g irs.append(ir / np.sqrt(np.sum(ir ** 2))) @@ -196,13 +185,12 @@ out = MIX + .14 * wet out = hp(out, 28) fade = np.ones(N); nf = int(.45 * SR); fade[-nf:] = np.cos(np.linspace(0, np.pi / 2, nf)) ** 2 out *= fade -# gentle peak control: soft-knee compress only the loudest transients, then normalise pk = np.max(np.abs(out)); x = out / pk knee = .5; ax = np.abs(x); over = ax > knee x[over] = np.sign(x[over]) * (knee + (1 - knee) * np.tanh((ax[over] - knee) / (1 - knee))) L0, L1 = int((LOCK + .06) * SR), int((LOCK + .76) * SR) body_rms = np.sqrt(np.mean(x[:, L0:L1].mean(0) ** 2)) -out = x * (10 ** (-14.9 / 20) / body_rms) # chord body held at the level the team liked (-14.9 dB RMS) +out = x * (10 ** (-14.9 / 20) / body_rms) assert np.max(np.abs(out)) < 10 ** (-1 / 20), 'would clip' import wave diff --git a/docs/agent-control.md b/docs/agent-control.md index da7e78098..c7d16c2f9 100644 --- a/docs/agent-control.md +++ b/docs/agent-control.md @@ -75,49 +75,49 @@ artifact or file error, 7 debugging tools disabled. ## Commands ```sh -tools/droiddeckctl state # schema 2, below -tools/droiddeckctl start steam --wait # --ui desktop, --url steam://..., --timeout -tools/droiddeckctl start steam --wait --reuse # keep a Steam session that is already up +tools/droiddeckctl state +tools/droiddeckctl start steam --wait +tools/droiddeckctl start steam --wait --reuse tools/droiddeckctl start desktop tools/droiddeckctl run /usr/bin/foo -- arg1 arg2 -tools/droiddeckctl wait ready|idle|failed # --timeout (default 90 s) +tools/droiddeckctl wait ready|idle|failed tools/droiddeckctl stop tools/droiddeckctl resume -tools/droiddeckctl launch 489830 --wait # Steam game by app id; --wait = until it has focus -tools/droiddeckctl wait focused 489830 # gamescope focuses it AND Steam lists it focusable -tools/droiddeckctl wait game # any app but the Steam client (769) has focus -tools/droiddeckctl quit 489830 # Steam's TerminateApp, then SIGTERM/SIGKILL -tools/droiddeckctl focus # focus properties and every Xwayland server's windows +tools/droiddeckctl launch 489830 --wait +tools/droiddeckctl wait focused 489830 +tools/droiddeckctl wait game +tools/droiddeckctl quit 489830 +tools/droiddeckctl focus -tools/droiddeckctl input key ENTER # names (ESC, F1, UP, A...) or evdev codes; --mod CTRL -tools/droiddeckctl input text 'hello world' # as US-layout keys; --commit for text-input -tools/droiddeckctl input tap 640 360 # session output pixels; --space fraction for 0..1 +tools/droiddeckctl input key ENTER +tools/droiddeckctl input text 'hello world' +tools/droiddeckctl input tap 640 360 tools/droiddeckctl input swipe 100 600 100 100 --duration 400 -tools/droiddeckctl input pad a # a b x y lb rb start select guide qam up down left right +tools/droiddeckctl input pad a tools/droiddeckctl input stick left 0 -1 --hold 500 tools/droiddeckctl input trigger right 1.0 -tools/droiddeckctl ui dump # tagged, labelled and clickable nodes; --all for every node -tools/droiddeckctl ui click rail-steam # by test tag -tools/droiddeckctl ui click --match "Play" # by text or description; --index for the nth match +tools/droiddeckctl ui dump +tools/droiddeckctl ui click rail-steam +tools/droiddeckctl ui click --match "Play" tools/droiddeckctl ui set-text some-field --text abc -tools/droiddeckctl guest -- xprop -root GAMESCOPE_FOCUSED_APP # --timeout --env K=V --cwd --stdin --check +tools/droiddeckctl guest -- xprop -root GAMESCOPE_FOCUSED_APP tools/droiddeckctl cdp targets -tools/droiddeckctl cdp eval 'SteamClient.Apps.GetMyRunningApps?.()' # --target, --no-await -tools/droiddeckctl env set DXVK_HUD=fps # next session only; --persistent until cleared +tools/droiddeckctl cdp eval 'SteamClient.Apps.GetMyRunningApps?.()' +tools/droiddeckctl env set DXVK_HUD=fps tools/droiddeckctl env list tools/droiddeckctl env clear [next|persistent|all] -tools/droiddeckctl override gamescope ./gamescope # first on the next session's PATH, removed after it +tools/droiddeckctl override gamescope ./gamescope tools/droiddeckctl prefs get session -tools/droiddeckctl prefs set session some_key true --type bool # returns the previous value +tools/droiddeckctl prefs set session some_key true --type bool tools/droiddeckctl logs latest ./session-artifacts tools/droiddeckctl screenshot ./screen.png --display 4 tools/droiddeckctl record ./clip.mp4 --seconds 8 --display 0 -tools/droiddeckctl displays # logical ids, names, sizes, capture ids -tools/droiddeckctl wake # wake the screen, dismiss the keyguard +tools/droiddeckctl displays +tools/droiddeckctl wake ``` A sleeping device suspends the session and hides the app's windows from `ui dump`; `wake` first. diff --git a/docs/development/stores.md b/docs/development/stores.md index 4ce7d1aab..79b1e2b73 100644 --- a/docs/development/stores.md +++ b/docs/development/stores.md @@ -39,11 +39,11 @@ volumes. A successful run removes the cache. ```json { "version": 1, - "store": "epic", // gog | epic | amazon - "id": "Samorost3", // the store's own id: GOG product id, Epic app name, Amazon product id + "store": "epic", + "id": "Samorost3", "title": "Samorost 3", - "exe": "Samorost3.exe", // relative to the folder, forward slashes; what the icon is read from - "launcher": ".droiddeck-launch.bat",// optional; what the shortcut runs instead of exe + "exe": "Samorost3.exe", + "launcher": ".droiddeck-launch.bat", "args": ["-EpicPortal", "-epicusername=\"Name\"", "..."], "env": {"FUEL_DIR": "C:\\ProgramData\\Amazon Games Services\\Legacy"}, "installVersion": "1.4.0", diff --git a/gradle.properties b/gradle.properties index 1eda82b0c..3289cc4c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,13 +1,9 @@ org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 -# Reuse task outputs, skip unchanged configuration, and run independent tasks in parallel. org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.parallel=true android.useAndroidX=true android.nonTransitiveRClass=true -# The NDK the compositor is built with. CI passes the one the runner has installed -# (-PndkVersion=…); this is the fallback for a local build. ndkVersion=27.3.13750724 -# Enabled parallel sync for Gradle 9.4+ org.gradle.tooling.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5f0709380..b29e48e44 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,3 @@ -#Fri Jun 07 19:24:10 CST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/gradlew b/gradlew index 4f906e0c8..5292cabd8 100755 --- a/gradlew +++ b/gradlew @@ -16,16 +16,7 @@ # limitations under the License. # -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link PRG="$0" -# Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` @@ -43,10 +34,8 @@ cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { @@ -60,7 +49,6 @@ die () { exit 1 } -# OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false @@ -83,10 +71,8 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar -# Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" @@ -105,7 +91,6 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi -# Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then @@ -121,19 +106,16 @@ if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then fi fi -# For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` - # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do @@ -141,17 +123,15 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + CHECK2=`echo "$arg"|egrep -c "^-"` - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" @@ -172,14 +152,12 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then esac fi -# Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..cb765c36a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -15,13 +15,7 @@ @rem @if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## -@rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 @@ -29,13 +23,10 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -@rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" -@rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe @@ -65,21 +56,16 @@ echo location of your Java installation. goto fail :execute -@rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar -@rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end -@rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 diff --git a/tools/aaudio-sink/build.sh b/tools/aaudio-sink/build.sh index 0f0f3a87b..eed2a2abc 100755 --- a/tools/aaudio-sink/build.sh +++ b/tools/aaudio-sink/build.sh @@ -1,9 +1,4 @@ #!/usr/bin/env bash -# Compiles module-aaudio-sink.so against the upstream PulseAudio 13.0 headers and the daemon's own -# libraries the app ships (app/src/main/jniLibs/arm64-v8a), with the Android NDK. -# NDK=<ndk root> tools/aaudio-sink/build.sh <pulseaudio-13.0 source dir> <output dir> -# Produces module-aaudio-sink.so in the output dir (the fallback sink; the DirectAudio sinks come -# from the pinned DirectAudio release, see tools/directaudio/fetch.sh). set -euo pipefail PA_SRC=$1 OUTDIR=$2 @@ -40,14 +35,11 @@ sed -e 's/@PA_MAJOR@/13/g' -e 's/@PA_MINOR@/0/g' -e 's/@PA_API_VERSION@/12/g' -e cp "$HERE/config.h" "$HERE/ltdl.h" "$INC/" build_one() { local src=$1 OUT=$2 extra=$3 - # PulseAudio 13's own headers (pulsecore/atomic.h) predate clang's int-conversion error; that - # one is downgraded for them, the modules themselves compile clean. "$CC" -O2 -shared -fPIC -Wall -Wno-unused-parameter -Wno-error=int-conversion -Wno-visibility -DHAVE_CONFIG_H \ -I"$INC" -I"$PA_SRC/src" -I"$HERE" \ -o "$OUT" "$HERE/$src" \ -L"$LIBS" -l:libpulsecore-13.0.so -l:libpulsecommon-13.0.so -l:libpulse.so $extra "$TOOLCHAIN/llvm-strip" --strip-unneeded "$OUT" - # What the daemon looks for, and nothing linked that a device would not have. for sym in pa__init pa__done pa__get_author pa__get_description pa__get_usage pa__get_version pa__load_once; do "$TOOLCHAIN/llvm-nm" -D --defined-only "$OUT" | grep -q " $sym$" || { echo "ERROR: $OUT does not export $sym"; exit 1; } done diff --git a/tools/aaudio-sink/config.h b/tools/aaudio-sink/config.h index 4615ca53f..0c7e1d0e4 100644 --- a/tools/aaudio-sink/config.h +++ b/tools/aaudio-sink/config.h @@ -1,6 +1,3 @@ -/* The handful of build-time facts PulseAudio 13's headers read, for compiling one module against - * the upstream source tree without running its configure. The daemon and libraries the app ships - * were built for the same target (Android arm64, bionic), so these match them. */ #ifndef DROIDDECK_PA_CONFIG_H #define DROIDDECK_PA_CONFIG_H #define PACKAGE "pulseaudio" diff --git a/tools/aaudio-sink/ltdl.h b/tools/aaudio-sink/ltdl.h index 84b3985c0..c183a4672 100644 --- a/tools/aaudio-sink/ltdl.h +++ b/tools/aaudio-sink/ltdl.h @@ -1,5 +1,3 @@ -/* pulsecore/module.h keeps a libltdl handle in pa_module; the module never touches it, so the - * handle type is all that is needed here. */ #ifndef DROIDDECK_LTDL_STUB_H #define DROIDDECK_LTDL_STUB_H typedef void *lt_dlhandle; diff --git a/tools/aaudio-sink/module-aaudio-sink.c b/tools/aaudio-sink/module-aaudio-sink.c index 3ad32896d..619bd2673 100644 --- a/tools/aaudio-sink/module-aaudio-sink.c +++ b/tools/aaudio-sink/module-aaudio-sink.c @@ -1,15 +1,4 @@ /* - * module-aaudio-sink: a PulseAudio 13 sink on Android's AAudio, for the DroidDeck app. - * - * The daemon's IO thread pulls rendered audio from the sink one burst at a time and hands it to - * a blocking AAudio output stream; the stream's own buffer paces the thread, and the latency the - * sink reports comes from the stream's playback timestamp. With adaptive=1 the buffer grows by a - * burst after each underrun, up to a cap, so a device that cannot keep a small buffer fed settles - * on one it can. - * - * Module arguments keep the names the app has always passed (performance_mode, adaptive, volume, - * buffer_frames, max_buffer_frames) so the daemon's config line did not have to change. - * * SPDX-License-Identifier: LGPL-2.1-or-later */ #ifdef HAVE_CONFIG_H @@ -58,18 +47,11 @@ PA_MODULE_USAGE( "buffer_frames=<initial buffer size in frames, 0 = two bursts> " "max_buffer_frames=<largest the buffer may grow to, 0 = the stream's capacity>"); -/* The name the app's daemon config addresses (set-default-sink). */ #define DEFAULT_SINK_NAME "AAudioSink" -/* Underruns are polled every so many writes, not every write. */ #define XRUN_CHECK_EVERY 8 #define FALLBACK_BURST_FRAMES 192 -/* The buffer starts at whichever is larger: this many bursts, or this much time. The daemon runs - * under proot, where every system call is traced, and a buffer of two bursts (8 ms) left the - * client's menu sounds choppy on device; 30 ms is inaudible for a menu and games have their own - * path. adaptive=1 still grows it further after real underruns. */ #define MIN_BUFFER_BURSTS 2 #define MIN_BUFFER_USEC (30 * PA_USEC_PER_MSEC) -/* Android's own audio threads run at nice -16 (THREAD_PRIORITY_AUDIO); an app may ask for it. */ #define AUDIO_NICE (-16) #define STATS_EVERY_USEC (30 * PA_USEC_PER_SEC) @@ -109,7 +91,6 @@ struct userdata { int32_t max_buffer_frames; int32_t requested_buffer_frames; bool adaptive; - /* rate= was given: ask the device for exactly that instead of its own rate. */ bool rate_requested; int32_t xruns_seen; unsigned writes_since_xrun_check; @@ -127,8 +108,6 @@ static int32_t usec_to_frames(const struct userdata *u, pa_usec_t usec) { return (int32_t) ((uint64_t) usec * u->ss.rate / PA_USEC_PER_SEC); } -/* Each write hands the device half the buffer, whole bursts, at least one: fewer, larger - * trips through proot's tracing than a burst at a time, with half the buffer of slack left. */ static void set_chunk(struct userdata *u) { int32_t frames = u->buffer_frames / 2; frames -= frames % u->burst_frames; @@ -166,7 +145,6 @@ static void apply_buffer_size(struct userdata *u, int32_t frames) { set_chunk(u); } -/* Opens the stream for u->ss; on return u->ss holds what the device actually gave. */ static int open_stream(struct userdata *u) { AAudioStreamBuilder *builder = NULL; aaudio_result_t r; @@ -180,9 +158,6 @@ static int open_stream(struct userdata *u) { AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_SHARED); AAudioStreamBuilder_setPerformanceMode(builder, u->performance_mode); AAudioStreamBuilder_setFormat(builder, u->ss.format == PA_SAMPLE_FLOAT32NE ? AAUDIO_FORMAT_PCM_FLOAT : AAUDIO_FORMAT_PCM_I16); - /* The device's own rate unless one was asked for: at any other rate Android falls back to a - * legacy stream with 20 ms bursts and room for three of them, and the daemon under proot - * cannot keep that fed. The sink takes whatever rate comes back; PulseAudio resamples clients. */ AAudioStreamBuilder_setSampleRate(builder, u->rate_requested ? (int32_t) u->ss.rate : AAUDIO_UNSPECIFIED); AAudioStreamBuilder_setChannelCount(builder, (int32_t) u->ss.channels); @@ -240,7 +215,6 @@ static void stop_stream(struct userdata *u) { u->started = false; } -/* How much audio the device still holds: frames written less frames it reports played. */ static pa_usec_t stream_latency(struct userdata *u) { int64_t position = 0, when_ns = 0; @@ -267,7 +241,6 @@ static void check_underruns(struct userdata *u) { u->writes_since_xrun_check = 0; xruns = AAudioStream_getXRunCount(u->stream); - /* A line every so often while underruns keep coming, so a session log shows the count. */ if (xruns != u->xruns_reported && pa_rtclock_now() - u->stats_at >= STATS_EVERY_USEC) { pa_log("aaudio-sink: %d underrun(s) so far, buffer %d frames", xruns, u->buffer_frames); u->xruns_reported = xruns; @@ -285,7 +258,6 @@ static void check_underruns(struct userdata *u) { pa_log("aaudio-sink: underrun %d: buffer now %d frames (%u ms)", xruns, u->buffer_frames, (unsigned) (frames_to_usec(u, u->buffer_frames) / PA_USEC_PER_MSEC)); } -/* One burst from the sink into the device. Returns <0 when the stream is gone for good. */ static int write_one_burst(struct userdata *u) { const void *data; aaudio_result_t written = 0; @@ -315,8 +287,6 @@ static int write_one_burst(struct userdata *u) { return 0; } - /* The device went away (headphones, a route change). Reopen once with the same settings; if - * the new stream differs in rate or format there is nothing sane to do but give up. */ pa_log("aaudio-sink: AAudioStream_write: %s; reopening the stream", AAudio_convertResultToText(written)); { pa_sample_spec before = u->ss; @@ -346,8 +316,6 @@ static int sink_process_msg(pa_msgobject *o, int code, void *data, int64_t offse return pa_sink_process_msg(o, code, data, offset, chunk); } -/* Runs in the IO thread: an opened sink means a running stream and a busy loop; a suspended - * one means a stopped stream and a thread asleep until a message arrives. */ static int sink_set_state_in_io_thread_cb(pa_sink *s, pa_sink_state_t new_state, pa_suspend_cause_t new_suspend_cause) { struct userdata *u = s->userdata; @@ -368,8 +336,6 @@ static void thread_func(void *userdata) { pa_log_debug("IO thread starting"); pa_thread_mq_install(&u->thread_mq); - /* Ahead of everything else in the app, as Android's own audio threads are; realtime on top - * of that where the kernel allows (it does not, for an app, but it costs nothing to ask). */ if (setpriority(PRIO_PROCESS, (id_t) syscall(SYS_gettid), AUDIO_NICE) < 0) pa_log_debug("aaudio-sink: could not raise the IO thread's priority (%s)", strerror(errno)); if (u->core->realtime_scheduling) @@ -379,7 +345,6 @@ static void thread_func(void *userdata) { int ret; if (PA_SINK_IS_OPENED(u->sink->thread_info.state) && u->started) { - /* Nothing written can be taken back from the device, so a rewind is just acknowledged. */ if (u->sink->thread_info.rewind_requested) pa_sink_process_rewind(u->sink, 0); if (write_one_burst(u) < 0) @@ -426,7 +391,6 @@ int pa__init(pa_module *m) { pa_log("Invalid sample format specification or channel map"); goto fail; } - /* AAudio speaks 16-bit or float; anything else is rendered as 16-bit. */ if (ss.format != PA_SAMPLE_FLOAT32NE) ss.format = PA_SAMPLE_S16NE; diff --git a/tools/adb_device.py b/tools/adb_device.py index 5e2990399..9b113c5d0 100755 --- a/tools/adb_device.py +++ b/tools/adb_device.py @@ -45,7 +45,6 @@ def _identity(adb: str, serial: str) -> str | None: def _rank(serial: str) -> tuple[int, str]: - # Prefer a stable mDNS transport over a network address with an ephemeral port. if "_adb-tls-connect._tcp" in serial: return (0, serial) if not re.search(r":\d+$", serial): diff --git a/tools/build_local.sh b/tools/build_local.sh index 0487104ed..2695ef084 100755 --- a/tools/build_local.sh +++ b/tools/build_local.sh @@ -47,7 +47,6 @@ fi ndk_version=${DROIDDECK_NDK_VERSION:-} if [[ -z "${ndk_version}" ]]; then - # Only complete NDKs: an interrupted sdkmanager install leaves a directory without source.properties. ndk_path=$(find "${sdk_dir}/ndk" -mindepth 2 -maxdepth 2 -name source.properties -print | xargs -n1 dirname | sort -V | tail -1) ndk_version=${ndk_path##*/} fi @@ -73,7 +72,6 @@ trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM -# A wedged daemon used to hang indefinitely and then trigger an unnecessary image rebuild. python3 "${repo_root}/tools/docker_preflight.py" "${image_name}" rm -rf -- "${linuxfs_dir}" mkdir -p "${linuxfs_dir}" @@ -84,7 +82,6 @@ docker run --rm --platform linux/amd64 \ set -euo pipefail d=app/src/main/assets/linuxfs mkdir -p "$d" - # Not install -m: the VirtioFS share sometimes refuses its chmod (EPERM). The APK keeps no modes. put() { mkdir -p "$(dirname "$2")" && cat "$1" > "$2"; } aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \ -pthread -std=c++17 -static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL \ @@ -131,7 +128,6 @@ docker run --rm --platform linux/amd64 \ chown -R '"$(id -u):$(id -g)"' app/src/main/assets/linuxfs ' -# Cardboard mode's SteamVR driver and Vulkan layer (tools/vr, docs/cardboard-vr.md). DROIDDECK_BUILD_IMAGE="${image_name}" "${repo_root}/tools/vr/build.sh" "${linuxfs_dir}/usr/local/lib/droiddeck-vr" github_repo=${DROIDDECK_GITHUB_REPOSITORY:-} @@ -150,12 +146,8 @@ if [[ -z "${github_repo}" ]]; then github_repo=${github_repo%.git} fi -# Pinned downloads are kept between builds, named by their checksum, so a rebuild fetches nothing -# it already has. A cached file is checked again before use; a bad one is fetched anew. cache_dir=${DROIDDECK_BUILD_CACHE:-"${HOME}/.cache/droiddeck-build"} mkdir -p "${cache_dir}" -# cached <sha256> <name> <command...>: prints the cached path, running the command (which writes -# to "$out") only when the cache has no file with that checksum. cached() { local sha=$1 name=$2 out shift 2 @@ -182,7 +174,6 @@ fi . "${repo_root}/tools/linuxfs/uruntime.env" uruntime_binary=$(cached "${URUNTIME_SHA256}" "${URUNTIME_ASSET}" \ bash -c 'curl -fsSL --retry 3 -o "$out" "$0"' "https://github.com/VHSgunzo/uruntime/releases/download/${URUNTIME_VERSION}/${URUNTIME_ASSET}") -# macOS's install has no -D. mkdir -p "${linuxfs_dir}/usr/local/lib/droiddeck" "${linuxfs_dir}/usr/local/share/licenses/uruntime" install -m644 "${uruntime_binary}" "${linuxfs_dir}/usr/local/lib/droiddeck/uruntime" install -m644 "${repo_root}/tools/linuxfs/licenses/uruntime-LICENSE" "${linuxfs_dir}/usr/local/share/licenses/uruntime/LICENSE" @@ -244,7 +235,6 @@ install -m644 "${mango_pkgs}/usr/bin/mangoapp" "${mango_dir}/mangoapp" for library in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do cp -L "${mango_pkgs}/usr/lib/${library}" "${mango_dir}/${library}" done -# Ours, not the package's: GPU memory without tracefs (tools/mangoapp/libtracefs-shim.c). docker run --rm --platform linux/amd64 --user "$(id -u):$(id -g)" -v "${repo_root}:/src" -w /src "${image_name}" \ aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \ -o app/src/main/assets/linuxfs/usr/local/lib/mangoapp/libtracefs.so.1 tools/mangoapp/libtracefs-shim.c @@ -270,8 +260,6 @@ fi sink_output="${staging_dir}/sink-out" "${repo_root}/tools/directaudio/fetch.sh" "${repo_root}" "${sink_output}" "${repo_root}/tools/aaudio-sink/build.sh" "${pa_source}" "${sink_output}" -# proot is rebuilt only when its sources (source.env, the patches, the build script) changed since -# the libraries in jniLibs were built. proot_out="${repo_root}/app/src/main/jniLibs/arm64-v8a" proot_sources=$(cd "${repo_root}/tools/proot" && find . -type f ! -name '*.pyc' | LC_ALL=C sort | xargs shasum -a 256 | shasum -a 256 | cut -d' ' -f1) proot_inputs=$(printf '%s\n' "${proot_sources}" "${ndk_version}" | shasum -a 256 | cut -d' ' -f1) @@ -301,10 +289,6 @@ tar -cf - -C "${bundle_dir}" . | zstd -19 -T0 -c > "${staging_dir}/pulseaudio.tz mkdir -p "${repo_root}/app/build/prepared-assets" mv "${staging_dir}/pulseaudio.tzst" "${repo_root}/app/build/prepared-assets/pulseaudio.tzst" -# The GOG / Epic / Amazon download engines are Rust (app/src/main/rust/stores); Gradle's -# buildRustStores task cross-compiles them with cargo-ndk, which needs the Android target and the -# cargo-ndk binary installed once. DROIDDECK_SKIP_RUST=1 builds an apk without the engines (the -# stores then report themselves unavailable) for a machine without a Rust toolchain. gradle_rust_arg="" if [[ "${DROIDDECK_SKIP_RUST:-0}" == 1 ]]; then gradle_rust_arg="-PskipRust=true" @@ -385,10 +369,6 @@ grep -qi 'CN=Android, OU=Android, O=Android' <<<"${signature_output}" || { exit 1 } -# With DroidDeck's own key at hand, sign as CI's main builds are (tools/release/sign-apk.sh), so -# the apk installs over a release instead of being refused as a different signer. The key's -# location and password live in an untracked .signing.env (RELEASE_KEYSTORE, RELEASE_STORE_PASSWORD, -# RELEASE_KEY_ALIAS), looked for in this checkout and then in the main one, which worktrees share. signing_env=${DROIDDECK_SIGNING_ENV:-} if [[ -z "${signing_env}" ]]; then for candidate in "${repo_root}/.signing.env" \ @@ -400,7 +380,6 @@ if [[ -n "${signing_env}" ]]; then echo "Signing with DroidDeck's key (${signing_env})" ( set -a - # shellcheck disable=SC1090 cd "$(dirname "${signing_env}")" . "${signing_env}" set +a diff --git a/tools/cardboard/test_cardboard_params.c b/tools/cardboard/test_cardboard_params.c index 360f08857..4eb35ffda 100644 --- a/tools/cardboard/test_cardboard_params.c +++ b/tools/cardboard/test_cardboard_params.c @@ -1,5 +1,3 @@ -/* Host test for the cardboard pass's pure helpers (waylandcomp/src/cardboard.c, built with - * CARDBOARD_HOST_TEST): clamping and the push-constant layout cardboard.frag reads. */ #include "cardboard.h" #include <math.h> #include <stdio.h> @@ -28,20 +26,15 @@ int main(void) { const float pose[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; float pc[24]; cardboard_push_constants(&q, pose, 1920, 1080, 2400, 1080, pc); - /* rot rows with the scalars in w */ CHECK(near(pc[0], 1) && near(pc[1], 2) && near(pc[2], 3) && near(pc[3], 0.032f)); CHECK(near(pc[4], 4) && near(pc[5], 5) && near(pc[6], 6) && near(pc[7], 3.0f)); CHECK(near(pc[8], 7) && near(pc[9], 8) && near(pc[10], 9) && near(pc[11], 2.0f)); - /* lens: k1, k2, tan(fov/2), chroma */ CHECK(near(pc[12], 0.34f) && near(pc[13], 0.55f) && near(pc[14], 1.0f) && near(pc[15], 0.006f)); - /* view: inset, source aspect, mode, eye viewport aspect (half the panel) */ CHECK(near(pc[16], 0.05f) && near(pc[17], 1920.0f / 1080.0f) && near(pc[18], 0.0f) && near(pc[19], 1200.0f / 1080.0f)); - /* A side-by-side source: each eye's aspect is half the frame's. */ q.source = CARDBOARD_SOURCE_SBS; cardboard_push_constants(&q, pose, 3840, 1080, 2400, 1080, pc); CHECK(near(pc[17], 1920.0f / 1080.0f) && near(pc[18], 1.0f)); - /* Unknown sizes fall back instead of dividing by zero. */ cardboard_push_constants(&q, pose, 0, 0, 0, 0, pc); CHECK(isfinite(pc[17]) && isfinite(pc[19])); diff --git a/tools/comet/droiddeck.patch b/tools/comet/droiddeck.patch index f65fe9efa..6dbb60ae9 100644 --- a/tools/comet/droiddeck.patch +++ b/tools/comet/droiddeck.patch @@ -119,7 +119,7 @@ index 0c539f1..50fdd21 100644 .unwrap_or_else(|| String::from("en-US")); } + -+/// A launcher may restrict Galaxy to explicitly enabled SDK clients in its private policy file. ++ +pub fn client_allowed(client_id: &str) -> bool { + let Ok(path) = std::env::var("COMET_ALLOWED_CLIENTS") else { return true; }; + std::fs::read(path).ok().and_then(|data| serde_json::from_slice::<Vec<String>>(&data).ok()) diff --git a/tools/desktop-kde/build-kwin.sh b/tools/desktop-kde/build-kwin.sh index f86f28e4a..ae6ac4943 100644 --- a/tools/desktop-kde/build-kwin.sh +++ b/tools/desktop-kde/build-kwin.sh @@ -1,6 +1,4 @@ #!/bin/bash -# Build the pinned KWin executable with the nested-scale patch on native aarch64 Linux. -# Its dependencies come from signature-checked Arch Linux ARM packages in a disposable chroot. set -euo pipefail out=$(realpath -m "${1:?out dir}") here=$(cd "$(dirname "$0")" && pwd) @@ -20,7 +18,6 @@ for repo in core extra alarm; do done touch "$work/runtime.txt" python3 "$here/closure.py" "$work/db" "$work/runtime.txt" "$here/kwin-build-seeds.txt" > "$work/packages.txt" -# Refuse a repository whose KWin headers/libraries have moved beyond the pinned source. grep -Eq '^extra/kwin-6\.7\.5-[0-9]+-aarch64\.pkg\.tar\.' "$work/packages.txt" export work mirror builder here download() { @@ -56,8 +53,6 @@ sudo cp "$work/root/build/out/bin/kwin_wayland" "$out/kwin_wayland" sudo cp "$work/root/build/out/bin/libkwin.so.$version" "$out/libkwin.so.$version" sudo chown "$(id -u):$(id -g)" "$out/kwin_wayland" "$out/libkwin.so.$version" strip --strip-unneeded "$out/kwin_wayland" "$out/libkwin.so.$version" -# The backend lives in libkwin, not the small launcher. Catch accidentally shipping only the -# executable, which boots successfully but still loads the distribution's unpatched backend. grep -aq DROIDDECK_NESTED_SCALE "$out/libkwin.so.$version" { echo "KWin $version source sha256 $source_sha" diff --git a/tools/desktop-kde/build.sh b/tools/desktop-kde/build.sh index b64c00482..524a76f46 100755 --- a/tools/desktop-kde/build.sh +++ b/tools/desktop-kde/build.sh @@ -1,24 +1,4 @@ #!/bin/bash -# Builds the KDE Plasma desktop package for the Linux runtime: the closure of seeds.txt over Arch -# Linux ARM's aarch64 repositories, minus what the runtime image already carries, plus overlay/, -# as a zstd tarball that extracts over the rootfs (DesktopCatalog's "tar" kind). -# -# tools/desktop-kde/build.sh <out dir> -# -# The runtime's own packages (runtime-packages.txt, "name version" from its pacman db) are not -# shipped again, except where something in the closure asks for a version of one the runtime does -# not have (a versioned dependency on it, or on a library it provides): then the repository's -# build comes along and replaces it. Nothing is deleted on extraction, so a library whose soname -# moved keeps its old file for what the runtime linked against it. -# -# Every package is fetched over HTTPS and must carry a valid signature by the Arch Linux ARM Build -# System key (archlinuxarm-builder.gpg; primary fingerprint pinned below, as archlinuxarm.org -# publishes it and archlinuxarm-keyring trusts it) and match the sha256 the repository database -# gives for it; anything else stops the build. The repository databases themselves are not signed -# by Arch Linux ARM, so they decide only which signed packages are taken. desktop-kde.packages.txt -# lists every package taken, with its sha256: what a release was built from. -# -# Needs curl, gpgv, tar, xz, zstd, python3. set -euo pipefail out=${1:?out dir} here=$(cd "$(dirname "$0")" && pwd) @@ -27,7 +7,6 @@ case $mirror in https://*) ;; *) echo "ALARM_MIRROR must be https" >&2; exit 1 ; builder=68B3537F39A313B3E574D06777193F152BDBE6A6 fetch() { curl -fsSL --proto '=https' --proto-redir '=https' --retry 6 --retry-delay 5 --retry-all-errors -o "$1" "$2"; } work=$(mktemp -d); mkdir -p "$out" "$work/db" "$work/root" -# PKG_CACHE keeps the downloaded packages between builds (a local rebuild after an overlay change). pkgs=${PKG_CACHE:-$work/pkgs}; mkdir -p "$pkgs" trap 'rm -rf "$work"' EXIT @@ -56,24 +35,19 @@ done < "$work/pkglist.txt" cp -a "$here/overlay/." "$work/root/" -# Same-version KWin, built by build-kwin.sh with the nested desktop scale patch. if [ -n "${KDE_KWIN_OVERRIDE:-}" ]; then install -m 755 "$KDE_KWIN_OVERRIDE/kwin_wayland" "$work/root/usr/bin/kwin_wayland" install -m 755 "$KDE_KWIN_OVERRIDE/libkwin.so.6.7.5" "$work/root/usr/lib/libkwin.so.6.7.5" fi -# Weight that no desktop on a phone reads. rm -rf "$work/root/usr/share/doc" "$work/root/usr/share/man" "$work/root/usr/share/info" \ "$work/root/usr/share/gtk-doc" "$work/root/usr/include" "$work/root/usr/lib/pkgconfig" \ "$work/root/usr/lib/cmake" "$work/root/usr/share/help" -# English only, on purpose: the desktop session runs in C.UTF-8 and nothing hands the app's language -# to KDE, so other translations would be weight no one sees. They come back with localizing the -# desktop itself (LANGUAGE from the app's language at session start). find "$work/root/usr/share/locale" -mindepth 1 -maxdepth 1 -type d ! -name 'en*' -exec rm -rf {} + 2>/dev/null || true find "$work/root/usr/lib" -name '*.a' -delete chmod -R u+rwX "$work/root" -sed 's#^[a-z]*/##' "$work/pkglist.txt" > "$out/desktop-kde.packages.txt" # "file sha256", each verified above +sed 's#^[a-z]*/##' "$work/pkglist.txt" > "$out/desktop-kde.packages.txt" tar -C "$work/root" --zstd -cf "$out/desktop-kde.tar.zst" . sha256sum "$out/desktop-kde.tar.zst" | awk '{print $1}' > "$out/desktop-kde.sha256" ls -l "$out/desktop-kde.tar.zst" diff --git a/tools/desktop-kde/closure.py b/tools/desktop-kde/closure.py index b0475cf6e..f078a5068 100755 --- a/tools/desktop-kde/closure.py +++ b/tools/desktop-kde/closure.py @@ -15,7 +15,6 @@ REPOS = ("core", "extra", "alarm") def vercmp_part(a, b): - # rpmvercmp, as pacman compares version strings. if a == b: return 0 sa = re.findall(r"\d+|[a-zA-Z]+", a) @@ -104,14 +103,11 @@ def main(): def resolve(dep): """The package to take for a dependency string, and whether the runtime's copy serves.""" name, op, want = parse_dep(dep) - # A runtime package that is the dependency or provides it, at a version that serves. if name in runtime and satisfies(runtime[name], op, want): return name, True candidates = providers.get(name, []) for real, _ in candidates: if real in runtime: - # A provided name (a soname, mostly): the runtime's build serves as long as it is - # the repository's own, or nothing asks for a particular version. if not op or runtime[real] == pkgs[real]["version"]: return real, True if satisfies_provider(real, name, op, want): diff --git a/tools/desktop-kde/kwin-build-seeds.txt b/tools/desktop-kde/kwin-build-seeds.txt index b544ad4b0..c0cfe51be 100644 --- a/tools/desktop-kde/kwin-build-seeds.txt +++ b/tools/desktop-kde/kwin-build-seeds.txt @@ -1,4 +1,3 @@ -# Native build tools and KWin's Arch makedepends; dependencies are resolved and signature-checked. kwin base-devel cmake diff --git a/tools/desktop-kde/lxqt-applications.txt b/tools/desktop-kde/lxqt-applications.txt index 812715567..6351c7764 100644 --- a/tools/desktop-kde/lxqt-applications.txt +++ b/tools/desktop-kde/lxqt-applications.txt @@ -1,5 +1,3 @@ -# Application packages from steamdeck-desktop-r1's desktop.packages.txt. -# Keep dependencies and shared apps/icons (Firefox, mpv, Breeze, xdg-utils and portals). labwc lxqt-panel lxqt-config diff --git a/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/actions/steamdeck-gaming-return.svg b/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/actions/steamdeck-gaming-return.svg index 5cff1ce9d..96d426607 100644 --- a/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/actions/steamdeck-gaming-return.svg +++ b/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/actions/steamdeck-gaming-return.svg @@ -1,4 +1,3 @@ -<!-- Created with Inkscape (http://www.inkscape.org/) --> <svg id="svg2" height="64" style="" viewBox="0 0 64 64" width="64" xmlns="http://www.w3.org/2000/svg" sodipodi:docname="deck_icon.svg" inkscape:version="1.2-dev (d2dd25d878, 2022-02-01, custom)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"> <path id="path42220" d="m 32 4 v 10 a 18 18 0 0 1 18 18 a 18 18 0 0 1 -18 18 v 10 a 28 28 0 0 0 28 -28 a 28 28 0 0 0 -28 -28 z" style="fill:#fff;fill-rule:evenodd"/> <circle id="path43518" cx="32" cy="32" r="14" style="fill:#1a9fff;fill-rule:evenodd"/> diff --git a/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/places/distributor-logo-steamdeck.svg b/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/places/distributor-logo-steamdeck.svg index 082485bf1..b0ac005cb 100644 --- a/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/places/distributor-logo-steamdeck.svg +++ b/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/places/distributor-logo-steamdeck.svg @@ -1,4 +1,3 @@ -<!-- Created with Inkscape (http://www.inkscape.org/) --> <svg id="svg2" height="64" style="" viewBox="0 0 64 64" width="64" xmlns="http://www.w3.org/2000/svg" sodipodi:docname="start.svg" inkscape:version="1.2-dev (d2dd25d878, 2022-02-01, custom)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"> <style type="text/css" id="current-color-scheme"> .ColorScheme-Text { diff --git a/tools/desktop-kde/overlay/usr/share/plasma/look-and-feel/com.valve.vapor.desktop/contents/layouts/org.kde.plasma.desktop-layout.js b/tools/desktop-kde/overlay/usr/share/plasma/look-and-feel/com.valve.vapor.desktop/contents/layouts/org.kde.plasma.desktop-layout.js index 485bf7468..e27c370d3 100644 --- a/tools/desktop-kde/overlay/usr/share/plasma/look-and-feel/com.valve.vapor.desktop/contents/layouts/org.kde.plasma.desktop-layout.js +++ b/tools/desktop-kde/overlay/usr/share/plasma/look-and-feel/com.valve.vapor.desktop/contents/layouts/org.kde.plasma.desktop-layout.js @@ -1,5 +1,3 @@ -// The first desktop: DroidDeck's wallpaper, and SteamOS's Plasma's default bottom panel with the Steam Deck -// launcher icon, Steam, the file manager, the browser and Konsole pinned to the task manager. var desktops = desktopsForActivity(currentActivity()); for (var i = 0; i < desktops.length; i++) { desktops[i].wallpaperPlugin = "org.kde.image"; @@ -36,7 +34,6 @@ if (trayId) { trayContainment.currentConfigGroup = ["General"]; trayContainment.writeConfig("scaleIconsToFit", true); } -// The runtime has no locale of its own (C.UTF-8), whose short date reads "9 10 2026". var clock = panel.addWidget("org.kde.plasma.digitalclock"); clock.currentConfigGroup = ["Appearance"]; clock.writeConfig("dateFormat", "isoDate"); diff --git a/tools/desktop-kde/overlay/usr/share/wallpapers/DroidDeck/contents/images/1920x1080.svg b/tools/desktop-kde/overlay/usr/share/wallpapers/DroidDeck/contents/images/1920x1080.svg index 4a3d4eeca..229397123 100644 --- a/tools/desktop-kde/overlay/usr/share/wallpapers/DroidDeck/contents/images/1920x1080.svg +++ b/tools/desktop-kde/overlay/usr/share/wallpapers/DroidDeck/contents/images/1920x1080.svg @@ -1,5 +1,4 @@ <svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080"> - <!-- DroidDeck's desktop wallpaper: the mark (artwork/droiddeck-mark.svg) on deep navy. --> <defs> <radialGradient id="bg" cx="960" cy="540" r="1150" gradientUnits="userSpaceOnUse"> <stop offset="0" stop-color="#14213a"/> diff --git a/tools/desktop-kde/patches/0001-respect-nested-desktop-scale.patch b/tools/desktop-kde/patches/0001-respect-nested-desktop-scale.patch index af8c1bcfb..7c8767ff8 100644 --- a/tools/desktop-kde/patches/0001-respect-nested-desktop-scale.patch +++ b/tools/desktop-kde/patches/0001-respect-nested-desktop-scale.patch @@ -28,7 +28,7 @@ - // intentionally ignored, as it would get overwritten - // with the fractional scale protocol anyways - // next.scale = props->scale.value_or(m_state.scale); -+ // DroidDeck owns the outer window size; Display Settings owns the inner UI scale. ++ + if (qEnvironmentVariableIsSet("DROIDDECK_NESTED_SCALE")) { + next.scale = props->scale.value_or(m_state.scale); + next.scaleSetting = props->scaleSetting.value_or(m_state.scaleSetting); @@ -53,7 +53,7 @@ void init(const QSize &pixelSize, qreal scale, bool fullscreen); -+ // The outer window scale, independent of the desktop UI scale in DroidDeck. ++ + qreal presentationScale() const; bool isReady() const; KWayland::Client::Surface *surface() const; diff --git a/tools/desktop-kde/removal-manifests.py b/tools/desktop-kde/removal-manifests.py index 86ed74e69..9c909dc84 100644 --- a/tools/desktop-kde/removal-manifests.py +++ b/tools/desktop-kde/removal-manifests.py @@ -68,7 +68,6 @@ def main(): cache, out, legacy_cache = map(pathlib.Path, sys.argv[5:8]) here = pathlib.Path(__file__).resolve().parent seeds = {line.strip() for line in (here / 'seeds.txt').read_text().splitlines() if line.strip() and not line.startswith('#')} - SHARED_SEEDS - # Published source package hashes; the caller places this beside the verified cache. packages = dict(line.split() for line in (cache / 'desktop-kde.packages.txt').read_text().splitlines()) selected = set() found = set() @@ -86,8 +85,6 @@ def main(): selected.update(str(p.relative_to(here / 'overlay')) for p in (here / 'overlay').rglob('*') if p.is_file()) inventories = [inventory(p, pin[1]) for p, pin in zip(archives, PINS)] out.mkdir(parents=True, exist_ok=True) - # The old runtime's pacman database omits some gamescope dependencies (e.g. libseat). - # Own only the LXQt application packages, never their dependency closure. legacy_seeds = {line.strip() for line in (here / 'lxqt-applications.txt').read_text().splitlines() if line.strip() and not line.startswith('#')} legacy_selected = set() diff --git a/tools/desktop-kde/seeds.txt b/tools/desktop-kde/seeds.txt index 816285de5..ed61bd8ee 100644 --- a/tools/desktop-kde/seeds.txt +++ b/tools/desktop-kde/seeds.txt @@ -1,6 +1,3 @@ -# The desktop: KDE Plasma, as SteamOS's desktop mode has it - KWin nested as a client of the app's -# compositor, Plasma's shell on it, and the apps SteamOS puts on its desktop. One package name per -# line; build.sh takes the closure over Arch Linux ARM at build time, minus what the runtime has. kwin plasma-workspace plasma-desktop diff --git a/tools/directaudio/fetch.sh b/tools/directaudio/fetch.sh index f2f21c196..b7ce2dbcb 100755 --- a/tools/directaudio/fetch.sh +++ b/tools/directaudio/fetch.sh @@ -1,21 +1,7 @@ #!/usr/bin/env bash -# Fetches the DirectAudio pieces DroidDeck ships from the pinned release in tools/directaudio/release.env -# and puts them where the build reads them. Four zips, checksummed: -# directaudio-linux-wine11.zip the game driver for Valve Proton 11 / Experimental / GE-Proton -# directaudio-linux-wine11-systhread.zip the game driver for Proton-CachyOS (system-thread mmdevapi) -# directaudio-linux-relay.zip the Android-side helper (speaker + microphone for games) -# directaudio-linux-sink.zip the PulseAudio sinks; the app ships module-directaudio-native-sink -# -# tools/directaudio/fetch.sh <repo root> <dir to receive the sink modules> -# -# Driver sets -> app/src/main/assets/directaudio/<set>/ (generated, ignored by git) -# Helper -> app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so (generated, ignored by git) -# Sink -> <sink dir>/module-directaudio-native-sink.so for the audio bundle, next to the -# module-aaudio-sink.so tools/aaudio-sink builds (the fallback). set -euo pipefail repo_root=$1 sink_out=$2 -# shellcheck source=release.env . "${repo_root}/tools/directaudio/release.env" for v in DIRECTAUDIO_REPO DIRECTAUDIO_TAG DIRECTAUDIO_WINE11_SHA256 DIRECTAUDIO_SYSTHREAD_SHA256 DIRECTAUDIO_RELAY_SHA256 DIRECTAUDIO_SINK_SHA256; do [[ -n "${!v:-}" && "${!v}" != TBD ]] || { echo "tools/directaudio/release.env: ${v} is not set" >&2; exit 1; } @@ -44,7 +30,6 @@ for set in linux-wine11 linux-wine11-systhread; do done rm -f "${assets}/${set}/README.md" done -# Each set says which interface it was built for; the name must not lie. grep -q '^abi wine11$' "${assets}/linux-wine11/version.txt" grep -q '^abi systhread$' "${assets}/linux-wine11-systhread/version.txt" diff --git a/tools/directaudio/release.env b/tools/directaudio/release.env index 1e30f83f1..5e052f0bb 100644 --- a/tools/directaudio/release.env +++ b/tools/directaudio/release.env @@ -1,6 +1,3 @@ -# The DirectAudio release this build ships, pinned: nothing compiled for audio lives in this repo. -# Source and release notes: https://github.com/The412Banner/directaudio -# Bump the tag and the four checksums together (sha256 of each zip, from the release's SHA256SUMS.txt). DIRECTAUDIO_REPO=The412Banner/directaudio DIRECTAUDIO_TAG=directaudio-linux-v1.1.0 DIRECTAUDIO_WINE11_SHA256=835e6cab072f5148893c43379e60506483aeb3f1444e777b430c295cbeb472c9 diff --git a/tools/droiddeck-scenario b/tools/droiddeck-scenario index 2a37eebfb..b4590b02f 100755 --- a/tools/droiddeck-scenario +++ b/tools/droiddeck-scenario @@ -155,7 +155,7 @@ class Runner: ): try: found[name] = action() - except Exception as exc: # evidence is best effort + except Exception as exc: found[name] = {"ok": False, "error": str(exc)} write_json(folder / "evidence.json", found) return {k: v.get("ok", True) if isinstance(v, dict) else True for k, v in found.items()} diff --git a/tools/droiddeckctl b/tools/droiddeckctl index d7456a81a..7c9f284b4 100755 --- a/tools/droiddeckctl +++ b/tools/droiddeckctl @@ -15,7 +15,7 @@ import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) -from adb_device import DeviceResolutionError, resolve_serial # noqa: E402 +from adb_device import DeviceResolutionError, resolve_serial AUTHORITY = "content://com.droiddeck.launcher.agent" @@ -215,7 +215,6 @@ def add_serial(response: dict, serial: str) -> dict: return response -# --- displays ------------------------------------------------------------------------------------ def displays(adb: str, serial: str) -> list[dict]: """Logical displays with the SurfaceFlinger (physical) id screencap and screenrecord want.""" @@ -285,7 +284,6 @@ def record(adb: str, serial: str, path: str, seconds: float, display: int | None return {"ok": True, "command": "record", "path": str(destination), "seconds": seconds, "display": display} -# --- commands ------------------------------------------------------------------------------------ def start_session(adb: str, serial: str, request: dict) -> dict: if request.get("mode") == "run": @@ -341,8 +339,6 @@ def execute(args: argparse.Namespace, adb: str, serial: str) -> dict: raise ControlError("Steam URL must start with steam://", "INVALID_STEAM_URL") request["steamUrl"] = args.url if args.reuse: - # A scenario's per-run "before": keep a session of this mode that is already up, so only - # a run after one that ended (a crash, the low-memory killer) starts a new one. current = get_state(adb, serial)["session"] wanted = {"steam": "steam", "desktop": "lxqt"}[args.mode] if current.get("running") and current.get("mode") == wanted and current.get("phase") not in {"STOPPING", "FAILED"}: @@ -508,7 +504,6 @@ def execute(args: argparse.Namespace, adb: str, serial: str) -> dict: return add_serial(record(adb, serial, args.path, args.seconds, args.display), serial) if command == "wake": - # A sleeping device stops the session and hides the app's windows from ui dump. run_adb(adb, serial, "shell", "input", "keyevent", "KEYCODE_WAKEUP") run_adb(adb, serial, "shell", "wm", "dismiss-keyguard") power = run_adb(adb, serial, "shell", "dumpsys", "power").stdout.decode(errors="replace") @@ -549,7 +544,6 @@ def build_parser() -> argparse.ArgumentParser: stop = commands.add_parser("stop", help="stop the session and wait for IDLE") stop.add_argument("--timeout", type=float, default=90) - # Collecting the client's logs and redacting them took about two minutes after a Skyrim session. stop.add_argument("--artifact-timeout", type=float, default=300) commands.add_parser("resume", help="resume a suspended session") @@ -711,7 +705,7 @@ def main(argv: list[str] | None = None) -> int: except (ControlError, DeviceResolutionError) as exc: error = {"code": getattr(exc, "code", "DEVICE_RESOLUTION_FAILED"), "message": str(exc)} code = exit_code(exc) - except Exception as exc: # the contract is JSON on stdout, even for a bug in this tool + except Exception as exc: error = {"code": "INTERNAL_ERROR", "message": f"{type(exc).__name__}: {exc}"} code = 2 response = {"ok": False, "error": error} diff --git a/tools/gamescope/build-in-arch.sh b/tools/gamescope/build-in-arch.sh index 2078592aa..7cc19fd00 100755 --- a/tools/gamescope/build-in-arch.sh +++ b/tools/gamescope/build-in-arch.sh @@ -1,26 +1,15 @@ #!/usr/bin/bash -# Runs INSIDE an Arch Linux ARM container (menci/archlinuxarm:base-devel) on an arm64 runner. -# Builds Arch's own gamescope package of the runtime's version with the patches in ./patches -# added, then keeps only the gamescope binary, checks its library needs against the runtime's -# list, and packs it as gamescope.tzst (usr/local/bin/gamescope) for the apk to stage. set -euxo pipefail VERSION=${GAMESCOPE_VERSION:-3.16.29} WORK=/work cd "$WORK" -# pacman's download sandbox (Landlock + the alpm user) cannot be set up inside the runner's -# container; the packages still come signed from Arch Linux ARM's mirrors. grep -q '^DisableSandbox' /etc/pacman.conf || sed -i 's/^\[options\]/[options]\nDisableSandbox/' /etc/pacman.conf -# The geo-redirecting mirror answered 500s and connection resets mid-transaction; name a few -# concrete mirrors ahead of it so pacman has somewhere to fall over to. { for m in https://ca.us.mirror.archlinuxarm.org https://fl.us.mirror.archlinuxarm.org https://de3.mirror.archlinuxarm.org https://nl.mirror.archlinuxarm.org; do echo "Server = $m/\$arch/\$repo"; done; cat /etc/pacman.d/mirrorlist; } > /etc/pacman.d/mirrorlist.new mv /etc/pacman.d/mirrorlist.new /etc/pacman.d/mirrorlist pacman -Syu --noconfirm --needed git sudo zstd binutils -# makepkg refuses root; a builder user with passwordless sudo installs the dependencies. id builder >/dev/null 2>&1 || useradd -m builder echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder rm -rf pkg && mkdir pkg && cp -r tools/gamescope/patches pkg/ && chown -R builder pkg -# Arch's packaging for exactly this version: the tag is <pkgver>-<pkgrel>; the first rel is tried -# first, later ones after it. sudo -u builder bash -c " set -euxo pipefail cd pkg @@ -31,13 +20,11 @@ test -f arch/PKGBUILD cd arch grep -q '^pkgver=${VERSION}$' PKGBUILD cp ../patches/*.patch . -# Add our patches to the source list and apply them after Arch's own prepare() steps. PATCHES=\$(ls *.patch | sort) { echo; echo 'source+=('; for p in \$PATCHES; do echo \" \$p\"; done; echo ')'; for p in \$PATCHES; do echo \"sha256sums+=('SKIP')\"; done; } >> PKGBUILD cat >> PKGBUILD <<'PREP' _droiddeck_prepare() { - # Arch's own prepare() leaves the shell inside the checkout; start from a known place. cd \"\$srcdir/gamescope\" for p in \$(ls \"\$srcdir\"/*.patch | sort); do echo \"applying \$(basename \"\$p\")\" @@ -51,7 +38,7 @@ else prepare() { _droiddeck_prepare; } fi PREP -makepkg -A -s --noconfirm --skipchecksums --skippgpcheck # -A: the PKGBUILD lists x86_64 only; Arch Linux ARM builds the same file +makepkg -A -s --noconfirm --skipchecksums --skippgpcheck ls -l *.pkg.tar.* " cd "$WORK" @@ -61,7 +48,6 @@ tar --use-compress-program=unzstd -xf "$PKG" -C out --strip-components=2 usr/bin mv out/gamescope out/usr/local/bin/gamescope chmod 755 out/usr/local/bin/gamescope strip --strip-unneeded out/usr/local/bin/gamescope || true -# Every NEEDED library must be one the runtime ships, or the binary would not load there. NEEDED=$(readelf -d out/usr/local/bin/gamescope | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p') echo "NEEDED: $NEEDED" MISSING="" diff --git a/tools/gamescope/patches/0019-steamcompmgr-arm64-virtual-white.patch b/tools/gamescope/patches/0019-steamcompmgr-arm64-virtual-white.patch index 62914a426..b98e4523d 100644 --- a/tools/gamescope/patches/0019-steamcompmgr-arm64-virtual-white.patch +++ b/tools/gamescope/patches/0019-steamcompmgr-arm64-virtual-white.patch @@ -22,9 +22,9 @@ index e54194b..4131467 100644 g_ColorMgmt.pending.outputVirtualWhite.x = santitize_float( bit_cast<float>( user_vec[0] ) ); g_ColorMgmt.pending.outputVirtualWhite.y = santitize_float( bit_cast<float>( user_vec[1] ) ); +#if defined(__aarch64__) -+ // The ARM64 Steam client packs the pair as one 64-bit element, so only x arrives; -+ // Steam derives the pair from the CIE daylight locus, so y follows from x. -+ // Drop once the client sends both floats (y arrives non-zero). ++ ++ ++ + float x = g_ColorMgmt.pending.outputVirtualWhite.x; + if ( x >= 0.25f && x <= 0.55f && g_ColorMgmt.pending.outputVirtualWhite.y <= 0.01f ) + g_ColorMgmt.pending.outputVirtualWhite.y = -3.0f * x * x + 2.87f * x - 0.275f; diff --git a/tools/gamescope/patches/0020-color-p3-red-is-wide-gamut.patch b/tools/gamescope/patches/0020-color-p3-red-is-wide-gamut.patch index 047ea2d37..aa183697d 100644 --- a/tools/gamescope/patches/0020-color-p3-red-is-wide-gamut.patch +++ b/tools/gamescope/patches/0020-color-p3-red-is-wide-gamut.patch @@ -18,7 +18,7 @@ index 64430a5..f4dcd98 100644 { - // Use red as a sentinal for a wide-gamut display - return ( nativeDisplayOutput.primaries.r.x > 0.650f && nativeDisplayOutput.primaries.r.y < 0.320f ); -+ // Use red as a sentinal for a wide-gamut display; DCI-P3 red sits exactly on the threshold ++ + return ( nativeDisplayOutput.primaries.r.x > 0.650f && nativeDisplayOutput.primaries.r.y <= 0.320f ); } diff --git a/tools/gamescope/patches/0100-realtime-queue-and-gamepad-cursor.patch b/tools/gamescope/patches/0100-realtime-queue-and-gamepad-cursor.patch index 0e390b099..75a355d4e 100644 --- a/tools/gamescope/patches/0100-realtime-queue-and-gamepad-cursor.patch +++ b/tools/gamescope/patches/0100-realtime-queue-and-gamepad-cursor.patch @@ -6,10 +6,10 @@ index 54e4efc..024af02 100644 } float queuePriorities = 1.0f; -+ // Without CAP_SYS_NICE gamescope never asks for realtime-priority queues. Under proot on -+ // Android there is no capability to have; the fallback below covers a driver that refuses. KGSL -+ // Turnip accepts but ignores it (one submit-queue priority, contexts created alike). Opt in with -+ // GAMESCOPE_FORCE_VULKAN_REALTIME=1. (Armada's --force-vulkan-realtime, as an environment switch.) ++ ++ ++ ++ + const char *pszForceRealtime = getenv( "GAMESCOPE_FORCE_VULKAN_REALTIME" ); + const bool useRealtimeVulkanQueue = gamescope::Process::HasCapSysNice() || ( pszForceRealtime && atoi( pszForceRealtime ) != 0 ); + if ( useRealtimeVulkanQueue && !gamescope::Process::HasCapSysNice() ) @@ -67,8 +67,8 @@ index 10990b8..08afbc4 100644 } - else + m_bConstrained = false; -+ // Nothing to draw (the gamepad UI, most games): the position only feeds focus and zoom, and -+ // wlserver's copy is what stock gamescope uses for those. No round trip to X. ++ ++ + if ( m_imageEmpty ) { - m_bConstrained = false; @@ -80,12 +80,12 @@ index 10990b8..08afbc4 100644 } wlserver_unlock(); + -+ // The gamepad-driven cursor is moved through XTest, which moves the X pointer and nothing -+ // else: wlserver's copy of the position never changes, so the sprite sat frozen while the -+ // pointer moved. Ask X where the pointer is. (Armada 0001, on 3.16.29.) That is a blocking -+ // round trip to Xwayland on the paint thread, which waits while Xwayland is busy drawing the -+ // client's interface: once per vblank only while the sprite is shown, and every 50 ms while -+ // it is hidden for inactivity - enough to bring it back within a few frames of a move. ++ ++ ++ ++ ++ ++ + if ( wlserver.bCursorHidden ) + { + const uint64_t now = get_time_in_nanos(); diff --git a/tools/gamescope/patches/0110-wayland-backend-touch.patch b/tools/gamescope/patches/0110-wayland-backend-touch.patch index 5a270e749..e85bf5d2a 100644 --- a/tools/gamescope/patches/0110-wayland-backend-touch.patch +++ b/tools/gamescope/patches/0110-wayland-backend-touch.patch @@ -6,8 +6,8 @@ index d74268b..bf72267 100644 void Wayland_Keyboard_RepeatInfo( wl_keyboard *pKeyboard, int32_t nRate, int32_t nDelay ); static const wl_keyboard_listener s_KeyboardListener; -+ // Touch from the host compositor (DroidDeck): each finger goes to wlserver's touch path, -+ // the one a Steam Deck's touchscreen drives, instead of being dropped. ++ ++ + void Wayland_Touch_Down( wl_touch *pTouch, uint32_t uSerial, uint32_t uTime, wl_surface *pSurface, int32_t nId, wl_fixed_t fX, wl_fixed_t fY ); + void Wayland_Touch_Up( wl_touch *pTouch, uint32_t uSerial, uint32_t uTime, int32_t nId ); + void Wayland_Touch_Motion( wl_touch *pTouch, uint32_t uTime, int32_t nId, wl_fixed_t fX, wl_fixed_t fY ); @@ -58,7 +58,7 @@ index d74268b..bf72267 100644 + } + } + -+ // Touch ++ + + bool CWaylandInputThread::TouchToOutput( wl_surface *pSurface, wl_fixed_t fX, wl_fixed_t fY, double *pflX, double *pflY ) + { @@ -75,7 +75,7 @@ index d74268b..bf72267 100644 + *pflY = ( wl_fixed_to_double( fY ) * uScale / 120.0 + oState->nDestY ) / g_nOutputHeight; + return true; + } -+ // Touch ids are offset by one: the nested pointer already moves wlserver's touch 0. ++ + void CWaylandInputThread::Wayland_Touch_Down( wl_touch *pTouch, uint32_t uSerial, uint32_t uTime, wl_surface *pSurface, int32_t nId, wl_fixed_t fX, wl_fixed_t fY ) + { + double flX, flY; diff --git a/tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch b/tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch index 466d0a781..c9027a309 100644 --- a/tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch +++ b/tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch @@ -7,9 +7,9 @@ index bf72267..c400978 100644 wlserver_lock(); - wlserver_touchmotion( flX, flY, 0, ++m_uFakeTimestamp ); -+ // Always move the real pointer too: in Passthrough (what Steam's Big Picture asks for) a -+ // touch motion for an id that is not down moves nothing, so hover never reached the -+ // client and a click landed wherever the pointer had last been. ++ ++ ++ + wlserver_touchmotion( flX, flY, 0, ++m_uFakeTimestamp, true ); wlserver_unlock(); } diff --git a/tools/gamescope/patches/0112-restore-iconified-game-on-resume.patch b/tools/gamescope/patches/0112-restore-iconified-game-on-resume.patch index 2eee045e4..9e24b7287 100644 --- a/tools/gamescope/patches/0112-restore-iconified-game-on-resume.patch +++ b/tools/gamescope/patches/0112-restore-iconified-game-on-resume.patch @@ -28,11 +28,11 @@ index 08afbc4..372a82c 100644 if ( keyboardFocusWindow && ctx->currentKeyboardFocusWindow && find_win( ctx, ctx->currentKeyboardFocusWindow ) == keyboardFocusWin ) keyboardFocusWindow = ctx->currentKeyboardFocusWindow; -+ // An overlay that takes input (the Steam menu) moves input away without changing the focus window, and a -+ // fullscreen game iconifies itself when it loses input. The focus window never changed, so it is never taken -+ // out of iconic above, and wine will not activate a window it believes iconic: the game stays minimized -+ // (its 160x24 caption in the corner) after Resume. Put it back to NormalState before input returns to it, -+ // then hand focus over again so wine sees the restore first and the activation after it. ++ ++ ++ ++ ++ + if ( inputFocus == ctx->focus.focusWindow && ctx->focus.focusWindow->iconifyRequested ) + { + ctx->focus.focusWindow->iconifyRequested = false; @@ -59,8 +59,8 @@ index 08afbc4..372a82c 100644 } + if ( ev->atom == ctx->atoms.gamescopeRestoreFocusWindow ) + { -+ // Anything outside (the session's watcher) can ask for the same restore: NormalState on the focus -+ // window and focus handed to it again. Harmless on a window that is not iconic. ++ ++ + if ( ctx->focus.focusWindow && get_prop( ctx, ctx->root, ctx->atoms.gamescopeRestoreFocusWindow, 0 ) ) + { + xwm_log.infof( "restore requested for focus window 0x%lx", ctx->focus.focusWindow->xwayland().id ); @@ -89,7 +89,7 @@ index 66022d7..af93a23 100644 bool isFullscreen = false; bool isSysTrayIcon = false; -+ // The client asked to be iconified (WM_CHANGE_STATE) and has not been put back to NormalState since. ++ + bool iconifyRequested = false; bool sizeHintsSpecified = false; bool skipTaskbar = false; diff --git a/tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch b/tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch index f2ea4a16f..492ec1116 100644 --- a/tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch +++ b/tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch @@ -26,7 +26,7 @@ index f00b62c..cb3b9c6 100644 if (w && w->xwayland().id == ev.xmap.window) + { -+ // Wine flips override-redirect while unmapped and the flip itself sends no event. ++ + w->xwayland().a.override_redirect = ev.xmap.override_redirect; map_win(ctx, ev.xmap.window, ev.xmap.serial); + } diff --git a/tools/linuxfs/check-preloads.sh b/tools/linuxfs/check-preloads.sh index 6e9e1dcff..78021c440 100644 --- a/tools/linuxfs/check-preloads.sh +++ b/tools/linuxfs/check-preloads.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# Shared native ABI checks for CI and local builds. set -euo pipefail D=${1:?asset directory} NEED=$("${READELF:-readelf}" -d "$D/libfakeinput.so" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p') @@ -13,9 +12,6 @@ FAKE=$(syms "$D/libfakeinput.so") for sym in open openat ioctl read close poll ppoll select stat fstat access scandir; do grep -qx "$sym" <<<"$FAKE" || { echo "::error::libfakeinput.so does not export $sym"; exit 1; } done -# The static C++ runtime stays private: every session process preloads this library, -# and an exported personality routine takes over other programs' unwinding (shadPS4 -# aborted in pthread_exit). if grep -qxE '__gxx_personality_v0|__cxa_throw|_Unwind_Resume' <<<"$FAKE"; then echo "::error::libfakeinput.so exports the C++ runtime"; exit 1 fi @@ -23,7 +19,6 @@ SESSION=$(syms "$D/libblsession.so") for sym in socket bind getsockname setsockopt statfs statvfs syscall shm_open shm_unlink mmap mmap64 munmap drmPrimeFDToHandle drmCloseBufferHandle; do grep -qx "$sym" <<<"$SESSION" || { echo "::error::libblsession.so does not export $sym"; exit 1; } done -# Zink's GEM_CLOSE is caught at ioctl(); an exported drmIoctl made NMS replay old frames. if grep -qx drmIoctl <<<"$SESSION"; then echo "::error::libblsession.so exports drmIoctl"; exit 1 fi diff --git a/tools/linuxfs/clipboard/clipboard.c b/tools/linuxfs/clipboard/clipboard.c index cd91d65d1..61b89ca6e 100644 --- a/tools/linuxfs/clipboard/clipboard.c +++ b/tools/linuxfs/clipboard/clipboard.c @@ -1,7 +1,3 @@ -/* Android text -> Gamescope's Xwayland selection. Gamescope's Wayland backend does not - * relay the outer compositor's clipboard. libX11 is loaded from the guest at runtime; - * the small stable Xlib ABI below keeps this helper independent of cross X11 headers. - * The private host file is atomically replaced, never put in session logs. */ #define _GNU_SOURCE #include <sys/inotify.h> #include <sys/stat.h> @@ -45,12 +41,9 @@ API(int, XConnectionNumber, (Display *)); API(int, XCloseDisplay, (Display *)); API(void *, XSetErrorHandler, (int (*)(Display *, void *))); -/* Stay below X11's normal maximum property request. Never paste a truncated command. */ #define MAX_TEXT (64 * 1024) static int ignore_x_error(Display *d, void *event) { (void)d; (void)event; return 0; } -/* The host renames complete files into place. The inode distinguishes copying the same - * text again after a guest program has taken ownership of the clipboard. */ static int refresh(Display *d, Window w, Atom clipboard, const char *path, char **text, size_t *len, ino_t *inode) { int fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); @@ -98,7 +91,7 @@ int main(int argc, char **argv) { LOAD(XSetErrorHandler); Display *d = XOpenDisplay(NULL); if (!d) { fprintf(stderr, "== clipboard: cannot open Xwayland\n"); return 1; } - XSetErrorHandler(ignore_x_error); /* A paste requester can close its window at any time. */ + XSetErrorHandler(ignore_x_error); Window w = XCreateSimpleWindow(d, XDefaultRootWindow(d), 0, 0, 1, 1, 0, 0, 0); Atom clipboard = XInternAtom(d, "CLIPBOARD", 0); Atom targets = XInternAtom(d, "TARGETS", 0), utf8 = XInternAtom(d, "UTF8_STRING", 0); @@ -117,7 +110,7 @@ int main(int argc, char **argv) { for (;;) { while (XPending(d)) { Event e; XNextEvent(d, &e); - if (e.type != 30) continue; /* SelectionRequest */ + if (e.type != 30) continue; SelectionRequest *r = &e.request; Atom property = r->property ? r->property : r->target; Event reply = {0}; @@ -126,14 +119,13 @@ int main(int argc, char **argv) { size_t ascii; for (ascii = 0; ascii < len && (unsigned char)text[ascii] < 128; ascii++) {} if (r->target == targets) { Atom supported[] = {targets, utf8, plain, text_atom, string}; - XChangeProperty(d, r->requestor, property, 4 /* XA_ATOM */, 32, 0, + XChangeProperty(d, r->requestor, property, 4 , 32, 0, (const unsigned char *)supported, ascii == len ? 5 : 4); reply.selection.property = property; } else if (r->target == utf8 || r->target == plain || r->target == text_atom) { XChangeProperty(d, r->requestor, property, utf8, 8, 0, (const unsigned char *)text, (int)len); reply.selection.property = property; } else if (r->target == string) { - /* STRING is Latin-1. Offer it only for ASCII rather than corrupt UTF-8. */ if (ascii == len) { XChangeProperty(d, r->requestor, property, string, 8, 0, (const unsigned char *)text, (int)len); reply.selection.property = property; diff --git a/tools/linuxfs/debian-image.env b/tools/linuxfs/debian-image.env index 5532bfe16..8d6902e38 100644 --- a/tools/linuxfs/debian-image.env +++ b/tools/linuxfs/debian-image.env @@ -1,8 +1,3 @@ -# The Debian 11 image the x86 preloads are compiled in (glibc 2.31, what Steam's Linux runtime -# has). Published once by .github/workflows/ci-debian-image.yml to a release on this repo; the -# build downloads it from there instead of pulling from a public registry mirror, whose -# anonymous rate limit the shared runners keep hitting. Debian 11 is frozen upstream, so a -# refresh is only ever a new -rN release with the three lines below updated. DEBIAN_IMAGE_TAG=ci-debian-bullseye-r1 DEBIAN_IMAGE_ASSET=debian-bullseye.tar.zst DEBIAN_IMAGE_SHA256=5ac13c6b0f2e3ab3e8acc8cadcd39ee453a51a72870645d592635e24bcb85459 diff --git a/tools/linuxfs/desktop/droiddeck-desktop b/tools/linuxfs/desktop/droiddeck-desktop index eb0d10cc6..15771a35b 100644 --- a/tools/linuxfs/desktop/droiddeck-desktop +++ b/tools/linuxfs/desktop/droiddeck-desktop @@ -1,90 +1,39 @@ #!/bin/bash -# The desktop session: KDE Plasma, set up as SteamOS's desktop mode - KWin nested as a client of the -# app's compositor, Plasma's shell on it, and Xwayland for the X11 programs (the Steam client, most -# emulators). Run by droiddeck-session in place of gamescope for mode "lxqt" (the mode's name from -# the LXQt desktop this replaced; it is what the app and its preferences use). -# Staged by the app over the desktop package's copy on every launch, so a fix here reaches an -# installed desktop without re-downloading the package. set -u export XDG_SESSION_TYPE=wayland XDG_CURRENT_DESKTOP=KDE XDG_SESSION_DESKTOP=KDE DESKTOP_SESSION=plasma export KDE_FULL_SESSION=true KDE_SESSION_VERSION=6 KDE_APPLICATIONS_AS_SCOPE=0 -# Qt tries Wayland and falls back to X11 (Xwayland): an AppImage that bundles only Qt's xcb -# plugin - melonDS - exits at once on a plain "wayland" ("Could not find the Qt platform plugin"). export QT_QPA_PLATFORM="wayland;xcb" GDK_BACKEND=wayland,x11 SDL_VIDEODRIVER=wayland,x11 unset QT_QPA_PLATFORMTHEME -# KDE's programs log through Qt, which writes to the journal when stderr is not a terminal; there is -# no journal here, so their messages go to the desktop log with everything else. export QT_FORCE_STDERR_LOGGING=1 -# SteamOS's /etc/profile.d/kde.sh. export MOZ_ENABLE_WAYLAND=1 MOZ_USE_XINPUT2=1 -# Konsole's profile starts the login shell, which it finds here. export SHELL=/bin/bash -# Firefox's child processes sandbox themselves with seccomp and namespaces, neither of which -# behaves under proot: the content process ends up unable to read the font files its parent can, -# reports "no fonts" and dies - the tab crash, with the chrome around it still drawing text. -# The prefs for this are only advisory and a profile can override them; the environment is not. export MOZ_DISABLE_CONTENT_SANDBOX=1 MOZ_DISABLE_GMP_SANDBOX=1 MOZ_DISABLE_RDD_SANDBOX=1 export MOZ_DISABLE_SOCKET_PROCESS_SANDBOX=1 MOZ_DISABLE_UTILITY_SANDBOX=1 -# Firefox seals the size of the shared memory it hands a tab; on Android 16 (Galaxy S25/S26, -# RedMagic) adding the seal fails, Firefox only warns, and the tab refuses the unsealed memory -# ("Shared memory PlatformHandle is not safe to map") and dies on signal 11 - every page. Firefox's -# own switch skips the seals. (#280; the line went missing in the fontconfig move of #252.) export MOZ_SHM_NO_SEALS=1 -# KWin composites in software (QPainter): its GPU path allocates through gbm on a DRM render node, -# and the KGSL stand-in is not one. Its windows get shm buffers only, so GL programs on it draw with -# Mesa's software rasterizer, Plasma's own Qt Quick draws with Qt's software renderer (plasmashell -# crashes on llvmpipe here), and a Vulkan program cannot draw at all: games and emulators from the -# menu run through droiddeck-gpu (their own gamescope on the app's compositor), and Steam's desktop -# client uses software CEF on KDE. The session's own GL choice (zink on Turnip) -# is put back by droiddeck-gpu. export KWIN_COMPOSE=Q QT_QUICK_BACKEND=software LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe -# mpv refuses that software GL and then fails its X11 fallback (an assertion): allowed it, it plays. [ -e /etc/mpv/mpv.conf ] || { mkdir -p /etc/mpv && echo gpu-sw=yes >/etc/mpv/mpv.conf; } 2>/dev/null export DROIDDECK_NESTED_SCALE=1 export DROIDDECK_GLTHREAD=${mesa_glthread:-} unset MESA_LOADER_DRIVER_OVERRIDE LIBGL_KOPPER_DRI2 mesa_glthread -# The desktop's own log, beside the session's, so KWin and the programs on it can be read apart -# from the session that started them. The session log keeps the milestones either way. if [ -n "${BL_DEBUG_DIR:-}" ]; then mkdir -p "$BL_DEBUG_DIR" 2>/dev/null || true exec >>"$BL_DEBUG_DIR/desktop.log" 2>&1 echo "== droiddeck-desktop $(date -u '+%Y-%m-%d %H:%M:%S UTC') ${BL_WIDTH:-1280}x${BL_HEIGHT:-720}" fi -# The runtime directory outlives a session. KDE's daemons are single-instance programs that leave a -# lock and a socket there; finding last session's, they decide they are already running and exit -# without a word. Only the app's own compositor socket (wayland-0) is kept. find "$XDG_RUNTIME_DIR" -mindepth 1 -maxdepth 1 ! -name 'wayland-0*' ! -name 'keymap.xkb' -exec rm -rf {} + 2>/dev/null chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null -# KWin will not start Xwayland when its socket directory is not sticky, as a shared /tmp would be, -# and Plasma's session manager cannot listen at all without the ICE directory, which only root may -# create for it. mkdir -p /tmp/.X11-unix /tmp/.ICE-unix && chmod 1777 /tmp/.X11-unix /tmp/.ICE-unix 2>/dev/null -# Fontconfig defaults and Android language fallbacks are set up by droiddeck-session before this -# script or any desktop program starts. -# Steam opens on KDE; Steam Big Picture explicitly hands off to the gaming session. export BL_DESKTOP=1 ln -sf /usr/local/bin/droiddeck-steam-launch /usr/local/bin/steamdeck-steam 2>/dev/null ln -sf /usr/local/bin/droiddeck-steam-shim /usr/local/bin/steam 2>/dev/null python3 /usr/local/bin/droiddeck-desktop-games 2>&1 | sed 's/^/[games] /' -# Player 1 on the controller in every emulator that has no profile of its own yet - now, and again -# every half minute: an emulator started for the first time writes its own config, which is then -# given the controller for its next start without waiting for the next desktop. python3 /usr/local/bin/droiddeck-pad-defaults 2>&1 | sed 's/^/[pads] /' ( while sleep 30; do python3 /usr/local/bin/droiddeck-pad-defaults 2>&1 | sed 's/^/[pads] /'; done ) & -# Games and emulators from the menu run through droiddeck-gpu; BL_DESKTOP_GPU=on/off overrides, -# and each entry keeps "Run in a desktop window" either way. python3 /usr/local/bin/droiddeck-desktop-gpu 2>&1 | sed 's/^/[gpu] /' -# The windowed action still opens melonDS at the size/placement the LXQt desktop used. /usr/local/bin/droiddeck-desktop-window-rules 2>&1 | sed 's/^/[rules] /' -# Windows programs, Linux programs, AppImages and scripts open with droiddeck-open (Steam's Proton, -# FEX for x86); an exec the kernel refuses is handed to it as well (libblsession's binfmt.c). An -# AppImage run directly cannot mount itself under proot, so it unpacks itself to run. python3 /usr/local/bin/droiddeck-open --install 2>&1 | sed 's/^/[open] /' export APPIMAGE_EXTRACT_AND_RUN=1 -# A click runs an executable script or program without KIO asking first; Dolphin's settings can still ask. [ -e /etc/xdg/kiorc ] || printf '[Executable scripts]\nbehaviourOnLaunch=execute\n' >/etc/xdg/kiorc 2>/dev/null -# SteamOS's desktop icons: Return to Gaming Mode and Steam, put there once - what the user moves -# or deletes stays that way. desktop_dir=$(xdg-user-dir DESKTOP 2>/dev/null); [ -n "$desktop_dir" ] && [ "$desktop_dir" != "$HOME" ] || desktop_dir=$HOME/Desktop if [ ! -e "$HOME/.config/droiddeck-desktop-icons" ]; then mkdir -p "$desktop_dir" "$HOME/.config" @@ -93,23 +42,16 @@ if [ ! -e "$HOME/.config/droiddeck-desktop-icons" ]; then chmod +x "$desktop_dir"/*.desktop 2>/dev/null touch "$HOME/.config/droiddeck-desktop-icons" fi -# A Return icon still there is kept current with the package's entry (what it runs has changed). [ -f "$desktop_dir/Return.desktop" ] && cp -f /usr/share/applications/droiddeck-return-gaming.desktop "$desktop_dir/Return.desktop" 2>/dev/null -# Plasma's caches of the menu, the icons and the MIME types, which a package install never ran. update-mime-database /usr/share/mime >/dev/null 2>&1 & [ -f /usr/share/icons/hicolor/icon-theme.cache ] || gtk-update-icon-cache -q -t /usr/share/icons/hicolor 2>/dev/null & [ -f /usr/share/glib-2.0/schemas/gschemas.compiled ] || glib-compile-schemas /usr/share/glib-2.0/schemas 2>/dev/null -# In debug runs Firefox (started from the panel or the menu, so it inherits this) writes its -# own network log: name lookups, connects, HTTP transactions, the link/offline decisions. if [ -f "$HOME/.droiddeck-desktop-debug" ]; then mkdir -p "$HOME/.cache/droiddeck" export MOZ_LOG="nsHttp:3,nsSocketTransport:3,nsHostResolver:4,NetworkLinkService:5,NetworkConnectivityService:5,nsIOService:5" export MOZ_LOG_FILE="$HOME/.cache/droiddeck/firefox-net.log" export QT_LOGGING_RULES="kwin_*.debug=true" fi -# Remote runs, for driving the desktop from a debugger: with ~/.droiddeck-remote-run present (only -# the app or root can create it), each script dropped into ~/.cache/droiddeck/run/ is run once, -# inside the session with its environment, its output tagged into the desktop log. if [ -f "$HOME/.droiddeck-remote-run" ]; then mkdir -p "$HOME/.cache/droiddeck/run" ( @@ -122,18 +64,10 @@ if [ -f "$HOME/.droiddeck-remote-run" ]; then done ) & fi -# Plasma as its own login starts it: startplasma-wayland sets up the session's environment and runs -# plasma_session, which starts KWin (through kwin_wayland_wrapper, ours first on PATH) as a client of -# the app's compositor with its own Xwayland, then Plasma's daemons, shell and autostart entries on -# it. The session bus is ours: Plasma, the file manager and the portal all expect one. echo "== droiddeck-desktop: Plasma on $WAYLAND_DISPLAY" dbus-run-session -- startplasma-wayland rc=$? echo "== droiddeck-desktop: Plasma ended ($rc)" -# Plasma logged out - Log Out, or Return to Gaming Mode, which logs out the same way, so open windows -# are asked to close (and to save) first. As on SteamOS, leaving the desktop goes to Gaming Mode: the -# app is asked for a Steam session, and it ends this one to start it. A desktop the app stops never -# gets here; a Plasma that failed does not count as a logout. if [ "$rc" -eq 0 ]; then /usr/local/bin/droiddeck-steam-launch bigpicture sleep 30 diff --git a/tools/linuxfs/desktop/droiddeck-desktop-gpu b/tools/linuxfs/desktop/droiddeck-desktop-gpu index af9e11209..b1cabbff0 100644 --- a/tools/linuxfs/desktop/droiddeck-desktop-gpu +++ b/tools/linuxfs/desktop/droiddeck-desktop-gpu @@ -1,24 +1,13 @@ #!/usr/bin/python3 -# The desktop's games and emulators, started on the GPU. Every menu entry whose categories say -# Game or Emulator gets a copy of its own in ~/.local/share/applications (which wins over -# /usr/share/applications) whose Exec runs it through droiddeck-gpu: full screen on the app's -# compositor, where Vulkan and OpenGL work. The entry keeps the old way as a second action, "Run in -# a desktop window". Run at every desktop start; copies whose original is gone, or all of them when -# the switch is off, are removed. -# -# BL_DESKTOP_GPU=on (default: the desktop composites in software) | off import os import re HOME = os.environ.get("HOME", "/root") SYSTEM_DIR = "/usr/share/applications" -# The user's own AppImages (the front end's Add AppImage). LOCAL_DIR = "/usr/local/share/applications" -# Flathub's games, as Flatpak exports them; the copies here win over these as they do over the system's. FLATPAK_DIR = os.path.join(HOME, ".local/share/flatpak/exports/share/applications") USER_DIR = os.path.join(HOME, ".local/share/applications") MARK = "X-DroidDeck-GPU=1" -# The client and its games already hand themselves to the app (droiddeck-steam-launch). SKIP = re.compile(r"^(steam|droiddeck-game-|droiddeck-steam|steamdeck-)", re.I) @@ -76,7 +65,6 @@ def wrap(text): out_group.append("Actions=" + ("droiddeck-window;" + actions if actions else "droiddeck-window;")) out_group.append(MARK) window = ["", "[Desktop Action droiddeck-window]", "Name=Run in a desktop window", "Exec=" + exec_line] - # The entry's own actions (new window, a different mode...) run on the GPU as well. wrapped_rest = [] for l in rest: if l.startswith("Exec=") and not l[5:].startswith("droiddeck-gpu "): @@ -105,7 +93,6 @@ def main(): if not name.endswith(".desktop") or SKIP.match(name): continue target = os.path.join(USER_DIR, name) - # A copy the user made or edited themselves is theirs. if os.path.exists(target) and not ours(target): continue try: diff --git a/tools/linuxfs/desktop/droiddeck-gpu b/tools/linuxfs/desktop/droiddeck-gpu index e68f55178..40a346085 100644 --- a/tools/linuxfs/desktop/droiddeck-gpu +++ b/tools/linuxfs/desktop/droiddeck-gpu @@ -1,10 +1,4 @@ #!/bin/bash -# Runs a program from the desktop on the GPU: in a gamescope of its own that draws straight into -# the app's compositor (wayland-0), not inside the desktop's KWin. KWin composites in software, and -# gives its windows no DMA-BUFs - a Vulkan program there fails at its swapchain -# (VK_ERROR_SURFACE_LOST, RPCS3's error). Here it gets the same path as a program started from the -# app's rail: full screen over the desktop until it exits, then the desktop is back. -# droiddeck-gpu <program> [args...] set -u [ $# -gt 0 ] || { echo "usage: droiddeck-gpu <program> [args...]" >&2; exit 64; } if [ -n "${BL_DEBUG_DIR:-}" ]; then @@ -20,16 +14,11 @@ elif [ "${BL_REFRESH:-0}" -gt 0 ]; then fi [ "${BL_HDR:-0}" = 1 ] && args+=(--hdr-enabled) echo "== droiddeck-gpu $(date -u +%H:%M:%S): $* (gamescope ${width}x${height} on wayland-0)" -# gamescope is a client of the app's compositor, beside the desktop's KWin; the program inside it reaches it -# through gamescope's own Xwayland, as in a Steam session. export WAYLAND_DISPLAY=wayland-0 export GDK_BACKEND=x11 QT_QPA_PLATFORM=xcb SDL_VIDEODRIVER=x11 -# GL on the GPU again (zink on Turnip, as the session sets it): the desktop itself draws GL in software. -# Qt Quick back on its GPU renderer and KWin's software compositing not passed on, too. unset LIBGL_ALWAYS_SOFTWARE QT_QUICK_BACKEND KWIN_COMPOSE export MESA_LOADER_DRIVER_OVERRIDE=zink GALLIUM_DRIVER=zink LIBGL_KOPPER_DRI2=true [ -n "${DROIDDECK_GLTHREAD:-}" ] && export mesa_glthread=$DROIDDECK_GLTHREAD -# AppImages cannot mount under proot; the menu entries already say so, a hand-typed one may not. export APPIMAGE_EXTRACT_AND_RUN=1 gamescope "${args[@]}" -- /usr/local/bin/droiddeck-gpu-window "$@" rc=$? diff --git a/tools/linuxfs/desktop/firefox-droiddeck.js b/tools/linuxfs/desktop/firefox-droiddeck.js index ad6b1e40f..053d7d03a 100644 --- a/tools/linuxfs/desktop/firefox-droiddeck.js +++ b/tools/linuxfs/desktop/firefox-droiddeck.js @@ -1,17 +1,9 @@ -// Firefox defaults for the DroidDeck desktop, staged by the app into -// /usr/lib/firefox/defaults/pref/. A user's own prefs.js still wins. -// Firefox judges "online" by dumping the routing table over netlink, which Android denies to -// apps; seeing no default route it switches itself to Work Offline and every page fails while -// the network is fine. Let it just try. pref("network.manage-offline-status", false); pref("browser.offline", false); -// No user namespaces on Android kernels and a ptrace'd process tree: the process sandboxes -// have nothing to set up and only add failure modes. pref("security.sandbox.content.level", 0); pref("media.cubeb.sandbox", false); pref("security.sandbox.socket.process.level", 0); -// Handheld-sized chrome, and nothing phoning home on first run. pref("browser.shell.checkDefaultBrowser", false); pref("datareporting.policy.dataSubmissionEnabled", false); pref("browser.aboutwelcome.enabled", false); diff --git a/tools/linuxfs/desktop/kwin_wayland_wrapper b/tools/linuxfs/desktop/kwin_wayland_wrapper index 020c3d9c4..a34e7455a 100755 --- a/tools/linuxfs/desktop/kwin_wayland_wrapper +++ b/tools/linuxfs/desktop/kwin_wayland_wrapper @@ -1,10 +1,4 @@ #!/bin/bash -# KWin for the desktop. Plasma's session (plasma_session) starts KWin through kwin_wayland_wrapper as -# found on PATH; this one, first on it, has KWin draw into the app's compositor (wayland-0) and serve -# its own clients on wayland-1 - by default it takes wayland-0 and finds the app's lock on it - with -# its nested output at the app's surface size (left alone, a 1024x768 window the desktop is scaled -# from), and without the lock screen, which the app's own suspend stands in for. Anything else (a KWin -# started outside DroidDeck's desktop) gets the real wrapper as it is. if [ "${BL_DESKTOP:-}" = 1 ]; then set -- --wayland-display wayland-0 --socket wayland-1 --width "${BL_WIDTH:-1280}" --height "${BL_HEIGHT:-720}" --no-lockscreen "$@" fi diff --git a/tools/linuxfs/overlay/usr/bin/jupiter-biosupdate b/tools/linuxfs/overlay/usr/bin/jupiter-biosupdate index 7feebb74a..039e4d006 100755 --- a/tools/linuxfs/overlay/usr/bin/jupiter-biosupdate +++ b/tools/linuxfs/overlay/usr/bin/jupiter-biosupdate @@ -1,3 +1,2 @@ #!/bin/sh -# No Deck BIOS here. The client calls this in Deck mode; a clean exit is "nothing to do". exit 0 diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-biosupdate b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-biosupdate index 7feebb74a..039e4d006 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-biosupdate +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-biosupdate @@ -1,3 +1,2 @@ #!/bin/sh -# No Deck BIOS here. The client calls this in Deck mode; a clean exit is "nothing to do". exit 0 diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-dock-updater b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-dock-updater index dadcac714..4dfb4b862 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-dock-updater +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-dock-updater @@ -1,8 +1,4 @@ #!/bin/sh -# No Deck dock here. The client runs this with --check at start (systemdockmanager.txt) and reads -# the exit code: 0 with nothing printed is taken as "an update is available" and puts a Dock -# Firmware Update row with an Apply button in Settings > System. 7, SteamOS's "nothing to do", -# is read as up to date (state 2 in that log, against state 4 for exit 0; both seen on device). case "${1:-}" in --check) exit 7 ;; esac diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-priv-write b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-priv-write index c875c2474..039e4d006 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-priv-write +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-priv-write @@ -1,6 +1,2 @@ #!/bin/sh -# steamos-priv-write <path> <value>: how the client in Deck mode sets brightness, TDP and fan -# controls through sysfs. None of those paths is writable from an app on Android, and the app -# already sets the panel's mode and the game-tier power policy from the Android side. Accept -# and do nothing, so the client never logs a failed helper. exit 0 diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-select-branch b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-select-branch index 6f0c9d806..501e630b1 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-select-branch +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-select-branch @@ -1,8 +1,4 @@ #!/bin/sh -# The SteamOS update-branch selector the client calls in Deck mode. The session forces the client -# branch on the command line and keeps package/beta in step with it, so a selection here would -# only start the restart loop the session script describes. List and report the stable branch; -# accept and ignore a choice. case "${1:-}" in -l) printf 'rel\n' ;; -c) printf 'rel\n' ;; diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-set-timezone b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-set-timezone index f60c99281..039e4d006 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-set-timezone +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-set-timezone @@ -1,3 +1,2 @@ #!/bin/sh -# The session carries the device's time zone in TZ already; the client's own setter is a no-op. exit 0 diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-update b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-update index fa99b087a..bc2d00e8e 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-update +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-update @@ -1,8 +1,4 @@ #!/bin/sh -# What the Steam client calls in Deck mode (from /usr/bin/steamos-polkit-helpers/, seen in its -# console log on device; a copy sits in /usr/bin as well) to look for a SteamOS update. There is no SteamOS -# here: the answer is "no update", which is exit 7 in SteamOS's convention, and the client moves -# on. --supports-duplicate-detection is a capability probe and answers yes. Staged from the apk. case "${1:-}" in --supports-duplicate-detection) exit 0 ;; esac diff --git a/tools/linuxfs/overlay/usr/bin/steamos-select-branch b/tools/linuxfs/overlay/usr/bin/steamos-select-branch index 6f0c9d806..501e630b1 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-select-branch +++ b/tools/linuxfs/overlay/usr/bin/steamos-select-branch @@ -1,8 +1,4 @@ #!/bin/sh -# The SteamOS update-branch selector the client calls in Deck mode. The session forces the client -# branch on the command line and keeps package/beta in step with it, so a selection here would -# only start the restart loop the session script describes. List and report the stable branch; -# accept and ignore a choice. case "${1:-}" in -l) printf 'rel\n' ;; -c) printf 'rel\n' ;; diff --git a/tools/linuxfs/overlay/usr/bin/steamos-session-select b/tools/linuxfs/overlay/usr/bin/steamos-session-select index 871f67b48..eaac87341 100644 --- a/tools/linuxfs/overlay/usr/bin/steamos-session-select +++ b/tools/linuxfs/overlay/usr/bin/steamos-session-select @@ -1,10 +1,4 @@ #!/bin/sh -# The SteamOS session switch. Big Picture's power menu runs it for "Switch to Desktop" (a Plasma -# session: "plasma", "plasma-wayland-persistent", ...) and waits for Game Mode to end; with nothing -# here it sat on "Switching to Desktop..." for ever. Here the desktop is DroidDeck's own: the app is -# asked for a desktop session with Steam's desktop client open in it (the request file -# droiddeck-steam-launch uses; the app ends this session and starts that one). Anything else - -# "gamescope", the session Game Mode already is - has nothing to do. echo "steamos-session-select: $*" >&2 case "${1:-}" in ""|gamescope) exit 0 ;; diff --git a/tools/linuxfs/overlay/usr/bin/steamos-update b/tools/linuxfs/overlay/usr/bin/steamos-update index f7536a443..bc2d00e8e 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-update +++ b/tools/linuxfs/overlay/usr/bin/steamos-update @@ -1,7 +1,4 @@ #!/bin/sh -# What the Steam client calls in Deck mode to look for a SteamOS update. There is no SteamOS -# here: the answer is "no update", which is exit 7 in SteamOS's convention, and the client moves -# on. --supports-duplicate-detection is a capability probe and answers yes. Staged from the apk. case "${1:-}" in --supports-duplicate-detection) exit 0 ;; esac diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent b/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent index c8ca11960..18130a57b 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent @@ -68,7 +68,6 @@ class AgentError(Exception): self.code = code -# --- X11: just enough of the protocol for root properties, the window tree and events --------- class X: """One connection to an Xwayland server. The rootfs has xprop but no python X library.""" @@ -266,7 +265,7 @@ class FocusWatcher(threading.Thread): while True: self.changed.wait(timeout=5) self.changed.clear() - time.sleep(0.2) # let a burst of property changes settle + time.sleep(0.2) try: snap = focus_snapshot() except (OSError, AgentError) as e: @@ -286,9 +285,7 @@ class FocusWatcher(threading.Thread): try: x = X(os.environ["DISPLAY"]) wanted = {x.atom(name) for name in FOCUS_PROPS} - x.select(x.root, 0x400000) # PropertyChangeMask - # Waits for events as long as there are none: the connect timeout left in place - # here dropped and remade the connection, and took a snapshot, every few seconds. + x.select(x.root, 0x400000) x.s.settimeout(None) self.changed.set() while True: @@ -300,7 +297,6 @@ class FocusWatcher(threading.Thread): time.sleep(2) -# --- Steam client DevTools (CDP) ----------------------------------------------------------------- class WebSocket: """A minimal RFC 6455 client: text frames out, text frames in. Enough for CDP.""" @@ -384,7 +380,7 @@ class WebSocket: op = b0 & 0x0F if op == 8: raise OSError("DevTools closed the WebSocket") - if op == 9: # ping + if op == 9: continue if op in (0, 1, 2): message += data @@ -481,7 +477,6 @@ def steam_selection(agent_dir, timeout=5): return {"appId": int(value)} -# --- Game processes ------------------------------------------------------------------------------ def game_pids(app_id): """Our processes started for this Steam app (Steam sets SteamAppId / SteamGameId in their @@ -538,7 +533,6 @@ def quit_game(agent_dir, app_id, timeout): return {"appId": int(app_id), "steps": steps, "remaining": game_pids(app_id)} -# --- Requests ------------------------------------------------------------------------------------ VR_HELPER = "/usr/local/bin/droiddeck-steamvr" VR_ACTIONS = ("status", "install", "start", "stop") @@ -716,7 +710,7 @@ def answer(agent_dir, path): result = dict(handle(agent_dir, req), ok=True) except AgentError as e: result = {"ok": False, "error": {"code": e.code, "message": str(e)}} - except Exception as e: # an answer of some kind always goes back + except Exception as e: result = {"ok": False, "error": {"code": "AGENT_FAILED", "message": "%s: %s" % (type(e).__name__, e)}} write_json(agent_dir / "resp" / (rid + ".json"), result) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run b/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run index 847c272b6..20e6bbba4 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run @@ -1,33 +1,17 @@ #!/bin/bash -# An AppImage imported from the front end (AppImageManager), extracted once at import into -# /opt/appimages/user/<id>/app -# and started from there: proot has no FUSE to mount the image, and extracting it at every -# start (APPIMAGE_EXTRACT_AND_RUN) would cost the whole image in /tmp each time. -# droiddeck-appimage-run /opt/appimages/user/<id> [args...] set -u dir=${1:?usage: droiddeck-appimage-run <imported AppImage directory> [args...]} shift export APPDIR=$dir/app -# APPIMAGE is left unset on purpose: with it an app believes it runs from an image file and offers -# to add itself to the menu (DuckStation) or update the file in place - DroidDeck already does the -# one, and there is no image file for the other. unset APPIMAGE export ARGV0=$(cat "$dir/name" 2>/dev/null || basename "$dir") if [ ! -x "$APPDIR/AppRun" ]; then echo "droiddeck-appimage-run: $APPDIR/AppRun is missing; import the AppImage again" >&2 exit 127 fi -# Browsers sandbox their own children with namespaces and a setuid helper, neither of which proot -# can give them (the Flatpak sandboxes get the same; see BwrapSpawner): Firefox and its forks (Zen, -# LibreWolf) by these switches, Electron and Chromium builds by --no-sandbox. Chromium on Wayland -# also asks the render node for a DRM version KGSL cannot give, so with an X server up it stays on -# X11, which it picks from the session type. export MOZ_DISABLE_CONTENT_SANDBOX=1 MOZ_DISABLE_GMP_SANDBOX=1 MOZ_DISABLE_RDD_SANDBOX=1 export MOZ_DISABLE_SOCKET_PROCESS_SANDBOX=1 MOZ_DISABLE_UTILITY_SANDBOX=1 -# Android 16 refuses Firefox the size seal on a tab's shared memory; see droiddeck-desktop. export MOZ_SHM_NO_SEALS=1 -# sharun images start the app through their own ld.so, patched not to read /etc/ld.so.preload, and -# drop LD_PRELOAD unless allowed: without this the app never gets the session's pads. if [ -s /etc/ld.so.preload ]; then preload=$(tr -s '\n' ':' < /etc/ld.so.preload) export LD_PRELOAD="${preload%:}${LD_PRELOAD:+:$LD_PRELOAD}" SHARUN_ALLOW_LD_PRELOAD=1 @@ -74,8 +58,6 @@ fi echo "== appimage $ARGV0 ($APPDIR) ${flags[*]} FEX $fex Qt $qt${QT_QPA_PLATFORM:+ ($QT_QPA_PLATFORM)}" cd "$APPDIR" || exit 1 started=$(mktemp "${TMPDIR:-/tmp}/droiddeck-appimage.XXXXXX") -# Apps expect a session bus; Electron's main process traps (SIGTRAP) on an address it cannot -# parse. A run mode has none, so one is started for the app, as for Flatpak apps. case ${DBUS_SESSION_BUS_ADDRESS:-} in unix:*) "${run[@]}" "${flags[@]}" "$@" ;; *) unset DBUS_SESSION_BUS_ADDRESS @@ -100,10 +82,6 @@ if [ "$rc" -ne 0 ] && [ -n "$started" ]; then done fi rm -f "$started" -# The program a session was started for may hand over to another process and exit: Electron apps -# relaunch themselves (Obsidian on its first start), launchers fork their real program. The -# session ends with this script, and takes the rest with it - so it waits while anything running -# from the image is still up. image_running() { local p exe arg cwd for p in /proc/[0-9]*; do diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap b/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap index 0070565c7..909056fcd 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap @@ -1,17 +1,4 @@ #!/usr/bin/python3 -# bubblewrap for Flatpak (FLATPAK_BWRAP), in a runtime that cannot have it. -# -# bwrap builds each sandbox from mount namespaces, which Android gives no app. proot gives the same -# picture - Flatpak's runtime as /usr, the app as /app, host paths bound where Flatpak asks - but a -# proot nested inside the session's proot runs every system call through two tracers, some thirty -# times slower on a program's start. So a sandbox is described to the app instead, over a socket -# only this app's uid may use, and the app starts it as a proot of its own beside the session's -# (BwrapSpawner). This process stands in for the sandbox until it ends: its output, its pid for -# --info-fd, and its exit status come back here. -# -# Sandboxes that remap nothing - Flatpak's install triggers (`--ro-bind / /` and the directory they -# update) and the D-Bus proxy (every top-level directory bound onto itself) - need no proot at all -# and run here directly, with the file descriptors Flatpak handed them. import base64 import json import os @@ -23,7 +10,6 @@ import sys SOCKET = "\0" + os.environ.get("DROIDDECK_BWRAP_SOCKET", "com.droiddeck.launcher.bwrap") VERSION = "bubblewrap 0.11.0 (droiddeck)" -# Options and how many arguments each takes; bwrap(1). NO_ARG = { "--unshare-user", "--unshare-user-try", "--unshare-ipc", "--unshare-pid", "--unshare-net", "--unshare-uts", "--unshare-cgroup", "--unshare-cgroup-try", "--unshare-all", "--share-net", @@ -68,7 +54,6 @@ def expand(argv): a = argv[i] if a == "--args" and i + 1 < len(argv): data = read_fd(int(argv[i + 1])) - # Each argument ends in a NUL; an empty one (--setenv LD_LIBRARY_PATH "") is kept. parts = data.split(b"\0") if parts and parts[-1] == b"": parts.pop() @@ -155,7 +140,6 @@ def parse(argv): elif a == "--dev": s.ops.append({"t": "dev", "dest": args[0]}) elif a in ("--seccomp", "--add-seccomp-fd"): - # proot is the sandbox; there is no filter to install under it. os.close(int(args[0])) elif a == "--block-fd": read_fd(int(args[0])) @@ -165,8 +149,6 @@ def parse(argv): s.info_fds.append(int(args[0])) elif a == "--json-status-fd": s.json_fds.append(int(args[0])) - # Everything else - namespaces, capabilities, labels, locks, --remount-ro, --chmod - has - # nothing to act on under proot. if not s.command: sys.stderr.write("bwrap: No command specified\n") sys.exit(1) @@ -196,7 +178,6 @@ def remaps_nothing(s): if not os.path.islink(op["dest"]) or os.readlink(op["dest"]) != op["target"]: return False elif t == "file": - # The D-Bus proxy's /.flatpak-info names the app to the bus; nothing here reads it. if op["dest"] != "/.flatpak-info": return False elif t == "dir": @@ -258,7 +239,6 @@ def run_by_app(s): for op in s.ops: op = dict(op) if op["t"] == "bind": - # The app resolves no guest symlinks; hand it the path they lead to. op["src"] = real(op["src"]) if op["t"] == "file": op["data"] = base64.b64encode(op["data"]).decode() @@ -274,7 +254,6 @@ def run_by_app(s): sys.stderr.write("bwrap: DroidDeck is not answering (%s); open DroidDeck and try again\n" % e) sys.exit(1) send(sock, b"R", json.dumps(request).encode()) - # This process is the sandbox as far as Flatpak is concerned: ended, it takes the sandbox down. for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP): signal.signal(sig, lambda *_: os._exit(128 + 15)) status = 1 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks index 865f7e3b6..28bdd8ac5 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks @@ -29,7 +29,7 @@ def steam_libraries(home, data): try: selected = {Path(e["path"]): str(e["label"]) for e in json.loads(Path(listing).read_text())} except (OSError, ValueError, KeyError, TypeError): - pass # Fall back to the client's registered libraries if the app's list is unreadable. + pass vdf = steam / "steamapps/libraryfolders.vdf" if vdf.is_file(): parser = runpy.run_path(str(Path(__file__).with_name("droiddeck-steam-ui-scale"))) @@ -76,7 +76,6 @@ def update_places(path, libraries): else: if not libraries: return - # Leave KDE's file version unset so it still adds its normal Home/Downloads/etc. places. root = ET.Element("xbel", {"version": "1.0"}) before = ET.tostring(root) seen = set() @@ -112,7 +111,6 @@ def update_places(path, libraries): def bookmarks_file(home, config): - # libfm-qt reads GTK3 bookmarks first, falling back to the legacy file when empty. current = config / "gtk-3.0/bookmarks" legacy = home / ".gtk-bookmarks" if current.is_file() and current.read_text(errors="surrogateescape").strip(): @@ -153,7 +151,6 @@ def main(): home = Path.home() config = Path(os.environ.get("XDG_CONFIG_HOME") or home / ".config") label = os.environ.get("BL_LIBRARY_LABEL") - # An unbound mount-point directory may remain from a previous session. available = bool(label) and (LIBRARY / "steamapps").is_dir() update_bookmark(bookmarks_file(home, config), label if available else None) data = Path(os.environ.get("XDG_DATA_HOME") or home / ".local/share") diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-clipboard b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-clipboard index 0de5aff20..ccb7768c6 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-clipboard +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-clipboard @@ -1,9 +1,5 @@ #!/bin/bash -# Plasma starts this after its nested Xwayland display is available. The session launcher still -# sees the outer compositor before startplasma, so starting its X11 helper there cannot work. [ -n "${BL_ANDROID_CLIPBOARD:-}" ] && [ -n "${DISPLAY:-}" ] || exit 0 -# The native helper dies with its parent. Keep this shell until it exits: Plasma's autostart -# launcher may itself exit immediately after starting us. /usr/local/bin/droiddeck-clipboard "$BL_ANDROID_CLIPBOARD" & child=$! trap 'kill "$child" 2>/dev/null' EXIT diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games index 0773601f0..7b59fb468 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games @@ -1,10 +1,4 @@ #!/usr/bin/python3 -# The client's installed games, on the desktop. The client writes a menu entry per game itself -# (`steam steam://rungameid/<id>`, with the game's icon), which the desktop's `steam` shim hands to -# the app - so an entry of ours exists only for a game the client wrote none for. Plus two links in -# the home folder to the game files. Run at every desktop start; stale entries go. -# -# droiddeck-desktop-games [steam root] import os import re import sys @@ -12,8 +6,6 @@ import sys HOME = os.environ.get("HOME", "/root") STEAM_ROOT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HOME, ".local/share/Steam") LIBRARIES = ((STEAM_ROOT, "Steam Games"), ("/mnt/droiddeck-sd", "SD Games")) -# The client's own tools and runtimes live in steamapps beside the games; they are not titles. A -# depot with a toolmanifest.vdf is one too (a runtime or a Proton), listed here or not. NOT_GAMES = {228980, 1070560, 1391110, 1493710, 1628350, 3127680, 4183110, 4185400, 4427310, 4628740} APPS_DIR = os.path.join(HOME, ".local/share/applications") PREFIX = "droiddeck-game-" @@ -41,7 +33,6 @@ def games(library): continue name = NAME.search(text) state = STATE.search(text) - # StateFlags 4 = fully installed; anything else is downloading, updating or broken. if not name or not state or (int(state.group(1)) & 4) == 0: continue installdir = INSTALLDIR.search(text) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-window-rules b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-window-rules index df6527ecd..aaf304a8d 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-window-rules +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-window-rules @@ -1,5 +1,4 @@ #!/bin/bash -# Port the old desktop's melonDS window rule once; later window rules belong to the user. set -euo pipefail config=${XDG_CONFIG_HOME:-$HOME/.config} marker=$config/droiddeck-window-rules-v1 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-esync b/tools/linuxfs/overlay/usr/local/bin/droiddeck-esync index 410a9c442..d4cb5267a 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-esync +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-esync @@ -1,7 +1,4 @@ #!/usr/bin/python3 -# droiddeck-esync: picks and stages the esync pack for each Proton build. -# Credit: esync and ntsync were designed by Elizabeth Figura for Wine and Proton, and fsync by Elizabeth Figura -# and Paul Gofman; droiddeck-esync, droiddeck-ntsync and droiddeck-fsync build on that work. import contextlib import fcntl import hashlib diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex index 22519f09b..91749f977 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex @@ -252,9 +252,6 @@ def install_preloads(root): if os.path.isfile(source): copy_if_changed(source, os.path.join(root, "lib/x86_64-linux-gnu/droiddeck", name)) wanted = [name for name in host_preloads() if name in shipped] - # The fault report is preloaded from the runtime's own list, which only x86 loaders read: through - # LD_PRELOAD it reached FEX's own loader too, and every FEX after it, which could not open it and - # said so at each start. Listed only with both builds, or i386 programs would say so instead. if len(shipped.get(FAULT_REPORT, ())) == len(GUEST_LIBDIRS): wanted.append(FAULT_REPORT) write_if_changed(os.path.join(root, "etc", "ld.so.preload"), "".join("/usr/$LIB/droiddeck/%s\n" % name for name in wanted)) @@ -411,8 +408,6 @@ def split_paths(value): def data_dir(env): - # FEXServer's socket lives here. Under ~/.local its host path passes sun_path's 108 bytes, and - # proot then binds it to a temporary name only the server sees: no client ever reaches it. runtime = env.get("XDG_RUNTIME_DIR") if runtime and os.path.isdir(runtime) and os.access(runtime, os.W_OK): return os.path.join(runtime, "fex") diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak index a8226b513..ca9f8ef70 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak @@ -1,27 +1,14 @@ #!/usr/bin/python3 -# The app's store talks to Flatpak through this: one JSON object per line on stdout, so progress -# reaches the front end as numbers rather than a terminal's redrawn bar. Per-user installation, -# Flathub as the remote, this device's architecture. -# -# droiddeck-flatpak install <app-id> the app and whatever runtimes it needs -# droiddeck-flatpak install-bundle <file> a .flatpak file, its runtimes from Flathub -# droiddeck-flatpak uninstall <app-id> the app, then runtimes nothing uses any more -# droiddeck-flatpak update [<app-id>] one app, or everything with an update -# droiddeck-flatpak updates what has one (a "updates" line) -# -# Lines: {"e":"op","i":1,"n":3,"kind":"install","ref":"..."} as each step starts, -# {"e":"progress","i":1,"n":3,"percent":45,"status":"..."} while it runs, -# {"e":"error","message":"..."} and {"e":"done"} at the end (with "id" for a bundle). Exit status 0 on success. import json import os import sys os.environ.setdefault("FLATPAK_BWRAP", "/usr/local/bin/droiddeck-bwrap") -import gi # noqa: E402 +import gi gi.require_version("Flatpak", "1.0") -from gi.repository import Flatpak, Gio, GLib # noqa: E402 +from gi.repository import Flatpak, Gio, GLib REMOTE = "flathub" @@ -82,12 +69,6 @@ def transaction(inst): progress.connect("changed", changed) def on_operation_error(tr, op, err, details): - # A runtime extension that fails to install (a missing translation, an unneeded GL - # driver) is not worth losing the app over; anything else ends the transaction. - # FLATPAK_TRANSACTION_ERROR_DETAILS_NON_FATAL is 1, but PyGObject does not always hand the - # flags over ("g_value_get_flags: assertion ... failed" and a 0). An extension installed - # alongside an app - its translations, debug data, a GL or codecs runtime - is what Flatpak - # marks non-fatal, as the flatpak CLI does: its failure costs that extension, not the app. try: non_fatal = bool(int(details) & 1) except (TypeError, ValueError): @@ -108,7 +89,6 @@ def transaction(inst): t.connect("ready", on_ready) t.connect("new-operation", on_new_operation) t.connect("operation-error", on_operation_error) - # A dependency found on more than one remote comes from the first (Flathub, the only one). t.connect("choose-remote-for-ref", lambda tr, ref, runtime_ref, remotes: 0) t.connect("end-of-lifed-with-rebase", lambda *a: False) t.connect("add-new-remote", lambda *a: False) @@ -135,7 +115,6 @@ def install(app_id): if app_ref(inst, app_id) is not None: emit(e="done", note="already installed") return 0 - # Flathub publishes apps on "stable"; libflatpak's default branch is "master". branch, last = None, None for candidate in ("stable", "master"): try: @@ -149,8 +128,6 @@ def install(app_id): t, state = transaction(inst) t.add_install(REMOTE, "app/%s/%s/%s" % (app_id, arch, branch), None) rc = run(t, state, report_done=False) - # A failure the transaction did not report (an error handler that raised, say) must not read - # as an install: the app is there, or it is not. if rc == 0 and app_ref(installation(), app_id) is None: emit(e="error", message="%s did not install; see flatpak-install.log" % app_id) return 1 @@ -192,7 +169,6 @@ def uninstall(app_id): t.add_uninstall(ref.format_ref()) rc = run(t, state) if rc == 0: - # Runtimes only this app used; Flatpak's own `uninstall --unused`. try: unused = inst.list_unused_refs(None, None) except GLib.Error: diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-run b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-run index 45a6e37ed..08e6e7a26 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-run +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-run @@ -1,19 +1,10 @@ #!/bin/bash -# A Flatpak app from the front end, full screen under gamescope like an emulator: -# droiddeck-flatpak-run <app-id> [args...] set -u export FLATPAK_BWRAP=/usr/local/bin/droiddeck-bwrap case ":${XDG_DATA_DIRS:-}:" in *"/flatpak/exports/share:"*) ;; *) export XDG_DATA_DIRS=$HOME/.local/share/flatpak/exports/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share} ;; esac -# Under gamescope - a program's session (BL_INSIDE), or droiddeck-gpu from the desktop -# (GAMESCOPE_WAYLAND_DISPLAY) - windows have to reach gamescope's X server. Flatpak would hand an -# app wayland-0 (it finds it in the runtime directory even with WAYLAND_DISPLAY unset) and the -# window would open behind gamescope. gamescope's own Wayland socket is no way out: Chromium on -# Wayland asks the render node it names for its DRM version, which KGSL cannot answer, and exits. -# The session is X11 as far as an app can tell; Electron picks its platform from that. -# Its own flatpak options (--branch, --command, from a menu entry) come first, then the app id. socket=() if [ -n "${BL_INSIDE:-}" ] || [ -n "${GAMESCOPE_WAYLAND_DISPLAY:-}" ]; then unset WAYLAND_DISPLAY @@ -21,7 +12,6 @@ if [ -n "${BL_INSIDE:-}" ] || [ -n "${GAMESCOPE_WAYLAND_DISPLAY:-}" ]; then socket=(--nosocket=wayland --socket=x11) fi echo "== flatpak run ${socket[*]} $*" -# Apps expect a session bus (settings, portals, single-instance checks); a run mode has none. if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-run-session >/dev/null 2>&1; then exec dbus-run-session -- flatpak run "${socket[@]}" "$@" fi diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-setup b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-setup index 2d0b2e790..7d117c453 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-setup +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-setup @@ -1,26 +1,10 @@ #!/bin/bash -# Puts Flatpak into the runtime, with Flathub as a per-user remote. Run by the app under proot's -# fake root (pacman and pacman-key refuse any other uid), outside a session. -# -# The runtime's pacman database lists only the base image: the desktop and emulator packages were -# unpacked over it without registering. Asked for flatpak the usual way, pacman "reinstalls" about -# a hundred of those - Mesa among them, over the runtime's own Turnip build with its KGSL backend - -# and then runs every hook in the image, mkinitcpio included. So pacman only fetches and checks the -# few packages Flatpak needs on top of this image, verifying each against the Arch Linux ARM -# keyring, and they are unpacked here without hooks or scripts, like the desktop package itself. -# -# droiddeck-flatpak-setup install or repair; prints "== STEP ..." milestones set -u export LANG=C.UTF-8 step() { echo "== STEP $*"; } fail() { echo "== FAIL $*"; exit 1; } -# Flatpak, the libraries it and appstream add to this image, and the D-Bus proxy flatpak run puts -# between an app and the session bus. bubblewrap is not among them: Android gives an app no user -# namespaces, and droiddeck-bwrap stands in for it (FLATPAK_BWRAP). PACKAGES="flatpak ostree appstream libxmlb libmalcontent libfyaml accountsservice composefs xdg-dbus-proxy" -# Libraries Flatpak and the store's helper also need, which the Desktop package brings but a -# runtime without it may lack: each is fetched only when its file is not already there. while read -r pkg probe; do [ -n "$pkg" ] || continue compgen -G "$probe" >/dev/null || PACKAGES="$PACKAGES $pkg" @@ -41,8 +25,6 @@ WORK=/var/cache/droiddeck-flatpak CONF=$WORK/pacman.conf mkdir -p "$WORK/pkg" "$WORK/db" || fail "cannot create $WORK" -# The image's pacman.conf names a download user this rootfs does not have (only root exists) and a -# sandbox proot cannot set up. Its one mirror is plain HTTP; these serve the same repos over TLS. { for m in https://ca.us.mirror.archlinuxarm.org https://fl.us.mirror.archlinuxarm.org \ https://de3.mirror.archlinuxarm.org https://nl.mirror.archlinuxarm.org; do echo "Server = $m/\$arch/\$repo"; done @@ -58,8 +40,6 @@ if [ ! -f /etc/pacman.d/gnupg/pubring.gpg ] && [ ! -f /etc/pacman.d/gnupg/pubrin pacman-key --populate archlinuxarm >/dev/null 2>&1 || fail "pacman-key --populate failed" fi -# A database of our own, so the image's stays exactly as it was built: -Sy on it would leave the -# rest of the image a partial upgrade behind the repos. step "Reading the package lists" pacman --config "$CONF" --dbpath "$WORK/db" --noconfirm -Sy >/dev/null 2>&1 \ || pacman --config "$CONF" --dbpath "$WORK/db" --noconfirm -Sy 2>&1 | tail -3 \ @@ -67,8 +47,6 @@ pacman --config "$CONF" --dbpath "$WORK/db" --noconfirm -Sy >/dev/null 2>&1 \ [ -f "$WORK/db/sync/extra.db" ] || fail "could not reach the Arch Linux ARM mirrors" step "Downloading Flatpak" -# -w only downloads; pacman still checks each package's signature against the keyring. -dd: the -# dependencies are already in the image (and checked below by what the binaries actually link). out=$(pacman --config "$CONF" --dbpath "$WORK/db" --cachedir "$WORK/pkg" --noconfirm -Sw -dd $PACKAGES 2>&1) rc=$? echo "$out" | grep -vE '^\s*$' | tail -4 @@ -84,10 +62,8 @@ for name in $PACKAGES; do echo "$name $ver" >> "$WORK/installed.new" done mv -f "$WORK/installed.new" "$WORK/installed" -# Only the newest file of each package is worth keeping for a repair. find "$WORK/pkg" -type f -mtime +1 -delete 2>/dev/null -# What the binaries link is the real dependency list: anything this image lacks shows here. missing=$(for f in /usr/bin/flatpak /usr/lib/libflatpak.so.0 /usr/lib/libostree-1.so.1 /usr/bin/xdg-dbus-proxy; do ldd "$f" 2>&1 | grep -E 'not found|version .* not found' done | sort -u) @@ -98,9 +74,6 @@ python3 -c 'import gi; gi.require_version("Flatpak", "1.0"); from gi.repository step "Adding Flathub" export FLATPAK_BWRAP=/usr/local/bin/droiddeck-bwrap -# Flathub's descriptor - its URL and the key its commits are signed with - is carried here rather -# than fetched: adding the remote then needs no network, and a descriptor that did not arrive -# whole cannot be what failed. Flathub's own copy: https://dl.flathub.org/repo/flathub.flatpakrepo cat > "$WORK/flathub.flatpakrepo" <<'REPO' [Flatpak Repo] Title=Flathub @@ -115,7 +88,6 @@ if ! flatpak remote-list --user --columns=name 2>/dev/null | grep -qx flathub; t out=$(flatpak remote-add --user --if-not-exists flathub "$WORK/flathub.flatpakrepo" 2>&1) rc=$? echo "$out" - # What Flatpak said goes up with the failure, so the app can show it rather than a guess. [ $rc -eq 0 ] || fail "could not add the Flathub remote: $(echo "$out" | grep -v '^[[:space:]]*$' | tail -1)" fi echo "== DONE $(flatpak --version)" diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fonts b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fonts index cddcc15bc..030585f3d 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fonts +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fonts @@ -1,24 +1,12 @@ #!/bin/bash -# DroidDeck's fontconfig setup for the Linux runtime. The rootfs ships DejaVu alone and no rules -# for a default family: applications then find fonts but no default, and Firefox logs "no fonts" -# and draws pages without text. DejaVu also has no CJK, Thai or Arabic glyphs, so those draw as -# tofu boxes. The app binds Android's fonts for the session, which adds language-specific fallbacks -# separately. This script links fontconfig's default rules and writes generic DejaVu aliases to an -# owned conf.d file. Fontconfig reads local.conf afterward, so user overrides keep precedence and -# are never replaced. -# The first argument is the root to set up, "/" in a session; the tests point it at a temp tree. set -u root=${1:-/} root=${root%/} -# "/" strips to empty above; put it back so the paths below read as absolute and the test for a -# real session ("/") still holds. [ -n "$root" ] || root=/ conf_dir=$root/etc/fonts defaults_dir=$root/usr/share/fontconfig/conf.default mkdir -p "$conf_dir/conf.d" 2>/dev/null -# The default rules (sans-serif -> DejaVu Sans and the rest) live as links in /etc/fonts/conf.d, -# and the rootfs has none. Linked by name, so a file we ship is never overwritten. if [ -d "$defaults_dir" ]; then for f in "$defaults_dir"/*.conf; do [ -e "$f" ] || continue diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env index 454fb7e74..f74779c86 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env @@ -25,8 +25,6 @@ def game_id(env): return str(number % 2**32) -# A .NET (CoreCLR) runtime under FEX crashes in the runtime itself unless memory ordering is fully -# emulated and multiblock is off: the Stability preset. DOTNET_FEX = { "FEX_TSOENABLED": "1", "FEX_VECTORTSOENABLED": "1", @@ -69,8 +67,6 @@ class Game: return any(a == name or a.startswith(name + "=") for a in self.args) -# Each fix looks at a Game and returns (environment, extra arguments, note) for the games it -# applies to, or None. Adding one is writing a function and decorating it. FIXES = [] @@ -91,16 +87,12 @@ def dotnet_memory_ordering(game): @fix def godot_vulkan(game): - # Godot 4 on Direct3D 12 through VKD3D-Proton fails to create its swap chain and runs behind a - # black window; its own Vulkan renderer draws. if is_godot(game) and not game.has_arg("--rendering-driver"): return {}, ["--rendering-driver", "vulkan"], "Godot: Vulkan renderer" @fix def godot_sentry(game): - # The Sentry GDExtension's native crash handler takes the .NET runtime down under FEX. Godot - # loads it on demand and carries on without it. sentry = sorted(n[:-4] for n in game.files if n.startswith("libsentry.windows.") and n.endswith(".dll")) if is_godot(game) and sentry: return {"WINEDLLOVERRIDES": ";".join(n + "=d" for n in sentry)}, [], "Godot: Sentry crash handler off" @@ -166,8 +158,6 @@ def apply_config(env, config, app_id, fixes=None): result[name] = value else: raise ValueError("invalid environment value") - # The app's texture filtering (Display page), as DXVK options appended after the profiles so - # a DXVK_CONFIG entry of the user's keeps its own. game_dxvk = config.get("dxvkConfigGames", {}) if not isinstance(game_dxvk, dict): raise ValueError("invalid game dxvk configuration") diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window b/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window index 42a01f274..f5cb4007c 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window @@ -8,8 +8,6 @@ import sys def advertise_compositing(): - # This is gamescope's private X server: it composites alpha buffers already, but GTK also - # requires the compositing-manager selection and frame-extents hint before enabling CSD. x = c.CDLL('libX11.so.6') display_type, window_type = c.c_void_p, c.c_ulong x.XOpenDisplay.argtypes = [c.c_char_p] @@ -41,7 +39,6 @@ def advertise_compositing(): x.XChangeProperty(display, root, atom('_NET_SUPPORTED'), atom('ATOM'), 32, 2, c.byref(extents), 1) x.XSync.argtypes = [display_type, c.c_int] x.XSync(display, False) - # Keep the selection's connection alive until the program exits. return x, display diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores b/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores index dad3d1033..3ea0c5681 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores @@ -22,14 +22,10 @@ import sys import time from pathlib import Path -# Image paths (lower case, backslashes), matched at the end: the helpers held back, and the -# programs whose window having focus means the user is looking at the launcher. HELPERS = ("\\rockstar games\\social club\\socialclubhelper.exe",) FRONTS = HELPERS + ("\\rockstar games\\launcher\\launcher.exe",) NICE = 19 POLL = 2.0 -# How often a held launcher is looked over again: for a helper the unheld Launcher.exe started -# since, and for threads the program has moved back. RESCAN = 10.0 STEAM_CLIENT = 769 @@ -72,8 +68,6 @@ class Cores: try: os.sched_setaffinity(tid, cpus) except OSError: - # A mask outside the app's cpuset now (Android shrinks it in the background) is - # EINVAL; the session's own is always allowed. os.sched_setaffinity(tid, self.own()) os.setpriority(os.PRIO_PROCESS, tid, nice) @@ -83,7 +77,7 @@ class Holder: self.proc = proc self.cpufreq = cpufreq self.cores = cores or Cores() - self.held = {} # pid -> {tid: (cpus, nice) before the hold} + self.held = {} self.core = None def slow_core(self): @@ -117,10 +111,8 @@ class Holder: if now != hold: self.cores.set(tid, *hold) except OSError: - continue # the thread ended + continue if tid not in before: - # A thread that already has the hold inherited it from a held parent: it gets - # what its process had, or the helpers' first. before[tid] = now if now != hold else before.get(pid) or self.first() or now for pid in [pid for pid in self.held if not (self.proc / str(pid)).exists()]: del self.held[pid] @@ -191,8 +183,6 @@ class Governor: self.holder.release() self.report("released: the launcher has focus") return - # Nothing to look for until a game has had focus: launchers only run for one, and a launcher - # left running after its game stays held through the Steam client's own focus. if self.app in (None, 0, STEAM_CLIENT) and not self.holder.held: return now = self.clock() diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 b/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 index ce74b0b50..7ce786740 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 @@ -14,7 +14,7 @@ import uuid import gi gi.require_version("Gio", "2.0") -from gi.repository import Gio, GLib # noqa: E402 +from gi.repository import Gio, GLib NAME = "org.freedesktop.login1" ROOT = "/org/freedesktop/login1" @@ -56,8 +56,6 @@ class LoginManager: self.preparation = None info = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION).interfaces[0] self.registration = bus.register_object(ROOT, info, self.method, self.property, None) - # Steam can issue its suspend command through the legacy UPower service even - # while listening to login1's sleep/resume notifications. Use one handshake. upower_info = Gio.DBusNodeInfo.new_for_xml(UPOWER_INTROSPECTION).interfaces[0] self.upower_registration = bus.register_object(UPOWER_ROOT, upower_info, self.method, None, None) self.monitor = Gio.File.new_for_path(str(self.directory)).monitor_directory(Gio.FileMonitorFlags.NONE, None) @@ -86,8 +84,6 @@ class LoginManager: self.pending = uuid.uuid4().hex self.notify(True) reply.return_value(None) - # Flush the signal and method reply without blocking this D-Bus callback. Once both are - # on the wire, publishing the request lets the host safely freeze this service. self.bus.flush(None, self.publish_after_flush, self.pending) def publish_after_flush(self, connection, result, token): @@ -122,7 +118,6 @@ class LoginManager: return if not re.fullmatch(r"[a-f0-9]{32}", token) or self.pending is not None: return - # Android may have returned before this helper read its request. if self.read_state() == token + ":awake": return self.pending = token @@ -138,8 +133,6 @@ class LoginManager: return self.check_state() if self.pending == token: - # Keep Steam runnable while it handles the system sleep notification. - # This is a bounded grace period, not a claim that every game has saved. self.preparation = GLib.timeout_add(NATIVE_PREPARE_MS, self.finish_preparation, token) def finish_preparation(self, token): @@ -171,7 +164,6 @@ class LoginManager: def check_ack(self): self.timeout = None if self.pending and self.read_state() != self.pending + ":paused": - # A missing host or a failed freeze must not strand Steam in black. self.request.unlink(missing_ok=True) self.native_ready.unlink(missing_ok=True) self.resume() @@ -180,8 +172,6 @@ class LoginManager: def resume(self, force=False): if self.pending is None: if force: - # A missed suspend command leaves Steam without the first half of - # the handshake. Replay both notifications, without pausing again. self.notify(True) self.bus.flush(None, self.finish_recovery, None) return diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install b/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install index c1fe35846..87e741c46 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install @@ -43,7 +43,6 @@ CABEXTRACT = os.environ.get("DROIDDECK_CABEXTRACT", str(TOOLS / "cabextract")) SEVENZIP = os.environ.get("DROIDDECK_7Z", str(TOOLS / "7z")) MSI_MAGIC = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" BURN_MAGIC = 0x00F14300 -# A bundle's manifest names ARM64 packages too; a game's prefix wants the x86 and x64 ones. SKIP_PLATFORMS = ("arm64", "arm") @@ -53,9 +52,6 @@ def tool_env(): env["LD_LIBRARY_PATH"] = ":".join(p for p in (str(TOOLS), env.get("LD_LIBRARY_PATH", "")) if p) return env -# The folders Windows Installer knows by name, as a 64-bit Windows 10 prefix lays them out (Proton's -# user is steamuser). None is a folder whose files a game never needs: installer temp files and -# shortcuts. SKIP = None KNOWN_FOLDERS = { "TARGETDIR": "C:\\", "ROOTDRIVE": "C:\\", "WindowsVolume": "C:\\", @@ -78,25 +74,17 @@ for _name in ("TempFolder", "DesktopFolder", "ProgramMenuFolder", "StartMenuFold "TemplateFolder", "MyPicturesFolder", "AdminToolsFolder"): KNOWN_FOLDERS[_name] = SKIP -# What Wine's msi reports on a 64-bit Windows 10 prefix, for the packages' conditions. SYSTEM_PROPERTIES = { "VersionNT": "603", "VersionNT64": "603", "WindowsBuild": "9600", "ServicePackLevel": "0", "Msix64": "6", "ALLUSERS": "1", "Privileged": "1", "AdminUser": "1", "MsiNTProductType": "1", "UILevel": "2", "INSTALLLEVEL": "1", "ProgramFilesFolder": None, "VersionMsi": "5.00", } -# Win64 component (msidbComponentAttributes64bit); file attributes. COMPONENT_64BIT = 0x100 FILE_NONCOMPRESSED = 0x2000 -# Custom action types that only set a property or a folder: done here, the rest are named in notes. SET_PROPERTY, SET_DIRECTORY, ERROR_ACTION = 51, 35, 19 -# Microsoft's "SFP CA" merge module (MDAC, MSXML): on Windows 2000 and later it parks system DLLs in a -# cache folder and its own code copies them into the system folder past Windows File Protection. -# Skipping the folder change puts them where that copy would. Its actions, and the side-by-side -# merge module's (done here from MsiAssembly), are then left out of the notes. SFP_CACHE_ACTION = "CDirSystemSFPCacheDir" HANDLED_ACTIONS = ("Wdsfpca_", "SxsInstallCA", "SxsUninstallCA") -# Tables whose work needs the package's own code or is not done here yet. UNSUPPORTED_TABLES = ("SelfReg", "Class", "ProgId", "TypeLib", "Extension", "Verb", "MIME", "AppId", "IniFile", "ServiceInstall", "MoveFile", "IsolatedComponent", "PublishComponent", "ODBCDriver", "ODBCDataSource") @@ -123,7 +111,6 @@ class Span: progress(text, self.lo + (self.hi - self.lo) * max(0.0, min(1.0, fraction))) -# ---------------------------------------------------------------- reading the package IDT_ESCAPES = str.maketrans({"\x19": "\n", "\x11": "\r", "\x15": "\t", "\x1b": ";"}) @@ -182,7 +169,6 @@ def to_int(value, default=0): return default -# ---------------------------------------------------------------- conditions TOKEN = re.compile(r'\s*(?:(?P<str>"[^"]*")|(?P<num>-?\d+)(?![\w.])|(?P<op>~?(?:<>|<=|>=|><|<<|>>|=|<|>))' r'|(?P<paren>[()])|(?P<word>[%$?&!]?[A-Za-z_][\w.]*))') @@ -330,7 +316,6 @@ class Conditions: return value not in ("", None) -# ---------------------------------------------------------------- the install def win_to_rel(path): """C:\\windows\\x\\ -> drive_c/windows/x; None for a path off drive C or a folder left out.""" @@ -375,7 +360,7 @@ class Installer: self.span = span or Span() self.notes = [] self.registry = [] - self.files = {} # File key -> rel path it was installed to + self.files = {} self.counts = {"files": 0, "assemblies": 0, "registry": 0} template = self.pkg.summary.get("template", "") self.platform = template.split(";")[0].strip().lower() @@ -385,7 +370,6 @@ class Installer: for name, path in KNOWN_FOLDERS.items(): self.props[name] = path if path is not None else "\0skip" for row in self.pkg.table("Property"): - # The package's own defaults, but never over the folders and the system it runs on. if row["Property"] not in self.props or row["Property"] in ("INSTALLLEVEL", "ALLUSERS", "UILevel"): self.props[row["Property"]] = row["Value"] self.props["ALLUSERS"] = "1" @@ -397,7 +381,6 @@ class Installer: self._resolving = set() self._set_by_action = set() - # -- states, folders, formatted text def feature_state(self, name): return 3 if name in self.included_features else -1 @@ -411,13 +394,11 @@ class Installer: return None value = self.props.get(key) row = self.directories.get(key) - # A folder Windows knows by name, one a custom action set, or a property naming a path. if key in KNOWN_FOLDERS or key in self._set_by_action or row is None: return None if value in (None, "", "\0skip") else win_to_rel(value) parent_key = row.get("Directory_Parent", "") default = long_name(row.get("DefaultDir", "").split(":")[0]) if not parent_key or parent_key == key: - # A root: TARGETDIR (SourceDir) is drive C. return "drive_c" self._resolving.add(key) try: @@ -466,7 +447,6 @@ class Installer: text = re.sub(r"\[([^\[\]]*)\]", one, text) return text - # -- what gets installed def run_set_actions(self): """The property/folder-setting custom actions, in sequence order; the rest go to the notes.""" @@ -581,8 +561,6 @@ class Installer: self.pkg.stream(cabinet[1:], cab) own = True else: - # An external cabinet: a file beside the package, as an installer wrapper lays it out - # (vc_red.cab beside vc_red.msi, netfx_Full.mzz beside netfx_Full_x64.msi). cab = sibling(Path(self.pkg.path), cabinet) if cab is None: self.notes.append("external cabinet %s is not part of the package; its files are missing" % cabinet) @@ -601,9 +579,6 @@ class Installer: def put(self, source, rel): target = self.out / rel - # A DuplicateFile row can name the file's own folder and name (the .NET host packages copy - # dotnet.exe onto itself); Windows Installer takes that as nothing to do. Found and diagnosed - # by whatsupbros on the DroidDeck Discord. if target.exists() and Path(source).samefile(target): return False target.parent.mkdir(parents=True, exist_ok=True) @@ -615,7 +590,6 @@ class Installer: files = self.pkg.table("File") missing = 0 destinations = {} - # Where each file goes: its component's folder, or the assembly store for a global assembly. for row in files: comp = row["Component_"] if comp not in self.included_components: @@ -676,9 +650,6 @@ class Installer: self.notes.append("%d file(s) were not in the package's cabinets" % missing) def value(self, hive, key, name, kind, data, wow64): - # A 32-bit component writes through the 32-bit view, as Wine's msi does: HKLM\Software is - # Software\Wow6432Node, and the prefix's link keys there (Classes, TypeLib, Fonts...) lead - # to the keys both views share - droiddeck-wincomponents follows them when it writes. lower = key.lower() if hive == "HKLM" and wow64 and lower.startswith("software\\") and not lower.startswith("software\\wow6432node\\"): key = "Software\\Wow6432Node\\" + key[len("software\\"):] @@ -787,7 +758,6 @@ class Installer: self.check() if not self.counts["files"] and not self.registry: raise MsiError("the package installs nothing here") - # A component made of several packages (32- and 64-bit) gets them all in one registry.json. values = [] try: with open(self.out / "registry.json", encoding="utf-8") as source: @@ -801,7 +771,6 @@ class Installer: "counts": self.counts, "notes": self.notes} -# ---------------------------------------------------------------- installer wrappers def sibling(path, name): """A file next to [path] called [name], whatever its case; None when there is none.""" @@ -979,8 +948,6 @@ def install_wrapper(path, out): try: result = Installer(str(package), out, span=Span(lo, hi)).install() except MsiError as error: - # A package with nothing for this prefix (a patch for another Windows) is skipped, - # unless it is the only one; anything else is the whole installer's failure. if "installs nothing" in str(error) and len(packages) > 1: total["notes"].append("%s: %s" % (package.name, error)) continue diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-netmanager b/tools/linuxfs/overlay/usr/local/bin/droiddeck-netmanager index 7d468e5ff..f8df451bb 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-netmanager +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-netmanager @@ -1,8 +1,4 @@ #!/usr/bin/python3 -# Based on WinNative (maxjivi05, feature/wayland-gamescope e7a224ae), GPL-3.0. -# Steam speaks NetworkManager; Android owns the radios, routes and connection. Export the -# app's live snapshot through that API, including nearby Wi-Fi access points. Requests to -# scan are handed to Android; Android settings still handle joining networks. import ipaddress import json import re @@ -16,7 +12,7 @@ from xml.sax.saxutils import escape import gi gi.require_version("Gio", "2.0") -from gi.repository import Gio, GLib # noqa: E402 +from gi.repository import Gio, GLib NAME = "org.freedesktop.NetworkManager" ROOT = "/org/freedesktop/NetworkManager" @@ -60,7 +56,6 @@ def v(signature, value): def read_snapshot(): - # Atomic replacement by the app means we never see a partially written handover. try: state = json.loads(SNAPSHOT.read_text()) if not isinstance(state, dict) or not isinstance(state.get("connected"), bool): @@ -71,7 +66,6 @@ def read_snapshot(): def legacy_snapshot(): - # Older APKs do not publish transport metadata. Never guess Wi-Fi from an interface name. try: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.connect(("192.0.2.1", 9)) @@ -101,10 +95,10 @@ def connection_name(state): def connectivity(state): if not state["connected"]: - return 1 # NONE + return 1 if state.get("validated"): - return 4 # FULL - return 2 if state.get("captivePortal") else 3 # PORTAL / LIMITED + return 4 + return 2 if state.get("captivePortal") else 3 def metered(state): @@ -155,7 +149,6 @@ def active_ap(state): def ap_security(capabilities): - # NetworkManager's cipher/key-management flags, from Android's beacon capabilities. wpa = rsn = 0 for group in re.findall(r"\[([^]]+)\]", capabilities.upper()): flags = 0 @@ -280,7 +273,6 @@ def objects(state): if present: found[path] = {NAME + f".IP{version}Config": ip_config(state, version)} if up and not wifi and state.get("wifiEnabled"): - # Android can keep its Wi-Fi radio available while cellular/VPN is the default route. wireless = objects(dict(state, connected=False, transport="none", interface=state.get("wifiInterface", "wlan0"), metered=None)) wifi_device = ROOT + "/Devices/Wifi" @@ -304,13 +296,11 @@ def settings(state): "ssid": v("ay", list((state.get("ssid") or "Wi-Fi").encode("utf-8"))), "mode": v("s", "infrastructure"), } elif state.get("transport") == "cellular": - result["gsm"] = {} # Android owns the SIM/APN; no credentials are exported. + result["gsm"] = {} return result def interface_info(interface, values): - # Register properties on the actual objects as well as returning ObjectManager snapshots. - # Steam/libnm also use Properties.Get/GetAll and introspect the wireless methods. props = "".join(f'<property name="{escape(key)}" type="{value.get_type_string()}" access="read"/>' for key, value in values.items()) node = Gio.DBusNodeInfo.new_for_xml(f'<node><interface name="{interface}">{METHODS.get(interface, "")}{props}</interface></node>') @@ -372,7 +362,7 @@ class NetworkManager: def check(self): state = read_snapshot() if state is None: - return True # A transient read failure must not replace a known link with Ethernet. + return True if state == self.state: return True before, after = self.objects, objects(state) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open index 3ceb080df..0809a228f 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open @@ -33,9 +33,7 @@ APPIMAGE_RUN = os.path.join(BIN, "droiddeck-appimage-run") SCRIPT_RUN = os.path.join(BIN, "droiddeck-script-run") STEAM_LAUNCH = os.path.join(BIN, "droiddeck-steam-launch") CACHE = os.path.join(HOME, ".cache", "droiddeck") -# Where the app adds each kind, and what an added one holds. IMPORTS = {"appimage": ("/opt/appimages/user", "app"), "folder": ("/opt/droiddeck-apps", "entry")} -# Android's own folders on shared storage: never copied in whole. SHARED_FOLDERS = ("alarms", "android", "audiobooks", "dcim", "documents", "download", "movies", "music", "notifications", "pictures", "podcasts", "recordings", "ringtones") SCAN_LIMIT = 4000 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url index 69d5c65aa..890833519 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url @@ -36,7 +36,6 @@ def where(url): def main(argv): url = argv[0] if argv else "" - # The Steam client's own form (steam://openurl/<address>), in case a launcher passes it on. if url.startswith("steam://openurl/"): url = url[len("steam://openurl/"):] launch_dir = os.environ.get("BL_LAUNCH_DIR", "") diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults b/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults index 4e15fae07..04966c4b2 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults @@ -1,9 +1,4 @@ #!/usr/bin/python3 -# Player 1 on the session's controller, for every emulator that would otherwise start on the -# keyboard. The session's pad is an Xbox 360 controller (libfakeinput's identity outside Steam) that -# SDL maps by itself; what an emulator lacks is a profile that points Player 1 at it. One is -# written only where the emulator has none yet - a profile the user made is never touched. -# Run at every desktop start and before every program from the rail. import os HOME = os.environ.get("HOME", "/root") @@ -21,10 +16,6 @@ def seed(path, text, what, done="Player 1 on the controller"): return True -# RPCS3: its global input configuration (rpcs3::utils::get_input_config_dir() + "Default.yml"). -# Without one it adds the keyboard pad handler (pad_config.cpp, cfg_input::load). SDL handler -# names from rpcs3/Input/sdl_pad_handler.cpp (button_list, init_config); the device is -# "<SDL gamepad name> <n>" (sdl_pad_handler::enumerate_devices). RPCS3_PLAYER1 = """Player 1 Input: Handler: SDL Device: Xbox 360 Controller 1 @@ -71,13 +62,8 @@ def rpcs3(): rpcs3_shader_mode() -# RPCS3's default shader mode (Async Recompiler with Shader Interpreter) builds 6650 interpreter -# pipelines at the start of every boot, minutes on an Adreno. The async recompiler compiles only -# what a game draws, in the background, once. A new install starts with it (a config.yml that -# holds only this is merged over RPCS3's defaults); an existing one still on the default is moved -# once, and a marker keeps any later choice the user makes in RPCS3's own settings. RPCS3_INTERPRETER = "Async Recompiler with Shader Interpreter" -RPCS3_RECOMPILER = "Async Recompiler (multi-threaded)" # the GUI says "Async Shader Recompiler"; this is the value (system_config_types.cpp) +RPCS3_RECOMPILER = "Async Recompiler (multi-threaded)" def rpcs3_shader_mode(): @@ -101,7 +87,6 @@ def rpcs3_shader_mode(): open(marker, "w").close() -# ---- ini helpers (DuckStation and PCSX2 share one ini dialect, keys may repeat) ------------- def ini_section(lines, name): """(start, end) line indices of [name]'s body, or None.""" @@ -132,7 +117,6 @@ def patch_ini_section(path, section, body, what, needle="SDL-", require=None, do return new = body.strip("\n").splitlines() if span: - # The controller type the emulator already wrote stays; only bindings are added. have = {l.split("=", 1)[0].strip() for l in lines[span[0]:span[1]] if "=" in l} new = [l for l in new if not (l.split("=", 1)[0].strip() == "Type" and "Type" in have)] end = span[1] @@ -147,8 +131,6 @@ def patch_ini_section(path, section, body, what, needle="SDL-", require=None, do print(f"droiddeck-pad-defaults: {what}: {done} ({path})") -# DuckStation: SDL-<n>/<name>, axes signed (src/util/sdl_input_source.cpp); Pad1 keys from -# src/core/analog_controller.cpp. Its defaults bind the keyboard only (settings.cpp). DUCKSTATION_PAD1 = """Type = AnalogController Up = SDL-0/DPadUp Down = SDL-0/DPadDown @@ -244,17 +226,12 @@ def bios_folder(base): d = os.path.join(base, "bios") if os.path.isdir(ROMS): try: - os.makedirs(d, exist_ok=True) # where the user is told to put their BIOS + os.makedirs(d, exist_ok=True) except OSError: pass return d -# DuckStation (PS1) - set up like ARMSX2: users bring their own BIOS and games, in the ROMs folder. -# Front-end runs use -batch -bigpicture -fullscreen, so the guide button is the pause menu -# (Hotkeys OpenPauseMenu, src/core/hotkeys.cpp), not the analog toggle. Keys from -# src/core/settings.cpp: [BIOS] SearchDirectory, [GPU] Renderer ("Vulkan"), [Main] -# SetupWizardIncomplete (src/duckstation-qt/qthost.cpp). DUCKSTATION_PAD1_GUIDE_FREE = "\n".join(l for l in DUCKSTATION_PAD1.splitlines() if not l.startswith("Analog ")) + "\n" @@ -266,8 +243,6 @@ def duckstation(): if not os.path.isfile(ini) and os.path.isfile(legacy): ini = legacy if not os.path.isfile(ini): - # A new player: everything DuckStation's first-time setup asks is answered here (it finds - # the BIOS in its folder by itself), so a game from the front end boots straight away. bios = bios_folder(ps1) seed(ini, "[Main]\nSetupWizardIncomplete = false\nInhibitScreensaver = false\n\n" f"[BIOS]\nSearchDirectory = {bios}\n\n" @@ -282,17 +257,12 @@ def duckstation(): done="the guide button opens the pause menu") patch_ini_section(ini, "GameList", f"RecursivePaths = {ps1}\n", "DuckStation", needle=ROMS, done="games listed from the ROMs folder") - # One set up by an earlier build still has its first-time setup pending, in the way of a game. if ini_get(ini, "Main", "SetupWizardIncomplete") == "true": ini_set(ini, "Main", "SetupWizardIncomplete", "false") print("droiddeck-pad-defaults: DuckStation: first-time setup already done here") - # Keeping the screen awake goes through org.freedesktop.ScreenSaver, which nothing in the session - # provides (Android decides when the screen sleeps): DuckStation only warns it failed, every game. if (ini_get(ini, "Main", "InhibitScreensaver") or "true") == "true": ini_set(ini, "Main", "InhibitScreensaver", "false") print("droiddeck-pad-defaults: DuckStation: no screensaver inhibit (no such service here)") - # Once, for a DuckStation set up before this: its own empty BIOS folder moves to the ROMs folder, - # and Automatic becomes Vulkan. A later choice in DuckStation's settings is kept. marker = os.path.join(os.path.dirname(ini), ".droiddeck-setup") if os.path.exists(marker): return @@ -307,8 +277,6 @@ def duckstation(): open(marker, "w").close() -# PCSX2: FaceSouth/... names (pcsx2/Input/SDLInputSource.cpp), keys from SIO/Pad/PadDualshock2.cpp. -# An ini without [UI] SettingsVersion is thrown away and rebuilt, so only a complete one is patched. PCSX2_PAD1 = """Type = DualShock2 Up = SDL-0/DPadUp Right = SDL-0/DPadRight @@ -346,8 +314,6 @@ def pcsx2(): -# Dolphin: SDL/<id>/<SDL gamepad name>; control names from InputCommon/.../SDL/SDLGamepad.h (Left Y+ -# is up). A section that exists is taken whole - keys it lacks are unbound - so both are complete. DOLPHIN_GC = """[GCPad1] Device = SDL/0/Xbox 360 Controller Buttons/A = `Button S` @@ -406,11 +372,6 @@ Nunchuk/Stick/Right = `Left X+` """ -# ARMSX2 - the PS2 emulator (PCSX2 with ARM64 recompilers; upstream PCSX2 interprets on ARM64). Its -# files are PCSX2's under $XDG_CONFIG_HOME/ARMSX2 (pcsx2/Pcsx2Config.cpp). The user supplies the BIOS -# and the games; they live in the ROMs folder (/root/ROMs). The guide button opens the pause menu -# (Hotkeys OpenPauseMenu) - the way out of a game started from the front end, which runs ARMSX2 -# without its window (-batch -bigpicture) - so it is not also the analog toggle here. ARMSX2_PAD1 = "\n".join(l for l in PCSX2_PAD1.splitlines() if not l.startswith("Analog ")) + "\n" ROMS = "/root/ROMs" @@ -432,7 +393,7 @@ def ps2_bios_folder(): d = os.path.join(base, "bios") if os.path.isdir(ROMS): try: - os.makedirs(d, exist_ok=True) # where the user is told to put their BIOS + os.makedirs(d, exist_ok=True) except OSError: pass return d @@ -443,8 +404,6 @@ def armsx2(): ini = os.path.join(root, "inis/PCSX2.ini") old = os.path.join(CONFIG, "PCSX2") if not os.path.isfile(ini) and os.path.isfile(os.path.join(old, "inis/PCSX2.ini")): - # Someone who played on PCSX2 here: the same settings, BIOS choice and memory cards (the - # saves), copied once. PCSX2's own folder is left as it was. import shutil for sub in ("inis", "memcards", "bios", "gamesettings", "cheats", "patches", "covers", "inputprofiles", "textures"): @@ -453,22 +412,16 @@ def armsx2(): shutil.copytree(src, os.path.join(root, sub), dirs_exist_ok=True) print(f"droiddeck-pad-defaults: ARMSX2: settings and memory cards copied from PCSX2 ({root})") elif not os.path.isfile(ini): - # A new player: everything ARMSX2's first-time setup asks is answered here - the BIOS - # folder in the ROMs folder, the ps2 folder as its game list, the controller on Player 1 - - # and the BIOS file itself is picked below once one is there. bios, games = ps2_bios_folder(), ps2_folder() seed(ini, "[UI]\nSettingsVersion = 1\nSetupWizardIncomplete = false\n\n" f"[Folders]\nBios = {bios}\n\n" f"[GameList]\nRecursivePaths = {games}\n\n" - # Vulkan outright rather than Auto's pick: the renderer every other path here uses. "[EmuCore/GS]\nRenderer = 14\n\n" "[Hotkeys]\nOpenPauseMenu = Keyboard/Escape\nOpenPauseMenu = SDL-0/Guide\n\n" "[Pad1]\n" + ARMSX2_PAD1, "ARMSX2", "first-time setup, BIOS from " + bios) armsx2_bios(ini) return patch_ini_section(ini, "Pad1", ARMSX2_PAD1, "ARMSX2", require="SettingsVersion") - # The guide button is the pause menu here; PCSX2's profile (ours, copied over) had it as the - # analog toggle as well. with open(ini, encoding="utf-8", errors="replace") as f: lines = f.read().splitlines() kept = [l for l in lines if l.replace(" ", "") != "Analog=SDL-0/Guide"] @@ -497,7 +450,6 @@ def armsx2_bios(ini): if current and os.path.isfile(os.path.join(folder, current)): return try: - # A PS2 BIOS dump is 4 MiB (a few are 8); the .nvm/.mec files beside it are not dumps. dumps = sorted(f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f)) and 4 << 20 <= os.path.getsize(os.path.join(folder, f)) <= 8 << 20) except OSError: @@ -510,12 +462,6 @@ def armsx2_bios(ini): print(f"droiddeck-pad-defaults: ARMSX2: BIOS {pick}") -# Dolphin (PC) has no controller menu, so the guide button is its Toggle Fullscreen hotkey: out of full -# screen is Dolphin's own window with its Config / Graphics / Stop buttons. Hotkeys.ini is all-or- -# nothing - a file with one line drops every other default - so a new one carries Dolphin's defaults, -# generated from HotkeyManager.cpp's LoadDefaults (Linux branch, RetroAchievements on) plus the guide; -# an existing one only gets the guide added to its Toggle Fullscreen. The Wii remote's Home, which -# was on the guide button, is on the right shoulder button instead. DOLPHIN_GUIDE = "`SDL/0/Xbox 360 Controller:Guide`" DOLPHIN_HOTKEYS = """[Hotkeys] Device = XInput2/0/Virtual core pointer @@ -632,11 +578,6 @@ def dolphin(): ini_set(hotkeys, "Hotkeys", "General/Toggle Fullscreen", (current + " | " if current else "") + DOLPHIN_GUIDE) print("droiddeck-pad-defaults: Dolphin: the guide button shows Dolphin's window (Toggle Fullscreen)") - # Games from the ROMs folder's gamecube and wii folders, subfolders included; Vulkan; no - # NKit warning (it will not let a game start until a box is ticked - games run either way, - # the warning is about loading times, netplay and save states); no statistics question. - # Names from Dolphin's MainSettings.cpp / UISettings.cpp. Only what is not set yet is added, - # except the game folders, which are added to whatever the user has. ini = os.path.join(d, "Dolphin.ini") gc = system_folder(("gamecube", "gc", "ngc"), "gamecube") wii = system_folder(("wii",), "wii") @@ -655,9 +596,6 @@ def dolphin(): count = 0 paths = [ini_get(ini, "General", "ISOPath%d" % i) for i in range(count)] changed = [] - # The whole ROMs folder as a game folder (what Dolphin's "set a games directory" was pointed at - # before) would, searched with its subfolders, have Dolphin open every PS2 and PS3 image too; - # the system folders below cover it. if any((p or "").rstrip("/") == ROMS for p in paths): paths = [p for p in paths if p and p.rstrip("/") != ROMS] for i, p in enumerate(paths): @@ -680,7 +618,6 @@ def dolphin(): changed.append("subfolders") if changed: print("droiddeck-pad-defaults: Dolphin: games from " + ", ".join(changed)) - # Once, for a Dolphin set up before this: Vulkan unless it was chosen already, and the NKit box. marker = os.path.join(d, ".droiddeck-setup") if os.path.exists(marker): return @@ -694,10 +631,6 @@ def dolphin(): print("droiddeck-pad-defaults: Dolphin: Vulkan, no NKit warning") -# Cemu: an SDLController profile for the GamePad. The uuid is "<index>_<SDL GUID>" and SDL's GUID -# carries a CRC of the device name; this is the one SDL reports for the session's pad -# ("Xbox 360 Controller (0)", 045e:028e - read from RPCS3's SDL log on the device). Mapping ids -# from VPADController.h, values from Controller.h, Cemu's own SDL default layout plus Home. CEMU_MAP = [(1, 1), (2, 0), (3, 3), (4, 2), (5, 9), (6, 10), (7, 42), (8, 43), (9, 6), (10, 4), (11, 11), (12, 12), (13, 13), (14, 14), (15, 7), (16, 8), (17, 45), (18, 39), (19, 44), (20, 38), (21, 47), (22, 41), (23, 46), (24, 40), (27, 5)] @@ -717,11 +650,6 @@ CEMU_XML = """<?xml version="1.0" encoding="UTF-8"?> """ -# Cemu's settings, so its Getting Started dialog (shown while settings.xml is missing, and whose -# folder picker cannot reach ROMs) never blocks a launch: the Wii U games folder, Vulkan -# (Graphic api 1), and audio through Cubeb (the only backend on Linux; api 3) on the default -# device for both the TV and the GamePad speaker - some titles play everything through the -# GamePad - at full volume (Cemu's own default is 20 %). Keys from config/CemuConfig.cpp. CEMU_SETTINGS = """<?xml version="1.0" encoding="UTF-8"?> <content> <mlc_path></mlc_path> @@ -749,11 +677,6 @@ def cemu(): done=f"games in {games}, Vulkan, Cubeb audio on the default device") -# melonDS 1.x: raw SDL joystick indices. The session's pad has the kernel's Xbox 360 layout -# (buttons A B X Y LB RB Back Start Guide LS RS = 0..10, hat 0 for the d-pad); the d-pad values -# also take the left stick (EmuInstanceInput.cpp; encoding InputConfig/MapButton.h). The guide -# button toggles full screen (HK_FullscreenToggle), as it does in Dolphin: the front end starts -# melonDS full screen (-f), and leaving it shows the window's menus for settings. MELONDS_GUIDE = 8 MELONDS_JOY = f"""[Instance0.Joystick] A = 1 @@ -771,28 +694,15 @@ Right = 65794 HK_FullscreenToggle = {MELONDS_GUIDE} """ -# The screen a handheld's wide panel suits best: the top screen as big as the panel's height allows -# with the bottom screen alone beside it, centred (ScreenLayout.h: Horizontal = 2, EmphTop = 1; -# melonDS's Hybrid shows the big screen a second time, small, above the bottom one; nobody needs -# it twice). Sharp pixels: no filtering, and no integer steps that leave the panel half used. Keys as -# melonDS writes them (Window.cpp windowCfg). MELONDS_WINDOW = """[Instance0.Window0] ScreenLayout = 2 ScreenSizing = 1 IntegerScaling = false ScreenFilter = false """ -# melonDS's own defaults for those, which an existing install is moved off once. Hybrid (3) was -# this preset's first layout, and moves on too. MELONDS_WINDOW_DEFAULTS = {"ScreenLayout": ("0", "3"), "ScreenSizing": ("0",), "ScreenFilter": ("true",)} -# The window melonDS returns to when the guide button leaves full screen: 1x wide (the width of -# two DS screens side by side, Screen.cpp screenGetMinSize) and 16:9, so gamescope stretches it to -# fill the panel and the menu bar and cursor come out large enough to use. A bigger window keeps -# its menus at their real size, tiny on a handheld. Qt's saveGeometry() v3 blob: magic, version, -# frame and normal rects (x, y, right, bottom), screen, maximized, full screen, screen width, and -# the normal rect again (QWidget::restoreGeometry). def melonds_geometry(): import base64, struct w, h = 512, 288 @@ -845,11 +755,6 @@ def toml_str(path): return '"' + path.replace("\\", "\\\\").replace('"', '\\"') + '"' -# The user's own BIOS, firmware and DSi NAND, found in the ROMs folder's nds/bios (or firmware) -# folder by what each file is: the DS BIOSes by size and checksum (the GBA BIOS is also 16 KB), -# firmware and NAND by size, the DSi boot ROMs (both 64 KB) by the 7 or 9 in their names. A path -# is only filled where melonDS has none, and external files are only switched on when a whole set -# was found - without them melonDS runs DS games on its built-in free BIOS. DS_BIOS7_MD5 = "df692a80a5b1bc90728bc3dfc76cd948" DS_BIOS9_MD5 = "a392174eb3e572fed6447e956bde4b25" @@ -924,7 +829,6 @@ def melonds(): lines = text.splitlines() done = [] if not any(isinstance(v, int) and v != -1 and k != "HK_FullscreenToggle" for k, v in joy.items()): - # Replace melonDS's own all-unbound table with ours (a TOML table can be defined only once). out, skipping = [], False for line in lines: t = line.strip() @@ -937,8 +841,6 @@ def melonds(): elif joy.get("HK_FullscreenToggle", -1) == -1 and MELONDS_GUIDE not in joy.values(): toml_set(lines, "Instance0.Joystick", "HK_FullscreenToggle", str(MELONDS_GUIDE)) done.append("guide button toggles full screen") - # Once, for a melonDS set up before this: the big top screen, where it is still on its own - # defaults. The marker keeps whatever the user picks in melonDS's View menu afterwards. if not os.path.exists(marker): for l in MELONDS_WINDOW.splitlines()[1:]: k, v = (x.strip() for x in l.split("=", 1)) @@ -957,9 +859,6 @@ def melonds(): print(f"droiddeck-pad-defaults: melonDS: {'; '.join(done)} ({path})") -# PPSSPP (SDL): its pad defaults leave the shoulders, the right stick and the triggers unbound on -# SDL builds (SDLJoystick.cpp codes vs KeyMapDefaults.cpp); a [ControlMapping] that exists replaces -# every default, so this one is complete, keyboard included (device 1 keyboard, 10 pad). PPSSPP_CONTROLS = """[ControlMapping] Up = 1-19,10-19 Down = 1-20,10-20 @@ -989,8 +888,6 @@ def ppsspp(): seed(os.path.join(CONFIG, "ppsspp/PSP/SYSTEM/controls.ini"), PPSSPP_CONTROLS, "PPSSPP") -# RetroArch: the udev joypad driver finds nothing here (no udev database in the runtime); sdl2 -# does, and matches this profile by vendor/product (task_autodetect.c). RETROARCH_PROFILE = """input_driver = "sdl2" input_device = "Xbox 360 Controller" input_vendor_id = "1118" @@ -1044,11 +941,6 @@ def retroarch(): print(f"droiddeck-pad-defaults: RetroArch: joypad driver udev -> sdl2 ({cfg})") -# yuzu and its forks: Player 1 in [Controls] of <name>/qt-config.ini, which each writes at its first -# start with the keyboard bound. Bindings are input_common ParamPackages, laid out by position as the -# frontend's own auto-map does (sdl_driver.cpp GetDefaultButtonBinding); the guid is SDL's with the -# name CRC cleared (GetGUID). A key is only read while its \default is false. Done once per emulator, -# and only while Player 1 is still on the keyboard. SWITCH_PAD = "engine:sdl,port:0,guid:030000005e0400008e02000010010000," SWITCH_PLAYER1 = { "button_a": "button:1", "button_b": "button:0", "button_x": "button:3", "button_y": "button:2", diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra b/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra index 68997cc5e..8625e5555 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra @@ -1,24 +1,4 @@ #!/usr/bin/python3 -# Installs a third-party Proton build into the client's compatibilitytools.d, so it appears in the -# Compatibility dropdown beside our own Proton ARM64 and can be picked per game. -# -# droiddeck-proton-extra <steam root> ge [--tag GE-Proton11-7] -# droiddeck-proton-extra <steam root> cachyos [--tag cachyos-11.0-20260703-slr] -# droiddeck-proton-extra <steam root> droiddeck [--tag DroidDeck-Proton-11-7.5] -# droiddeck-proton-extra <steam root> <url of an aarch64 tarball> -# droiddeck-proton-extra <steam root> /path/to/an/aarch64/tarball -# droiddeck-proton-extra <steam root> --list what is installed now -# -# All three projects publish native ARM64 builds - GloriousEggroll and DroidDeck-Proton as -# "*-aarch64.tar.gz", CachyOS as "*-arm64.tar.xz" - and all are ordinary Proton trees: a python -# entry point beside a files/ directory holding Wine, DXVK and VKD3D. That is the same shape as the -# depot the Steam client downloads, so the runtime can run them the same way. -# -# What it cannot use is their toolmanifest. Like Valve's ARM64 depot they declare -# require_tool_appid, which makes Steam stack pressure-vessel underneath - and pressure-vessel -# needs unprivileged user namespaces, which an Android app does not get. Patching that out is -# steam-compatibility's job (adopt_extras); this script only puts the tree in place and then -# calls it, so an install is usable without restarting the client. import hashlib import json import os @@ -30,17 +10,12 @@ import urllib.error import urllib.request RELEASES = "https://api.github.com/repos/%s/releases" -# Each source: the GitHub repo, and how its ARM64 asset is named. Ordered newest-release-first by -# the API, so "latest usable" is the first full release that actually carries an ARM64 asset - the -# projects do not build one for every tag. SOURCES = { "ge": ("GloriousEggroll/proton-ge-custom", re.compile(r"aarch64\.tar\.(gz|xz)$")), "cachyos": ("CachyOS/proton-cachyos", re.compile(r"arm64\.tar\.(gz|xz)$")), "droiddeck": ("The412Banner/DroidDeck-Proton", re.compile(r"aarch64\.tar\.(gz|xz)$")), } USER_AGENT = "droiddeck-proton-extra" -# Room for the tarball and the tree it unpacks to. GE-Proton's aarch64 build is ~650 MB packed and -# unpacks to about three times that; refusing early beats a half-written tool directory. NEED_BYTES = 4 * 1024 * 1024 * 1024 @@ -121,8 +96,6 @@ def extract(tarball, into): rc = subprocess.call(["tar", "-xf", tarball, "-C", into]) if rc != 0: raise SystemExit("droiddeck-proton-extra: tar failed (%d)" % rc) - # A Proton tarball holds one top-level directory. Take whichever entry has the entry point so a - # stray ._ file or a nested layout does not decide it. entries = [e for e in sorted(os.listdir(into)) if not e.startswith(".")] for entry in entries: path = os.path.join(into, entry) @@ -162,9 +135,6 @@ def main(): argv.pop(0) tag = argv.pop(0) if argv else None - # A tarball already on the device is used as it is: a session that has to pull 650 MB down - # first shows nothing on screen for ten minutes, so the download can be done ahead of time and - # handed over by path. local = None if what in SOURCES: tag, url, sums = release_asset(what, tag) if tag else pick_asset(what) @@ -200,8 +170,6 @@ def main(): tree = extract(tarball, unpacked) name = os.path.basename(tree) final = os.path.join(tools, name) - # Replaced rather than merged: a half-written tree from an interrupted run would otherwise - # leave stale files behind that Proton would load. if os.path.exists(final): shutil.rmtree(final, ignore_errors=True) os.rename(tree, final) @@ -211,8 +179,6 @@ def main(): shutil.rmtree(staging, ignore_errors=True) log("installed %s" % name) - # Patches out require_tool_appid and wraps the entry point, which is what makes it launchable - # here. Doing it now means the client picks the tool up on its next restart with no extra step. subprocess.call(["steam-compatibility", steam_root]) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-script-run b/tools/linuxfs/overlay/usr/local/bin/droiddeck-script-run index c4dd30c57..55606d8c5 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-script-run +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-script-run @@ -1,18 +1,14 @@ #!/bin/bash -# A script added on the Desktop page (UserApps), run from its own folder: -# droiddeck-script-run /opt/droiddeck-apps/<id> [args...] set -u dir=${1:?usage: droiddeck-script-run <added app directory> [args...]} shift entry=$(cat "$dir/entry" 2>/dev/null) || { echo "droiddeck-script-run: $dir has no entry" >&2; exit 127; } cd -P "$dir/files" 2>/dev/null || { echo "droiddeck-script-run: $dir/files is missing; add the script again" >&2; exit 127; } folder=$PWD -# Shared storage is noexec, so the script is handed to its interpreter rather than executed. first= IFS= read -r first < "$entry" first=${first%$'\r'} interp=(/bin/bash) -# A program as the entry (the desktop's copy of a game's folder) runs itself. case $first in '#!'*) read -r -a interp <<< "${first#'#!'}" ;; $'\x7f'ELF*) interp=() ;; esac fex=$(cat "$dir/fex" 2>/dev/null) case $fex in on|off) ;; *) fex=auto ;; esac @@ -31,7 +27,6 @@ case ${DBUS_SESSION_BUS_ADDRESS:-} in fi ;; esac rc=$? -# A launcher script often starts the game and returns; the session lasts while it runs. running() { local p exe arg cwd for p in /proc/[0-9]*; do diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists b/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists index 053df0e93..e6a8ecb4d 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists @@ -1,40 +1,11 @@ #!/bin/bash -# Marks Steam's shared redistributables as already run in every Proton prefix, and in the -# template Proton makes new prefixes from. -# -# A game whose depot lists _CommonRedist makes the client run the real installer inside the -# prefix on first launch. Those are x86 bundles, so they run under FEX, and the 2019 one leaves -# a task behind every time: its main thread exits while a helper stays blocked reading a pipe it -# also holds the write end of, so the process never reaps and the client keeps waiting on it. -# -# The client skips a "Run Process" block whose HasRunKey is already set, which is the same escape -# WineUtils.seedVcRedistRegistryIfDllsPresent() takes for the app's own containers. The names and -# versions come out of the install scripts rather than a list kept here, because Valve revises -# them; a prefix already carrying the same set is left alone. -# -# Games also carry install scripts of their own (<game>/_CommonRedist/vcredist/2022/installscript.vdf -# and the like), often with another version in the value name; those are read too. And the VC++ -# 2015-2022 runtime is recorded as installed the way Microsoft's installers look for it: on ARM64 -# Proton the x64 bundle otherwise stops with "Microsoft Visual C++ 2015-2022 Redistributable (arm64) -# is required", while Proton's own msvcp140/vcruntime140 already serve the game. -# -# A Vulkan runtime installer is the one left to run. Proton has no stand-in for it: for a game whose -# Steam compat config says nativevulkanloader (Red Dead Redemption 2) it loads vulkan-1.dll native -# only, and Khronos' loader is in the prefix only once that installer has run - without it the game -# exits with STATUS_DLL_NOT_FOUND before it opens a window. The installer finishes in a second. set -u steam_root=${1:?steam root} LIBRARIES=${DROIDDECK_LIBRARIES-/mnt/droiddeck /mnt/droiddeck-sd} redist="$steam_root/steamapps/common/Steamworks Shared/_CommonRedist" -# Every process an install script would run, as the registry key and value the client writes once -# it has run it. Names sit one level inside "Run Process", the key they share a level further in. -# "read_scripts vulkan" lists only the Vulkan runtime installers, which the plain call leaves out. read_scripts() { xargs -0 -r awk -v want="${1:-redists}" ' - # A block whose HasRunKey is set is one process; its name is the key the block opened under, at - # whatever depth - an install script nests it under "Run Process", the client'"'"'s evaluator script - # for a game wraps the same blocks once more. FNR == 1 { depth = 0; pending = "" } /^[[:space:]]*\{/ { depth++; name[depth] = pending; key[depth] = ""; vulkan[depth] = 0; pending = ""; next } /^[[:space:]]*\}/ { @@ -56,8 +27,6 @@ read_scripts() { xargs -0 -r awk -v want="${1:-redists}" ' } '; } pairs=$(find "$redist" -name installscript.vdf -print0 2>/dev/null | sort -z | read_scripts) -# The client downloads the scripts with the first game that wants them, which is too late for that -# game; until then the set they held when this was written stands in. (WinNative's list.) [ -n "$pairs" ] || pairs=$(sed 's/^\([^ ]*\) /HKEY_LOCAL_MACHINE\\\\Software\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\\1\t/' <<'KNOWN' DirectX\\Jun2010 dxsetup .NET\\4.8 4.8 @@ -78,8 +47,6 @@ vcredist\\2022 x64 14.51.36247.0 KNOWN ) -# The games' own scripts. Finding them walks every game folder, so the list is kept until a game is -# installed, updated or removed (its appmanifest changes) - this runs every quarter minute. cache="$steam_root/.droiddeck-game-scripts" libraries="$steam_root" for library in $LIBRARIES; do libraries="$libraries @@ -96,21 +63,14 @@ if [ "$(head -n 1 "$cache" 2>/dev/null)" != "$sig" ]; then done <<<"$libraries" | sort -u } >"$cache.tmp" && mv -f "$cache.tmp" "$cache" fi -# DROIDDECK_SEED_SCRIPTS: more scripts, one path a line - droiddeck-game-env passes the client's -# evaluator script for the game about to run its installers, read before the client looks. game_scripts() { { tail -n +2 "$cache" 2>/dev/null; printf '%s\n' "${DROIDDECK_SEED_SCRIPTS:-}"; } | sed '/^$/d' | tr '\n' '\0'; } game_pairs=$(game_scripts | read_scripts) [ -n "$(printf '%s' "$game_pairs" | tr -d '[:space:]')" ] && pairs=$(printf '%s\n%s\n' "$pairs" "$game_pairs" | sort -u | sed '/^$/d') -# A Vulkan runtime installer marked as run by an earlier seeding still has its marker: those come out -# again below, so the client runs the installer on the game's next launch. declare -A vulkan=() while IFS= read -r line; do [[ $line == *$'\t'* ]] && vulkan[$line]=1 done < <(game_scripts | read_scripts vulkan) -# The VC++ 2015-2022 runtime as its installers check for it, for each architecture and both registry -# views. The version is the newest the scripts above know, so any 2015-2022 bundle finds itself -# already installed and exits without asking for anything. VCRT_MAJOR=14 VCRT_MINOR=51 VCRT_BUILD=36247 vcrt() { local view arch @@ -124,9 +84,6 @@ vcrt() { done } -# Each prefix's marker lists the names it was given, so it is handed only what it lacks. One stamp -# for the whole set flipped between the session's set and the evaluator's (which adds the game's -# own names) on every launch, and every prefix was appended to again, inside the launch. want=() declare -A wanted=() while IFS= read -r line; do @@ -142,9 +99,6 @@ block_for() { printf '%s\n' "$@" | awk -F'\t' -v now="$now" ' if (path == $1) next printf "\n[%s] %d\n\"%s\"=dword:00000001\n", path, now, $2 - # The client writes these from 32-bit steam.exe, so its own copy lands under Wow6432Node; - # both spellings are seeded because the reader picks by its own word size. - # Joined rather than sub()stituted: gawk and mawk read backslashes in a replacement differently. if (substr(path, 1, 10) != "Software\\\\") next printf "\n[%s] %d\n\"%s\"=dword:00000001\n", "Software\\\\Wow6432Node\\\\" substr(path, 11), now, $2 @@ -154,7 +108,6 @@ block_for() { printf '%s\n' "$@" | awk -F'\t' -v now="$now" ' for line; do [[ $line == vcrt$'\t'* ]] && vcrt "$now"; done } -# Takes the given markers out of a registry file again, in both views, keeping its date. unseed() { local reg=$1 out shift @@ -186,9 +139,6 @@ unseed() { return 1 } -# Proton builds a new prefix from its template, and the client runs the install scripts in that -# prefix moments later, so the template carries the markers too. Its date is kept: Proton reads a -# changed one as a new template and upgrades every prefix made from the old. for reg in "$steam_root"/steamapps/common/Proton*/files/share/default_pfx*/system.reg \ /mnt/droiddeck-sd/steamapps/common/Proton*/files/share/default_pfx*/system.reg \ /root/Games/*/steamapps/common/Proton*/files/share/default_pfx*/system.reg \ diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index 6eb0059e8..f915472ed 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -1,27 +1,14 @@ #!/bin/bash -# The Linux runtime's session. DroidDeck execs this under proot with the output size and mode; -# the first pass starts gamescope, which execs this script again inside it for the program: -# droiddeck-session desktop the file manager -# droiddeck-session run <path> a Linux program (AppImage, script or ELF) -# droiddeck-session steam [steam-url] the native Steam client -# droiddeck-session lxqt the KDE Plasma desktop (DroidDeck's desktop package) -# BL_WIDTH / BL_HEIGHT size the output, BL_FPS limits it, BL_LOG collects everything said here. set -u export HOME=${HOME:-/root} mode=${1:-desktop} shift || true -# One line per milestone, greppable as "== STEP", so a first run reads as a narrative rather than -# something to be reconstructed from Steam's own chatter. The app shows these while it waits. step() { echo "== STEP $(date -u +%H:%M:%S) $*"; } if [ -n "${BL_LOG:-}" ] && [ -z "${BL_INSIDE:-}" ]; then mkdir -p "$(dirname "$BL_LOG")" 2>/dev/null - # The client alone writes megabytes an hour; one earlier log is kept, not every session's. if [ "$(stat -c %s "$BL_LOG" 2>/dev/null || echo 0)" -gt 8388608 ]; then mv -f "$BL_LOG" "$BL_LOG.1" fi - # A guest looping on an error writes gigabytes in minutes (an Xwayland proot had lost track of - # managed 9 GB of one line in 19 min): the log keeps its first BL_LOG_CAP bytes and the rest is - # drained, not refused - a writer that filled it must not die of SIGPIPE. exec > >({ dd of="$BL_LOG" oflag=append conv=notrunc iflag=count_bytes count="${BL_LOG_CAP:-268435456}" bs=65536 status=none; cat >/dev/null; }) 2>&1 echo "== droiddeck-session $(date) mode=$mode $*" echo "== uid=$(id -u) size=${BL_WIDTH:-?}x${BL_HEIGHT:-?} fps=${BL_FPS:-0} icd=${VK_ICD_FILENAMES:-none}" @@ -29,11 +16,6 @@ if [ -n "${BL_LOG:-}" ] && [ -z "${BL_INSIDE:-}" ]; then echo "== rootfs: $(cat /.version 2>/dev/null || echo unversioned)" fi -# Every program loads the locale LANG names (C.UTF-8) before main(). glibc ships it as a directory, -# which it looks for once per category under two spellings before reading gconv's module lists: -# about 30 opens, each a stop in proot, for every process in the session, a script's dirname or cat -# included. An archive holding it (and gconv's cache) makes that one open. Built again when the -# glibc package changes, or when the archive does (locale-gen rewrites it). prepare_locale() { local dir=/usr/lib/locale stamp=/usr/lib/locale/.droiddeck-archive want have= pkg pkg=(/var/lib/pacman/local/glibc-[0-9]*) @@ -41,7 +23,6 @@ prepare_locale() { [ -f "$stamp" ] && read -r have < "$stamp" [ "$have" = "$want" ] && [ -f "$dir/locale-archive" ] && [ ! "$dir/locale-archive" -nt "$stamp" ] && return 0 [ -d "$dir/C.utf8" ] && command -v localedef >/dev/null 2>&1 || return 0 - # localedef names a new archive by hard link, which Android refuses; libblsession renames it. DROIDDECK_LINK_RENAME=1 localedef --add-to-archive --replace "$dir/C.utf8" >/dev/null 2>&1 || return 0 rm -f /usr/lib/gconv/gconv-modules.cache command -v iconvconfig >/dev/null 2>&1 && iconvconfig >/dev/null 2>&1 @@ -50,12 +31,6 @@ prepare_locale() { } [ -z "${BL_INSIDE:-}" ] && prepare_locale -# What earlier sessions left in /tmp. The guest's /tmp is a directory in the rootfs, never emptied -# by a boot, and Android ends sessions by killing them: each left its mangoapp config, the client's -# temporary directories and shared-memory sockets, Chromium's singleton directory (the web -# helper's, an Electron app's), a session bus socket per run and a run's start stamp - 1,400 -# entries on a device after a few weeks. Each kind is swept while nothing that could own one runs, -# with one find for the lot. clear_stale_tmp() { local running names=() n running=$(pgrep -af 'steamwebhelper|steamrtarm64/steam|--type=|droiddeck-appimage-run|dbus-run-session|dbus-daemon.*--session|dbus-launch|kwin|plasmashell' 2>/dev/null) @@ -81,22 +56,13 @@ clear_stale_tmp() { } [ -z "${BL_INSIDE:-}" ] && clear_stale_tmp -# Until the runtime is stable, Steam's own logs are copied beside the session log at exit so a -# failed launch can be handed over as one folder. collect_steam_logs() { [ -n "${BL_DEBUG_DIR:-}" ] || return 0 local steam_root=${XDG_DATA_HOME:-$HOME/.local/share}/Steam mkdir -p "$BL_DEBUG_DIR/steam" 2>/dev/null || return 0 - # Into steam/ beside this session's own logs. The app scrubs these line by line after the - # session ends - they carry the account's session token and id - so nothing here is the last - # word on what the user ends up sharing; this is just the copy that survives the runtime. - # Up to 8 MB each, as the app's own copy takes them: the client's CEF and *.previous.txt logs - # reach 17 MB and say little about this session, and every share scrubs whatever is here. [ -d "$steam_root/logs" ] && find "$steam_root/logs" -maxdepth 1 -type f -size -8192k \ -exec cp {} "$BL_DEBUG_DIR/steam/" \; 2>/dev/null [ -f "$HOME/.steam/steam/logs/steamwebhelper.log" ] && cp "$HOME/.steam/steam/logs/steamwebhelper.log" "$BL_DEBUG_DIR/steam/" 2>/dev/null - # NOT copied, ever: loginusers.vdf, config.vdf and the ssfn* files, which hold the credentials - # themselves rather than a mention of them. local sync_store=$HOME/.local/share/droiddeck-esync sync_file if [ -d "$sync_store" ] && mkdir -p "$BL_DEBUG_DIR/droiddeck-esync" 2>/dev/null; then for sync_file in launches.log tools.tsv wanted.tsv prefixes.json; do @@ -109,20 +75,9 @@ collect_steam_logs() { fi echo "== steam logs copied to $BL_DEBUG_DIR/steam" } -# A session that is killed never reaches the copy below, and those are the sessions worth reading. trap 'collect_steam_logs 2>/dev/null || true' TERM INT HUP -# The driver everything in this session draws with: gamescope's compositing, the client's UI -# (OpenGL through Zink), every game Proton launches, and in desktop mode every program on it. -# BL_VK_DRIVER names an imported driver's ICD manifest on the Android side, which is bound into the -# session at its own path; unset means the runtime keeps the Turnip it was built with. -# VK_DRIVER_FILES REPLACES the loader's search, so a manifest whose library is unreadable would -# leave the session with no Vulkan at all - check both before setting it, and say so in the log -# rather than fail silently half a minute later. if [ -n "${BL_VK_DRIVER:-}" ]; then bl_icd_lib=$(sed -n 's/.*"library_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$BL_VK_DRIVER" 2>/dev/null | head -1) - # JSON may escape the separator: org.json writes "\/data\/user\/0\/...". The Vulkan loader - # parses that correctly; this check has to as well, or an absolute path looks relative and gets - # glued onto the manifest's directory - which is exactly what happened the first time. bl_icd_lib=$(printf '%s' "$bl_icd_lib" | sed 's|\\/|/|g') case $bl_icd_lib in /*) ;; @@ -130,7 +85,6 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then esac if [ -r "$BL_VK_DRIVER" ] && [ -n "$bl_icd_lib" ] && [ -r "$bl_icd_lib" ]; then export VK_DRIVER_FILES="$BL_VK_DRIVER" - # Loaders older than 1.3.207 only know the old name; both are harmless together. export VK_ICD_FILENAMES="$BL_VK_DRIVER" echo "== vulkan driver: imported $bl_icd_lib (via $BL_VK_DRIVER)" else @@ -138,20 +92,8 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then fi unset bl_icd_lib fi -# The app's gamescope build (tools/gamescope) asks for realtime-priority Vulkan queues on this -# switch instead of on CAP_SYS_NICE, which proot never has; it falls back to normal priority -# where the driver refuses, and the runtime's own build ignores it. On KGSL Turnip (Mesa 26.2) it -# changes nothing yet: the driver reports one submit-queue priority and creates every KGSL context -# with the same flags, so no priority reaches the GPU. Kept for a driver that maps it, and off -# unless the app's Performance page turns it on (BL_GAMESCOPE_REALTIME=1). export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0} -# A sleep that starts no process. Under proot every exec is a traced execve plus the loader's -# opens and maps, and the watchers below poll for the whole session: `sleep 1` in a loop was 60 -# execs a minute each. read -t on a pipe nothing ever writes to waits in bash itself. -# The agent bridge's guest side (docs/agent-control.md): runs what the app's agent provider asks -# for inside this session and publishes gamescope's focus. Its files live under the session root -# ($BL_LAUNCH_DIR/agent), which the app empties before each session. start_agent() { [ -n "${BL_LAUNCH_DIR:-}" ] && [ -x /usr/local/bin/droiddeck-agent ] || return 0 mkdir -p "$BL_LAUNCH_DIR/agent" @@ -165,23 +107,13 @@ export XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share} export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache} mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" -# No SteamVR runs at session start. A marker left by a session that died with SteamVR up would put -# the Cardboard VR layer's display stand-in into this one (docs/cardboard-vr.md). rm -f "${XDG_RUNTIME_DIR:-/run/droiddeck}/droiddeck-steamvr/active" if [ "$mode" = lxqt ] || [ "$mode" = desktop ]; then /usr/local/bin/droiddeck-desktop-bookmarks || true fi -# Flatpak (the store's apps): its sandboxes through the app instead of bubblewrap, which needs the -# user namespaces Android keeps from apps, and its apps' menu entries and icons on the desktop. export FLATPAK_BWRAP=/usr/local/bin/droiddeck-bwrap export XDG_DATA_DIRS=$XDG_DATA_HOME/flatpak/exports/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share} -# The cores a program runs on outside Steam (the desktop and everything started from it, a -# program from the rail): the user's game cores when set (BL_GAME_CPUS), else every core but the -# slowest cluster - Android is free to park an emulator's threads on the little cores, and those -# are what compile shaders and recompile PPU/SPU code. BL_PIN_CORES=0 leaves the scheduler alone. -# A prime-only client mask can be reset without a thread-count change (Steam or Android). -# Read status with shell builtins; only invoke taskset when a single-core mask has drifted. client_single_core_matches() { local wanted=$1 task key cpus shift @@ -216,9 +148,7 @@ program_cores() { } if [ -z "${BL_INSIDE:-}" ]; then - # Install the shared default-family aliases before Steam, gamescope or the desktop queries fonts. /usr/local/bin/droiddeck-fonts 2>&1 | sed 's/^/[fonts] /' - # Android's directory binding and language-specific family matches handle CJK and Thai fonts. device_fonts=/usr/local/share/fonts/android if [ -d "$device_fonts" ] && [ -d /etc/fonts/conf.d ] && command -v fc-cache >/dev/null 2>&1; then fonts_rules=$( @@ -239,14 +169,10 @@ if [ -z "${BL_INSIDE:-}" ]; then echo "== fontconfig: indexed $(ls "$device_fonts" | wc -l) device fonts, zh falls back to $(fc-match -f '%{family[0]}' ':lang=zh-cn' 2>/dev/null)" fi fi - # The compositor may still be starting, and a socket left by an earlier session is not - # a compositor: probe until a connection succeeds. for _ in $(seq 1 100); do wayland-info >/dev/null 2>&1 && break sleep 0.1 done - # The desktop (DroidDeck's hosted package) is its own nested compositor, KWin, drawing straight - # into the app's: no gamescope in between. if [ "$mode" = lxqt ]; then step "starting the desktop" start_agent --no-focus @@ -254,9 +180,6 @@ if [ -z "${BL_INSIDE:-}" ]; then cores=$(program_cores) if [ -n "$cores" ]; then echo "== desktop cores: $cores (the desktop and every program started from it)" - # Pin this shell and let exec carry the mask. Android keeps the app on a smaller cpuset - # until its activity is on top (foreground 0-5, background fewer), and a mask with no core - # of that set is EINVAL: taskset -c then exits 1 and takes the whole session with it. taskset -cp "$cores" $$ >/dev/null 2>&1 || echo "== desktop cores: not allowed right now, every core" fi exec /usr/local/bin/droiddeck-desktop @@ -264,9 +187,6 @@ if [ -z "${BL_INSIDE:-}" ]; then width=${BL_WIDTH:-1280} height=${BL_HEIGHT:-720} nested=$width - # Follow screen (BL_FOLLOW_OUTPUT=1): the app resizes gamescope's output when a foldable opens or - # closes, and with Steam on its own Xwayland gamescope resizes Steam's X screen with it, as on a - # Steam Deck docking. A stretched 16:9 nested size would pin it, so the two are exclusive. if [ "${BL_FOLLOW_OUTPUT:-0}" = 1 ]; then echo "== gamescope: the output follows the screen (${width}x${height} to start)" elif [ "$mode" = steam ] && [ "${BL_GAMESCOPE_STRETCH_16X9:-0}" = 1 ]; then @@ -278,51 +198,21 @@ if [ -z "${BL_INSIDE:-}" ]; then fi args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$nested" -h "$height") [ "$nested" != "$width" ] && args+=(-S stretch) && export BL_STRETCHED=1 - # -r is what gamescope advertises as the refresh rate, and what a game reads as the display's. - # Left unset it defaults to 60, so a 120 Hz panel offers only 60 Hz in game settings. The FPS - # limiter wins when it is on (it is a deliberate cap); otherwise use the panel's own rate. if [ "${BL_FPS:-0}" -gt 0 ]; then args+=(-r "$BL_FPS") elif [ "${BL_REFRESH:-0}" -gt 0 ]; then args+=(-r "$BL_REFRESH") fi - # Steam integration advertises a Deck session and makes the client select Big Picture even - # without -gamepadui. The desktop UI uses ordinary gamescope window management instead. [ "$mode" = steam ] && [ "${BL_STEAM_UI:-}" != desktop ] && args+=(-e) - # Two Xwayland servers, as SteamOS runs Steam: the client on the first, games on the second - # (STEAM_GAME_DISPLAY_0). The client then sizes the games' server to each game's Game Resolution - # (GAMESCOPE_XWAYLAND_MODE_CONTROL; native unless one is set), and gamescope scales it to the - # output. With one server the client and its games share the output's size, so a game's own Game - # Resolution never reaches its display (docs/development/steam-display-scaling.md). The client's - # overlay is also on another server than the game, so opening it no longer takes the game's X - # keyboard focus, which made games like FlatOut shrink their window. - # BL_XWAYLAND_COUNT=1 in Download/droiddeck-env goes back to one. if [ "$mode" = steam ] && [ "${BL_XWAYLAND_COUNT:-2}" -gt 1 ]; then args+=(--xwayland-count "${BL_XWAYLAND_COUNT:-2}") export STEAM_MULTIPLE_XWAYLANDS=1 fi - # Touch passes through to the client as touch until the client says otherwise, as SteamOS's - # gamescope-session starts it (TOUCH_MODE=4). The client sets the mode itself (STEAM_TOUCH_CLICK_MODE) - # only once it has a controller; until then gamescope's own default made a finger a left-click - # mouse, so the Steam interface took taps but would not scroll on a phone with no pad attached. [ "$mode" = steam ] && args+=(--default-touch-mode 4) - # No --force-windows-fullscreen: it was for games that shrink their window when the Steam menu - # opens over them (FlatOut), which no longer happens with the games on their own Xwayland server - # (above), and it made any game drawing at another size than its display fight it. An experiment - # can still pass it with BL_GAMESCOPE_ARGS. - # HDR10: the app opened its compositor's colour-management gate for this session (BL_HDR=1); - # gamescope then offers HDR to its clients through the same wp_color_manager_v1 it binds on ours. [ "${BL_HDR:-0}" = 1 ] && args+=(--hdr-enabled) - # Extra gamescope arguments for an experiment (--debug-focus, say), from Download/droiddeck-env. [ -n "${BL_GAMESCOPE_ARGS:-}" ] && read -r -a _bl_extra <<<"$BL_GAMESCOPE_ARGS" && args+=("${_bl_extra[@]}") - # The performance overlay (Quick Access Menu → Performance → Overlay Level) is drawn by - # mangoapp, which gamescope starts beside the client; the client sets the level by rewriting - # MANGOHUD_CONFIGFILE. The apk stages Valve's aarch64 build (tools/mangoapp) - without it the menu - # still offers the levels and nothing appears. Starts hidden until the client writes a level. - # BL_MANGOAPP=0 turns it off (one community build found Valve's mangoapp crashing on Turnip). if [ "$mode" = steam ] && [ "${BL_MANGOAPP:-1}" = 1 ] \ && [ "${BL_STEAM_UI:-}" != desktop ] && command -v mangoapp >/dev/null 2>&1; then - # One fixed file, rewritten each session: a mktemp name per session was never removed. MANGOHUD_CONFIGFILE=/tmp/droiddeck-mangohud.conf echo no_display > "$MANGOHUD_CONFIGFILE" export MANGOHUD_CONFIGFILE STEAM_USE_MANGOAPP=1 STEAM_MANGOAPP_PRESETS_SUPPORTED=1 STEAM_MANGOAPP_HORIZONTAL_SUPPORTED=1 @@ -333,10 +223,6 @@ if [ -z "${BL_INSIDE:-}" ]; then exec gamescope "${args[@]}" -- "$0" "$mode" "$@" fi -# gamescope manages X windows: toolkits go through Xwayland, not gamescope's own Wayland socket. -# Root-window requests for the game (restore) go to every display gamescope gave us: -# the client's ($DISPLAY) and, with two servers, the games' (STEAM_GAME_DISPLAY_0); gamescope reads -# them per server. bl_displays() { printf '%s\n' "$DISPLAY" [ -n "${STEAM_GAME_DISPLAY_0:-}" ] && [ "$STEAM_GAME_DISPLAY_0" != "$DISPLAY" ] && printf '%s\n' "$STEAM_GAME_DISPLAY_0" @@ -350,10 +236,6 @@ bl_xprop_set_all() { [ "$ok" = 1 ] } export GDK_BACKEND=x11 QT_QPA_PLATFORM=xcb SDL_VIDEODRIVER=x11 -# gamescope's reaper waits for every process left under it before the compositor comes down. A -# process a game left behind - a Wine service blocked on a server that is already gone - is -# reparented there when the client exits, and the screen stays black on it for good. Nothing of -# the session outlives the program it was started for. reap_orphans() { local round p stat pid left for round in 1 2 3 4 5; do @@ -373,13 +255,10 @@ reap_orphans() { done } -# Gamescope's Wayland backend does not forward the outer clipboard into Xwayland. -# This helper owns only Android text selections and exits with this guest session. if [ "${BL_DESKTOP:-}" != 1 ] && [ -n "${BL_ANDROID_CLIPBOARD:-}" ] && [ -x /usr/local/bin/droiddeck-clipboard ]; then /usr/local/bin/droiddeck-clipboard "$BL_ANDROID_CLIPBOARD" & fi -# KDE already has a session agent; starting another would race its request files. [ "${BL_DESKTOP:-}" = 1 ] || start_agent case $mode in @@ -394,7 +273,6 @@ case $mode in echo "== program cores: $cores" taskset -cp "$cores" $$ >/dev/null 2>&1 || true fi - # Player 1 on the controller in an emulator that has no profile of its own yet. python3 /usr/local/bin/droiddeck-pad-defaults 2>&1 || true fex=() if [ -x /usr/local/bin/droiddeck-fex ]; then @@ -425,13 +303,6 @@ case $mode in droiddeck-steam-install || exit $? step "Steam client ready" steam_root=$XDG_DATA_HOME/Steam - # Register the card library (bound at /mnt/droiddeck-sd) with the client - internal storage is - # its main library already - and the ARM64 Proton that lets it launch the Windows ones. - # A request left in ~/.bl-proton-extra installs a third-party Proton before the client starts, - # so it is in the Compatibility dropdown the first time the user looks. One per line, each the - # arguments to droiddeck-proton-extra: "ge", "cachyos", "droiddeck", "ge --tag GE-Proton11-7", - # or a URL. Hundreds of megabytes each, so it is only ever done on request, and the line is - # dropped once it has succeeded - a failed download stays and is retried next session. if [ -s "$HOME/.bl-proton-extra" ]; then _bl_left=$HOME/.bl-proton-extra.left : > "$_bl_left" @@ -447,11 +318,6 @@ case $mode in done < "$HOME/.bl-proton-extra" mv -f "$_bl_left" "$HOME/.bl-proton-extra" fi - # steam:// URLs left in ~/.bl-steam-urls are handed to the client on its command line. The app - # writes one when the user asks for a Proton that is one of Valve's own depots rather than a - # tarball: those are not files anywhere, the client fetches them like any other app. One per - # line, and the file is cleared once they have been passed on - the client either acts on a URL - # or it does not, and repeating it every session would re-ask forever. if [ -s "$HOME/.bl-steam-urls" ]; then while read -r _bl_url || [ -n "$_bl_url" ]; do case ${_bl_url# } in steam://*) ;; *) continue ;; esac @@ -460,15 +326,6 @@ case $mode in done < "$HOME/.bl-steam-urls" rm -f "$HOME/.bl-steam-urls" fi - # Cores for the client itself. Steam pins its own interface renderer to a subset - on the - # device it took 5 of 8, leaving out both little cores and the fastest one - which is a - # reasonable thing to do while a game is running and the wrong thing when the client is all - # there is. BL_CLIENT_CPUS overrides it. Re-applied on a beat because steamwebhelper spawns - # children continuously and each one inherits Steam's choice, not ours. The beat reads /proc - # itself instead of running pgrep, and runs taskset for new threads or a single-core mask - # that has drifted since it was last pinned: every fork is a round of traced syscalls, and a - # beat that forked a taskset per process every 5 s did that for an hour. taskset -a, because - # without it only a process's main thread moves and the renderer's workers stay where they were. if [ -n "${BL_CLIENT_CPUS:-}" ]; then step "client cores: $BL_CLIENT_CPUS" ( @@ -489,9 +346,6 @@ case $mode in done ) & fi - # zRAM compression (BL_ZRAM): 20 s into a game the client and its web helpers page their memory - # out to the device's zram (libblsession zram.c), again every 5 minutes, and read it back once - # the game has exited. A game is running while Steam's reaper is. if [ "${BL_ZRAM:-0}" = 1 ]; then printf 0 > /tmp/.bl-zram step "zRAM compression: on" @@ -543,9 +397,6 @@ case $mode in else rm -f /tmp/.bl-zram fi - # Rockstar's launcher keeps drawing its hidden web interface for as long as a game of theirs - # runs; droiddeck-launcher-cores holds it on one slow core while it is out of sight, from - # gamescope's focus as the agent publishes it. BL_LAUNCHER_CORES=0 leaves launchers alone. if [ "${BL_LAUNCHER_CORES:-1}" != 0 ] && [ -n "${BL_LAUNCH_DIR:-}" ] && [ -z "${BL_DESKTOP:-}" ] \ && [ "${BL_STEAM_UI:-}" != desktop ] && [ -x /usr/local/bin/droiddeck-launcher-cores ]; then /usr/local/bin/droiddeck-launcher-cores "$BL_LAUNCH_DIR/agent/focus.json" 2>&1 & @@ -558,32 +409,18 @@ case $mode in if [ -z "${BL_DESKTOP:-}" ] && [ "${BL_STEAM_UI:-}" != desktop ]; then droiddeck-steam-ui-scale "$steam_root" "${BL_WIDTH:-1280}" "${BL_HEIGHT:-720}" || true fi - # The user's own games, as non-Steam shortcuts under that tool; the list is written by the app - # each session (empty = the app's earlier entries are removed, the client's own are kept). if [ -n "${BL_ADDED_GAMES:-}" ]; then step "adding the app's own games to the client's library" droiddeck-steam-shortcuts "$steam_root" "$BL_ADDED_GAMES" || true fi - # Steam runs a game's shared redistributables inside its prefix on first launch; marking - # them as already run keeps those x86 installers from starting under emulation at all. droiddeck-seed-redists "$steam_root" || true - # The compatibility tool runs Valve's arm64 Proton, which the client fetches like any depot. - # Asked for on the command line the first time, so a Windows title has it before it is needed. if [ ! -f "$steam_root/steamapps/appmanifest_4427310.acf" ]; then step "asking the client to fetch the ARM64 Proton" set -- "$@" steam://install/4427310 - # Ported from Bannerlator's session script (device-proven 2026-09-22). The client decides - # a title's compatibility tool when the title is installed and keeps that decision for the - # rest of its run, and the tool is set up before the client comes up. So once the depot has - # fully landed, restart the client once - its own -shutdown, then the loop below, which knows - # the marker - and every title installed from then on runs through the tool, with the - # refreshed tool list and defaults, and no second launch by the user. A first run only: with - # the depot present at session start nothing here is armed. ( _bl_acf="$steam_root/steamapps/appmanifest_4427310.acf" for _ in $(seq 1 360); do sleep 10 - # StateFlags 4 = fully installed; a moment more for the client to finish its bookkeeping. if grep -q '"StateFlags"[[:space:]]*"4"' "$_bl_acf" 2>/dev/null; then sleep 10 steam-compatibility "$steam_root" >/dev/null 2>&1 || true @@ -594,17 +431,6 @@ case $mode in fi done ) & - # A client that starts signed out drops that request - on a fresh install nobody has signed - # in yet - so the install dialog only appeared after the user restarted the client by hand. - # With no remembered sign-in, ask again once the sign-in has happened: the client binary - # started a second time with a URL hands it to the running instance and exits. - # - # The request is also dropped while the client is still in its post-logon compat phase (it - # probes every registered tool then): with publicbeta 1790380355 that phase took 27 s and a - # request sent 15 s after sign-in vanished (2026-09-26, no dialog; 1790121765 was done in - # time). So wait for the client to say the phase is over ("Waiting for compat in post-logon - # took" in its console log; at most 90 s if it never does), then ask - and ask again, twice - # at most, if no install has started a minute later. if ! grep -q '"RememberPassword"[[:space:]]*"1"' "$steam_root/config/loginusers.vdf" 2>/dev/null; then ( _bl_acf="$steam_root/steamapps/appmanifest_4427310.acf" @@ -633,26 +459,6 @@ case $mode in ) & fi fi - # The tool is registered before the client starts and finds the depot at launch time, so - # nothing here waits for a download. What can still change under a running client is which - # titles are installed: a title installed this session gets the client's own per-app Proton - # choice, which it cannot start. Refresh the per-app entries every so often; the registrar is - # idempotent, so a repeat costs nothing. - # The seeder has the same problem and a worse consequence. It marks the shared redistributables - # as already run in each prefix that exists, and a game's prefix is not created until the - # client launches it - so a title played for the first time this session was never covered, and - # its install scripts run for real. That is exactly the hang the seeder exists to prevent: Need - # for Speed Payback sat on "Running install script" with vcredist_x86.exe blocked in pipe_read, - # holding both ends of a pipe nobody would write to, while the client waited for it to exit. - # Seeding again on the same beat covers a prefix the moment it appears. - # The seeder runs on a shorter beat than the registrar because it is racing something. The - # client creates a prefix when it launches a title and runs that title's install scripts - # moments later, so the seeder has only that gap to mark the redistributables as already run. - # It stamps each prefix it has covered and skips it thereafter, so the extra passes cost - # almost nothing. - # With the 16:9 stretch on, the client's own scaler must not undo it: in Deck mode it sets - # GAMESCOPE_NEW_SCALING_SCALER to Auto (0), which fits the 16:9 picture with bars and outranks -S. - # Auto is put back to Stretch (4); a mode picked in the QAM (Integer, Fit, Fill) is kept. [ -z "${BL_DESKTOP:-}" ] && [ "${BL_STRETCHED:-0}" = 1 ] && ( while :; do nap 1 @@ -663,13 +469,6 @@ case $mode in && echo "== stretch 16:9 to panel: the client's scaler was Auto, set to Stretch" done ) & - # Resume from the Steam menu. The menu is an overlay that takes input, and a fullscreen game that - # loses input minimizes itself; gamescope only takes a window out of that state when the focused - # window changes, which the menu never does, so the game stayed minimized after Resume (a black - # screen with its small caption in the top-left corner). gamescope-3.16.29-p4 restores it itself; - # this asks it again a moment after input comes back from the client (app 769) to anything else, - # which covers a game that minimizes late. Harmless when nothing is minimized. - # BL_RESTORE_ON_RESUME=0 turns it off. [ -z "${BL_DESKTOP:-}" ] && [ "${BL_RESTORE_ON_RESUME:-1}" = 1 ] && ( prev= xprop -spy -root GAMESCOPE_FOCUSED_APP 2>/dev/null | while read -r line; do @@ -683,13 +482,7 @@ case $mode in prev=$app done ) & - # Evidence for touch reports: the touch mode the client asks gamescope for (STEAM_TOUCH_CLICK_MODE, - # set once it has a controller, per focused app) and which app has focus, logged as they change. - # A finger that clicks but won't scroll, or touch that stops once a pad is used, shows here as - # the mode in force at that moment. 4 is touch passed through, 1 a left-click mouse. [ -z "${BL_DESKTOP:-}" ] && ( - # What follows "=" in an xprop line, or $2 for a property that is missing or has no value - # yet ("NAME(CARDINAL) =", which read leaves with nothing after the "="). xprop_value() { case $1 in *'not found'*|*=) echo "$2"; return ;; esac local v=${1#*=} @@ -714,13 +507,6 @@ case $mode in esac done ) & - # The library beat. Both helpers are whole programs under proot (the seeder some forty of them, - # the compat one a python start and a parse of the client's config), and nearly every beat has - # nothing new for them: a burst of thousands of proot stops every quarter minute, game loads - # included. Each runs only when what it reads has changed, which bash answers without starting - # anything ([ -nt ], globs), and every five minutes regardless. The seeder acts on a prefix (or - # Proton's template for them) it has not marked, and on new install scripts, which arrive with a - # manifest; the compat pass on a new manifest, depot or tool, or the client rewriting config.vdf. ( beat=0 compat_stamp=$HOME/.bl-compat-beat @@ -762,8 +548,6 @@ case $mode in seed_due && seed=1 compat_due && compat=1 [ -n "$seed$compat" ] || continue - # Not beside a running game: the client rewrites its manifest at each launch, which made both - # due while it loaded. Its own launch seeds its prefix (droiddeck-game-env) regardless. pgrep -x wineserver >/dev/null 2>&1 && continue if [ -n "$seed" ]; then : > "$seed_stamp" @@ -775,12 +559,6 @@ case $mode in fi done ) & - # A game the app asked to launch on a cold start is not put on the client's command line: the - # client takes it before its interface is up, its launch stops at "waiting for user response to - # ShowInterstitials" with nobody to answer, and the game page reads Launching for ever (device, - # 2026-10-09, and 2026-10-08 before it). It is handed over once the interface has focus - # (gamescope's focused app 769) and has had a moment, through the same steam-game request an - # in-session launch uses; after two minutes it goes anyway. if [ -n "${BL_LAUNCH_DIR:-}" ] && [ -z "${BL_DESKTOP:-}" ]; then _bl_held= _bl_rest=() @@ -803,13 +581,8 @@ case $mode in ) & fi fi - # Debug aid while the runtime is being brought up: an app id dropped in ~/.bl-autolaunch is - # handed to the client as a rungameid URL, so a launch can be reproduced without touching the - # screen. Removed once the runtime is stable. if [ -s "$HOME/.bl-autolaunch" ]; then _bl_app=$(cat "$HOME/.bl-autolaunch") - # A bare app id runs the game; a full steam:// URL is passed through, which is how a - # validate or an install is driven without touching the screen. case $_bl_app in steam://*) _bl_url=$_bl_app ;; *) _bl_url=steam://rungameid/$_bl_app ;; @@ -817,13 +590,6 @@ case $mode in echo "== autolaunch $_bl_url" set -- "$@" "$_bl_url" fi - # The client builds Settings > Internet and its connection indicator on NetworkManager, which - # it looks for on the SYSTEM bus as it starts. This runtime has neither, so the page failed - # ("StartScanningForNetworks is not a function") under a header that showed no connection even - # while downloads ran. A system bus and a stand-in that reports Android's connection - and - # refuses what it cannot do, since the app has no say over Wi-Fi - answer what the client asks. - # The bus config is written here rather than shipped, so a runtime installed earlier gets this - # from the app's copy of the session scripts without a re-host. (WinNative, maxjivi05, e7a224ae.) if command -v dbus-daemon >/dev/null 2>&1 && command -v droiddeck-netmanager >/dev/null 2>&1; then mkdir -p /run/dbus /usr/local/share/droiddeck cat > /usr/local/share/droiddeck/system-bus.conf <<'BUSCONF' @@ -852,9 +618,6 @@ BUSCONF droiddeck-netmanager & netmanager=$! echo "== network: system bus up, NetworkManager stand-in running" - # Steam blanks its main UI before calling login1.Suspend. Without the - # matching resume notification it stays black although the menus work. - # Own the name before Steam constructs its login1 proxy at startup. rm -f "$BL_LAUNCH_DIR/steam-sleep-ready" droiddeck-login1 & login_manager=$! @@ -880,9 +643,6 @@ BUSCONF export STEAM_RUNTIME=1 export LD_LIBRARY_PATH=$steam_root/steamrtarm64:$steam_root/steamrtarm64/panorama${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} export SDL_VIDEO_X11_DGAMOUSE=0 - # Decky is optional and session scoped: the app's marker opts in, and this supervisor is - # reaped with the client session. Native ARM64 runs directly. A legacy x86_64 asset is used - # only when its recorded architecture says so and droiddeck-fex has FEX and its runtime ready. decky_loader=$HOME/homebrew/services/PluginLoader if [ -f "$HOME/.droiddeck-decky-enabled" ] && [ -x "$decky_loader" ]; then decky_arch=$(cat "$HOME/homebrew/services/.droiddeck-decky-arch" 2>/dev/null || echo unknown) @@ -929,47 +689,23 @@ BUSCONF export BL_CDP_GUARD=$BL_CDP_PORT echo "== steam: devtools port $BL_CDP_PORT, guest processes only" fi - # The agent bridge drives the client through it (droiddeck-agent's cdp requests). [ -n "${BL_LAUNCH_DIR:-}" ] && mkdir -p "$BL_LAUNCH_DIR/agent" && echo "$BL_CDP_PORT" > "$BL_LAUNCH_DIR/agent/cdp-port" droiddeck-steam-compat "$steam_root" 2>&1 & - # The client exits 42 when it has updated itself and wants a fresh start (steam.sh does this). - # BL_STEAM_UI=desktop = the client's desktop UI (a window, the library as a list), which the - # desktop's launchers ask for. KDE owns its windows when BL_DESKTOP=1. _bl_ui=(-gamepadui) [ "${BL_STEAM_UI:-}" = desktop ] && _bl_ui=() _bl_cef=(-cef-force-gpu -cef-use-gl=angle -cef-use-angle=vulkan) if [ "${BL_DESKTOP:-}" = 1 ] && [ "${BL_STEAM_UI:-}" = desktop ]; then - # KDE uses QPainter/shm on Android. CEF's Vulkan buffers cannot be - # presented there; its software path keeps the library and dialogs on KDE. _bl_cef=(-cef-disable-gpu) _bl_ui=(-nobigpicture) fi - # Deck mode, always outside the desktop UI: -steamdeck is the flag that puts - # the Quick Access Menu and the Deck settings pages on screen; the QAM's performance overlay - # controls stay greyed out without -steamos3, as SteamOS and Armada launch it. -steamos3 makes - # the client manage itself as SteamOS does - it calls steamos-select-branch and writes - # steamdeck_stable to package/beta, and a branch on disk that loses to the -clientbeta below - # is an update that never installs: exit 42, a restart, for ever (Bannerlator 38996ea1). The - # loop below puts package/beta back before every start. The SteamOS helpers the client calls - # in this mode are staged as no-op stubs under /usr/bin. if [ "${BL_STEAM_UI:-}" != desktop ]; then _bl_ui+=(-steamdeck -steamos3) - # The QAM's Scaling Filter lists NVIDIA Image Scaling only when told gamescope has it (it does). export STEAM_GAMESCOPE_NIS_SUPPORTED=1 - # And shows its Scaling Mode and Scaling Filter only when told gamescope takes them. export STEAM_GAMESCOPE_FANCY_SCALING_SUPPORT=1 echo "== steam: Deck mode on (-steamdeck -steamos3)" fi - # The power menu's Shut Down and Restart. Without these the client blanks its interface and - # stays up (device, 2026-10-12: System.ShutdownPC and nothing after it). With them it does what - # SteamOS's gamescope session expects: writes the sentinel and exits. The loop below ends the - # session for one and starts the client again for the other. export STEAMOS_STEAM_SHUTDOWN_SENTINEL="$HOME/.bl-steam-shutdown" export STEAMOS_STEAM_REBOOT_SENTINEL="$HOME/.bl-steam-reboot" - # What a killed session leaves behind. Android ends sessions without warning (backgrounding, the - # low-memory killer), and the client then finds .crash - opening its update UI on the spinner - - # and the web helper's Singleton* locks in htmlcache, owned by a process that is gone, which can - # leave the Gamepad UI blank. No client or helper is running at this point in a session. if ! pgrep -f 'steamwebhelper|steamrtarm64/steam' >/dev/null 2>&1; then _stale="" for _f in "$steam_root/.crash" "$steam_root/steam.pid" \ @@ -980,18 +716,7 @@ BUSCONF [ -n "$_stale" ] && echo "== steam: cleared what an earlier session left:$_stale" unset _stale _f fi - # The Deck's beta branch, the one the first-run download takes: with -steamos3 on publicbeta - # the client settled on steamdeck_stable, an older client it then offered as a "Software - # Update" every session (2026-09-30); on steamdeck_publicbeta it finds no update. Whatever - # wrote package/beta, the branch on disk has to agree with the one forced here - a - # disagreement is the restart loop above, and it is silent apart from rc=42 in this log. A - # client installed from publicbeta by an older app moves over this way at its next start. - # Checked before every start, not once: the client rewrites package/beta during its own run, - # and a restart below (rc=42, or the one after the ARM64 Proton lands) would otherwise start it - # again on the branch it just wrote. steam_channel=steamdeck_publicbeta - # The app's language (Setup, or the system's): the client's interface takes -language over its - # registry, and the registry is rewritten before each start below for its update window. _bl_lang=() [ -n "${BL_STEAM_LANGUAGE:-}" ] && _bl_lang=(-language "$BL_STEAM_LANGUAGE") steam_beta=$steam_root/package/beta @@ -999,16 +724,12 @@ BUSCONF droiddeck-steam-games watch "$steam_root" "$BL_ADDED_GAMES" & games_watcher=$! fi - # Lossless Scaling (Steam app 993090): whether this account owns it, asked of the signed-in - # client once a session until the answer is yes. From then on the app offers importing - # Lossless.dll and passes BL_LOSSLESS_OWNED=1, so the question is never asked again. if [ "${BL_LOSSLESS_OWNED:-0}" != 1 ]; then ( owned=$HOME/.config/droiddeck/lossless-owned for ((_i = 0; _i < 60; _i++)); do nap 30 [ -e "$owned" ] && exit 0 - # Loading steamclient.so under proot takes seconds of CPU: not while a game loads or runs. pgrep -x wineserver >/dev/null 2>&1 && continue answer=$(droiddeck-steam-games probe "$steam_root" 993090 2>/dev/null) || continue case $answer in @@ -1020,15 +741,8 @@ BUSCONF ) & fi echo "== steam args: ${_bl_ui[*]} ${_bl_cdp[*]} ${_bl_lang[*]} -clientbeta $steam_channel -no-cef-sandbox -cef-ozone-platform=x11 ${_bl_cef[*]} $*" - # An update that cannot install asks for a restart (rc=42) every time; the count is capped so a - # session can never again loop in silence, which reads as a menu opening and closing by itself. steam_updates=0 rm -f "$HOME/.bl-stop-requested" "$STEAMOS_STEAM_SHUTDOWN_SENTINEL" "$STEAMOS_STEAM_REBOOT_SENTINEL" - # The client brings a game it launched to the front only while it counts someone as present, - # which it reads from gamescope's input counter. A game the app launches after a few minutes - # with no touch starts behind the library page and stays there until the next input (device, - # 2026-10-09). The game's window turning up behind the interface is counted as one input, as - # gamescope counts a key. steam_bring_forward() { local end=$((SECONDS + 600)) behind=0 line name value focused other n while [ "$SECONDS" -lt "$end" ]; do @@ -1099,7 +813,6 @@ BUSCONF echo start >> "$steam_root/package/droiddeck-unconfirmed" _bl_counted=1 fi - # Between starts only: the client holds registry.vdf in memory and writes it back on exit. if [ -n "${BL_STEAM_LANGUAGE:-}" ]; then droiddeck-steam-language "$HOME" "$BL_STEAM_LANGUAGE" || true fi @@ -1140,8 +853,6 @@ BUSCONF set -- continue fi - # The session asked for this exit (the ARM64 Proton landed): start again, without the URL - # arguments, which were acted on. Not counted against the update cap. if [ -f "$HOME/.bl-restart-once" ]; then echo "== steam: restarting once so the new compatibility layer is in the client's lists" set -- @@ -1153,7 +864,6 @@ BUSCONF echo "== steam: asked to restart for an update $steam_updates times and it is not installing; the session stops here rather than loop" break fi - # The URL arguments were acted on the first time; a restart must not repeat them. set -- done if [ -n "${stop_watcher:-}" ]; then @@ -1165,10 +875,6 @@ BUSCONF wait "$games_watcher" 2>/dev/null || true fi if [ -n "${BL_DESKTOP:-}" ]; then - # Started from the desktop (droiddeck-steam-launch): the desktop's own programs share this - # script's parent, so reap_orphans - right under gamescope, where everything is the client's - - # is not for here. The helpers this run started (the cores and redistributables beats, the - # system bus and the NetworkManager stand-in) go with the client instead. if [ -n "${decky_supervisor:-}" ]; then kill "$decky_supervisor" 2>/dev/null || true wait "$decky_supervisor" 2>/dev/null || true diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat index 31c06358d..24b2ad88b 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat @@ -126,10 +126,6 @@ INSTALL_JS = r""" return "installed"; })() """.replace("__BINDING__", BINDING) -# Big Picture's "Switch to Desktop" asks the client's SteamOS manager, which the client has only when it -# runs as SteamOS (-steamos3, Deck mode); otherwise the request fails and the screen stays on "Switching -# to Desktop..." for good. The request is caught on its way to the client and answered as SteamOS's -# own path does, with steamos-session-select. SWITCH_BINDING = "droiddeckSwitchToDesktop" SWITCH_JS = r""" (() => { @@ -526,7 +522,6 @@ def main(): os.makedirs(COMPAT["COMPAT_DIR"], exist_ok=True) watcher = Watcher([COMPAT["state_path"]("request.json"), COMPAT["state_path"]("games.json")] + [os.path.join(lib, "steamapps", "libraryfolders.vdf") for lib in (steam_root, COMPAT["LIBRARY"] + "-sd")]) - # The client has its own SteamOS manager when started as SteamOS (droiddeck-session's Deck mode). steamos = os.environ.get("BL_STEAMDECK") == "1" and os.environ.get("BL_STEAM_UI") != "desktop" helper = Helper(steam_root, port, steamos) failures = 0 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-desktop-ui b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-desktop-ui index 6eda34131..68b0a2a3a 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-desktop-ui +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-desktop-ui @@ -16,7 +16,6 @@ def select_desktop(evaluate, timeout=90): if result.get('value') is True: return True except Exception: - # CEF and SharedJSContext are not ready during the client's initial startup/update. pass time.sleep(1) return False diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games index 5fe62eb1e..85ecce1f2 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games @@ -113,7 +113,6 @@ def method(obj, slot, result, *args): def probe(steam_root, ids): - # Slots belong to Valve's SteamClient020, SteamUser021 and STEAMAPPS_INTERFACE_VERSION008. library = C.CDLL(str(Path(steam_root) / "steamrtarm64/steamclient.so")) library.CreateInterface.argtypes = [C.c_char_p, C.c_void_p] library.CreateInterface.restype = C.c_void_p @@ -185,12 +184,6 @@ def watch(steam_root, listing): candidates, sources = {}, {} next_lookup = 0 last_status = None - # The probe is a fresh python that loads steamclient.so, seconds of CPU under proot. Ownership - # only moves with a purchase or a new match, so it runs when the matches or their sources - # change, after a failed probe, and otherwise every five minutes - not on every 30 s beat. - # A failed probe is retried on a doubling delay up to those five minutes rather than on every - # beat (a client that is not logged on fails it for as long as it stays that way), and none - # starts while a game is running: the answer can wait, the game's cores cannot. probed, next_probe, failures = None, 0, 0 pending, requested = [], [] while True: diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install index 0eae6d0d2..1ae379d94 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install @@ -1,10 +1,6 @@ #!/bin/bash -# Fetches the native arm64 Steam client from Valve's client-update CDN into ~/.local/share/Steam. -# The client is not redistributable, so the runtime ships this script instead of the client. set -eu cdn=https://client-update.fastly.steamstatic.com -# The Deck's beta branch, which droiddeck-session forces on the command line too. A client already -# on disk from another branch is left for the client to move (droiddeck-session's package/beta). steam_channel=steamdeck_publicbeta manifest=steam_client_${steam_channel}_linuxarm64 steam_root=${XDG_DATA_HOME:-$HOME/.local/share}/Steam @@ -43,7 +39,6 @@ work=$(mktemp -d) trap 'rm -rf "$work"' EXIT curl -fsSL "$cdn/$manifest" -o "$work/manifest" version=$(awk -F'"' '$2 == "version" { print $4; exit }' "$work/manifest") -# Every component block: its name, then the plain zip's file name and sha256. awk -F'"' ' /^\t"/ && NF >= 3 && $4 == "" { block = $2 } $2 == "file" { file[block] = $4 } @@ -62,7 +57,6 @@ while read -r block file sum; do unzip -qo "$work/$file" -d "$steam_root" rm -f "$work/$file" done < "$work/parts" -# Some entries are zipped with Windows separators; unzip keeps the backslash in the file name. find "$steam_root" -depth -name '*\\*' | while read -r odd; do fixed=${odd//\\//} mkdir -p "$(dirname "$fixed")" @@ -75,17 +69,11 @@ done echo "$steam_channel" > "$steam_root/package/beta" ln -sfn "$steam_root/steamrtarm64" "$steam_root/steamrtarm32" chmod +x "$steam_root/steamrtarm64/steam" 2>/dev/null || true -# What Valve's steam.sh sets up before it runs the client (it has no arm64 branch itself). mkdir -p "$HOME/.steam" ln -sfn "$steam_root" "$HOME/.steam/root" ln -sfn "$steam_root" "$HOME/.steam/steam" ln -sfn "$steam_root/steamrtarm64" "$HOME/.steam/bin64" ln -sfn "$steam_root/linuxarm64" "$HOME/.steam/sdk64" -# The arm64 client runs every game through $HOME/.steam/sdkarm64/steam-launch-wrapper. Valve's -# steam.sh has no arm64 branch to create it, and without it a launch dies in /bin/sh before Proton -# is ever invoked - "No such file or directory", no Proton log, and the only other clue is an -# unrelated-looking installscript assertion. Same target as sdk64; the client picks the name by -# architecture. ln -sfn "$steam_root/linuxarm64" "$HOME/.steam/sdkarm64" ln -sfn "$steam_root/steamrtarm64" "$HOME/.steam/binarm64" ln -sfn "$steam_root/linux32" "$HOME/.steam/sdk32" diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language index bde57b06f..f892e8e22 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language @@ -122,7 +122,6 @@ def set_value(tokens, opening, key, value): def with_language(text, language): tokens = tokenize(text) if child(tokens, 0, len(tokens), "Registry") is None: - # Not a registry the client wrote (or none at all): start one it will read. tokens = [] for path in KEYS: set_value(tokens, ensure_block(tokens, path), "language", language) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-launch b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-launch index b24dffe05..71da571dd 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-launch +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-launch @@ -1,11 +1,4 @@ #!/bin/bash -# Starts the Steam client from inside a session (the desktop's menu entries and the per-game -# launchers droiddeck-desktop-games writes): -# droiddeck-steam-launch desktop [appid|steam-url] Steam's desktop UI, optionally a game -# droiddeck-steam-launch bigpicture [appid|steam-url] Big Picture -# -# The desktop client stays on KDE's Xwayland. Big Picture is the explicit handoff -# to the app's gaming session. set -u ui=${1:-desktop} target=${2:-} @@ -21,13 +14,9 @@ esac if [ "$ui" = desktop ] && [ "${BL_DESKTOP:-}" = 1 ]; then steam_root=${XDG_DATA_HOME:-${HOME:-/root}/.local/share}/Steam export LD_LIBRARY_PATH=$steam_root/steamrtarm64:$steam_root/steamrtarm64/panorama${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} - # A second menu click forwards its URL to Steam's existing instance. Keep only - # one session supervisor, including while the client restarts for an update. mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}/droiddeck" exec 9>"${XDG_CACHE_HOME:-$HOME/.cache}/droiddeck/steam-desktop.lock" if ! flock -n 9; then - # The supervisor may still be preparing Proton or updating Steam. Invoking - # the binary before its IPC pipe exists would start a competing client. for ((attempt = 0; attempt < 180; attempt++)); do pid=$(cat "$HOME/.steam/steam.pid" 2>/dev/null || true) if [ -p "$HOME/.steam/steam.pipe" ] && [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null; then @@ -43,12 +32,10 @@ if [ "$ui" = desktop ] && [ "${BL_DESKTOP:-}" = 1 ]; then exec /usr/local/bin/droiddeck-session steam "${url:-steam://open/library}" fi -# Ask the app for a Steam session. dir=${BL_LAUNCH_DIR:-} if [ -z "$dir" ] || [ ! -d "$dir" ]; then echo "droiddeck-steam-launch: not inside a DroidDeck session (BL_LAUNCH_DIR unset)" >&2 exit 69 fi -# Written whole, then renamed: the app reads it on close, never half-written. printf 'ui=%s\nurl=%s\n' "$ui" "$url" > "$dir/steam-launch.tmp" && mv -f "$dir/steam-launch.tmp" "$dir/steam-launch" echo "droiddeck-steam-launch: asked the app for Steam ($ui${url:+, $url})" diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library index 0a961c87a..171400b31 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library @@ -1,23 +1,10 @@ #!/usr/bin/python3 -# Registers the card the app installs to (bound at /mnt/droiddeck-sd) as a library folder of the -# native Steam client, so they show as installed there instead of being downloaded a second time. -# -# droiddeck-steam-library <steam root> -# -# Ported from WinNative's gamescope runtime (GPL-3.0). import json import os import re import sys -# The client's own library is internal storage (its steamapps/common is bound to the app's root); -# the card is the one extra library folder. -# The app binds the storage the user chose (Steam cog -> Game storage) at this path and names it -# in BL_LIBRARY_LABEL; nothing bound = internal only, and the entry is removed so the client does -# not keep offering a place that is not there. LIBRARIES = (("/mnt/droiddeck-sd", os.environ.get("BL_LIBRARY_LABEL", "SD Card")),) -# The client calls its own library "Local Drive (/)", which says nothing about where that is. -# It is the phone's internal storage, so say so. MAIN_LABEL = "Internal Storage" @@ -153,10 +140,6 @@ def relabel(steam_root, target, label): with open(path, "w", encoding="utf-8") as f: f.write("\n".join(out)) print("droiddeck-steam-library: labelled %s %r" % (target, label)) - # A library also carries its own libraryfolder.vdf, and the client takes the label from there - # back into the list above - so editing the list alone is undone on the next start. The - # client's own library has no such file, which is why that one kept the new name and the card - # went back to the name it was first registered with. marker = os.path.join(target, "libraryfolder.vdf") if not os.path.isfile(marker): return diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shim b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shim index 9f97bb791..3ecbfceae 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shim +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shim @@ -1,8 +1,4 @@ #!/bin/bash -# `steam` on the desktop. The client writes a menu entry per installed game that runs -# `steam steam://rungameid/<id>`, and its own launchers say `steam`. On the desktop that goes -# through droiddeck-steam-launch, which keeps the desktop client on KDE and -# hands URLs to a client already running there. Anywhere else it is the client itself. set -u if [ "${BL_DESKTOP:-}" = 1 ]; then url= diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts index 3fa78c900..d8e24069e 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts @@ -29,7 +29,6 @@ def account_dir(steam_root): pass userdata = os.path.join(steam_root, "userdata") if recent is not None: - # loginusers holds the 64-bit id; userdata folders use the 32-bit account id. acct = recent - 76561197960265728 return os.path.join(userdata, str(acct)) try: @@ -39,7 +38,6 @@ def account_dir(steam_root): return os.path.join(userdata, dirs[0]) if len(dirs) == 1 else None -# ---- binary VDF: 0x00 map, 0x01 string, 0x02 int32, 0x08 end ---- def parse(data): pos = [0] @@ -123,8 +121,6 @@ def shortcut(game, launch_options=None): RECORD = ".droiddeck-shortcuts.json" AWAY_LIMIT = 256 FIELDS = ("AppName", "Exe", "StartDir", "icon", "LaunchOptions") -# What the app may change after the shortcut exists. Each follows the app only while Steam still -# has what the app last asked for; a value the user set in Steam's Properties stays. FOLLOWED = ("StartDir", "LaunchOptions") @@ -164,9 +160,6 @@ def merge(fresh, current, state, seen): for field in ("AppName", "icon"): if current.get(field) == base.get(field): entry[field] = fresh[field] - # Start in and launch options the app sets: only once there is a record of what it asked for - # before (a first run keeps whatever the shortcut already had), and an older record that did - # not keep launch options means the app asked for none. if isinstance(state.get("app"), dict): for field in FOLLOWED: was = base.get(field, "" if field == "LaunchOptions" else current.get(field)) @@ -240,8 +233,6 @@ def save_record(path, record): ART_MANIFEST = ".droiddeck-art.json" -# listing key -> the client's grid file suffix: <appid>p = portrait capsule, <appid> = header, -# <appid>_hero, <appid>_logo. ART_SLOTS = (("p", "p"), ("header", ""), ("hero", "_hero"), ("logo", "_logo")) @@ -273,7 +264,7 @@ def install_art(acct, games): dst = os.path.join(grid, name) size = os.path.getsize(src) if os.path.exists(dst) and name not in manifest: - continue # the user's own choice inside the client; never ours to touch + continue wanted[name] = size if os.path.exists(dst) and manifest.get(name) == size: continue @@ -344,9 +335,7 @@ def main(): save_record(record_path, record) print("droiddeck-steam-shortcuts: %d added game(s) written, %d of the client's own kept" % (len(ours), len(kept))) install_art(acct, [g for g in games if g.get("exe") and g.get("name")]) - # Each added game runs under the ARM64 Proton: the compat registrar's own mapping writer. try: - # No .py suffix on the registrar, so the loader has to be named. compat_path = "/usr/local/bin/steam-compatibility" spec = importlib.util.spec_from_file_location("compat", compat_path, loader=importlib.machinery.SourceFileLoader("compat", compat_path)) compat = importlib.util.module_from_spec(spec) @@ -356,7 +345,7 @@ def main(): compat.register_default(cfg, apps=[str(int(g["appid"]) & 0xFFFFFFFF) for g in games if g.get("exe")] + [str(appid) for appid in set(routes.values())], protect=compat.adopt_extras(os.path.join(steam_root, "compatibilitytools.d"))) print("droiddeck-steam-shortcuts: mapped them to %s" % compat.TOOL) - except Exception as e: # noqa: BLE001 - the games still show; the client falls back to the default tool + except Exception as e: print("droiddeck-steam-shortcuts: compat mapping skipped (%s)" % e) return 0 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-ui-scale b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-ui-scale index b3e009e60..4a5997684 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-ui-scale +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-ui-scale @@ -21,7 +21,6 @@ import tempfile DISPLAY = "External: gamescope|||Windowed" NATIVE_RESOLUTION = "EnableSteamNativeExternalResolution" -# screensaver_while_ingame: kept in config.vdf like the Gamescope settings, under this path. SCREENSAVER_IN_GAME = ("Customization", "Screensaver", "WhileInGame") TOKEN = re.compile(r'\s+|//[^\n]*|"(?:\\.|[^"\\])*"|[{}]') @@ -79,8 +78,6 @@ def parse(text): def initial_scale(width, height): - # The Thor setting chosen on device at 1280x720 was 140% of its automatic scale. - # Adjust with canvas area so choosing a different session resolution keeps the UI usable. if not 320 <= width <= 16384 or not 320 <= height <= 16384: raise ValueError("invalid session size") return 1.3281567096710205 * math.sqrt(width * height / (1280 * 720)) @@ -97,8 +94,6 @@ def seed_text(text, scale): if display is not None and not isinstance(display, Block): raise ValueError("invalid display settings") if display is not None: - # Current describes the active display and is written even with automatic scaling. - # Named display entries are the user's saved choices, including a return to Auto. if any(name.lower() != 'current' and isinstance(value, Block) and value.get('ScaleFactor') is not None for name, value in display.pairs): return text, False @@ -111,7 +106,6 @@ def seed_text(text, scale): else: entry = '"UI"\n{\n"display"\n{\n' + entry + '}\n}\n' at, indent = root.end, 1 - # Insert only the new block; leave every existing byte, comment and credential untouched. insertion = '\n' + '\n'.join('\t' * indent + line for line in entry.splitlines()) + '\n' return text[:at] + insertion + text[at:], True @@ -138,7 +132,6 @@ def seed_value(text, path, value): for level, section in reversed(list(enumerate(sections))): lines = [(level, f'"{section}"'), (level, '{')] + lines + [(level, '}')] insertion = ''.join('\t' * (depth + level) + line + '\n' for level, line in lines) - # Before the line the block closes on, when it has one of its own; else beside the brace. at = text.rfind('\n', 0, block.end) + 1 if text[at:block.end].strip(): at, insertion = block.end, '\n' + insertion diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr index 162e471da..722769699 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr @@ -1,14 +1,4 @@ #!/bin/sh -# SteamVR for Cardboard mode (docs/cardboard-vr.md). Experimental. -# -# droiddeck-steamvr status one line: installed=0|1 running=0|1 installing=0|1 driver=0|1 -# droiddeck-steamvr install fetch Valve's ARM64 SteamVR (the Steam Frame's package) into /opt/steamvr -# droiddeck-steamvr start start SteamVR with the DroidDeck Cardboard driver -# droiddeck-steamvr stop stop it -# -# SteamVR renders through the driver (tools/vr/src/driver_droiddeck.cpp) into an overlay window that -# gamescope shows over everything; the app's Cardboard pass, with a side-by-side source, puts the lens -# on it. vrcompositor needs the Vulkan layer (tools/vr/src/vk_layer_droiddeck_vr.c) on Turnip/KGSL. set -u steamvr=${STEAMVR:-/opt/steamvr} @@ -19,7 +9,6 @@ logs=$HOME/.local/share/Steam/logs state=${XDG_RUNTIME_DIR:-/tmp}/droiddeck-steamvr mkdir -p "$state" -# The Steam Frame's SteamVR, from Valve's package server; the same build the Frame's SteamOS ships. PKG_URL=https://steamdeck-packages.steamos.cloud/archlinux-deckard-hotfixes/release/0.3.x/deckard-steamvr-rel-r25358740%2B28b72a4f-1-aarch64.pkg.tar.zst PKG_SHA256=90a600775eb3e8856dc475f83166a93a290a0b1f3b4e82a69c8e2a8f06b9ddc9 @@ -31,9 +20,7 @@ stop() { for n in vrcompositor vrserver vrmonitor vrdashboard vrstartup vrwebhelper; do for p in $(pgrep -x "$n"); do kill "$p" 2>/dev/null; done done - # pgrep -x matches 15 characters of the name at most. for p in $(pgrep -f "$steamvr/bin/linuxarm64/vrcompositor-launcher"); do kill "$p" 2>/dev/null; done - # A process that exited on its own meanwhile is not a failure: the app reads this exit code. return 0 } @@ -41,9 +28,6 @@ case "${1:-status}" in status) i=0; installed && i=1 r=0; running && r=1 - # A session that died with SteamVR up leaves the marker behind; it would keep the layer active. - # Only an old one: vrserver takes a while to appear after start, and Steam's web helper needs the - # marker from the start. if [ "$r" = 0 ] && [ -n "$(find "$state/active" -mmin +2 2>/dev/null)" ]; then rm -f "$state/active"; fi n=0; [ -f "$state/installing" ] && n=1 d=0; [ -f "$driver/bin/linuxarm64/driver_droiddeck.so" ] && [ -f "$layer/libVkLayer_droiddeck_vr.so" ] && d=1 @@ -88,13 +72,10 @@ start) JSON ln -sf "$steamvr/steamxr_linuxarm64.json" "$HOME/.config/openxr/1/active_runtime.json" - # vrcompositor asks for VK_KHR_display, which makes Turnip/KGSL report no GPU at all; the layer stands - # in for a display. gamescope's implicit WSI layer stays out of SteamVR's processes. export VK_LAYER_PATH="$layer${VK_LAYER_PATH:+:$VK_LAYER_PATH}" export VK_INSTANCE_LAYERS="VK_LAYER_DROIDDECK_vr${VK_INSTANCE_LAYERS:+:$VK_INSTANCE_LAYERS}" export VK_LOADER_LAYERS_DISABLE="${VK_LOADER_LAYERS_DISABLE:-~implicit~}" - # SteamVR's web helper (the dashboard) checks the X server with xset, which the runtime lacks. shims=$state/shims mkdir -p "$shims" if ! command -v xset >/dev/null 2>&1; then @@ -103,8 +84,6 @@ JSON export PATH="$shims:$PATH" fi - # While this file exists, the layer is active in every Vulkan instance of the session, not only - # SteamVR's: Steam's web helper shares its in-VR overlay with SteamVR and needs it too. : > "$state/active" cd "$steamvr/bin/linuxarm64" || exit 1 setsid ./vrstartup > "$state/vrstartup.log" 2>&1 < /dev/null & diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch b/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch index 710c58dd6..ba1415112 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch @@ -116,7 +116,6 @@ def ask(channel, request, timeout=TIMEOUT): return value except (OSError, ValueError): time.sleep(0.05) - # Not answered: the request is withdrawn so a late app does not mint a code nobody reads. try: (req_dir / (rid + ".json")).unlink() except OSError: @@ -229,10 +228,7 @@ def provision_prefix(compat_data, wait=SERVER_WAIT): reg = pfx / "user.reg" if not reg.is_file(): return "prefix-not-created" - # A wineserver still up for this prefix would write its own copy of user.reg over these lines. busy = not wait_for_server(pfx, wait) - # winebrowser starts the first entry it can; an absolute path does not depend on the PATH Wine - # hands it. set_value(reg, BROWSER_KEY, "Browsers", BROWSER + ",xdg-open") removed = remove_value(reg, EOS_KEY, "OverlayPath") state = "browser-set" if has_value(reg, BROWSER_KEY, "Browsers", BROWSER + ",xdg-open") else "browser-not-written" @@ -243,8 +239,6 @@ def provision_prefix(compat_data, wait=SERVER_WAIT): CLOUD_TIMEOUT = 15.0 CLOUD_STORES = ("gog", "epic") -# Exit status that tells droiddeck-proton this was a store game with cloud saves: it then waits for -# Proton instead of exec'ing it, and runs `droiddeck-store-launch --exited` after. STORE_GAME = 10 @@ -283,8 +277,6 @@ def provision_galaxy(compat_data, proton, timeout=90): stamp = version + " " + " ".join(hashlib.sha256(p.read_bytes()).hexdigest() for p in tools) if marker.is_file() and marker.read_text() == stamp and (data / "pfx/system.reg").is_file(): return "ready" - # cmd/sc run through this title's selected Proton. The run verb bypasses the managed - # wrapper, so this cannot recursively request credentials or sync cloud saves. with tempfile.TemporaryDirectory(prefix="droiddeck-galaxy-") as tmp: folder = Path(tmp) result = folder / "result" @@ -352,7 +344,6 @@ def main(argv): drop_stale(folder) choice = options(sidecar) if not (choice["eos"] and not choice["offline"]): - # Sign-in off, or offline: no code, and none left over from before. try: (folder / EPIC_CODE).unlink() except OSError: @@ -373,7 +364,6 @@ def main(argv): log("cloud %s %s down result=skipped reason=off" % (store, game)) return 0 if launch_dir: - # The cloud copy where it is newer, before the game reads its saves; bounded, then on regardless. answer = ask(Path(launch_dir) / "stores", {"op": "cloud-down", "store": store, "id": game}, timeout=CLOUD_TIMEOUT) if answer is None: log("cloud %s %s down result=timeout" % (store, game)) @@ -387,6 +377,6 @@ def main(argv): if __name__ == "__main__": try: sys.exit(main(sys.argv[1:])) - except Exception as e: # never in the way of the launch + except Exception as e: log("skipped: %s" % type(e).__name__) sys.exit(0) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents b/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents index 533f6674c..7349e6784 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents @@ -32,20 +32,13 @@ from pathlib import Path STORE = Path("/opt/droiddeck/wincomponents") SELECTION = Path.home() / ".config/droiddeck/wincomponents.json" -# Components the app is still downloading for a game Steam installed during this session: -# {"games": {"<appid>": ["vcredist2019_dll", ...]}}. A launch waits a little for them. PENDING = Path.home() / ".config/droiddeck/wincomponents-pending.json" PENDING_WAIT = 10.0 -# What this prefix holds from us, so a component the user turns off is taken out again. STATE = ".droiddeck-wincomponents.json" -# A file ours replaced that was not Proton's (a real installer's, the user's own), put back when the -# component is turned off. Proton's builtins need no copy: it restores those itself. BACKUP = ".droiddeck-wincomponents-backup" BUILTIN_MARKS = (b"Wine builtin DLL", b"Wine placeholder DLL") ARCH_DIRS = ("system32", "syswow64") ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}") -# The registry file of each hive in a prefix, and the kinds of value registry.json holds: "key" -# makes a key, "append"/"prepend" add to a list such as PATH unless it is already there. HIVES = {"HKLM": "system.reg", "HKCU": "user.reg"} VALUE_TYPES = {"key", "sz", "expand_sz", "multi_sz", "dword", "binary", "append", "prepend"} @@ -80,7 +73,6 @@ def component(store, cid): for arch in ARCH_DIRS: folder = root / arch if folder.is_dir(): - # A few go one level down (gmdls: system32/drivers/gm.dls). for entry in sorted(folder.rglob("*")): if entry.is_file() and not entry.is_symlink(): rel = entry.relative_to(folder).as_posix().lower() @@ -195,12 +187,10 @@ def place(src, target, backup=None): final = found or target temp = final.with_name(".%s.droiddeck" % final.name) shutil.copy2(src, temp) - # Over a Proton builtin this swaps its symlink for our file. os.replace(temp, final) return True -# ---------------------------------------------------------------- the prefix's registry files def reg_escape(text, extra=""): """A string as Wine's registry files write it.""" @@ -556,7 +546,6 @@ def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING """Brings the prefix in line with the game's picks. Returns (overrides, notes).""" compat = Path(compat_path) pfx = compat / "pfx" - # A game Steam installed during this session may have components still on their way. waited = wait(app_id, pending, store) if wait else None ids = wanted(app_id, selection) state = load_state(compat) @@ -576,7 +565,6 @@ def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING overrides += names values += [(cid, value) for value in registry] for rel, src in files: - # A file this prefix already has from us is ours to replace; anything else is kept. if place(src, pfx / rel, None if rel in state["files"] else compat / BACKUP / rel): copied += 1 keep[rel] = cid @@ -594,8 +582,6 @@ def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING path.unlink() removed += 1 if removed: - # Proton refreshes its builtins only when its config changes; forgetting the config puts - # its own DLLs back where ours were on the next launch - this one. try: (compat / "config_info").unlink() except FileNotFoundError: @@ -627,7 +613,6 @@ def overrides_value(names): def main(): - # droiddeck-wincomponents <compatdata dir> <appid>: what a launch would do, for a shell. if len(sys.argv) != 3: print("usage: droiddeck-wincomponents <compatdata dir> <appid>", file=sys.stderr) return 2 diff --git a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility index b64d721b7..71557cf00 100755 --- a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility +++ b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility @@ -1,20 +1,4 @@ #!/usr/bin/python3 -# Makes the ARM64 Proton the client already downloaded usable here, and sets it as the default tool -# for Windows titles (CompatToolMapping "0" in config.vdf) when the user has not chosen one. Run -# before the client starts; it rewrites config.vdf on exit. -# -# steam-compatibility <steam root> -# -# Valve ships a native aarch64 Proton (arm64ec Wine with FEX as the x86 layer) as an ordinary depot, -# which is exactly what this runtime wants. Its toolmanifest declares require_tool_appid 4185400 - -# Steam Linux Runtime 4 for arm64 - so Steam stacks pressure-vessel underneath it, and -# pressure-vessel needs unprivileged user namespaces, which an Android app does not get. Steam then -# drops the launch on the floor: no window, no log, straight back to the library. -# -# So the depot is re-registered here as a compatibility tool of our own: every entry symlinked so -# nothing is copied and Steam still updates the real depot, but with a toolmanifest that leaves out -# require_tool_appid. Proton takes its base directory from dirname(sys.argv[0]) without resolving -# it, so it reads files/ through those symlinks and runs directly on this rootfs. import json import os import re @@ -26,22 +10,10 @@ TOOL = "droiddeck-proton-arm64" TOOL_11 = "droiddeck-proton-11-arm64" LEGACY_TOOL = "bannerlator-proton-arm64" LABEL_FILE = "/etc/steam-compatibility-label" -# The ARM64 Proton depot the client downloads for itself, as a compatibility-tool appid. -# The session asks the client to install it on the command line the first time (steam://install), -# so a new client has it before a Windows title is first launched. It is deliberately NOT named as -# this tool's dependency: a dependency is composed with its own dependencies, and this depot -# requires 4185400, the arm64 Steam Linux Runtime, whose container needs user namespaces Android -# does not give - the launch would be wrapped in it and die before this tool ever ran. PROTON_ARM64_APPID = "4427310" LAUNCHER = "droiddeck-proton" LEGACY_LAUNCHER = "bannerlator-proton" -# The extra Steam library the app mounts the installed games into (see droiddeck-steam-library). -# installed_apps() scans it too; it lives only in the sibling script, so define it here as well. -LIBRARY = "/mnt/droiddeck" # the card library is LIBRARY + "-sd"; internal storage is steam_root itself -# The client lists its own "Proton Experimental (ARM64)" and "Proton 11.0 (ARM64)" in the same -# Compatibility dropdown, and those are the two that cannot start a game here. Ours has to be -# unmistakable at a glance, or a user picks a neighbour and gets a launch that fails silently. -# Newest first: whichever the client has downloaded is used. +LIBRARY = "/mnt/droiddeck" SOURCES = ("Proton Experimental (ARM64)", "Proton 11.0 (ARM64)") OWN_TOOLS = ((TOOL, SOURCES), (TOOL_11, ("Proton 11.0 (ARM64)",))) FEX_TOOL = "droiddeck-fex" @@ -53,13 +25,7 @@ FEX_DEPENDENCIES = (FEX_APPID, SNIPER_APPID) REQUEST_HANDOFF_SECONDS = 30 PLAYABLE_TYPES = frozenset(("game", "demo", "application", "beta")) COMPAT_DIR = os.path.join(os.environ.get("HOME", "/root"), ".local", "share", "droiddeck-compat") -# Written by us; everything else in the depot is symlinked through. OWN_FILES = ("toolmanifest.vdf", "compatibilitytool.vdf", LAUNCHER) -# The client assigns a Proton to a Windows title by itself, as a per-app CompatToolMapping entry at -# priority 250 - which outranks the "0" default, so setting that default alone changes nothing. Every -# Proton it can pick is one this runtime cannot start: the ARM64 ones want pressure-vessel (see -# above) and the x86_64 ones want pressure-vessel and FEX on top. Ours is the only one that runs -# here, so every Proton mapping is repointed at it. Tools that are not Proton are left alone. COMPAT_TOOL_VDF = """"compatibilitytools" { "compat_tools" @@ -84,49 +50,25 @@ TOOL_MANIFEST_VDF = """"manifest" """ % LAUNCHER -# Steam runs its own graphics probe through EVERY registered tool at once, ~40 s after logging in, -# all with STEAM_COMPAT_DATA_PATH=.../compatdata/0, and those concurrent Protons can write their -# builtin DLLs through each other's prefix links into each other's trees (seen: GE's FEX DLLs became -# Proton Experimental's). Moving those runs to a prefix per tool was tried and crashed the Steam -# client right after the probe started (segfault, rc 139, first ever in 25 sessions), so the client -# relies on compatdata/0 itself. The launch-time check below is the protection instead. - -# The Components page's choices, enforced in the one place every game launch passes: the app keeps -# desired.tsv (scope, tool dir, Proton build, component, file, sha256, source copy) and an unpacked -# copy of each package in use. A game's rows replace General rows for that component. A `-` source -# removes a managed file absent from the selected package, so one game's package cannot leak into -# the next game. A file that no longer matches - whatever changed it - is put back before Proton -# starts. Only the matching Proton build is changed; after a Steam update the build differs and the -# Proton's own new files are left alone until the user chooses again. -# A store game (GOG, Epic, Amazon) is started through the launcher .bat DroidDeck wrote beside it. -# An Epic game needs a fresh sign-in code, which only the app can mint: droiddeck-store-launch asks -# for it and the app writes it into the game's folder, so a launch from the client's own Play -# button signs in just as one from the app does. A real launch only; never in the way of it. BL_STORE_SETUP = """ bl_store_launch() { [ "${verb:-}" = waitforexitandrun ] || return 0 - # A web address the game opens (Wine's winebrowser runs xdg-open) goes to Android's browser: - # droiddeck-open-url as xdg-open, first on the game's PATH, with the session channel left in a - # file in case Wine does not pass BL_LAUNCH_DIR on. (The prefix also names it as Wine's browser.) if [ -x /usr/local/bin/droiddeck-open-url ]; then mkdir -p /tmp/droiddeck-browser ln -sfn /usr/local/bin/droiddeck-open-url /tmp/droiddeck-browser/xdg-open - [ -n "${BL_LAUNCH_DIR:-}" ] && printf '%s\n' "$BL_LAUNCH_DIR" > /tmp/droiddeck-browser/launch-dir + [ -n "${BL_LAUNCH_DIR:-}" ] && printf '%s +' "$BL_LAUNCH_DIR" > /tmp/droiddeck-browser/launch-dir PATH="/tmp/droiddeck-browser:$PATH"; export PATH fi - # BL_DEBUG_BROWSER=1 (droiddeck-env): Wine traces how the game opens a web address. if [ "${BL_DEBUG_BROWSER:-}" = 1 ]; then WINEDEBUG="${WINEDEBUG:+$WINEDEBUG,}+shell,+winebrowser"; export WINEDEBUG fi [ -x /usr/local/bin/droiddeck-store-launch ] || return 0 /usr/local/bin/droiddeck-store-launch "$@" - # 10: a store game with cloud saves - the launch waits for Proton and uploads after (bl_run). [ $? -eq 10 ] && BL_STORE_EXIT=1 return 0 } -# Runs Proton: exec'd as always, except for a store game with cloud saves, where the wrapper stays -# to tell the app the game has exited so it can upload what changed. bl_run() { if [ -n "${BL_STORE_EXIT:-}" ]; then "$@" @@ -143,11 +85,6 @@ bl_components() { base=$1 cm_dir="${HOME:-/root}/.local/share/droiddeck-components" cm_list="$cm_dir/desired.tsv" - # A real game launch only: the verb the client starts a game with and the game's own prefix. Every - # other run - above all the client's own probes right after login, which all share compatdata/0 - - # is left exactly as it was: with this check running through them, the client segfaulted (rc 139) - # within seconds of starting its probes, four sessions out of four. The prefix decides rather than - # the app id because a non-Steam shortcut launches with app id 0 but still gets a prefix of its own. [ "${verb:-}" = waitforexitandrun ] || return 0 case "${STEAM_COMPAT_DATA_PATH:-}" in ''|*/compatdata/0|*/compatdata/0/|*/compatdata/0-*) return 0 ;; esac [ -f "$cm_list" ] || return 0 @@ -192,8 +129,6 @@ bl_components() { if (scopes[i] == app || (scopes[i] == "*" && !specific[comps[i]])) print rows[i] } ' "$cm_list") - # One sha256sum for every file: each program started here pays a dozen traced library lookups - # under the game's search path before it does anything. local cm_rows=() cm_files=() row h p local -A cm_sums=() while IFS=$'\\t' read -r scope t want comp rel sum src; do @@ -230,36 +165,17 @@ bl_components() { } """ -# Resolves the depot at launch time, so the tool can be registered before the depot exists and -# survives the client updating or replacing it. Proton takes its base directory from -# dirname(sys.argv[0]) without resolving it, so it is run from the depot itself. -# Cores for a game the client launches, kept separate from the client's own. One mask suits the -# client's menus and another suits a game, and they are wanted at the same time - so the game's is -# applied here, in the launch path every game goes through, rather than to the session as a whole. BL_GAME_CPUS_SETUP = """ bl_game_cpus() { [ -n "${BL_GAME_CPUS:-}" ] || return 0 command -v taskset >/dev/null 2>&1 || return 0 - # exec'd through taskset rather than set afterwards: a mask applied to a pid races the process - # into spawning its own threads, and this way every thread inherits it from the first instruction. BL_TASKSET="taskset -c $BL_GAME_CPUS" echo "droiddeck-proton: game cores $BL_GAME_CPUS" >&2 } """ -# Quake III engine games (id Tech 3) set their own resolution and switch the display to it. Under -# gamescope with the ARM64 Proton that switch is refused ("calling CDS: failed, bad mode") and the -# game never gets OpenGL; left at its own size in a window, it draws small in a corner. So these -# titles are told to stay windowed at the session's own size - unless the user's launch options -# already say something about the video mode, which are left as they are. -# The session puts the client's own libraries (steamrtarm64, its panorama/) ahead of everything for -# the client, and a game inherits that: Wine then resolved gnutls, SDL, FFmpeg and the rest against -# the client's private copies instead of the rootfs it was built for. Inside Steam's runtime -# container that path never reaches Proton either. (WinNative's winnative-proton-launch.) BL_LIBPATH_SETUP = """ bl_libpath() { - # A Proton built to prefer PipeWire (CachyOS's among the adopted ones) hung its audio thread - # here, where only PulseAudio answers. (WinNative, winnative-proton-launch.) export PROTON_USE_PIPEWIRE=${PROTON_USE_PIPEWIRE:-0} [ -n "${LD_LIBRARY_PATH:-}" ] || return 0 local kept= dir old_ifs=$IFS @@ -279,7 +195,6 @@ BL_IDTECH3_SETUP = """ bl_idtech3() { BL_EXTRA_ARGS=() case "${SteamAppId:-${STEAM_COMPAT_APP_ID:-}}" in - # Quake III Arena, Quake III: Team Arena, Return to Castle Wolfenstein, Jedi Academy. 2200|2350|9010|6020) ;; *) return 0 ;; esac @@ -307,10 +222,6 @@ bl_directaudio() { echo "droiddeck-proton: DirectAudio left off - it is built for Wine 11 and this Proton is '${version:-unknown}'. Pairing them would be silent rather than broken, so it is not worth guessing." >&2 return 0 fi - # Two builds of the driver ship, one per Wine audio interface, and both Wines call themselves - # 11.0: the private mmdevapi table is not versioned, and the wrong set plays silence, not an - # error. Proton's own winepulse.so tells them apart - it imports PsCreateSystemThread only on - # the system-thread table (Proton-CachyOS), never on the classic one (Valve, GE-Proton). winepulse="$base/files/lib/wine/aarch64-unix/winepulse.so" if [ ! -f "$winepulse" ]; then echo "droiddeck-proton: DirectAudio left off - no winepulse.so to read this Proton's audio interface from" >&2 @@ -322,20 +233,10 @@ bl_directaudio() { echo "droiddeck-proton: DirectAudio left off - no driver set for the $set interface is staged" >&2 return 0 fi - # Wine appends the per-architecture subdirectory to each entry, so the directory holding - # aarch64-unix/ and aarch64-windows/ is what goes here. WINEDLLPATH="$da/lib/wine${WINEDLLPATH:+:$WINEDLLPATH}" export WINEDLLPATH - # Proton-CachyOS's launcher sets WINE_AUDIO_DRIVER=pulse,alsa (setdefault, so an existing value - # wins), and its mmdevapi reads that variable BEFORE the registry - so the selection below was - # being overridden to pulse on every CachyOS game. Valve's and GE's mmdevapi do not read the - # variable at all and take the registry, which is why both are done. A value the user set for - # the game still wins: the per-game environment is applied after this. WINE_AUDIO_DRIVER="directaudio,pulse" export WINE_AUDIO_DRIVER - # mmdevapi only tries the drivers named in the registry; its built-in list is pulse, alsa, oss and - # coreaudio and knows nothing about this one. The key is appended to the prefix's user.reg rather - # than written with `wine reg`, which would start a wineserver here just to set one value. pfx="${STEAM_COMPAT_DATA_PATH:-}/pfx" reg="$pfx/user.reg" if [ -f "$reg" ]; then @@ -352,46 +253,21 @@ bl_directaudio() { elif ! BL_KEY="$key" awk 'index($0, "[") == 1 { inside = index($0, ENVIRON["BL_KEY"] " ") == 1 } inside && index($0, "\\"Audio\\"=") == 1 { last = $0 } END { exit last != "\\"Audio\\"=\\"directaudio,pulse\\"" }' "$reg" 2>/dev/null; then printf '\\n%s %s\\n"Audio"="directaudio,pulse"\\n' "$key" "$(date +%s)" >> "$reg" echo "droiddeck-proton: DirectAudio selected in the prefix" >&2 - # Steam runs this tool several times per launch (getcompatpath, run, waitforexitandrun); a - # wineserver from an earlier verb that still holds the registry saves its own copy on exit - # and drops this line. Said out loud so a game that stays on pulse once is explained; it - # takes on the next launch. (Writing through the live server with a bare `wine reg add` was - # tried and withdrawn: a second Wine invocation inside Steam's launch sequence is not safe.) srv="${BL_WINE_SERVER_BASE:-/tmp/.wine-$(id -u)}/server-$(printf '%x-%x' $(stat -c '%d %i' "$pfx" 2>/dev/null || echo 0 0))/socket" if [ -S "$srv" ]; then echo "droiddeck-proton: a wineserver is already running for this prefix; DirectAudio may only engage on the next launch" >&2 fi fi else - # A prefix that does not exist yet is created by Proton moments from now, after this runs, so - # the value lands on the NEXT launch of that game. Said out loud so a first launch on Proton's - # own audio does not look like a fault. (Having Proton create the prefix from here first was - # tried and withdrawn: a second Proton run inside Steam's launch sequence got the launch - # aborted - Steam games bounced back to their page with a half-made prefix.) echo "droiddeck-proton: no prefix yet - DirectAudio takes effect the next time this game starts" >&2 fi echo "droiddeck-proton: DirectAudio ready (Wine $major, $set)" >&2 } """ -# Resolves the depot at launch time, so the tool can be registered before the depot exists and -# survives the client updating or replacing it. Proton takes its base directory from -# dirname(sys.argv[0]) without resolving it, so it is run from the depot itself. LAUNCHER_SH = """#!/bin/bash -# DroidDeck's Proton ARM64: runs the ARM64 Proton the Steam client downloaded, without the -# Steam Linux Runtime container its own manifest asks for (no user namespaces on Android). verb=$1; shift steam_root=${STEAM_COMPAT_CLIENT_INSTALL_PATH:-${XDG_DATA_HOME:-$HOME/.local/share}/Steam} -# Steam's overlay is preloaded into every game Valve launches, and used to be dropped here: it was -# landing in front of open() and read() on /dev/input, letting Wine open the session's pad and -# answering every read of it with nothing - a controller that never reports, and Wine's HID thread -# spinning a core on it. -# -# Removing it was the wrong cure for an ordering problem. glibc processes LD_PRELOAD BEFORE -# /etc/ld.so.preload, and the interposer lives in the latter, so the overlay was simply ahead of it. -# Naming the interposer first in LD_PRELOAD puts it back in front and the overlay loads as Valve -# intends. That matters beyond the overlay itself: every deviation from the launch Valve ships is a -# difference this runtime would otherwise have to justify. for fake in /usr/local/lib/libfakeinput.so /usr/local/lib/libblsession.so; do if [ -f "$fake" ]; then case ":${LD_PRELOAD:-}:" in @@ -400,9 +276,6 @@ for fake in /usr/local/lib/libfakeinput.so /usr/local/lib/libblsession.so; do esac fi done -# ld.so looks every library up along the game's search path (its own directory, Steam's runtime -# and video directories, Proton's), and under proot each miss is a round trip to the tracer. With -# the fast path, the audit module skips the misses it can prove (tools/proot/fastpath/audit.c). audit=/usr/local/lib/libblaudit.so if [ -n "${PROOT_FP_KEY:-}" ] && [ -f "$audit" ]; then case ":${LD_AUDIT:-}:" in @@ -410,8 +283,6 @@ if [ -n "${PROOT_FP_KEY:-}" ] && [ -f "$audit" ]; then *) LD_AUDIT="$audit${LD_AUDIT:+:$LD_AUDIT}"; export LD_AUDIT ;; esac fi -# The depot this tool is named after, newest across the libraries that hold it; the others in the -# list stand in only when it is missing. depot= libraries=$(printf '%%s\\n' "$steam_root" /mnt/droiddeck-sd; sed -n 's/^[[:space:]]*"path"[[:space:]]*"\\(.*\\)"[[:space:]]*$/\\1/p' "$steam_root/steamapps/libraryfolders.vdf" 2>/dev/null) for name in %s; do @@ -447,9 +318,6 @@ LAUNCHER_TEMPLATE = LAUNCHER_SH def launcher_sh(sources=SOURCES): - # Spliced in after the formatting, never through it: the function body contains ${major%%.*} - # and a % format would eat that into a single %, leaving a shell expansion that silently does - # something else. return (LAUNCHER_TEMPLATE % " ".join('"%s"' % n for n in sources)).replace( "verb=$1; shift", "verb=$1; shift\n" + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP + BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP + BL_STORE_SETUP, 1) @@ -457,10 +325,6 @@ def launcher_sh(sources=SOURCES): LAUNCHER_SH = launcher_sh() -# A third-party Proton dropped into compatibilitytools.d - GE-Proton, proton-cachyos, anything of -# that shape - is adopted rather than used as shipped, for the same two reasons Valve's ARM64 depot -# is: its manifest declares require_tool_appid, and the client's overlay library has to come out of -# LD_PRELOAD. Its own entry point is left untouched and called through this wrapper. EXTRA_WRAPPER = "droiddeck-proton-wrap" LEGACY_EXTRA_WRAPPER = "bannerlator-proton-wrap" ORIGINAL_SUFFIX = ".droiddeck-orig" @@ -473,16 +337,9 @@ EXTRA_MANIFEST_VDF = """"manifest" "compatmanager_layer_name" "proton" } """ % EXTRA_WRAPPER -# %s is the tool's own entry point, read out of the manifest it shipped with. EXTRA_WRAPPER_SH = """#!/bin/bash -# DroidDeck: runs a Proton installed into compatibilitytools.d without the Steam Linux Runtime -# container its own manifest asks for - pressure-vessel needs unprivileged user namespaces, which -# an Android app does not get, and the launch dies before Proton is reached. verb=$1; shift here=$(dirname "$0") -# See droiddeck-proton: the interposer goes first in LD_PRELOAD rather than the overlay being -# removed, because glibc reads LD_PRELOAD before /etc/ld.so.preload and the overlay was simply -# ahead of it on /dev/input. for fake in /usr/local/lib/libfakeinput.so /usr/local/lib/libblsession.so; do if [ -f "$fake" ]; then case ":${LD_PRELOAD:-}:" in @@ -491,7 +348,6 @@ for fake in /usr/local/lib/libfakeinput.so /usr/local/lib/libblsession.so; do esac fi done -# See droiddeck-proton: library lookups that cannot succeed are skipped with the fast path. audit=/usr/local/lib/libblaudit.so if [ -n "${PROOT_FP_KEY:-}" ] && [ -f "$audit" ]; then case ":${LD_AUDIT:-}:" in @@ -507,8 +363,6 @@ bl_libpath BL_STORE_PROTON="$here/%s"; export BL_STORE_PROTON bl_store_launch "$@" BL_STORE_ARGS=("$@") -# Proton takes its base directory from dirname(sys.argv[0]) without resolving it, so it has to be -# started from its own tree. bl_run ${BL_TASKSET:-} /usr/local/bin/droiddeck-game-env "$BL_STORE_PROTON" "$verb" "$@" "${BL_EXTRA_ARGS[@]}" """ EXTRA_COMPAT_TOOL_VDF = """"compatibilitytools" @@ -707,8 +561,6 @@ def adopt_extras(tools): manifest = os.path.join(tool_dir, "toolmanifest.vdf") if not os.path.isdir(tool_dir) or not os.path.isfile(manifest): continue - # The shipped manifest is kept, once, and stays the source of the entry point: re-running - # must not read back our own rewritten commandline and wrap the wrapper. original = manifest + ORIGINAL_SUFFIX legacy = manifest + LEGACY_ORIGINAL_SUFFIX if not os.path.isfile(original) and os.path.isfile(legacy): @@ -731,8 +583,6 @@ def adopt_extras(tools): name = first_key(compat_vdf, ["compatibilitytools", "compat_tools"]) or entry wrapper = os.path.join(tool_dir, EXTRA_WRAPPER) try: - # Formatted first, spliced second: the function carries a printf %s and a - # ${major%%.*} of its own, and putting it through the format would eat both. body = (EXTRA_WRAPPER_SH % entry_point).replace( 'here=$(dirname "$0")', 'here=$(dirname "$0")\n' + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP @@ -750,19 +600,17 @@ def adopt_extras(tools): return adopted -# Steam's own tools and runtimes live in steamapps beside the games; they are not titles and must -# not be mapped to a compatibility tool. NOT_GAMES = frozenset(( - "228980", # Steamworks Common Redistributables - "1070560", # Steam Linux Runtime 1.0 (scout) - "1391110", # Steam Linux Runtime 2.0 (soldier) - "1628350", # Steam Linux Runtime 3.0 (sniper) - "1493710", # Proton Experimental - "3127680", # FEX - "4183110", # Steam Linux Runtime 4.0 - "4185400", # Steam Linux Runtime 4.0 - Arm64 - "4427310", # Proton Experimental (ARM64) - "4628740", # Proton 11.0 (ARM64) + "228980", + "1070560", + "1391110", + "1628350", + "1493710", + "3127680", + "4183110", + "4185400", + "4427310", + "4628740", )) @@ -1299,8 +1147,6 @@ def build_tool(tool_dir, depot, sources=SOURCES, name=TOOL): if os.path.islink(tool_dir): os.unlink(tool_dir) elif os.path.isdir(tool_dir) and set(os.listdir(tool_dir)) - set(OWN_FILES): - # Earlier versions mirrored the depot in here as symlinks; start clean. A directory holding - # only our own files is kept, and each file rewritten only when it changed. shutil.rmtree(tool_dir) os.makedirs(tool_dir, exist_ok=True) write_changed(os.path.join(tool_dir, "toolmanifest.vdf"), TOOL_MANIFEST_VDF) diff --git a/tools/linuxfs/preload/binfmt.c b/tools/linuxfs/preload/binfmt.c index 6592a8d5e..7aee2ee2f 100644 --- a/tools/linuxfs/preload/binfmt.c +++ b/tools/linuxfs/preload/binfmt.c @@ -1,11 +1,3 @@ -/* - * binfmt_misc for the desktop, which proot does not have: an exec of an x86 Linux program, a Windows - * program, or a program or script on noexec shared storage goes to droiddeck-open --exec, which runs - * it under FEX, Proton or its interpreter. x86 and Windows programs are caught before the exec: proot - * loads a static x86 program as a native one, which dies on its first instruction, and execvp runs a - * Windows program as a shell script. A program on noexec storage also passes access(X_OK), which KIO - * and Qt ask before they start one. Only in the desktop session. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <elf.h> @@ -27,7 +19,6 @@ typedef int (*spawn_fn)(pid_t *, const char *, const posix_spawn_file_actions_t *, const posix_spawnattr_t *, char *const[], char *const[]); -/* BINFMT_TEST: tools/tests builds it for the host, whose own x86 programs stand in for foreign ones. */ #if defined(__aarch64__) || defined(BINFMT_TEST) static int desktop(void) { @@ -35,7 +26,6 @@ static int desktop(void) { return value && value[0] == '1' && value[1] == 0; } -/* 1: x86 program or noexec storage, 2: Windows program, 3: script; 0 for anything else. */ static int foreign(const char *path, int error) { if (!path || !strcmp(path, BINFMT_HANDLER)) return 0; int fd = open(path, O_RDONLY | O_CLOEXEC); @@ -52,7 +42,6 @@ static int foreign(const char *path, int error) { } else if (n >= 2 && head[0] == '#' && head[1] == '!' && error == EACCES) { kind = 3; } - /* What the kernel refuses for its permissions stays refused; noexec storage has no execute bits to give. */ struct statvfs vfs; int allowed = kind && (((st.st_mode & 0111) && error != EACCES) || (fstatvfs(fd, &vfs) == 0 && (vfs.f_flag & ST_NOEXEC))); close(fd); @@ -63,7 +52,6 @@ static int wanted(const char *path, int error) { return (error == ENOEXEC || error == ENOENT || error == EACCES) && desktop() && foreign(path, error); } -/* The program on PATH as execvp would find it, into [out]. */ static int lookup(const char *file, char *out, size_t size) { if (!file || !*file) return 0; if (strchr(file, '/')) return snprintf(out, size, "%s", file) < (int)size; @@ -111,19 +99,16 @@ static int spawn_handler(pid_t *pid, const char *path, const posix_spawn_file_ac return real(pid, BINFMT_HANDLER, fa, attr, args, envp); } -/* Before an exec of [path]: an x86 or Windows program goes to the handler instead. */ __attribute__((visibility("hidden"))) void bl_binfmt_before(const char *path, char *const argv[], char *const envp[]) { if (desktop() && foreign(path, ENOEXEC)) hand_off(path, argv, envp); } -/* After a failed exec of [path]: the handler takes it over, or the exec's own error stands. */ __attribute__((visibility("hidden"))) int bl_binfmt_retry(const char *path, char *const argv[], char *const envp[], int error) { if (wanted(path, error)) hand_off(path, argv, envp); errno = error; return -1; } -/* The same around execvp, for the program it finds on PATH. */ __attribute__((visibility("hidden"))) void bl_binfmt_before_search(const char *file, char *const argv[], char *const envp[]) { char path[PATH_MAX]; if (desktop() && lookup(file, path, sizeof path)) bl_binfmt_before(path, argv, envp); @@ -136,7 +121,6 @@ __attribute__((visibility("hidden"))) int bl_binfmt_after_search(const char *fil return -1; } -/* After access(X_OK) of [path] was refused (pathcache.c): a program on noexec storage runs all the same. */ __attribute__((visibility("hidden"))) int bl_binfmt_runnable(const char *path, int mode) { if (mode != X_OK || !desktop()) return 0; int runs = foreign(path, EACCES) != 0; diff --git a/tools/linuxfs/preload/dircache.c b/tools/linuxfs/preload/dircache.c index baf0ada12..bdfec14da 100644 --- a/tools/linuxfs/preload/dircache.c +++ b/tools/linuxfs/preload/dircache.c @@ -1,10 +1,3 @@ -/* - * The Steam client lists a whole content directory about ten times per file it creates, moves or - * deletes, to match names case-insensitively. On Android's FUSE storage that made reserving space - * quadratic. Listings under steamapps/ on FUSE are kept for at most DIR_TTL_NS and dropped on any - * inotify event the client's own patched changes don't account for; directory timestamps alone - * can't be trusted, as NTFS volumes report whole seconds. BL_NO_DIR_CACHE=1 turns it off. - */ #define _GNU_SOURCE #include <dirent.h> #include <dlfcn.h> @@ -32,7 +25,6 @@ #define FUSE_MAGIC 0x65735546 #define WATCHED (IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE_SELF | IN_MOVE_SELF | IN_ONLYDIR) -/* The client is 64-bit; 32-bit builds only pass creates through. */ #if __SIZEOF_POINTER__ == 8 _Static_assert(sizeof(struct dirent) == sizeof(struct dirent64), "dirent layouts differ"); @@ -88,7 +80,6 @@ static long long now_ns(void) { return (long long)t.tv_sec * 1000000000LL + t.tv_nsec; } -/* A child shares the parent's inotify queue, so it stops keeping listings. */ static void after_fork(void) { pthread_mutex_init(&lock, NULL); pthread_mutex_init(&oplock, NULL); @@ -162,7 +153,6 @@ static void unref(struct listing *l) { if (--l->refs == 0 && l->detached) destroy(l); } -/* The rest is called with lock held. */ static void release(int wd) { if (wd < 0 || notify < 0) return; @@ -179,7 +169,6 @@ static void detach(struct listing *l) { if (l->refs == 0) destroy(l); } -/* Takes the listing out of the table; freed now or when its last reader closes. */ static void drop(int slot) { struct listing *l = table[slot]; int wd = l->wd; @@ -195,7 +184,6 @@ static void changed(int wd) { if (handles[i] && (handles[i]->wd == wd || wd < 0)) __atomic_store_n(&handles[i]->dirty, 1, __ATOMIC_RELAXED); } -/* Applies queued events; the expected ones are the client's own changes, already patched in. */ static void pump(struct expect *x, int nx) { char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event)))); if (notify < 0 || forked) return; @@ -302,7 +290,6 @@ static DIR *wrap(struct handle *h) { #define RECLEN(namelen) ((offsetof(struct dirent64, d_name) + (namelen) + 1 + 7) & ~(size_t)7) -/* The listing as packed dirent64 records, built once per version. */ static int records(struct listing *l) { if (l->recs) return 0; size_t len = 0; @@ -326,7 +313,6 @@ static int records(struct listing *l) { return 0; } -/* Lock-free from here on unless noted: a stream is only ever used by one thread at a time. */ static struct handle *lookup(DIR *d) { return registered(d) >= 0 ? (struct handle *)d : NULL; @@ -356,7 +342,6 @@ REAL(symlink); REAL(readdir64_r); REAL(readdir_r); -/* Resolved before the client starts threads; the calls below only fill gaps for earlier callers. */ __attribute__((constructor)) static void resolve(void) { NEXT(opendir); NEXT(readdir64); @@ -378,7 +363,6 @@ __attribute__((constructor)) static void resolve(void) { } #pragma GCC diagnostic pop -/* A reader of the kept listing when it is still current. */ static DIR *hit(const struct stat *st, struct handle *h, long long now) { pthread_mutex_lock(&lock); if (!changing) pump(NULL, 0); @@ -401,7 +385,6 @@ static DIR *hit(const struct stat *st, struct handle *h, long long now) { return NULL; } -/* Registers the stream and its watch before anything is read, so no change goes unseen. */ static int track(struct handle *h, const char *path) { pthread_mutex_lock(&lock); h->wd = watch(path); @@ -411,7 +394,6 @@ static int track(struct handle *h, const char *path) { return d != NULL; } -/* Reads through the real stream, keeping what it returns. */ static void filling(struct handle *h, DIR *real, const struct stat *st, long long now) { h->real = real; h->fill = calloc(1, sizeof(*h->fill)); @@ -625,7 +607,6 @@ static int watching(const char *path) { return eligible(path) && __atomic_load_n(&cached_listings, __ATOMIC_ACQUIRE) > 0; } -/* Called with oplock held: the client's tracked changes happen one at a time. */ static void begin(void) { pthread_mutex_lock(&lock); pump(NULL, 0); @@ -639,7 +620,6 @@ static void before(struct side *s) { enum { ADDED, REMOVED }; -/* Patches the listing after the client's own change, or drops it when unsure. Called with lock held. */ static int patch(struct side *s, int what, uint32_t event, int ok, int error, const char *target, struct expect *x) { struct stat now; if (!s->known) return 0; @@ -795,7 +775,6 @@ ONE_PATH(remove, REMOVED, IN_DELETE, next_remove(path)) return r; \ } -/* A refused hard link to a lock file is taken as a symlink (links.c). */ int bl_link_lock(const char *oldpath, const char *newpath, int result) __attribute__((visibility("hidden"))); static int link_or_lock(const char *from, const char *to) { return bl_link_lock(from, to, next_link(from, to)); } diff --git a/tools/linuxfs/preload/drm.c b/tools/linuxfs/preload/drm.c index bb4374be0..101512387 100644 --- a/tools/linuxfs/preload/drm.c +++ b/tools/linuxfs/preload/drm.c @@ -1,10 +1,3 @@ -/* - * GEM handle emulation for the KGSL device presented as a DRM render node. - * - * Compositors and Mesa validate or convert dma-bufs with PRIME ioctls on the render node; KGSL - * has no GEM. Handles only serve as tokens to these callers, so a table of duplicated dma-buf - * descriptors stands in for the kernel's, and every other device is left to libdrm. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -19,7 +12,6 @@ #define MAX_HANDLES 4096 -/* DRM_IOCTL_GEM_CLOSE from drm.h: _IOW('d', 0x09, struct drm_gem_close { u32 handle, pad; }). */ struct gem_close { uint32_t handle; uint32_t pad; @@ -36,7 +28,7 @@ static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; static struct handle_entry handle_entries[MAX_HANDLES]; static uint32_t next_handle = 1; static dev_t kgsl_dev; -static int kgsl_state; /* 0 unknown, 1 known, -1 absent */ +static int kgsl_state; static int is_kgsl(int fd) { struct stat st; @@ -63,7 +55,7 @@ static int same_file_description(int a, int b) { } #ifdef SYS_kcmp int saved_errno = errno; - long ret = syscall(SYS_kcmp, getpid(), getpid(), 0 /* KCMP_FILE */, a, b); + long ret = syscall(SYS_kcmp, getpid(), getpid(), 0 , a, b); errno = saved_errno; return ret == 0; #else @@ -81,11 +73,6 @@ static int find_handle_locked(int fd, uint32_t handle) { return -1; } -/* - * The old table used its slot index as the public handle. Once #275 started freeing slots, - * rapidly recreated swapchains immediately reused those numbers. Keep storage slots reusable but - * make the public token monotonic so a stale handle can fail instead of aliasing a newer buffer. - */ static uint32_t alloc_handle_locked(void) { for (int tries = 0; tries <= MAX_HANDLES; tries++) { uint32_t candidate = next_handle++; @@ -157,7 +144,6 @@ int drmPrimeHandleToFD(int fd, uint32_t handle, uint32_t flags, int *prime_fd) { return ret; } -/* Drops only a handle belonging to this KGSL file description. */ static int release_handle(int fd, uint32_t handle) { int found = 0; pthread_mutex_lock(&lock); @@ -179,16 +165,6 @@ int drmCloseBufferHandle(int fd, uint32_t handle) { return release_handle(fd, handle) ? 0 : -EINVAL; } -/* - * Zink gives its handles back with the GEM_CLOSE ioctl itself, not drmCloseBufferHandle - * (zink_bo.c, bo_destroy). KGSL refuses the ioctl, so the duplicated descriptor stayed open and - * kept the whole buffer alive. Preserve #275's reclamation without letting another KGSL file - * description release the token or making a new buffer immediately inherit a retired token. - * - * Caught at ioctl() (netif.c), which libdrm's drmIoctl calls, rather than by exporting drmIoctl: - * every session process preloads this library, and exporting drmIoctl from it brought on a - * judder replaying old frames in No Man's Sky on an Adreno 840 even with no handle released. - */ __attribute__((visibility("hidden"))) int bl_drm_gem_close(int fd, unsigned long request, void *arg, int *rc) { if (request != GEM_CLOSE || !arg || !is_kgsl(fd)) { return 0; @@ -202,12 +178,6 @@ __attribute__((visibility("hidden"))) int bl_drm_gem_close(int fd, unsigned long return 1; } -/* - * fork() carries over only the calling thread, so a lock another thread was holding at that - * instant stays held in the child by a thread that is not there to release it. Taking it before - * the fork makes the copy consistent; the parent then unlocks it and the child, whose one thread - * never locked it, gets a fresh one. (WinNative 79aa7f68.) - */ static void drm_lock_before_fork(void) { pthread_mutex_lock(&lock); } static void drm_unlock_after_fork(void) { pthread_mutex_unlock(&lock); } static void drm_reset_after_fork(void) { pthread_mutex_init(&lock, NULL); } diff --git a/tools/linuxfs/preload/fsync.c b/tools/linuxfs/preload/fsync.c index 844af92b8..dfcb06979 100644 --- a/tools/linuxfs/preload/fsync.c +++ b/tools/linuxfs/preload/fsync.c @@ -1,6 +1,3 @@ -/* droiddeck-fsync: futex_waitv(2) in userspace, so Proton's own fsync runs where the app sandbox refuses the syscall. - * Credit: fsync, the futex-based synchronization in Proton, by Elizabeth Figura and Paul Gofman; - * futex_waitv(2) by André Almeida. */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> diff --git a/tools/linuxfs/preload/inputudev.c b/tools/linuxfs/preload/inputudev.c index b4ff0b3ef..2de0ef908 100644 --- a/tools/linuxfs/preload/inputudev.c +++ b/tools/linuxfs/preload/inputudev.c @@ -1,34 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with names - * changed to this project's. The reasoning below is the original author's. - * - * The session's controllers, described to libudev's callers. - * - * The pads are evdev nodes the app backs with its own input rings, bound over /dev/input. Wine's - * HID bus finds them there, but it identifies a node by resolving /sys/class/input/<name> and - * asking libudev about the result: on Android that resolves to whatever real input device the - * host numbered the same, whose device node libudev cannot read, so every pad is discarded before - * its identity is ever looked at. The fallback is Wine's SDL bus, and SDL enumerates through udev - * too, over a netlink socket the app sandbox refuses. A game was left with no controller at all - * while the Steam client, which reads the nodes itself, had one. - * - * So the pads are described here instead: the resolved path is answered for the session's own - * nodes, and libudev's accessors answer for that path from what the app already knows. They carry - * the identity of the pad Steam Input would present if it could - it has no /dev/uinput here to - * make one - which is the one identity Wine's own bus keeps rather than handing on to SDL. - * - * Only Wine's device host is answered, and only for the session's own nodes: the Steam client - * enumerates the same nodes for itself and would take a pad wearing Steam Input's identity for one - * of its own and ignore it. Every other caller, path and device object goes straight through. - * - * With libfakeinput's /dev/uinput stand-in (FAKE_EVDEV_UINPUT=1) Steam Input's own virtual pads - * exist too, as event16 and up, and those are what Wine is meant to read: each is described with - * the identity the client gave it, from the .uevent file libfakeinput writes beside the node. - * While one exists the app's pads are described as what they are, an Xbox 360 controller, which - * Wine's HID bus leaves alone - the game reads Steam Input's output, not the pad under it, as on - * a Steam Deck. With none (Steam Input off for the game) they keep the virtual identity, so the - * game still has a controller. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE #include <dlfcn.h> #include <limits.h> @@ -39,20 +12,16 @@ #include <sys/sysmacros.h> #include <sys/types.h> -/* The app opens one ring per configured controller; four is what its launcher offers. Nodes from - * UINPUT_BASE up are the virtual pads made through libfakeinput's /dev/uinput stand-in. */ #define RING_MAX 4 #define UINPUT_BASE 16 #define PAD_MAX 64 #define EVENT_MINOR_BASE 64 -/* BUS_VIRTUAL, and the identity Steam Input gives the gamepad it presents to a game. */ #define PAD_UEVENT_FORMAT \ "PRODUCT=6/28de/11ff/110\n" \ "NAME=\"Generic HID Gamepad %d\"\n" \ "PHYS=\"usb-fakeinput/input%d\"\n" -/* The identity libfakeinput gives the pad itself (FAKE_EVDEV_IDENTITY=xbox360). */ #define PAD_PHYSICAL_UEVENT_FORMAT \ "PRODUCT=3/45e/28e/110\n" \ "NAME=\"Xbox 360 Controller (%d)\"\n" \ @@ -70,11 +39,6 @@ static struct pad pads[PAD_MAX]; static int pad_count; static pthread_mutex_t virtual_lock = PTHREAD_MUTEX_INITIALIZER; -/* - * The slots the app published rings for, named in FAKE_EVDEV_MEMFD_PATHS as "<slot>=<path>" - * separated by semicolons. Without it the process has no pads of the session's and every call - * here is a passthrough. - */ static void discover_pads(void) { const char *spec = getenv("FAKE_EVDEV_MEMFD_PATHS"); const char *entry; @@ -113,7 +77,6 @@ static int uinput_enabled(void) { return enabled != NULL && atoi(enabled) != 0; } -/* Reads the .uevent libfakeinput wrote for virtual pad <node> into buf; 0 when there is none. */ static int read_virtual_uevent(int node, char *buf, size_t size) { const char *dir = getenv("FAKE_EVDEV_DIR"); char path[PATH_MAX]; @@ -138,10 +101,6 @@ static int any_virtual_pad(void) { return 0; } -/* - * Brings the entry for event<node> up to date before Wine resolves it: a virtual pad comes and - * goes with the client, and the app's pads change identity with it. - */ static void refresh_pad(int node) { struct pad *pad; @@ -168,7 +127,6 @@ static void refresh_pad(int node) { pthread_mutex_unlock(&virtual_lock); } -/* The pad a synthetic syspath names, or NULL when the path is not one of ours. */ static struct pad *pad_for_syspath(const char *syspath) { int i; @@ -180,7 +138,6 @@ static struct pad *pad_for_syspath(const char *syspath) { return NULL; } -/* A device object is ours exactly when it is one of the pads. */ static struct pad *pad_for_device(void *device) { int i; @@ -192,7 +149,6 @@ static struct pad *pad_for_device(void *device) { return NULL; } -/* The pad /sys/class/input/<name> stands for, or NULL when the path names something else. */ static struct pad *pad_for_syslink(const char *path) { static const char prefix[] = "/sys/class/input/"; const char *name; @@ -213,7 +169,6 @@ static void *next_symbol(const char *name) { return dlsym(RTLD_NEXT, name); } char *bl_cached_realpath(const char *path, char *resolved, char *(*real)(const char *, char *)) __attribute__((visibility("hidden"))); -/* Wine reaches libudev from its HID bus driver, which is the only caller these pads are for. */ static int caller_is_winebus(void *caller) { Dl_info info; @@ -228,7 +183,6 @@ char *realpath(const char *path, char *resolved) { if (pad != NULL) { if (resolved == NULL) return strdup(pad->syspath); - /* PATH_MAX is what the caller promises when it passes a buffer. */ memcpy(resolved, pad->syspath, strlen(pad->syspath) + 1); return resolved; } @@ -336,10 +290,6 @@ dev_t udev_device_get_devnum(void *device) { return real_get(device); } -/* - * A pad has no bus behind it, so it stands in for its own input device and has no other parent. - * The reference is a borrowed one either way. - */ void *udev_device_get_parent_with_subsystem_devtype(void *device, const char *subsystem, const char *devtype) { static void *(*real_get)(void *, const char *, const char *); diff --git a/tools/linuxfs/preload/kgslid.c b/tools/linuxfs/preload/kgslid.c index 3dee629d1..c909f4618 100644 --- a/tools/linuxfs/preload/kgslid.c +++ b/tools/linuxfs/preload/kgslid.c @@ -1,16 +1,3 @@ -/* - * Adreno revisions the runtime's Turnip does not know, reported as the revision it does. - * - * Retail SM8850 phones report their A840 as chip_id 0x44050A21 (KGSL gpu_model "Adreno840v2"). - * Mesa only lists 0x44050A31, so the runtime's glibc Turnip refuses the device outright - * ("device (chip_id = 44050A21, gpu_id = 6860) is unsupported") and gamescope exits before Steam - * starts. Banners-Turnip fixes its own builds by registering the v2 id on the same A840 GPU info - * (patches/a840v2.py); answering KGSL's device-info query with the known id has the same effect - * on any Turnip in the session, including the one the runtime was built with. A driver that - * already knows the v2 id resolves the rewritten one to the same entry. - * - * Only the KGSL device-info property is touched. Its layout is the kernel's uapi (msm_kgsl.h). - */ #define _GNU_SOURCE 1 #include <stddef.h> #include <sys/ioctl.h> @@ -38,7 +25,7 @@ static const struct { unsigned int reported; unsigned int known; } CHIP_IDS[] = { - {0x44050A21, 0x44050A31}, /* Adreno 840v2 -> Adreno 840 */ + {0x44050A21, 0x44050A31}, }; void bl_kgsl_chip_id_fixup(unsigned long request, void *arg, int rc) { diff --git a/tools/linuxfs/preload/kgslpoll.c b/tools/linuxfs/preload/kgslpoll.c index 00eb8f01f..841c049ee 100644 --- a/tools/linuxfs/preload/kgslpoll.c +++ b/tools/linuxfs/preload/kgslpoll.c @@ -1,19 +1,3 @@ -/* - * A zero-timeout KGSL timestamp wait answered as the poll it was meant to be. - * - * Turnip's KGSL backend emulates timeline semaphores with timestamps, and asks "has this one - * retired yet?" by waiting on it with IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID and a timeout of 0. - * KGSL reads a timeout of 0 as no timeout at all, so the poll blocks until the GPU gets there. - * Mesa's timeline bookkeeping polls like that under a lock on every signalling submit, which put - * vkd3d-proton, DXVK, gamescope and Zink in lock-step with the GPU: one frame queued, never two. - * Found and measured by Bannerlator (PROGRESS_LOG 2026-09-29: a D3D12 demo went from 614 to - * 4335 fps with the same answer given in its Vulkan adapter); the driver-side fix is on - * Banners-Turnip, and this covers the drivers that do not carry it yet. - * - * The answer comes from the context's retired timestamp, compared the way KGSL compares them - * (with wrap-around). Anything the read does not answer - an unknown context, a device that is not - * KGSL - goes to the kernel as before. BL_NO_KGSL_POLL_FIX=1 turns it off. - */ #define _GNU_SOURCE #include <errno.h> #include <stdint.h> @@ -50,7 +34,6 @@ static int enabled(void) { return cached; } -/* True when the call was answered here, with its result in *rc (and errno set on failure). */ __attribute__((visibility("hidden"))) int bl_kgsl_poll(int (*real)(int, unsigned long, void *), int fd, unsigned long request, void *arg, int *rc) { diff --git a/tools/linuxfs/preload/links.c b/tools/linuxfs/preload/links.c index 7a963205b..350990b61 100644 --- a/tools/linuxfs/preload/links.c +++ b/tools/linuxfs/preload/links.c @@ -1,22 +1,3 @@ -/* - * Lock files taken by hard link, which Android denies apps. - * - * libICE and libXau lock an authority file the classic way: create "<file>-c", then link() it to - * "<file>-l" - the link either appears or fails because the other holder's is there. Android's - * SELinux policy refuses hard links in an app's data, so the link fails for everyone, the lock is - * never taken, and Plasma's session manager gives up at "Could not lock ICEAuthority file". With - * no session manager there is no Plasma desktop. - * - * Only that convention is answered here: when the real link is refused and the new name ends in - * "-l", a symlink stands in. Creating a symlink fails on an existing name just as a link does, so - * the lock excludes as before, and the holder removes both names when it is done. Every other - * link keeps the real answer: a program that links a file into place and then deletes the - * original would be left with a dangling symlink. - * - * That pattern itself is answered for one caller that asks: with DROIDDECK_LINK_RENAME=1 (the - * session sets it for localedef, which names a finished locale archive that way) a refused link - * becomes a rename that will not replace an existing name, which is all the link promised there. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -38,9 +19,7 @@ static int lock_name(const char *path) { return n > 2 && strcmp(path + n - 2, "-l") == 0; } -/* The answer to link(oldpath, newpath), given the real call's: a refused lock link becomes a symlink. */ __attribute__((visibility("hidden"))) int bl_link_lock(const char *oldpath, const char *newpath, int result) { - /* libc's own symlink: dircache.c's wrapper may be holding its lock around this call. */ static int (*real_symlink)(const char *, const char *); if (result == 0 || !refused(errno)) return result; if (moves_instead()) return renameat2(AT_FDCWD, oldpath, AT_FDCWD, newpath, RENAME_NOREPLACE); @@ -49,7 +28,6 @@ __attribute__((visibility("hidden"))) int bl_link_lock(const char *oldpath, cons return real_symlink(oldpath, newpath); } -/* 64-bit builds wrap link() in dircache.c, which hands its result to bl_link_lock. */ #if __SIZEOF_POINTER__ != 8 int link(const char *oldpath, const char *newpath) { static int (*real_link)(const char *, const char *); @@ -64,7 +42,6 @@ int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int result = real_linkat(olddirfd, oldpath, newdirfd, newpath, flags); if (result != 0 && refused(errno) && flags == 0 && moves_instead()) return renameat2(olddirfd, oldpath, newdirfd, newpath, RENAME_NOREPLACE); - /* A relative old name would resolve against the new name's directory as a symlink's target. */ if (result != 0 && refused(errno) && lock_name(newpath) && oldpath[0] == '/' && flags == 0) return symlinkat(oldpath, newdirfd, newpath); return result; diff --git a/tools/linuxfs/preload/lock.c b/tools/linuxfs/preload/lock.c index a11d024e2..b802fded6 100644 --- a/tools/linuxfs/preload/lock.c +++ b/tools/linuxfs/preload/lock.c @@ -1,24 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with names - * changed to this project's. The reasoning below is the original author's. - * - * File locking for the game library on external storage. - * - * The user's games are bound in from /storage/emulated, which Android serves over FUSE, and that - * filesystem implements no locking at all: flock() and fcntl()'s lock commands answer ENOSYS for - * every file under it. The Steam client takes a shared lock on each content file as it validates - * or updates a title and treats the refusal as a failure, so a download walks its progress back - * and forth instead of finishing; a game reading its own data hits the same wall. - * - * Where the kernel can lock, it does: the real call runs first and its answer stands, so files on - * the app's own f2fs storage keep genuine cross-process exclusion. Only the filesystem that cannot - * lock at all is answered here, and granting is the honest reading of it - a filesystem with no - * lock state has nothing to contend over, and the one other party that could contend is inside - * this same session. Reporting failure instead buys no safety and costs the download. - * - * F_GETLK is the exception: it asks who holds a lock, and on a filesystem holding none the true - * answer is "nobody", so it is reported unlocked rather than granted. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -28,7 +11,6 @@ #include <sys/file.h> #include <unistd.h> -/* The filesystem refuses the whole operation, rather than declining this particular lock. */ static int unsupported(int error) { return error == ENOSYS || error == ENOTSUP || error == EOPNOTSUPP; } @@ -44,12 +26,6 @@ int flock(int fd, int operation) { return result; } -/* - * fcntl()'s third argument is an int, a pointer, or absent depending on the command; only the - * lock commands are of interest here and all of those take a struct flock *. Anything else is - * passed straight through as a void * - wide enough for both an int and a pointer on every ABI - * the runtime is built for. - */ static int is_lock_command(int cmd) { switch (cmd) { case F_SETLK: @@ -104,6 +80,5 @@ int fcntl(int fd, int cmd, ...) { } #if defined(__GLIBC__) && defined(F_SETLK64) -/* glibc's large-file build redirects both names; the runtime is 64-bit, so they are the same call. */ int fcntl64(int fd, int cmd, ...) __attribute__((alias("fcntl"))); #endif diff --git a/tools/linuxfs/preload/net.c b/tools/linuxfs/preload/net.c index 636ba1aea..bc9b25afe 100644 --- a/tools/linuxfs/preload/net.c +++ b/tools/linuxfs/preload/net.c @@ -1,11 +1,3 @@ -/* - * Loopback socket ownership for Steam's IPC peer check. - * - * Steam validates a websocket peer by running lsof to find which process owns a 127.0.0.1 port. - * The Android sandbox denies both /proc/net/tcp and sock_diag, so no tool can answer. Every Steam - * process is preloaded with this library, so each records the ports it binds and connects, and the - * lsof Steam spawns is answered from that shared registry instead of being run. - */ #define _GNU_SOURCE #include <arpa/inet.h> #include <spawn.h> @@ -54,8 +46,6 @@ static int loopback_family(const struct sockaddr *address, socklen_t length) { return 0; } -/* Record that this process owns a loopback socket on its local port; peer is the far end's port, - * 0 for a listening socket. */ static void record_port(int fd, unsigned peer) { struct sockaddr_storage ss; socklen_t len = sizeof(ss); @@ -119,7 +109,6 @@ typedef int (*connect_fn)(int, const struct sockaddr *, socklen_t); typedef int (*accept4_fn)(int, struct sockaddr *, socklen_t *, int); static unsigned guarded_port(void); -/* udevmon.c: a stand-in for the netlink socket the sandbox refuses, bound here without the kernel. */ __attribute__((visibility("hidden"))) int bl_udevmon_stand_in(int fd); int bind(int fd, const struct sockaddr *addr, socklen_t len) { @@ -239,7 +228,6 @@ int connect(int fd, const struct sockaddr *addr, socklen_t len) { if (peer) record_port(fd, peer); int ret = real(fd, addr, len); if (ret == 0 || errno == EINPROGRESS) { - /* The caller goes on to test for EINPROGRESS, which the bookkeeping must not disturb. */ int saved = errno; record_port(fd, peer); errno = saved; @@ -267,7 +255,6 @@ int accept(int fd, struct sockaddr *addr, socklen_t *len) { return accept4(fd, addr, len, 0); } -/* binfmt.c: an exec of a program the kernel cannot run, handed to droiddeck-open in the desktop. */ extern char **environ; typedef int (*spawn_fn)(pid_t *, const char *, const posix_spawn_file_actions_t *, const posix_spawnattr_t *, char *const[], char *const[]); @@ -279,8 +266,6 @@ __attribute__((visibility("hidden"))) int bl_binfmt_spawn(pid_t *pid, const char const posix_spawnattr_t *attr, char *const argv[], char *const envp[], spawn_fn real); -/* lsof would read the denied /proc/net/tcp; answer its -i TCP@host:port query from the registry - * instead of executing it. Recognised only when invoked as lsof with that query. */ static int maybe_answer_lsof(const char *path, char *const argv[]) { const char *base = strrchr(path, '/'); base = base ? base + 1 : path; @@ -302,7 +287,7 @@ static int maybe_answer_lsof(const char *path, char *const argv[]) { snprintf(path_buf, sizeof(path_buf), "%s/p%u", net_dir(), (unsigned)atoi(port_str)); int in = open(path_buf, O_RDONLY); if (in < 0) { - _exit(1); /* lsof exit code for "nothing found" */ + _exit(1); } char buf[64]; int n = read(in, buf, sizeof(buf) - 1); @@ -314,14 +299,12 @@ static int maybe_answer_lsof(const char *path, char *const argv[]) { int pid = 0, ppid = 0, uid = 0; unsigned peer = 0; sscanf(buf, "%d %d %d %u", &pid, &ppid, &uid, &peer); - /* Steam splits the address on "->" and wants exactly two halves, taking the port it asked - * about from the left one; only a listening socket has no far end. */ char out[160]; int m = peer ? snprintf(out, sizeof(out), "p%d\nR%d\nu%d\nn127.0.0.1:%u->127.0.0.1:%u\n", pid, ppid, uid, (unsigned)atoi(port_str), peer) : snprintf(out, sizeof(out), "p%d\nR%d\nu%d\nn127.0.0.1:%u\n", pid, ppid, uid, (unsigned)atoi(port_str)); - if (write(1, out, m) != m) { /* best effort */ } + if (write(1, out, m) != m) { } _exit(0); } @@ -334,8 +317,6 @@ int execve(const char *path, char *const argv[], char *const envp[]) { return bl_binfmt_retry(path, argv, envp, errno); } -/* popen(3) and posix_spawn(3) reach lsof without going through the execve symbol above. The - * check is cheap, so a spawned lsof is answered here too by handling it in a short-lived child. */ static int answer_lsof_via_spawn(const char *path, char *const argv[], pid_t *pid) { pid_t child = fork(); if (child < 0) { diff --git a/tools/linuxfs/preload/netif.c b/tools/linuxfs/preload/netif.c index acdc6e66d..bcd12e924 100644 --- a/tools/linuxfs/preload/netif.c +++ b/tools/linuxfs/preload/netif.c @@ -1,16 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with names - * changed to this project's. The reasoning below is the original author's. - * - * Network interfaces for a sandbox that hides them. - * - * Android denies an app the rtnetlink dump behind getifaddrs() and if_nameindex(), the hardware - * address ioctl, and every table under /proc/net. Wine builds its adapter, address, route and - * neighbour tables from exactly those, so a game saw a machine with no adapter, no address, no - * gateway and no MAC, and one that checks its adapters before going online waited for good. - * The app knows the real link from ConnectivityManager and writes it to /etc/droiddeck-net; - * whatever the kernel refuses is answered from that file. Anything the kernel does answer wins. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE 1 #include <arpa/inet.h> #include <dlfcn.h> @@ -108,9 +99,6 @@ static int fresh(long long *at) { return then != 0 && now_ns() - then < FRESH_NS; } -/* Wine's network driver reads the link several times for each table it builds, and some games - * have it build them all hundreds of times a second. The app rewrites the file when the device - * changes network, so a reading stands for a second. */ static pthread_mutex_t link_lock = PTHREAD_MUTEX_INITIALIZER; static struct link link_read; static int link_ok; @@ -128,7 +116,6 @@ static int load_link(struct link *link) { return ok; } -/* See drm.c: a fork must not leave the lock held by a thread the child does not have. */ static void link_lock_before_fork(void) { pthread_mutex_lock(&link_lock); } static void link_unlock_after_fork(void) { pthread_mutex_unlock(&link_lock); } static void link_reset_after_fork(void) { pthread_mutex_init(&link_lock, NULL); } @@ -137,8 +124,6 @@ __attribute__((constructor)) static void install_link_fork_handlers(void) { pthread_atfork(link_lock_before_fork, link_unlock_after_fork, link_reset_after_fork); } -/* When the kernel's own list was of no use, it is not asked again for a second: each asking binds - * a netlink socket, a stop in the tracer, only to be refused again. */ static long long ifaddrs_unusable_at, nameindex_unusable_at; static void prefix_mask(int family, unsigned prefix, unsigned char *out) { @@ -152,8 +137,6 @@ static int is_link_local(const struct link_addr *addr) { return addr->family == AF_INET6 && addr->bytes[0] == 0xfe && (addr->bytes[1] & 0xc0) == 0x80; } -/* One allocation per list. ifa_data of the head points at the head itself, which glibc never - * does, so freeifaddrs() can tell the two kinds of list apart without keeping any state. */ struct entry { struct ifaddrs ifa; struct sockaddr_storage addr, mask, extra; @@ -246,7 +229,6 @@ int getifaddrs(struct ifaddrs **out) { if (asked) __atomic_store_n(&ifaddrs_unusable_at, now_ns(), __ATOMIC_RELAXED); if (rc == 0 && list) real_freeifaddrs(list); - /* A leading slot keeps fill_entry()'s back link in bounds; the list starts after it. */ struct entry *block = calloc((size_t) link.addr_count + 5, sizeof(*block)); if (block == NULL) { errno = ENOMEM; @@ -277,7 +259,6 @@ void freeifaddrs(struct ifaddrs *list) { real_freeifaddrs(list); } -/* Names live in the same allocation as the array, right behind it; glibc strdup()s each one. */ struct name_block { struct if_nameindex index[3]; char names[2][IFNAMSIZ]; @@ -353,7 +334,6 @@ char *if_indextoname(unsigned int index, char name[IF_NAMESIZE]) { static int mac_unusable(const struct ifreq *ifr) { const unsigned char *a = (const unsigned char *) ifr->ifr_hwaddr.sa_data; if (ifr->ifr_hwaddr.sa_family != ARPHRD_ETHER) return 1; - /* Android hands out 02:00:00:00:00:00 where it hides the real address. */ int rest = a[1] | a[2] | a[3] | a[4] | a[5]; return (a[0] & 0x03) || (rest == 0 && (a[0] == 0 || a[0] == 0x02)); } @@ -420,15 +400,13 @@ static int is_link_query(unsigned long request) { || request == SIOCGIFBRDADDR; } -void bl_kgsl_chip_id_fixup(unsigned long request, void *arg, int rc); /* kgslid.c */ +void bl_kgsl_chip_id_fixup(unsigned long request, void *arg, int rc); #include "../../quest/kgsl_compat.h" int bl_kgsl_poll(int (*real)(int, unsigned long, void *), int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden"))); int bl_ntsync_ioctl(int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden"))); int bl_drm_gem_close(int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden"))); -/* Every DRM and evdev call comes through here, so the lookup is done once. Two threads racing - * to store it store the same pointer. */ int ioctl(int fd, unsigned long request, ...) { typedef int (*ioctl_fn)(int, unsigned long, void *); static ioctl_fn real; @@ -538,7 +516,6 @@ static const char *denied_table(const char *path) { return NULL; } -/* A memfd rather than fmemopen(): the stream owns its contents, so fclose() frees everything. */ static FILE *synthesize(const char *table) { struct link link; if (!load_link(&link)) return NULL; @@ -558,7 +535,6 @@ static FILE *synthesize(const char *table) { return out; } -/* tracer.c */ int bl_status_without_tracer(const char *path, int flags) __attribute__((visibility("hidden"))); static FILE *open_stream(fopen_fn real, const char *path, const char *mode) { diff --git a/tools/linuxfs/preload/noexec.c b/tools/linuxfs/preload/noexec.c index 26aa539b7..0b0326bda 100644 --- a/tools/linuxfs/preload/noexec.c +++ b/tools/linuxfs/preload/noexec.c @@ -1,9 +1,3 @@ -/* - * noexec.c: FEX runs an x86 program and its libraries where they lie, on noexec shared storage too. - * FEX maps them with the guest's PROT_EXEC, which the kernel refuses for a file on a noexec mount, - * but it never runs those pages itself - it runs its own translation of them - so a mapping or an - * mprotect refused for that alone is made without PROT_EXEC. Only in FEX's own processes. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -12,7 +6,6 @@ #include <sys/statvfs.h> #include <unistd.h> -/* NOEXEC_TEST: tools/tests builds it for the host. */ #if defined(__aarch64__) || defined(NOEXEC_TEST) static int in_fex(void) { @@ -28,7 +21,6 @@ static int in_fex(void) { return known; } -/* After an mmap failed (fsync.c's mmap asks): whether to map it again without PROT_EXEC. */ __attribute__((visibility("hidden"))) int bl_noexec_retry(int prot, int flags, int fd) { int error = errno; struct statvfs vfs; @@ -40,7 +32,6 @@ __attribute__((visibility("hidden"))) int bl_noexec_retry(int prot, int flags, i static int (*real_mprotect)(void *, size_t, int); -/* Looked up before anything runs: Wine calls mprotect from its signal handlers, where dlsym is unsafe. */ __attribute__((constructor)) static void noexec_init(void) { real_mprotect = (int (*)(void *, size_t, int))dlsym(RTLD_NEXT, "mprotect"); } diff --git a/tools/linuxfs/preload/ntsync.c b/tools/linuxfs/preload/ntsync.c index dc21b117f..69f40a145 100644 --- a/tools/linuxfs/preload/ntsync.c +++ b/tools/linuxfs/preload/ntsync.c @@ -1,5 +1,3 @@ -/* droiddeck-ntsync: a userspace implementation of the /dev/ntsync interface for kernels without the driver. - * Credit: ntsync, the kernel driver and its Wine client, by Elizabeth Figura. */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -1243,7 +1241,6 @@ int bl_udevmon_recvmsg(int fd, struct msghdr *msg, int flags, ssize_t (*real_rec ssize_t recvmsg(int fd, struct msghdr *msg, int flags) { static ssize_t (*real)(int, struct msghdr *, int); if (!real) real = (ssize_t (*)(int, struct msghdr *, int))dlsym(RTLD_NEXT, "recvmsg"); - /* udevmon.c's stand-in netlink sockets first: their messages are dressed as udevd's. */ ssize_t answered; if (msg && bl_udevmon_recvmsg(fd, msg, flags, real, &answered)) return answered; ssize_t r = real(fd, msg, flags); diff --git a/tools/linuxfs/preload/opens.c b/tools/linuxfs/preload/opens.c index 2e4a687e8..7c0e766d2 100644 --- a/tools/linuxfs/preload/opens.c +++ b/tools/linuxfs/preload/opens.c @@ -1,29 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with names - * changed to this project's. The reasoning below is the original author's. - * - * The adjustments a session's opens need. - * - * O_NOATIME on the game library: the user's games are bound in from shared storage, which Android - * serves over FUSE with every file owned by its media provider rather than by the app. O_NOATIME - * is reserved for a file's owner, so the kernel refuses such an open outright with EPERM. The - * Steam client opens every content file it reads that way, and treats the refusal as the file - * being unreadable or missing: a validation reports corrupt or missing files and an update is - * cancelled. The flag only asks the filesystem to skip an access-time update; the open is retried - * without it. Every other refusal is passed through untouched, and a file the process may open - * with the flag still is. - * - * The kernel's trace marker: tracefs is world-writable here, so the client's tier0 opens - * /sys/kernel/tracing/trace_marker at startup - three times over, once per module that profiles - - * and writes a "[%s] tgid=%d begin_ctx=%lld" line for every scope it enters. Nothing in the - * session reads those markers, and they are one more thing that can land in the wrong file when - * a descriptor number changes hands (tracer.c has the cause). They have been found appended to an - * app manifest, which the client then cannot parse, to a download's state file, and to content - * chunks, which it quarantines as corrupt. Refused the descriptor, tier0 writes nothing at all and - * has nothing to lose. - * - * A process status is served without the container's tracer in it; see tracer.c. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -55,7 +33,6 @@ static int retry_without_noatime(int fd, int flags) { return fd < 0 && errno == EPERM && (flags & O_NOATIME); } -/* Both spellings tier0 tries, the debugfs one first. */ static int is_trace_marker(const char *path) { return path != NULL && (strcmp(path, "/sys/kernel/tracing/trace_marker") == 0 @@ -83,7 +60,6 @@ int open(const char *path, int flags, ...) { } fd = bl_dircache_create(AT_FDCWD, path, flags, mode, via_open); if (retry_without_noatime(fd, flags)) fd = real_open(path, flags & ~O_NOATIME, mode); - /* A read-only file the Steam client rewrites (perms.c). */ if (fd < 0 && bl_writable_retry(AT_FDCWD, path, flags)) fd = real_open(path, flags & ~O_NOATIME, mode); return fd; } @@ -113,6 +89,5 @@ int openat(int dirfd, const char *path, int flags, ...) { return fd; } -/* The runtime is 64-bit, so the large-file names are the same calls. */ int open64(const char *path, int flags, ...) __attribute__((alias("open"))); int openat64(int dirfd, const char *path, int flags, ...) __attribute__((alias("openat"))); diff --git a/tools/linuxfs/preload/pathcache.c b/tools/linuxfs/preload/pathcache.c index 0aab91bd7..c183eba84 100644 --- a/tools/linuxfs/preload/pathcache.c +++ b/tools/linuxfs/preload/pathcache.c @@ -1,18 +1,3 @@ -/* - * Path lookups the Steam client repeats, answered from a short-lived cache. - * - * While it registers its compatibility tools after logon, the client resolves the same few - * directories - its local tools under compatibilitytools.d and its own platform directory - with - * realpath() and access() thousands of times a second. Each realpath() is one readlinkat() per - * path component, and under proot every one of those is a trapped syscall with a round trip - * through the tracer. Measured on an AYN Thor, that loop was 23 of the client's 31 seconds of - * startup, with proot busy for 16 of them. - * - * Only the client itself uses the cache, never a game or a helper. Only successful answers are - * kept, and only for a moment: a path that stops existing is noticed within PATH_CACHE_TTL_NS, and - * one that starts existing is never hidden, since failures always go to the filesystem. - * BL_NO_PATH_CACHE=1 turns it off. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -152,7 +137,6 @@ static int (*next_faccessat(void))(int, const char *, int, int) { return real; } -/* binfmt.c: a program on noexec storage, which the desktop runs all the same. */ __attribute__((visibility("hidden"))) int bl_binfmt_runnable(const char *path, int mode); static int check_cwd(const char *path, int mode, int flags) { diff --git a/tools/linuxfs/preload/perms.c b/tools/linuxfs/preload/perms.c index 96f1540c2..4ca79d3d4 100644 --- a/tools/linuxfs/preload/perms.c +++ b/tools/linuxfs/preload/perms.c @@ -1,20 +1,3 @@ -/* - * Root's way past a read-only file, for the Steam client. - * - * The client runs as "root" inside the session - the rootfs names the app's own uid root, and - * proot (-i) shows it that uid - but the kernel treats it as the ordinary user it is. The client - * leaves many content files read-only (r-x------, - * ~290 files of Proton Experimental (ARM64) alone), and when an update later rewrites one of them - * it opens the file for writing expecting root's permission to override the mode. The kernel - * refuses (EACCES), the client reports "Missing file permissions" and cancels the update - * (2026-09-26: Proton Experimental (ARM64), "CFileWriter: errno: 13 ... wineopenxr.dll"). - * - * So for the client's own processes, a refused write-open of a file this user owns is answered the - * way root's would be: the owner's write bit is added - to the file, or to the directory a new - * file is being created in - and the open is tried once more. chmod only succeeds on what the app - * owns, so nothing it could not already change is touched. Games, Wine and everything else keep - * ordinary permissions: only the processes started from the client's own directories take this. - */ #define _GNU_SOURCE #include <errno.h> #include <fcntl.h> @@ -28,11 +11,6 @@ static int in_steam_dirs(const char *path) { || strstr(path, "/Steam/ubuntu12_64/") != NULL || strstr(path, "/Steam/linuxarm64/") != NULL); } -/* - * The client and its helpers run from the Steam root's platform directories. The kernel's - * /proc/self/exe names proot's loader for every process in the session, so the program's own - * name - the path it was started by - is asked as well. - */ static int is_steam_client(void) { static int cached = -1; char exe[PATH_MAX]; @@ -52,10 +30,6 @@ static int grant_owner_write(int dirfd, const char *path) { return fchmodat(dirfd, path, (st.st_mode & 07777) | S_IWUSR, 0) == 0; } -/* - * After an open refused with EACCES: returns 1 when a write bit was added and the open is worth - * one more try. errno is left as the open set it. - */ __attribute__((visibility("hidden"))) int bl_writable_retry(int dirfd, const char *path, int flags) { int saved = errno; @@ -67,7 +41,6 @@ int bl_writable_retry(int dirfd, const char *path, int flags) { if (faccessat(dirfd, path, F_OK, 0) == 0) { granted = grant_owner_write(dirfd, path); } else if (flags & O_CREAT) { - /* A new file in a read-only directory. */ slash = strrchr(path, '/'); if (slash == NULL) { granted = grant_owner_write(dirfd, "."); diff --git a/tools/linuxfs/preload/quest_kgsl.c b/tools/linuxfs/preload/quest_kgsl.c index 09037fafe..da5ef79fd 100644 --- a/tools/linuxfs/preload/quest_kgsl.c +++ b/tools/linuxfs/preload/quest_kgsl.c @@ -1,3 +1,2 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ -/* Shared with the Android driver wrapper so the two translation paths cannot drift. */ #include "../../quest/kgsl_compat.c" diff --git a/tools/linuxfs/preload/robust.c b/tools/linuxfs/preload/robust.c index 97bb9eec9..6bbb6f91e 100644 --- a/tools/linuxfs/preload/robust.c +++ b/tools/linuxfs/preload/robust.c @@ -1,11 +1,3 @@ -/* - * get_robust_list(2) for the session's own threads. - * - * Android's app policy refuses the call, and Steam's cross-process mutex insists on seeing the - * head glibc registered. The kernel's list protocol gives it away: a robust mutex this thread - * holds is linked directly behind the registered head, so locking a private one and following - * the link back finds the head without depending on libc's layout. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -48,8 +40,6 @@ static struct robust_list_head *thread_robust_head(void) { if (pthread_mutex_lock(&mutex) != 0) { return NULL; } - /* The mutex holds tids and lock words beside the link, so a word is only followed - * through a read that fails instead of faulting on a non-pointer. */ uintptr_t lo = (uintptr_t)&mutex, hi = lo + sizeof(mutex); for (uintptr_t p = lo; p + sizeof(void *) <= hi && !head; p += sizeof(void *)) { struct robust_list_head *candidate = *(struct robust_list_head **)p; diff --git a/tools/linuxfs/preload/space.c b/tools/linuxfs/preload/space.c index 5edc0bd35..950197f65 100644 --- a/tools/linuxfs/preload/space.c +++ b/tools/linuxfs/preload/space.c @@ -1,23 +1,3 @@ -/* - * The free space a library reports. - * - * A library's games can live somewhere the folder that names the library does not. The card - * library is exactly that: its steamapps/common is bound to the card, but the directory above it - * belongs to the runtime image in the phone's own storage, and that directory is what the client - * measures. So it offers both install locations with the phone's free space against each, and the - * card's real figure - seventeen gigabytes more, on the device this was found on - never appears. - * - * The number is not only cosmetic: the client refuses an install it believes will not fit. Once - * the phone fills up it would turn down a card install with room to spare, and say nothing about - * why. - * - * Moving the whole library onto the card would fix the number and break something worse. The - * prefixes under steamapps/compatdata want symlinks and file locks, and the card is served over - * FUSE, which gives us neither - that is why they are kept in the runtime image in the first - * place. So the question is answered where the content actually is: a query about a library root - * named in BL_LIBRARY_SPACE is served from its steamapps/common, the bind that points at the - * card. Every other path is passed through untouched. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <stddef.h> @@ -26,8 +6,6 @@ #include <sys/statfs.h> #include <sys/statvfs.h> -/* A library root, or its steamapps, resolved to the directory that holds the games. Returns the - * rewritten path in `buf`, or NULL when `path` is not one of ours and should be left alone. */ static const char *content_of(const char *path, char *buf, size_t len) { const char *roots = getenv("BL_LIBRARY_SPACE"); if (!path || !roots || !*roots) @@ -38,8 +16,6 @@ static const char *content_of(const char *path, char *buf, size_t len) { const char *end = strchr(roots, ':'); size_t root_len = end ? (size_t)(end - roots) : strlen(roots); if (root_len > 0) { - /* The root itself, or the steamapps directly beneath it. Anything deeper is already inside - * a bind and answers for itself. */ int match = (path_len == root_len && !strncmp(path, roots, root_len)) || (path_len == root_len + 10 && !strncmp(path, roots, root_len) && !strcmp(path + root_len, "/steamapps")); diff --git a/tools/linuxfs/preload/storage.c b/tools/linuxfs/preload/storage.c index 201f93cd4..4b9c44e66 100644 --- a/tools/linuxfs/preload/storage.c +++ b/tools/linuxfs/preload/storage.c @@ -1,13 +1,3 @@ -/* - * Opt-in observations for file allocation and writes to the selected Steam library. - * - * The session enables them only when storage diagnostics are turned on. Counters are local to - * each process, allocation details are capped, writes are sampled, and storage.log has a global - * size limit because every Steam child shares it. - * - * LD_PRELOAD cannot see direct syscalls or libc-internal calls that bypass public symbols. A - * missing entry therefore does not prove the operation did not happen. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -126,7 +116,6 @@ static void append_line(const char *line, size_t length) { syscall(SYS_close, (long)fd, 0L, 0L, 0L, 0L, 0L); goto done; } - /* One small append write per complete line; no per-write log flood. */ (void)syscall(SYS_write, (long)fd, (long)line, (long)length, 0L, 0L, 0L); syscall(SYS_close, (long)fd, 0L, 0L, 0L, 0L, 0L); done: @@ -223,7 +212,7 @@ int posix_fallocate(int fd, off_t offset, off_t length) { int call_errno = errno; uint64_t elapsed = now_ns() - start; allocation_line("posix_fallocate", result, call_errno, 0, offset, length, elapsed); - errno = entry_errno; /* POSIX requires the error to be returned, leaving errno unchanged. */ + errno = entry_errno; return result; } @@ -262,8 +251,6 @@ int ftruncate(int fd, off_t length) { int known = 0; if (steam_client()) { if (next_fstat == NULL) next_fstat = dlsym(RTLD_NEXT, "fstat"); - /* staging_file() reads /proc/self/fd, a round trip through proot: skip it for a shrink, which - * is passed through untouched either way. */ known = next_fstat != NULL && next_fstat(fd, &st) == 0 && S_ISREG(st.st_mode) && length >= st.st_size && staging_file(fd); } diff --git a/tools/linuxfs/preload/sysv.c b/tools/linuxfs/preload/sysv.c index 5c1416b2b..dc349c232 100644 --- a/tools/linuxfs/preload/sysv.c +++ b/tools/linuxfs/preload/sysv.c @@ -1,14 +1,3 @@ -/* System V semaphores and shared memory for programs in the Linux runtime. - * - * Android kernels are built without System V IPC, so semget/shmget fail with ENOSYS. Steam's tier0 - * creates its client-instance semaphore with semget and gives up when that fails. This library is - * LD_PRELOADed into the session and keeps each object in a file under /dev/shm (a bound directory - * the whole session shares): a semaphore set is an array of counters waited on with futexes, a - * segment is a plain file that shmat maps. Objects are found by the inode of their file, which is - * what the id is. semop applies its operations one by one (no all-or-nothing across a set) and - * SEM_UNDO is accepted but not performed - enough for the client, which uses single-counter sets. - * A message queue is a table of slots in its file: gamescope hands mangoapp (Deck mode's performance - * overlay) each frame's timings through one. */ #define _GNU_SOURCE #include <dirent.h> #include <errno.h> @@ -34,7 +23,7 @@ #include <unistd.h> #define DIR_DEFAULT "/dev/shm/wnsysv" -#define SEM_MAGIC 0x574e5345u /* "WNSE" */ +#define SEM_MAGIC 0x574e5345u #define MAX_OBJECTS 64 struct sem_header { @@ -43,15 +32,11 @@ struct sem_header { uint32_t key; uint32_t mode; int32_t creator_pid; - /* What IPC_STAT reports as sem_otime / sem_ctime (seconds). The usual race-free init has the - * creator semop the new set once, and every other process wait until sem_otime is non-zero; - * SteamVR's web helper spins on exactly that. */ _Atomic uint32_t otime; _Atomic uint32_t ctime; int32_t pad[9]; }; _Static_assert(sizeof(struct sem_header) == 64, "semaphore files written by older sessions keep their layout"); -/* The counters follow the header, one 32-bit word each (a futex word). */ struct sem_map { int id; struct sem_header *hdr; size_t len; }; struct shm_map { void *addr; size_t len; int id; }; @@ -76,7 +61,6 @@ static int object_id(int fd) { return (int)(st.st_ino & 0x7fffffff); } -/* Open the object file of kind ("sem"/"shm") for key, creating it as flags ask. */ static int open_object(const char *kind, key_t key, int flags, int *created) { char path[PATH_MAX]; int oflags = O_RDWR | O_CLOEXEC; @@ -98,7 +82,6 @@ static int open_object(const char *kind, key_t key, int flags, int *created) { return fd; } -/* The object file for an id, found by inode. */ static int open_by_id(const char *kind, int id) { DIR *d = opendir(object_dir()); if (!d) return -1; @@ -141,7 +124,6 @@ static long futex(void *addr, int op, int val, const struct timespec *timeout) { return syscall(SYS_futex, addr, op, val, timeout, NULL, 0); } -/* ------------------------------------------------------------------ semaphores */ static struct sem_map *sem_lookup(int id) { for (int i = 0; i < MAX_OBJECTS; i++) @@ -227,7 +209,7 @@ int semtimedop(int semid, struct sembuf *sops, size_t nsops, const struct timesp } else if (sem_wait_op(w, sops[i].sem_op, sops[i].sem_flg, timeout) != 0) { return -1; } - if (sops[i].sem_op < 0) futex(w, FUTEX_WAKE, INT_MAX, NULL); /* zero-waiters see the change */ + if (sops[i].sem_op < 0) futex(w, FUTEX_WAKE, INT_MAX, NULL); } atomic_store(&h->otime, (uint32_t)time(NULL)); return 0; @@ -293,11 +275,7 @@ int semctl(int semid, int semnum, int cmd, ...) { } } -/* ------------------------------------------------------------------ shared memory */ -/* Linux keeps a removed segment attachable until the last detach, which X clients rely on: they - * remove the segment right after asking the server to attach it. A removed segment is renamed out - * of key lookup instead, and removed for good once it has been out of use for a while. */ static int remove_shm(int id) { DIR *d = opendir(object_dir()); if (!d) return -1; @@ -411,18 +389,18 @@ int shmctl(int shmid, int cmd, struct shmid_ds *buf) { } } -/* ------------------------------------------------------------------ message queues */ -/* Only what gamescope and mangoapp use: msgtyp 0 or > 0, IPC_NOWAIT and IPC_RMID. */ -#define MSG_MAGIC 0x574e4d51u /* "WNMQ" */ + + +#define MSG_MAGIC 0x574e4d51u #define MSG_SLOTS 32 struct msg_queue { uint32_t magic; - _Atomic int32_t lock; /* 0 free, else the holder's pid */ - _Atomic int32_t changed; /* bumped on every send and receive; waiters sleep on it */ + _Atomic int32_t lock; + _Atomic int32_t changed; uint64_t next_seq; - struct { uint64_t seq; long mtype; size_t len; char data[2048]; } slot[MSG_SLOTS]; /* seq 0: free */ + struct { uint64_t seq; long mtype; size_t len; char data[2048]; } slot[MSG_SLOTS]; }; static struct msg_queue *msg_map(int id) { @@ -437,7 +415,6 @@ static struct msg_queue *msg_map(int id) { return NULL; } -/* A process killed while holding the lock (Android ends sessions without warning) loses it. */ static void msg_lock(struct msg_queue *q) { for (int32_t owner = 0; !atomic_compare_exchange_strong(&q->lock, &owner, getpid()); owner = 0) { if (kill(owner, 0) != 0 && errno == ESRCH) atomic_compare_exchange_strong(&q->lock, &owner, 0); @@ -531,12 +508,6 @@ int msgctl(int msqid, int cmd, struct msqid_ds *buf) { return rc; } -/* - * fork() carries over only the calling thread, so a lock another thread was holding at that - * instant stays held in the child by a thread that is not there to release it. Taking it before - * the fork makes the copy consistent; the parent then unlocks it and the child, whose one thread - * never locked it, gets a fresh one. (WinNative 79aa7f68.) - */ static void sysv_lock_before_fork(void) { pthread_mutex_lock(&g_lock); } static void sysv_unlock_after_fork(void) { pthread_mutex_unlock(&g_lock); } static void sysv_reset_after_fork(void) { pthread_mutex_init(&g_lock, NULL); } diff --git a/tools/linuxfs/preload/tracefs.c b/tools/linuxfs/preload/tracefs.c index 17d45a01a..8c59f2963 100644 --- a/tools/linuxfs/preload/tracefs.c +++ b/tools/linuxfs/preload/tracefs.c @@ -1,15 +1,3 @@ -/* - * A trace instance for mangoapp where the kernel's tracefs is closed to the app. - * - * Valve's mangoapp (Deck mode's performance overlay) reads the GPU memory total on a Qualcomm - * display driver (msm_dpu / msm_drm) from the gpu_mem_total event, through a tracefs instance of - * its own - and aborts when tracefs_instance_create() fails, taking the whole overlay with it. - * Android mounts tracefs for root and the readtracefs group only, so it always fails here. - * - * The real calls run first. Only when they fail is the instance placed in an empty directory of - * the session's own, and reading its events answers with one gpu_mem_total record carrying the - * total KGSL - the GPU driver of every Adreno under Android - reports to the app. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <limits.h> @@ -25,7 +13,6 @@ struct tracefs_instance; struct tep_handle; struct tep_event; -/* The head of libtraceevent 1.8's struct tep_record (the version tools/mangoapp/packages.txt pins). */ struct tep_record { unsigned long long ts, offset; long long missed_events; @@ -35,7 +22,6 @@ struct tep_record { void *priv; }; -/* The event as Android kernels declare it (trace/events/gpu_mem.h). */ static const char gpu_mem_total_format[] = "name: gpu_mem_total\n" "ID: 1\n" diff --git a/tools/linuxfs/preload/tracer.c b/tools/linuxfs/preload/tracer.c index 326a8f353..f713cc254 100644 --- a/tools/linuxfs/preload/tracer.c +++ b/tools/linuxfs/preload/tracer.c @@ -1,21 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with names - * changed to this project's. The reasoning below is the original author's. - * - * A process status without the container's tracer in it. - * - * proot runs the session under ptrace, so every process here reads a TracerPid in its - * /proc/<pid>/status. The Steam client takes that to mean a debugger is attached, and its arm64 - * build breaks into the supposed debugger with a bare "svc #0" after every failed assertion. That - * is no breakpoint: the registers still hold the call Plat_IsInDebugSession() just made, close(), - * with its own result, 1, as the argument - so each assertion closes descriptor 1. The number then - * goes to whatever the client opens next, and its standard output is written into that: app - * manifests and content chunks, its own connections, and the socket a game's Steam API talks - * over, which is how a game that stayed silent while loading found its pipe dead. - * - * The tracer is part of the container, not something a program should react to, so the status - * files are served with TracerPid 0. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE #include <ctype.h> #include <errno.h> @@ -34,7 +20,6 @@ static const char *skip_digits(const char *s) { return end == s ? NULL : end; } -/* /proc/{self,thread-self,<pid>}[/task/<tid>]/status */ static int is_status_path(const char *path) { const char *rest; if (path == NULL || strncmp(path, "/proc/", 6) != 0) return 0; @@ -46,9 +31,6 @@ static int is_status_path(const char *path) { return strcmp(rest, "/status") == 0; } -/* Chromium stands its hang watchdogs down when it sees a tracer, and the client's web helper does - * not come up within their patience here: it is left reading the truth. SteamVR's web helper (the - * dashboard, Cardboard mode's SteamVR) is the same Chromium and stalls the same way. */ static int is_web_helper(void) { return strcmp(program_invocation_short_name, "steamwebhelper") == 0 || strcmp(program_invocation_short_name, "vrwebhelper") == 0; @@ -65,8 +47,6 @@ static int write_all(int fd, const char *data, size_t len) { return 0; } -/* The descriptor to hand back for a read-only open of a status file, or -1 with errno untouched - * when the path is something else or the copy cannot be made, and the real open should run. */ __attribute__((visibility("hidden"))) int bl_status_without_tracer(const char *path, int flags) { char text[STATUS_MAX]; size_t len = 0; diff --git a/tools/linuxfs/preload/udevmon.c b/tools/linuxfs/preload/udevmon.c index 86baed110..33baf2bda 100644 --- a/tools/linuxfs/preload/udevmon.c +++ b/tools/linuxfs/preload/udevmon.c @@ -1,29 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with the - * symbol prefix changed to match this library. The reasoning below is the original author's. - * - * udev's netlink monitor, stood in for where the sandbox refuses the socket it is built on. - * - * The app sandbox denies a NETLINK_KOBJECT_UEVENT socket outright. libudev cannot create a - * monitor without one, and SDL's HID layer cannot start without a monitor: every SDL_hid_init() - * fails, the Steam client's controller code retries it hundreds of times a second, its main loop - * stalls, and the client asserts out from under the running game. Refusing SDL the library - * altogether (the earlier answer here) is what produced that failure. - * - * So the socket is answered instead: one end of a datagram socketpair stands in for the netlink - * socket, its peer held open so it never reads as hung up, and the few calls libudev makes on - * the socket that only a netlink one could answer are answered here. Nothing is ever written to - * it, so the monitor is live and silent, which is the most the kernel would deliver in a - * container anyway. The real socket is tried first, so this stays out of the way wherever a - * monitor could genuinely be created. - * - * The stand-in can also carry events after all (bl_udevmon_inject, below): libfakeinput plugs - * Steam's touch controller in and out of the client that way. - * - * Wine is left without one. Its HID bus reads the session's pads itself only when its SDL bus - * cannot start, and that is the path inputudev.c describes them on. Given a monitor the SDL bus - * comes up and takes the pads over from it. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -39,9 +17,9 @@ #include <unistd.h> struct stand_in { - int fd; /* the descriptor handed out, -1 when the slot is free */ - int peer; /* the other end, kept open */ - ino_t inode; /* identifies the socket after fd is closed and its number reused */ + int fd; + int peer; + ino_t inode; }; static struct stand_in stand_ins[16]; @@ -62,8 +40,6 @@ static ino_t socket_inode(int fd) { return fstat(fd, &st) == 0 ? st.st_ino : 0; } -/* Whether fd is one of the stand-in sockets. A slot whose descriptor was closed and reused by - * something else is retired on sight, so a later socket with the same number is not mistaken. */ static int is_stand_in(int fd) { int found = 0; if (fd < 0) @@ -108,7 +84,6 @@ static int make_stand_in(int type) { errno = saved; return -1; } - /* The peer is ours alone: it must not follow the socket into a child. */ fcntl(sv[1], F_SETFD, FD_CLOEXEC); pthread_mutex_lock(&lock); for (size_t i = 0; i < sizeof(stand_ins) / sizeof(stand_ins[0]); i++) { @@ -147,7 +122,6 @@ int socket(int domain, int type, int protocol) { return make_stand_in(type); } -/* For net.c's bind(): whether fd is a stand-in, which accepts any netlink address as bound. */ __attribute__((visibility("hidden"))) int bl_udevmon_stand_in(int fd) { return is_stand_in(fd); } @@ -180,8 +154,6 @@ int setsockopt(int fd, int level, int optname, const void *optval, socklen_t opt static int (*real_setsockopt)(int, int, int, const void *, socklen_t); if (is_stand_in(fd)) { - /* Group membership and packet filters only mean something on a socket that receives; this - * one never does, so they are accepted as set. The rest is genuine on a unix socket. */ if (level == SOL_NETLINK) return 0; if (level == SOL_SOCKET && (optname == SO_ATTACH_FILTER || optname == SO_DETACH_FILTER)) @@ -195,8 +167,6 @@ int setsockopt(int fd, int level, int optname, const void *optval, socklen_t opt return real_setsockopt(fd, level, optname, optval, optlen); } -/* udevd's message to the "udev" multicast group (systemd, device-monitor.c): this header, then the - * properties as NUL-separated KEY=VALUE strings. */ struct monitor_netlink_header { char prefix[8]; unsigned magic; @@ -209,7 +179,6 @@ struct monitor_netlink_header { unsigned filter_tag_bloom_lo; }; -/* systemd's string_hash32: MurmurHash2, seed 0. */ static unsigned murmur_hash2(const char *key, size_t len) { const unsigned m = 0x5bd1e995; unsigned h = (unsigned)len; @@ -236,8 +205,6 @@ static unsigned murmur_hash2(const char *key, size_t len) { return h; } -/* Sends a device event to every stand-in in this process, as udevd would; returns how many got it. - * For libfakeinput (found with dlsym), which knows when its devices come and go. */ __attribute__((visibility("default"))) int bl_udevmon_inject(const char *action, const char *devpath, const char *subsystem, const char *devname, unsigned major, unsigned minor) { @@ -247,7 +214,7 @@ __attribute__((visibility("default"))) int bl_udevmon_inject(const char *action, action, 0, devpath, 0, subsystem, 0, devname, 0, major, 0, minor, 0, __atomic_add_fetch(&seqnum, 1, __ATOMIC_RELAXED)); if (used <= 0 || (size_t)used >= sizeof(props)) return -1; - used++; /* the last string's NUL */ + used++; struct monitor_netlink_header header; memset(&header, 0, sizeof(header)); memcpy(header.prefix, "libudev", 8); @@ -269,10 +236,6 @@ __attribute__((visibility("default"))) int bl_udevmon_inject(const char *action, return sent; } -/* For ntsync.c's recvmsg(), which owns the symbol: 1 when fd is a stand-in, answered here (*result - * is what recvmsg returns), 0 otherwise. A message is dressed as udevd's: libudev drops one that - * does not come from the udev multicast group with root credentials. The sender address goes in - * the room msg_namelen had before the call; a unix socket sets it to 0. */ __attribute__((visibility("hidden"))) int bl_udevmon_recvmsg(int fd, struct msghdr *msg, int flags, ssize_t (*real_recvmsg)(int, struct msghdr *, int), ssize_t *result) { @@ -286,7 +249,7 @@ __attribute__((visibility("hidden"))) int bl_udevmon_recvmsg(int fd, struct msgh struct sockaddr_nl nl; memset(&nl, 0, sizeof(nl)); nl.nl_family = AF_NETLINK; - nl.nl_groups = 2; /* udev's group */ + nl.nl_groups = 2; memcpy(msg->msg_name, &nl, sizeof(nl)); msg->msg_namelen = sizeof(nl); } diff --git a/tools/linuxfs/preload/xmode.c b/tools/linuxfs/preload/xmode.c index 8579f70bc..e6842d73a 100644 --- a/tools/linuxfs/preload/xmode.c +++ b/tools/linuxfs/preload/xmode.c @@ -1,26 +1,9 @@ -/* - * The Steam client's own screen, in a Follow screen session. - * - * With two Xwayland servers the client sizes them through GAMESCOPE_XWAYLAND_MODE_CONTROL - * ({server, width, height, allow_super_res} on the root window): the games' server (1) to a game's - * Game Resolution, and its own (0) to what it recommends for the display - 1920x1080 on a Fold's - * 2448x1848 inner panel. From then on it reads that 16:9 size back as the display's "native" one, - * so its interface and every game it starts are 16:9 with bars on a near-square panel, and stay so - * when the phone folds onto the cover screen. - * - * gamescope already keeps server 0 at the output's size and resizes it when the output changes (a - * Steam Deck docking; here, the app resizing the output as the phone folds and unfolds). So in a - * Follow screen session (BL_FOLLOW_OUTPUT=1) a request for server 0 is dropped and the client's - * screen stays the panel's; requests for the games' servers pass through untouched. Any other - * session, and any other property, is passed straight on. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -/* Xlib's types, without needing its headers in the build image. */ typedef struct _XDisplay Display; typedef unsigned long Atom; typedef unsigned long Window; @@ -48,12 +31,10 @@ int XChangeProperty(Display *dpy, Window w, Atom property, Atom type, int format static Display *atom_dpy; static Atom mode_control; if (!intern) intern = (intern_atom_fn)dlsym(RTLD_NEXT, "XInternAtom"); - /* Atoms belong to a connection; the client's is the one that writes this. */ if (intern && atom_dpy != dpy) { mode_control = intern(dpy, "GAMESCOPE_XWAYLAND_MODE_CONTROL", 1); atom_dpy = dpy; } - /* Format-32 property data is an array of long in Xlib. */ const long *v = (const long *)data; if (mode_control && property == mode_control && v[0] == 0) { static int said; diff --git a/tools/linuxfs/preload/zram.c b/tools/linuxfs/preload/zram.c index 058b5bd4f..565e0f7dd 100644 --- a/tools/linuxfs/preload/zram.c +++ b/tools/linuxfs/preload/zram.c @@ -11,9 +11,6 @@ #include <sys/uio.h> #include <unistd.h> -/* zRAM for the Steam client while a game runs: on SIGURG the process pages its private anonymous - * memory out (ZR_CTL holds 1) or reads it back (0). Raw syscalls only, as this library wraps - * close(), openat() and syscall(). */ #ifndef MADV_PAGEOUT #define MADV_PAGEOUT 21 @@ -56,8 +53,6 @@ static unsigned long zr_hex(const char **p) { } } -/* Paged out: private writable anonymous memory (the heap, unnamed and [anon:name] mappings). - * Read back: every private readable mapping, as the kernel may have swapped others meanwhile. */ static int zr_eligible(const char *line, int compress, unsigned long *start, unsigned long *end) { const char *p = line; *start = zr_hex(&p); diff --git a/tools/linuxfs/ssbs/ssbs_adapter.c b/tools/linuxfs/ssbs/ssbs_adapter.c index 92b3000f6..a3125f070 100644 --- a/tools/linuxfs/ssbs/ssbs_adapter.c +++ b/tools/linuxfs/ssbs/ssbs_adapter.c @@ -1,30 +1,3 @@ -/* - * libssbs.so - keep PSTATE.SSBS set across signal returns, without rebuilding Wine. - * - * Wine's arm64 signal code writes PSTATE back from a Windows CONTEXT, whose Cpsr never carries - * SSBS (bit 12). Every signal return through that code (exception fixups, NtSetContextThread on - * another thread, the SIGUSR2 suspend path) therefore resumes the thread with SSBS clear, so the - * CPU runs it with speculative store bypass disabled - slower on cores such as Oryon. - * force_ssbs.diff (Sonicadvance1, FEX) fixes this inside ntdll; this library does the same from - * outside, for Protons that cannot be rebuilt: the Valve, GE and CachyOS ARM64 builds DroidDeck - * runs, which come prebuilt from Steam. Same source as Bannerlator's adapter (bionic), built here - * for glibc. - * - * How: interpose sigaction()/signal() via LD_PRELOAD. Every handler is installed behind one - * SA_SIGINFO trampoline that calls the real handler and then sets SSBS in the ucontext it is - * about to return through. The kernel accepts SSBS from userspace on sigreturn and context- - * switches it per thread, so the bit sticks until the next sigreturn. Handlers that never return - * (siglongjmp) are unaffected either way. - * - * Env: - * SSBS_ADAPTER_DISABLE=1 pass everything through untouched. - * SSBS_ADAPTER_OBSERVE=1 wrap and count, but leave SSBS as the handler left it (the baseline). - * SSBS_ADAPTER_DEBUG=1 log to stderr (implied by OBSERVE): - * - "returned with SSBS clear": a handler was about to resume a thread without SSBS. - * - "arrived with SSBS clear": a thread was RUNNING without SSBS when a signal reached it. - * Under the adapter this should stop after each thread's first fixed return; if it keeps - * counting, something other than a signal return is clearing the bit. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <signal.h> @@ -41,7 +14,6 @@ typedef void (*sigact_fn)(int, siginfo_t *, void *); typedef void (*sighand_fn)(int); typedef int (*sigaction_fn)(int, const struct sigaction *, struct sigaction *); -/* Per signal: the handler the program asked for, and whether it wanted SA_SIGINFO. */ static _Atomic(void *) real_handler[MAX_SIG]; static atomic_bool real_siginfo[MAX_SIG]; @@ -69,7 +41,6 @@ __attribute__((constructor)) static void ctor(void) if (debug) fprintf(stderr, "libssbs: loaded (%s)\n", disabled ? "disabled" : observe ? "observing only" : "active"); } -/* 1, 2, 4, 8, ...: enough lines to see a trend without flooding the log. */ static int milestone(unsigned long n) { return (n & (n - 1)) == 0; } static void trampoline(int sig, siginfo_t *info, void *uc_v) @@ -130,7 +101,6 @@ int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact) void *prev_h = atomic_load(&real_handler[sig]); int prev_info = atomic_load(&real_siginfo[sig]); - /* Publish before installing so the trampoline never runs with a stale handler. */ if (act && wrapped(new_h)) { atomic_store(&real_siginfo[sig], new_info); atomic_store_explicit(&real_handler[sig], new_h, memory_order_release); @@ -142,7 +112,6 @@ int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact) atomic_store(&real_handler[sig], prev_h); } - /* Report the caller's own handler, not the trampoline, so handler chaining keeps working. */ if (ret == 0 && oldact && oldact->sa_sigaction == trampoline && prev_h) { if (prev_info) { oldact->sa_sigaction = (sigact_fn)prev_h; @@ -167,7 +136,6 @@ sighandler_t signal(int sig, sighandler_t handler) sighandler_t bsd_signal(int sig, sighandler_t handler) { return signal(sig, handler); } -/* glibc's System V flavour: a one-shot handler with no restart. */ sighandler_t sysv_signal(int sig, sighandler_t handler) { struct sigaction sa, old; diff --git a/tools/mangoapp/libtracefs-shim.c b/tools/mangoapp/libtracefs-shim.c index 58506cada..7095a961f 100644 --- a/tools/mangoapp/libtracefs-shim.c +++ b/tools/mangoapp/libtracefs-shim.c @@ -1,15 +1,3 @@ -// libtracefs.so.1 for mangoapp: the GPU memory Valve's overlay reads, without the kernel's tracing. -// -// Valve's mangoapp (msm.cpp, ftrace_gpumem.c) takes the GPU's memory in use - the VRAM and PVRAM -// lines - from the gpu_mem_total tracepoint (pid 0, the device's total) through libtracefs. An -// Android app may not use tracefs at all: the instance could not be made, and both lines read 0. -// This stands in for the libtracefs calls mangoapp makes, and hands its record walker one -// gpu_mem_total record carrying what the app writes to /run/droiddeck-hud/gpu-mem (bytes; the -// session's GpuMemComponent, from KGSL's own accounting). Only mangoapp loads this copy. -// -// The walker reads libtraceevent's structures directly, so the ones below are libtraceevent -// 1.8.2's (event-parse.h), the version tools/mangoapp/packages.txt pins; tep_find_event_by_record -// and tep_read_number are answered here too, ahead of the real libtraceevent in mangoapp's lookup. #include <sched.h> #include <stddef.h> @@ -76,7 +64,6 @@ struct tep_record { void *priv; }; -// gpu_mem_total's record: gpu_id, pid, size - as the kernel lays it out after the common fields. struct gpu_mem_total { unsigned int gpu_id; unsigned int pid; @@ -90,7 +77,6 @@ static struct tep_format_field pid_field = { static struct tep_event gpu_mem_event = { .name = "gpu_mem_total", .system = "gpu_mem", .format = {.nr_fields = 2, .fields = &pid_field}}; -// Never dereferenced by mangoapp: they only have to be non-null. static char handle; static char instance; diff --git a/tools/mangoapp/mangoapp b/tools/mangoapp/mangoapp index e912be1a8..0a208fef3 100644 --- a/tools/mangoapp/mangoapp +++ b/tools/mangoapp/mangoapp @@ -1,15 +1,5 @@ #!/bin/sh -# gamescope --mangoapp runs `mangoapp` from PATH; this puts Valve's build beside the libraries the -# runtime lacks (fmt 10, spdlog 1.13, glfw, libtracefs, libtraceevent), staged by the app. -# mangoapp reads its window through GLFW's X11 API. GLFW 3.4 takes XDG_SESSION_TYPE=wayland with a -# WAYLAND_DISPLAY as Wayland-only, and unset, libwayland falls back to wayland-0 - DroidDeck's own -# compositor, which shares XDG_RUNTIME_DIR - so both fail; x11 makes GLFW use gamescope's X server. export XDG_SESSION_TYPE=x11 -# gamescope's reaper restarts mangoapp the moment it exits, with no backoff. One that dies at start -# (a stale session preload without the System V queue, a driver it crashes on) was started about -# once a second for the whole session, each start a full exec under proot - the Steam menu itself -# lagged. More than 5 starts in 60 s leaves the overlay off: this waits instead of exiting, so the -# reaper has nothing to restart until the session ends. starts=/tmp/droiddeck-mangoapp-starts now=$(date +%s) recent=$(awk -v now="$now" '$1 > now - 60' "$starts" 2>/dev/null) diff --git a/tools/mangoapp/packages.txt b/tools/mangoapp/packages.txt index 9637e246a..c5131beee 100644 --- a/tools/mangoapp/packages.txt +++ b/tools/mangoapp/packages.txt @@ -1,12 +1,5 @@ -# Valve's aarch64 mangoapp (the Deck performance overlay) and the libraries it needs that the -# runtime does not have, at the versions it was built against (its .BUILDINFO: fmt 10.2.0, -# spdlog 1.13.0). CI fetches each, checks the sha256 and stages the files named below into the apk -# under usr/local/lib/mangoapp; SessionFiles copies them into the runtime at every launch. -# sha256 url 5b203a711e4e48f01fdffaa4f5a330f8fef22f1eab865f067a3e60bd99a28a5c https://holo-packages.steamos.cloud/archlinux-deckard-hotfixes/release/0.4.x/mangohud-20260902.1-1-aarch64.pkg.tar.zst ef7411229ea4e9b3ab4e36ac356a85b5072f362ee1de68b28d09c43f0b0ecc44 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/fmt-10.2.0-1-aarch64.pkg.tar.zst c9bfd2c3f15071329d381b9c2061d07503c4787ceb7001b9dfbf72a23c85cb04 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/spdlog-1.13.0-1-aarch64.pkg.tar.zst f9fae6c84a0382188f1cb20761424051593fd09b1456191cc250bd16e0a1d7a5 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/glfw-3.4-2-aarch64.pkg.tar.zst -# libtracefs is ours (libtracefs-shim.c), and reads libtraceevent's structures as 1.8.2 lays them -# out: moving libtraceevent off 1.8.2 means checking event-parse.h against the shim. 0cccdc9c92cb3cae1e37ebeb0ed20b93bb3b5000c8f955b63d0e07bd878c7018 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/libtraceevent-1%3A1.8.2-2-aarch64.pkg.tar.zst diff --git a/tools/msitools/packages.txt b/tools/msitools/packages.txt index 8329e68a7..69a9dedba 100644 --- a/tools/msitools/packages.txt +++ b/tools/msitools/packages.txt @@ -1,12 +1,3 @@ -# What droiddeck-msi-install runs: msitools' msiinfo reads a Windows Installer package's tables -# and streams, cabextract unpacks its cabinets (every compression a cabinet can use, which gcab -# alone cannot), 7-Zip (7z + its 7z.so format plugin) opens the installer .exe wrappers around -# packages (self-extracting 7-Zip and cabinet files, WiX bundles). Arch Linux ARM's aarch64 packages, hosted unchanged on this repo's -# msitools-0.106-r1 release because the mirror keeps only the newest of each. CI fetches each, -# checks the sha256 and stages the files below into the apk under usr/local/lib/droiddeck-msitools; -# SessionFiles copies them into the runtime at every launch. Everything else they need (glib, gio, -# libxml2.so.16, zlib, bzip2, libgcc_s) is in the runtime already. -# sha256 url 4ab93d800b320d19c6c1090e21b69e5764ee241b6370012459e0c5df03739a8d https://github.com/Droid-Deck/DroidDeck/releases/download/msitools-0.106-r1/msitools-0.106-3-aarch64.pkg.tar.xz 4028f44d83b2956ba2931d32aee8cceecbd6481e2febf86a3b6bfed69ef403d5 https://github.com/Droid-Deck/DroidDeck/releases/download/msitools-0.106-r1/libgsf-1.14.60-1-aarch64.pkg.tar.xz 3d7bd103313cb36d6cb703a5e583363e56ff255f105bcc0727916d195967976d https://github.com/Droid-Deck/DroidDeck/releases/download/msitools-0.106-r1/gcab-1.6-2-aarch64.pkg.tar.xz diff --git a/tools/proot/bench/equiv2.py b/tools/proot/bench/equiv2.py index 397f41b56..b267ad121 100644 --- a/tools/proot/bench/equiv2.py +++ b/tools/proot/bench/equiv2.py @@ -185,7 +185,6 @@ def sockets(base): srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) out.append(" bind " + call(lambda: srv.bind(path))) if not os.path.exists(path): - # adb's shell may not create sockets under /data/local/tmp; the app can. srv.close() return out out.append(" name " + call(lambda: srv.getsockname())) diff --git a/tools/proot/bench/fpbench.c b/tools/proot/bench/fpbench.c index c9b42d888..f64e909e9 100644 --- a/tools/proot/bench/fpbench.c +++ b/tools/proot/bench/fpbench.c @@ -1,5 +1,3 @@ -/* The calls the fast path took over from proot, timed: run under fpoff and fastpath and compare. - * Usage: fpbench <scratch dir in the guest> [iterations]. Builds a Wine-like prefix there. */ #define _GNU_SOURCE #include <fcntl.h> #include <limits.h> diff --git a/tools/proot/bench/mechbench.c b/tools/proot/bench/mechbench.c index ae4319a95..4b9e034b4 100644 --- a/tools/proot/bench/mechbench.c +++ b/tools/proot/bench/mechbench.c @@ -1,14 +1,3 @@ -/* Cost of the syscall-interception mechanisms available to an Android app, measured on the - * trapped call getppid() (the tracer/handler does no work, so this is pure mechanism cost). - * - * mechbench filter <n_linear> allowed syscall through a filter of n JEQs (linear, proot-style) - * mechbench tree allowed syscall through a binary-search filter (~150 entries) - * mechbench ptrace [spin] [regs] seccomp RET_TRACE + ptrace tracer (proot's mechanism) - * mechbench sigsys seccomp RET_TRAP + in-process SIGSYS handler - * mechbench notif [sync] [spin] seccomp RET_USER_NOTIF + supervisor thread - * mechbench openat2 <root> <path> openat2(RESOLVE_IN_ROOT) vs openat on the host path - * Env CPU_T / CPU_S pin the trapped thread / the tracer-supervisor to a cpu. - */ #define _GNU_SOURCE #include <errno.h> #include <fcntl.h> @@ -68,8 +57,6 @@ static double time_trapped(void) { return (now() - t0) / N; } -/* A filter: arch check, then `n` JEQs on numbers that never match (like proot's list of ~150 - * traced syscalls seen by an allowed syscall), then `action` for TRAPPED, else ALLOW. */ static int install(int n_linear, uint32_t action, unsigned flags) { struct sock_filter *f = calloc(n_linear + 16, sizeof *f); int k = 0; @@ -77,7 +64,7 @@ static int install(int n_linear, uint32_t action, unsigned flags) { f[k++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_AARCH64, 1, 0); f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)); - for (int i = 0; i < n_linear; i++) /* numbers far above any real syscall */ + for (int i = 0; i < n_linear; i++) f[k++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 5000 + i, 0, 0); f[k++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, TRAPPED, 0, 1); f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, action); @@ -87,7 +74,6 @@ static int install(int n_linear, uint32_t action, unsigned flags) { return syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, flags, &p); } -/* ~150 traced numbers as a balanced binary search (what libseccomp's optimize=2 emits). */ static int emit_tree(struct sock_filter *f, int *k, int lo, int hi) { if (lo > hi) { f[(*k)++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); return 0; } int mid = (lo + hi) / 2; @@ -106,7 +92,7 @@ static void *sigsys_unused; static void on_sigsys(int sig, siginfo_t *si, void *ctx) { (void)sig; (void)si; ucontext_t *uc = ctx; - uc->uc_mcontext.regs[0] = 4242; /* the emulated result */ + uc->uc_mcontext.regs[0] = 4242; } struct notif_arg { int fd; int spin; }; @@ -184,22 +170,17 @@ int main(int argc, char **argv) { if (ioctl(notif_fd, SECCOMP_IOCTL_NOTIF_SET_FLAGS, SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP) < 0) perror("SYNC_WAKE_UP"); else printf("(sync wake-up on)\n"); } - /* The filter lives on thread w, which has exited; install again on a fresh worker that - * does the timing. */ struct notif_arg na = {.fd = -1}; (void)na; printf("note: user-notif is measured in 'notif2'\n"); return 0; } if (!strcmp(mode, "notif2")) { - /* Supervisor = child thread (unfiltered), timed thread = main after installing. */ int sync = argc > 2 && !strcmp(argv[2], "sync"); pin("CPU_T"); int fd = install(0, SECCOMP_RET_USER_NOTIF, SECCOMP_FILTER_FLAG_NEW_LISTENER); if (fd < 0) { perror("NEW_LISTENER"); return 1; } if (sync && ioctl(fd, SECCOMP_IOCTL_NOTIF_SET_FLAGS, SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP, 0) < 0) perror("SYNC_WAKE_UP"); - /* The supervisor thread is created AFTER the filter, so it inherits it - but it only - * makes ioctl calls, which the filter allows. */ static struct notif_arg na; na.fd = fd; pthread_t s; diff --git a/tools/proot/bench/parstat.c b/tools/proot/bench/parstat.c index 0b229f080..cb016b5b1 100644 --- a/tools/proot/bench/parstat.c +++ b/tools/proot/bench/parstat.c @@ -1,4 +1,3 @@ -/* T threads each stat()ing a path for D seconds: aggregate trapped-syscall throughput. */ #define _GNU_SOURCE #include <pthread.h> #include <stdio.h> diff --git a/tools/proot/bench/run-device.sh b/tools/proot/bench/run-device.sh index 20333318f..0feacc437 100644 --- a/tools/proot/bench/run-device.sh +++ b/tools/proot/bench/run-device.sh @@ -1,14 +1,4 @@ #!/system/bin/sh -# Runs a guest command on an adb-connected device the way a DroidDeck session runs proot (same -# options, a similar number of binds), as the shell user from /data/local/tmp/dd. Layout there: -# mini/ a glibc rootfs subset (glibc, bash, coreutils, python) + the bench binaries -# proot, loader this repo's proot (tools/proot/build.sh) -> variant "patched" -# proot-vanilla termux proot at source.env with no patches -> variant "vanilla" -# proot-fp this repo's proot incl. 0014 -> "fastpath" / "fpoff" -# mini/usr/lib/libfastpath.so tools/proot/fastpath/fastpath.c -# NOTE: adb shell is NOT under the app seccomp policy; see docs/development/proot-performance.md. -# -# run-device.sh <patched|vanilla|noseccomp|bare|fastpath|fpoff|direct> cmd... D=/data/local/tmp/dd V=$1; shift U=$(id -u) diff --git a/tools/proot/bench/sysbench.c b/tools/proot/bench/sysbench.c index 628e36e46..2d42ffdc5 100644 --- a/tools/proot/bench/sysbench.c +++ b/tools/proot/bench/sysbench.c @@ -1,6 +1,3 @@ -/* Per-syscall latency under whatever runs this program (native, proot, ...). - * Usage: sysbench <deep-file> [iterations] - * Prints "name ns/op" lines. */ #define _GNU_SOURCE #include <fcntl.h> #include <linux/futex.h> diff --git a/tools/proot/bench/workloads.sh b/tools/proot/bench/workloads.sh index f09c0b9e5..a727605c3 100644 --- a/tools/proot/bench/workloads.sh +++ b/tools/proot/bench/workloads.sh @@ -1,5 +1,4 @@ #!/system/bin/sh -# workloads: direct glibc (patched PT_INTERP) vs proot variants D=/data/local/tmp/dd; cd $D t() { local s=$(date +%s%N); "$@" >/dev/null 2>&1; local e=$(date +%s%N); echo $(( (e - s) / 1000000 )); } base() { t ./run.sh $1 /usr/bin/true; } diff --git a/tools/proot/fastpath/audit.c b/tools/proot/fastpath/audit.c index 01628712e..6913ffa6f 100644 --- a/tools/proot/fastpath/audit.c +++ b/tools/proot/fastpath/audit.c @@ -1,20 +1,3 @@ -/* - * libblaudit: an LD_AUDIT module that keeps ld.so from asking proot about libraries that are not - * there. ld.so finds a library by trying to open it in each directory of its search path in turn, - * and under proot every one of those opens is a round trip through the tracer. A game's environment - * puts the game's own directory, Steam's runtime and video directories and Proton's library - * directories ahead of /usr/lib, so most of the opens of every Wine process fail. - * - * la_objsearch() is asked about each candidate before ld.so opens it. One that provably does not - * exist - its parent directory is reached through no symlink, so proot would walk the same path, - * and the name itself is missing there - is skipped. Everything else is left to ld.so, which then - * behaves exactly as it would have. - * - * Built without libc: an audit module is loaded into a link namespace of its own, and a libc there - * would be searched for along that same path. Its calls go through the trampoline page of - * libblfastpath.so, mapped here first (libblfastpath finds it in place), and only in a process traced - * by the proot the environment describes (PROOT_FP_KEY, as libblfastpath checks it). - */ #define _GNU_SOURCE #include <errno.h> #include <fcntl.h> @@ -26,7 +9,7 @@ #include <sys/syscall.h> #include <time.h> -#define FP_STUB_ADDR 0xffff00000UL /* as in fastpath.c */ +#define FP_STUB_ADDR 0xffff00000UL #define FP_STUB_SIZE 4096UL #define MAX_BINDS 256 #define PATH 4096 @@ -38,8 +21,6 @@ static stub_fn sys; static long sc(long nr, long a, long b, long c, long d) { return sys(nr, a, b, c, d, 0, 0); } -/* A syscall of its own, before the trampoline exists: only mmap, mprotect and munmap, which proot - * does not stop. */ static long raw(long nr, long a, long b, long c, long d) { register long x8 __asm__("x8") = nr; register long x0 __asm__("x0") = a; @@ -52,7 +33,6 @@ static long raw(long nr, long a, long b, long c, long d) { return x0; } -/* What __builtin___clear_cache does on aarch64, without libgcc. */ static void sync_code(char *b, char *e) { for (char *p = b; p < e; p += 4) __asm__ volatile("dc cvau, %0" : : "r"(p) : "memory"); __asm__ volatile("dsb ish" : : : "memory"); @@ -80,7 +60,6 @@ static int under(const char *path, const char *dir, size_t dlen) { return same(path, dir, dlen) && (path[dlen] == 0 || path[dlen] == '/'); } -/* ---------------------------------------------------------------- setup */ static char env[65536], tracer_env[32768]; static size_t env_len; @@ -109,7 +88,6 @@ static const char *var(const char *buf, size_t len, const char *name) { return NULL; } -/* The canonical form of a host path, as proot's realpath() made it; 0 when it has none. */ static int canonical(const char *path, char *out) { long fd = sc(SYS_openat, AT_FDCWD, (long)path, O_PATH | O_CLOEXEC, 0); if (fd < 0) return 0; @@ -148,14 +126,13 @@ static int traced_by_match(const char *key) { static void setup(void) { long p = raw(SYS_mmap, FP_STUB_ADDR, FP_STUB_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE); - /* Loaded before anything else maps, so whatever is already there is not the trampoline. */ if (p != (long)FP_STUB_ADDR) { if (p > 0) raw(SYS_munmap, p, FP_STUB_SIZE, 0, 0); return; } static const uint32_t code[] = { 0xAA0003E8, 0xAA0103E0, 0xAA0203E1, 0xAA0303E2, 0xAA0403E3, 0xAA0503E4, 0xAA0603E5, - 0xD4000001, 0xD65F03C0, /* mov x8,x0; mov x0..x5,x1..x6; svc #0; ret */ + 0xD4000001, 0xD65F03C0, }; copy((char *)p, (const char *)code, sizeof code); sync_code((char *)p, (char *)p + sizeof code); @@ -174,7 +151,6 @@ static void setup(void) { root = root_canon; rootlen = len_of(root); if (rootlen <= 1) return; - /* "host[:guest]|..." taken apart in place; a list this cannot hold leaves everything to ld.so. */ for (char *s = (char *)b; s && *s;) { if (nbinds == MAX_BINDS) return; char *end = s; @@ -196,9 +172,7 @@ static void setup(void) { ready = 1; } -/* ---------------------------------------------------------------- lookups */ -/* The host path proot binds there, as it canonicalised it: NULL for one it dropped (or /proc/self). */ static const char *bind_host(struct bind *bd) { if (!bd->state) { bd->state = under(bd->host, "/proc/self", 10) || !canonical(bd->host, bd->canon) ? 2 : 1; @@ -212,8 +186,6 @@ static long long now(void) { return ts.tv_sec * 1000000000LL + ts.tv_nsec; } -/* The host path proot gives a canonical guest path: under the longest binding over it (the last of - * two for the same path), else the rootfs. 0 for a binding proot dropped, or no room. */ static int to_host(const char *g, char *host, int *bound) { size_t n = len_of(g); struct bind *bd = NULL; @@ -235,12 +207,10 @@ static int to_host(const char *g, char *host, int *bound) { return 1; } -/* Under a directory proot or the kernel answers for in its own way. */ static int special(const char *g) { return under(g, "/proc", 5) || under(g, "/dev", 4) || under(g, "/sys", 4); } -/* A guest path proot only shows as glue, or one with a binding below it. */ static int above_binding(const char *g) { size_t n = len_of(g); for (int i = 0; i < nbinds; i++) @@ -248,12 +218,6 @@ static int above_binding(const char *g) { return 0; } -/* - * The canonical guest path of the directory `g`, walked as canonicalize() walks it: a symlink in the - * rootfs is followed in guest terms, its text losing the rootfs prefix as detranslate_path() has it. - * -1 when a component is missing, so proot fails the path with ENOENT. Anything this cannot follow - * exactly (a link inside a binding, glue, a component not a directory) gives 0. - */ static int canonical_dir(const char *g, char *out) { char path[PATH], next[PATH], host[PATH], text[PATH]; char st_buf[256] __attribute__((aligned(16))); @@ -312,8 +276,6 @@ static int canonical_dir(const char *g, char *out) { static struct { char dir[PATH]; char canon[PATH]; long long at; } known[VERIFIED]; static int next_slot; -/* canonical_dir(), remembered for a while: ld.so looks in the same few directories for every - * library. */ static int known_dir(const char *g, char *out) { size_t n = len_of(g); long long t = now(); @@ -331,8 +293,6 @@ static int known_dir(const char *g, char *out) { return 1; } -/* The guest path certainly names nothing: its directory is one proot reaches the same way, and the - * name is missing there. */ static int missing(const char *g) { char dir[PATH], cand[PATH], host[PATH]; size_t n = len_of(g); @@ -363,13 +323,6 @@ unsigned int la_version(unsigned int version) { return LAV_CURRENT; } -/* - * A candidate skipped here is one whose open "failed" with ld.so's errno as it stands, and ld.so - * gives up on the whole search path unless that is ENOENT or EACCES. So each lookup's first - * candidate is always opened for real: once that has failed, errno says so, and nothing ld.so - * does between candidates changes it unless the search ends anyway. The default directories are - * left alone too: what ld.so meets there is what it reports when a library is nowhere. - */ static int tried; char *la_objsearch(const char *name, uintptr_t *cookie, unsigned int flag) { diff --git a/tools/proot/fastpath/fastpath.c b/tools/proot/fastpath/fastpath.c index 3914423a0..22bc38fe4 100644 --- a/tools/proot/fastpath/fastpath.c +++ b/tools/proot/fastpath/fastpath.c @@ -1,38 +1,3 @@ -/* - * proot fast path: path syscalls answered inside the process, without a ptrace round trip. - * - * proot traps every path syscall into its tracer: 15-60 us a call on a Snapdragon, most of it - * the two cross-core wake-ups between tracee and tracer. This library, preloaded into every guest - * process, answers the common calls itself and issues the translated syscall from a trampoline - * page at a fixed address, which proot's seccomp filter lets through without a stop - * (PROOT_FASTPATH, tools/proot/patches/0014). - * - * It decides nothing proot would decide differently. A guest path is resolved the way proot's - * canonicalize() does it, one component at a time from the root or the guest directory a - * relative path starts in: each component is mapped through the longest binding whose guest path - * is a prefix of it (else the rootfs) and looked at without following it; a symlink is read and - * its text resolved in its place, ".." drops the last resolved component, and a directory that - * only exists as proot's glue above a binding is walked through. The kernel then gets the host - * path that proot would have given it. In /proc, /proc/self and /proc/thread-self are followed as - * proot follows them, and a link proot emulates is read the way it answers readlink(); anything - * proot keeps state about (a renamed directory, sockets bound at a long path, glue itself), the - * /dev links into /proc, any flag not handled here and any failure to decide go to proot: the real - * libc call runs, proot traps it and answers as it always has. - * - * Only syscalls Android's app seccomp policy allows are used - not openat2 or faccessat2, which - * it answers with SIGSYS. - * - * Directories resolved once are remembered for PROOT_FP_TTL_MS (default 2000; 0 = resolve every - * call): a directory replaced by a symlink within that window would be missed, the same trade the - * session's pathcache.c makes for the Steam client. Renames, removals and new symlinks made here - * forget them at once. - * - * Config (the launcher sets it): PROOT_FP_ROOT=<host rootfs>, PROOT_FP_BINDS=<host:guest|...>; - * PROOT_FP_KEY=<key>, with PROOT_FASTPATH=<key> in proot's own environment: nothing is answered - * here unless the tracer is that proot (traced_by_match). - * PROOT_FP_OFF=1 disables it; PROOT_FP_STATS=1 reports hits and misses at exit, and - * PROOT_FP_LOG=<file> appends every call left to proot to that file. - */ #define _GNU_SOURCE #include <dirent.h> #include <dlfcn.h> @@ -66,14 +31,10 @@ #include <unistd.h> #include <utime.h> -#define FP_STUB_ADDR 0xffff00000UL /* must match FASTPATH_STUB_ADDR in proot's seccomp.c */ +#define FP_STUB_ADDR 0xffff00000UL #define FP_STUB_SIZE 4096UL -/* A session binds 80-odd paths (devices, the Steam libraries, each Games folder...). More than fit, - * or one that cannot be held, turns the fast path off: a bind it did not know would map that - * guest path into the rootfs instead (an empty mount point), which proot never does. */ #define MAX_BINDS 256 #define FP_SLOW (-100000L) -/* proot gives up at MAXSYMLINKS; a chain this long is left to it, whatever it decides. */ #define MAX_LINKS 16 typedef long (*stub_fn)(long nr, long a, long b, long c, long d, long e, long f); @@ -91,7 +52,6 @@ static unsigned long hits, misses; static long sc(long nr, long a, long b, long c, long d, long e) { return fp_sys(nr, a, b, c, d, e, 0); } static void fp_init(void); static pthread_once_t init_once = PTHREAD_ONCE_INIT; -/* Set up on the first call that could use it: a process that only execs on pays nothing. */ static inline int fp_ready(void) { pthread_once(&init_once, fp_init); return fp_on; @@ -102,7 +62,6 @@ static int under(const char *path, const char *dir, size_t dlen) { return strncmp(path, dir, dlen) == 0 && (path[dlen] == 0 || path[dlen] == '/'); } -/* ---------------------------------------------------------------- setup */ static void stats(void) { char line[160]; @@ -114,7 +73,6 @@ static void stats(void) { static const char *miss_log; static long fp_open(int dirfd, const char *path, int flags, mode_t mode); -/* A call left to proot. Opened for each line: a program may close or reuse any descriptor kept. */ static void missed(const char *call, int dirfd, const char *path) { misses++; if (!miss_log) return; @@ -133,8 +91,6 @@ static void missed(const char *call, int dirfd, const char *path) { sc(SYS_close, fd, 0, 0, 0, 0); } -/* The canonical form of a host path (bindings may name /sdcard, /data/user/0, ...), as realpath() - * gives it; -errno, leaving the path as it is, when it does not lead to one. */ static long canonical(char *path, size_t size) { long fd = sc(SYS_openat, AT_FDCWD, (long)path, O_PATH | O_CLOEXEC, 0, 0); if (fd < 0) return fd; @@ -155,15 +111,8 @@ static pthread_mutex_t cache_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t bind_lock; static void after_fork(void) { pthread_mutex_init(&cache_lock, NULL); pthread_mutex_init(&bind_lock, NULL); } -/* - * The process is traced by a proot that lets the trampoline through and was given the same - * rootfs and bindings: its own environment carries PROOT_FASTPATH=<PROOT_FP_KEY> (the launcher - * derives the key from them) and no PROOT_NO_SECCOMP. A process that inherited the variables but - * runs under another proot, or none, must not translate paths itself: a proot that traps the - * trampoline would translate the host path a second time. - */ static int traced_by_match(const char *key) { - static char buf[32768]; /* under init_once */ + static char buf[32768]; char path[48], want[160]; long fd = sc(SYS_openat, AT_FDCWD, (long)"/proc/self/status", O_RDONLY | O_CLOEXEC, 0, 0); if (fd < 0) return 0; @@ -191,11 +140,6 @@ static int traced_by_match(const char *key) { return found; } -/* - * proot canonicalised every binding's host path with realpath() in its own process, so a binding of - * /proc/self/fd/1 (/dev/stdout) names whatever proot's own descriptor 1 is - /dev/null in a session - - * and one onto a pipe was dropped. Read through the tracer's /proc; 0 when that cannot be seen. - */ static int in_tracer(struct bind *bd) { char h[PATH_MAX]; if (snprintf(h, sizeof h, "/proc/%ld%s", tracer, bd->host + 10) >= (int)sizeof h) return 0; @@ -216,12 +160,12 @@ static void fp_init(void) { MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); if (p != (void *)FP_STUB_ADDR) { if (p != MAP_FAILED) { munmap(p, FP_STUB_SIZE); return; } - if (errno != EEXIST) return; /* mapped already: this library loaded twice */ + if (errno != EEXIST) return; } else { static const uint32_t code[] = { - 0xAA0003E8, /* mov x8, x0 */ 0xAA0103E0, /* mov x0, x1 */ 0xAA0203E1, /* mov x1, x2 */ - 0xAA0303E2, /* mov x2, x3 */ 0xAA0403E3, /* mov x3, x4 */ 0xAA0503E4, /* mov x4, x5 */ - 0xAA0603E5, /* mov x5, x6 */ 0xD4000001, /* svc #0 */ 0xD65F03C0, /* ret */ + 0xAA0003E8, 0xAA0103E0, 0xAA0203E1, + 0xAA0303E2, 0xAA0403E3, 0xAA0503E4, + 0xAA0603E5, 0xD4000001, 0xD65F03C0, }; memcpy(p, code, sizeof code); __builtin___clear_cache((char *)p, (char *)p + sizeof code); @@ -246,8 +190,6 @@ static void fp_init(void) { char *colon = strchr(spec, ':'); if (colon) *colon = 0; struct bind *bd = &binds[nbinds]; - /* Canonicalised on first use: some (/sdcard) are FUSE, slow to look up at every exec. - * Static storage: malloc here would grow the heap with brk(2), which proot traps. */ static char hosts[MAX_BINDS][PATH_MAX], guests[MAX_BINDS][512]; const char *g = colon ? colon + 1 : spec; if (strlen(g) >= sizeof guests[0]) { unheld = 1; break; } @@ -267,15 +209,13 @@ static void fp_init(void) { if (unheld || (s && *s)) { fp_sys = NULL; return; } for (int i = 0; i < nbinds; i++) if (under(binds[i].host, "/proc/self", 10) && !in_tracer(&binds[i])) { fp_sys = NULL; return; } - pthread_atfork(NULL, NULL, after_fork); /* registered once per exec */ + pthread_atfork(NULL, NULL, after_fork); fp_on = 1; if (getenv("PROOT_FP_STATS")) atexit(stats); miss_log = getenv("PROOT_FP_LOG"); if (miss_log && miss_log[0] != '/') miss_log = NULL; } -/* Whether proot holds a binding: one whose host path did not resolve it dropped ("can't sanitize - * binding"). 1, 0, or -1 when that cannot be told now. */ static pthread_mutex_t bind_lock = PTHREAD_MUTEX_INITIALIZER; static int held(struct bind *bd) { if (!__atomic_load_n(&bd->canon, __ATOMIC_ACQUIRE)) { @@ -290,16 +230,12 @@ static int held(struct bind *bd) { return !bd->dropped; } -/* ---------------------------------------------------------------- guest <-> host */ -/* Guest paths never answered here: bindings onto /proc/self, which proot resolves as its own. */ static int reserved(const char *g) { return under(g, "/dev/fd", 7) || under(g, "/dev/stdin", 10) || under(g, "/dev/stdout", 11) || under(g, "/dev/stderr", 11); } -/* The binding a canonical guest path is under (the longest guest prefix; of two for the same guest - * path, proot keeps the last), or NULL for the rootfs. -1 when it cannot be told. */ static int bind_of(const char *g, struct bind **out) { struct bind *bd = NULL; for (int i = 0; i < nbinds; i++) { @@ -312,7 +248,6 @@ static int bind_of(const char *g, struct bind **out) { return 0; } -/* The host path proot gives the kernel for a canonical guest path; -1 when it cannot be told. */ static int to_host(const char *g, char *host, struct bind **through) { struct bind *bd; if (bind_of(g, &bd) < 0) return -1; @@ -322,8 +257,6 @@ static int to_host(const char *g, char *host, struct bind **through) { return n < PATH_MAX ? 0 : -1; } -/* A guest directory proot only shows as glue: missing in the rootfs, with a binding below it. - * -1 when it cannot be told. */ static int glue_above_binding(const char *g, size_t len) { int glue = 0; for (int i = 0; i < nbinds && glue == 0; i++) @@ -331,13 +264,6 @@ static int glue_above_binding(const char *g, size_t len) { return glue; } -/* - * The k-th guest path proot could name a canonical host path by (the cwd, a dirfd), as its - * detranslation does: inside the rootfs, the rootfs's view of it, whatever is bound; elsewhere the - * binding with the deepest host path - two bindings of the same host path are two names, and which - * one proot uses depends on its list order. A name must also map back to the path, i.e. no longer - * binding hides it. 0, or -1 when there is no k-th. - */ static int guest_name(const char *h, int k, char *g) { char back[PATH_MAX]; size_t deepest = 0; @@ -359,7 +285,6 @@ static int guest_name(const char *h, int k, char *g) { return -1; } -/* The one guest path that names a host path, or -1. */ static int to_guest(const char *h, char *g, size_t size) { char other[PATH_MAX]; if (size < PATH_MAX || guest_name(h, 0, g) < 0) return -1; @@ -368,8 +293,6 @@ static int to_guest(const char *h, char *g, size_t size) { return 0; } -/* A host path or link text proot shows for a host path it read from a /proc link: in guest terms - * when the rootfs or a binding holds it, else as it is (a pipe, a socket, a path outside the guest). */ static int guest_text(const char *h, char *g) { int in = h[0] == '/' && under(h, root, rootlen); for (int i = 0; h[0] == '/' && !in && i < nbinds; i++) { @@ -382,8 +305,6 @@ static int guest_text(const char *h, char *g) { return 0; } -/* The binding proot's get_binding(HOST) finds for a host path outside the rootfs: the deepest host - * prefix. -1 for none, -2 when it cannot be told (two at that depth with different guest paths). */ static int host_bind(const char *h) { int found = -1; for (int i = 0; i < nbinds; i++) { @@ -396,11 +317,6 @@ static int host_bind(const char *h) { return found == -3 ? -2 : found; } -/* - * proot's detranslate_path() for the text of a link outside /proc whose own host path is `at`: - * text into the binding the link itself is in shows under that binding's guest path, other text - * naming the rootfs loses the rootfs prefix. 1 when rewritten, 0 when not, or FP_SLOW. - */ static long detranslated(char *text, const char *at) { if (text[0] != '/') return 0; if (!under(at, root, rootlen) && !under(text, root, rootlen)) { @@ -421,10 +337,9 @@ static long detranslated(char *text, const char *at) { return 1; } -/* ---------------------------------------------------------------- resolved directories */ #define SLOTS 512 -#define SLOT_PATH 256 /* longer directories are resolved every time */ +#define SLOT_PATH 256 struct slot { long long stamp; unsigned hash; unsigned gen; char literal[SLOT_PATH]; char guest[SLOT_PATH]; }; static struct slot cache[SLOTS]; static unsigned generation; @@ -441,7 +356,6 @@ static unsigned hash_of(const char *s) { return h; } -/* Something here changed which paths name which directories. */ static void forget(void) { __atomic_add_fetch(&generation, 1, __ATOMIC_RELEASE); } static int cached(const char *literal, unsigned h, char *guest) { @@ -467,12 +381,6 @@ static void remember(const char *literal, unsigned h, const char *guest) { pthread_mutex_unlock(&cache_lock); } -/* - * A binding below a symlink: proot canonicalised its guest path when it set it up - * (initialize_binding), so -b X:/sys/class/thermal/thermal_zone1/type binds the link's target, - * /sys/devices/virtual/thermal/thermal_zone1/type, and a path through the link meets it there. - * Known here by its literal text only, it would be missed; such a path is left to proot. - */ static int binding_below(const char *g) { size_t len = strlen(g); for (int i = 0; i < nbinds; i++) @@ -480,14 +388,7 @@ static int binding_below(const char *g) { return 0; } -/* ---------------------------------------------------------------- resolution */ -/* - * Where a resolution ended: the canonical guest path, its host path and, when it exists, its lstat. - * `libc` asks for links to be read as glibc's realpath() reads them, through readlink; `moving` is - * set once the answer depends on the caller (a /proc link), so it is not remembered. `magic`: it - * ends on /proc/<pid>/fd/N, which proot leaves for the kernel to follow; st is what it leads to. - */ struct res { char guest[PATH_MAX]; char host[PATH_MAX]; @@ -499,12 +400,10 @@ struct res { int magic; }; -/* A directory removed while open: the kernel names it "<path> (deleted)". */ static int gone(const char *h, size_t n) { return n > 10 && strcmp(h + n - 10, " (deleted)") == 0; } -/* The guest cwd proot keeps for this process, as the kernel's cwd names it. */ static long own_cwd(char *g) { char h[PATH_MAX]; long n = sc(SYS_readlinkat, AT_FDCWD, (long)"/proc/self/cwd", (long)h, sizeof h - 1, 0); @@ -514,7 +413,6 @@ static long own_cwd(char *g) { return (long)strlen(g); } -/* The program proot started this process with, asked of proot once: it is its own record. */ static char exe_text[PATH_MAX]; static int exe_len; static long own_exe(char *g) { @@ -537,12 +435,6 @@ static long own_exe(char *g) { return t; } -/* - * What proot's readlink() answers for the link r names under /proc (readlink_proc2, then - * detranslate_path): exe, cwd and root of a process it traces from its own records, every other - * link as the kernel has it with a host path put in guest terms. `changed` is set when that differs - * from the kernel's text, which is cut to `size` (0: no limit) before proot reads it. - */ static long proc_readlink(const struct res *r, char *text, size_t size, int *changed) { const char *name = strrchr(r->guest, '/') + 1; size_t blen = name - 1 - r->guest; @@ -566,7 +458,6 @@ static long proc_readlink(const struct res *r, char *text, size_t size, int *cha return (long)strlen(text); } -/* /proc/<pid>/fd/N. */ static int fd_link(const char *g) { const char *p = g + 6; if (strncmp(g, "/proc/", 6) != 0 || *p < '1' || *p > '9') return 0; @@ -575,11 +466,6 @@ static int fd_link(const char *g) { return 1; } -/* - * A link met under /proc while resolving, as canonicalize() reads it: /proc/self and - * /proc/thread-self name the caller for proot as for the kernel, the others are what proot's - * readlink() gives. 0 for /proc/<pid>/fd/N named last, which proot does not follow itself. - */ static long proc_link(struct res *r, char *text, int final) { int changed; if (!under(r->host, "/proc", 5)) return FP_SLOW; @@ -603,20 +489,10 @@ static int pop(char *g) { return 0; } -/* The final component is there for the call to meet but could not be looked at (sysfs and /proc - * nodes the app may not stat): proot leaves it to the kernel too. Only callers that act on r->host - * alone ask for it with MAY_BE_UNSEEN; to the others it is FP_SLOW. */ #define UNSEEN 2 #define MAY_BE_UNSEEN 2 -/* The final component is proot's glue: a directory, but not one the kernel has. */ #define GLUE 3 -/* - * Resolves `text` below r->guest (an existing canonical directory; "/" for an absolute text), as - * canonicalize() does. The final component is followed when `follow` is set; `last`: it is also the - * last of the whole path. 0 with r->exists set, 1 when only the final component is missing, - * UNSEEN, GLUE, -errno, or FP_SLOW. - */ static long walk(struct res *r, const char *text, int follow, int last) { char rest[PATH_MAX]; size_t len = strlen(text); @@ -646,9 +522,6 @@ static long walk(struct res *r, const char *text, int follow, int last) { if (to_host(r->guest, r->host, &through) < 0) return FP_SLOW; long s = sc(SYS_newfstatat, AT_FDCWD, (long)r->host, (long)&r->st, AT_SYMLINK_NOFOLLOW, 0); if (s < 0) { - /* A directory above a binding that proot could not look at when it set the binding up (one - * missing, or debugfs) is its glue; any other component it cannot look at is ENOENT unless it - * is the last, which the call itself then meets. */ int glue = glue_above_binding(r->guest, glen + n); if (glue < 0) return FP_SLOW; if (glue) { @@ -656,7 +529,6 @@ static long walk(struct res *r, const char *text, int follow, int last) { c = e; continue; } - /* glibc's realpath() meets it as its readlink() fails. */ if (!final || !last) return r->libc ? s : -ENOENT; return s == -ENOENT ? 1 : UNSEEN; } @@ -695,7 +567,6 @@ static long walk(struct res *r, const char *text, int follow, int last) { c = e; } if (!stated) { - /* Ended on the root, ".", ".." or glue: the directory itself. */ if (to_host(r->guest, r->host, NULL) < 0) return FP_SLOW; long s = sc(SYS_newfstatat, AT_FDCWD, (long)r->host, (long)&r->st, AT_SYMLINK_NOFOLLOW, 0); if (s < 0 && strcmp(r->guest, "/") != 0 && glue_above_binding(r->guest, strlen(r->guest))) return FP_SLOW; @@ -705,8 +576,6 @@ static long walk(struct res *r, const char *text, int follow, int last) { return 0; } -/* The host directory a relative path starts in; -ENOTDIR, as proot answers, for a descriptor that - * is no file-system object (a pipe, a socket), or -1. */ static int base_of(int dirfd, char *h) { char link[40]; if (dirfd == AT_FDCWD) snprintf(link, sizeof link, "/proc/self/cwd"); @@ -718,19 +587,12 @@ static int base_of(int dirfd, char *h) { return gone(h, n) ? -1 : 0; } -/* - * Resolves the guest path `base`/`path` (base NULL for an absolute path). Directories are looked - * up in the cache by their literal absolute path; only a path with ".." in it is resolved in full. - * A trailing "/" asks for an existing directory, as the kernel does; what a missing one creates is - * the caller's to check. - */ static long resolve_in(const char *base, const char *path, int follow, struct res *r) { char norm[PATH_MAX]; size_t o = 0; int dotdot = 0; size_t plen = strlen(path); int trailing = plen > 1 && path[plen - 1] == '/'; - /* "//" and "/./" go; ".." stays, it is not lexical once symlinks are involved. */ for (int part = base ? 0 : 1; part < 2; part++) { for (const char *c = part ? path : base; *c;) { while (*c == '/') c++; @@ -771,12 +633,6 @@ static long resolve_in(const char *base, const char *path, int follow, struct re return w; } -/* - * Resolves `path` relative to dirfd, as the guest means it. When the starting directory has two - * guest names (a library bound twice), proot resolves from the one it was reached by, which is - * not known here: the answer stands only if every name leads to the same place. r->named is set - * when the result's guest path is the one proot would report (realpath). - */ static __attribute__((noinline)) long same_from_other_names(const char *h, const char *path, int follow, long rs, const struct res *r) { char base[PATH_MAX]; @@ -811,12 +667,10 @@ static long resolve_as(int dirfd, const char *path, int how, int libc, struct re return rs == UNSEEN && !(how & MAY_BE_UNSEEN) ? FP_SLOW : rs; } -/* `how`: whether to follow a final link (1), and MAY_BE_UNSEEN. */ static long resolve(int dirfd, const char *path, int how, struct res *r) { return resolve_as(dirfd, path, how, 0, r); } -/* "name/" makes the kernel follow a final link; calls that act on the link itself leave it to proot. */ static int trailing_slash(const char *path) { size_t len = path ? strlen(path) : 0; return len > 1 && path[len - 1] == '/'; @@ -831,11 +685,9 @@ static int ret(long r) { static __typeof__(type) real_##name; \ if (!real_##name) real_##name = (type)dlsym(RTLD_NEXT, #name) -/* An answer from here (r != FP_SLOW) or the real call. */ #define ANSWER(r, dirfd, path, real_call) \ do { long r_ = (r); if (r_ != FP_SLOW) { hits++; return ret(r_); } missed(__func__, dirfd, path); return real_call; } while (0) -/* ---------------------------------------------------------------- open */ static long fp_open(int dirfd, const char *path, int flags, mode_t mode) { struct res r; @@ -846,7 +698,6 @@ static long fp_open(int dirfd, const char *path, int flags, mode_t mode) { if (rs < 0) return rs; if (rs == 1 && !(flags & O_CREAT)) return -ENOENT; if (rs == UNSEEN) return sc(SYS_openat, AT_FDCWD, (long)r.host, flags, (flags & O_CREAT) ? mode : 0, 0); - /* Resolved here: the kernel must not follow a link that appeared since in host terms. */ return sc(SYS_openat, AT_FDCWD, (long)r.host, flags | (follow && !r.magic ? O_NOFOLLOW : 0), (flags & O_CREAT) ? mode : 0, 0); } @@ -879,11 +730,9 @@ int __openat64_2(int d, const char *path, int flags) { return openat(d, path, fl int creat(const char *path, mode_t mode) { return open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); } int creat64(const char *path, mode_t mode) __attribute__((alias("creat"))); -/* ---------------------------------------------------------------- stat family */ static long fp_stat(int dirfd, const char *path, int flags, struct stat *st) { struct res r; - /* fstat() is fstatat(fd, "", AT_EMPTY_PATH): nothing to translate. */ if ((flags & AT_EMPTY_PATH) && path && !path[0] && fp_ready()) return sc(SYS_newfstatat, dirfd, (long)"", (long)st, flags, 0); if (flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT)) return FP_SLOW; @@ -916,8 +765,6 @@ int lstat(const char *path, struct stat *st) { } int lstat64(const char *p, struct stat64 *st) { return lstat(p, (struct stat *)st); } -/* glibc turns fstat() into fstatat(fd, "", AT_EMPTY_PATH), which proot traps as a path call; the - * plain syscall is not traced at all. */ int fstat(int fd, struct stat *st) { REAL(fstat, int (*)(int, struct stat *)); if (!fp_ready()) return real_fstat(fd, st); @@ -926,13 +773,6 @@ int fstat(int fd, struct stat *st) { } int fstat64(int fd, struct stat64 *st) { return fstat(fd, (struct stat *)st); } -/* - * The pre-2.33 entry points. Programs linked against an older glibc - the Steam client, - * steamclient.so and filesystem_stdio.so among them - call these rather than stat(), so without - * them every stat the client makes goes to proot: a download's preallocation stats each file it - * creates. On aarch64 the only version is 0 (_STAT_VER), whose struct stat is the one above; - * glibc refuses any other with EINVAL, and so does this. - */ #define FP_STAT_VER 0 static int xstat_common(int ver, int dirfd, const char *path, struct stat *st, int flags) { REAL(fstatat, int (*)(int, const char *, struct stat *, int)); @@ -967,10 +807,7 @@ int statx(int dirfd, const char *path, int flags, unsigned mask, struct statx *o ANSWER(rs, dirfd, path, real_statx(dirfd, path, flags, mask, out)); } -/* ---------------------------------------------------------------- access */ -/* AT_EACCESS only differs from a plain check in a set-ID program, where glibc does the work itself; - * elsewhere glibc's own fallback is the plain faccessat, and so is this. */ static int effective_is_real(void) { return !getauxval(AT_SECURE) && getuid() == geteuid() && getgid() == getegid(); } @@ -979,8 +816,6 @@ static int access_common(int dirfd, const char *path, int mode, int flags) { REAL(faccessat, int (*)(int, const char *, int, int)); struct res r; if ((flags & AT_EACCESS) && effective_is_real()) flags &= ~AT_EACCESS; - /* faccessat(2) has no flags; AT_SYMLINK_NOFOLLOW (the Steam client passes it for every file it - * reserves) only differs on a symlink, which goes to proot. */ long rs = (flags & ~AT_SYMLINK_NOFOLLOW) == 0 ? resolve(dirfd, path, (flags & AT_SYMLINK_NOFOLLOW ? 0 : 1) | MAY_BE_UNSEEN, &r) : FP_SLOW; if (rs == 1) rs = -ENOENT; @@ -988,7 +823,6 @@ static int access_common(int dirfd, const char *path, int mode, int flags) { if (rs == 0 || rs == UNSEEN) rs = sc(SYS_faccessat, AT_FDCWD, (long)r.host, mode, 0, 0); if (rs != FP_SLOW) { hits++; return ret(rs); } missed(__func__, dirfd, path); - /* glibc tries faccessat2 first, which Android answers with SIGSYS: a second round trip. */ if (flags == 0) return (int)syscall(SYS_faccessat, dirfd, path, mode); return real_faccessat(dirfd, path, mode, flags); } @@ -997,13 +831,7 @@ int access(const char *path, int mode) { return access_common(AT_FDCWD, path, mo int eaccess(const char *path, int mode) { return access_common(AT_FDCWD, path, mode, AT_EACCESS); } int euidaccess(const char *path, int mode) __attribute__((alias("eaccess"))); -/* ---------------------------------------------------------------- readlink, realpath, getcwd */ -/* - * The text proot's readlink() gives for the link r names: the stored text as the kernel cuts it to - * `size` (0: no limit), detranslated. `changed`: proot wrote its own answer rather than leaving the - * kernel's. - */ static long link_text(const struct res *r, char *text, size_t size, int *changed) { int in_proc = under(r->guest, "/proc", 5); *changed = 0; @@ -1040,8 +868,6 @@ static ssize_t readlink_common(int dirfd, const char *path, char *buf, size_t si memcpy(buf, text, n); return n; } - /* As proot writes its answer: with the terminator when it fits, else cut at the buffer (or - * PATH_MAX) and reported as that long. */ size_t max = size < PATH_MAX ? size : PATH_MAX; if (n + 1 < max) { memcpy(buf, text, n + 1); @@ -1053,8 +879,6 @@ static ssize_t readlink_common(int dirfd, const char *path, char *buf, size_t si ssize_t readlinkat(int dirfd, const char *path, char *buf, size_t size) { return readlink_common(dirfd, path, buf, size); } ssize_t readlink(const char *path, char *buf, size_t size) { return readlink_common(AT_FDCWD, path, buf, size); } -/* _FORTIFY_SOURCE builds call these, which reach libc's own readlink and getcwd directly; an - * overflow is still the real one's to report. */ ssize_t __readlink_chk(const char *path, char *buf, size_t size, size_t room) { REAL(__readlink_chk, ssize_t (*)(const char *, char *, size_t, size_t)); if (size > room) return real___readlink_chk(path, buf, size, room); @@ -1066,9 +890,6 @@ ssize_t __readlinkat_chk(int dirfd, const char *path, char *buf, size_t size, si return readlink_common(dirfd, path, buf, size); } -/* glibc's realpath() reads every component as a link, each a trapped readlinkat with an exit stop: - * wineserver and Wine resolve their prefix that way for every file they open, and a file's name - * through /proc/self/fd. */ char *realpath(const char *path, char *resolved) { REAL(realpath, char *(*)(const char *, char *)); struct res r; @@ -1093,8 +914,6 @@ char *__realpath_chk(const char *path, char *resolved, size_t size) { } char *canonicalize_file_name(const char *path) { return realpath(path, NULL); } -/* proot keeps the guest cwd and answers getcwd on the way out; with the fast path the kernel's cwd - * is its host directory, which names it whenever only one guest path does. */ char *getcwd(char *buf, size_t size) { REAL(getcwd, char *(*)(char *, size_t)); char g[PATH_MAX]; @@ -1117,7 +936,6 @@ char *__getcwd_chk(char *buf, size_t size, size_t room) { return getcwd(buf, size); } -/* ---------------------------------------------------------------- changes */ int mkdirat(int dirfd, const char *path, mode_t mode) { REAL(mkdirat, int (*)(int, const char *, mode_t)); @@ -1128,7 +946,6 @@ int mkdirat(int dirfd, const char *path, mode_t mode) { } int mkdir(const char *path, mode_t mode) { return mkdirat(AT_FDCWD, path, mode); } -/* The last name in a path is "." or "..", which the kernel refuses to remove. */ static int ends_in_dots(const char *path) { size_t n = strlen(path); while (n > 1 && path[n - 1] == '/') n--; @@ -1143,7 +960,6 @@ static long fp_unlink(int dirfd, const char *path, int flags) { int slash = trailing_slash(path); if ((flags & ~AT_REMOVEDIR) || !path || ends_in_dots(path) || (slash && !(flags & AT_REMOVEDIR))) return FP_SLOW; if (slash) { - /* rmdir("dir/") removes dir itself, never what a link of that name points to. */ size_t n = strlen(path); if (n >= sizeof bare) return FP_SLOW; while (n > 1 && path[n - 1] == '/') n--; @@ -1172,8 +988,6 @@ int unlinkat(int dirfd, const char *path, int flags) { return unlink_common(dirf int unlink(const char *path) { return unlink_common(AT_FDCWD, path, 0); } int rmdir(const char *path) { return unlink_common(AT_FDCWD, path, AT_REMOVEDIR); } -/* A renamed directory moves the guest cwd proot keeps for every process under it, so those stay - * with proot; files are what programs rename all the time (write a copy, rename it over). */ static int rename_common(int olddirfd, const char *old, int newdirfd, const char *new, unsigned flags) { REAL(renameat2, int (*)(int, const char *, int, const char *, unsigned)); struct res from, to; @@ -1192,7 +1006,6 @@ static int rename_common(int olddirfd, const char *old, int newdirfd, const char } if (rs != FP_SLOW) { hits++; return ret(rs); } missed(__func__, olddirfd, old); - /* renameat, not renameat2: it is the one proot follows to move the cwd it keeps. */ if (!flags) return (int)syscall(SYS_renameat, olddirfd, old, newdirfd, new); return real_renameat2(olddirfd, old, newdirfd, new, flags); } @@ -1202,7 +1015,6 @@ int renameat2(int olddirfd, const char *old, int newdirfd, const char *new, unsi int renameat(int olddirfd, const char *old, int newdirfd, const char *new) { return rename_common(olddirfd, old, newdirfd, new, 0); } int rename(const char *old, const char *new) { return rename_common(AT_FDCWD, old, AT_FDCWD, new, 0); } -/* The link's text is stored as given, as proot stores it. */ int symlinkat(const char *target, int dirfd, const char *path) { REAL(symlinkat, int (*)(const char *, int, const char *)); struct res r; @@ -1230,7 +1042,7 @@ static int utimens_common(int dirfd, const char *path, const struct timespec tim struct res r; long rs; if (!path && fp_ready()) { - rs = sc(SYS_utimensat, dirfd, 0, (long)times, flags, 0); /* futimens() */ + rs = sc(SYS_utimensat, dirfd, 0, (long)times, flags, 0); } else { rs = flags & ~AT_SYMLINK_NOFOLLOW ? FP_SLOW : resolve(dirfd, path, (flags & AT_SYMLINK_NOFOLLOW ? 0 : 1) | MAY_BE_UNSEEN, &r); if (rs == 1) rs = -ENOENT; @@ -1273,7 +1085,6 @@ int truncate(const char *path, off_t length) { } int truncate64(const char *path, off_t length) __attribute__((alias("truncate"))); -/* proot reports /dev/shm as tmpfs on the way out of statfs; that stays with it. */ static int statfs_common(const char *path, struct statfs *buf, int (*real)(const char *, struct statfs *)) { struct res r; long rs = resolve(AT_FDCWD, path, 1, &r); @@ -1291,9 +1102,9 @@ int statfs64(const char *path, struct statfs64 *buf) { return statfs_common(path, (struct statfs *)buf, real_statfs64); } -#define FP_ST_VALID 0x0020 /* the kernel's ST_VALID: statfs filled in f_flags */ -/* statvfs() is statfs() converted as glibc converts it; without ST_VALID the mount flags would come - * from /proc/mounts, which glibc reads itself. */ +#define FP_ST_VALID 0x0020 + + static int statvfs_common(const char *path, struct statvfs *buf, int (*real)(const char *, struct statvfs *)) { struct res r; struct statfs fs; @@ -1328,8 +1139,6 @@ int statvfs64(const char *path, struct statvfs64 *buf) { return statvfs_common(path, (struct statvfs *)buf, real_statvfs64); } -/* Reading attributes only: fake_id0 rewrites the permission side of setting them. Wine reads - * user.DOSATTRIB for every file it reports on. */ static ssize_t getxattr_common(const char *path, const char *name, void *value, size_t size, int follow, ssize_t (*real)(const char *, const char *, void *, size_t)) { struct res r; @@ -1377,14 +1186,7 @@ int inotify_add_watch(int fd, const char *path, uint32_t mask) { ANSWER(rs, AT_FDCWD, path, real_inotify_add_watch(fd, path, mask)); } -/* ---------------------------------------------------------------- Unix sockets */ -/* - * proot only translates the path of a named Unix socket; every other address (abstract, unnamed, - * IP) passes through untouched, so the call itself needs no stop. A named one is resolved as proot - * would and sent with its host path, as long as that fits sun_path: a longer one proot binds to a - * short temporary name, so that stays with it. - */ static long socket_address(const struct sockaddr *addr, socklen_t len, int follow, struct sockaddr_un *out, socklen_t *out_len) { const struct sockaddr_un *un = (const struct sockaddr_un *)addr; size_t room = sizeof un->sun_path; @@ -1410,9 +1212,6 @@ static long socket_address(const struct sockaddr *addr, socklen_t len, int follo return 1; } -/* accept() and connect() are cancellation points in libc; a raw call is not, so only those that - * cannot block for long come here: a non-blocking socket, a listener with a connection waiting, - * or connecting a Unix one. */ static int nonblocking(int fd) { long fl = sc(SYS_fcntl, fd, F_GETFL, 0, 0, 0); return fl >= 0 && (fl & O_NONBLOCK); @@ -1439,8 +1238,6 @@ int bind(int fd, const struct sockaddr *addr, socklen_t len) { ANSWER(rs, AT_FDCWD, "(bind)", real_bind(fd, addr, len)); } -/* An address the kernel reports, as proot reports it: a named Unix socket's host path in guest - * terms. One no guest path names unambiguously is left as is, which proot cannot do better. */ static void report_address(const struct sockaddr_storage *got, socklen_t got_len, struct sockaddr *addr, socklen_t *len) { struct sockaddr_storage shown = *got; socklen_t shown_len = got_len; @@ -1500,10 +1297,7 @@ int getpeername(int fd, struct sockaddr *addr, socklen_t *len) { return name_common(SYS_getpeername, fd, addr, len, real_getpeername); } -/* ---------------------------------------------------------------- libc calls that open internally */ -/* shm_open() and shm_unlink() open /dev/shm/<name> with libc's internal calls; an overlay asks for - * FEX's statistics that way every frame. The name is checked and the file opened as glibc does. */ static int shm_path(const char *name, char *path) { while (*name == '/') name++; size_t n = strlen(name); @@ -1561,8 +1355,6 @@ static FILE *fopen_common(const char *path, const char *m, FILE *(*real)(const c FILE *fopen(const char *path, const char *m) { REAL(fopen, FILE *(*)(const char *, const char *)); return fopen_common(path, m, real_fopen); } FILE *fopen64(const char *path, const char *m) { REAL(fopen64, FILE *(*)(const char *, const char *)); return fopen_common(path, m, real_fopen64); } -/* glibc reads /sys/devices/system/cpu/online with its own open for every get_nprocs(), and - * sysconf(_SC_NPROCESSORS_ONLN) is that: overlays and thread pools ask it every frame. */ static int online_cpus(void) { long fd = fp_open(AT_FDCWD, "/sys/devices/system/cpu/online", O_RDONLY | O_CLOEXEC, 0); if (fd < 0) return -1; @@ -1595,11 +1387,7 @@ long sysconf(int name) { return real_sysconf(name); } -/* ---------------------------------------------------------------- uname */ -/* proot answers uname() with the utsname it started with (--kernel-release), the same for every - * call: asked once, it is every later answer. libX11 asks for each display connection, glibc's - * gethostname() through it. */ static struct utsname uts; static int uts_known; static long own_uname(struct utsname *u) { @@ -1639,8 +1427,6 @@ int gethostname(char *name, size_t len) { return 0; } -/* glibc's isatty() asks with TCGETS2, which proot rewrites to TCGETS (Android allows no termios2 - * request on a tty): one stop each way for every check. libudev and shells check all the time. */ int isatty(int fd) { REAL(isatty, int (*)(int)); struct termios k; @@ -1651,9 +1437,6 @@ int isatty(int fd) { return r == 0; } -/* proot reads a memfd's name only to refuse the few that are then executed or locked; the rest are - * made here. The session's /proc/net and status copies, wineserver and Xwayland make them often. - * Without memfd in the kernel, proot's ashmem stands in. */ int memfd_create(const char *name, unsigned flags) { REAL(memfd_create, int (*)(const char *, unsigned)); if (!name || !strncmp(name, "JITCode:", 8) || !strcmp(name, "opcache_lock") || !strncmp(name, "lib/apk/exec/", 13) @@ -1664,16 +1447,7 @@ int memfd_create(const char *name, unsigned flags) { return ret(r); } -/* ---------------------------------------------------------------- passwd */ -/* - * glibc's NSS stats /etc/nsswitch.conf and reads /etc/passwd with its own calls on every lookup, - * two stops each: Xwayland looks a user up for every client, every shell for itself. When - * nsswitch.conf sends passwd to "files" first with no action after it, a lookup that finds its - * entry there ends there, so that case is answered here from the same file. Anything else - not - * found, a file nss_files might read differently from this strict reading, a buffer it might call - * too small - goes to glibc. - */ static long read_small(const char *path, char *buf, size_t size) { long fd = fp_open(AT_FDCWD, path, O_RDONLY | O_CLOEXEC, 0); if (fd < 0) return -1; @@ -1707,8 +1481,6 @@ static int passwd_is_files(char *conf, size_t size) { return found; } -/* The line for `name` (or `uid`, name NULL) split in place into its seven fields; -1 for a file - * read strictly that has none, 0 when this cannot tell. `longest`: the longest line read up to it. */ static int passwd_line(char *file, const char *name, uid_t uid, char **f, size_t *longest) { *longest = 0; for (char *line = file; line && *line;) { @@ -1733,7 +1505,6 @@ static int passwd_line(char *file, const char *name, uid_t uid, char **f, size_t return -1; } -/* 1 with *pw filled from buf, 0 to leave it to glibc. */ static int own_passwd(const char *name, uid_t uid, struct passwd *pw, char *buf, size_t buflen) { char file[16384], *f[7]; size_t longest; @@ -1783,7 +1554,6 @@ int getpwuid_r(uid_t uid, struct passwd *pw, char *buf, size_t buflen, struct pa return passwd_r(NULL, uid, pw, buf, buflen, result, NULL, real_getpwuid_r); } -/* glibc's own getpwnam/getpwuid keep one static entry, as these do. */ static struct passwd static_pw; static char static_pw_buf[4096]; static pthread_mutex_t static_pw_lock = PTHREAD_MUTEX_INITIALIZER; diff --git a/tools/proot/patches/0004-seccomp-filter-by-argument.patch b/tools/proot/patches/0004-seccomp-filter-by-argument.patch index 302a97eba..f5497337d 100644 --- a/tools/proot/patches/0004-seccomp-filter-by-argument.patch +++ b/tools/proot/patches/0004-seccomp-filter-by-argument.patch @@ -101,8 +101,8 @@ index 6d1ba47..8557d4a 100644 * Append to @program->filter the statements that allow anything (if - * unfiltered). Note that @nb_traced_syscalls is used to make a - * sanity check. This function returns -errno if an error occurred, -+ * unfiltered). Note that @section_length is used to make a sanity -+ * check. This function returns -errno if an error occurred, ++ ++ * otherwise 0. */ -static int end_arch_section(struct sock_fprog *program, size_t nb_traced_syscalls) @@ -125,7 +125,7 @@ index 6d1ba47..8557d4a 100644 /** * Append to @program->filter the statements that check the current - * @architecture. Note that @nb_traced_syscalls is used to make a -+ * @architecture. Note that @section_length is used to make a ++ * sanity check. This function returns -errno if an error occurred, * otherwise 0. */ diff --git a/tools/proot/patches/0010-new-mount-api-enosys.patch b/tools/proot/patches/0010-new-mount-api-enosys.patch index 87fd43d1f..f10c1fb2a 100644 --- a/tools/proot/patches/0010-new-mount-api-enosys.patch +++ b/tools/proot/patches/0010-new-mount-api-enosys.patch @@ -15,8 +15,8 @@ index 1d3b3fd..32d2503 100644 status = -ENOSYS; break; -+ /* The new mount API opens and moves paths proot never sees: open_tree(AT_FDCWD, "/") -+ * hands back the host's root. Callers (libglnx, systemd) fall back to openat. */ ++ ++ + case PR_open_tree: + case PR_move_mount: + case PR_fspick: diff --git a/tools/proot/patches/0011-kompat-utsname-only.patch b/tools/proot/patches/0011-kompat-utsname-only.patch index 9c1949cce..407970b25 100644 --- a/tools/proot/patches/0011-kompat-utsname-only.patch +++ b/tools/proot/patches/0011-kompat-utsname-only.patch @@ -16,9 +16,9 @@ index 5c39942..4cb7dec 100644 FILTERED_SYSNUM_END, }; -+/* When the virtual kernel is not older than the actual one and the -+ * hwcap is left alone, the only thing left to emulate is the utsname -+ * itself: every other handler above returns without a change. */ ++ ++ ++ +static FilteredSysnum utsname_only_sysnums[] = { + { PR_setdomainname, FILTER_SYSEXIT }, + { PR_sethostname, FILTER_SYSEXIT }, @@ -26,12 +26,12 @@ index 5c39942..4cb7dec 100644 + FILTERED_SYSNUM_END, +}; + -+/** -+ * Return whether @config only renames the kernel identity (nodename, -+ * domainname, version string) without emulating an older kernel or -+ * another hwcap, in which case futex, fcntl, epoll_pwait, ... don't -+ * have to stop in the tracer and the vDSO can be left in place. -+ */ ++ ++ ++ ++ ++ ++ +static bool utsname_only(const Config *config) +{ + return config->actual_release != 0 diff --git a/tools/proot/patches/0012-android-hardlink-denial.patch b/tools/proot/patches/0012-android-hardlink-denial.patch index e2d089fb3..b8346a8cd 100644 --- a/tools/proot/patches/0012-android-hardlink-denial.patch +++ b/tools/proot/patches/0012-android-hardlink-denial.patch @@ -19,7 +19,7 @@ index 8c3eb2e..4c28e10 100644 case PR_open: flags = peek_reg(tracee, CURRENT, SYSARG_2); +#ifdef __ANDROID__ -+ /* An O_TMPFILE file can only be named by linkat, which Android denies apps. */ ++ + if ((flags & __O_TMPFILE) != 0) { + status = -EOPNOTSUPP; + break; @@ -50,8 +50,8 @@ index df41084..5d981db 100644 break; +#ifdef __ANDROID__ -+ /* Android denies apps hard links with EACCES; EPERM is what callers such as -+ * libostree take as "no hard links here" and copy instead. */ ++ ++ + case PR_link: + case PR_linkat: + if ((int) syscall_result == -EACCES) diff --git a/tools/proot/patches/0012-fake_id0-identity-only.patch b/tools/proot/patches/0012-fake_id0-identity-only.patch index 155b62b8e..73f9ac35e 100644 --- a/tools/proot/patches/0012-fake_id0-identity-only.patch +++ b/tools/proot/patches/0012-fake_id0-identity-only.patch @@ -17,7 +17,7 @@ index 2e78783..5227ced 100644 #define FAKE_ID0_CONFIG_H #include <sys/types.h> /* uid_t, gid_t */ -+#include <stdbool.h> /* bool, */ ++#include <stdbool.h> typedef struct { uid_t ruid; @@ -26,8 +26,8 @@ index 2e78783..5227ced 100644 mode_t umask; + -+ /* The emulated ids are the ones PRoot really has: see -+ * identity_sysnums in fake_id0.c. */ ++ ++ + bool identity; } Config; @@ -40,17 +40,17 @@ index 9477f1c..fe08372 100644 FILTERED_SYSNUM_END, }; -+/* When the emulated ids are the ones PRoot really has, and the tracee -+ * is not root, every handler for the syscalls above but these is a -+ * no-op: stat and get*id report the real ids already, SCM_CREDENTIALS -+ * and SO_PEERCRED carry them, chown swaps an id for itself, and the -+ * permission and chroot fixups only apply to euid 0. The set*id -+ * family is still emulated (Android's app seccomp policy traps it) and -+ * so are the xattr permission fixups, which apply to any euid. An -+ * unprivileged set*id can only take an id the tracee already holds, -+ * and set-user-ID bits are not honoured in this mode (Android mounts -+ * the app's data nosuid, so the kernel would not honour them either), -+ * so the ids never change and the list stays sufficient. */ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +static FilteredSysnum identity_sysnums[] = { + { PR_fsetxattr, FILTER_SYSEXIT }, + { PR_lsetxattr, FILTER_SYSEXIT }, @@ -81,8 +81,8 @@ index 9477f1c..fe08372 100644 if ((int) result < 0 || tracee->status < 0 || sysnum != PR_execve) return 0; -+ /* The auxv already carries the real ids, and set-user-ID bits -+ * are not honoured in this mode: see identity_sysnums. */ ++ ++ + if (config->identity) + return 0; + diff --git a/tools/proot/patches/0013-seccomp-ioctl-by-request-and-kernel-exit-stops.patch b/tools/proot/patches/0013-seccomp-ioctl-by-request-and-kernel-exit-stops.patch index eaf62c565..08fb2954c 100644 --- a/tools/proot/patches/0013-seccomp-ioctl-by-request-and-kernel-exit-stops.patch +++ b/tools/proot/patches/0013-seccomp-ioctl-by-request-and-kernel-exit-stops.patch @@ -16,11 +16,11 @@ index 0b381bb..1116ab2 100644 #include <stdint.h> /* uint*_t, UINT*_MAX, */ #include <stdbool.h> #include <sys/resource.h> -+#include <sys/utsname.h> /* uname(2), */ -+#include <stdio.h> /* sscanf(3), */ -+#include <asm/ioctls.h> /* TCGETS2, TCSETS*2, */ -+#include <asm/termbits.h> /* struct termios2, */ -+#include <linux/fs.h> /* FICLONE, */ ++#include <sys/utsname.h> ++#include <stdio.h> ++#include <asm/ioctls.h> ++#include <asm/termbits.h> ++#include <linux/fs.h> #include <assert.h> /* assert(3), */ #include "syscall/seccomp.h" @@ -28,27 +28,27 @@ index 0b381bb..1116ab2 100644 return add_statements(program, LENGTH_TRACE_PRLIMIT_STACK, statements); } -+/** -+ * Append to @program->filter the statements that trace @syscall -+ * (ioctl) only for the requests enter.c and exit.c act on - the -+ * termios requests Android's policy denies an app on a pty, rewritten -+ * to ones it allows, and FICLONE, whose EACCES is turned into -+ * EOPNOTSUPP - and allow any other request at once. Every GPU submit -+ * and wait is an ioctl, so they no longer stop in the tracer, and the -+ * block is emitted first so they are also spared the rest of the list. -+ * (After WinNative, maxjivi05, 53836ca9.) -+ */ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +static int add_trace_ioctl_requests(struct sock_fprog *program, word_t syscall, int flag) +{ + const size_t request_offset = offsetof(struct seccomp_data, args[1]); + + #define LENGTH_TRACE_IOCTL 10 + struct sock_filter statements[LENGTH_TRACE_IOCTL] = { -+ /* Not this syscall: skip the block, the accumulator -+ * still holds the syscall number. */ ++ ++ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, syscall, 0, 9), + -+ /* Load the request's low word into the accumulator. */ ++ + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, request_offset), + + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCSETSF, 5, 0), @@ -102,8 +102,8 @@ index 0b381bb..1116ab2 100644 if (status < 0) goto end; -+ /* Two passes: ioctl first, it is by far the most frequent -+ * syscall and almost every one of them is allowed. */ ++ ++ + for (pass = 0; pass < 2; pass++) for (j = 0; j < seccomp_archs[i].nb_abis; j++) { for (k = 0; sysnums[k].value != PR_void; k++) { @@ -117,15 +117,15 @@ index 0b381bb..1116ab2 100644 return 0; } -+/** -+ * Drop the exit stops PRoot only needs on kernels older than the one it -+ * runs on. The faccessat2 exit stop restores the arguments for glibc's -+ * fallback to faccessat on ENOSYS (kernels before 5.8, termux 5ba8b95); -+ * the statx one emulates the call when it fails, which only a kernel -+ * before 4.11 does for want of it - a statx Android's policy traps is -+ * emulated from SIGSYS instead. Both stops re-read and re-translate -+ * the path, and are paid on every call. -+ */ ++ ++ ++ ++ ++ ++ ++ ++ ++ +static void drop_unneeded_exit_stops(void) +{ + static bool done = false; diff --git a/tools/proot/patches/0014-fastpath-trampoline.patch b/tools/proot/patches/0014-fastpath-trampoline.patch index a865a5046..8daacbbee 100644 --- a/tools/proot/patches/0014-fastpath-trampoline.patch +++ b/tools/proot/patches/0014-fastpath-trampoline.patch @@ -22,7 +22,7 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. -#include <errno.h> /* errno(3), E* */ +#include <errno.h> -+#include <stdlib.h> /* errno(3), E* */ ++#include <stdlib.h> #include <talloc.h> /* talloc_*, */ #include <sys/un.h> /* struct sockaddr_un, */ #include <linux/net.h> /* SYS_*, */ @@ -30,15 +30,15 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. if ((statl.st_mode & S_IXUSR) == 0) return -EACCES; -+ /* With the fast path, the kernel's cwd follows the guest's -+ * (at its host path), so relative lookups made inside the -+ * tracee resolve where proot would resolve them. */ ++ ++ ++ + if (getenv("PROOT_FASTPATH") != NULL) { + int fd = open(path, O_PATH | O_DIRECTORY | O_CLOEXEC); + -+ /* Where the kernel could not follow, it goes to the -+ * host's "/", which no binding names: the fast path -+ * then sends every relative lookup to proot. */ ++ ++ ++ + strcpy(oldpath, fd >= 0 ? path : "/"); + if (fd >= 0) + close(fd); @@ -52,7 +52,7 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. talloc_set_name_const(tracee->fs->cwd, "$cwd"); + if (getenv("PROOT_FASTPATH") != NULL) { -+ /* fchdir(2) runs as it is; chdir(2) on the host path. */ ++ + status = syscall_number == PR_chdir + ? set_sysarg_path(tracee, oldpath, SYSARG_1) + : 0; @@ -68,7 +68,7 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. #include <talloc.h> /* talloc_*, */ #include <errno.h> /* E*, */ #include <string.h> /* memcpy(3), */ -+#include <stdlib.h> /* getenv(3), */ ++#include <stdlib.h> #include <stddef.h> /* offsetof(3), */ #include <stdint.h> /* uint*_t, UINT*_MAX, */ #include <stdbool.h> @@ -76,11 +76,11 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. * items). This function returns -errno if an error occurred, * otherwise 0. */ -+/* The fast path's trampoline page (tools/proot/fastpath): a traced syscall made from it was -+ * already translated inside the tracee and is let through without a stop. With PROOT_FASTPATH -+ * set, every SECCOMP_RET_TRACE is preceded by a check of the caller's address. The check sits -+ * after the syscall-number dispatch, so syscalls proot never traces stay constant-ALLOW and keep -+ * the kernel's seccomp action cache. FASTPATH_STUB_ADDR must match FP_STUB_ADDR in fastpath.c. */ ++ ++ ++ ++ ++ +#define FASTPATH_STUB_ADDR 0xffff00000ULL +#define FASTPATH_STUB_SIZE 0x1000ULL +#define LENGTH_FASTPATH_TAIL 7 @@ -102,8 +102,8 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. +static int add_statements_raw(struct sock_fprog *program, size_t nb_statements, + struct sock_filter *statements); + -+/* Append @statements, each RET_TRACE expanded into the trampoline check when the fast path is -+ * on, every jump within (or out of) the block adjusted for the expansion. */ ++ ++ static int add_statements(struct sock_fprog *program, size_t nb_statements, struct sock_filter *statements) { @@ -176,7 +176,7 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. + static size_t trace_length(Sysnum sysnum) { -+ /* Each trace block has exactly one RET_TRACE (see add_statements). */ ++ + return trace_length_raw(sysnum) + (fastpath_enabled() ? LENGTH_FASTPATH_TAIL - 1 : 0); +} + @@ -191,9 +191,9 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. if (getenv("PROOT_NO_SECCOMP") == NULL) (void) enable_syscall_filtering(tracee); -+ /* The fast path resolves relative paths against the -+ * kernel's cwd: start it at the guest's (see chdir in -+ * syscall/enter.c). */ ++ ++ ++ + if (getenv("PROOT_FASTPATH") != NULL) { + char host_cwd[PATH_MAX]; + if (translate_path(tracee, host_cwd, AT_FDCWD, tracee->fs->cwd, true) >= 0) diff --git a/tools/quest/android_wrapper.c b/tools/quest/android_wrapper.c index 241ad7846..6f211b1be 100644 --- a/tools/quest/android_wrapper.c +++ b/tools/quest/android_wrapper.c @@ -1,7 +1,4 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later - * Loaded as the AdrenoTools driver; DT_NEEDED points to the original Turnip. - * Vulkan/HAL symbols are resolved from that dependency. Its ioctl calls resolve here. - */ +/* SPDX-License-Identifier: GPL-3.0-or-later */ #define _GNU_SOURCE #include "kgsl_compat.h" #include <dlfcn.h> diff --git a/tools/quest/build-driver.py b/tools/quest/build-driver.py index f5376f20b..e72b2991e 100644 --- a/tools/quest/build-driver.py +++ b/tools/quest/build-driver.py @@ -94,7 +94,6 @@ def build(ndk, output=OUTPUT): with tempfile.TemporaryDirectory(prefix='quest-driver-') as folder: work = Path(folder) with tarfile.open(fileobj=io.BytesIO(data)) as archive: - # Only read the known driver entry; no archive paths are extracted. driver = archive.extractfile('vulkan.ad07xx.so').read() (work / 'vulkan.ad07xx.so').write_bytes(driver) wrapper = work / 'libquest_turnip.so' diff --git a/tools/quest/kgsl_compat.c b/tools/quest/kgsl_compat.c index 4b1ab6684..33f4cfe18 100644 --- a/tools/quest/kgsl_compat.c +++ b/tools/quest/kgsl_compat.c @@ -1,8 +1,4 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later - * Quest experimental build, 2026-10-11. - * Horizon permits GPUOBJ allocation, but rejects the legacy GPUMEM alloc/free calls. - * Retry only those two calls after EACCES/EPERM. No change to device permissions. - */ +/* SPDX-License-Identifier: GPL-3.0-or-later */ #include "kgsl_compat.h" #include <errno.h> #include <limits.h> @@ -22,7 +18,6 @@ _Static_assert(sizeof(struct kgsl_gpuobj_info) == 48, "GPUOBJ info ABI"); static void report(const char *operation, int error) { static unsigned successes, failures; unsigned *counter = error ? &failures : &successes; - /* Log the first success and at most eight failures per process. */ if (__atomic_fetch_add(counter, 1, __ATOMIC_RELAXED) >= (error ? 8u : 1u)) return; int saved = errno; #ifdef __ANDROID__ @@ -52,7 +47,6 @@ int quest_kgsl_ioctl(quest_ioctl_fn real, int fd, unsigned long request, void *a error = EOVERFLOW; } if (rc != 0) { - /* The caller never receives this allocation, so clean it up on an INFO failure. */ struct kgsl_gpuobj_free release = {.id = alloc.id}; if (real(fd, IOCTL_KGSL_GPUOBJ_FREE, &release) != 0) report("cleanup failed", errno); diff --git a/tools/quest/kgsl_compat.h b/tools/quest/kgsl_compat.h index f54e334ec..b6f0eb053 100644 --- a/tools/quest/kgsl_compat.h +++ b/tools/quest/kgsl_compat.h @@ -2,7 +2,6 @@ #ifndef DROIDDECK_QUEST_KGSL_COMPAT_H #define DROIDDECK_QUEST_KGSL_COMPAT_H -/* Use the vendored Qualcomm UAPI, including its architecture-dependent ioctl sizes. */ #ifndef __user #define __user #endif diff --git a/tools/release/axml_rename.py b/tools/release/axml_rename.py index 92da2753a..24fba660c 100644 --- a/tools/release/axml_rename.py +++ b/tools/release/axml_rename.py @@ -1,6 +1,4 @@ #!/usr/bin/env python3 -# axml_rename.py in.apk out.apk old=new ... : rewrite whole strings in AndroidManifest.xml's string -# pool (binary XML), leaving every other entry of the apk byte-identical. Unsigned output. import struct, sys, zipfile def dec_len8(b, o): diff --git a/tools/release/runtime_inputs.py b/tools/release/runtime_inputs.py index eac1c27c8..6c7944ce1 100644 --- a/tools/release/runtime_inputs.py +++ b/tools/release/runtime_inputs.py @@ -95,7 +95,6 @@ def required(): def record(root=ROOT): - # Refresh scripts even on a CI native-cache hit; these are source, not cached binaries. if root == ROOT: from check_session_assets import source_assets for source, asset in source_assets().items(): @@ -112,7 +111,6 @@ def record(root=ROOT): gzip.decompress((root / path).read_bytes()) except (OSError, EOFError) as error: raise ValueError('Invalid desktop removal inventory: ' + name) from error - # Check inside the compressed payload, where a plain Gradle build used to ship no sinks. process = subprocess.Popen(['zstd', '-dc', str(root / paths['assets/pulseaudio.tzst'])], stdout=subprocess.PIPE) try: with tarfile.open(fileobj=process.stdout, mode='r|') as archive: diff --git a/tools/release/sign-apk.sh b/tools/release/sign-apk.sh index 63475b400..351e2fb0b 100755 --- a/tools/release/sign-apk.sh +++ b/tools/release/sign-apk.sh @@ -1,20 +1,4 @@ #!/usr/bin/env bash -# sign-apk.sh <in.apk> <variant> <out.apk> -# -# Turns a CI build (signed with the public AOSP testkey) into one package of tools/release/variants.txt -# signed with DroidDeck's own key, carrying the hand-over from the testkey: -# - v1 + v2 are signed by the testkey, v3 by our key, with a signed lineage testkey -> our key; -# - an install from before (testkey) updates to it and keeps its data; -# - the testkey loses its rollback right, so an apk signed with the testkey alone - which anyone -# can make - is refused as an update to it (device-proven on the Pocket FIT, 2026-09-24); -# - --rotation-min-sdk-version 28: the lineage applies from Android 9, not only from 13. -# -# The key comes from the environment and is never written anywhere but the path given: -# RELEASE_KEYSTORE path to the PKCS12 keystore -# RELEASE_STORE_PASSWORD its password (the key has the same one: PKCS12) -# RELEASE_KEY_ALIAS the key's alias -# BUILD_TOOLS Android build-tools dir (zipalign, apksigner, aapt) -# RELEASE_SIGNER_SHA256 optional: the expected certificate digest, else keystore/release-signer.sha256 set -euo pipefail die() { echo "::error::$*" >&2; exit 1; } @@ -37,7 +21,6 @@ export TESTKEY_PASSWORD=android work=$(mktemp -d) trap 'rm -rf "$work"' EXIT -# The package name and everything the manifest derives from it; component class names stay. P=com.droiddeck.launcher if [ "$pkg" = "$P" ]; then python3 "$here/axml_rename.py" "$in" "$work/renamed.apk" @@ -50,7 +33,6 @@ fi got=$("$BUILD_TOOLS/aapt" dump badging "$work/renamed.apk" | sed -n "s/^package: name='\([^']*\)'.*/\1/p") [ "$got" = "$pkg" ] || die "$variant: the apk says package '$got', not '$pkg'" if [ "$pkg" != "$P" ]; then - # A provider left on the old authority would clash with the standard app installed beside it. tree=$("$BUILD_TOOLS/aapt" dump xmltree "$work/renamed.apk" AndroidManifest.xml) grep -E 'android:(authorities|taskAffinity)' <<<"$tree" | grep -F "\"$P." \ && die "$variant: the manifest still names $P in an authority or task" @@ -58,8 +40,6 @@ fi "$BUILD_TOOLS/zipalign" -p -f 4 "$work/renamed.apk" "$work/aligned.apk" -# The lineage: the testkey vouches for our key. Made fresh each time (it is two certificates and -# a signature, nothing secret beyond the key that signs it). "$BUILD_TOOLS/apksigner" rotate --out "$work/lineage" \ --old-signer --ks "$TESTKEY" --ks-type PKCS12 --ks-pass env:TESTKEY_PASSWORD \ --ks-key-alias testkey --key-pass env:TESTKEY_PASSWORD --set-rollback false \ @@ -76,13 +56,9 @@ fi --out "$out" "$work/aligned.apk" rm -f "$out.idsig" -# What the apk must carry, checked rather than assumed. "$BUILD_TOOLS/apksigner" verify --min-sdk-version 26 "$out" >/dev/null || die "$variant: does not verify" -# (Listed first, then searched: grep -q stops reading early, which fails a pipe under pipefail.) listing=$(unzip -l "$out") grep -qE 'META-INF/.*\.(SF|RSA)$' <<<"$listing" || die "$variant: no JAR signature (old installers)" -# Android 9+ must see our key and only our key. apksigner labels signers "Signer #1", "Signer -# (minSdkVersion=28, ...)" or "V3.0 Signer:" depending on its version; the digests are what count. certs=$("$BUILD_TOOLS/apksigner" verify --min-sdk-version 28 --print-certs "$out") v3=$(sed -n 's/^.*Signer.* certificate SHA-256 digest: //p' <<<"$certs" | sort -u) if [ "$v3" != "$expected" ]; then diff --git a/tools/release/update_catalog.py b/tools/release/update_catalog.py index b16e4eab3..808e576a0 100755 --- a/tools/release/update_catalog.py +++ b/tools/release/update_catalog.py @@ -368,8 +368,6 @@ def build_catalog(source: GitHub, ci: GitHub, source_repo: str, ci_repo: str) -> if version_code_text and version_code_text.isdigit(): version_code = int(version_code_text) else: - # Legacy test releases did not record it. If the PR did not change build.gradle, - # the base value is exact; otherwise use the PR head's value. version_code = base_code try: changed = source.get(f"/repos/{source_repo}/pulls/{pr}/files?per_page=100") diff --git a/tools/release/variants.txt b/tools/release/variants.txt index 607106800..0df71a299 100644 --- a/tools/release/variants.txt +++ b/tools/release/variants.txt @@ -1,8 +1,3 @@ -# The packages a signed build ships as. The same apk, with only the manifest's package name (and -# the names derived from it) changed: some phones give their performance modes only to apps with -# certain names. Each is its own app to Android - installs beside the others, with its own data. -# -# name package file suffix standard com.droiddeck.launcher - pubg com.tencent.ig -pubg antutu com.antutu.benchmark.full -antutu diff --git a/tools/test_local.sh b/tools/test_local.sh index a2a69da62..bd29aef7d 100755 --- a/tools/test_local.sh +++ b/tools/test_local.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# Session helpers and ELF preload tests need Linux (/proc, inotify and glibc), as in CI. set -euo pipefail repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) cd "$repo_root" diff --git a/tools/tests/effects_source_cache_harness.c b/tools/tests/effects_source_cache_harness.c index e45e4bb78..1d355c244 100644 --- a/tools/tests/effects_source_cache_harness.c +++ b/tools/tests/effects_source_cache_harness.c @@ -3,7 +3,6 @@ #include <stdio.h> #include <string.h> -/* Exercise the real cache with Vulkan lifetime callbacks, without an Android surface or GPU. */ #define VK_PRESENT_H const char *vkp_gpu_name(void) { return "cache test"; } void droiddeck_log(const char *tag, const char *fmt, ...) { (void)tag; (void)fmt; } @@ -61,7 +60,6 @@ int main(int argc, char **argv) { assert(scene_ensure(source, 1280, 720) == 0); VkImageView original = bound_view; if (!strcmp(argv[1], "recycled")) { - /* The owner destroys the image, then Vulkan reuses its handle at the same size. */ vkp_effects_forget_image(source); assert(freed_sets == 1 && freed_views == 1); assert(scene_ensure(source, 1280, 720) == 0); diff --git a/tools/tests/sysv_otime_harness.c b/tools/tests/sysv_otime_harness.c index 2c362189e..6fa57ca73 100644 --- a/tools/tests/sysv_otime_harness.c +++ b/tools/tests/sysv_otime_harness.c @@ -1,6 +1,3 @@ -/* Host test for sysv.c: IPC_STAT's sem_otime turns non-zero with the first semop, which the usual - * race-free init (and SteamVR's web helper) waits for. Linked with sysv.c, whose semget/semop/semctl - * replace libc's. */ #define _GNU_SOURCE #include <stdio.h> #include <sys/ipc.h> diff --git a/tools/tests/termux_jni_harness.c b/tools/tests/termux_jni_harness.c index 66dc923c8..049419ed2 100644 --- a/tools/tests/termux_jni_harness.c +++ b/tools/tests/termux_jni_harness.c @@ -1,6 +1,3 @@ -// Drives Java_com_termux_terminal_JNI_createSubprocess against a mock JNIEnv that tracks local -// references the way ART does, so the JNI hygiene of app/src/main/cpp/termux/termux.c can be -// checked on the host. Built and run by tools/tests/test_termux_jni.py. #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -26,7 +23,6 @@ static jsize mock_array_length(JNIEnv* TERMUX_UNUSED(env), jarray array) static jobject mock_get_element(JNIEnv* TERMUX_UNUSED(env), jobjectArray TERMUX_UNUSED(array), jsize TERMUX_UNUSED(index)) { if (++live_refs > peak_refs) peak_refs = live_refs; - // ART aborts the process here: "local reference table overflow (max=512)". if (live_refs > LOCAL_REF_LIMIT) overflowed = 1; return (jobject) malloc(1); } @@ -53,7 +49,6 @@ static void mock_release_utf(JNIEnv* TERMUX_UNUSED(env), jstring string, const c { for (size_t i = 0; i < sizeof(pinned) / sizeof(pinned[0]); i++) { if (pinned[i].owner != NULL && pinned[i].chars == chars) { - // Releasing chars against some other string is a JNI contract violation. if (pinned[i].owner != string) mismatched_releases++; free(pinned[i].chars); pinned[i].owner = NULL; @@ -83,7 +78,6 @@ int main(int argc, char** argv) jsize arg_count = atoi(argv[1]); jsize env_count = atoi(argv[2]); - // The struct is JNINativeInterface in the NDK's header and JNINativeInterface_ in the JDK's. __typeof__(**(JNIEnv*) 0) table = { .GetArrayLength = mock_array_length, .GetObjectArrayElement = mock_get_element, @@ -97,7 +91,6 @@ int main(int argc, char** argv) }; const __typeof__(table)* env = &table; - // Arrays are modelled as their length; the caller's own references are not counted. jsize args = arg_count, envs = env_count; jint pid_slot[1] = {0}; jstring cmd = (jstring) "cmd", cwd = (jstring) "cwd"; diff --git a/tools/tests/test_binfmt_preload.py b/tools/tests/test_binfmt_preload.py index b1b0c0940..3a940b6b8 100644 --- a/tools/tests/test_binfmt_preload.py +++ b/tools/tests/test_binfmt_preload.py @@ -46,7 +46,6 @@ int main(int argc, char **argv) { } """ -# Shell builtins only: anything it ran would be an x86 program, handed back to it. HANDLER = "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$HANDLER_LOG\"\nexit 7\n" diff --git a/tools/tests/test_cardboard.py b/tools/tests/test_cardboard.py index fa6f3d120..8e1d26080 100644 --- a/tools/tests/test_cardboard.py +++ b/tools/tests/test_cardboard.py @@ -28,8 +28,6 @@ class CardboardParamsTest(unittest.TestCase): @staticmethod def _env(folder): - # cardboard.h includes <vulkan/vulkan.h>; the host test only needs the types, so a stub - # header stands in when the Vulkan SDK is not installed. import os stub = Path(folder) / 'vulkan' stub.mkdir(exist_ok=True) @@ -50,7 +48,6 @@ class CardboardShaderTest(unittest.TestCase): 'cardboard_frag.h is stale: regenerate it from cardboard.frag (see cardboard.c)') def test_push_constants_fit_the_guaranteed_range(self): - # 7 vec4 = 112 bytes in the shader; Vulkan guarantees 128. glsl = (SRC / 'cardboard.frag').read_text() block = glsl[glsl.index('uniform PC {'):glsl.index('} pc;')] self.assertLessEqual(16 * len(re.findall(r'\bvec4\b', block)), 128) diff --git a/tools/tests/test_desktop_removal.py b/tools/tests/test_desktop_removal.py index 674b9643f..845e08357 100644 --- a/tools/tests/test_desktop_removal.py +++ b/tools/tests/test_desktop_removal.py @@ -13,8 +13,6 @@ class DesktopRemovalInventoryTest(unittest.TestCase): self.assertIn('usr/bin/lxqt-session', paths) self.assertIn('usr/bin/labwc', paths) self.assertIn('usr/local/bin/steamdeck-desktop', paths) - # The runtime pacman database did not protect libseat on the Thor. Removing it - # made gamescope and every Steam/native session exit 127 after desktop cleanup. self.assertNotIn('usr/lib/libseat.so.1', paths) self.assertNotIn('usr/lib/libseat.so', paths) self.assertFalse(any(p.startswith('usr/lib/libSDL2') for p in paths)) diff --git a/tools/tests/test_dircache_preload.py b/tools/tests/test_dircache_preload.py index 62792888c..50ad24d5f 100644 --- a/tools/tests/test_dircache_preload.py +++ b/tools/tests/test_dircache_preload.py @@ -21,7 +21,6 @@ FAKE = r""" int real_opendirs; -/* COARSE=1: directory stats that never change, like NTFS's whole-second times and fixed size. */ static void coarse(const char *path, struct stat *st) { if (getenv("COARSE") && strstr(path, "/fuse/") && S_ISDIR(st->st_mode)) { st->st_mtim = st->st_ctim = (struct timespec){1, 0}; @@ -70,7 +69,6 @@ int statfs(const char *path, struct statfs *out) { return r; } -/* FASTPATH=1: open directories the way the fast-path preload does, through fdopendir. */ DIR *opendir(const char *path) { static DIR *(*next)(const char *); if (!next) next = dlsym(RTLD_NEXT, "opendir"); diff --git a/tools/tests/test_droiddeckctl.py b/tools/tests/test_droiddeckctl.py index 9e3c87ac9..02400452d 100644 --- a/tools/tests/test_droiddeckctl.py +++ b/tools/tests/test_droiddeckctl.py @@ -28,8 +28,6 @@ def load(name): ctl = load("droiddeckctl") scenario = load("droiddeck-scenario") -# A stand-in for adb: answers the provider's content calls from a JSON config (a list answers -# successive calls), records every invocation, and fakes the few shell commands droiddeckctl uses. FAKE_ADB = textwrap.dedent('''\ #!/usr/bin/env python3 import base64, json, os, sys @@ -136,7 +134,6 @@ class DroiddeckctlTest(FakeAdbTest): "launch": {"ok": True, "command": "launch", "appId": "489830"}, "state": [state(focused=769, focusable=[769]), state(focused=489830, focusable=[769])], }) - # Focus alone is not enough: the game must also be in the list Steam orders from. code, out = self.run_ctl("launch", "489830", "--wait", "--timeout", "1") self.assertEqual(5, code) self.assertEqual("TIMEOUT", out["error"]["code"]) @@ -333,7 +330,7 @@ class ScenarioTest(FakeAdbTest): "steps": [["state"], {"assert": "session.focus.focusedApp", "equals": 489830}]}) code, report = self.run_scenario(path) self.assertEqual(1, code) - self.assertEqual(1, len(report["runs"])) # stops at the first failure without --keep-going + self.assertEqual(1, len(report["runs"])) self.assertIn("is 769", report["runs"][0]["error"]) evidence = json.loads((Path(report["runs"][0]["folder"]) / "evidence.json").read_text()) self.assertEqual(769, evidence["focus"]["focusedApp"]) diff --git a/tools/tests/test_effects_source_cache.py b/tools/tests/test_effects_source_cache.py index 4ad95710a..d42045b1c 100644 --- a/tools/tests/test_effects_source_cache.py +++ b/tools/tests/test_effects_source_cache.py @@ -29,7 +29,6 @@ class EffectsSourceCacheTest(unittest.TestCase): cls.temp = tempfile.TemporaryDirectory(prefix='droiddeck-effects-cache-') cls.addClassCleanup(cls.temp.cleanup) folder = Path(cls.temp.name) - # Copy only Vulkan headers so Android's libc headers cannot shadow the host's libc. shutil.copytree(headers, folder / 'include/vulkan', ignore=shutil.ignore_patterns('*.hpp')) if (headers.parent / 'vk_video').is_dir(): shutil.copytree(headers.parent / 'vk_video', folder / 'include/vk_video') diff --git a/tools/tests/test_game_environment.py b/tools/tests/test_game_environment.py index 6b2362faa..06e063339 100644 --- a/tools/tests/test_game_environment.py +++ b/tools/tests/test_game_environment.py @@ -341,8 +341,6 @@ class GameEnvironmentTest(unittest.TestCase): self.assertIn("GE-Proton11-7", [entry["name"] for entry in COMPAT["tool_catalog"](str(home), None)]) def test_droiddeck_proton_is_adopted_and_labelled_like_any_other_extra(self): - # Its folder has a space ("DroidDeck-Proton 11-7.5") and its internal name starts with - # "DroidDeck-Proton-", which lower-cases to our own tools' prefix: neither may matter. with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) (home / "label").write_text("Compatible\n") @@ -370,15 +368,12 @@ class GameEnvironmentTest(unittest.TestCase): config.write_text(config_text([("0", COMPAT["TOOL"]), ("42", "DroidDeck-Proton-11-7.5")])) COMPAT["register_default"](str(config), ["42", "43"], protect=protect) self.assertEqual(mapping_of(config.read_text()), {"0": COMPAT["TOOL"], "42": "DroidDeck-Proton-11-7.5", "43": COMPAT["TOOL"]}) - # Once removed it is no longer runnable, and its games go back to the default. changes, _ = COMPAT["plan_mapping"]({"0": COMPAT["TOOL"], "42": "DroidDeck-Proton-11-7.5"}, ["42"], [COMPAT["TOOL"]], COMPAT["TOOL"], {}) self.assertEqual(changes, {"42": COMPAT["TOOL"]}) def test_both_proton_wrappers_call_environment_launcher(self): for script in (COMPAT["LAUNCHER_SH"], COMPAT["EXTRA_WRAPPER_SH"] % "proton"): subprocess.run(["bash", "-n"], input=script, text=True, check=True) - # Started through bl_run, which execs it - or, for a store game with cloud saves, waits - # for it so droiddeck-store-launch --exited can follow (test_store_launch.CloudHooks). self.assertIn('bl_run ${BL_TASKSET:-} /usr/local/bin/droiddeck-game-env', script) def test_wrappers_preload_the_session_library_before_the_input_shim(self): @@ -564,8 +559,6 @@ class DirectAudioPrefixTest(unittest.TestCase): wine = self.tool / "files/bin-arm64/wine" wine.write_text("#!/bin/sh\necho wine-11.0-4c8f2e1 '(Staging)'\n") wine.chmod(0o755) - # Proton's own winepulse.so names the audio interface: the classic table never imports - # PsCreateSystemThread, the system-thread one (Proton-CachyOS) always does. (self.tool / "files/lib/wine/aarch64-unix").mkdir(parents=True) self.winepulse = self.tool / "files/lib/wine/aarch64-unix/winepulse.so" self.winepulse.write_bytes(b"\x7fELF classic wine 11 pulse driver") @@ -607,8 +600,6 @@ class DirectAudioPrefixTest(unittest.TestCase): result = self.run_setup() self.assertIn("DirectAudio selected", result.stderr) self.assertIn("WINEDLLPATH=%s/lib/wine" % self.audio, result.stdout) - # Proton-CachyOS's mmdevapi takes the driver list from this variable, and its launcher - # pre-sets it to pulse,alsa unless it is already set. self.assertIn("WINE_AUDIO_DRIVER=directaudio,pulse\n", result.stdout) self.assertEqual(self.link("system32"), str(self.audio / "lib/wine/aarch64-windows/winedirectaudio.drv")) self.assertEqual(self.link("syswow64"), str(self.audio / "lib/wine/i386-windows/winedirectaudio.drv")) @@ -701,16 +692,11 @@ class StartWithoutSteamTest(unittest.TestCase): game = [str(ge), "waitforexitandrun", "/games/Game.exe", "-windowed"] self.assertEqual(start(game, {}, "3384071994"), (game, {})) on = {"DROIDDECK_NO_STEAM": "1"} - # An added game (a shortcut id) has no Steam fixes: UMU_ID 0. The command is unchanged. self.assertEqual(start(game, on, "3384071994"), (game, dict(on, UMU_ID="0"))) - # A Steam game keeps its protonfixes under its own id. self.assertEqual(start(game, on, "570940")[1]["UMU_ID"], "570940") - # A UMU_ID the user set wins. self.assertEqual(start(game, dict(on, UMU_ID="umu-1"), "570940")[1]["UMU_ID"], "umu-1") - # Valve's Proton has no umu launch: the game runs straight in the prefix. plain = [str(valve)] + game[1:] self.assertEqual(start(plain, on, "570940"), ([str(valve), "runinprefix", "/games/Game.exe", "-windowed"], on)) - # Only a game launch: the client's own runs and other verbs are left alone. for other in ([str(ge), "run", "/x.exe"], [str(ge), "waitforexitandrun"], [str(valve), "getcompatpath", "/x"]): self.assertEqual(start(other, on, "570940"), (other, on)) diff --git a/tools/tests/test_launcher_cores.py b/tools/tests/test_launcher_cores.py index 98656b745..4f05df242 100644 --- a/tools/tests/test_launcher_cores.py +++ b/tools/tests/test_launcher_cores.py @@ -48,7 +48,7 @@ class LauncherCoresTest(unittest.TestCase): self.proc = self.root / 'proc' self.proc.mkdir() cpufreq = self.root / 'cpu' - for cpu in ALL: # 0-5 one cluster, 6-7 the fast one + for cpu in ALL: (cpufreq / f'cpu{cpu}/cpufreq').mkdir(parents=True) (cpufreq / f'cpu{cpu}/cpufreq/cpuinfo_max_freq').write_text('3532800\n' if cpu < 6 else '4320000\n') self.cores = FakeCores() @@ -118,7 +118,7 @@ class LauncherCoresTest(unittest.TestCase): self.process(100, LAUNCHER) self.process(200, HELPER) self.process(9, '/root/.local/share/Steam/steamrtarm64/steam') - self.governor.tick() # no focus file yet + self.governor.tick() self.focus_on(9, app=769) self.governor.tick() self.focus_on(100) @@ -141,7 +141,6 @@ class LauncherCoresTest(unittest.TestCase): self.process(400, GAME) self.focus_on(400) self.governor.tick() - # A new thread, and a new helper process the held one started: both born held. self.thread(200, 203, {5}, NICE) self.process(500, HELPER, (501,), cpus={5}, nice=NICE) self.now += 10 @@ -156,16 +155,16 @@ class LauncherCoresTest(unittest.TestCase): self.process(400, GAME) self.focus_on(400) self.governor.tick() - self.cores.state[201] = (ALL, -5) # the program raised a thread's priority itself + self.cores.state[201] = (ALL, -5) self.now += 5 self.governor.tick() - self.assertFalse(self.held(201)) # not looked at again before the rescan + self.assertFalse(self.held(201)) self.now += 5 self.governor.tick() self.assertTrue(self.held(201)) self.focus_on(200) self.governor.tick() - self.assertEqual((ALL, 0), self.cores.state[201]) # its first state, not the program's + self.assertEqual((ALL, 0), self.cores.state[201]) def test_a_steady_hold_is_not_rewritten(self): self.process(200, HELPER, (201,)) @@ -182,7 +181,7 @@ class LauncherCoresTest(unittest.TestCase): self.process(200, HELPER, (201,)) self.process(300, HELPER) self.process(400, GAME) - del self.cores.state[201] # ended between the listing and the call + del self.cores.state[201] self.focus_on(400) self.governor.tick() self.assertNotIn(201, self.holder.held[200]) diff --git a/tools/tests/test_login1.py b/tools/tests/test_login1.py index 947fd8eeb..abfdc70a2 100644 --- a/tools/tests/test_login1.py +++ b/tools/tests/test_login1.py @@ -80,7 +80,7 @@ class SleepHandshakeTest(unittest.TestCase): token = self.suspend() self.assertTrue(self.preparing()) self.state(token, "paused") - self.state("0" * 32, "awake") # stale session acknowledgments cannot wake this one + self.state("0" * 32, "awake") time.sleep(0.1) self.assertTrue(self.preparing()) self.state(token, "awake") diff --git a/tools/tests/test_msi_install.py b/tools/tests/test_msi_install.py index b896288eb..dda3eeea5 100644 --- a/tools/tests/test_msi_install.py +++ b/tools/tests/test_msi_install.py @@ -120,9 +120,6 @@ class LayoutTest(unittest.TestCase): self.assertEqual(inst.included_components, {"Up", "Wide"}) def test_a_duplicate_onto_the_file_itself_is_skipped(self): - # The .NET host packages (dotnet-host-*-win-x86.msi) duplicate dotnet.exe into its own - # folder under its own name; copying it onto itself raised SameFileError and stopped every - # .NET Core / Desktop runtime install. with tempfile.TemporaryDirectory() as tmp: src = Path(tmp) / "cab" / "dotnet.exe" src.parent.mkdir() @@ -269,13 +266,11 @@ class RegistryApplyTest(unittest.TestCase): text = (self.pfx / "system.reg").read_text() self.assertIn('"Version"="4.0"', text) self.assertIn('"Kept"="mine"', text) - # Through the Wow6432Node\Classes link into the shared Classes\Wow6432Node key, never under the link. self.assertIn('[Software\\\\Classes\\\\Wow6432Node\\\\CLSID\\\\{X}]', text) self.assertNotIn('[Software\\\\Wow6432Node\\\\Classes\\\\CLSID', text) self.assertIn('"PATH"=str(2):"C:\\\\PhysX;%SystemRoot%\\\\system32;%SystemRoot%"', text) self.assertNotIn("[System\\\\CurrentControlSet\\\\", text) self.assertIn('"Opt"=dword:00000002', (self.pfx / "user.reg").read_text()) - # The prefix's own spelling of Program Files (x86) is kept. self.assertTrue((self.pfx / "drive_c/program files (x86)/Microsoft XNA/xna.dll").is_file()) self.assertFalse((self.pfx / "drive_c/Program Files (x86)").exists()) diff --git a/tools/tests/test_netmanager.py b/tools/tests/test_netmanager.py index 23ce08070..69159fdfc 100644 --- a/tools/tests/test_netmanager.py +++ b/tools/tests/test_netmanager.py @@ -117,7 +117,6 @@ class NetworkObjectsTest(unittest.TestCase): self.assertTrue(secured["RsnFlags"].unpack() & 0x100) self.assertTrue(secured["RsnFlags"].unpack() & 0x400) self.assertEqual(found[nm.ap_path(other["bssid"])][nm.NAME + ".AccessPoint"]["Flags"].unpack(), 0) - # Keep nearby-network discovery while a cellular connection provides the default route. cellular = nm.objects(dict(CELLULAR, accessPoints=[other])) self.assertIn(nm.ap_path(other["bssid"]), cellular) self.assertEqual(cellular[nm.ROOT + "/Devices/Wifi"][nm.NAME + ".Device.Wireless"]["ActiveAccessPoint"].unpack(), "/") @@ -131,7 +130,6 @@ class NetworkBusTest(unittest.TestCase): self.write(WIFI) self.bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) env = dict(os.environ, DBUS_SYSTEM_BUS_ADDRESS=os.environ["DBUS_SESSION_BUS_ADDRESS"]) - # Use the production service and only redirect its input file for the fixture. launch = "import runpy,sys; from pathlib import Path; n=runpy.run_path(sys.argv[1]); g=n['main'].__globals__; g['SNAPSHOT']=Path(sys.argv[2]); g['SCAN_REQUEST']=Path(sys.argv[2]).with_suffix('.request'); n['main']()" self.process = subprocess.Popen([sys.executable, "-c", launch, str(SCRIPT), str(self.snapshot)], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -217,7 +215,6 @@ class NetworkBusTest(unittest.TestCase): self.assertTrue(any(interface == nm.OBJECT_MANAGER and member == "InterfacesRemoved" and args[0] == nm.AP for _, interface, member, args in signals)) self.assertTrue(any(member == "AccessPointAdded" for _, _, member, _ in signals)) - # A missing/invalid snapshot is a transient input error, never a fake Ethernet handover. self.snapshot.write_text("{") time.sleep(1.2) self.assertEqual(self.prop(nm.ROOT, nm.NAME, "PrimaryConnectionType"), "802-11-wireless") @@ -250,8 +247,6 @@ class NetworkBusTest(unittest.TestCase): from gi.repository import NM except ValueError: self.skipTest("requires gir1.2-nm-1.0") - # libnm is a real consumer of the same API Steam reads. It caches objects by path, - # so the handover must replace the Wi-Fi device with a modem rather than mutate its type. previous = os.environ.get("DBUS_SYSTEM_BUS_ADDRESS") os.environ["DBUS_SYSTEM_BUS_ADDRESS"] = os.environ["DBUS_SESSION_BUS_ADDRESS"] try: diff --git a/tools/tests/test_proton_extra.py b/tools/tests/test_proton_extra.py index dab3e2c95..5d8cc95f2 100644 --- a/tools/tests/test_proton_extra.py +++ b/tools/tests/test_proton_extra.py @@ -89,7 +89,6 @@ class ProtonExtraTest(unittest.TestCase): steam = Path(tmp) / "Steam" adopted = [] real = subprocess.call - # tar runs for real; steam-compatibility (the adoption step) is only recorded. fake = lambda args, **kw: adopted.append(args) or 0 if args[0] == "steam-compatibility" else real(args, **kw) with mock.patch.object(GLOBALS["subprocess"], "call", fake), contextlib.redirect_stdout(io.StringIO()) as said: with mock.patch.object(GLOBALS["sys"], "argv", ["droiddeck-proton-extra", str(steam), str(tarball)]): @@ -98,7 +97,6 @@ class ProtonExtraTest(unittest.TestCase): self.assertEqual(sorted(os.listdir(tools)), [".droiddeck-download", "DroidDeck-Proton 11-7.5"]) self.assertTrue((tools / "DroidDeck-Proton 11-7.5" / "toolmanifest.vdf").is_file()) self.assertEqual(adopted, [["steam-compatibility", str(steam)]]) - # The app reads the folder from this line, space and all. self.assertIn("droiddeck-proton-extra: installed DroidDeck-Proton 11-7.5\n", said.getvalue()) self.assertEqual(EXTRA["installed"](str(tools)), ["DroidDeck-Proton 11-7.5"]) diff --git a/tools/tests/test_seed_redists.py b/tools/tests/test_seed_redists.py index 1f9e2ae71..248e3f75a 100644 --- a/tools/tests/test_seed_redists.py +++ b/tools/tests/test_seed_redists.py @@ -22,8 +22,6 @@ GAME_SCRIPT = r'''"InstallScript" } ''' -# Red Dead Redemption 2's own script: Proton needs Khronos' vulkan-1.dll for it, which only the -# Vulkan runtime installer puts in the prefix. VULKAN_SCRIPT = r'''"InstallScript" { "Run Process" @@ -148,7 +146,6 @@ class SeedRedistsTest(unittest.TestCase): self.assertIn('"Other"=dword:00000001', reg) self.assertNotIn("Vulkan111080", (self.reg.parent / ".droiddeck-redists").read_text()) self.assertEqual(int(self.reg.stat().st_mtime), 1_000_000_000) - # The client writes the marker itself once the installer has run; that one is left alone. with self.reg.open("a") as out: out.write('\n[Software\\\\Valve\\\\Steam\\\\Apps\\\\1174180] 1791730000\n"Vulkan111080"=dword:00000001\n') self.run_seed() diff --git a/tools/tests/test_session_guest_command.py b/tools/tests/test_session_guest_command.py index 536450824..9c319ce95 100644 --- a/tools/tests/test_session_guest_command.py +++ b/tools/tests/test_session_guest_command.py @@ -53,8 +53,6 @@ class SessionGuestCommandTest(unittest.TestCase): def test_child_receives_its_package_socket_after_environment_is_cleared(self): for package in PACKAGES: with self.subTest(package=package): - # env with no command prints the environment it would pass to the - # session. A misplaced assignment fails at exec, before it runs. result = subprocess.run( guest_prefix(package), capture_output=True, text=True, check=True, timeout=5, env={"DROIDDECK_BWRAP_SOCKET": "wrong.bwrap", "STALE_HOST": "1"}) diff --git a/tools/tests/test_session_tmp.py b/tools/tests/test_session_tmp.py index 58607911d..55caf4278 100644 --- a/tools/tests/test_session_tmp.py +++ b/tools/tests/test_session_tmp.py @@ -12,7 +12,7 @@ SESSION = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/d STEAM = ["mangohud.Ab3dE6gH", ".com.valvesoftware.Steam.x1Y2z3/", "steam_chrome_shmem_uid10480_0", "steamADfAnd/"] CHROMIUM = ["scoped_dir3yu9Xt/SingletonSocket"] APPIMAGE = ["droiddeck-appimage.19PoOD"] -BUS = ["dbus-1zWfOP1IaR="] # = a socket +BUS = ["dbus-1zWfOP1IaR="] KDE = ["plasma-csd-generator.BVWIWA/"] STALE = STEAM + CHROMIUM + APPIMAGE + BUS + KDE KEPT = ["droiddeck-mangohud.conf", ".X11-unix/", "steamworks", "steamABCDEF/inside", "notes.txt", diff --git a/tools/tests/test_steam_desktop_launch.py b/tools/tests/test_steam_desktop_launch.py index 4461e0e4f..bf91609d1 100644 --- a/tools/tests/test_steam_desktop_launch.py +++ b/tools/tests/test_steam_desktop_launch.py @@ -107,7 +107,7 @@ class DesktopSteamLaunchTest(unittest.TestCase): def test_cef_software_is_scoped_to_the_kde_client(self): source = (BIN / 'droiddeck-session').read_text() - setup = source[source.index(' _bl_ui=(-gamepadui)'):source.index(' # Deck mode, always')] + setup = source[source.index(' _bl_ui=(-gamepadui)'):source.index(' if [ "${BL_STEAM_UI:-}" != desktop ]; then')] for desktop, ui, software in [('1', 'desktop', True), ('1', 'bigpicture', False), ('', 'desktop', False)]: with self.subTest(desktop=desktop, ui=ui): env = dict(self.env, BL_DESKTOP=desktop, BL_STEAM_UI=ui) diff --git a/tools/tests/test_steam_install.py b/tools/tests/test_steam_install.py index f3e29bbe2..03b2d694a 100644 --- a/tools/tests/test_steam_install.py +++ b/tools/tests/test_steam_install.py @@ -25,7 +25,6 @@ class SteamInstallTest(unittest.TestCase): self.requests = self.root / 'requests' self.bin = self.root / 'bin' self.bin.mkdir() - # Only the network is faked: exercise real manifest parsing, checksums and zip extraction. curl = self.bin / 'curl' curl.write_text('''#!/bin/bash set -eu @@ -33,7 +32,6 @@ printf '%s\\n' "$2" >> "$TEST_REQUESTS" cp "$TEST_CDN/${2##*/}" "$4" ''') curl.chmod(0o755) - # macOS has shasum; the guest and Linux CI have sha256sum. if shutil.which('sha256sum') is None: sha = self.bin / 'sha256sum' sha.write_text('#!/bin/bash\nexec shasum -a 256 "$@"\n') @@ -78,7 +76,6 @@ cp "$TEST_CDN/${2##*/}" "$4" self.assert_installed() def test_an_install_from_another_branch_is_left_for_steam_to_move(self): - # An older app could install publicbeta; droiddeck-session moves the client at its start. self.assert_installed() requests = self.requests.read_text() (self.steam / 'package/beta').write_text('publicbeta\n') diff --git a/tools/tests/test_steam_language.py b/tools/tests/test_steam_language.py index 43b1e1a95..22d26e463 100644 --- a/tools/tests/test_steam_language.py +++ b/tools/tests/test_steam_language.py @@ -37,7 +37,6 @@ def value(text, *path): STEAM = ('Registry', 'HKCU', 'Software', 'Valve', 'Steam', 'language') GLOBAL = ('Registry', 'HKCU', 'Software', 'Valve', 'Steamsteamglobal', 'language') -# As the client leaves it on a device (trimmed): both keys, other values beside them, and HKLM. CLIENT = '''"Registry" { "HKLM" diff --git a/tools/tests/test_steam_shortcuts.py b/tools/tests/test_steam_shortcuts.py index 1213888f1..a00dbc9e7 100644 --- a/tools/tests/test_steam_shortcuts.py +++ b/tools/tests/test_steam_shortcuts.py @@ -172,7 +172,6 @@ class ShortcutsTest(unittest.TestCase): self.assertEqual('Example: Renamed', entry['AppName']) self.assertEqual('"%s/data"' % GUEST, entry['StartDir']) self.assertEqual('-dx11', entry['LaunchOptions']) - # One shortcut, the same appid. self.assertEqual([APPID], list(self.run_writer(game))) def test_a_target_chosen_in_the_app_updates_the_same_shortcut(self): diff --git a/tools/tests/test_store_launch.py b/tools/tests/test_store_launch.py index 014be591e..88e9f4156 100644 --- a/tools/tests/test_store_launch.py +++ b/tools/tests/test_store_launch.py @@ -56,7 +56,6 @@ class Detection(unittest.TestCase): def test_anything_else_is_left_alone(self): with tempfile.TemporaryDirectory() as root: folder = game(root) - # The exe of a store game is one too (a GOG game without a launcher); a stray file is not. self.assertEqual(folder, store.find_store_game([str(folder / 'Metalstorm.exe')])[0]) self.assertIsNone(store.find_store_game([str(Path(root) / 'elsewhere/readme.txt')])) (folder / '.droiddeck-store.json').unlink() @@ -65,7 +64,6 @@ class Detection(unittest.TestCase): def test_only_epic_asks_for_a_code(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog') - # Cloud saves off: GOG still prepares achievements, never an Epic exchange code. sc = json.loads((folder / '.droiddeck-store.json').read_text()); sc['cloud'] = False (folder / '.droiddeck-store.json').write_text(json.dumps(sc)) os.environ['BL_LAUNCH_DIR'] = str(Path(root) / 'session') @@ -136,7 +134,6 @@ class Prefix(unittest.TestCase): text = self.reg.read_text() self.assertIn('[Software\\\\Wine\\\\WineBrowser] ', text) self.assertIn('"Browsers"="/usr/local/bin/droiddeck-open-url,xdg-open"', text) - # A second launch adds nothing: the last word already says so. store.provision_prefix(str(self.compat)) self.assertEqual(text, self.reg.read_text()) @@ -158,7 +155,6 @@ class Choices(unittest.TestCase): folder = game(root) sidecar = json.loads((folder / '.droiddeck-store.json').read_text()) self.assertEqual({'eos': True, 'offline': False}, store.options(sidecar)) - # An earlier build's overlay field is read past. self.assertEqual({'eos': True, 'offline': False}, store.options({'epic': {'v': 2, 'overlay': True}})) sidecar['epic'] = {'eos': True, 'offline': True} sidecar['cloud'] = False @@ -175,7 +171,6 @@ class Choices(unittest.TestCase): finally: del os.environ['BL_LAUNCH_DIR'] del os.environ['STEAM_COMPAT_DATA_PATH'] - # Offline: no request was made and no code is left; the old overlay pointer is gone. self.assertFalse((Path(root) / 'session/stores/req').exists()) self.assertFalse((folder / '.droiddeck-epic-code').exists()) text = reg.read_text() @@ -200,7 +195,6 @@ class Browser(unittest.TestCase): class ExeTarget(unittest.TestCase): def test_a_game_started_by_its_exe_is_found_from_a_subfolder(self): - # Most GOG games have no launcher .bat: Steam runs the exe, which may sit in a subfolder. with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1207664643') (folder / 'bin/x64').mkdir(parents=True) @@ -222,7 +216,6 @@ class CloudHooks(unittest.TestCase): original = store.ask asked = [] store.ask = lambda channel, request, timeout=5.0: asked.append((request, timeout)) or {'ok': True, 'files': 2, 'reason': 'ok'} - # 10: the launcher waits for Proton and calls --exited afterwards. self.assertEqual(store.STORE_GAME, store.main([bat])) self.assertEqual(0, store.main(['--exited', bat])) finally: @@ -309,7 +302,6 @@ class GogAchievements(unittest.TestCase): self.assertTrue((data / 'pfx/.droiddeck-comet-service').is_file()) self.assertEqual('ready', store.provision_galaxy(str(data), str(proton))) self.assertEqual(1, len(seen)) - # A recreated prefix must be provisioned again, even with the same Proton. __import__('shutil').rmtree(data / 'pfx') self.assertEqual('ready', store.provision_galaxy(str(data), str(proton))) self.assertEqual(2, len(seen)) diff --git a/tools/tests/test_termux_jni.py b/tools/tests/test_termux_jni.py index 084114006..6fd3d9787 100644 --- a/tools/tests/test_termux_jni.py +++ b/tools/tests/test_termux_jni.py @@ -59,8 +59,6 @@ class TermuxJniTest(unittest.TestCase): return {key: int(value) for key, value in re.findall(r"(\w+)=(\d+)", line)} def test_a_long_argv_and_environment_stay_under_the_local_reference_limit(self): - # 300 + 300 is under 512 for each loop on its own but over it together, as the issue says: - # ART aborts with "local reference table overflow (max=512)". stats = self.run_harness(300, 300) self.assertEqual(stats["overflowed"], 0, stats) diff --git a/tools/tests/test_wincomponents.py b/tools/tests/test_wincomponents.py index fe0902af4..ac50803d1 100644 --- a/tools/tests/test_wincomponents.py +++ b/tools/tests/test_wincomponents.py @@ -98,7 +98,7 @@ class WinComponentsTest(unittest.TestCase): self.pick("vc") self.apply() self.assertEqual(installer.read_bytes(), b"ours") - self.apply() # a second launch keeps the first backup, not ours + self.apply() self.pick() names, notes = self.apply() self.assertEqual(installer.read_bytes(), b"from a real installer") diff --git a/tools/tests/test_zram_preload.py b/tools/tests/test_zram_preload.py index b0967cf62..a9a437e3e 100644 --- a/tools/tests/test_zram_preload.py +++ b/tools/tests/test_zram_preload.py @@ -47,7 +47,6 @@ static uint64_t sum(const unsigned char *p, size_t n) { return s; } -/* Rewrites its pages with a round number and checks every page it wrote last round. */ static void *writer(void *arg) { unsigned char *mine = shared + (size_t)(uintptr_t)arg * (size / 4); for (unsigned char round = 1; !stop; round++) { @@ -78,7 +77,6 @@ int main(int argc, char **argv) { sigaction(SIGURG, NULL, &sa); printf("handler %d\n", (sa.sa_flags & SA_SIGINFO) ? 1 : 0); if (argc > 4) { - /* Read-only memory the process never pages out itself, swapped as the kernel would. */ unsigned char *ro = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); memset(ro, 0x5a, size); mprotect(ro, size, PROT_READ); diff --git a/tools/vr/build.sh b/tools/vr/build.sh index f9e4fa49c..fc90d8905 100755 --- a/tools/vr/build.sh +++ b/tools/vr/build.sh @@ -1,14 +1,4 @@ #!/usr/bin/env bash -# Builds Cardboard mode's SteamVR pieces for the guest (glibc aarch64): -# <out>/droiddeck the SteamVR driver: driver_droiddeck.so, its manifest and default settings -# <out>/layer VK_LAYER_DROIDDECK_vr, the Vulkan layer SteamVR's compositor needs on Turnip/KGSL -# -# tools/vr/build.sh [OUT] in the local cross image (default OUT: tools/vr/out) -# tools/vr/build.sh --native OUT with aarch64-linux-gnu-gcc/g++ on PATH (CI, or inside the image) -# -# Vulkan headers: VK_INCLUDE (a directory holding vulkan/), else the NDK's (ANDROID_NDK_HOME, the -# runner's latest NDK, or the newest under ANDROID_HOME). Only vulkan/ is copied out of it: the rest of -# the NDK sysroot is Android's libc, not the guest's. set -euo pipefail here=$(cd "$(dirname "$0")" && pwd) repo=$(cd "$here/../.." && pwd) @@ -29,8 +19,6 @@ if [ -z "$vkinc" ]; then fi [ -n "$vkinc" ] && [ -f "$vkinc/vulkan/vulkan_core.h" ] || { echo "tools/vr/build.sh: no Vulkan headers (set VK_INCLUDE)" >&2; exit 1; } -# Everything the compilers read sits under the repo, so the cross image (which mounts only the repo) -# sees it too. work="$repo/build/vr" rm -rf "$work" && mkdir -p "$work/include" cp -R "$vkinc/vulkan" "$work/include/" @@ -40,7 +28,6 @@ mkdir -p "$work/out/droiddeck/bin/linuxarm64" "$work/out/layer" compile() { local cmd=(sh -c ' set -e - # libwayland-client: linked against a stub with its soname (the guest has the real one). mkdir -p build/vr/stub aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wl,-soname,libwayland-client.so.0 -Itools/vr/third_party/wayland \ -o build/vr/stub/libwayland-client.so tools/vr/src/wayland_client_stub.c diff --git a/tools/vr/src/driver_droiddeck.cpp b/tools/vr/src/driver_droiddeck.cpp index eaded9ebb..1397cc1cf 100644 --- a/tools/vr/src/driver_droiddeck.cpp +++ b/tools/vr/src/driver_droiddeck.cpp @@ -1,15 +1,3 @@ -// SteamVR driver for DroidDeck's Cardboard mode: one 3DoF HMD whose pose is the phone's rotation -// sensor, read from the head pose file the app shares with the guest -// (app/src/main/cpp/waylandcomp/src/headpose_shm.h, /run/droiddeck/droiddeck-headpose). -// -// The HMD renders an undistorted side-by-side pair: the app's Cardboard pass, in its side-by-side -// mode, does the lens distortion for the viewer the user picked. So the projection here is exactly -// the frustum that pass samples (tan_half_fov, eye_aspect from the pose file), and the distortion is -// the identity. -// -// How the frames reach the screen is the open question this driver is for: see docs/cardboard-vr.md. -// It exposes IVRDisplayComponent with the display "on the desktop" (extended mode) at the session's -// size, and logs what vrserver / vrcompositor ask of it. #include "openvr_driver.h" #include <algorithm> @@ -50,7 +38,6 @@ void Log(const char *fmt, ...) { fprintf(stderr, "[droiddeck] %s\n", buf); } -// The shared pose file, mapped read-only; reads follow the seqlock in headpose_shm.h. class PoseFile { public: bool Read(droiddeck_headpose *out) { @@ -82,7 +69,6 @@ private: }; HmdQuaternion_t QuatFromMatrix(const float m[9]) { - // Row-major rotation -> quaternion (Shepperd's method). HmdQuaternion_t q; float tr = m[0] + m[4] + m[8]; if (tr > 0.0f) { @@ -101,8 +87,6 @@ HmdQuaternion_t QuatFromMatrix(const float m[9]) { return q; } -// IVRVirtualDisplay: the compositor renders the final frame into its own backbuffer and hands it -// here instead of scanning it out. What the handle is on Linux is what this logs (experiment). class VirtualDisplay : public IVRVirtualDisplay { public: void Present(const PresentInfo_t *info, uint32_t size) override { @@ -115,7 +99,6 @@ public: } void WaitForPresent() override {} bool GetTimeSinceLastVsync(float *seconds, uint64_t *counter) override { - // A free-running 60 Hz vsync. using namespace std::chrono; double t = duration<double>(steady_clock::now().time_since_epoch()).count(); double period = 1.0 / 60.0; @@ -129,8 +112,6 @@ private: std::chrono::steady_clock::time_point last_; }; -// The vsync grid SteamVR paces against in driver-direct mode (Prop_DriverDirectModeSendsVsyncEvents): -// a free-running 60 Hz tick, declared a little ahead of time (ALVR's announcer, alvr/server_openvr/src/vsync.rs). class Vsync { public: static constexpr std::chrono::nanoseconds kInterval{16666667}; @@ -172,10 +153,6 @@ private: static inline std::chrono::steady_clock::time_point last_{}; }; -// IVRDriverDirectModeComponent: SteamVR renders each eye into textures this driver allocates -// (shared Vulkan images from SteamVR's IPC resource manager, dmabufs on Linux) and hands them -// over per frame. This is how ALVR gets frames on Linux; it avoids the compositor's own direct mode, -// which needs VK_KHR_display - something Turnip on KGSL refuses outright. class DirectMode : public IVRDriverDirectModeComponent { public: struct Set { @@ -190,8 +167,8 @@ public: Set set; set.pid = pid; set.desc = *d; - const uint32_t usage = 0x1 /* TRANSFER_SRC */ | 0x4 /* SAMPLED */ | 0x80 /* INPUT_ATTACHMENT */; - const uint32_t create = 0x8 /* MUTABLE_FORMAT */; + const uint32_t usage = 0x1 | 0x4 | 0x80 ; + const uint32_t create = 0x8 ; for (int i = 0; i < 3; i++) { SharedTextureHandle_t h = 0; if (!VRIPCResourceManager()->NewSharedVulkanImage(d->nFormat, d->nWidth, d->nHeight, true, false, true, 1, 1, @@ -245,7 +222,6 @@ public: void SubmitLayer(const SubmitLayerPerEye_t (&eyes)[2]) override { layers_++; - // The first layer of a frame is the compositor's own picture; overlays after it are not drawn yet. if (layerInFrame_++ == 0) { for (int e = 0; e < 2; e++) { frame_.texture[e] = eyes[e].hTexture; @@ -274,7 +250,6 @@ public: } void PostPresent(const Throttling_t *t) override { - // Hold until the tick after the last one declared (ALVR's pacing): a slow frame costs one frame. auto last = Vsync::LastTick(); auto until = last + Vsync::kInterval * (1 + (t ? t->nFramesToThrottle : 0)); if (until > std::chrono::steady_clock::now()) std::this_thread::sleep_until(until); @@ -304,12 +279,9 @@ private: droiddeck::EyeFrame frame_{}; }; -// The session's game pad, as the app publishes it for the guest: the absolute-state snapshot at the -// head of fake input ring 0 (app/src/main/cpp/fakeinput_steam.cpp, FakeInputRingHeader), under a -// seqlock. FAKE_EVDEV_MEMFD_PATHS names the rings in every session process's environment. struct PadState { - uint32_t buttons = 0; // bit order of FakeInputWriter.BUTTON_MAP; bit 11 = the QAM button - int16_t axes[8] = {}; // X, Y, RX, RY (+-32767), GAS = right trigger, BRAKE = left (0..255), HAT0X, HAT0Y + uint32_t buttons = 0; + int16_t axes[8] = {}; }; enum : uint32_t { PAD_A = 1u << 0, PAD_B = 1u << 1, PAD_X = 1u << 2, PAD_Y = 1u << 3, PAD_LB = 1u << 4, PAD_RB = 1u << 5, @@ -340,7 +312,7 @@ private: std::string path; if (const char *env = getenv("DROIDDECK_VR_PAD_RING")) path = env; if (path.empty()) { - const char *paths = getenv("FAKE_EVDEV_MEMFD_PATHS"); // "0=/path;1=/path;..." + const char *paths = getenv("FAKE_EVDEV_MEMFD_PATHS"); if (paths && !strncmp(paths, "0=", 2)) path.assign(paths + 2, strcspn(paths + 2, ";")); } if (path.empty()) { @@ -363,10 +335,6 @@ private: std::chrono::steady_clock::time_point retry_{}; }; -// One hand: a Vive-profile controller driven by the game pad. 3DoF, so it is a gaze pointer: it sits -// low in front of its side of the body and points where the head looks, and SteamVR's laser follows the -// view. Right hand: right stick = trackpad, A = trackpad click, RT = trigger, RB = grip, Start = menu, -// the Steam/guide button = system. Left: left stick, X, LT, LB, Select. class CardboardController : public ITrackedDeviceServerDriver { public: explicit CardboardController(bool right) : right_(right) {} @@ -404,14 +372,12 @@ public: DriverPose_t GetPose() override { return pose_; } - // From the driver's pose thread: the head's orientation and the pad. void Update(const HmdQuaternion_t &head, const PadState *pad) { if (id_ == k_unTrackedDeviceIndexInvalid) return; DriverPose_t p = {}; p.qWorldFromDriverRotation.w = 1; p.qDriverFromHeadRotation.w = 1; p.qRotation = head; - // Low and in front of its side, in head space, then into the room with the head's rotation. const double off[3] = {right_ ? 0.18 : -0.18, -0.35, -0.30}; double m[3][3]; const double w = head.w, x = head.x, y = head.y, z = head.z; @@ -460,7 +426,6 @@ public: if (height_ <= 0) height_ = 1080; float hz = VRSettings()->GetFloat(kSettingsSection, "displayFrequency"); if (hz <= 0.0f) hz = 60.0f; - // Until the app has drawn an eye: the Cardboard pass's defaults for a 1920x1080 panel. tanHalf_ = 1.0f; eyeAspect_ = (width_ * 0.5f) / height_; droiddeck_headpose hp; @@ -476,8 +441,6 @@ public: VRProperties()->SetFloatProperty(c, Prop_DisplayFrequency_Float, hz); VRProperties()->SetFloatProperty(c, Prop_SecondsFromVsyncToPhotons_Float, 0.011f); VRProperties()->SetUint64Property(c, Prop_CurrentUniverseId_Uint64, 2); - // Off the desktop: with a direct mode component that sends SteamVR's compositor down the - // driver-direct path; on it, the compositor opens its own direct mode window (VK_KHR_display). VRProperties()->SetBoolProperty(c, Prop_IsOnDesktop_Bool, false); VRProperties()->SetBoolProperty(c, Prop_DisplayDebugMode_Bool, false); directMode_ = VRSettings()->GetBool(kSettingsSection, "directMode"); @@ -489,9 +452,6 @@ public: VRProperties()->SetBoolProperty(c, Prop_HasDisplayComponent_Bool, true); virtualDisplay_ = VRSettings()->GetBool(kSettingsSection, "virtualDisplay"); VRProperties()->SetBoolProperty(c, Prop_HasVirtualDisplayComponent_Bool, virtualDisplay_); - // A proximity sensor that reads "worn" while Cardboard mode is on: otherwise SteamVR sees a headset - // lying still (a phone on a table between sessions) and puts the display in standby, after which - // the compositor submits no layers at all. VRProperties()->SetBoolProperty(c, Prop_ContainsProximitySensor_Bool, true); VRDriverInput()->CreateBooleanComponent(c, "/proximity", &proximity_); VRDriverInput()->UpdateBooleanComponent(proximity_, true, 0); @@ -528,7 +488,7 @@ public: p.qWorldFromDriverRotation.w = 1; p.qDriverFromHeadRotation.w = 1; p.qRotation.w = 1; - p.vecPosition[1] = 1.6; // standing eye height: 3DoF, the head never moves + p.vecPosition[1] = 1.6; droiddeck_headpose hp; bool ok = pose_.Read(&hp); if (ok) { @@ -543,14 +503,12 @@ public: return p; } - // ---- IVRDisplayComponent void GetWindowBounds(int32_t *x, int32_t *y, uint32_t *w, uint32_t *h) override { *x = 0; *y = 0; *w = (uint32_t)width_; *h = (uint32_t)height_; } bool IsDisplayOnDesktop() override { return !directMode_; } bool IsDisplayRealDisplay() override { return false; } void GetRecommendedRenderTargetSize(uint32_t *w, uint32_t *h) override { - // Rendered a little larger than the panel: the lens pass magnifies the middle. *w = (uint32_t)(width_ / 2 * 1.4f); *h = (uint32_t)(height_ * 1.4f); } @@ -559,7 +517,6 @@ public: *x = eye == Eye_Left ? 0 : (uint32_t)width_ / 2; } void GetProjectionRaw(EVREye, float *left, float *right, float *top, float *bottom) override { - // Exactly what the Cardboard pass samples from each half of a side-by-side frame. *left = -tanHalf_ * eyeAspect_; *right = tanHalf_ * eyeAspect_; *top = -tanHalf_; *bottom = tanHalf_; } @@ -582,7 +539,6 @@ private: droiddeck_headpose hp; bool fresh = pose_.Read(&hp) && hp.seq != lastSeq; auto now = std::chrono::steady_clock::now(); - // A new sample, or a heartbeat so SteamVR never thinks the HMD went away. PadState pad; const bool havePad = pad_.Read(&pad); const bool padChanged = havePad && (pad.buttons != lastPad_.buttons || memcmp(pad.axes, lastPad_.axes, sizeof(pad.axes))); @@ -657,7 +613,7 @@ private: Provider g_provider; -} // namespace +} DD_EXPORT void *HmdDriverFactory(const char *interfaceName, int *returnCode) { if (!strcmp(interfaceName, IServerTrackedDeviceProvider_Version)) return &g_provider; diff --git a/tools/vr/src/openvr_driver.h b/tools/vr/src/openvr_driver.h index fcd675583..a781cacb6 100644 --- a/tools/vr/src/openvr_driver.h +++ b/tools/vr/src/openvr_driver.h @@ -1,8 +1,6 @@ #pragma once -// openvr_driver.h //========= Copyright Valve Corporation ============// -// Dynamically generated file. Do not modify this file directly. #ifndef _OPENVR_DRIVER_API #define _OPENVR_DRIVER_API @@ -11,16 +9,14 @@ -// version.h namespace vr { static const uint32_t k_nSteamVRVersionMajor = 2; static const uint32_t k_nSteamVRVersionMinor = 15; static const uint32_t k_nSteamVRVersionBuild = 6; -} // namespace vr +} -// public_vrtypes.h #ifndef _INCLUDE_CORE_VRTYPES_PUBLIC_H #define _INCLUDE_CORE_VRTYPES_PUBLIC_H @@ -31,11 +27,6 @@ namespace vr typedef uint32_t PropertyTypeTag_t; -// right-handed system -// +y is up -// +x is to the right -// -z is forward -// Distance unit is meters struct HmdMatrix34_t { float m[3][4]; @@ -97,7 +88,6 @@ struct HmdRect2_t HmdVector2_t vBottomRight; }; -/** Holds the transform for a single bone */ struct VRBoneTransform_t { HmdVector4_t position; @@ -110,13 +100,10 @@ struct VREyeTrackingData_t bool bValid; bool bTracked; - vr::HmdVector3_t vGazeOrigin; // Ray origin - vr::HmdVector3_t vGazeTarget; // Gaze target (fixation point) + vr::HmdVector3_t vGazeOrigin; + vr::HmdVector3_t vGazeTarget; }; -/** Used to return the post-distortion UVs for each color channel. -* UVs range from 0 to 1 with 0,0 in the upper left corner of the -* source render target. The 0,0 to 1,1 range covers a single eye. */ struct DistortionCoordinates_t { float rfRed[2]; @@ -132,55 +119,52 @@ enum EVREye enum ETextureType { - TextureType_Invalid = -1, // Handle has been invalidated - TextureType_DirectX = 0, // Handle is an ID3D11Texture - TextureType_OpenGL = 1, // Handle is an OpenGL texture name or an OpenGL render buffer name, depending on submit flags - TextureType_Vulkan = 2, // Handle is a pointer to a VRVulkanTextureData_t structure - TextureType_IOSurface = 3, // Handle is a macOS cross-process-sharable IOSurfaceRef, deprecated in favor of TextureType_Metal on supported platforms - TextureType_DirectX12 = 4, // Handle is a pointer to a D3D12TextureData_t structure - TextureType_DXGISharedHandle = 5, // Handle is a HANDLE DXGI share handle, only supported for Overlay render targets. - // this texture is used directly by our renderer, so only perform atomic (copyresource or resolve) on it - TextureType_Metal = 6, // Handle is a MTLTexture conforming to the MTLSharedTexture protocol. Textures submitted to IVRCompositor::Submit which - // are of type MTLTextureType2DArray assume layer 0 is the left eye texture (vr::EVREye::Eye_left), layer 1 is the right - // eye texture (vr::EVREye::Eye_Right) + TextureType_Invalid = -1, + TextureType_DirectX = 0, + TextureType_OpenGL = 1, + TextureType_Vulkan = 2, + TextureType_IOSurface = 3, + TextureType_DirectX12 = 4, + TextureType_DXGISharedHandle = 5, + + TextureType_Metal = 6, + + TextureType_Reserved = 7, - TextureType_SharedTextureHandle = 8, // A pointer to a vr::SharedTextureHandle_t that was imported via, eg. ImportDmabuf. + TextureType_SharedTextureHandle = 8, }; enum EColorSpace { - ColorSpace_Auto = 0, // Assumes 'gamma' for 8-bit per component formats, otherwise 'linear'. This mirrors the DXGI formats which have _SRGB variants. - ColorSpace_Gamma = 1, // Texture data can be displayed directly on the display without any conversion (a.k.a. display native format). - ColorSpace_Linear = 2, // Same as gamma but has been converted to a linear representation using DXGI's sRGB conversion algorithm. + ColorSpace_Auto = 0, + ColorSpace_Gamma = 1, + ColorSpace_Linear = 2, }; struct Texture_t { - void* handle; // See ETextureType definition above + void* handle; ETextureType eType; EColorSpace eColorSpace; }; -/** Allows the application to control what part of the provided texture will be used in the -* frame buffer. */ struct VRTextureBounds_t { float uMin, vMin; float uMax, vMax; }; -/** Allows specifying pose used to render provided scene texture (if different from value returned by WaitGetPoses). */ struct VRTextureWithPose_t : public Texture_t { - HmdMatrix34_t mDeviceToAbsoluteTracking; // Actual pose used to render scene textures. + HmdMatrix34_t mDeviceToAbsoluteTracking; }; struct VRTextureDepthInfo_t { - void* handle; // See ETextureType definition above + void* handle; HmdMatrix44_t mProjection; - HmdVector2_t vRange; // 0..1 + HmdVector2_t vRange; }; struct VRTextureWithDepth_t : public Texture_t @@ -195,8 +179,8 @@ struct VRTextureWithPoseAndDepth_t : public VRTextureWithPose_t struct VRTextureMotionInfo_t { - void *handle; // See ETextureType definition above - HmdMatrix44_t mDeltaPose; // Incremental application-applied transform, if any, since the previous frame that affects the view. + void *handle; + HmdMatrix44_t mDeltaPose; }; struct VRTextureWithMotion_t : VRTextureWithPoseAndDepth_t @@ -204,8 +188,6 @@ struct VRTextureWithMotion_t : VRTextureWithPoseAndDepth_t VRTextureMotionInfo_t motion; }; -// 64-bit types that are part of public structures -// that are replicated in shared memory. #if defined(__linux__) || defined(__APPLE__) typedef uint64_t vrshared_uint64_t __attribute__ ((aligned(8))); typedef double vrshared_double __attribute__ ((aligned(8))); @@ -220,12 +202,12 @@ struct DmabufPlane_t { uint32_t unOffset; uint32_t unStride; - int32_t nFd; // This is not consumed, it is dup'ed. + int32_t nFd; }; struct DmabufAttributes_t { - void *pNext; // MUST be NULL. Unused right now, but could be used to extend this structure in the future. + void *pNext; uint32_t unWidth; uint32_t unHeight; @@ -233,8 +215,8 @@ struct DmabufAttributes_t uint32_t unMipLevels; uint32_t unArrayLayers; uint32_t unSampleCount; - uint32_t unFormat; // DRM_FORMAT_ - uint64_t ulModifier; // DRM_FORMAT_MOD_ + uint32_t unFormat; + uint64_t ulModifier; uint32_t unPlaneCount; DmabufPlane_t plane[MaxDmabufPlaneCount]; @@ -242,22 +224,19 @@ struct DmabufAttributes_t #pragma pack( pop ) -} // namespace vr +} #endif -// vrtypes.h #ifndef _INCLUDE_VRTYPES_H #define _INCLUDE_VRTYPES_H -// Forward declarations to avoid requiring vulkan.h struct VkDevice_T; struct VkPhysicalDevice_T; struct VkInstance_T; struct VkQueue_T; -// Forward declarations to avoid requiring d3d12.h struct ID3D12Resource; struct ID3D12CommandQueue; @@ -265,8 +244,6 @@ namespace vr { #pragma pack( push, 8 ) -/** A handle for a spatial anchor. This handle is only valid during the session it was created in. -* Anchors that live beyond one session should be saved by their string descriptors. */ typedef uint32_t SpatialAnchorHandle_t; typedef void* glSharedTextureHandle_t; @@ -274,7 +251,6 @@ typedef int32_t glInt_t; typedef uint32_t glUInt_t; -// Handle to a shared texture (HANDLE on Windows obtained using OpenSharedResource). typedef uint64_t SharedTextureHandle_t; #define INVALID_SHARED_TEXTURE_HANDLE ((vr::SharedTextureHandle_t)0) @@ -296,41 +272,37 @@ static const uint32_t k_nDriverNone = 0xFFFFFFFF; static const uint32_t k_unMaxDriverDebugResponseSize = 32768; -/** Used to pass device IDs to API calls */ typedef uint32_t TrackedDeviceIndex_t; static const uint32_t k_unTrackedDeviceIndex_Hmd = 0; static const uint32_t k_unMaxTrackedDeviceCount = 64; static const uint32_t k_unTrackedDeviceIndexOther = 0xFFFFFFFE; static const uint32_t k_unTrackedDeviceIndexInvalid = 0xFFFFFFFF; -/** Describes what kind of object is being tracked at a given ID */ enum ETrackedDeviceClass { - TrackedDeviceClass_Invalid = 0, // the ID was not valid. - TrackedDeviceClass_HMD = 1, // Head-Mounted Displays - TrackedDeviceClass_Controller = 2, // Tracked controllers - TrackedDeviceClass_GenericTracker = 3, // Generic trackers, similar to controllers - TrackedDeviceClass_TrackingReference = 4, // Camera and base stations that serve as tracking reference points - TrackedDeviceClass_DisplayRedirect = 5, // Accessories that aren't necessarily tracked themselves, but may redirect video output from other tracked devices + TrackedDeviceClass_Invalid = 0, + TrackedDeviceClass_HMD = 1, + TrackedDeviceClass_Controller = 2, + TrackedDeviceClass_GenericTracker = 3, + TrackedDeviceClass_TrackingReference = 4, + TrackedDeviceClass_DisplayRedirect = 5, TrackedDeviceClass_Max }; -/** Describes what specific role associated with a tracked device */ enum ETrackedControllerRole { - TrackedControllerRole_Invalid = 0, // Invalid value for controller type - TrackedControllerRole_LeftHand = 1, // Tracked device associated with the left hand - TrackedControllerRole_RightHand = 2, // Tracked device associated with the right hand - TrackedControllerRole_OptOut = 3, // Tracked device is opting out of left/right hand selection - TrackedControllerRole_Treadmill = 4, // Tracked device is a treadmill or other locomotion device - TrackedControllerRole_Stylus = 5, // Tracked device is a stylus + TrackedControllerRole_Invalid = 0, + TrackedControllerRole_LeftHand = 1, + TrackedControllerRole_RightHand = 2, + TrackedControllerRole_OptOut = 3, + TrackedControllerRole_Treadmill = 4, + TrackedControllerRole_Stylus = 5, TrackedControllerRole_Max = 5 }; -/** Returns true if the tracked controller role is allowed to be a hand */ inline bool IsRoleAllowedAsHand( ETrackedControllerRole eRole ) { switch ( eRole ) @@ -345,27 +317,22 @@ inline bool IsRoleAllowedAsHand( ETrackedControllerRole eRole ) } -/** describes a single pose for a tracked object */ struct TrackedDevicePose_t { HmdMatrix34_t mDeviceToAbsoluteTracking; - HmdVector3_t vVelocity; // velocity in tracker space in m/s - HmdVector3_t vAngularVelocity; // angular velocity in radians/s (?) + HmdVector3_t vVelocity; + HmdVector3_t vAngularVelocity; ETrackingResult eTrackingResult; bool bPoseIsValid; - // This indicates that there is a device connected for this spot in the pose array. - // It could go from true to false if the user unplugs the device. bool bDeviceIsConnected; }; -/** Identifies which style of tracking origin the application wants to use -* for the poses it is requesting */ enum ETrackingUniverseOrigin { - TrackingUniverseSeated = 0, // Poses are provided relative to the seated zero pose - TrackingUniverseStanding = 1, // Poses are provided relative to the safe bounds configured by the user - TrackingUniverseRawAndUncalibrated = 2, // Poses are provided in the coordinate system defined by the driver. It has Y up and is unified for devices of the same driver. You usually don't want this one. + TrackingUniverseSeated = 0, + TrackingUniverseStanding = 1, + TrackingUniverseRawAndUncalibrated = 2, }; enum EAdditionalRadioFeatures @@ -379,7 +346,6 @@ enum EAdditionalRadioFeatures typedef uint64_t WebConsoleHandle_t; #define INVALID_WEB_CONSOLE_HANDLE ((vr::WebConsoleHandle_t)0) -// Refers to a single container of properties typedef uint64_t PropertyContainerHandle_t; typedef uint32_t PropertyTypeTag_t; @@ -389,7 +355,6 @@ static const PropertyTypeTag_t k_unInvalidPropertyTag = 0; typedef PropertyContainerHandle_t DriverHandle_t; static const PropertyContainerHandle_t k_ulInvalidDriverHandle = 0; -// Use these tags to set/get common types as struct properties static const PropertyTypeTag_t k_unFloatPropertyTag = 1; static const PropertyTypeTag_t k_unInt32PropertyTag = 2; static const PropertyTypeTag_t k_unUint64PropertyTag = 3; @@ -421,13 +386,10 @@ static const PropertyTypeTag_t k_unOpenVRInternalReserved_Start = 1000; static const PropertyTypeTag_t k_unOpenVRInternalReserved_End = 10000; -/** Each entry in this enum represents a property that can be retrieved about a -* tracked device. Many fields are only valid for one ETrackedDeviceClass. */ enum ETrackedDeviceProperty { Prop_Invalid = 0, - // general properties that apply to all device classes Prop_TrackingSystemName_String = 1000, Prop_ModelNumber_String = 1001, Prop_SerialNumber_String = 1002, @@ -440,7 +402,7 @@ enum ETrackedDeviceProperty Prop_ConnectedWirelessDongle_String = 1009, Prop_DeviceIsWireless_Bool = 1010, Prop_DeviceIsCharging_Bool = 1011, - Prop_DeviceBatteryPercentage_Float = 1012, // 0 is empty, 1 is full + Prop_DeviceBatteryPercentage_Float = 1012, Prop_StatusDisplayTransform_Matrix34 = 1013, Prop_Firmware_UpdateAvailable_Bool = 1014, Prop_Firmware_ManualUpdate_Bool = 1015, @@ -465,16 +427,16 @@ enum ETrackedDeviceProperty Prop_ParentDriver_Uint64 = 1034, Prop_ResourceRoot_String = 1035, Prop_RegisteredDeviceType_String = 1036, - Prop_InputProfilePath_String = 1037, // input profile to use for this device in the input system. Will default to tracking system name if this isn't provided - Prop_NeverTracked_Bool = 1038, // Used for devices that will never have a valid pose by design + Prop_InputProfilePath_String = 1037, + Prop_NeverTracked_Bool = 1038, Prop_NumCameras_Int32 = 1039, - Prop_CameraFrameLayout_Int32 = 1040, // EVRTrackedCameraFrameLayout value - Prop_CameraStreamFormat_Int32 = 1041, // ECameraVideoStreamFormat value - Prop_AdditionalDeviceSettingsPath_String = 1042, // driver-relative path to additional device and global configuration settings - Prop_Identifiable_Bool = 1043, // Whether device supports being identified from vrmonitor (e.g. blink LED, vibrate haptics, etc) + Prop_CameraFrameLayout_Int32 = 1040, + Prop_CameraStreamFormat_Int32 = 1041, + Prop_AdditionalDeviceSettingsPath_String = 1042, + Prop_Identifiable_Bool = 1043, Prop_BootloaderVersion_Uint64 = 1044, - Prop_AdditionalSystemReportData_String = 1045, // additional string to include in system reports about a tracked device - Prop_CompositeFirmwareVersion_String = 1046, // additional FW components from a device that gets propagated into reports + Prop_AdditionalSystemReportData_String = 1045, + Prop_CompositeFirmwareVersion_String = 1046, Prop_Firmware_RemindUpdate_Bool = 1047, Prop_PeripheralApplicationVersion_Uint64 = 1048, Prop_ManufacturerSerialNumber_String = 1049, @@ -482,15 +444,14 @@ enum ETrackedDeviceProperty Prop_EstimatedDeviceFirstUseTime_Int32 = 1051, Prop_DevicePowerUsage_Float = 1052, Prop_IgnoreMotionForStandby_Bool = 1053, - Prop_ActualTrackingSystemName_String = 1054, // the literal local driver name in case someone is playing games with prop 1000 - Prop_AllowCameraToggle_Bool = 1055, // Shows the Enable/Disable camera option. Hide this for certain headsets if they have the camera tracking (since it's always on) - Prop_AllowLightSourceFrequency_Bool = 1056, // Shows the Anti-Flicker option in camera settings. - Prop_SteamRemoteClientID_Uint64 = 1057, // For vrlink + Prop_ActualTrackingSystemName_String = 1054, + Prop_AllowCameraToggle_Bool = 1055, + Prop_AllowLightSourceFrequency_Bool = 1056, + Prop_SteamRemoteClientID_Uint64 = 1057, Prop_Reserved_1058 = 1058, Prop_Reserved_1059 = 1059, Prop_Reserved_1060 = 1060, - // Properties that are unique to TrackedDeviceClass_HMD Prop_ReportsTimeSinceVSync_Bool = 2000, Prop_SecondsFromVsyncToPhotons_Float = 2001, Prop_DisplayFrequency_Float = 2002, @@ -538,14 +499,14 @@ enum ETrackedDeviceProperty Prop_DisplayDebugMode_Bool = 2044, Prop_GraphicsAdapterLuid_Uint64 = 2045, Prop_DriverProvidedChaperonePath_String = 2048, - Prop_ExpectedTrackingReferenceCount_Int32 = 2049, // expected number of sensors or basestations to reserve UI space for - Prop_ExpectedControllerCount_Int32 = 2050, // expected number of tracked controllers to reserve UI space for - Prop_NamedIconPathControllerLeftDeviceOff_String = 2051, // placeholder icon for "left" controller if not yet detected/loaded - Prop_NamedIconPathControllerRightDeviceOff_String = 2052, // placeholder icon for "right" controller if not yet detected/loaded - Prop_NamedIconPathTrackingReferenceDeviceOff_String = 2053, // placeholder icon for sensor/base if not yet detected/loaded - Prop_DoNotApplyPrediction_Bool = 2054, // currently no effect. was used to disable HMD pose prediction on MR, which is now done by MR driver setting velocity=0 + Prop_ExpectedTrackingReferenceCount_Int32 = 2049, + Prop_ExpectedControllerCount_Int32 = 2050, + Prop_NamedIconPathControllerLeftDeviceOff_String = 2051, + Prop_NamedIconPathControllerRightDeviceOff_String = 2052, + Prop_NamedIconPathTrackingReferenceDeviceOff_String = 2053, + Prop_DoNotApplyPrediction_Bool = 2054, Prop_CameraToHeadTransforms_Matrix34_Array = 2055, - Prop_DistortionMeshResolution_Int32 = 2056, // custom resolution of compositor calls to IVRSystem::ComputeDistortion + Prop_DistortionMeshResolution_Int32 = 2056, Prop_DriverIsDrawingControllers_Bool = 2057, Prop_DriverRequestsApplicationPause_Bool = 2058, Prop_DriverRequestsReducedRendering_Bool = 2059, @@ -557,20 +518,19 @@ enum ETrackedDeviceProperty Prop_ImuFactoryGyroScale_Vector3 = 2065, Prop_ImuFactoryAccelerometerBias_Vector3 = 2066, Prop_ImuFactoryAccelerometerScale_Vector3 = 2067, - // reserved 2068 Prop_ConfigurationIncludesLighthouse20Features_Bool = 2069, Prop_AdditionalRadioFeatures_Uint64 = 2070, - Prop_CameraWhiteBalance_Vector4_Array = 2071, // Prop_NumCameras_Int32-sized array of float[4] RGBG white balance calibration data (max size is vr::k_unMaxCameras) - Prop_CameraDistortionFunction_Int32_Array = 2072, // Prop_NumCameras_Int32-sized array of vr::EVRDistortionFunctionType values (max size is vr::k_unMaxCameras) - Prop_CameraDistortionCoefficients_Float_Array = 2073, // Prop_NumCameras_Int32-sized array of double[vr::k_unMaxDistortionFunctionParameters] (max size is vr::k_unMaxCameras) + Prop_CameraWhiteBalance_Vector4_Array = 2071, + Prop_CameraDistortionFunction_Int32_Array = 2072, + Prop_CameraDistortionCoefficients_Float_Array = 2073, Prop_ExpectedControllerType_String = 2074, - Prop_HmdTrackingStyle_Int32 = 2075, // one of EHmdTrackingStyle + Prop_HmdTrackingStyle_Int32 = 2075, Prop_DriverProvidedChaperoneVisibility_Bool = 2076, Prop_HmdColumnCorrectionSettingPrefix_String = 2077, Prop_CameraSupportsCompatibilityModes_Bool = 2078, Prop_SupportsRoomViewDepthProjection_Bool = 2079, - Prop_DisplayAvailableFrameRates_Float_Array = 2080, // populated by compositor from actual EDID list when available from GPU driver - Prop_DisplaySupportsMultipleFramerates_Bool = 2081, // if this is true but Prop_DisplayAvailableFrameRates_Float_Array is empty, explain to user + Prop_DisplayAvailableFrameRates_Float_Array = 2080, + Prop_DisplaySupportsMultipleFramerates_Bool = 2081, Prop_DisplayColorMultLeft_Vector3 = 2082, Prop_DisplayColorMultRight_Vector3 = 2083, Prop_DisplaySupportsRuntimeFramerateChange_Bool = 2084, @@ -579,12 +539,10 @@ enum ETrackedDeviceProperty Prop_DisplayMaxAnalogGain_Float = 2087, Prop_CameraExposureTime_Float = 2088, Prop_CameraGlobalGain_Float = 2089, - // Prop_DashboardLayoutPathName_String = 2090, // DELETED Prop_DashboardScale_Float = 2091, - // Prop_PeerButtonInfo_String = 2092, // DELETED Prop_Hmd_SupportsHDR10_Bool = 2093, Prop_Hmd_EnableParallelRenderCameras_Bool = 2094, - Prop_DriverProvidedChaperoneJson_String = 2095, // higher priority than Prop_DriverProvidedChaperonePath_String + Prop_DriverProvidedChaperoneJson_String = 2095, Prop_ForceSystemLayerUseAppPoses_Bool = 2096, Prop_DashboardLinkSupport_Int32 = 2097, Prop_DisplayMinUIAnalogGain_Float = 2098, @@ -598,7 +556,6 @@ enum ETrackedDeviceProperty Prop_Hmd_SupportsAppThrottling_Bool = 2106, Prop_Hmd_SupportsGpuBusMonitoring_Bool = 2107, Prop_DriverDisplaysIPDChanges_Bool = 2108, - // Prop_Driver_RecenterSupport_Int32 = 2109, // DELETED Prop_Reserved_2110 = 2110, Prop_Reserved_2111 = 2111, Prop_Reserved_2112 = 2112, @@ -610,7 +567,6 @@ enum ETrackedDeviceProperty Prop_Hmd_AllowsClientToControlTextureIndex = 2116, Prop_Reserved_2117 = 2117, - // Driver requested mura correction properties Prop_DriverRequestedMuraCorrectionMode_Int32 = 2200, Prop_DriverRequestedMuraFeather_InnerLeft_Int32 = 2201, Prop_DriverRequestedMuraFeather_InnerRight_Int32 = 2202, @@ -632,21 +588,18 @@ enum ETrackedDeviceProperty Prop_Audio_DriverRecordingVolume_Float = 2308, Prop_Audio_DriverRecordingMute_Bool = 2309, - // Pipewire Audio Stuff Prop_Audio_PipewirePlaybackNode_Int32 = 2400, Prop_Audio_PipewireRecordingNode_Int32 = 2401, - // Properties that are unique to TrackedDeviceClass_Controller Prop_AttachedDeviceId_String = 3000, Prop_SupportedButtons_Uint64 = 3001, - Prop_Axis0Type_Int32 = 3002, // Return value is of type EVRControllerAxisType - Prop_Axis1Type_Int32 = 3003, // Return value is of type EVRControllerAxisType - Prop_Axis2Type_Int32 = 3004, // Return value is of type EVRControllerAxisType - Prop_Axis3Type_Int32 = 3005, // Return value is of type EVRControllerAxisType - Prop_Axis4Type_Int32 = 3006, // Return value is of type EVRControllerAxisType - Prop_ControllerRoleHint_Int32 = 3007, // Return value is of type ETrackedControllerRole + Prop_Axis0Type_Int32 = 3002, + Prop_Axis1Type_Int32 = 3003, + Prop_Axis2Type_Int32 = 3004, + Prop_Axis3Type_Int32 = 3005, + Prop_Axis4Type_Int32 = 3006, + Prop_ControllerRoleHint_Int32 = 3007, - // Properties that are unique to TrackedDeviceClass_TrackingReference Prop_FieldOfViewLeftDegrees_Float = 4000, Prop_FieldOfViewRightDegrees_Float = 4001, Prop_FieldOfViewTopDegrees_Float = 4002, @@ -654,28 +607,27 @@ enum ETrackedDeviceProperty Prop_TrackingRangeMinimumMeters_Float = 4004, Prop_TrackingRangeMaximumMeters_Float = 4005, Prop_ModeLabel_String = 4006, - Prop_CanWirelessIdentify_Bool = 4007, // volatile, based on radio presence and fw discovery + Prop_CanWirelessIdentify_Bool = 4007, Prop_Nonce_Int32 = 4008, - // Properties that are used for user interface like icons names - Prop_IconPathName_String = 5000, // DEPRECATED. Value not referenced. Now expected to be part of icon path properties. - Prop_NamedIconPathDeviceOff_String = 5001, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceSearching_String = 5002, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceSearchingAlert_String = 5003, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceReady_String = 5004, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceReadyAlert_String = 5005, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceNotReady_String = 5006, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceStandby_String = 5007, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceAlertLow_String = 5008, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceStandbyAlert_String = 5009, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - // Properties that are used by helpers, but are opaque to applications + Prop_IconPathName_String = 5000, + Prop_NamedIconPathDeviceOff_String = 5001, + Prop_NamedIconPathDeviceSearching_String = 5002, + Prop_NamedIconPathDeviceSearchingAlert_String = 5003, + Prop_NamedIconPathDeviceReady_String = 5004, + Prop_NamedIconPathDeviceReadyAlert_String = 5005, + Prop_NamedIconPathDeviceNotReady_String = 5006, + Prop_NamedIconPathDeviceStandby_String = 5007, + Prop_NamedIconPathDeviceAlertLow_String = 5008, + Prop_NamedIconPathDeviceStandbyAlert_String = 5009, + + Prop_DisplayHiddenArea_Binary_Start = 5100, Prop_DisplayHiddenArea_Binary_End = 5150, Prop_ParentContainer = 5151, Prop_OverrideContainer_Uint64 = 5152, - // Properties that are unique to drivers Prop_UserConfigPath_String = 6000, Prop_InstallPath_String = 6001, Prop_HasDisplayComponent_Bool = 6002, @@ -689,16 +641,14 @@ enum ETrackedDeviceProperty Prop_DeviceHasNoIMU_Bool = 6010, Prop_UseAdvancedPrediction_Bool = 6011, - // Properties that are set internally based on other information provided by drivers Prop_ControllerType_String = 7000, - //Prop_LegacyInputProfile_String = 7001, // This is no longer used. See "legacy_binding" in the input profile instead. - Prop_ControllerHandSelectionPriority_Int32 = 7002, // Allows hand assignments to prefer some controllers over others. High numbers are selected over low numbers - // Vendors are free to expose private debug data in this reserved region + Prop_ControllerHandSelectionPriority_Int32 = 7002, + + Prop_VendorSpecific_Reserved_Start = 10000, Prop_VendorSpecific_Reserved_End = 10999, - // Addl SteamVR Reserved Space Prop_Reserved_11000 = 11000, Prop_Reserved_11001 = 11001, Prop_Reserved_11002 = 11002, @@ -708,22 +658,20 @@ enum ETrackedDeviceProperty Prop_TrackedDeviceProperty_Max = 1000000, }; -/** No string property will ever be longer than this length */ static const uint32_t k_unMaxPropertyStringSize = 32 * 1024; -/** Used to return errors that occur when reading properties. */ enum ETrackedPropertyError { TrackedProp_Success = 0, TrackedProp_WrongDataType = 1, TrackedProp_WrongDeviceClass = 2, TrackedProp_BufferTooSmall = 3, - TrackedProp_UnknownProperty = 4, // Driver has not set the property (and may not ever). + TrackedProp_UnknownProperty = 4, TrackedProp_InvalidDevice = 5, TrackedProp_CouldNotContactServer = 6, TrackedProp_ValueNotProvidedByDevice = 7, TrackedProp_StringExceedsMaximumLength = 8, - TrackedProp_NotYetAvailable = 9, // The property value isn't known yet, but is expected soon. Call again later. + TrackedProp_NotYetAvailable = 9, TrackedProp_PermissionDenied = 10, TrackedProp_InvalidOperation = 11, TrackedProp_CannotWriteToWildcards = 12, @@ -732,14 +680,13 @@ enum ETrackedPropertyError TrackedProp_InvalidContainer = 15, }; -/** Used to drive certain text in the UI when talking about the tracking system for the HMD */ enum EHmdTrackingStyle { HmdTrackingStyle_Unknown = 0, - HmdTrackingStyle_Lighthouse = 1, // base stations and lasers - HmdTrackingStyle_OutsideInCameras = 2, // Cameras and LED, Rift 1 style - HmdTrackingStyle_InsideOutCameras = 3, // Cameras on HMD looking at the world + HmdTrackingStyle_Lighthouse = 1, + HmdTrackingStyle_OutsideInCameras = 2, + HmdTrackingStyle_InsideOutCameras = 3, }; typedef uint64_t VRActionHandle_t; @@ -753,48 +700,30 @@ static const VRInputValueHandle_t k_ulInvalidInputValueHandle = 0; static const VRInputComponentHandle_t k_ulInvalidInputComponentHandle = 0; -/** Allows the application to control how scene textures are used by the compositor when calling Submit. */ enum EVRSubmitFlags { - // Simple render path. App submits rendered left and right eye images with no lens distortion correction applied. Submit_Default = 0x00, - // App submits final left and right eye images with lens distortion already applied (lens distortion makes the images appear - // barrel distorted with chromatic aberration correction applied). The app would have used the data returned by - // vr::IVRSystem::ComputeDistortion() to apply the correct distortion to the rendered images before calling Submit(). Submit_LensDistortionAlreadyApplied = 0x01, - // If the texture pointer passed in is actually a renderbuffer (e.g. for MSAA in OpenGL) then set this flag. Submit_GlRenderBuffer = 0x02, - // Do not use Submit_Reserved = 0x04, - // Set to indicate that pTexture is a pointer to a VRTextureWithPose_t. - // This flag can be combined with Submit_TextureWithDepth to pass a VRTextureWithPoseAndDepth_t. Submit_TextureWithPose = 0x08, - // Set to indicate that pTexture is a pointer to a VRTextureWithDepth_t. - // This flag can be combined with Submit_TextureWithPose to pass a VRTextureWithPoseAndDepth_t. Submit_TextureWithDepth = 0x10, - // Set to indicate a discontinuity between this and the last frame. - // This will prevent motion smoothing from attempting to extrapolate using the pair. Submit_FrameDiscontinuity = 0x20, - // Set to indicate that pTexture->handle is a contains VRVulkanTextureArrayData_t Submit_VulkanTextureWithArrayData = 0x40, - // If the texture pointer passed in is an OpenGL Array texture, set this flag Submit_GlArrayTexture = 0x80, - // If the texture is an EGL texture and not an glX/wGL texture (Linux only, currently) Submit_IsEgl = 0x100, - // Set to indicate that pTexture is a pointer to a VRTextureWithMotion_t. Submit_TextureWithMotion = 0x200 | Submit_TextureWithPose | Submit_TextureWithDepth, - // Do not use Submit_Reserved2 = 0x08000, Submit_Reserved3 = 0x10000, Submit_Reserved4 = 0x20000, @@ -802,12 +731,9 @@ enum EVRSubmitFlags Submit_Reserved6 = 0x80000, }; -/** Data required for passing Vulkan textures to IVRCompositor::Submit. -* Be sure to call OpenVR_Shutdown before destroying these resources. -* Please see https://github.com/ValveSoftware/openvr/wiki/Vulkan for Vulkan-specific documentation */ struct VRVulkanTextureData_t { - uint64_t m_nImage; // VkImage + uint64_t m_nImage; VkDevice_T *m_pDevice; VkPhysicalDevice_T *m_pPhysicalDevice; VkInstance_T *m_pInstance; @@ -816,17 +742,12 @@ struct VRVulkanTextureData_t uint32_t m_nWidth, m_nHeight, m_nFormat, m_nSampleCount; }; -/** Data required for passing Vulkan texture arrays to IVRCompositor::Submit. -* Be sure to call OpenVR_Shutdown before destroying these resources. -* Please see https://github.com/ValveSoftware/openvr/wiki/Vulkan for Vulkan-specific documentation */ struct VRVulkanTextureArrayData_t : public VRVulkanTextureData_t { uint32_t m_unArrayIndex; uint32_t m_unArraySize; }; -/** Data required for passing D3D12 textures to IVRCompositor::Submit. -* Be sure to call OpenVR_Shutdown before destroying these resources. */ struct D3D12TextureData_t { ID3D12Resource *m_pResource; @@ -834,7 +755,6 @@ struct D3D12TextureData_t uint32_t m_nNodeMask; }; -/** Status of the overall system or tracked objects */ enum EVRState { VRState_Undefined = -1, @@ -848,7 +768,6 @@ enum EVRState VRState_Ready_Alert_Low = 7, }; -/** The types of events that could be posted (and what the parameters mean for each event type) */ enum EVREventType { VREvent_None = 0, @@ -870,132 +789,132 @@ enum EVREventType VREvent_Reserved_0114 = 114, VREvent_Reserved_0115 = 115, - VREvent_ButtonPress = 200, // data is controller - VREvent_ButtonUnpress = 201, // data is controller - VREvent_ButtonTouch = 202, // data is controller - VREvent_ButtonUntouch = 203, // data is controller + VREvent_ButtonPress = 200, + VREvent_ButtonUnpress = 201, + VREvent_ButtonTouch = 202, + VREvent_ButtonUntouch = 203, - // VREvent_DualAnalog_Press = 250, // No longer sent - // VREvent_DualAnalog_Unpress = 251, // No longer sent - // VREvent_DualAnalog_Touch = 252, // No longer sent - // VREvent_DualAnalog_Untouch = 253, // No longer sent - // VREvent_DualAnalog_Move = 254, // No longer sent - // VREvent_DualAnalog_ModeSwitch1 = 255, // No longer sent - // VREvent_DualAnalog_ModeSwitch2 = 256, // No longer sent - VREvent_Modal_Cancel = 257, // Sent to overlays with the - VREvent_MouseMove = 300, // data is mouse - VREvent_MouseButtonDown = 301, // data is mouse - VREvent_MouseButtonUp = 302, // data is mouse - VREvent_FocusEnter = 303, // data is overlay - VREvent_FocusLeave = 304, // data is overlay - VREvent_ScrollDiscrete = 305, // data is scroll - VREvent_TouchPadMove = 306, // data is mouse - VREvent_OverlayFocusChanged = 307, // data is overlay, global event + + + + + + + VREvent_Modal_Cancel = 257, + + VREvent_MouseMove = 300, + VREvent_MouseButtonDown = 301, + VREvent_MouseButtonUp = 302, + VREvent_FocusEnter = 303, + VREvent_FocusLeave = 304, + VREvent_ScrollDiscrete = 305, + VREvent_TouchPadMove = 306, + VREvent_OverlayFocusChanged = 307, VREvent_ReloadOverlays = 308, - VREvent_ScrollSmooth = 309, // data is scroll - VREvent_LockMousePosition = 310, // data is mouse - VREvent_UnlockMousePosition = 311, // data is mouse + VREvent_ScrollSmooth = 309, + VREvent_LockMousePosition = 310, + VREvent_UnlockMousePosition = 311, - VREvent_InputFocusCaptured = 400, // data is process DEPRECATED - VREvent_InputFocusReleased = 401, // data is process DEPRECATED - // VREvent_SceneFocusLost = 402, // data is process - // VREvent_SceneFocusGained = 403, // data is process - VREvent_SceneApplicationChanged = 404, // data is process - The App actually drawing the scene changed (usually to or from the compositor) - // VREvent_SceneFocusChanged = 405, // data is process - This is defunct and will not be called. - VREvent_InputFocusChanged = 406, // data is process - // VREvent_SceneApplicationSecondaryRenderingStarted = 407, - VREvent_SceneApplicationUsingWrongGraphicsAdapter = 408, // data is process - VREvent_ActionBindingReloaded = 409, // data is process - The App that action binds reloaded for + VREvent_InputFocusCaptured = 400, + VREvent_InputFocusReleased = 401, - VREvent_HideRenderModels = 410, // Sent to the scene application to request hiding render models temporarily - VREvent_ShowRenderModels = 411, // Sent to the scene application to request restoring render model visibility - VREvent_SceneApplicationStateChanged = 412, // No data; but query VRApplications()->GetSceneApplicationState(); + VREvent_SceneApplicationChanged = 404, - VREvent_SceneAppPipeDisconnected = 413, // data is process - Called when the scene app's pipe has been closed. + VREvent_InputFocusChanged = 406, + + VREvent_SceneApplicationUsingWrongGraphicsAdapter = 408, + VREvent_ActionBindingReloaded = 409, + + VREvent_HideRenderModels = 410, + VREvent_ShowRenderModels = 411, + + VREvent_SceneApplicationStateChanged = 412, + + VREvent_SceneAppPipeDisconnected = 413, VREvent_ConsoleOpened = 420, VREvent_ConsoleClosed = 421, - VREvent_OverlayShown = 500, // Indicates that an overlay is now visible to someone and should be rendering normally. Reflects IVROverlay::IsOverlayVisible() becoming true. - VREvent_OverlayHidden = 501, // Indicates that an overlay is no longer visible to someone and doesn't need to render frames. Reflects IVROverlay::IsOverlayVisible() becoming false. + VREvent_OverlayShown = 500, + VREvent_OverlayHidden = 501, VREvent_DashboardActivated = 502, VREvent_DashboardDeactivated = 503, - //VREvent_DashboardThumbSelected = 504, // Sent to the overlay manager - data is overlay - No longer sent - //VREvent_DashboardRequested = 505, // Sent to the overlay manager - data is overlay - VREvent_ResetDashboard = 506, // Send to the overlay manager - //VREvent_RenderToast = 507, // Send to the dashboard to render a toast - data is the notification ID -- no longer sent - VREvent_ImageLoaded = 508, // Sent to overlays when a SetOverlayRaw or SetOverlayFromFile call finishes loading - VREvent_ShowKeyboard = 509, // Sent to keyboard renderer in the dashboard to invoke it - VREvent_HideKeyboard = 510, // Sent to keyboard renderer in the dashboard to hide it - VREvent_OverlayGamepadFocusGained = 511, // Sent to an overlay when IVROverlay::SetFocusOverlay is called on it - VREvent_OverlayGamepadFocusLost = 512, // Send to an overlay when it previously had focus and IVROverlay::SetFocusOverlay is called on something else + + + VREvent_ResetDashboard = 506, + + VREvent_ImageLoaded = 508, + VREvent_ShowKeyboard = 509, + VREvent_HideKeyboard = 510, + VREvent_OverlayGamepadFocusGained = 511, + VREvent_OverlayGamepadFocusLost = 512, VREvent_OverlaySharedTextureChanged = 513, - //VREvent_DashboardGuideButtonDown = 514, // These are no longer sent - //VREvent_DashboardGuideButtonUp = 515, - VREvent_ScreenshotTriggered = 516, // Screenshot button combo was pressed, Dashboard should request a screenshot - VREvent_ImageFailed = 517, // Sent to overlays when a SetOverlayRaw or SetOverlayfromFail fails to load + + + VREvent_ScreenshotTriggered = 516, + VREvent_ImageFailed = 517, VREvent_DashboardOverlayCreated = 518, VREvent_SwitchGamepadFocus = 519, - // Screenshot API - VREvent_RequestScreenshot = 520, // Sent by vrclient application to compositor to take a screenshot - VREvent_ScreenshotTaken = 521, // Sent by compositor to the application that the screenshot has been taken - VREvent_ScreenshotFailed = 522, // Sent by compositor to the application that the screenshot failed to be taken - VREvent_SubmitScreenshotToDashboard = 523, // Sent by compositor to the dashboard that a completed screenshot was submitted - VREvent_ScreenshotProgressToDashboard = 524, // Sent by compositor to the dashboard that a completed screenshot was submitted + + VREvent_RequestScreenshot = 520, + VREvent_ScreenshotTaken = 521, + VREvent_ScreenshotFailed = 522, + VREvent_SubmitScreenshotToDashboard = 523, + VREvent_ScreenshotProgressToDashboard = 524, VREvent_PrimaryDashboardDeviceChanged = 525, - VREvent_RoomViewShown = 526, // Sent by compositor whenever room-view is enabled (for scene apps only - not for construct or transient bounds) - VREvent_RoomViewHidden = 527, // Sent by compositor whenever room-view is disabled (for scene apps only - not for construct or transient bounds) - VREvent_ShowUI = 528, // data is showUi - VREvent_ShowDevTools = 529, // data is showDevTools + VREvent_RoomViewShown = 526, + VREvent_RoomViewHidden = 527, + VREvent_ShowUI = 528, + VREvent_ShowDevTools = 529, VREvent_DesktopViewUpdating = 530, VREvent_DesktopViewReady = 531, VREvent_StartDashboard = 532, VREvent_ElevatePrism = 533, - VREvent_OverlayClosed = 534, // The overlay's close button is pressed. - VREvent_DashboardThumbChanged = 535, // Sent when a dashboard thumbnail image changes - VREvent_DesktopMightBeVisible = 536, // Sent when any known desktop related overlay is visible - VREvent_DesktopMightBeHidden = 537, // Sent when all known desktop related overlays are hidden - VREvent_MutualSteamCapabilitiesChanged = 538, // Sent when the set of capabilities common between both Steam and SteamVR have changed. - VREvent_OverlayCreated = 539, // An OpenVR overlay of any sort was created. Data is overlay. - VREvent_OverlayDestroyed = 540, // An OpenVR overlay of any sort was destroyed. Data is overlay. - VREvent_OverlayNameChanged = 544, // An OpenVR overlay's name changed. Data is overlay. + VREvent_OverlayClosed = 534, + VREvent_DashboardThumbChanged = 535, + VREvent_DesktopMightBeVisible = 536, + VREvent_DesktopMightBeHidden = 537, + VREvent_MutualSteamCapabilitiesChanged = 538, + VREvent_OverlayCreated = 539, + VREvent_OverlayDestroyed = 540, + VREvent_OverlayNameChanged = 544, VREvent_TrackingRecordingStarted = 541, VREvent_TrackingRecordingStopped = 542, VREvent_SetTrackingRecordingPath = 543, - VREvent_Reserved_0560 = 560, // No data - VREvent_Reserved_0561 = 561, // No data - VREvent_Reserved_0562 = 562, // No data - VREvent_Reserved_0563 = 563, // No data + VREvent_Reserved_0560 = 560, + VREvent_Reserved_0561 = 561, + VREvent_Reserved_0562 = 562, + VREvent_Reserved_0563 = 563, VREvent_Notification_Shown = 600, VREvent_Notification_Hidden = 601, VREvent_Notification_BeginInteraction = 602, VREvent_Notification_Destroyed = 603, - VREvent_Quit = 700, // data is process - VREvent_ProcessQuit = 701, // data is process - //VREvent_QuitAborted_UserPrompt = 702, // data is process - VREvent_QuitAcknowledged = 703, // data is process - VREvent_DriverRequestedQuit = 704, // The driver has requested that SteamVR shut down - VREvent_RestartRequested = 705, // A driver or other component wants the user to restart SteamVR - VREvent_InvalidateSwapTextureSets = 706, - VREvent_RequestDisconnectWirelessHMD = 707, // vrserver asks vrlink to disconnect + VREvent_Quit = 700, + VREvent_ProcessQuit = 701, - VREvent_ChaperoneDataHasChanged = 800, // this will never happen with the new chaperone system + VREvent_QuitAcknowledged = 703, + VREvent_DriverRequestedQuit = 704, + VREvent_RestartRequested = 705, + VREvent_InvalidateSwapTextureSets = 706, + VREvent_RequestDisconnectWirelessHMD = 707, + + VREvent_ChaperoneDataHasChanged = 800, VREvent_ChaperoneUniverseHasChanged = 801, - VREvent_ChaperoneTempDataHasChanged = 802, // this will never happen with the new chaperone system + VREvent_ChaperoneTempDataHasChanged = 802, VREvent_ChaperoneSettingsHaveChanged = 803, VREvent_SeatedZeroPoseReset = 804, - VREvent_ChaperoneFlushCache = 805, // Sent when the process needs to reload any cached data it retrieved from VRChaperone() - VREvent_ChaperoneRoomSetupStarting = 806, // Triggered by CVRChaperoneClient::RoomSetupStarting - VREvent_ChaperoneRoomSetupCommitted = 807, // Triggered by CVRChaperoneClient::CommitWorkingCopy (formerly VREvent_ChaperoneRoomSetupFinished) + VREvent_ChaperoneFlushCache = 805, + VREvent_ChaperoneRoomSetupStarting = 806, + VREvent_ChaperoneRoomSetupCommitted = 807, VREvent_StandingZeroPoseReset = 808, VREvent_Reserved_0809 = 809, VREvent_Reserved_0810 = 810, @@ -1040,32 +959,26 @@ enum EVREventType VREvent_FirmwareUpdateStarted = 1100, VREvent_FirmwareUpdateFinished = 1101, - VREvent_KeyboardClosed = 1200, // DEPRECATED: Sent only to the overlay it closed for, or globally if it was closed for a scene app - VREvent_KeyboardCharInput = 1201, // Sent on keyboard input. Warning: event type appears as both global event and overlay event - VREvent_KeyboardDone = 1202, // Sent when DONE button clicked on keyboard. Warning: event type appears as both global event and overlay event - VREvent_KeyboardOpened_Global = 1203, // Sent globally when the keyboard is opened. data.keyboard.overlayHandle is who it was opened for (scene app if k_ulOverlayHandleInvalid) - VREvent_KeyboardClosed_Global = 1204, // Sent globally when the keyboard is closed. data.keyboard.overlayHandle is who it was opened for (scene app if k_ulOverlayHandleInvalid) + VREvent_KeyboardClosed = 1200, + VREvent_KeyboardCharInput = 1201, + VREvent_KeyboardDone = 1202, + VREvent_KeyboardOpened_Global = 1203, + VREvent_KeyboardClosed_Global = 1204, - //VREvent_ApplicationTransitionStarted = 1300, - //VREvent_ApplicationTransitionAborted = 1301, - //VREvent_ApplicationTransitionNewAppStarted = 1302, VREvent_ApplicationListUpdated = 1303, VREvent_ApplicationMimeTypeLoad = 1304, - // VREvent_ApplicationTransitionNewAppLaunchComplete = 1305, VREvent_ProcessConnected = 1306, VREvent_ProcessDisconnected = 1307, - //VREvent_Compositor_MirrorWindowShown = 1400, // DEPRECATED - //VREvent_Compositor_MirrorWindowHidden = 1401, // DEPRECATED VREvent_Compositor_ChaperoneBoundsShown = 1410, VREvent_Compositor_ChaperoneBoundsHidden = 1411, VREvent_Compositor_DisplayDisconnected = 1412, VREvent_Compositor_DisplayReconnected = 1413, - VREvent_Compositor_HDCPError = 1414, // data is hdcpError + VREvent_Compositor_HDCPError = 1414, VREvent_Compositor_ApplicationNotResponding = 1415, VREvent_Compositor_ApplicationResumed = 1416, VREvent_Compositor_OutOfVideoMemory = 1417, - VREvent_Compositor_DisplayModeNotSupported = 1418, // k_pch_SteamVR_PreferredRefreshRate + VREvent_Compositor_DisplayModeNotSupported = 1418, VREvent_Compositor_StageOverrideReady = 1419, VREvent_Compositor_RequestDisconnectReconnect = 1420, @@ -1082,56 +995,49 @@ enum EVREventType VREvent_MessageOverlay_Closed = 1650, VREvent_MessageOverlayCloseRequested = 1651, - VREvent_Input_HapticVibration = 1700, // data is hapticVibration - VREvent_Input_BindingLoadFailed = 1701, // data is inputBinding - VREvent_Input_BindingLoadSuccessful = 1702, // data is inputBinding - VREvent_Input_ActionManifestReloaded = 1703, // no data - VREvent_Input_ActionManifestLoadFailed = 1704, // data is actionManifest - VREvent_Input_ProgressUpdate = 1705, // data is progressUpdate + VREvent_Input_HapticVibration = 1700, + VREvent_Input_BindingLoadFailed = 1701, + VREvent_Input_BindingLoadSuccessful = 1702, + VREvent_Input_ActionManifestReloaded = 1703, + VREvent_Input_ActionManifestLoadFailed = 1704, + VREvent_Input_ProgressUpdate = 1705, VREvent_Input_TrackerActivated = 1706, VREvent_Input_BindingsUpdated = 1707, VREvent_Input_BindingSubscriptionChanged = 1708, - VREvent_SpatialAnchors_PoseUpdated = 1800, // data is spatialAnchor. broadcast - VREvent_SpatialAnchors_DescriptorUpdated = 1801, // data is spatialAnchor. broadcast - VREvent_SpatialAnchors_RequestPoseUpdate = 1802, // data is spatialAnchor. sent to specific driver - VREvent_SpatialAnchors_RequestDescriptorUpdate = 1803, // data is spatialAnchor. sent to specific driver + VREvent_SpatialAnchors_PoseUpdated = 1800, + VREvent_SpatialAnchors_DescriptorUpdated = 1801, + VREvent_SpatialAnchors_RequestPoseUpdate = 1802, + VREvent_SpatialAnchors_RequestDescriptorUpdate = 1803, - VREvent_SystemReport_Started = 1900, // user or system initiated generation of a system report. broadcast + VREvent_SystemReport_Started = 1900, - VREvent_Monitor_ShowHeadsetView = 2000, // data is process - VREvent_Monitor_HideHeadsetView = 2001, // data is process + VREvent_Monitor_ShowHeadsetView = 2000, + VREvent_Monitor_HideHeadsetView = 2001, VREvent_Audio_SetSpeakersVolume = 2100, VREvent_Audio_SetSpeakersMute = 2101, VREvent_Audio_SetMicrophoneVolume = 2102, VREvent_Audio_SetMicrophoneMute = 2103, - VREvent_RenderModel_CountChanged = 2200, //Number of RenderModels in the system has changed + VREvent_RenderModel_CountChanged = 2200, - // Vendors are free to expose private events in this reserved region VREvent_VendorSpecific_Reserved_Start = 10000, VREvent_VendorSpecific_Reserved_End = 19999, }; -/** Level of Hmd activity */ -// UserInteraction_Timeout means the device is in the process of timing out. -// InUse = ( k_EDeviceActivityLevel_UserInteraction || k_EDeviceActivityLevel_UserInteraction_Timeout ) -// VREvent_TrackedDeviceUserInteractionStarted fires when the devices transitions from Standby -> UserInteraction or Idle -> UserInteraction. -// VREvent_TrackedDeviceUserInteractionEnded fires when the devices transitions from UserInteraction_Timeout -> Idle enum EDeviceActivityLevel { k_EDeviceActivityLevel_Unknown = -1, - k_EDeviceActivityLevel_Idle = 0, // No activity for the last 10 seconds - k_EDeviceActivityLevel_UserInteraction = 1, // Activity (movement or prox sensor) is happening now - k_EDeviceActivityLevel_UserInteraction_Timeout = 2, // No activity for the last 0.5 seconds - k_EDeviceActivityLevel_Standby = 3, // Idle for at least 5 seconds (configurable in Settings -> Power Management) + k_EDeviceActivityLevel_Idle = 0, + k_EDeviceActivityLevel_UserInteraction = 1, + k_EDeviceActivityLevel_UserInteraction_Timeout = 2, + k_EDeviceActivityLevel_Standby = 3, k_EDeviceActivityLevel_Idle_Timeout = 4, }; -/** VR controller button and axis IDs */ enum EVRButtonId { k_EButton_System = 0, @@ -1151,7 +1057,6 @@ enum EVRButtonId k_EButton_Axis3 = 35, k_EButton_Axis4 = 36, - // aliases for well known controllers k_EButton_SteamVR_Touchpad = k_EButton_Axis0, k_EButton_SteamVR_Trigger = k_EButton_Axis1, @@ -1169,14 +1074,12 @@ enum EVRButtonId inline uint64_t ButtonMaskFromId( EVRButtonId id ) { return 1ull << id; } -/** used for controller button events */ struct VREvent_Controller_t { - uint32_t button; // EVRButtonId enum + uint32_t button; }; -/** used for simulated mouse events in overlay space */ enum EVRMouseButton { VRMouseButton_Left = 0x0001, @@ -1185,93 +1088,71 @@ enum EVRMouseButton }; -/** used for simulated mouse events in overlay space */ struct VREvent_Mouse_t { - float x, y; // co-ords are in GL space, bottom left of the texture is 0,0 - uint32_t button; // EVRMouseButton enum + float x, y; + uint32_t button; - // if from an event triggered by cursor input on an overlay that supports multiple cursors, this is the index of - // which tracked cursor the event is for uint32_t cursorIndex; }; -/** used for simulated mouse wheel scroll */ struct VREvent_Scroll_t { float xdelta, ydelta; uint32_t unused; - float viewportscale; // For scrolling on an overlay with laser mouse, this is the overlay's vertical size relative to the overlay height. Range: [0,1] + float viewportscale; - // if from an event triggered by cursor input on an overlay that supports multiple cursors, this is the index of - // which tracked cursor the event is for uint32_t cursorIndex; }; -/** when in mouse input mode you can receive data from the touchpad, these events are only sent if the users finger - is on the touchpad (or just released from it). These events are sent to overlays with the VROverlayFlags_SendVRTouchpadEvents - flag set. -**/ struct VREvent_TouchPadMove_t { - // true if the users finger is detected on the touch pad bool bFingerDown; - // How long the finger has been down in seconds float flSecondsFingerDown; - // These values indicate the starting finger position (so you can do some basic swipe stuff) float fValueXFirst; float fValueYFirst; - // This is the raw sampled coordinate without deadzoning float fValueXRaw; float fValueYRaw; }; -/** notification related events. Details will still change at this point */ struct VREvent_Notification_t { uint64_t ulUserValue; uint32_t notificationId; }; -/** Used for events about processes */ struct VREvent_Process_t { uint32_t pid; uint32_t oldPid; bool bForced; - // If the associated event was triggered by a connection loss bool bConnectionLost; }; -/** Used for a few events about overlays */ struct VREvent_Overlay_t { - uint64_t overlayHandle; // VROverlayHandle_t + uint64_t overlayHandle; uint64_t devicePath; uint64_t memoryBlockId; - // if from an event triggered by cursor input on an overlay that supports multiple cursors, this is the index of - // which tracked cursor the event is for uint32_t cursorIndex; }; -/** Used for a few events about overlays */ struct VREvent_Status_t { - uint32_t statusState; // EVRState enum + uint32_t statusState; }; -/** Used for keyboard events */ struct VREvent_Keyboard_t { - char cNewInput[8]; // 7 bytes of utf8 + null - uint64_t uUserValue; // caller specified opaque token - uint64_t overlayHandle; // VROverlayHandle_t + char cNewInput[8]; + uint64_t uUserValue; + uint64_t overlayHandle; }; struct VREvent_Ipd_t @@ -1285,7 +1166,6 @@ struct VREvent_Chaperone_t uint64_t m_nCurrentUniverse; }; -/** Not actually used for any events */ struct VREvent_Reserved_t { uint64_t reserved0; @@ -1331,7 +1211,7 @@ struct VREvent_EditingCameraSurface_t struct VREvent_MessageOverlay_t { - uint32_t unVRMessageOverlayResponse; // vr::VRMessageOverlayResponse enum + uint32_t unVRMessageOverlayResponse; }; struct VREvent_Property_t @@ -1342,8 +1222,8 @@ struct VREvent_Property_t struct VREvent_HapticVibration_t { - uint64_t containerHandle; // property container handle of the device with the haptic component - uint64_t componentHandle; // Which haptic component needs to vibrate + uint64_t containerHandle; + uint64_t componentHandle; float fDurationSeconds; float fFrequency; float fAmplitude; @@ -1389,7 +1269,6 @@ enum EShowUIType { ShowUI_ControllerBinding = 0, ShowUI_ManageTrackers = 1, - // ShowUI_QuickStart = 2, // Deprecated ShowUI_Pairing = 3, ShowUI_Settings = 4, ShowUI_DebugCommands = 5, @@ -1464,23 +1343,18 @@ typedef union VREvent_HDCPError_t hdcpError; VREvent_AudioVolumeControl_t audioVolumeControl; VREvent_AudioMuteControl_t audioMuteControl; - /** NOTE!!! If you change this you MUST manually update openvr_interop.cs.py and openvr_api_flat.h.py */ } VREvent_Data_t; #if defined(__linux__) || defined(__APPLE__) -// This structure was originally defined mis-packed on Linux, preserved for -// compatibility. #pragma pack( push, 4 ) #endif -/** An event posted by the server to all running applications */ struct VREvent_t { - uint32_t eventType; // EVREventType enum + uint32_t eventType; TrackedDeviceIndex_t trackedDeviceIndex; float eventAgeSeconds; - // event data must be the end of the struct as its size is variable VREvent_Data_t data; }; @@ -1501,11 +1375,10 @@ enum EVRComponentProperty }; -/** Describes state information about a render-model component, including transforms and other dynamic properties */ struct RenderModel_ComponentState_t { - HmdMatrix34_t mTrackingToComponentRenderModel; // Transform required when drawing the component render model - HmdMatrix34_t mTrackingToComponentLocal; // Transform available for attaching to a local component coordinate system (-Z out from surface ) + HmdMatrix34_t mTrackingToComponentRenderModel; + HmdMatrix34_t mTrackingToComponentLocal; VRComponentProperties uProperties; }; @@ -1553,12 +1426,6 @@ enum EVRSpatialAnchorError VRSpatialAnchorError_UnknownDriver = 13, }; -/** The mesh to draw into the stencil (or depth) buffer to perform -* early stencil (or depth) kills of pixels that will never appear on the HMD. -* This mesh draws on all the pixels that will be hidden after distortion. -* -* If the HMD does not provide a visible area mesh pVertexData will be -* NULL and unTriangleCount will be 0. */ struct HiddenAreaMesh_t { const HmdVector2_t *pVertexData; @@ -1576,48 +1443,36 @@ enum EHiddenAreaMeshType }; -/** Identifies what kind of axis is on the controller at index n. Read this type -* with pVRSystem->Get( nControllerDeviceIndex, Prop_Axis0Type_Int32 + n ); -*/ enum EVRControllerAxisType { k_eControllerAxis_None = 0, k_eControllerAxis_TrackPad = 1, k_eControllerAxis_Joystick = 2, - k_eControllerAxis_Trigger = 3, // Analog trigger data is in the X axis + k_eControllerAxis_Trigger = 3, }; -/** contains information about one axis on the controller */ struct VRControllerAxis_t { - float x; // Ranges from -1.0 to 1.0 for joysticks and track pads. Ranges from 0.0 to 1.0 for triggers were 0 is fully released. - float y; // Ranges from -1.0 to 1.0 for joysticks and track pads. Is always 0.0 for triggers. + float x; + float y; }; -/** the number of axes in the controller state */ static const uint32_t k_unControllerStateAxisCount = 5; #if defined(__linux__) || defined(__APPLE__) -// This structure was originally defined mis-packed on Linux, preserved for -// compatibility. #pragma pack( push, 4 ) #endif -/** Holds all the state of a controller at one moment in time. */ struct VRControllerState001_t { - // If packet num matches that on your prior call, then the controller state hasn't been changed since - // your last call and there is no need to process it uint32_t unPacketNum; - // bit flags for each of the buttons. Use ButtonMaskFromId to turn an ID into a mask uint64_t ulButtonPressed; uint64_t ulButtonTouched; - // Axis data for the controller's analog inputs VRControllerAxis_t rAxis[ k_unControllerStateAxisCount ]; }; #if defined(__linux__) || defined(__APPLE__) @@ -1628,7 +1483,6 @@ struct VRControllerState001_t typedef VRControllerState001_t VRControllerState_t; -/** determines how to provide output to the application of various event processing functions. */ enum EVRControllerEventOutputType { ControllerEventOutput_OSEvents = 0, @@ -1637,7 +1491,6 @@ enum EVRControllerEventOutputType -/** Collision Bounds Style */ enum ECollisionBoundsStyle { COLLISION_BOUNDS_STYLE_BEGINNER = 0, @@ -1649,12 +1502,10 @@ enum ECollisionBoundsStyle COLLISION_BOUNDS_STYLE_COUNT }; -/** used to refer to a single VR overlay */ typedef uint64_t VROverlayHandle_t; static const VROverlayHandle_t k_ulOverlayHandleInvalid = 0; -/** Errors that can occur around VR overlays */ enum EVROverlayError { VROverlayError_None = 0, @@ -1662,7 +1513,7 @@ enum EVROverlayError VROverlayError_UnknownOverlay = 10, VROverlayError_InvalidHandle = 11, VROverlayError_PermissionDenied = 12, - VROverlayError_OverlayLimitExceeded = 13, // No more overlays could be created because the maximum number already exist + VROverlayError_OverlayLimitExceeded = 13, VROverlayError_WrongVisibilityType = 14, VROverlayError_KeyTooLong = 15, VROverlayError_NameTooLong = 16, @@ -1685,33 +1536,29 @@ enum EVROverlayError VROverlayError_TimedOut = 34, }; -/** enum values to pass in to VR_Init to identify whether the application will -* draw a 3D scene. */ enum EVRApplicationType { - VRApplication_Other = 0, // Some other kind of application that isn't covered by the other entries - VRApplication_Scene = 1, // Application will submit 3D frames - VRApplication_Overlay = 2, // Application only interacts with overlays - VRApplication_Background = 3, // Application should not start SteamVR if it's not already running, and should not - // keep it running if everything else quits. - VRApplication_Utility = 4, // Init should not try to load any drivers. The application needs access to utility - // interfaces (like IVRSettings and IVRApplications) but not hardware. - VRApplication_VRMonitor = 5, // Reserved for vrmonitor - VRApplication_SteamWatchdog = 6,// Reserved for Steam - VRApplication_Bootstrapper = 7, // reserved for vrstartup - VRApplication_WebHelper = 8, // reserved for vrwebhelper - VRApplication_OpenXRInstance = 9, // reserved for openxr (created instance, but not session yet) - VRApplication_OpenXRScene = 10, // reserved for openxr (started session) - VRApplication_OpenXROverlay = 11, // reserved for openxr (started overlay session) - VRApplication_Prism = 12, // reserved for the vrprismhost process - VRApplication_RoomView = 13, // reserved for the RoomView process + VRApplication_Other = 0, + VRApplication_Scene = 1, + VRApplication_Overlay = 2, + VRApplication_Background = 3, + + VRApplication_Utility = 4, + + VRApplication_VRMonitor = 5, + VRApplication_SteamWatchdog = 6, + VRApplication_Bootstrapper = 7, + VRApplication_WebHelper = 8, + VRApplication_OpenXRInstance = 9, + VRApplication_OpenXRScene = 10, + VRApplication_OpenXROverlay = 11, + VRApplication_Prism = 12, + VRApplication_RoomView = 13, VRApplication_Max }; -/** returns true if the specified application type is one of the -* OpenXR types */ inline bool IsOpenXRAppType( EVRApplicationType eType ) { return eType == VRApplication_OpenXRInstance @@ -1720,7 +1567,6 @@ inline bool IsOpenXRAppType( EVRApplicationType eType ) } -/** returns true if the specified application type submits eye buffers */ inline bool BAppTypeSubmitsEyeBuffers( EVRApplicationType eType ) { return eType == VRApplication_Scene @@ -1729,7 +1575,6 @@ inline bool BAppTypeSubmitsEyeBuffers( EVRApplicationType eType ) } -/** error codes for firmware */ enum EVRFirmwareError { VRFirmwareError_None = 0, @@ -1738,7 +1583,6 @@ enum EVRFirmwareError }; -/** error codes for notifications */ enum EVRNotificationError { VRNotificationError_OK = 0, @@ -1752,31 +1596,17 @@ enum EVRNotificationError enum EVRSkeletalMotionRange { - // The range of motion of the skeleton takes into account any physical limits imposed by - // the controller itself. This will tend to be the most accurate pose compared to the user's - // actual hand pose, but might not allow a closed fist for example VRSkeletalMotionRange_WithController = 0, - // Retarget the range of motion provided by the input device to make the hand appear to move - // as if it was not holding a controller. eg: map "hand grasping controller" to "closed fist" VRSkeletalMotionRange_WithoutController = 1, }; enum EVRSkeletalTrackingLevel { - // body part location can't be directly determined by the device. Any skeletal pose provided by - // the device is estimated by assuming the position required to active buttons, triggers, joysticks, - // or other input sensors. - // E.g. Vive Controller, Gamepad VRSkeletalTracking_Estimated = 0, - // body part location can be measured directly but with fewer degrees of freedom than the actual body - // part. Certain body part positions may be unmeasured by the device and estimated from other input data. - // E.g. Index Controllers, gloves that only measure finger curl VRSkeletalTracking_Partial = 1, - // Body part location can be measured directly throughout the entire range of motion of the body part. - // E.g. Mocap suit for the full body, gloves that measure rotation of each finger segment VRSkeletalTracking_Full = 2, VRSkeletalTrackingLevel_Count, @@ -1784,14 +1614,11 @@ enum EVRSkeletalTrackingLevel }; -/** Type used for referring to bones by their index */ typedef int32_t BoneIndex_t; const BoneIndex_t k_unInvalidBoneIndex = -1; -/** error codes returned by Vr_Init */ -// Please add adequate error description to https://developer.valvesoftware.com/w/index.php?title=Category:SteamVRHelp enum EVRInitError { VRInitError_None = 0, @@ -1812,8 +1639,8 @@ enum EVRInitError VRInitError_Init_NoLogPath = 112, VRInitError_Init_PathRegistryNotWritable = 113, VRInitError_Init_AppInfoInitFailed = 114, - VRInitError_Init_Retry = 115, // Used internally to cause retries to vrserver - VRInitError_Init_InitCanceledByUser = 116, // The calling application should silently exit. The user canceled app startup + VRInitError_Init_Retry = 115, + VRInitError_Init_InitCanceledByUser = 116, VRInitError_Init_AnotherAppLaunching = 117, VRInitError_Init_SettingsInitFailed = 118, VRInitError_Init_ShuttingDown = 119, @@ -1878,14 +1705,12 @@ enum EVRInitError VRInitError_Driver_CalibrationInvalid = 207, VRInitError_Driver_HmdDisplayNotFound = 208, VRInitError_Driver_TrackedDeviceInterfaceUnknown = 209, - // VRInitError_Driver_HmdDisplayNotFoundAfterFix = 210, // not needed: here for historic reasons VRInitError_Driver_HmdDriverIdOutOfBounds = 211, VRInitError_Driver_HmdDisplayMirrored = 212, VRInitError_Driver_HmdDisplayNotFoundLaptop = 213, VRInitError_Driver_PeerDriverNotInstalled = 214, VRInitError_Driver_WirelessHmdNotConnected = 215, - // Never make error 259 because we return it from main and it would conflict with STILL_ACTIVE VRInitError_IPC_ServerInitFailed = 300, VRInitError_IPC_ConnectFailed = 301, @@ -2029,14 +1854,13 @@ enum EVRInitError VRInitError_Steam_SteamInstallationNotFound = 2000, - // Strictly a placeholder VRInitError_LastError }; enum EVRScreenshotType { VRScreenshotType_None = 0, - VRScreenshotType_Mono = 1, // left eye only + VRScreenshotType_Mono = 1, VRScreenshotType_Stereo = 2, VRScreenshotType_Cubemap = 3, VRScreenshotType_MonoPanorama = 4, @@ -2074,15 +1898,15 @@ enum EVRTrackedCameraFrameLayout { EVRTrackedCameraFrameLayout_Mono = 0x0001, EVRTrackedCameraFrameLayout_Stereo = 0x0002, - EVRTrackedCameraFrameLayout_VerticalLayout = 0x0010, // Stereo frames are Top/Bottom (left/right) - EVRTrackedCameraFrameLayout_HorizontalLayout = 0x0020, // Stereo frames are Left/Right + EVRTrackedCameraFrameLayout_VerticalLayout = 0x0010, + EVRTrackedCameraFrameLayout_HorizontalLayout = 0x0020, }; enum EVRTrackedCameraFrameType { - VRTrackedCameraFrameType_Distorted = 0, // This is the camera video frame size in pixels, still distorted. - VRTrackedCameraFrameType_Undistorted, // In pixels, an undistorted inscribed rectangle region without invalid regions. This size is subject to changes shortly. - VRTrackedCameraFrameType_MaximumUndistorted, // In pixels, maximum undistorted with invalid regions. Non zero alpha component identifies valid regions. + VRTrackedCameraFrameType_Distorted = 0, + VRTrackedCameraFrameType_Undistorted, + VRTrackedCameraFrameType_MaximumUndistorted, MAX_CAMERA_FRAME_TYPES }; @@ -2111,35 +1935,33 @@ struct CameraVideoStreamFrameHeader_t TrackedDevicePose_t trackedDevicePose; - uint64_t ulFrameExposureTime; // mid-point of the exposure of the image in host system ticks + uint64_t ulFrameExposureTime; }; -// Screenshot types typedef uint32_t ScreenshotHandle_t; static const uint32_t k_unScreenshotHandleInvalid = 0; -/** Compositor frame timing reprojection flags. */ const uint32_t VRCompositor_ReprojectionReason_Cpu = 0x01; const uint32_t VRCompositor_ReprojectionReason_Gpu = 0x02; -const uint32_t VRCompositor_ReprojectionAsync = 0x04; // This flag indicates the async reprojection mode is active, - // but does not indicate if reprojection actually happened or not. - // Use the ReprojectionReason flags above to check if reprojection - // was actually applied (i.e. scene texture was reused). - // NumFramePresents > 1 also indicates the scene texture was reused, - // and also the number of times that it was presented in total. +const uint32_t VRCompositor_ReprojectionAsync = 0x04; -const uint32_t VRCompositor_ReprojectionMotion = 0x08; // This flag indicates whether or not motion smoothing was triggered for this frame -const uint32_t VRCompositor_PredictionMask = 0xF0; // The runtime may predict more than one frame ahead if - // it detects the application is taking too long to render. These - // bits will contain the count of additional frames (normally zero). - // Use the VR_COMPOSITOR_ADDITIONAL_PREDICTED_FRAMES macro to read from - // the latest frame timing entry. -const uint32_t VRCompositor_ThrottleMask = 0xF00; // Number of frames the compositor is throttling the application. - // Use the VR_COMPOSITOR_NUMBER_OF_THROTTLED_FRAMES macro to read from - // the latest frame timing entry. + + + +const uint32_t VRCompositor_ReprojectionMotion = 0x08; + +const uint32_t VRCompositor_PredictionMask = 0xF0; + + + + + +const uint32_t VRCompositor_ThrottleMask = 0xF00; + + #define VR_COMPOSITOR_ADDITIONAL_PREDICTED_FRAMES( timing ) ( ( ( timing ).m_nReprojectionFlags & vr::VRCompositor_PredictionMask ) >> 4 ) #define VR_COMPOSITOR_NUMBER_OF_THROTTLED_FRAMES( timing ) ( ( ( timing ).m_nReprojectionFlags & vr::VRCompositor_ThrottleMask ) >> 8 ) @@ -2147,45 +1969,43 @@ const uint32_t VRCompositor_ThrottleMask = 0xF00; // Number of frames the comp #if defined(__linux__) || defined(__APPLE__) #pragma pack( push, 4 ) #endif -/** Provides a single frame's timing information to the app */ struct Compositor_FrameTiming { - uint32_t m_nSize; // Set to sizeof( Compositor_FrameTiming ) + uint32_t m_nSize; uint32_t m_nFrameIndex; - uint32_t m_nNumFramePresents; // number of times this frame was presented - uint32_t m_nNumMisPresented; // number of times this frame was presented on a vsync other than it was originally predicted to - uint32_t m_nNumDroppedFrames; // number of additional times previous frame was scanned out + uint32_t m_nNumFramePresents; + uint32_t m_nNumMisPresented; + uint32_t m_nNumDroppedFrames; uint32_t m_nReprojectionFlags; - /** Absolute time reference for comparing frames. This aligns with the vsync that running start is relative to. */ double m_flSystemTimeInSeconds; - /** These times may include work from other processes due to OS scheduling. - * The fewer packets of work these are broken up into, the less likely this will happen. - * GPU work can be broken up by calling Flush. This can sometimes be useful to get the GPU started - * processing that work earlier in the frame. */ - float m_flPreSubmitGpuMs; // time spent rendering the scene (gpu work submitted between WaitGetPoses and second Submit) - float m_flPostSubmitGpuMs; // additional time spent rendering by application (e.g. companion window) - float m_flTotalRenderGpuMs; // time between work submitted immediately after present (ideally vsync) until the end of compositor submitted work - float m_flCompositorRenderGpuMs; // time spend performing distortion correction, rendering chaperone, overlays, etc. - float m_flCompositorRenderCpuMs; // time spent on cpu submitting the above work for this frame - float m_flCompositorIdleCpuMs; // time spent waiting for running start (application could have used this much more time) - /** Miscellaneous measured intervals. */ - float m_flClientFrameIntervalMs; // time between calls to WaitGetPoses - float m_flPresentCallCpuMs; // time blocked on call to present (usually 0.0, but can go long) - float m_flWaitForPresentCpuMs; // time spent spin-waiting for frame index to change (not near-zero indicates wait object failure) - float m_flSubmitFrameMs; // time spent in IVRCompositor::Submit (not near-zero indicates driver issue) - /** The following are all relative to this frame's SystemTimeInSeconds */ + + + float m_flPreSubmitGpuMs; + float m_flPostSubmitGpuMs; + float m_flTotalRenderGpuMs; + float m_flCompositorRenderGpuMs; + float m_flCompositorRenderCpuMs; + float m_flCompositorIdleCpuMs; + + + float m_flClientFrameIntervalMs; + float m_flPresentCallCpuMs; + float m_flWaitForPresentCpuMs; + float m_flSubmitFrameMs; + + float m_flWaitGetPosesCalledMs; float m_flNewPosesReadyMs; - float m_flNewFrameReadyMs; // second call to IVRCompositor::Submit + float m_flNewFrameReadyMs; float m_flCompositorUpdateStartMs; float m_flCompositorUpdateEndMs; float m_flCompositorRenderStartMs; - vr::TrackedDevicePose_t m_HmdPose; // pose used by app to render this frame + vr::TrackedDevicePose_t m_HmdPose; uint32_t m_nNumVSyncsReadyForUse; uint32_t m_nNumVSyncsToFirstView; @@ -2196,34 +2016,32 @@ struct Compositor_FrameTiming #pragma pack( pop ) #endif -/** Provides compositor benchmark results to the app */ struct Compositor_BenchmarkResults { - float m_flMegaPixelsPerSecond; // Measurement of GPU MP/s performed by compositor benchmark - float m_flHmdRecommendedMegaPixelsPerSecond; // Recommended default MP/s given the HMD resolution, refresh, and panel mask. + float m_flMegaPixelsPerSecond; + float m_flHmdRecommendedMegaPixelsPerSecond; }; -/** Frame timing data provided by direct mode drivers. */ struct DriverDirectMode_FrameTiming { - uint32_t m_nSize; // Set to sizeof( DriverDirectMode_FrameTiming ) - uint32_t m_nNumFramePresents; // number of times frame was presented - uint32_t m_nNumMisPresented; // number of times frame was presented on a vsync other than it was originally predicted to - uint32_t m_nNumDroppedFrames; // number of additional times previous frame was scanned out (i.e. compositor missed vsync) + uint32_t m_nSize; + uint32_t m_nNumFramePresents; + uint32_t m_nNumMisPresented; + uint32_t m_nNumDroppedFrames; uint32_t m_nReprojectionFlags; }; -/** These flags will be set on DriverDirectMode_FrameTiming::m_nReprojectionFlags when IVRDriverDirectModeComponent::GetFrameTiming is called for drivers to optionally respond to. */ -const uint32_t VRCompositor_ReprojectionMotion_Enabled = 0x100; // Motion Smoothing is enabled in the UI for the currently running application -const uint32_t VRCompositor_ReprojectionMotion_ForcedOn = 0x200; // Motion Smoothing is forced on in the UI for the currently running application -const uint32_t VRCompositor_ReprojectionMotion_AppThrottled = 0x400; // Application is requesting throttling via ForceInterleavedReprojectionOn + +const uint32_t VRCompositor_ReprojectionMotion_Enabled = 0x100; +const uint32_t VRCompositor_ReprojectionMotion_ForcedOn = 0x200; +const uint32_t VRCompositor_ReprojectionMotion_AppThrottled = 0x400; enum EVSync { VSync_None, - VSync_WaitRender, // block following render work until vsync - VSync_NoWaitRender, // do not block following render work (allow to get started early) + VSync_WaitRender, + VSync_NoWaitRender, }; enum EVRMuraCorrectionMode @@ -2232,7 +2050,6 @@ enum EVRMuraCorrectionMode EVRMuraCorrectionMode_NoCorrection }; -/** raw IMU data provided by IVRIOBuffer from paths to tracked devices with IMUs */ enum Imu_OffScaleFlags { OffScale_AccelX = 0x01, @@ -2253,23 +2070,22 @@ struct ImuSample_t enum class EVRDistortionChannel : uint32_t { - Red = 0, // given a coordinate in distorted panel space, returns the coordinate to sample in rectilinear render space for the red channel - Green, // given a coordinate in distorted panel space, returns the coordinate to sample in rectilinear render space for the green channel - Blue, // given a coordinate in distorted panel space, returns the coordinate to sample in rectilinear render space for the blue channel - InverseRed, // given a coordinate in rectilinear render space, returns the corresponding coordinate in distorted panel space for the red channel - InverseGreen, // given a coordinate in rectilinear render space, returns the corresponding coordinate in distorted panel space for the green channel - InverseBlue, // given a coordinate in rectilinear render space, returns the corresponding coordinate in distorted panel space for the blue channel + Red = 0, + Green, + Blue, + InverseRed, + InverseGreen, + InverseBlue, Count }; struct DistortionCoordinate_t { - float u, v; // 0..1 + float u, v; }; #pragma pack( pop ) -// figure out how to import from the VR API dll #if defined(_WIN32) #if !defined(OPENVR_BUILD_STATIC) @@ -2301,10 +2117,9 @@ struct DistortionCoordinate_t #define VR_CALLTYPE #endif -} // namespace vr -#endif // _INCLUDE_VRTYPES_H +} +#endif -// vrannotation.h #ifdef API_GEN # define VR_CLANG_ATTR(ATTR) __attribute__((annotate( ATTR ))) @@ -2324,7 +2139,6 @@ struct DistortionCoordinate_t #define VR_OUT_BUFFER_COUNT(COUNTER) VR_CLANG_ATTR( "out_buffer_count:" #COUNTER ";" ) #define VR_OUT_STRING_COUNT(COUNTER) VR_CLANG_ATTR( "out_string_count:" #COUNTER ";" ) -// vrtrackedcameratypes.h #ifndef _VRTRACKEDCAMERATYPES_H #define _VRTRACKEDCAMERATYPES_H @@ -2337,14 +2151,14 @@ namespace vr enum ECameraVideoStreamFormat { CVS_FORMAT_UNKNOWN = 0, - CVS_FORMAT_RAW10 = 1, // 10 bits per pixel - CVS_FORMAT_NV12 = 2, // 12 bits per pixel - CVS_FORMAT_RGB24 = 3, // 24 bits per pixel - CVS_FORMAT_NV12_2 = 4, // 12 bits per pixel, 2x height - CVS_FORMAT_YUYV16 = 5, // 16 bits per pixel - CVS_FORMAT_BAYER16BG = 6, // 16 bits per pixel, 10-bit BG-format Bayer, see https://docs.opencv.org/3.1.0/de/d25/imgproc_color_conversions.html - CVS_FORMAT_MJPEG = 7, // variable-sized MJPEG Open DML format, see https://www.loc.gov/preservation/digital/formats/fdd/fdd000063.shtml - CVS_FORMAT_RGBX32 = 8, // Full-sized pixels, 4BPP, LSB = RED + CVS_FORMAT_RAW10 = 1, + CVS_FORMAT_NV12 = 2, + CVS_FORMAT_RGB24 = 3, + CVS_FORMAT_NV12_2 = 4, + CVS_FORMAT_YUYV16 = 5, + CVS_FORMAT_BAYER16BG = 6, + CVS_FORMAT_MJPEG = 7, + CVS_FORMAT_RGBX32 = 8, CVS_MAX_FORMATS }; @@ -2380,7 +2194,7 @@ enum ECameraRoomViewStyle #ifdef _MSC_VER #define VR_CAMERA_DECL_ALIGN( x ) __declspec( align( x ) ) #else -#define VR_CAMERA_DECL_ALIGN( x ) // +#define VR_CAMERA_DECL_ALIGN( x ) #endif static const uint32_t k_unMaxCameras = 4; @@ -2393,16 +2207,16 @@ VR_CAMERA_DECL_ALIGN( 8 ) struct CameraVideoStreamFrame_t uint32_t m_nWidth; uint32_t m_nHeight; - uint32_t m_nImageDataSize; // Based on stream format, width, height + uint32_t m_nImageDataSize; - uint32_t m_nFrameSequence; // Starts from 0 when stream starts. + uint32_t m_nFrameSequence; - uint32_t m_nBufferIndex; // Identifies which buffer the image data is hosted - uint32_t m_nBufferCount; // Total number of configured buffers + uint32_t m_nBufferIndex; + uint32_t m_nBufferCount; uint32_t m_nExposureTime; - uint32_t m_nISPFrameTimeStamp; // Driver provided time stamp per driver centric time base + uint32_t m_nISPFrameTimeStamp; uint32_t m_nISPReferenceTimeStamp; uint32_t m_nSyncCounter; @@ -2411,17 +2225,17 @@ VR_CAMERA_DECL_ALIGN( 8 ) struct CameraVideoStreamFrame_t double m_flReferenceCamSyncTime; - double m_flFrameElapsedTime; // Starts from 0 when stream starts. In seconds. + double m_flFrameElapsedTime; double m_flFrameDeliveryRate; - double m_flFrameCaptureTime_DriverAbsolute; // In USB time, via AuxEvent - double m_flFrameCaptureTime_ServerRelative; // In System time within the server - uint64_t m_nFrameCaptureTicks_ServerAbsolute; // In system ticks within the server - double m_flFrameCaptureTime_ClientRelative; // At the client, relative to when the frame was exposed/captured. + double m_flFrameCaptureTime_DriverAbsolute; + double m_flFrameCaptureTime_ServerRelative; + uint64_t m_nFrameCaptureTicks_ServerAbsolute; + double m_flFrameCaptureTime_ClientRelative; double m_flSyncMarkerError; - TrackedDevicePose_t m_RawTrackedDevicePose; // Raw-and-uncalibrated pose, supplied by HMD layer when used as a tracked camera + TrackedDevicePose_t m_RawTrackedDevicePose; uint64_t m_pImageData; }; @@ -2430,9 +2244,8 @@ VR_CAMERA_DECL_ALIGN( 8 ) struct CameraVideoStreamFrame_t } -#endif // _VRTRACKEDCAMERATYPES_H +#endif -// ivrsettings.h #ifndef OPENVR_NO_STL #include <string> @@ -2447,11 +2260,10 @@ namespace vr VRSettingsError_WriteFailed = 2, VRSettingsError_ReadFailed = 3, VRSettingsError_JsonParseFailed = 4, - VRSettingsError_UnsetSettingHasNoDefault = 5, // This will be returned if the setting does not appear in the appropriate default file and has not been set + VRSettingsError_UnsetSettingHasNoDefault = 5, VRSettingsError_AccessDenied = 6, }; - // The maximum length of a settings key static const uint32_t k_unMaxSettingsKeyLength = 128; class IVRSettings @@ -2464,8 +2276,6 @@ namespace vr virtual void SetFloat( const char *pchSection, const char *pchSettingsKey, float flValue, EVRSettingsError *peError = nullptr ) = 0; virtual void SetString( const char *pchSection, const char *pchSettingsKey, const char *pchValue, EVRSettingsError *peError = nullptr ) = 0; - // Users of the system need to provide a proper default in default.vrsettings in the resources/settings/ directory - // of either the runtime or the driver_xxx directory. Otherwise the default will be false, 0, 0.0 or "" virtual bool GetBool( const char *pchSection, const char *pchSettingsKey, EVRSettingsError *peError = nullptr ) = 0; virtual int32_t GetInt32( const char *pchSection, const char *pchSettingsKey, EVRSettingsError *peError = nullptr ) = 0; virtual float GetFloat( const char *pchSection, const char *pchSettingsKey, EVRSettingsError *peError = nullptr ) = 0; @@ -2475,7 +2285,6 @@ namespace vr virtual void RemoveKeyInSection( const char *pchSection, const char *pchSettingsKey, EVRSettingsError *peError = nullptr ) = 0; }; - //----------------------------------------------------------------------------- static const char * const IVRSettings_Version = "IVRSettings_003"; class CVRSettingHelper @@ -2558,8 +2367,6 @@ namespace vr }; - //----------------------------------------------------------------------------- - // steamvr keys static const char * const k_pch_SteamVR_Section = "steamvr"; static const char * const k_pch_SteamVR_Contrast_Float = "contrast"; static const char * const k_pch_SteamVR_RequireHmd_String = "requireHmd"; @@ -2649,21 +2456,15 @@ namespace vr static const char * const k_pch_SteamVR_AllowFallbackMirrorWindowLinux_Bool = "allowFallbackMirrorWindowLinux"; static const char * const k_pch_SteamVR_DisableKeyboardPrivacy_Bool = "disableKeyboardPrivacy"; - //----------------------------------------------------------------------------- - // openxr keys static const char * const k_pch_OpenXR_Section = "openxr"; static const char * const k_pch_OpenXR_MetaUnityPluginCompatibility_Int32 = "metaUnityPluginCompatibility"; - //----------------------------------------------------------------------------- - // direct mode keys static const char * const k_pch_DirectMode_Section = "direct_mode"; static const char * const k_pch_DirectMode_Enable_Bool = "enable"; static const char * const k_pch_DirectMode_Count_Int32 = "count"; static const char * const k_pch_DirectMode_EdidVid_Int32 = "edidVid"; static const char * const k_pch_DirectMode_EdidPid_Int32 = "edidPid"; - //----------------------------------------------------------------------------- - // lighthouse keys static const char * const k_pch_Lighthouse_Section = "driver_lighthouse"; static const char * const k_pch_Lighthouse_DisableIMU_Bool = "disableimu"; static const char * const k_pch_Lighthouse_DisableIMUExceptHMD_Bool = "disableimuexcepthmd"; @@ -2677,8 +2478,6 @@ namespace vr static const char * const k_pch_Lighthouse_InactivityTimeoutForBaseStations_Int32 = "InactivityTimeoutForBaseStations"; static const char * const k_pch_Lighthouse_EnableImuFallback_Bool = "enableImuFallback"; - //----------------------------------------------------------------------------- - // null keys static const char * const k_pch_Null_Section = "driver_null"; static const char * const k_pch_Null_SerialNumber_String = "serialNumber"; static const char * const k_pch_Null_ModelNumber_String = "modelNumber"; @@ -2691,12 +2490,8 @@ namespace vr static const char * const k_pch_Null_SecondsFromVsyncToPhotons_Float = "secondsFromVsyncToPhotons"; static const char * const k_pch_Null_DisplayFrequency_Float = "displayFrequency"; - //----------------------------------------------------------------------------- - // Windows MR keys static const char * const k_pch_WindowsMR_Section = "driver_holographic"; - //----------------------------------------------------------------------------- - // user interface keys static const char * const k_pch_UserInterface_Section = "userinterface"; static const char * const k_pch_UserInterface_StatusAlwaysOnTop_Bool = "StatusAlwaysOnTop"; static const char * const k_pch_UserInterface_MinimizeToTray_Bool = "MinimizeToTray"; @@ -2706,13 +2501,9 @@ namespace vr static const char * const k_pch_UserInterface_CheckStatusInterval_Int = "vrmStatusCheckInterval"; static const char * const k_pch_UserInterface_CheckForSteam_Bool = "vrmCheckForSteam"; - //----------------------------------------------------------------------------- - // notification keys static const char * const k_pch_Notifications_Section = "notifications"; static const char * const k_pch_Notifications_DoNotDisturb_Bool = "DoNotDisturb"; - //----------------------------------------------------------------------------- - // keyboard keys static const char * const k_pch_Keyboard_Section = "keyboard"; static const char * const k_pch_Keyboard_TutorialCompletions = "TutorialCompletions"; static const char * const k_pch_Keyboard_ScaleX = "ScaleX"; @@ -2722,8 +2513,6 @@ namespace vr static const char * const k_pch_Keyboard_OffsetY = "OffsetY"; static const char * const k_pch_Keyboard_Smoothing = "Smoothing"; - //----------------------------------------------------------------------------- - // perf keys static const char * const k_pch_Perf_Section = "perfcheck"; static const char * const k_pch_Perf_PerfGraphInHMD_Bool = "perfGraphInHMD"; static const char * const k_pch_Perf_AllowTimingStore_Bool = "allowTimingStore"; @@ -2732,8 +2521,6 @@ namespace vr static const char * const k_pch_Perf_GPUProfiling_Bool = "GPUProfiling"; static const char * const k_pch_Perf_GpuBusMonitoring_Bool = "gpuBusMonitoring"; - //----------------------------------------------------------------------------- - // collision bounds keys static const char * const k_pch_CollisionBounds_Section = "collisionBounds"; static const char * const k_pch_CollisionBounds_Style_Int32 = "CollisionBoundsStyle"; static const char * const k_pch_CollisionBounds_GroundPerimeterOn_Bool = "CollisionBoundsGroundPerimeterOn"; @@ -2747,8 +2534,6 @@ namespace vr static const char * const k_pch_CollisionBounds_ColorGammaA_Int32 = "CollisionBoundsColorGammaA"; static const char * const k_pch_CollisionBounds_EnableDriverImport = "enableDriverBoundsImport"; - //----------------------------------------------------------------------------- - // camera keys static const char * const k_pch_Camera_Section = "camera"; static const char * const k_pch_Camera_EnableCamera_Bool = "enableCamera"; static const char * const k_pch_Camera_ShowOnController_Bool = "showOnController"; @@ -2761,8 +2546,6 @@ namespace vr static const char * const k_pch_Camera_BoundsStrength_Int32 = "cameraBoundsStrength"; static const char * const k_pch_Camera_RoomViewStyle_Int32 = "roomViewStyle"; - //----------------------------------------------------------------------------- - // audio keys static const char * const k_pch_audio_Section = "audio"; static const char * const k_pch_audio_SetOsDefaultPlaybackDevice_Bool = "setOsDefaultPlaybackDevice"; static const char * const k_pch_audio_EnablePlaybackDeviceOverride_Bool = "enablePlaybackDeviceOverride"; @@ -2783,8 +2566,6 @@ namespace vr static const char * const k_pch_audio_DualSpeakerAndJackOutput_Bool = "dualSpeakerAndJackOutput"; static const char * const k_pch_audio_MuteMicMonitor_Bool = "muteMicMonitor"; - //----------------------------------------------------------------------------- - // power management keys static const char * const k_pch_Power_Section = "power"; static const char * const k_pch_Power_PowerOffOnExit_Bool = "powerOffOnExit"; static const char * const k_pch_Power_TurnOffScreensTimeout_Float = "turnOffScreensTimeout"; @@ -2794,8 +2575,6 @@ namespace vr static const char * const k_pch_Power_PauseCompositorOnStandby_Bool = "pauseCompositorOnStandby"; static const char * const k_pch_Power_OverrideWindowsPowerScheme_Bool = "overrideWindowsPowerScheme"; - //----------------------------------------------------------------------------- - // dashboard keys static const char * const k_pch_Dashboard_Section = "dashboard"; static const char * const k_pch_Dashboard_EnableDashboard_Bool = "enableDashboard"; static const char * const k_pch_Dashboard_ArcadeMode_Bool = "arcadeMode"; @@ -2808,35 +2587,22 @@ namespace vr static const char * const k_pch_Dashboard_GrabHandleAcceleration = "grabHandleAcceleration"; static const char * const k_pch_Dashboard_OverlayBacksideColor_String = "overlayBacksideColor"; - //----------------------------------------------------------------------------- - // model skin keys static const char * const k_pch_modelskin_Section = "modelskins"; - //----------------------------------------------------------------------------- - // driver keys - These could be checked in any driver_<name> section static const char * const k_pch_Driver_Enable_Bool = "enable"; static const char * const k_pch_Driver_BlockedBySafemode_Bool = "blocked_by_safe_mode"; static const char * const k_pch_Driver_LoadPriority_Int32 = "loadPriority"; static const char * const k_pch_Driver_Hmd_AllowsClientToControlTextureIndex_Bool = "hmdAllowsClientToControlTextureIndex"; static const char * const k_pch_Driver_ForceSystemLayerUseAppPoses_Bool = "forceSystemLayerUseAppPoses"; - //----------------------------------------------------------------------------- - // web interface keys static const char* const k_pch_WebInterface_Section = "WebInterface"; - //----------------------------------------------------------------------------- - // vrwebhelper keys static const char* const k_pch_VRWebHelper_Section = "VRWebHelper"; static const char* const k_pch_VRWebHelper_DebuggerEnabled_Bool = "DebuggerEnabled"; static const char* const k_pch_VRWebHelper_DebuggerPort_Int32 = "DebuggerPort"; - //----------------------------------------------------------------------------- - // tracking overrides - keys are device paths, values are the device paths their - // tracking/pose information overrides static const char* const k_pch_TrackingOverride_Section = "TrackingOverrides"; - //----------------------------------------------------------------------------- - // per-app keys - the section name for these is the app key itself. Some of these are prefixed by the controller type static const char* const k_pch_App_BindingAutosaveURLSuffix_String = "AutosaveURL"; static const char* const k_pch_App_BindingLegacyAPISuffix_String = "_legacy"; static const char *const k_pch_App_BindingSteamVRInputAPISuffix_String = "_steamvrinput"; @@ -2847,45 +2613,30 @@ namespace vr static const char* const k_pch_App_DominantHand_Int32 = "DominantHand"; static const char* const k_pch_App_BlockOculusSDK_Bool = "blockOculusSDK"; - //----------------------------------------------------------------------------- - // configuration for trackers static const char * const k_pch_Trackers_Section = "trackers"; - //----------------------------------------------------------------------------- - // configuration for desktop UI windows static const char * const k_pch_DesktopUI_Section = "DesktopUI"; - //----------------------------------------------------------------------------- - // Last known keys for righting recovery static const char * const k_pch_LastKnown_Section = "LastKnown"; static const char* const k_pch_LastKnown_HMDManufacturer_String = "HMDManufacturer"; static const char *const k_pch_LastKnown_HMDModel_String = "HMDModel"; static const char* const k_pch_LastKnown_ActualHMDDriver_String = "ActualHMDDriver"; static const char* const k_pch_LastKnown_HMDSerialNumber_String = "HMDSerialNumber"; - static const char* const k_pch_LastKnown_HMDRemoteClientID_String = "RemoteClientID"; // uint64 in string + static const char* const k_pch_LastKnown_HMDRemoteClientID_String = "RemoteClientID"; - //----------------------------------------------------------------------------- - // Dismissed warnings static const char * const k_pch_DismissedWarnings_Section = "DismissedWarnings"; - //----------------------------------------------------------------------------- - // Input Settings static const char * const k_pch_Input_Section = "input"; static const char* const k_pch_Input_LeftThumbstickRotation_Float = "leftThumbstickRotation"; static const char* const k_pch_Input_RightThumbstickRotation_Float = "rightThumbstickRotation"; static const char* const k_pch_Input_ThumbstickDeadzone_Float = "thumbstickDeadzone"; - //----------------------------------------------------------------------------- - // Log of GPU performance static const char * const k_pch_GpuSpeed_Section = "GpuSpeed"; - //----------------------------------------------------------------------------- - // OpenXR Render Model Extension keys static const char *const k_pch_XRRenderModelCache_Section = "XRRenderModelUuidCache"; -} // namespace vr +} -// iservertrackeddevicedriver.h namespace vr { @@ -2898,60 +2649,24 @@ struct DriverPoseQuaternion_t struct DriverPose_t { - /* Time offset of this pose, in seconds from the actual time of the pose, - * relative to the time of the PoseUpdated() call made by the driver. - */ double poseTimeOffset; - /* Generally, the pose maintained by a driver - * is in an inertial coordinate system different - * from the world system of x+ right, y+ up, z+ back. - * Also, the driver is not usually tracking the "head" position, - * but instead an internal IMU or another reference point in the HMD. - * The following two transforms transform positions and orientations - * to app world space from driver world space, - * and to HMD head space from driver local body space. - * - * We maintain the driver pose state in its internal coordinate system, - * so we can do the pose prediction math without having to - * use angular acceleration. A driver's angular acceleration is generally not measured, - * and is instead calculated from successive samples of angular velocity. - * This leads to a noisy angular acceleration values, which are also - * lagged due to the filtering required to reduce noise to an acceptable level. - */ vr::HmdQuaternion_t qWorldFromDriverRotation; double vecWorldFromDriverTranslation[ 3 ]; vr::HmdQuaternion_t qDriverFromHeadRotation; double vecDriverFromHeadTranslation[ 3 ]; - /* State of driver pose, in meters and radians. */ - /* Position of the driver tracking reference in driver world space - * +[0] (x) is right - * +[1] (y) is up - * -[2] (z) is forward - */ double vecPosition[ 3 ]; - /* Velocity of the pose in meters/second */ double vecVelocity[ 3 ]; - /* Acceleration of the pose in meters/second */ double vecAcceleration[ 3 ]; - /* Orientation of the tracker, represented as a quaternion */ vr::HmdQuaternion_t qRotation; - /* Angular velocity of the pose in axis-angle - * representation. The direction is the angle of - * rotation and the magnitude is the angle around - * that axis in radians/second. */ double vecAngularVelocity[ 3 ]; - /* Angular acceleration of the pose in axis-angle - * representation. The direction is the angle of - * rotation and the magnitude is the angle around - * that axis in radians/second^2. */ double vecAngularAcceleration[ 3 ]; ETrackingResult result; @@ -2963,41 +2678,20 @@ struct DriverPose_t }; -// ---------------------------------------------------------------------------------------------- -// Purpose: Represents a single tracked device in a driver -// ---------------------------------------------------------------------------------------------- class ITrackedDeviceServerDriver { public: - // ------------------------------------ - // Management Methods - // ------------------------------------ - /** This is called before an HMD is returned to the application. It will always be - * called before any display or tracking methods. Memory and processor use by the - * ITrackedDeviceServerDriver object should be kept to a minimum until it is activated. - * The pose listener is guaranteed to be valid until Deactivate is called, but - * should not be used after that point. */ virtual EVRInitError Activate( uint32_t unObjectId ) = 0; - /** This is called when The VR system is switching from this Hmd being the active display - * to another Hmd being the active display. The driver should clean whatever memory - * and thread use it can when it is deactivated */ virtual void Deactivate() = 0; - /** Handles a request from the system to put this device into standby mode. What that means is defined per-device. */ virtual void EnterStandby() = 0; - /** Requests a component interface of the driver for device-specific functionality. The driver should return NULL - * if the requested interface or version is not supported. */ virtual void *GetComponent( const char *pchComponentNameAndVersion ) = 0; - /** A VR Client has made this debug request of the driver. The set of valid requests is entirely - * up to the driver and the client to figure out, as is the format of the response. Responses that - * exceed the length of the supplied buffer should be truncated and null terminated */ virtual void DebugRequest( const char *pchRequest, char *pchResponseBuffer, uint32_t unResponseBufferSize ) = 0; - /** This interface is unused, and will never be called. */ virtual DriverPose_t GetPose() = 0; }; @@ -3007,48 +2701,30 @@ static const char *ITrackedDeviceServerDriver_Version = "ITrackedDeviceServerDri } -// ivrdisplaycomponent.h namespace vr { - // ---------------------------------------------------------------------------------------------- - // Purpose: The display component on a single tracked device - // ---------------------------------------------------------------------------------------------- class IVRDisplayComponent { public: - // ------------------------------------ - // Display Methods - // ------------------------------------ - /** Size and position that the window needs to be on the VR display. */ virtual void GetWindowBounds( int32_t *pnX, int32_t *pnY, uint32_t *pnWidth, uint32_t *pnHeight ) = 0; - /** Returns true if the display is extending the desktop. */ virtual bool IsDisplayOnDesktop( ) = 0; - /** Returns true if the display is real and not a fictional display. */ virtual bool IsDisplayRealDisplay( ) = 0; - /** Suggested size for the intermediate render target that the distortion pulls from. */ virtual void GetRecommendedRenderTargetSize( uint32_t *pnWidth, uint32_t *pnHeight ) = 0; - /** Gets the viewport in the frame buffer to draw the output of the distortion into */ virtual void GetEyeOutputViewport( EVREye eEye, uint32_t *pnX, uint32_t *pnY, uint32_t *pnWidth, uint32_t *pnHeight ) = 0; - /** The components necessary to build your own projection matrix in case your - * application is doing something fancy like infinite Z */ virtual void GetProjectionRaw( EVREye eEye, float *pfLeft, float *pfRight, float *pfTop, float *pfBottom ) = 0; - /** Returns the result of the distortion function for the specified eye and input UVs. UVs go from 0,0 in - * the upper left of that eye's viewport and 1,1 in the lower right of that eye's viewport. */ virtual DistortionCoordinates_t ComputeDistortion( EVREye eEye, float fU, float fV ) = 0; - /** Computes the result of the inverse distortion function for the specified eye, channel, and input UV. - * Operation can fail, returns success/failure, on success result is stored in *pResult */ virtual bool ComputeInverseDistortion( HmdVector2_t *pResult, EVREye eEye, uint32_t unChannel, float fU, float fV ) = 0; }; @@ -3056,27 +2732,18 @@ namespace vr } -// ivrdriverdirectmodecomponent.h namespace vr { enum VRSwapTextureFlag { - // Specify that the shared texture resource was created with the SHARED_NTHANDLE option (Windows) VRSwapTextureFlag_Shared_NTHandle = 1 << 0, }; - // ---------------------------------------------------------------------------------------------- - // Purpose: This component is used for drivers that implement direct mode entirely on their own - // without allowing the VR Compositor to own the window/device. - // ---------------------------------------------------------------------------------------------- class IVRDriverDirectModeComponent { public: - // ----------------------------------- - // Direct mode methods - // ----------------------------------- struct SwapTextureSetDesc_t { @@ -3092,44 +2759,30 @@ namespace vr uint32_t unTextureFlags; }; - /** Called to allocate textures for applications to render into. One of these per eye will be passed back to SubmitLayer each frame. */ virtual void CreateSwapTextureSet( uint32_t unPid, const SwapTextureSetDesc_t *pSwapTextureSetDesc, SwapTextureSet_t *pOutSwapTextureSet ) {} - /** Used to textures created using CreateSwapTextureSet. Only one of the set's handles needs to be used to destroy the entire set. */ virtual void DestroySwapTextureSet( vr::SharedTextureHandle_t sharedTextureHandle ) {} - /** Used to purge all texture sets for a given process. */ virtual void DestroyAllSwapTextureSets( uint32_t unPid ) {} - /** After Present returns, calls this to get the next index to use for rendering. */ virtual void GetNextSwapTextureSetIndex( vr::SharedTextureHandle_t sharedTextureHandles[ 2 ], uint32_t( *pIndices )[ 2 ] ) {} - /** Call once per layer to draw for this frame. One shared texture handle per eye. Textures must be created - * using CreateSwapTextureSet and should be alternated per frame. Call Present once all layers have been submitted. */ struct SubmitLayerPerEye_t { - // Shared texture handles (depth not always provided). vr::SharedTextureHandle_t hTexture, hDepthTexture; - // Valid region of provided texture (and depth). vr::VRTextureBounds_t bounds; - // Projection matrix used to render the depth buffer. vr::HmdMatrix44_t mProjection; - // Hmd pose used to render this layer. vr::HmdMatrix34_t mHmdPose; - // Time in seconds from now that mHmdPose was predicted to. float flHmdPosePredictionTimeInSecondsFromNow; }; virtual void SubmitLayer( const SubmitLayerPerEye_t( &perEye )[ 2 ] ) {} - /** Submits queued layers for display. */ virtual void Present( vr::SharedTextureHandle_t syncTexture ) {} - /** Called after Present to allow driver to take more time until vsync after they've successfully acquired the sync texture in Present. - * Set Prop_Hmd_SupportsAppThrottling_Bool to enable throttling / prediction UI in per-app video settings. */ struct Throttling_t { uint32_t nFramesToThrottle; @@ -3137,11 +2790,8 @@ namespace vr }; virtual void PostPresent( const Throttling_t *pThrottling ) {} - /** Called to get additional frame timing stats from driver. Check m_nSize for versioning (new members will be added to end only). */ virtual void GetFrameTiming( DriverDirectMode_FrameTiming *pFrameTiming ) { - /** VRCompositor_ReprojectionMotion_XXX flags get passed in, and since these overlap with VRCompositor_ThrottleMask, they need - * to be cleared out if this function isn't implemented; otherwise, those settings will get interpreted as throttling. */ pFrameTiming->m_nReprojectionFlags = 0; } }; @@ -3150,27 +2800,18 @@ namespace vr } -// ivrcameracomponent.h namespace vr { - //----------------------------------------------------------------------------- - //----------------------------------------------------------------------------- class ICameraVideoSinkCallback { public: virtual void OnCameraVideoSinkCallback() = 0; }; - // ---------------------------------------------------------------------------------------------- - // Purpose: The camera on a single tracked device - // ---------------------------------------------------------------------------------------------- class IVRCameraComponent { public: - // ------------------------------------ - // Camera Methods - // ------------------------------------ virtual bool GetCameraFrameDimensions( vr::ECameraVideoStreamFormat nVideoStreamFormat, uint32_t *pWidth, uint32_t *pHeight ) = 0; virtual bool GetCameraFrameBufferingRequirements( int *pDefaultFrameQueueSize, uint32_t *pFrameBufferDataSize ) = 0; virtual bool SetCameraFrameBuffering( int nFrameBufferCount, void **ppFrameBuffers, uint32_t nFrameBufferDataSize ) = 0; @@ -3197,7 +2838,6 @@ namespace vr static const char *IVRCameraComponent_Version = "IVRCameraComponent_003"; } -// itrackeddevicedriverprovider.h namespace vr { @@ -3206,58 +2846,32 @@ class ITrackedDeviceServerDriver; struct TrackedDeviceDriverInfo_t; struct DriverPose_t; -/** This interface is provided by vrserver to allow the driver to notify -* the system when something changes about a device. These changes must -* not change the serial number or class of the device because those values -* are permanently associated with the device's index. */ class IVRDriverContext { public: - /** Returns the requested interface. If the interface was not available it will return NULL and fill - * out the error. */ virtual void *GetGenericInterface( const char *pchInterfaceVersion, EVRInitError *peError = nullptr ) = 0; - /** Returns the property container handle for this driver */ virtual DriverHandle_t GetDriverHandle() = 0; }; -/** This interface must be implemented in each driver. It will be loaded in vrserver.exe */ class IServerTrackedDeviceProvider { public: - /** initializes the driver. This will be called before any other methods are called. - * If Init returns anything other than VRInitError_None the driver DLL will be unloaded. - * - * pDriverHost will never be NULL, and will always be a pointer to a IServerDriverHost interface - * - * pchUserDriverConfigDir - The absolute path of the directory where the driver should store user - * config files. - * pchDriverInstallDir - The absolute path of the root directory for the driver. - */ virtual EVRInitError Init( IVRDriverContext *pDriverContext ) = 0; - /** cleans up the driver right before it is unloaded */ virtual void Cleanup() = 0; - /** Returns the version of the ITrackedDeviceServerDriver interface used by this driver */ virtual const char * const *GetInterfaceVersions() = 0; - /** Allows the driver do to some work in the main loop of the server. */ virtual void RunFrame() = 0; - // ------------ Power State Functions ----------------------- // - /** Returns true if the driver wants to block Standby mode. */ virtual bool ShouldBlockStandbyMode() = 0; - /** Called when the system is entering Standby mode. The driver should switch itself into whatever sort of low-power - * state it has. */ virtual void EnterStandby() = 0; - /** Called when the system is leaving Standby mode. The driver should switch itself back to - full operation. */ virtual void LeaveStandby() = 0; }; @@ -3268,14 +2882,11 @@ static const char *IServerTrackedDeviceProvider_Version = "IServerTrackedDeviceP -/** This interface must be implemented in each driver. It will be loaded in vrclient.dll */ class IVRWatchdogProvider { public: - /** initializes the driver in watchdog mode. */ virtual EVRInitError Init( IVRDriverContext *pDriverContext ) = 0; - /** cleans up the driver right before it is unloaded */ virtual void Cleanup() = 0; }; @@ -3284,21 +2895,15 @@ static const char *IVRWatchdogProvider_Version = "IVRWatchdogProvider_001"; -/** This is an optional interface drivers may implement. It will be loaded in vrcompositor.exe */ class IVRCompositorPluginProvider { public: - /** initializes the driver when used to load compositor plugins */ virtual EVRInitError Init( IVRDriverContext *pDriverContext ) = 0; - /** cleans up the driver right before it is unloaded */ virtual void Cleanup() = 0; - /** Returns the versions of interfaces used by this driver */ virtual const char * const *GetInterfaceVersions() = 0; - /** Requests a component interface of the driver for specific functionality. The driver should return NULL - * if the requested interface or version is not supported. */ virtual void *GetComponent( const char *pchComponentNameAndVersion ) = 0; }; @@ -3306,7 +2911,6 @@ static const char *IVRCompositorPluginProvider_Version = "IVRCompositorPluginPro } -// ivrproperties.h #include <string> #include <vector> @@ -3314,9 +2918,6 @@ static const char *IVRCompositorPluginProvider_Version = "IVRCompositorPluginPro namespace vr { - /** This container is automatically created before a display redirect device is activated. - * Any properties in this container will be returned when that property is read from the HMD's - * property container. */ static const PropertyContainerHandle_t k_ulDisplayRedirectContainer = 0x600000003; enum EPropertyWriteType @@ -3352,17 +2953,12 @@ class IVRProperties { public: - /** Reads a set of properties atomically. See the PropertyReadBatch_t struct for more information. */ virtual ETrackedPropertyError ReadPropertyBatch( PropertyContainerHandle_t ulContainerHandle, PropertyRead_t *pBatch, uint32_t unBatchEntryCount ) = 0; - /** Writes a set of properties atomically. See the PropertyWriteBatch_t struct for more information. */ virtual ETrackedPropertyError WritePropertyBatch( PropertyContainerHandle_t ulContainerHandle, PropertyWrite_t *pBatch, uint32_t unBatchEntryCount ) = 0; - /** returns a string that corresponds with the specified property error. The string will be the name - * of the error enum value for all valid error codes */ virtual const char *GetPropErrorNameFromEnum( ETrackedPropertyError error ) = 0; - /** Returns a container handle given a tracked device index */ virtual PropertyContainerHandle_t TrackedDeviceToPropertyContainer( TrackedDeviceIndex_t nDevice ) = 0; }; @@ -3373,12 +2969,9 @@ class CVRPropertyHelpers public: CVRPropertyHelpers( IVRProperties * pProperties ) : m_pProperties( pProperties ) {} - /** Gets the typed property according to the caller's expected return type. */ template <typename T> T GetTypedProperty( vr::PropertyContainerHandle_t ulContainer, vr::ETrackedDeviceProperty prop, vr::ETrackedPropertyError *peError ); - /** Returns a scaler property. If the device index is not valid or the property value type does not match, - * this function will return false. */ bool GetBoolProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError *pError = 0L ); float GetFloatProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError *pError = 0L ); int32_t GetInt32Property( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError *pError = 0L ); @@ -3388,25 +2981,15 @@ public: HmdVector4_t GetVec4Property( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError *pError = 0L ); double GetDoubleProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError *pError = 0L ); - /** Returns a single typed property. If the device index is not valid or the property is not a string type this function will - * return 0. Otherwise it returns the length of the number of bytes necessary to hold this string including the trailing - * null. Strings will always fit in buffers of k_unMaxPropertyStringSize characters. */ uint32_t GetProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, VR_OUT_STRING() void *pvBuffer, uint32_t unBufferSize, PropertyTypeTag_t *punTag, ETrackedPropertyError *pError = 0L ); - /** Returns a string property. If the device index is not valid or the property is not a string type this function will - * return 0. Otherwise it returns the length of the number of bytes necessary to hold this string including the trailing - * null. Strings will always fit in buffers of k_unMaxPropertyStringSize characters. */ uint32_t GetStringProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, VR_OUT_STRING() char *pchValue, uint32_t unBufferSize, ETrackedPropertyError *pError = 0L ); - /** Returns a string property as a std::string. If the device index is not valid or the property is not a string type this function will - * return an empty string. */ std::string GetStringProperty( vr::PropertyContainerHandle_t ulContainer, vr::ETrackedDeviceProperty prop, vr::ETrackedPropertyError *peError = nullptr ); - /** Reads a std::vector of data from a property. */ template< typename T> ETrackedPropertyError GetPropertyVector( PropertyContainerHandle_t ulContainer, ETrackedDeviceProperty prop, PropertyTypeTag_t unExpectedTag, std::vector<T> *pvecResults ); - /** Sets a scaler property. The new value will be returned on any subsequent call to get this property in any process. */ ETrackedPropertyError SetBoolProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, bool bNewValue ); ETrackedPropertyError SetFloatProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, float fNewValue ); ETrackedPropertyError SetInt32Property( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, int32_t nNewValue ); @@ -3416,26 +2999,19 @@ public: ETrackedPropertyError SetVec4Property( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, const HmdVector4_t & vNewValue ); ETrackedPropertyError SetDoubleProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, double vNewValue ); - /** Sets a string property. The new value will be returned on any subsequent call to get this property in any process. */ ETrackedPropertyError SetStringProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, const char *pchNewValue ); - /** Sets a single typed property. The new value will be returned on any subsequent call to get this property in any process. */ ETrackedPropertyError SetProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, void *pvNewValue, uint32_t unNewValueSize, PropertyTypeTag_t unTag ); - /** Sets the error return value for a property. This value will be returned on all subsequent requests to get the property */ ETrackedPropertyError SetPropertyError( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError eError ); - /** Clears any value or error set for the property. */ ETrackedPropertyError EraseProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop ); - /* Turns a device index into a property container handle. */ PropertyContainerHandle_t TrackedDeviceToPropertyContainer( TrackedDeviceIndex_t nDevice ) { return m_pProperties->TrackedDeviceToPropertyContainer( nDevice ); } - /** Sets a std::vector of typed data to a property. */ template< typename T> ETrackedPropertyError SetPropertyVector( PropertyContainerHandle_t ulContainer, ETrackedDeviceProperty prop, PropertyTypeTag_t unExpectedTag, std::vector<T> *vecProperties ); - /** Returns true if the specified property is set on the specified container */ bool IsPropertySet( PropertyContainerHandle_t ulContainer, ETrackedDeviceProperty prop, ETrackedPropertyError *peError = nullptr ); private: template<typename T> @@ -3445,8 +3021,6 @@ private: }; -/** Returns a string property as a std::string. If the device index is not valid or the property is not a string type this function will -* return an empty string. */ template <> inline std::string CVRPropertyHelpers::GetTypedProperty( vr::PropertyContainerHandle_t ulContainer, vr::ETrackedDeviceProperty prop, vr::ETrackedPropertyError *peError ) { @@ -3533,7 +3107,6 @@ inline uint32_t CVRPropertyHelpers::GetProperty( PropertyContainerHandle_t ulCon } -/** Sets a single typed property. The new value will be returned on any subsequent call to get this property in any process. */ inline ETrackedPropertyError CVRPropertyHelpers::SetProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, void *pvNewValue, uint32_t unNewValueSize, PropertyTypeTag_t unTag ) { PropertyWrite_t batch; @@ -3549,9 +3122,6 @@ inline ETrackedPropertyError CVRPropertyHelpers::SetProperty( PropertyContainerH } -/** Returns a string property. If the device index is not valid or the property is not a string type this function will -* return 0. Otherwise it returns the length of the number of bytes necessary to hold this string including the trailing -* null. Strings will always fit in buffers of k_unMaxPropertyStringSize characters. */ inline uint32_t CVRPropertyHelpers::GetStringProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, VR_OUT_STRING() char *pchValue, uint32_t unBufferSize, ETrackedPropertyError *pError ) { PropertyTypeTag_t unTag; @@ -3579,8 +3149,6 @@ inline uint32_t CVRPropertyHelpers::GetStringProperty( PropertyContainerHandle_t } -/** Returns a string property as a std::string. If the device index is not valid or the property is not a string type this function will -* return an empty string. */ inline std::string CVRPropertyHelpers::GetStringProperty( vr::PropertyContainerHandle_t ulContainer, vr::ETrackedDeviceProperty prop, vr::ETrackedPropertyError *peError ) { char buf[1024]; @@ -3610,13 +3178,11 @@ inline std::string CVRPropertyHelpers::GetStringProperty( vr::PropertyContainerH } -/** Sets a string property. The new value will be returned on any subsequent call to get this property in any process. */ inline ETrackedPropertyError CVRPropertyHelpers::SetStringProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, const char *pchNewValue ) { if ( !pchNewValue ) return TrackedProp_InvalidOperation; - // this is strlen without the dependency on string.h const char *pchCurr = pchNewValue; while ( *pchCurr ) { @@ -3735,7 +3301,6 @@ inline ETrackedPropertyError CVRPropertyHelpers::SetVec4Property( PropertyContai return SetProperty( ulContainerHandle, prop, ( void * ) &vNewValue, sizeof( HmdVector4_t ), k_unHmdVector4PropertyTag ); } -/** Sets the error return value for a property. This value will be returned on all subsequent requests to get the property */ inline ETrackedPropertyError CVRPropertyHelpers::SetPropertyError( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError eError ) { PropertyWrite_t batch; @@ -3748,7 +3313,6 @@ inline ETrackedPropertyError CVRPropertyHelpers::SetPropertyError( PropertyConta return batch.eError; } -/** Clears any value or error set for the property. */ inline ETrackedPropertyError CVRPropertyHelpers::EraseProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop ) { PropertyWrite_t batch; @@ -3821,7 +3385,6 @@ inline bool CVRPropertyHelpers::IsPropertySet( PropertyContainerHandle_t ulConta -// ivrdriverinput.h namespace vr { @@ -3834,53 +3397,41 @@ namespace vr enum EVRScalarUnits { - VRScalarUnits_NormalizedOneSided = 0, // Value ranges from 0 to 1 - VRScalarUnits_NormalizedTwoSided = 1, // Value ranges from -1 to 1 + VRScalarUnits_NormalizedOneSided = 0, + VRScalarUnits_NormalizedTwoSided = 1, }; class IVRDriverInput { public: - /** Creates a boolean input component for the device */ virtual EVRInputError CreateBooleanComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle ) = 0; - /** Updates a boolean component */ virtual EVRInputError UpdateBooleanComponent( VRInputComponentHandle_t ulComponent, bool bNewValue, double fTimeOffset ) = 0; - /** Creates a scalar input component for the device */ virtual EVRInputError CreateScalarComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle, EVRScalarType eType, EVRScalarUnits eUnits ) = 0; - /** Updates a boolean component */ virtual EVRInputError UpdateScalarComponent( VRInputComponentHandle_t ulComponent, float fNewValue, double fTimeOffset ) = 0; - /** Creates a haptic component for the device */ virtual EVRInputError CreateHapticComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle ) = 0; - /** Creates a skeleton component. */ virtual EVRInputError CreateSkeletonComponent( PropertyContainerHandle_t ulContainer, const char *pchName, const char *pchSkeletonPath, const char *pchBasePosePath, EVRSkeletalTrackingLevel eSkeletalTrackingLevel, const VRBoneTransform_t *pGripLimitTransforms, uint32_t unGripLimitTransformCount, VRInputComponentHandle_t *pHandle ) = 0; - /** Updates a skeleton component. */ virtual EVRInputError UpdateSkeletonComponent( VRInputComponentHandle_t ulComponent, EVRSkeletalMotionRange eMotionRange, const VRBoneTransform_t *pTransforms, uint32_t unTransformCount ) = 0; - /** Creates a pose component */ virtual EVRInputError CreatePoseComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle ) = 0; - /** Updates a pose component. */ virtual EVRInputError UpdatePoseComponent( VRInputComponentHandle_t ulComponent, const HmdMatrix34_t *pMatPoseOffset, double fTimeOffset ) = 0; - /** Creates an eye tracking component **/ virtual EVRInputError CreateEyeTrackingComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle ) = 0; - /** Updates an eye tracking component. */ virtual EVRInputError UpdateEyeTrackingComponent( VRInputComponentHandle_t ulComponent, const VREyeTrackingData_t *pEyeTrackingData, double fTimeOffset ) = 0; }; static const char * const IVRDriverInput_Version = "IVRDriverInput_004"; -} // namespace vr +} -// ivrdriverlog.h namespace vr { @@ -3888,7 +3439,6 @@ namespace vr class IVRDriverLog { public: - /** Writes a log message to the log file prefixed with the driver name */ virtual void Log( const char *pchLogMessage ) = 0; }; @@ -3897,7 +3447,6 @@ static const char *IVRDriverLog_Version = "IVRDriverLog_001"; } -// ivrserverdriverhost.h namespace vr { @@ -3906,58 +3455,31 @@ class ITrackedDeviceServerDriver; struct TrackedDeviceDriverInfo_t; struct DriverPose_t; -/** This interface is provided by vrserver to allow the driver to notify -* the system when something changes about a device. These changes must -* not change the serial number or class of the device because those values -* are permanently associated with the device's index. */ class IVRServerDriverHost { public: - /** Notifies the server that a tracked device has been added. If this function returns true - * the server will call Activate on the device. If it returns false some kind of error - * has occurred and the device will not be activated. */ virtual bool TrackedDeviceAdded( const char *pchDeviceSerialNumber, ETrackedDeviceClass eDeviceClass, ITrackedDeviceServerDriver *pDriver ) = 0; - /** Notifies the server that a tracked device's pose has been updated */ virtual void TrackedDevicePoseUpdated( uint32_t unWhichDevice, const DriverPose_t & newPose, uint32_t unPoseStructSize ) = 0; - /** Notifies the server that vsync has occurred on the the display attached to the device. This is - * only permitted on devices of the HMD class. */ virtual void VsyncEvent( double vsyncTimeOffsetSeconds ) = 0; - /** Sends a vendor specific event (VREvent_VendorSpecific_Reserved_Start..VREvent_VendorSpecific_Reserved_End */ virtual void VendorSpecificEvent( uint32_t unWhichDevice, vr::EVREventType eventType, const VREvent_Data_t & eventData, double eventTimeOffset ) = 0; - /** Returns true if SteamVR is exiting */ virtual bool IsExiting() = 0; - /** Returns true and fills the event with the next event on the queue if there is one. If there are no events - * this method returns false. uncbVREvent should be the size in bytes of the VREvent_t struct */ virtual bool PollNextEvent( VREvent_t *pEvent, uint32_t uncbVREvent ) = 0; - /** Provides access to device poses for drivers. Poses are in their "raw" tracking space which is uniquely - * defined by each driver providing poses for its devices. It is up to clients of this function to correlate - * poses across different drivers. Poses are indexed by their device id, and their associated driver and - * other properties can be looked up via IVRProperties. */ virtual void GetRawTrackedDevicePoses( float fPredictedSecondsFromNow, TrackedDevicePose_t *pTrackedDevicePoseArray, uint32_t unTrackedDevicePoseArrayCount ) = 0; - /** Requests that SteamVR be restarted. The provided reason will be displayed to the user and should be in the current locale. */ virtual void RequestRestart( const char *pchLocalizedReason, const char *pchExecutableToStart, const char *pchArguments, const char *pchWorkingDirectory ) = 0; - /** Interface for copying a range of timing data. Frames are returned in ascending order (oldest to newest) with the last being the most recent frame. - * Only the first entry's m_nSize needs to be set, as the rest will be inferred from that. Returns total number of entries filled out. */ virtual uint32_t GetFrameTimings( Compositor_FrameTiming *pTiming, uint32_t nFrames ) = 0; - /** Notifies the server that a tracked device's display component transforms have been updated. - * only permitted on devices of the HMD class. */ virtual void SetDisplayEyeToHead( uint32_t unWhichDevice, const HmdMatrix34_t & eyeToHeadLeft, const HmdMatrix34_t & eyeToHeadRight ) = 0; - /** Notifies the server that a tracked device's display projection has changed. - * only permitted on devices of the HMD class. */ virtual void SetDisplayProjectionRaw( uint32_t unWhichDevice, const HmdRect2_t & eyeLeft, const HmdRect2_t & eyeRight ) = 0; - /** Notifies the server that a tracked device's recommended render target resolution has changed. - * only permitted on devices of the HMD class. */ virtual void SetRecommendedRenderTargetSize( uint32_t unWhichDevice, uint32_t nWidth, uint32_t nHeight ) = 0; }; @@ -3965,7 +3487,6 @@ static const char *IVRServerDriverHost_Version = "IVRServerDriverHost_006"; } -// ivrcompositordriverhost.h namespace vr { @@ -3973,8 +3494,6 @@ namespace vr class IVRCompositorDriverHost { public: - /** Returns true and fills the event with the next event on the queue if there is one. If there are no events - * this method returns false. uncbVREvent should be the size in bytes of the VREvent_t struct */ virtual bool PollNextEvent( VREvent_t *pEvent, uint32_t uncbVREvent ) = 0; }; @@ -3982,7 +3501,6 @@ static const char *IVRCompositorDriverHost_Version = "IVRCompositorDriverHost_00 } -// ivrhiddenarea.h namespace vr { @@ -3992,10 +3510,8 @@ class CVRHiddenAreaHelpers public: CVRHiddenAreaHelpers( IVRProperties *pProperties ) : m_pProperties( pProperties ) {} - /** Stores a hidden area mesh in a property */ ETrackedPropertyError SetHiddenArea( EVREye eEye, EHiddenAreaMeshType type, HmdVector2_t *pVerts, uint32_t unVertCount ); - /** retrieves a hidden area mesh from a property. Returns the vert count read out of the property. */ uint32_t GetHiddenArea( EVREye eEye, EHiddenAreaMeshType type, HmdVector2_t *pVerts, uint32_t unVertCount, ETrackedPropertyError *peError ); private: @@ -4039,17 +3555,13 @@ inline uint32_t CVRHiddenAreaHelpers::GetHiddenArea( EVREye eEye, EHiddenAreaMes } -// ivrwatchdoghost.h namespace vr { -/** This interface is provided by vrclient to allow the driver to make everything wake up */ class IVRWatchdogHost { public: - /** Client drivers in watchdog mode should call this when they have received a signal from hardware that should - * cause SteamVR to start */ virtual void WatchdogWakeUp( vr::ETrackedDeviceClass eDeviceClass ) = 0; }; @@ -4059,7 +3571,6 @@ static const char *IVRWatchdogHost_Version = "IVRWatchdogHost_002"; -// ivrvirtualdisplay.h namespace vr { @@ -4071,20 +3582,14 @@ namespace vr double flVSyncTimeInSeconds; }; - // ---------------------------------------------------------------------------------------------- - // Purpose: This component is used for drivers that implement a virtual display (e.g. wireless). - // ---------------------------------------------------------------------------------------------- class IVRVirtualDisplay { public: - /** Submits final backbuffer for display. */ virtual void Present( const PresentInfo_t *pPresentInfo, uint32_t unPresentInfoSize ) = 0; - /** Block until the last presented buffer start scanning out. */ virtual void WaitForPresent() = 0; - /** Provides timing data for synchronizing with display. */ virtual bool GetTimeSinceLastVsync( float *pfSecondsSinceLastVsync, uint64_t *pulFrameCounter ) = 0; }; @@ -4092,7 +3597,6 @@ namespace vr } -// ivrresources.h namespace vr { @@ -4101,17 +3605,9 @@ class IVRResources { public: - // ------------------------------------ - // Shared Resource Methods - // ------------------------------------ - /** Loads the specified resource into the provided buffer if large enough. - * Returns the size in bytes of the buffer required to hold the specified resource. */ virtual uint32_t LoadSharedResource( const char *pchResourceName, char *pchBuffer, uint32_t unBufferLen ) = 0; - /** Provides the full path to the specified resource. Resource names can include named directories for - * drivers and other things, and this resolves all of those and returns the actual physical path. - * pchResourceTypeDirectory is the subdirectory of resources to look in. */ virtual uint32_t GetResourceFullPath( const char *pchResourceName, const char *pchResourceTypeDirectory, VR_OUT_STRING() char *pchPathBuffer, uint32_t unBufferLen ) = 0; }; @@ -4120,7 +3616,6 @@ static const char * const IVRResources_Version = "IVRResources_001"; } -// ivriobuffer.h namespace vr { @@ -4146,35 +3641,25 @@ static const uint64_t k_ulInvalidIOBufferHandle = 0; IOBufferMode_Create = 0x0200, }; - // ---------------------------------------------------------------------------------------------- - // Purpose: - // ---------------------------------------------------------------------------------------------- class IVRIOBuffer { public: - /** opens an existing or creates a new IOBuffer of unSize bytes */ virtual vr::EIOBufferError Open( const char *pchPath, vr::EIOBufferMode mode, uint32_t unElementSize, uint32_t unElements, vr::IOBufferHandle_t *pulBuffer ) = 0; - /** closes a previously opened or created buffer */ virtual vr::EIOBufferError Close( vr::IOBufferHandle_t ulBuffer ) = 0; - /** reads up to unBytes from buffer into *pDst, returning number of bytes read in *punRead */ virtual vr::EIOBufferError Read( vr::IOBufferHandle_t ulBuffer, void *pDst, uint32_t unBytes, uint32_t *punRead ) = 0; - /** writes unBytes of data from *pSrc into a buffer. */ virtual vr::EIOBufferError Write( vr::IOBufferHandle_t ulBuffer, void *pSrc, uint32_t unBytes ) = 0; - /** retrieves the property container of an buffer. */ virtual vr::PropertyContainerHandle_t PropertyContainer( vr::IOBufferHandle_t ulBuffer ) = 0; - /** inexpensively checks for readers to allow writers to fast-fail potentially expensive copies and writes. */ virtual bool HasReaders( vr::IOBufferHandle_t ulBuffer ) = 0; }; static const char * const IVRIOBuffer_Version = "IVRIOBuffer_002"; } -// ivrdrivermanager.h namespace vr { @@ -4184,7 +3669,6 @@ class IVRDriverManager public: virtual uint32_t GetDriverCount() const = 0; - /** Returns the length of the number of bytes necessary to hold this string including the trailing null. */ virtual uint32_t GetDriverName( vr::DriverId_t nDriver, VR_OUT_STRING() char *pchValue, uint32_t unBufferSize ) = 0; virtual DriverHandle_t GetDriverHandle( const char *pchDriverName ) = 0; @@ -4194,173 +3678,86 @@ public: static const char * const IVRDriverManager_Version = "IVRDriverManager_001"; -} // namespace vr +} -// ivrdriverspatialanchors.h namespace vr { struct SpatialAnchorDriverPose_t { - /** This position is in the same "world" space (+Y up) as provided by DriverPose_t. */ vr::HmdQuaternion_t qWorldRotation; vr::HmdVector3d_t vWorldTranslation; - /** The pose will automatically start returning VRSpatialAnchorError_NotAvailableInThisUniverse - * if this is nonzero and does not match the current universe ID. */ uint64_t ulRequiredUniverseId; - /** When this time expires, SteamVR will start generating - * VREvent_SpatialAnchors_RequestPoseUpdate when the pose is read by an application - * to let the driver know it is still worth updating. - * You can use this facility in several ways: - * 1. Set to -1 to never receive an update request for this pose. The driver - * may still update poses at any time. - * 2. Set to 0 to always receive an update request *after* each time the pose - * is read. The rate of requests could be very high if the application gets - * the pose at framerate. - * 3. If the driver knows there is no reason to update the pose for some amount of - * time, it can set that time here and receive the update request reminder later. - * 4. If the driver plans to automatically update this pose for some amount of time - * (as it gets better information about the virtual location of this anchor) it can - * set that duration here to indicate that no "update requested" reminders are needed. - * When that automatic update period expires, any future interest in the pose will - * be indicated by a pose update request. - * The driver may always update the pose, including during the valid duration. */ - double fValidDuration; // seconds + + + + + + + + + + + + + + + + + + double fValidDuration; }; class IVRDriverSpatialAnchors { public: - /* NOTE: You must declare support for spatial anchors in your driver manifest. Add - * "spatialAnchorsSupport": true to your manifest. Without that setting, SteamVR - * will short-circuit anchor requests from applications and provide a generic descriptor - * that does not have any of the advantages of true spatial anchors. */ - - /* The driver should monitor for events VREvent_SpatialAnchors_RequestPoseUpdate (for new - * descriptors from applications that need UpdateSpatialAnchorPose()) and for - * VREvent_SpatialAnchors_RequestDescriptorUpdate (for new poses that need UpdateSpatialAnchorDescriptor()). - * For automatic pose updates over time, the driver should keep track of the handles it - * has seen and provide updates when conditions change. If the driver uses fValidDuration, - * it may wait for VREvent_SpatialAnchors_RequestPoseUpdate instead of keeping track itself. */ - - /** Update a pose for a spatial anchor. Should be called when an event notifies the driver that a - * new descriptor has been registered by an application. May be called for any anchor whenever the - * driver has better information about the best virtual coordinate to represent the anchor. Should - * be called on all active handles whenever driver state changes in a way that changes how physical - * world locations map to virtual coordinates (e.g. anything that would cause a universe ID change). - * This fires an event when it is called for the first time (to alert whoever submitted the descriptor). */ virtual EVRSpatialAnchorError UpdateSpatialAnchorPose( SpatialAnchorHandle_t unHandle, const SpatialAnchorDriverPose_t *pPose ) = 0; - /** Invalidate any pose associated with the handle and cause future calls to GetSpatialAnchorPose (on - * both the client and driver side) to return the specified error. eError must be one of - * VRSpatialAnchorError_NotYetAvailable, VRSpatialAnchorError_NotAvailableInThisUniverse, or - * VRSpatialAnchorError_PermanentlyUnavailable */ virtual EVRSpatialAnchorError SetSpatialAnchorPoseError( SpatialAnchorHandle_t unHandle, EVRSpatialAnchorError eError, double fValidDuration ) = 0; - /** Update the descriptor for a spatial anchor. Should be called when an event notifies the driver - * that a new pose has been registered by an application. May be called for any anchor whenever the - * driver has better or additional information it wants to include in the anchor descriptor. Note, - * however, that the application may never fetch the updated anchor descriptor and may request the - * original descriptor in a future session having ignored the update. - * The supplied descriptor should be only the driver's opaque internal data, not the decorated form that - * is used by clients (wrapped by runtime metadata). The descriptor must not contain non-ASCII characters or - * the two special characters ~ or " - * This fires an event every time it is called. */ virtual EVRSpatialAnchorError UpdateSpatialAnchorDescriptor( SpatialAnchorHandle_t unHandle, const char *pchDescriptor ) = 0; - /** Get the pose for a given handle. */ virtual EVRSpatialAnchorError GetSpatialAnchorPose( SpatialAnchorHandle_t unHandle, SpatialAnchorDriverPose_t *pDriverPoseOut ) = 0; - /** Get the descriptor for a given handle. This will be VRSpatialAnchorError_NotYetAvailable for handles - * where the driver has not yet built a descriptor. It will be the application-supplied descriptor for previously - * saved anchors that the application is requesting poses for. If the driver has called UpdateSpatialAnchorDescriptor() - * already in this session, it will be the descriptor provided by the driver. - * If bDecorated, returns the descriptor wrapped with runtime metadata suitable for a client to save. Else returns only - * the driver's opaque internal data. - */ virtual EVRSpatialAnchorError GetSpatialAnchorDescriptor( SpatialAnchorHandle_t unHandle, VR_OUT_STRING() char *pchDescriptorOut, uint32_t *punDescriptorBufferLenInOut, bool bDecorated ) = 0; }; static const char * const IVRDriverSpatialAnchors_Version = "IVRDriverSpatialAnchors_001"; -} // namespace vr +} -// ivripcresourcemanagerclient.h namespace vr { -// ----------------------------------------------------------------------------- -// Purpose: Interact with the IPCResourceManager -// ----------------------------------------------------------------------------- class IVRIPCResourceManagerClient { public: - /** Create a new tracked Vulkan Image - * - * nImageFormat: in VkFormat - */ virtual bool NewSharedVulkanImage( uint32_t nImageFormat, uint32_t nWidth, uint32_t nHeight, bool bRenderable, bool bMappable, bool bComputeAccess, uint32_t unMipLevels, uint32_t unArrayLayerCount, uint32_t unAdditionalVkCreateFlags, uint32_t unAdditionalVkUsageFlags, vr::SharedTextureHandle_t *pSharedHandle ) = 0; - /** Create a new tracked Vulkan Buffer */ virtual bool NewSharedVulkanBuffer( uint32_t nSize, uint32_t nUsageFlags, vr::SharedTextureHandle_t *pSharedHandle ) = 0; - /** Create a new tracked Vulkan Semaphore */ virtual bool NewSharedVulkanSemaphore( bool bCounting, vr::SharedTextureHandle_t *pSharedHandle ) = 0; - /** Grab a reference to hSharedHandle, and optionally generate a new IPC handle if pNewIpcHandle is not nullptr */ virtual bool RefResource( vr::SharedTextureHandle_t hSharedHandle, uint64_t *pNewIpcHandle ) = 0; - /** Drop a reference to hSharedHandle */ virtual bool UnrefResource( vr::SharedTextureHandle_t hSharedHandle ) = 0; - /* Get all the DRM formats we support using DMA-BUF images for. - * - * pOutFormatCount and pOutFormats function like Vulkan: - * - If pOutFormats is NULL, then pOutFormatCount will be overwritten with the format count. - * - If pOutFormats is not NULL, then pOutFormatCount specifies the size of the pOutFormats array, - * and will be overwritten with the number of formats written to the array. - * - * If the function fails, false is returned, and pOutFormatCount will be 0. - * Supported on Linux only. - */ virtual bool GetDmabufFormats( uint32_t *pOutFormatCount, uint32_t *pOutFormats ) = 0; - /** Get dmabuf modifiers we are allowed to use. - * - * pOutModifierCount and pOutModifiers function like Vulkan: - * - If pOutModifiers is NULL, then pOutModifierCount will be overwritten with the modifier count. - * - If pOutModifiers is not NULL, then pOutModifierCount specifies the size of the pOutModifiers array, - * and will be overwritten with the number of modifiers written to the array. - * - * If modifiers are not supported, a single DRM_FORMAT_MOD_INVALID entry will be returned. - * - * If the function fails, false is returned, and pOutModifierCount will be 0. - * Supported on Linux only. - */ virtual bool GetDmabufModifiers( vr::EVRApplicationType eApplicationType, uint32_t unDRMFormat, uint32_t *pOutModifierCount, uint64_t *pOutModifiers ) = 0; - /** Import a dmabuf directly. - * Note: the FD you pass in will be dup'ed, so you must close it yourself. - * This function does NOT take ownership of the fd you pass in. - * Supported on Linux only. - */ virtual bool ImportDmabuf( vr::EVRApplicationType eApplicationType, vr::DmabufAttributes_t *pDmabufAttributes, vr::SharedTextureHandle_t *pSharedHandle ) = 0; - /** Consumes an IPC handle (eg. from RefResource) and returns a file-descriptor. - * Caller acquires ownership of fd and is responsible for closing it. - * Supported on Linux only. - */ virtual bool ReceiveSharedFd( uint64_t ulIpcHandle, int *pOutFd ) = 0; protected: - /** Non-deletable */ virtual ~IVRIPCResourceManagerClient() {}; }; @@ -4566,7 +3963,7 @@ namespace vr inline COpenVRDriverContext &OpenVRInternal_ModuleServerDriverContext() { static void *ctx[sizeof( COpenVRDriverContext ) / sizeof( void * )]; - return *(COpenVRDriverContext *)ctx; // bypass zero-init constructor + return *(COpenVRDriverContext *)ctx; } inline IVRSettings *VR_CALLTYPE VRSettings() { return OpenVRInternal_ModuleServerDriverContext().VRSettings(); } @@ -4604,7 +4001,6 @@ namespace vr inline EVRInitError COpenVRDriverContext::InitServer() { Clear(); - // VRIPCResourceManager initialized async. if ( !VRServerDriverHost() || !VRSettings() || !VRProperties() @@ -4693,11 +4089,11 @@ namespace vr vr::CleanupDriverContext(); -#endif // OPENVR_INTERFACE_INTERNAL +#endif } -// End -#endif // _OPENVR_DRIVER_API + +#endif diff --git a/tools/vr/src/presenter.cpp b/tools/vr/src/presenter.cpp index d807c0764..2b0d446ec 100644 --- a/tools/vr/src/presenter.cpp +++ b/tools/vr/src/presenter.cpp @@ -1,4 +1,3 @@ -// See presenter.h. #define VK_NO_PROTOTYPES #include <vulkan/vulkan_core.h> typedef struct _XDisplay Display; @@ -41,7 +40,6 @@ void Log(const char *fmt, ...) { fprintf(stderr, "[droiddeck] %s\n", buf); } -// ---------------------------------------------------------------- Xlib, loaded at runtime struct X11 { void *lib = nullptr; @@ -68,7 +66,6 @@ struct X11 { } }; -// ---------------------------------------------------------------- Vulkan, loaded at runtime #define VK_GLOBAL(X) X(CreateInstance) #define VK_INSTANCE(X) \ @@ -124,7 +121,7 @@ public: std::lock_guard<std::mutex> lock(mutex_); for (int i = 0; i < 3; i++) { if (fds[i] < 0) continue; - int fd = dup(fds[i]); // the import owns its fd; the driver keeps its own + int fd = dup(fds[i]); if (fd >= 0) pending_.push_back({handles[i], fd, format, w, h}); } EnsureThread(); @@ -163,7 +160,6 @@ private: } } - // ------------------------------------------------------------ the presenting thread void Run() { const char *mode = getenv("DROIDDECK_VR_PRESENT"); @@ -198,9 +194,6 @@ private: if (wl_) { wl_display_dispatch_pending(wl_); wl_display_flush(wl_); } for (auto &p : imports) Import(p); for (auto h : removals) Forget(h); - // SteamVR hands the textures over at Present with no fence the driver can wait on: give the - // compositor's GPU work a moment to land before copying, as ALVR does (a torn or half-drawn - // frame otherwise). if (f.texture[0]) std::this_thread::sleep_until(at + guard_); if (f.texture[0] && Draw(f)) shown++; auto now = std::chrono::steady_clock::now(); @@ -216,10 +209,6 @@ private: std::chrono::microseconds guard_{4000}; - // ------------------------------------------------------------ Wayland: straight to DroidDeck's compositor - // The compositor the app runs (wayland-0 in the session's runtime dir, beside gamescope's socket) - // takes a fullscreen toplevel at the panel's size: one hop and one scale fewer than the X11 overlay - // through gamescope, whose output is the session's (often smaller) resolution. struct wl_display *wl_ = nullptr; struct wl_compositor *wlComp_ = nullptr; struct xdg_wm_base *wm_ = nullptr; @@ -272,9 +261,6 @@ private: xdg_toplevel_set_fullscreen(toplevel_, nullptr); wl_surface_commit(wlSurf_); for (int i = 0; i < 20 && !configured_; i++) wl_display_roundtrip(wl_); - // The size: the panel the Cardboard pass draws (two eyes, from the shared pose file), so nothing - // scales the picture down on the way. The compositor's configure carries the session's output - // size, which is often smaller; it is the fallback. width_ = height_ = 0; int fd = open("/run/droiddeck/droiddeck-headpose", O_RDONLY | O_CLOEXEC); if (fd >= 0) { @@ -291,8 +277,6 @@ private: bool InitWindow() { if (const char *g = getenv("DROIDDECK_VR_PRESENT_GUARD_MS")) guard_ = std::chrono::microseconds((long)(atof(g) * 1000)); if (!x_.Load()) { Log("presenter: no libX11"); return false; } - // gamescope's first Xwayland (:0) is where its overlays (mangoapp, Steam's) live; SteamVR's - // processes inherit the games' display (:1), so it is not taken from DISPLAY. const char *name = getenv("DROIDDECK_VR_DISPLAY"); if (!name || !*name) name = ":0"; dpy_ = x_.OpenDisplay(name); @@ -302,14 +286,11 @@ private: height_ = (uint32_t)x_.DisplayHeight(dpy_, scr); win_ = x_.CreateSimpleWindow(dpy_, x_.DefaultRootWindow(dpy_), 0, 0, width_, height_, 0, 0, 0); x_.StoreName(dpy_, win_, "DroidDeck Cardboard VR"); - // gamescope shows an external overlay above whatever has focus (mangoapp is one). - // gamescope composites one external overlay (mangoapp, the perf HUD, usually holds it) and one Steam - // overlay; the Steam overlay slot is free while nothing is shown over a game. const char *atom = getenv("DROIDDECK_VR_OVERLAY_ATOM"); if (!atom || !*atom) atom = "STEAM_OVERLAY"; - unsigned long cardinal = 6; // XA_CARDINAL - unsigned long one = 1; // format 32 properties are longs in Xlib - x_.ChangeProperty(dpy_, win_, x_.InternAtom(dpy_, atom, 0), cardinal, 32, 0 /* PropModeReplace */, + unsigned long cardinal = 6; + unsigned long one = 1; + x_.ChangeProperty(dpy_, win_, x_.InternAtom(dpy_, atom, 0), cardinal, 32, 0 , (const unsigned char *)&one, 1); x_.MapWindow(dpy_, win_); x_.Flush(dpy_); @@ -438,15 +419,10 @@ private: return -1; } - // Imports one of SteamVR's shared images with the parameters it was made with (ALVR's recipe): - // optimal tiling, mutable format (which keeps it uncompressed), opaque fd, dedicated memory. UNORM so a - // blit copies the bytes rather than decoding sRGB. void Import(const PendingImport &p) { Imported im{p.handle}; im.width = p.width; im.height = p.height; - // The same format list SteamVR's mutable sRGB images carry: with it Turnip keeps UBWC compression on, - // and the import has to agree on that or it reads compressed data as plain pixels. const char *fl = getenv("DROIDDECK_VR_IMPORT_FORMAT_LIST"); const bool formatList = !fl || strcmp(fl, "0") != 0; VkFormat formats[2] = {(VkFormat)p.format, UnormOf(p.format)}; @@ -516,8 +492,6 @@ private: VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, nullptr, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; vk_.BeginCommandBuffer(cmd_, &bi); VkImage dst = images_[idx]; - // The eye textures come from SteamVR's compositor (another process): take them from the external - // queue family as they are (GENERAL keeps the contents; they are uncompressed). VkImageMemoryBarrier pre[3] = { Barrier(dst, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT), Barrier(eye[0]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_TRANSFER_READ_BIT, @@ -597,11 +571,11 @@ private: std::vector<Imported> textures_; }; -} // namespace +} void Presenter::AddTextures(const uint64_t h[3], const int fds[3], uint32_t f, uint32_t w, uint32_t hh) { Impl::Get().Add(h, fds, f, w, hh); } void Presenter::RemoveTexture(uint64_t h) { Impl::Get().Remove(h); } void Presenter::Submit(const EyeFrame &f) { Impl::Get().Submit(f); } void Presenter::Shutdown() { Impl::Get().Shutdown(); } -} // namespace droiddeck +} diff --git a/tools/vr/src/presenter.h b/tools/vr/src/presenter.h index 47c6c8ac7..abbf455d6 100644 --- a/tools/vr/src/presenter.h +++ b/tools/vr/src/presenter.h @@ -1,34 +1,19 @@ -// Puts SteamVR's eye textures on the screen for the Cardboard driver (driver_droiddeck.cpp). -// -// The driver-direct component hands each frame over as two eye textures: shared Vulkan images, -// dmabufs on Linux. The presenter imports them into a Vulkan device of its own and draws them side by -// side into a fullscreen X11 window on gamescope's Xwayland, marked as an external overlay so gamescope -// shows it above whatever app has focus. DroidDeck's compositor, in Cardboard mode with a -// side-by-side source, then puts the lens distortion on it for the viewer. -// -// SteamVR compositor -> eye textures -> [presenter: blit L|R] -> X11 overlay -> gamescope -// -> DroidDeck compositor -> Cardboard lens pass -> panel -// -// Everything is loaded at runtime (libvulkan.so.1, libX11.so.6): the driver links against nothing -// the cross build image lacks. #pragma once #include <cstdint> namespace droiddeck { struct EyeFrame { - uint64_t texture[2]; // SharedTextureHandle_t per eye - float bounds[2][4]; // uMin, vMin, uMax, vMax per eye + uint64_t texture[2]; + float bounds[2][4]; }; class Presenter { public: - // Called from CreateSwapTextureSet: the three images of one set, as SteamVR made them. static void AddTextures(const uint64_t handles[3], const int fds[3], uint32_t vkFormat, uint32_t width, uint32_t height); static void RemoveTexture(uint64_t handle); - // Called from Present: the frame's eye textures; returns at once (a thread of its own presents). static void Submit(const EyeFrame &frame); static void Shutdown(); }; -} // namespace droiddeck +} diff --git a/tools/vr/src/vk_layer_droiddeck_vr.c b/tools/vr/src/vk_layer_droiddeck_vr.c index 8ac54888f..d1bb2bd21 100644 --- a/tools/vr/src/vk_layer_droiddeck_vr.c +++ b/tools/vr/src/vk_layer_droiddeck_vr.c @@ -1,33 +1,4 @@ #define _GNU_SOURCE -/* - * VK_LAYER_DROIDDECK_vr: lets SteamVR's vrcompositor create its Vulkan instance on Turnip/KGSL. - * - * vrcompositor always asks for VK_KHR_display and the direct-mode extensions around it (it opens a - * direct mode window even when a driver-direct-mode driver - tools/vr/src/driver_droiddeck.cpp - - * will receive the frames). Turnip's KGSL backend has no displays and refuses to enumerate any GPU - * on an instance with VK_KHR_display enabled. This layer takes those extensions out of the instance - * it creates below itself and answers the display queries on its own: there are no displays. - * - * Shared semaphores: SteamVR shares a timeline semaphore with every image it shares between its - * processes, exported as an opaque fd. Turnip/KGSL cannot export one. Such a semaphore is created - * unshared here instead, its "fd" is a placeholder (a memfd named droiddeck-detached-sem), and a - * process importing a placeholder keeps a detached semaphore whose GPU waits are dropped and whose - * host waits return at once. Each process still orders its own work; what is lost is only the GPU - * ordering between processes, which SteamVR's own IPC (one frame at a time) mostly covers. - * - * Where it runs: droiddeck-steamvr enables it explicitly in SteamVR's own processes (VK_INSTANCE_LAYERS - * + VK_LAYER_PATH). It is also an implicit layer for the whole session, because processes SteamVR did - * not start share textures with it too: Steam's web helper draws its in-VR overlay through SteamVR, - * needs the exportable semaphores, and without them crashes until Steam quits. As an implicit layer - * it stays out of the way: - * - the display stand-in (taking display extensions off, the fake display) happens only while - * SteamVR runs (droiddeck-steamvr's marker file) or where the layer was enabled by name, never in - * an ordinary session: gamescope and games see the driver's extensions unchanged; - * - the semaphore bridge is active in those same processes, and in Steam's web helper once SteamVR - * is installed (its GPU process outlives SteamVR runs); - * - everything else - every process of a session without SteamVR installed - gets the next layer's - * functions untouched. - */ #include <vulkan/vulkan.h> #include <vulkan/vk_layer.h> @@ -51,10 +22,7 @@ static void say(const char *fmt, const char *arg) { fputc('\n', stderr); } -/* ------------------------------------------------------------------ crash trace (debug) */ -/* DROIDDECK_VR_CRASHTRACE=1: print a backtrace on a fatal signal. vrcompositor's launcher drops - * LD_PRELOAD, but this layer is loaded into it, so this is the way to see where it dies. */ #include <dlfcn.h> #include <execinfo.h> #include <signal.h> @@ -86,7 +54,6 @@ static void crash_trace_install(void) { static int done; if (done++) return; if (!getenv("DROIDDECK_VR_CRASHTRACE")) { - /* Or a marker beside droiddeck-steamvr's, for processes whose environment is not ours (Steam). */ const char *rt = getenv("XDG_RUNTIME_DIR"); char path[512]; snprintf(path, sizeof(path), "%s/droiddeck-steamvr/crashtrace", rt && *rt ? rt : "/run/droiddeck"); @@ -100,33 +67,28 @@ static void crash_trace_install(void) { sa.sa_flags = SA_SIGINFO | SA_ONSTACK; int sigs[] = {SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE}; for (unsigned i = 0; i < sizeof(sigs) / sizeof(*sigs); i++) { - /* Only where nothing handles the signal yet: Wine and FEX (Proton games) take SIGSEGV as part - * of their normal work, and replacing their handler crashes the game. */ struct sigaction old; if (sigaction(sigs[i], NULL, &old) == 0 && !(old.sa_flags & SA_SIGINFO) && old.sa_handler == SIG_DFL) sigaction(sigs[i], &sa, NULL); } } -/* ------------------------------------------------------------------ dispatch */ struct inst { VkInstance handle; PFN_vkGetInstanceProcAddr gipa; PFN_vkDestroyInstance destroy; - int stripped; /* the app asked for display extensions we took out */ - int active; /* see the top of the file */ + int stripped; + int active; }; struct dev { VkDevice handle; PFN_vkGetDeviceProcAddr gdpa; PFN_vkDestroyDevice destroy; int active; - int display; /* its instance has the pretend display */ + int display; }; -/* SteamVR is running (droiddeck-steamvr start/stop keeps this file), or the layer was asked for by - * name. Only then does the layer stand in for a display. */ static int steamvr_running(void) { const char *layers = getenv("VK_INSTANCE_LAYERS"); if (layers && strstr(layers, "VK_LAYER_DROIDDECK_vr")) return 1; @@ -137,10 +99,6 @@ static int steamvr_running(void) { return access(path, F_OK) == 0; } -/* Whether an instance gets the semaphore bridge. Steam's web helper shares its in-VR overlays with - * SteamVR, and its GPU process lives across SteamVR runs: one created before SteamVR started crashes - * once SteamVR shows the overlay, and after a few of those Steam quits. So it is bridged whenever - * SteamVR is installed, not only while it runs. */ static int bridge_wanted(void) { if (steamvr_running()) return 1; if (strcmp(program_invocation_short_name, "steamwebhelper")) return 0; @@ -155,7 +113,6 @@ static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; static struct inst g_inst[MAX_OBJS]; static struct dev g_dev[MAX_OBJS]; -/* The dispatch key: the loader's dispatch table pointer at the start of every dispatchable object. */ static void *key(const void *obj) { return *(void **)obj; } static struct inst *inst_for(const void *obj) { @@ -178,7 +135,6 @@ static struct dev *dev_for(const void *obj) { #define NEXT(dev, fn) ((PFN_vk##fn)dev_for(dev)->gdpa(dev, "vk" #fn)) #define HOOK_AS(str, fn) if (!strcmp(name, str)) return (PFN_vkVoidFunction)fn; -/* Extensions this layer takes off the instance and stands in for. */ static const char *const DISPLAY_EXTS[] = { "VK_KHR_display", "VK_EXT_direct_mode_display", "VK_EXT_display_surface_counter", "VK_KHR_get_display_properties2", "VK_EXT_acquire_xlib_display", "VK_EXT_acquire_drm_display", @@ -189,7 +145,6 @@ static int is_display_ext(const char *name) { return 0; } -/* ------------------------------------------------------------------ instance */ static VkResult VKAPI_CALL CreateInstance(const VkInstanceCreateInfo *ci, const VkAllocationCallbacks *alloc, VkInstance *out) { @@ -203,7 +158,6 @@ static VkResult VKAPI_CALL CreateInstance(const VkInstanceCreateInfo *ci, const PFN_vkCreateInstance next_create = (PFN_vkCreateInstance)next_gipa(VK_NULL_HANDLE, "vkCreateInstance"); if (!next_create) return VK_ERROR_INITIALIZATION_FAILED; - /* Outside SteamVR the instance is created exactly as asked: display extensions stay on. */ const int vr = steamvr_running(); VkInstanceCreateInfo mine = *ci; const char **names = calloc(ci->enabledExtensionCount + 1, sizeof(*names)); @@ -216,7 +170,6 @@ static VkResult VKAPI_CALL CreateInstance(const VkInstanceCreateInfo *ci, const if (stripped) { int have = 0; for (uint32_t i = 0; i < n; i++) have |= !strcmp(names[i], "VK_EXT_headless_surface"); - /* The pretend display's surface is a headless one; it needs VK_KHR_surface too. */ const char *more[] = {"VK_EXT_headless_surface", "VK_KHR_surface"}; names = realloc(names, (n + 3) * sizeof(*names)); if (!have) names[n++] = more[0]; @@ -253,7 +206,6 @@ static void VKAPI_CALL DestroyInstance(VkInstance instance, const VkAllocationCa if (d) d(instance, alloc); } -/* ------------------------------------------------------------------ device */ static VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceCreateInfo *ci, const VkAllocationCallbacks *alloc, VkDevice *out) { @@ -270,7 +222,6 @@ static VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceCreat const int active = in && in->active; const int display = in && in->stripped; - /* VK_EXT_display_control (vblank events on a display) goes too: there is no display. */ VkDeviceCreateInfo mine = *ci; const char **names = calloc(ci->enabledExtensionCount + 1, sizeof(*names)); uint32_t n = 0; @@ -307,13 +258,6 @@ static void VKAPI_CALL DestroyDevice(VkDevice device, const VkAllocationCallback if (f) f(device, alloc); } -/* ------------------------------------------------------------------ the display stand-in - * - * One pretend display with one mode and one plane. vrcompositor keeps a direct mode window on it even - * when a driver-direct driver takes the frames, and waits on that window's presents, so the display - * needs a working swapchain: its surface is a headless one (VK_EXT_headless_surface, Mesa's WSI). - * The vblank side of VK_EXT_display_control is a 60 Hz clock. Size and rate: - * DROIDDECK_VR_DISPLAY_MODE=WIDTHxHEIGHT@HZ (default 1920x1080@60). */ #include <time.h> @@ -452,8 +396,6 @@ static VkResult VKAPI_CALL GetRandROutputDisplayEXT(VkPhysicalDevice pd, void *d *d = FAKE_DISPLAY; return VK_SUCCESS; } -/* VK_EXT_display_control on the device: a 60 Hz clock. A vblank event's fence comes back signalled - * once the next tick has passed (the caller's vblank thread waits right after registering). */ static VkResult VKAPI_CALL DisplayPowerControlEXT(VkDevice d, VkDisplayKHR disp, const VkDisplayPowerInfoEXT *info) { return VK_SUCCESS; } static VkResult signalled_fence(VkDevice d, const VkAllocationCallbacks *a, VkFence *fence) { VkFenceCreateInfo fci = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, NULL, VK_FENCE_CREATE_SIGNALED_BIT}; @@ -477,14 +419,10 @@ static VkResult VKAPI_CALL GetSwapchainCounterEXT(VkDevice d, VkSwapchainKHR sc, return VK_SUCCESS; } -/* The pretend display's swapchain, on the headless surface: no swapchain counter (that belongs to - * VK_EXT_display_control, which the device does not have), and a request the headless WSI accepts. */ static VkResult VKAPI_CALL CreateSwapchainKHR(VkDevice d, const VkSwapchainCreateInfoKHR *ci, const VkAllocationCallbacks *a, VkSwapchainKHR *out) { PFN_vkCreateSwapchainKHR next = NEXT(d, CreateSwapchainKHR); VkSwapchainCreateInfoKHR sized = *ci; - /* A headless surface has no size of its own (UINT32_MAX), and SteamVR passes that through: the - * pretend display's mode is the size. */ mode_init(); if (sized.imageExtent.width == UINT32_MAX || sized.imageExtent.height == UINT32_MAX) sized.imageExtent = (VkExtent2D){g_mode_w, g_mode_h}; @@ -498,13 +436,12 @@ static VkResult VKAPI_CALL CreateSwapchainKHR(VkDevice d, const VkSwapchainCreat for (const VkBaseInStructure *p = ci->pNext; p && n < (int)sizeof(line) - 16; p = p->pNext) n += snprintf(line + n, sizeof(line) - n, " %d", p->sType); say("vkCreateSwapchainKHR refused (%s); retrying for the headless surface", line); VkSwapchainCreateInfoKHR mine = *ci; - mine.pNext = NULL; /* drops VkSwapchainCounterCreateInfoEXT and friends */ + mine.pNext = NULL; mine.presentMode = VK_PRESENT_MODE_FIFO_KHR; mine.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; if (mine.minImageCount < 2) mine.minImageCount = 2; r = next(d, &mine, a, out); if (r != VK_SUCCESS) { - /* Last resort: the plainest swapchain the surface can take. */ mine.imageFormat = VK_FORMAT_B8G8R8A8_UNORM; mine.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; mine.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; @@ -524,16 +461,6 @@ static PFN_vkVoidFunction display_device_hook(const char *name) { return NULL; } -/* ------------------------------------------------------------------ bridged shared semaphores - * - * SteamVR shares a timeline semaphore with every image it shares between processes, exported as an - * opaque fd; Turnip/KGSL cannot export one. Here such a semaphore is created unshared, and its "fd" is - * a memfd (named droiddeck-shared-sem) holding a 64-bit counter. Every process that has the semaphore - - * the creator and each importer - keeps its own real timeline semaphore, and a poller thread in each - * process carries values both ways: a value reached locally goes up into the counter, a value the - * counter shows (another process got there) is signalled locally from the host. GPU waits then work - * as written, about a poll period late. A binary semaphore cannot be bridged that way; GPU waits on an - * imported binary one are dropped (each process still orders its own work). */ #include <sched.h> #include <stdatomic.h> @@ -547,14 +474,14 @@ struct bridged { VkSemaphore sem; VkDevice dev; _Atomic uint64_t *counter; - int fd; /* the memfd, kept by the creator so every export can dup it; -1 for an importer */ - int timeline; /* 0: binary, bridged only by dropping its waits */ - unsigned long ino; /* the memfd's inode, which names the semaphore across processes in traces */ - uint64_t submitted; /* the highest value this process has queued a signal of */ + int fd; + int timeline; + unsigned long ino; + uint64_t submitted; }; static pthread_mutex_t g_br_lock = PTHREAD_MUTEX_INITIALIZER; static struct bridged g_br[MAX_SEMS]; -static VkSemaphore g_timeline_sems[MAX_TYPED]; /* every timeline semaphore this process made */ +static VkSemaphore g_timeline_sems[MAX_TYPED]; static pthread_t g_poller; static int g_poller_started; @@ -573,7 +500,6 @@ static void timeline_note(VkSemaphore s, int add) { pthread_mutex_unlock(&g_br_lock); } -/* Under g_br_lock. */ static struct bridged *bridged_find(VkSemaphore s) { for (int i = 0; i < MAX_SEMS; i++) if (g_br[i].sem == s) return &g_br[i]; return NULL; @@ -585,7 +511,6 @@ static void bridged_drop(struct bridged *b) { b->fd = -1; } -/* A `semtrace` file beside droiddeck-steamvr's marker: log every bridged semaphore every 2 s. */ static int sem_trace_on(void) { static int checked, on; if (!checked++) { @@ -673,7 +598,6 @@ static VkResult VKAPI_CALL CreateSemaphore(VkDevice d, const VkSemaphoreCreateIn return r; } if (!export_info) return r; - /* Rebuild the chain without the export request. */ VkSemaphoreTypeCreateInfo type = {0}; VkSemaphoreCreateInfo mine = *ci; mine.pNext = NULL; @@ -688,7 +612,7 @@ static VkResult VKAPI_CALL CreateSemaphore(VkDevice d, const VkSemaphoreCreateIn int fd = memfd_create(SHARED_NAME, MFD_CLOEXEC); _Atomic uint64_t *counter = NULL; if (fd >= 0 && ftruncate(fd, 4096) == 0) counter = map_counter(fd); - if (!counter) { if (fd >= 0) close(fd); return r; } /* unshared, unbridged */ + if (!counter) { if (fd >= 0) close(fd); return r; } atomic_store(counter, timeline ? type_info->initialValue : 0); pthread_mutex_lock(&g_br_lock); struct bridged *slot = bridged_find(VK_NULL_HANDLE); @@ -732,7 +656,7 @@ static VkResult VKAPI_CALL ImportSemaphoreFdKHR(VkDevice d, const VkImportSemaph if (info->fd < 0 || !is_shared_counter(info->fd)) return NEXT(d, ImportSemaphoreFdKHR)(d, info); _Atomic uint64_t *counter = map_counter(info->fd); const unsigned long ino = fd_ino(info->fd); - close(info->fd); /* a successful import owns the fd */ + close(info->fd); const int timeline = timeline_known(info->semaphore); pthread_mutex_lock(&g_br_lock); struct bridged *b = bridged_find(info->semaphore); @@ -748,7 +672,6 @@ static VkResult VKAPI_CALL ImportSemaphoreFdKHR(VkDevice d, const VkImportSemaph return VK_SUCCESS; } -/* An imported binary one: nothing here will ever signal it. */ static int drop_wait(VkSemaphore s) { pthread_mutex_lock(&g_br_lock); struct bridged *b = bridged_find(s); @@ -763,17 +686,8 @@ static uint64_t now_us(void) { return (uint64_t)ts.tv_sec * 1000000u + (uint64_t)ts.tv_nsec / 1000u; } -/* How long a submit may wait for another process to reach a shared value before the wait is dropped. */ #define SUBMIT_GRACE_US 3000 -/* Whether a submit should drop its GPU wait on s for value. - * - * Turnip on KGSL does not defer a submit that waits on a timeline value nobody has signalled yet ("wait - * before signal"): vkQueueSubmit blocks the calling thread until the value is signalled. SteamVR's - * client waits on a value of the compositor's that the compositor signals only after the client has - * submitted and told it so, which on Turnip deadlocks the client before its first frame (helloxr's - * acquire, DXVK's submit thread in Proton games). For an imported timeline semaphore whose value is - * neither reached nor queued here, the counter gets a short grace period, then the wait goes. */ static int submit_drop_wait(VkSemaphore s, uint64_t value) { pthread_mutex_lock(&g_br_lock); struct bridged *b = bridged_find(s); @@ -787,7 +701,6 @@ static int submit_drop_wait(VkSemaphore s, uint64_t value) { if (!imported || !value || value <= queued) return 0; struct dev *d = dev_for(dev); if (!d) return 0; - /* A Vulkan 1.0 device (SteamVR's client interop) has only the KHR names. */ PFN_vkGetSemaphoreCounterValue get = (PFN_vkGetSemaphoreCounterValue)d->gdpa(dev, "vkGetSemaphoreCounterValue"); if (!get) get = (PFN_vkGetSemaphoreCounterValue)d->gdpa(dev, "vkGetSemaphoreCounterValueKHR"); PFN_vkSignalSemaphore sig = (PFN_vkSignalSemaphore)d->gdpa(dev, "vkSignalSemaphore"); @@ -812,7 +725,6 @@ static int submit_drop_wait(VkSemaphore s, uint64_t value) { return 1; } -/* Notes the values this process queues signals of, so its own later waits on them are kept. */ static void note_signal(VkSemaphore s, uint64_t value) { pthread_mutex_lock(&g_br_lock); struct bridged *b = bridged_find(s); @@ -836,7 +748,6 @@ static void note_submit2_signals(uint32_t count, const VkSubmitInfo2 *submits) { note_signal(submits[i].pSignalSemaphoreInfos[j].semaphore, submits[i].pSignalSemaphoreInfos[j].value); } -/* Drops waits on detached semaphores from a list; returns the new count. */ static uint32_t keep_waits(const VkSemaphore *in, const VkPipelineStageFlags *stages, const uint64_t *values, uint32_t n, VkSemaphore *out, VkPipelineStageFlags *out_stages, uint64_t *out_values) { uint32_t k = 0; @@ -850,7 +761,6 @@ static uint32_t keep_waits(const VkSemaphore *in, const VkPipelineStageFlags *st return k; } -/* semtrace: the first wait and the first signal of each shared semaphore, and its first few values. */ static void trace_sem(const char *what, VkSemaphore sem, uint64_t value) { if (!sem_trace_on() || value > 3) return; pthread_mutex_lock(&g_br_lock); @@ -876,8 +786,6 @@ static VkResult VKAPI_CALL QueueSubmit(VkQueue q, uint32_t count, const VkSubmit for (uint32_t j = 0; j < submits[i].signalSemaphoreCount; j++) trace_sem("signals", submits[i].pSignalSemaphores[j], t && j < t->signalSemaphoreValueCount ? t->pSignalSemaphoreValues[j] : 0); } - /* Waits are judged before this submit's own signals count as queued: an acquire may wait on one - * value and signal the next on the same semaphore. */ int any = 0; for (uint32_t i = 0; i < count && !any; i++) { const VkTimelineSemaphoreSubmitInfo *t = NULL; @@ -911,7 +819,6 @@ static VkResult VKAPI_CALL QueueSubmit(VkQueue q, uint32_t count, const VkSubmit copy[i].pWaitSemaphores = sems; copy[i].pWaitDstStageMask = stages; if (t) { - /* Only this struct is replaced; the rest of the chain is kept behind it. */ tl[i] = *t; tl[i].waitSemaphoreValueCount = t->waitSemaphoreValueCount ? k : 0; tl[i].pWaitSemaphoreValues = vals; @@ -920,7 +827,7 @@ static VkResult VKAPI_CALL QueueSubmit(VkQueue q, uint32_t count, const VkSubmit for (const VkBaseInStructure *p = submits[i].pNext; p; prev = p, p = p->pNext) if (p == (const void *)t) { rest = p->pNext; break; } (void)prev; tl[i].pNext = rest; - copy[i].pNext = &tl[i]; /* other structs before it are dropped: SteamVR chains only this one */ + copy[i].pNext = &tl[i]; } } VkResult r = next(q, count, copy, fence); @@ -989,7 +896,6 @@ static VkResult VKAPI_CALL GetSemaphoreCounterValue(VkDevice d, VkSemaphore s, u return next(d, s, value); } -/* semtrace also logs how shared (external memory) images are created, to compare both sides. */ static VkResult VKAPI_CALL CreateImage(VkDevice d, const VkImageCreateInfo *ci, const VkAllocationCallbacks *a, VkImage *out) { VkResult r = NEXT(d, CreateImage)(d, ci, a, out); if (!sem_trace_on()) return r; @@ -1026,7 +932,6 @@ static PFN_vkVoidFunction semaphore_hook(const char *name) { return NULL; } -/* ------------------------------------------------------------------ proc addr */ #define HOOK(fn) if (!strcmp(name, "vk" #fn)) return (PFN_vkVoidFunction)fn; @@ -1085,7 +990,6 @@ EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DroidDeckVr_GetDeviceProcAddr(Vk if (f) return f; } if (d) { - /* Only where the driver has the function: the hooks call through to it. */ PFN_vkVoidFunction mine = semaphore_hook(name); if (mine && d->gdpa(device, name)) return mine; } diff --git a/tools/vr/src/wayland_client_stub.c b/tools/vr/src/wayland_client_stub.c index 8b1b6cdff..d81a07fda 100644 --- a/tools/vr/src/wayland_client_stub.c +++ b/tools/vr/src/wayland_client_stub.c @@ -1,6 +1,3 @@ -/* Link-time stand-in for libwayland-client.so.0 (tools/vr/build.sh): the symbols the presenter uses, so - * the driver links with -z defs against a library the build machine does not have. Never loaded: the - * runtime's real libwayland-client.so.0 satisfies the dependency on the device. */ #include <stdint.h> #include <stddef.h> #include "wayland-util.h" diff --git a/tools/vr/third_party/wayland/wayland-client-core.h b/tools/vr/third_party/wayland/wayland-client-core.h index e0523e49d..3f55d23c7 100644 --- a/tools/vr/third_party/wayland/wayland-client-core.h +++ b/tools/vr/third_party/wayland/wayland-client-core.h @@ -36,94 +36,12 @@ extern "C" { struct timespec; -/** \class wl_proxy - * - * \brief Represents a protocol object on the client side. - * - * A wl_proxy acts as a client side proxy to an object existing in the - * compositor. The proxy is responsible for converting requests made by the - * clients with \ref wl_proxy_marshal() into Wayland's wire format. Events - * coming from the compositor are also handled by the proxy, which will in - * turn call the handler set with \ref wl_proxy_add_listener(). - * - * \note With the exception of function \ref wl_proxy_set_queue(), functions - * accessing a wl_proxy are not normally used by client code. Clients - * should normally use the higher level interface generated by the scanner to - * interact with compositor objects. - * - */ struct wl_proxy; -/** \class wl_display - * - * \brief Represents a connection to the compositor and acts as a proxy to - * the wl_display singleton object. - * - * A wl_display object represents a client connection to a Wayland - * compositor. It is created with either \ref wl_display_connect() or - * \ref wl_display_connect_to_fd(). A connection is terminated using - * \ref wl_display_disconnect(). - * - * A wl_display is also used as the \ref wl_proxy for the wl_display - * singleton object on the compositor side. - * - * A wl_display object handles all the data sent from and to the - * compositor. When a \ref wl_proxy marshals a request, it will write its wire - * representation to the display's write buffer. The data is sent to the - * compositor when the client calls \ref wl_display_flush(). - * - * Incoming data is handled in two steps: queueing and dispatching. In the - * queue step, the data coming from the display fd is interpreted and - * added to a queue. On the dispatch step, the handler for the incoming - * event set by the client on the corresponding \ref wl_proxy is called. - * - * A wl_display has at least one event queue, called the <em>default - * queue</em>. Clients can create additional event queues with \ref - * wl_display_create_queue() and assign \ref wl_proxy's to it. Events - * occurring in a particular proxy are always queued in its assigned queue. - * A client can ensure that a certain assumption, such as holding a lock - * or running from a given thread, is true when a proxy event handler is - * called by assigning that proxy to an event queue and making sure that - * this queue is only dispatched when the assumption holds. - * - * The default queue is dispatched by calling \ref wl_display_dispatch(). - * This will dispatch any events queued on the default queue and attempt - * to read from the display fd if it's empty. Events read are then queued - * on the appropriate queues according to the proxy assignment. - * - * A user created queue is dispatched with \ref wl_display_dispatch_queue(). - * This function behaves exactly the same as wl_display_dispatch() - * but it dispatches given queue instead of the default queue. - * - * A real world example of event queue usage is Mesa's implementation of - * eglSwapBuffers() for the Wayland platform. This function might need - * to block until a frame callback is received, but dispatching the default - * queue could cause an event handler on the client to start drawing - * again. This problem is solved using another event queue, so that only - * the events handled by the EGL code are dispatched during the block. - * - * This creates a problem where a thread dispatches a non-default - * queue, reading all the data from the display fd. If the application - * would call \em poll(2) after that it would block, even though there - * might be events queued on the default queue. Those events should be - * dispatched with \ref wl_display_dispatch_pending() or \ref - * wl_display_dispatch_queue_pending() before flushing and blocking. - */ struct wl_display; -/** \class wl_event_queue - * - * \brief A queue for \ref wl_proxy object events. - * - * Event queues allows the events on a display to be handled in a thread-safe - * manner. See \ref wl_display for details. - * - */ struct wl_event_queue; -/** Destroy proxy after marshalling - * \relates wl_proxy - */ #define WL_MARSHAL_FLAG_DESTROY (1 << 0) void diff --git a/tools/vr/third_party/wayland/wayland-client-protocol.h b/tools/vr/third_party/wayland/wayland-client-protocol.h index 43a1e4e55..8c99c054f 100644 --- a/tools/vr/third_party/wayland/wayland-client-protocol.h +++ b/tools/vr/third_party/wayland/wayland-client-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.26.0 */ #ifndef WAYLAND_CLIENT_PROTOCOL_H #define WAYLAND_CLIENT_PROTOCOL_H @@ -11,34 +10,8 @@ extern "C" { #endif -/** - * @page page_wayland The wayland protocol - * @section page_ifaces_wayland Interfaces - * - @subpage page_iface_wl_display - core global object - * - @subpage page_iface_wl_registry - global registry object - * - @subpage page_iface_wl_callback - callback object - * - @subpage page_iface_wl_compositor - the compositor singleton - * - @subpage page_iface_wl_shm_pool - a shared memory pool - * - @subpage page_iface_wl_shm - shared memory support - * - @subpage page_iface_wl_buffer - content for a wl_surface - * - @subpage page_iface_wl_data_offer - offer to transfer data - * - @subpage page_iface_wl_data_source - offer to transfer data - * - @subpage page_iface_wl_data_device - data transfer device - * - @subpage page_iface_wl_data_device_manager - data transfer interface - * - @subpage page_iface_wl_shell - create desktop-style surfaces - * - @subpage page_iface_wl_shell_surface - desktop-style metadata interface - * - @subpage page_iface_wl_surface - an onscreen surface - * - @subpage page_iface_wl_seat - group of input devices - * - @subpage page_iface_wl_pointer - pointer input device - * - @subpage page_iface_wl_keyboard - keyboard input device - * - @subpage page_iface_wl_touch - touchscreen input device - * - @subpage page_iface_wl_output - compositor output region - * - @subpage page_iface_wl_region - region interface - * - @subpage page_iface_wl_subcompositor - sub-surface compositing - * - @subpage page_iface_wl_subsurface - sub-surface interface to a wl_surface - * - @subpage page_iface_wl_fixes - wayland protocol fixes +/* * @section page_copyright_wayland Copyright - * <pre> * * Copyright © 2008-2011 Kristian Høgsberg * Copyright © 2010-2011 Intel Corporation @@ -64,8 +37,34 @@ extern "C" { * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. - * </pre> */ + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_buffer; struct wl_callback; struct wl_compositor; @@ -92,942 +91,118 @@ struct wl_touch; #ifndef WL_DISPLAY_INTERFACE #define WL_DISPLAY_INTERFACE -/** - * @page page_iface_wl_display wl_display - * @section page_iface_wl_display_desc Description - * - * The core global object. This is a special singleton object. It - * is used for internal Wayland protocol features. - * @section page_iface_wl_display_api API - * See @ref iface_wl_display. - */ -/** - * @defgroup iface_wl_display The wl_display interface - * - * The core global object. This is a special singleton object. It - * is used for internal Wayland protocol features. - */ extern const struct wl_interface wl_display_interface; #endif #ifndef WL_REGISTRY_INTERFACE #define WL_REGISTRY_INTERFACE -/** - * @page page_iface_wl_registry wl_registry - * @section page_iface_wl_registry_desc Description - * - * The singleton global registry object. The server has a number of - * global objects that are available to all clients. These objects - * typically represent an actual object in the server (for example, - * an input device) or they are singleton objects that provide - * extension functionality. - * - * When a client creates a registry object, the registry object - * will emit a global event for each global currently in the - * registry. Globals come and go as a result of device or - * monitor hotplugs, reconfiguration or other events, and the - * registry will send out global and global_remove events to - * keep the client up to date with the changes. To mark the end - * of the initial burst of events, the client can use the - * wl_display.sync request immediately after calling - * wl_display.get_registry. - * - * A client can bind to a global object by using the bind - * request. This creates a client-side handle that lets the object - * emit events to the client and lets the client invoke requests on - * the object. - * @section page_iface_wl_registry_api API - * See @ref iface_wl_registry. - */ -/** - * @defgroup iface_wl_registry The wl_registry interface - * - * The singleton global registry object. The server has a number of - * global objects that are available to all clients. These objects - * typically represent an actual object in the server (for example, - * an input device) or they are singleton objects that provide - * extension functionality. - * - * When a client creates a registry object, the registry object - * will emit a global event for each global currently in the - * registry. Globals come and go as a result of device or - * monitor hotplugs, reconfiguration or other events, and the - * registry will send out global and global_remove events to - * keep the client up to date with the changes. To mark the end - * of the initial burst of events, the client can use the - * wl_display.sync request immediately after calling - * wl_display.get_registry. - * - * A client can bind to a global object by using the bind - * request. This creates a client-side handle that lets the object - * emit events to the client and lets the client invoke requests on - * the object. - */ extern const struct wl_interface wl_registry_interface; #endif #ifndef WL_CALLBACK_INTERFACE #define WL_CALLBACK_INTERFACE -/** - * @page page_iface_wl_callback wl_callback - * @section page_iface_wl_callback_desc Description - * - * Clients can handle the 'done' event to get notified when - * the related request is done. - * - * Note, because wl_callback objects are created from multiple independent - * factory interfaces, the wl_callback interface is frozen at version 1. - * @section page_iface_wl_callback_api API - * See @ref iface_wl_callback. - */ -/** - * @defgroup iface_wl_callback The wl_callback interface - * - * Clients can handle the 'done' event to get notified when - * the related request is done. - * - * Note, because wl_callback objects are created from multiple independent - * factory interfaces, the wl_callback interface is frozen at version 1. - */ extern const struct wl_interface wl_callback_interface; #endif #ifndef WL_COMPOSITOR_INTERFACE #define WL_COMPOSITOR_INTERFACE -/** - * @page page_iface_wl_compositor wl_compositor - * @section page_iface_wl_compositor_desc Description - * - * A compositor. This object is a singleton global. The - * compositor is in charge of combining the contents of multiple - * surfaces into one displayable output. - * @section page_iface_wl_compositor_api API - * See @ref iface_wl_compositor. - */ -/** - * @defgroup iface_wl_compositor The wl_compositor interface - * - * A compositor. This object is a singleton global. The - * compositor is in charge of combining the contents of multiple - * surfaces into one displayable output. - */ extern const struct wl_interface wl_compositor_interface; #endif #ifndef WL_SHM_POOL_INTERFACE #define WL_SHM_POOL_INTERFACE -/** - * @page page_iface_wl_shm_pool wl_shm_pool - * @section page_iface_wl_shm_pool_desc Description - * - * The wl_shm_pool object encapsulates a piece of memory shared - * between the compositor and client. Through the wl_shm_pool - * object, the client can allocate shared memory wl_buffer objects. - * All objects created through the same pool share the same - * underlying mapped memory. Reusing the mapped memory avoids the - * setup/teardown overhead and is useful when interactively resizing - * a surface or for many small buffers. - * @section page_iface_wl_shm_pool_api API - * See @ref iface_wl_shm_pool. - */ -/** - * @defgroup iface_wl_shm_pool The wl_shm_pool interface - * - * The wl_shm_pool object encapsulates a piece of memory shared - * between the compositor and client. Through the wl_shm_pool - * object, the client can allocate shared memory wl_buffer objects. - * All objects created through the same pool share the same - * underlying mapped memory. Reusing the mapped memory avoids the - * setup/teardown overhead and is useful when interactively resizing - * a surface or for many small buffers. - */ extern const struct wl_interface wl_shm_pool_interface; #endif #ifndef WL_SHM_INTERFACE #define WL_SHM_INTERFACE -/** - * @page page_iface_wl_shm wl_shm - * @section page_iface_wl_shm_desc Description - * - * A singleton global object that provides support for shared - * memory. - * - * Clients can create wl_shm_pool objects using the create_pool - * request. - * - * On binding the wl_shm object one or more format events - * are emitted to inform clients about the valid pixel formats - * that can be used for buffers. - * @section page_iface_wl_shm_api API - * See @ref iface_wl_shm. - */ -/** - * @defgroup iface_wl_shm The wl_shm interface - * - * A singleton global object that provides support for shared - * memory. - * - * Clients can create wl_shm_pool objects using the create_pool - * request. - * - * On binding the wl_shm object one or more format events - * are emitted to inform clients about the valid pixel formats - * that can be used for buffers. - */ extern const struct wl_interface wl_shm_interface; #endif #ifndef WL_BUFFER_INTERFACE #define WL_BUFFER_INTERFACE -/** - * @page page_iface_wl_buffer wl_buffer - * @section page_iface_wl_buffer_desc Description - * - * A buffer provides the content for a wl_surface. Buffers are - * created through factory interfaces such as wl_shm, wp_linux_buffer_params - * (from the linux-dmabuf protocol extension) or similar. It has a width and - * a height and can be attached to a wl_surface, but the mechanism by which a - * client provides and updates the contents is defined by the buffer factory - * interface. - * - * Color channels are assumed to be electrical rather than optical (in other - * words, encoded with a transfer function) unless otherwise specified. If - * the buffer uses a format that has an alpha channel, the alpha channel is - * assumed to be premultiplied into the electrical color channel values - * (after transfer function encoding) unless otherwise specified. - * - * Note, because wl_buffer objects are created from multiple independent - * factory interfaces, the wl_buffer interface is frozen at version 1. - * @section page_iface_wl_buffer_api API - * See @ref iface_wl_buffer. - */ -/** - * @defgroup iface_wl_buffer The wl_buffer interface - * - * A buffer provides the content for a wl_surface. Buffers are - * created through factory interfaces such as wl_shm, wp_linux_buffer_params - * (from the linux-dmabuf protocol extension) or similar. It has a width and - * a height and can be attached to a wl_surface, but the mechanism by which a - * client provides and updates the contents is defined by the buffer factory - * interface. - * - * Color channels are assumed to be electrical rather than optical (in other - * words, encoded with a transfer function) unless otherwise specified. If - * the buffer uses a format that has an alpha channel, the alpha channel is - * assumed to be premultiplied into the electrical color channel values - * (after transfer function encoding) unless otherwise specified. - * - * Note, because wl_buffer objects are created from multiple independent - * factory interfaces, the wl_buffer interface is frozen at version 1. - */ extern const struct wl_interface wl_buffer_interface; #endif #ifndef WL_DATA_OFFER_INTERFACE #define WL_DATA_OFFER_INTERFACE -/** - * @page page_iface_wl_data_offer wl_data_offer - * @section page_iface_wl_data_offer_desc Description - * - * A wl_data_offer represents a piece of data offered for transfer - * by another client (the source client). It is used by the - * copy-and-paste and drag-and-drop mechanisms. The offer - * describes the different mime types that the data can be - * converted to and provides the mechanism for transferring the - * data directly from the source client. - * @section page_iface_wl_data_offer_api API - * See @ref iface_wl_data_offer. - */ -/** - * @defgroup iface_wl_data_offer The wl_data_offer interface - * - * A wl_data_offer represents a piece of data offered for transfer - * by another client (the source client). It is used by the - * copy-and-paste and drag-and-drop mechanisms. The offer - * describes the different mime types that the data can be - * converted to and provides the mechanism for transferring the - * data directly from the source client. - */ extern const struct wl_interface wl_data_offer_interface; #endif #ifndef WL_DATA_SOURCE_INTERFACE #define WL_DATA_SOURCE_INTERFACE -/** - * @page page_iface_wl_data_source wl_data_source - * @section page_iface_wl_data_source_desc Description - * - * The wl_data_source object is the source side of a wl_data_offer. - * It is created by the source client in a data transfer and - * provides a way to describe the offered data and a way to respond - * to requests to transfer the data. - * @section page_iface_wl_data_source_api API - * See @ref iface_wl_data_source. - */ -/** - * @defgroup iface_wl_data_source The wl_data_source interface - * - * The wl_data_source object is the source side of a wl_data_offer. - * It is created by the source client in a data transfer and - * provides a way to describe the offered data and a way to respond - * to requests to transfer the data. - */ extern const struct wl_interface wl_data_source_interface; #endif #ifndef WL_DATA_DEVICE_INTERFACE #define WL_DATA_DEVICE_INTERFACE -/** - * @page page_iface_wl_data_device wl_data_device - * @section page_iface_wl_data_device_desc Description - * - * There is one wl_data_device per seat which can be obtained - * from the global wl_data_device_manager singleton. - * - * A wl_data_device provides access to inter-client data transfer - * mechanisms such as copy-and-paste and drag-and-drop. - * @section page_iface_wl_data_device_api API - * See @ref iface_wl_data_device. - */ -/** - * @defgroup iface_wl_data_device The wl_data_device interface - * - * There is one wl_data_device per seat which can be obtained - * from the global wl_data_device_manager singleton. - * - * A wl_data_device provides access to inter-client data transfer - * mechanisms such as copy-and-paste and drag-and-drop. - */ extern const struct wl_interface wl_data_device_interface; #endif #ifndef WL_DATA_DEVICE_MANAGER_INTERFACE #define WL_DATA_DEVICE_MANAGER_INTERFACE -/** - * @page page_iface_wl_data_device_manager wl_data_device_manager - * @section page_iface_wl_data_device_manager_desc Description - * - * The wl_data_device_manager is a singleton global object that - * provides access to inter-client data transfer mechanisms such as - * copy-and-paste and drag-and-drop. These mechanisms are tied to - * a wl_seat and this interface lets a client get a wl_data_device - * corresponding to a wl_seat. - * - * Depending on the version bound, the objects created from the bound - * wl_data_device_manager object will have different requirements for - * functioning properly. See wl_data_source.set_actions, - * wl_data_offer.accept and wl_data_offer.finish for details. - * @section page_iface_wl_data_device_manager_api API - * See @ref iface_wl_data_device_manager. - */ -/** - * @defgroup iface_wl_data_device_manager The wl_data_device_manager interface - * - * The wl_data_device_manager is a singleton global object that - * provides access to inter-client data transfer mechanisms such as - * copy-and-paste and drag-and-drop. These mechanisms are tied to - * a wl_seat and this interface lets a client get a wl_data_device - * corresponding to a wl_seat. - * - * Depending on the version bound, the objects created from the bound - * wl_data_device_manager object will have different requirements for - * functioning properly. See wl_data_source.set_actions, - * wl_data_offer.accept and wl_data_offer.finish for details. - */ extern const struct wl_interface wl_data_device_manager_interface; #endif #ifndef WL_SHELL_INTERFACE #define WL_SHELL_INTERFACE -/** - * @page page_iface_wl_shell wl_shell - * @section page_iface_wl_shell_desc Description - * - * This interface is implemented by servers that provide - * desktop-style user interfaces. - * - * It allows clients to associate a wl_shell_surface with - * a basic surface. - * - * Note! This protocol is deprecated and not intended for production use. - * For desktop-style user interfaces, use xdg_shell. Compositors and clients - * should not implement this interface. - * @section page_iface_wl_shell_api API - * See @ref iface_wl_shell. - */ -/** - * @defgroup iface_wl_shell The wl_shell interface - * - * This interface is implemented by servers that provide - * desktop-style user interfaces. - * - * It allows clients to associate a wl_shell_surface with - * a basic surface. - * - * Note! This protocol is deprecated and not intended for production use. - * For desktop-style user interfaces, use xdg_shell. Compositors and clients - * should not implement this interface. - */ extern const struct wl_interface wl_shell_interface; #endif #ifndef WL_SHELL_SURFACE_INTERFACE #define WL_SHELL_SURFACE_INTERFACE -/** - * @page page_iface_wl_shell_surface wl_shell_surface - * @section page_iface_wl_shell_surface_desc Description - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides requests to treat surfaces like toplevel, fullscreen - * or popup windows, move, resize or maximize them, associate - * metadata like title and class, etc. - * - * On the server side the object is automatically destroyed when - * the related wl_surface is destroyed. On the client side, - * wl_shell_surface_destroy() must be called before destroying - * the wl_surface object. - * @section page_iface_wl_shell_surface_api API - * See @ref iface_wl_shell_surface. - */ -/** - * @defgroup iface_wl_shell_surface The wl_shell_surface interface - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides requests to treat surfaces like toplevel, fullscreen - * or popup windows, move, resize or maximize them, associate - * metadata like title and class, etc. - * - * On the server side the object is automatically destroyed when - * the related wl_surface is destroyed. On the client side, - * wl_shell_surface_destroy() must be called before destroying - * the wl_surface object. - */ extern const struct wl_interface wl_shell_surface_interface; #endif #ifndef WL_SURFACE_INTERFACE #define WL_SURFACE_INTERFACE -/** - * @page page_iface_wl_surface wl_surface - * @section page_iface_wl_surface_desc Description - * - * A surface is a rectangular area that may be displayed on zero - * or more outputs, and shown any number of times at the compositor's - * discretion. They can present wl_buffers, receive user input, and - * define a local coordinate system. - * - * The size of a surface (and relative positions on it) is described - * in surface-local coordinates, which may differ from the buffer - * coordinates of the pixel content, in case a buffer_transform - * or a buffer_scale is used. - * - * A surface without a "role" is fairly useless: a compositor does - * not know where, when or how to present it. The role is the - * purpose of a wl_surface. Examples of roles are a cursor for a - * pointer (as set by wl_pointer.set_cursor), a drag icon - * (wl_data_device.start_drag), a sub-surface - * (wl_subcompositor.get_subsurface), and a window as defined by a - * shell protocol (e.g. wl_shell.get_shell_surface). - * - * A surface can have only one role at a time. Initially a - * wl_surface does not have a role. Once a wl_surface is given a - * role, it is set permanently for the whole lifetime of the - * wl_surface object. Giving the current role again is allowed, - * unless explicitly forbidden by the relevant interface - * specification. - * - * Surface roles are given by requests in other interfaces such as - * wl_pointer.set_cursor. The request should explicitly mention - * that this request gives a role to a wl_surface. Often, this - * request also creates a new protocol object that represents the - * role and adds additional functionality to wl_surface. When a - * client wants to destroy a wl_surface, they must destroy this role - * object before the wl_surface, otherwise a defunct_role_object error is - * sent. - * - * Destroying the role object does not remove the role from the - * wl_surface, but it may stop the wl_surface from "playing the role". - * For instance, if a wl_subsurface object is destroyed, the wl_surface - * it was created for will be unmapped and forget its position and - * z-order. It is allowed to create a wl_subsurface for the same - * wl_surface again, but it is not allowed to use the wl_surface as - * a cursor (cursor is a different role than sub-surface, and role - * switching is not allowed). - * @section page_iface_wl_surface_api API - * See @ref iface_wl_surface. - */ -/** - * @defgroup iface_wl_surface The wl_surface interface - * - * A surface is a rectangular area that may be displayed on zero - * or more outputs, and shown any number of times at the compositor's - * discretion. They can present wl_buffers, receive user input, and - * define a local coordinate system. - * - * The size of a surface (and relative positions on it) is described - * in surface-local coordinates, which may differ from the buffer - * coordinates of the pixel content, in case a buffer_transform - * or a buffer_scale is used. - * - * A surface without a "role" is fairly useless: a compositor does - * not know where, when or how to present it. The role is the - * purpose of a wl_surface. Examples of roles are a cursor for a - * pointer (as set by wl_pointer.set_cursor), a drag icon - * (wl_data_device.start_drag), a sub-surface - * (wl_subcompositor.get_subsurface), and a window as defined by a - * shell protocol (e.g. wl_shell.get_shell_surface). - * - * A surface can have only one role at a time. Initially a - * wl_surface does not have a role. Once a wl_surface is given a - * role, it is set permanently for the whole lifetime of the - * wl_surface object. Giving the current role again is allowed, - * unless explicitly forbidden by the relevant interface - * specification. - * - * Surface roles are given by requests in other interfaces such as - * wl_pointer.set_cursor. The request should explicitly mention - * that this request gives a role to a wl_surface. Often, this - * request also creates a new protocol object that represents the - * role and adds additional functionality to wl_surface. When a - * client wants to destroy a wl_surface, they must destroy this role - * object before the wl_surface, otherwise a defunct_role_object error is - * sent. - * - * Destroying the role object does not remove the role from the - * wl_surface, but it may stop the wl_surface from "playing the role". - * For instance, if a wl_subsurface object is destroyed, the wl_surface - * it was created for will be unmapped and forget its position and - * z-order. It is allowed to create a wl_subsurface for the same - * wl_surface again, but it is not allowed to use the wl_surface as - * a cursor (cursor is a different role than sub-surface, and role - * switching is not allowed). - */ extern const struct wl_interface wl_surface_interface; #endif #ifndef WL_SEAT_INTERFACE #define WL_SEAT_INTERFACE -/** - * @page page_iface_wl_seat wl_seat - * @section page_iface_wl_seat_desc Description - * - * A seat is a group of keyboards, pointer and touch devices. This - * object is published as a global during start up, or when such a - * device is hot plugged. A seat typically has a pointer and - * maintains a keyboard focus and a pointer focus. - * @section page_iface_wl_seat_api API - * See @ref iface_wl_seat. - */ -/** - * @defgroup iface_wl_seat The wl_seat interface - * - * A seat is a group of keyboards, pointer and touch devices. This - * object is published as a global during start up, or when such a - * device is hot plugged. A seat typically has a pointer and - * maintains a keyboard focus and a pointer focus. - */ extern const struct wl_interface wl_seat_interface; #endif #ifndef WL_POINTER_INTERFACE #define WL_POINTER_INTERFACE -/** - * @page page_iface_wl_pointer wl_pointer - * @section page_iface_wl_pointer_desc Description - * - * The wl_pointer interface represents one or more input devices, - * such as mice, which control the pointer location and pointer_focus - * of a seat. - * - * The wl_pointer interface generates motion, enter and leave - * events for the surfaces that the pointer is located over, - * and button and axis events for button presses, button releases - * and scrolling. - * @section page_iface_wl_pointer_api API - * See @ref iface_wl_pointer. - */ -/** - * @defgroup iface_wl_pointer The wl_pointer interface - * - * The wl_pointer interface represents one or more input devices, - * such as mice, which control the pointer location and pointer_focus - * of a seat. - * - * The wl_pointer interface generates motion, enter and leave - * events for the surfaces that the pointer is located over, - * and button and axis events for button presses, button releases - * and scrolling. - */ extern const struct wl_interface wl_pointer_interface; #endif #ifndef WL_KEYBOARD_INTERFACE #define WL_KEYBOARD_INTERFACE -/** - * @page page_iface_wl_keyboard wl_keyboard - * @section page_iface_wl_keyboard_desc Description - * - * The wl_keyboard interface represents one or more keyboards - * associated with a seat. - * - * Each wl_keyboard has the following logical state: - * - * - an active surface (possibly null), - * - the keys currently logically down, - * - the active modifiers, - * - the active group. - * - * By default, the active surface is null, the keys currently logically down - * are empty, the active modifiers and the active group are 0. - * @section page_iface_wl_keyboard_api API - * See @ref iface_wl_keyboard. - */ -/** - * @defgroup iface_wl_keyboard The wl_keyboard interface - * - * The wl_keyboard interface represents one or more keyboards - * associated with a seat. - * - * Each wl_keyboard has the following logical state: - * - * - an active surface (possibly null), - * - the keys currently logically down, - * - the active modifiers, - * - the active group. - * - * By default, the active surface is null, the keys currently logically down - * are empty, the active modifiers and the active group are 0. - */ extern const struct wl_interface wl_keyboard_interface; #endif #ifndef WL_TOUCH_INTERFACE #define WL_TOUCH_INTERFACE -/** - * @page page_iface_wl_touch wl_touch - * @section page_iface_wl_touch_desc Description - * - * The wl_touch interface represents a touchscreen - * associated with a seat. - * - * Touch interactions can consist of one or more contacts. - * For each contact, a series of events is generated, starting - * with a down event, followed by zero or more motion events, - * and ending with an up event. Events relating to the same - * contact point can be identified by the ID of the sequence. - * @section page_iface_wl_touch_api API - * See @ref iface_wl_touch. - */ -/** - * @defgroup iface_wl_touch The wl_touch interface - * - * The wl_touch interface represents a touchscreen - * associated with a seat. - * - * Touch interactions can consist of one or more contacts. - * For each contact, a series of events is generated, starting - * with a down event, followed by zero or more motion events, - * and ending with an up event. Events relating to the same - * contact point can be identified by the ID of the sequence. - */ extern const struct wl_interface wl_touch_interface; #endif #ifndef WL_OUTPUT_INTERFACE #define WL_OUTPUT_INTERFACE -/** - * @page page_iface_wl_output wl_output - * @section page_iface_wl_output_desc Description - * - * An output describes part of the compositor geometry. The - * compositor works in the 'compositor coordinate system' and an - * output corresponds to a rectangular area in that space that is - * actually visible. This typically corresponds to a monitor that - * displays part of the compositor space. This object is published - * as global during start up, or when a monitor is hotplugged. - * @section page_iface_wl_output_api API - * See @ref iface_wl_output. - */ -/** - * @defgroup iface_wl_output The wl_output interface - * - * An output describes part of the compositor geometry. The - * compositor works in the 'compositor coordinate system' and an - * output corresponds to a rectangular area in that space that is - * actually visible. This typically corresponds to a monitor that - * displays part of the compositor space. This object is published - * as global during start up, or when a monitor is hotplugged. - */ extern const struct wl_interface wl_output_interface; #endif #ifndef WL_REGION_INTERFACE #define WL_REGION_INTERFACE -/** - * @page page_iface_wl_region wl_region - * @section page_iface_wl_region_desc Description - * - * A region object describes an area. - * - * Region objects are used to describe the opaque and input - * regions of a surface. - * @section page_iface_wl_region_api API - * See @ref iface_wl_region. - */ -/** - * @defgroup iface_wl_region The wl_region interface - * - * A region object describes an area. - * - * Region objects are used to describe the opaque and input - * regions of a surface. - */ extern const struct wl_interface wl_region_interface; #endif #ifndef WL_SUBCOMPOSITOR_INTERFACE #define WL_SUBCOMPOSITOR_INTERFACE -/** - * @page page_iface_wl_subcompositor wl_subcompositor - * @section page_iface_wl_subcompositor_desc Description - * - * The global interface exposing sub-surface compositing capabilities. - * A wl_surface, that has sub-surfaces associated, is called the - * parent surface. Sub-surfaces can be arbitrarily nested and create - * a tree of sub-surfaces. - * - * The root surface in a tree of sub-surfaces is the main - * surface. The main surface cannot be a sub-surface, because - * sub-surfaces must always have a parent. - * - * A main surface with its sub-surfaces forms a (compound) window. - * For window management purposes, this set of wl_surface objects is - * to be considered as a single window, and it should also behave as - * such. - * - * The aim of sub-surfaces is to offload some of the compositing work - * within a window from clients to the compositor. A prime example is - * a video player with decorations and video in separate wl_surface - * objects. This should allow the compositor to pass YUV video buffer - * processing to dedicated overlay hardware when possible. - * @section page_iface_wl_subcompositor_api API - * See @ref iface_wl_subcompositor. - */ -/** - * @defgroup iface_wl_subcompositor The wl_subcompositor interface - * - * The global interface exposing sub-surface compositing capabilities. - * A wl_surface, that has sub-surfaces associated, is called the - * parent surface. Sub-surfaces can be arbitrarily nested and create - * a tree of sub-surfaces. - * - * The root surface in a tree of sub-surfaces is the main - * surface. The main surface cannot be a sub-surface, because - * sub-surfaces must always have a parent. - * - * A main surface with its sub-surfaces forms a (compound) window. - * For window management purposes, this set of wl_surface objects is - * to be considered as a single window, and it should also behave as - * such. - * - * The aim of sub-surfaces is to offload some of the compositing work - * within a window from clients to the compositor. A prime example is - * a video player with decorations and video in separate wl_surface - * objects. This should allow the compositor to pass YUV video buffer - * processing to dedicated overlay hardware when possible. - */ extern const struct wl_interface wl_subcompositor_interface; #endif #ifndef WL_SUBSURFACE_INTERFACE #define WL_SUBSURFACE_INTERFACE -/** - * @page page_iface_wl_subsurface wl_subsurface - * @section page_iface_wl_subsurface_desc Description - * - * An additional interface to a wl_surface object, which has been - * made a sub-surface. A sub-surface has one parent surface. A - * sub-surface's size and position are not limited to that of the parent. - * Particularly, a sub-surface is not automatically clipped to its - * parent's area. - * - * A sub-surface becomes mapped, when a non-NULL wl_buffer is applied - * and the parent surface is mapped. The order of which one happens - * first is irrelevant. A sub-surface is hidden if the parent becomes - * hidden, or if a NULL wl_buffer is applied. These rules apply - * recursively through the tree of surfaces. - * - * A sub-surface can be in one of two modes. The possible modes are - * synchronized and desynchronized, see methods wl_subsurface.set_sync and - * wl_subsurface.set_desync. - * - * The main surface can be thought to be always in desynchronized mode, - * since it does not have a parent in the sub-surfaces sense. - * - * Even if a sub-surface is in desynchronized mode, it will behave as - * in synchronized mode, if its parent surface behaves as in - * synchronized mode. This rule is applied recursively throughout the - * tree of surfaces. This means, that one can set a sub-surface into - * synchronized mode, and then assume that all its child and grand-child - * sub-surfaces are synchronized, too, without explicitly setting them. - * - * If a surface behaves as in synchronized mode, it is effectively - * synchronized, otherwise it is effectively desynchronized. - * - * A sub-surface is initially in the synchronized mode. - * - * The wl_subsurface interface has requests which modify double-buffered - * state of the parent surface (wl_subsurface.set_position, .place_above and - * .place_below). - * - * Destroying a sub-surface takes effect immediately. If you need to - * synchronize the removal of a sub-surface to the parent surface update, - * unmap the sub-surface first by attaching a NULL wl_buffer, update parent, - * and then destroy the sub-surface. - * - * If the parent wl_surface object is destroyed, the sub-surface is - * unmapped. - * - * A sub-surface never has the keyboard focus of any seat. - * - * The wl_surface.offset request is ignored: clients must use set_position - * instead to move the sub-surface. - * @section page_iface_wl_subsurface_api API - * See @ref iface_wl_subsurface. - */ -/** - * @defgroup iface_wl_subsurface The wl_subsurface interface - * - * An additional interface to a wl_surface object, which has been - * made a sub-surface. A sub-surface has one parent surface. A - * sub-surface's size and position are not limited to that of the parent. - * Particularly, a sub-surface is not automatically clipped to its - * parent's area. - * - * A sub-surface becomes mapped, when a non-NULL wl_buffer is applied - * and the parent surface is mapped. The order of which one happens - * first is irrelevant. A sub-surface is hidden if the parent becomes - * hidden, or if a NULL wl_buffer is applied. These rules apply - * recursively through the tree of surfaces. - * - * A sub-surface can be in one of two modes. The possible modes are - * synchronized and desynchronized, see methods wl_subsurface.set_sync and - * wl_subsurface.set_desync. - * - * The main surface can be thought to be always in desynchronized mode, - * since it does not have a parent in the sub-surfaces sense. - * - * Even if a sub-surface is in desynchronized mode, it will behave as - * in synchronized mode, if its parent surface behaves as in - * synchronized mode. This rule is applied recursively throughout the - * tree of surfaces. This means, that one can set a sub-surface into - * synchronized mode, and then assume that all its child and grand-child - * sub-surfaces are synchronized, too, without explicitly setting them. - * - * If a surface behaves as in synchronized mode, it is effectively - * synchronized, otherwise it is effectively desynchronized. - * - * A sub-surface is initially in the synchronized mode. - * - * The wl_subsurface interface has requests which modify double-buffered - * state of the parent surface (wl_subsurface.set_position, .place_above and - * .place_below). - * - * Destroying a sub-surface takes effect immediately. If you need to - * synchronize the removal of a sub-surface to the parent surface update, - * unmap the sub-surface first by attaching a NULL wl_buffer, update parent, - * and then destroy the sub-surface. - * - * If the parent wl_surface object is destroyed, the sub-surface is - * unmapped. - * - * A sub-surface never has the keyboard focus of any seat. - * - * The wl_surface.offset request is ignored: clients must use set_position - * instead to move the sub-surface. - */ extern const struct wl_interface wl_subsurface_interface; #endif #ifndef WL_FIXES_INTERFACE #define WL_FIXES_INTERFACE -/** - * @page page_iface_wl_fixes wl_fixes - * @section page_iface_wl_fixes_desc Description - * - * This global fixes problems with other core-protocol interfaces that - * cannot be fixed in these interfaces themselves. - * @section page_iface_wl_fixes_api API - * See @ref iface_wl_fixes. - */ -/** - * @defgroup iface_wl_fixes The wl_fixes interface - * - * This global fixes problems with other core-protocol interfaces that - * cannot be fixed in these interfaces themselves. - */ extern const struct wl_interface wl_fixes_interface; #endif #ifndef WL_DISPLAY_ERROR_ENUM #define WL_DISPLAY_ERROR_ENUM -/** - * @ingroup iface_wl_display - * global error values - * - * These errors are global and can be emitted in response to any - * server request. - */ enum wl_display_error { - /** - * server couldn't find object - */ WL_DISPLAY_ERROR_INVALID_OBJECT = 0, - /** - * method doesn't exist on the specified interface or malformed request - */ WL_DISPLAY_ERROR_INVALID_METHOD = 1, - /** - * server is out of memory - */ WL_DISPLAY_ERROR_NO_MEMORY = 2, - /** - * implementation error in compositor - */ WL_DISPLAY_ERROR_IMPLEMENTATION = 3, }; -#endif /* WL_DISPLAY_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_display - * @struct wl_display_listener - */ struct wl_display_listener { - /** - * fatal error event - * - * The error event is sent out when a fatal (non-recoverable) - * error has occurred. The object_id argument is the object where - * the error occurred, most often in response to a request to that - * object. The code identifies the error and is defined by the - * object interface. As such, each interface defines its own set of - * error codes. The message is a brief description of the error, - * for (debugging) convenience. - * @param object_id object where the error occurred - * @param code error code - * @param message error description - */ void (*error)(void *data, struct wl_display *wl_display, void *object_id, uint32_t code, const char *message); - /** - * acknowledge object ID deletion - * - * This event is used internally by the object ID management - * logic. - * - * When the server stops using an object created by the client, the - * server sends this event. In particular, after sending this - * event, the server will no longer send any events that contain - * the object as the receiver or as an argument. - * - * When the client receives this event, it knows that it can reuse - * the object ID. - * @param id deleted object ID - */ void (*delete_id)(void *data, struct wl_display *wl_display, uint32_t id); }; -/** - * @ingroup iface_wl_display - */ static inline int wl_display_add_listener(struct wl_display *wl_display, const struct wl_display_listener *listener, void *data) @@ -1039,32 +214,18 @@ wl_display_add_listener(struct wl_display *wl_display, #define WL_DISPLAY_SYNC 0 #define WL_DISPLAY_GET_REGISTRY 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_ERROR_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_DELETE_ID_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_SYNC_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_GET_REGISTRY_SINCE_VERSION 1 -/** @ingroup iface_wl_display */ static inline void wl_display_set_user_data(struct wl_display *wl_display, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_display, user_data); } -/** @ingroup iface_wl_display */ static inline void * wl_display_get_user_data(struct wl_display *wl_display) { @@ -1077,21 +238,6 @@ wl_display_get_version(struct wl_display *wl_display) return wl_proxy_get_version((struct wl_proxy *) wl_display); } -/** - * @ingroup iface_wl_display - * - * The sync request asks the server to emit the 'done' event - * on the returned wl_callback object. Since requests are - * handled in-order and events are delivered in-order, this can - * be used as a barrier to ensure all previous requests and the - * resulting events have been handled. - * - * The object returned by this request will be destroyed by the - * compositor after the callback is fired and as such the client must not - * attempt to use it after that point. - * - * The callback_data passed in the callback is undefined and should be ignored. - */ static inline struct wl_callback * wl_display_sync(struct wl_display *wl_display) { @@ -1103,19 +249,6 @@ wl_display_sync(struct wl_display *wl_display) return (struct wl_callback *) callback; } -/** - * @ingroup iface_wl_display - * - * This request creates a registry object that allows the client - * to list and bind the global objects available from the - * compositor. - * - * It should be noted that the server side resources consumed in - * response to a get_registry request can only be released when the - * client disconnects, not when the client side proxy is destroyed. - * Therefore, clients should invoke get_registry as infrequently as - * possible to avoid wasting memory. - */ static inline struct wl_registry * wl_display_get_registry(struct wl_display *wl_display) { @@ -1127,51 +260,17 @@ wl_display_get_registry(struct wl_display *wl_display) return (struct wl_registry *) registry; } -/** - * @ingroup iface_wl_registry - * @struct wl_registry_listener - */ struct wl_registry_listener { - /** - * announce global object - * - * Notify the client of global objects. - * - * The event notifies the client that a global object with the - * given name is now available, and it implements the given version - * of the given interface. - * @param name numeric name of the global object - * @param interface interface implemented by the object - * @param version interface version - */ void (*global)(void *data, struct wl_registry *wl_registry, uint32_t name, const char *interface, uint32_t version); - /** - * announce removal of global object - * - * Notify the client of removed global objects. - * - * This event notifies the client that the global identified by - * name is no longer available. If the client bound to the global - * using the bind request, the client should now destroy that - * object. - * - * The object remains valid and requests to the object will be - * ignored until the client destroys it, to avoid races between the - * global going away and a client sending a request to it. - * @param name numeric name of the global object - */ void (*global_remove)(void *data, struct wl_registry *wl_registry, uint32_t name); }; -/** - * @ingroup iface_wl_registry - */ static inline int wl_registry_add_listener(struct wl_registry *wl_registry, const struct wl_registry_listener *listener, void *data) @@ -1182,28 +281,17 @@ wl_registry_add_listener(struct wl_registry *wl_registry, #define WL_REGISTRY_BIND 0 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_GLOBAL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_GLOBAL_REMOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_BIND_SINCE_VERSION 1 -/** @ingroup iface_wl_registry */ static inline void wl_registry_set_user_data(struct wl_registry *wl_registry, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_registry, user_data); } -/** @ingroup iface_wl_registry */ static inline void * wl_registry_get_user_data(struct wl_registry *wl_registry) { @@ -1216,19 +304,12 @@ wl_registry_get_version(struct wl_registry *wl_registry) return wl_proxy_get_version((struct wl_proxy *) wl_registry); } -/** @ingroup iface_wl_registry */ static inline void wl_registry_destroy(struct wl_registry *wl_registry) { wl_proxy_destroy((struct wl_proxy *) wl_registry); } -/** - * @ingroup iface_wl_registry - * - * Binds a new, client-created object to the server using the - * specified name as the identifier. - */ static inline void * wl_registry_bind(struct wl_registry *wl_registry, uint32_t name, const struct wl_interface *interface, uint32_t version) { @@ -1240,25 +321,12 @@ wl_registry_bind(struct wl_registry *wl_registry, uint32_t name, const struct wl return (void *) id; } -/** - * @ingroup iface_wl_callback - * @struct wl_callback_listener - */ struct wl_callback_listener { - /** - * done event - * - * Notify the client when the related request is done. - * @param callback_data request-specific data for the callback - */ void (*done)(void *data, struct wl_callback *wl_callback, uint32_t callback_data); }; -/** - * @ingroup iface_wl_callback - */ static inline int wl_callback_add_listener(struct wl_callback *wl_callback, const struct wl_callback_listener *listener, void *data) @@ -1267,20 +335,15 @@ wl_callback_add_listener(struct wl_callback *wl_callback, (void (**)(void)) listener, data); } -/** - * @ingroup iface_wl_callback - */ #define WL_CALLBACK_DONE_SINCE_VERSION 1 -/** @ingroup iface_wl_callback */ static inline void wl_callback_set_user_data(struct wl_callback *wl_callback, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_callback, user_data); } -/** @ingroup iface_wl_callback */ static inline void * wl_callback_get_user_data(struct wl_callback *wl_callback) { @@ -1293,7 +356,6 @@ wl_callback_get_version(struct wl_callback *wl_callback) return wl_proxy_get_version((struct wl_proxy *) wl_callback); } -/** @ingroup iface_wl_callback */ static inline void wl_callback_destroy(struct wl_callback *wl_callback) { @@ -1305,27 +367,16 @@ wl_callback_destroy(struct wl_callback *wl_callback) #define WL_COMPOSITOR_RELEASE 2 -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_CREATE_SURFACE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_CREATE_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_RELEASE_SINCE_VERSION 7 -/** @ingroup iface_wl_compositor */ static inline void wl_compositor_set_user_data(struct wl_compositor *wl_compositor, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_compositor, user_data); } -/** @ingroup iface_wl_compositor */ static inline void * wl_compositor_get_user_data(struct wl_compositor *wl_compositor) { @@ -1338,18 +389,12 @@ wl_compositor_get_version(struct wl_compositor *wl_compositor) return wl_proxy_get_version((struct wl_proxy *) wl_compositor); } -/** @ingroup iface_wl_compositor */ static inline void wl_compositor_destroy(struct wl_compositor *wl_compositor) { wl_proxy_destroy((struct wl_proxy *) wl_compositor); } -/** - * @ingroup iface_wl_compositor - * - * Ask the compositor to create a new surface. - */ static inline struct wl_surface * wl_compositor_create_surface(struct wl_compositor *wl_compositor) { @@ -1361,11 +406,6 @@ wl_compositor_create_surface(struct wl_compositor *wl_compositor) return (struct wl_surface *) id; } -/** - * @ingroup iface_wl_compositor - * - * Ask the compositor to create a new region. - */ static inline struct wl_region * wl_compositor_create_region(struct wl_compositor *wl_compositor) { @@ -1377,11 +417,6 @@ wl_compositor_create_region(struct wl_compositor *wl_compositor) return (struct wl_region *) id; } -/** - * @ingroup iface_wl_compositor - * - * This request destroys the wl_compositor. This has no effect on any other objects. - */ static inline void wl_compositor_release(struct wl_compositor *wl_compositor) { @@ -1391,50 +426,27 @@ wl_compositor_release(struct wl_compositor *wl_compositor) #ifndef WL_SHM_POOL_ERROR_ENUM #define WL_SHM_POOL_ERROR_ENUM -/** - * @ingroup iface_wl_shm_pool - * wl_shm_pool error values - * - * These errors can be emitted in response to wl_shm_pool requests. - */ enum wl_shm_pool_error { - /** - * buffer format is not known - */ WL_SHM_POOL_ERROR_INVALID_FORMAT = 0, - /** - * invalid size or stride during buffer creation - */ WL_SHM_POOL_ERROR_INVALID_STRIDE = 1, }; -#endif /* WL_SHM_POOL_ERROR_ENUM */ +#endif #define WL_SHM_POOL_CREATE_BUFFER 0 #define WL_SHM_POOL_DESTROY 1 #define WL_SHM_POOL_RESIZE 2 -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_CREATE_BUFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_RESIZE_SINCE_VERSION 1 -/** @ingroup iface_wl_shm_pool */ static inline void wl_shm_pool_set_user_data(struct wl_shm_pool *wl_shm_pool, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_shm_pool, user_data); } -/** @ingroup iface_wl_shm_pool */ static inline void * wl_shm_pool_get_user_data(struct wl_shm_pool *wl_shm_pool) { @@ -1447,21 +459,6 @@ wl_shm_pool_get_version(struct wl_shm_pool *wl_shm_pool) return wl_proxy_get_version((struct wl_proxy *) wl_shm_pool); } -/** - * @ingroup iface_wl_shm_pool - * - * Create a wl_buffer object from the pool. - * - * The buffer is created offset bytes into the pool and has - * width and height as specified. The stride argument specifies - * the number of bytes from the beginning of one row to the beginning - * of the next. The format is the pixel format of the buffer and - * must be one of those advertised through the wl_shm.format event. - * - * A buffer will keep a reference to the pool it was created from - * so it is valid to destroy the pool immediately after creating - * a buffer from it. - */ static inline struct wl_buffer * wl_shm_pool_create_buffer(struct wl_shm_pool *wl_shm_pool, int32_t offset, int32_t width, int32_t height, int32_t stride, uint32_t format) { @@ -1473,15 +470,6 @@ wl_shm_pool_create_buffer(struct wl_shm_pool *wl_shm_pool, int32_t offset, int32 return (struct wl_buffer *) id; } -/** - * @ingroup iface_wl_shm_pool - * - * Destroy the shared memory pool. - * - * The mmapped memory will be released when all - * buffers that have been created from this pool - * are gone. - */ static inline void wl_shm_pool_destroy(struct wl_shm_pool *wl_shm_pool) { @@ -1489,20 +477,6 @@ wl_shm_pool_destroy(struct wl_shm_pool *wl_shm_pool) WL_SHM_POOL_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shm_pool), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_shm_pool - * - * This request will cause the server to remap the backing memory - * for the pool from the file descriptor passed when the pool was - * created, but using the new size. This request can only be - * used to make the pool bigger. - * - * This request only changes the amount of bytes that are mmapped - * by the server and does not touch the file corresponding to the - * file descriptor passed at creation time. It is the client's - * responsibility to ensure that the file is at least as big as - * the new pool size. - */ static inline void wl_shm_pool_resize(struct wl_shm_pool *wl_shm_pool, int32_t size) { @@ -1512,385 +486,99 @@ wl_shm_pool_resize(struct wl_shm_pool *wl_shm_pool, int32_t size) #ifndef WL_SHM_ERROR_ENUM #define WL_SHM_ERROR_ENUM -/** - * @ingroup iface_wl_shm - * wl_shm error values - * - * These errors can be emitted in response to wl_shm requests. - */ enum wl_shm_error { - /** - * buffer format is not known - * @deprecated Deprecated since version 3 - */ WL_SHM_ERROR_INVALID_FORMAT = 0, - /** - * invalid size or stride during pool creation - */ WL_SHM_ERROR_INVALID_STRIDE = 1, - /** - * mmapping the file descriptor failed - */ WL_SHM_ERROR_INVALID_FD = 2, }; -#endif /* WL_SHM_ERROR_ENUM */ +#endif #ifndef WL_SHM_FORMAT_ENUM #define WL_SHM_FORMAT_ENUM -/** - * @ingroup iface_wl_shm - * pixel formats - * - * This describes the memory layout of an individual pixel. - * - * All renderers should support argb8888 and xrgb8888 but any other - * formats are optional and may not be supported by the particular - * renderer in use. - * - * The drm format codes match the macros defined in drm_fourcc.h, except - * argb8888 and xrgb8888. The formats actually supported by the compositor - * will be reported by the format event. See drm_fourcc.h for more detailed - * format descriptions. - * - * For all wl_shm formats and unless specified in another protocol - * extension, pre-multiplied alpha is used for pixel values. - */ enum wl_shm_format { - /** - * 32-bit ARGB format, [31:0] A:R:G:B 8:8:8:8 little endian - */ WL_SHM_FORMAT_ARGB8888 = 0, - /** - * 32-bit RGB format, [31:0] x:R:G:B 8:8:8:8 little endian - */ WL_SHM_FORMAT_XRGB8888 = 1, - /** - * 8-bit color index format, [7:0] C - */ WL_SHM_FORMAT_C8 = 0x20203843, - /** - * 8-bit RGB format, [7:0] R:G:B 3:3:2 - */ WL_SHM_FORMAT_RGB332 = 0x38424752, - /** - * 8-bit BGR format, [7:0] B:G:R 2:3:3 - */ WL_SHM_FORMAT_BGR233 = 0x38524742, - /** - * 16-bit xRGB format, [15:0] x:R:G:B 4:4:4:4 little endian - */ WL_SHM_FORMAT_XRGB4444 = 0x32315258, - /** - * 16-bit xBGR format, [15:0] x:B:G:R 4:4:4:4 little endian - */ WL_SHM_FORMAT_XBGR4444 = 0x32314258, - /** - * 16-bit RGBx format, [15:0] R:G:B:x 4:4:4:4 little endian - */ WL_SHM_FORMAT_RGBX4444 = 0x32315852, - /** - * 16-bit BGRx format, [15:0] B:G:R:x 4:4:4:4 little endian - */ WL_SHM_FORMAT_BGRX4444 = 0x32315842, - /** - * 16-bit ARGB format, [15:0] A:R:G:B 4:4:4:4 little endian - */ WL_SHM_FORMAT_ARGB4444 = 0x32315241, - /** - * 16-bit ABGR format, [15:0] A:B:G:R 4:4:4:4 little endian - */ WL_SHM_FORMAT_ABGR4444 = 0x32314241, - /** - * 16-bit RGBA format, [15:0] R:G:B:A 4:4:4:4 little endian - */ WL_SHM_FORMAT_RGBA4444 = 0x32314152, - /** - * 16-bit BGRA format, [15:0] B:G:R:A 4:4:4:4 little endian - */ WL_SHM_FORMAT_BGRA4444 = 0x32314142, - /** - * 16-bit xRGB format, [15:0] x:R:G:B 1:5:5:5 little endian - */ WL_SHM_FORMAT_XRGB1555 = 0x35315258, - /** - * 16-bit xBGR 1555 format, [15:0] x:B:G:R 1:5:5:5 little endian - */ WL_SHM_FORMAT_XBGR1555 = 0x35314258, - /** - * 16-bit RGBx 5551 format, [15:0] R:G:B:x 5:5:5:1 little endian - */ WL_SHM_FORMAT_RGBX5551 = 0x35315852, - /** - * 16-bit BGRx 5551 format, [15:0] B:G:R:x 5:5:5:1 little endian - */ WL_SHM_FORMAT_BGRX5551 = 0x35315842, - /** - * 16-bit ARGB 1555 format, [15:0] A:R:G:B 1:5:5:5 little endian - */ WL_SHM_FORMAT_ARGB1555 = 0x35315241, - /** - * 16-bit ABGR 1555 format, [15:0] A:B:G:R 1:5:5:5 little endian - */ WL_SHM_FORMAT_ABGR1555 = 0x35314241, - /** - * 16-bit RGBA 5551 format, [15:0] R:G:B:A 5:5:5:1 little endian - */ WL_SHM_FORMAT_RGBA5551 = 0x35314152, - /** - * 16-bit BGRA 5551 format, [15:0] B:G:R:A 5:5:5:1 little endian - */ WL_SHM_FORMAT_BGRA5551 = 0x35314142, - /** - * 16-bit RGB 565 format, [15:0] R:G:B 5:6:5 little endian - */ WL_SHM_FORMAT_RGB565 = 0x36314752, - /** - * 16-bit BGR 565 format, [15:0] B:G:R 5:6:5 little endian - */ WL_SHM_FORMAT_BGR565 = 0x36314742, - /** - * 24-bit RGB format, [23:0] R:G:B little endian - */ WL_SHM_FORMAT_RGB888 = 0x34324752, - /** - * 24-bit BGR format, [23:0] B:G:R little endian - */ WL_SHM_FORMAT_BGR888 = 0x34324742, - /** - * 32-bit xBGR format, [31:0] x:B:G:R 8:8:8:8 little endian - */ WL_SHM_FORMAT_XBGR8888 = 0x34324258, - /** - * 32-bit RGBx format, [31:0] R:G:B:x 8:8:8:8 little endian - */ WL_SHM_FORMAT_RGBX8888 = 0x34325852, - /** - * 32-bit BGRx format, [31:0] B:G:R:x 8:8:8:8 little endian - */ WL_SHM_FORMAT_BGRX8888 = 0x34325842, - /** - * 32-bit ABGR format, [31:0] A:B:G:R 8:8:8:8 little endian - */ WL_SHM_FORMAT_ABGR8888 = 0x34324241, - /** - * 32-bit RGBA format, [31:0] R:G:B:A 8:8:8:8 little endian - */ WL_SHM_FORMAT_RGBA8888 = 0x34324152, - /** - * 32-bit BGRA format, [31:0] B:G:R:A 8:8:8:8 little endian - */ WL_SHM_FORMAT_BGRA8888 = 0x34324142, - /** - * 32-bit xRGB format, [31:0] x:R:G:B 2:10:10:10 little endian - */ WL_SHM_FORMAT_XRGB2101010 = 0x30335258, - /** - * 32-bit xBGR format, [31:0] x:B:G:R 2:10:10:10 little endian - */ WL_SHM_FORMAT_XBGR2101010 = 0x30334258, - /** - * 32-bit RGBx format, [31:0] R:G:B:x 10:10:10:2 little endian - */ WL_SHM_FORMAT_RGBX1010102 = 0x30335852, - /** - * 32-bit BGRx format, [31:0] B:G:R:x 10:10:10:2 little endian - */ WL_SHM_FORMAT_BGRX1010102 = 0x30335842, - /** - * 32-bit ARGB format, [31:0] A:R:G:B 2:10:10:10 little endian - */ WL_SHM_FORMAT_ARGB2101010 = 0x30335241, - /** - * 32-bit ABGR format, [31:0] A:B:G:R 2:10:10:10 little endian - */ WL_SHM_FORMAT_ABGR2101010 = 0x30334241, - /** - * 32-bit RGBA format, [31:0] R:G:B:A 10:10:10:2 little endian - */ WL_SHM_FORMAT_RGBA1010102 = 0x30334152, - /** - * 32-bit BGRA format, [31:0] B:G:R:A 10:10:10:2 little endian - */ WL_SHM_FORMAT_BGRA1010102 = 0x30334142, - /** - * packed YCbCr format, [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian - */ WL_SHM_FORMAT_YUYV = 0x56595559, - /** - * packed YCbCr format, [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian - */ WL_SHM_FORMAT_YVYU = 0x55595659, - /** - * packed YCbCr format, [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian - */ WL_SHM_FORMAT_UYVY = 0x59565955, - /** - * packed YCbCr format, [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian - */ WL_SHM_FORMAT_VYUY = 0x59555956, - /** - * packed AYCbCr format, [31:0] A:Y:Cb:Cr 8:8:8:8 little endian - */ WL_SHM_FORMAT_AYUV = 0x56555941, - /** - * 2 plane YCbCr Cr:Cb format, 2x2 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV12 = 0x3231564e, - /** - * 2 plane YCbCr Cb:Cr format, 2x2 subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV21 = 0x3132564e, - /** - * 2 plane YCbCr Cr:Cb format, 2x1 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV16 = 0x3631564e, - /** - * 2 plane YCbCr Cb:Cr format, 2x1 subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV61 = 0x3136564e, - /** - * 3 plane YCbCr format, 4x4 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV410 = 0x39565559, - /** - * 3 plane YCbCr format, 4x4 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU410 = 0x39555659, - /** - * 3 plane YCbCr format, 4x1 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV411 = 0x31315559, - /** - * 3 plane YCbCr format, 4x1 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU411 = 0x31315659, - /** - * 3 plane YCbCr format, 2x2 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV420 = 0x32315559, - /** - * 3 plane YCbCr format, 2x2 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU420 = 0x32315659, - /** - * 3 plane YCbCr format, 2x1 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV422 = 0x36315559, - /** - * 3 plane YCbCr format, 2x1 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU422 = 0x36315659, - /** - * 3 plane YCbCr format, non-subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV444 = 0x34325559, - /** - * 3 plane YCbCr format, non-subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU444 = 0x34325659, - /** - * [7:0] R - */ WL_SHM_FORMAT_R8 = 0x20203852, - /** - * [15:0] R little endian - */ WL_SHM_FORMAT_R16 = 0x20363152, - /** - * [15:0] R:G 8:8 little endian - */ WL_SHM_FORMAT_RG88 = 0x38384752, - /** - * [15:0] G:R 8:8 little endian - */ WL_SHM_FORMAT_GR88 = 0x38385247, - /** - * [31:0] R:G 16:16 little endian - */ WL_SHM_FORMAT_RG1616 = 0x32334752, - /** - * [31:0] G:R 16:16 little endian - */ WL_SHM_FORMAT_GR1616 = 0x32335247, - /** - * [63:0] x:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_XRGB16161616F = 0x48345258, - /** - * [63:0] x:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_XBGR16161616F = 0x48344258, - /** - * [63:0] A:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_ARGB16161616F = 0x48345241, - /** - * [63:0] A:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_ABGR16161616F = 0x48344241, - /** - * [31:0] X:Y:Cb:Cr 8:8:8:8 little endian - */ WL_SHM_FORMAT_XYUV8888 = 0x56555958, - /** - * [23:0] Cr:Cb:Y 8:8:8 little endian - */ WL_SHM_FORMAT_VUY888 = 0x34325556, - /** - * Y followed by U then V, 10:10:10. Non-linear modifier only - */ WL_SHM_FORMAT_VUY101010 = 0x30335556, - /** - * [63:0] Cr0:0:Y1:0:Cb0:0:Y0:0 10:6:10:6:10:6:10:6 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y210 = 0x30313259, - /** - * [63:0] Cr0:0:Y1:0:Cb0:0:Y0:0 12:4:12:4:12:4:12:4 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y212 = 0x32313259, - /** - * [63:0] Cr0:Y1:Cb0:Y0 16:16:16:16 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y216 = 0x36313259, - /** - * [31:0] A:Cr:Y:Cb 2:10:10:10 little endian - */ WL_SHM_FORMAT_Y410 = 0x30313459, - /** - * [63:0] A:0:Cr:0:Y:0:Cb:0 12:4:12:4:12:4:12:4 little endian - */ WL_SHM_FORMAT_Y412 = 0x32313459, - /** - * [63:0] A:Cr:Y:Cb 16:16:16:16 little endian - */ WL_SHM_FORMAT_Y416 = 0x36313459, - /** - * [31:0] X:Cr:Y:Cb 2:10:10:10 little endian - */ WL_SHM_FORMAT_XVYU2101010 = 0x30335658, - /** - * [63:0] X:0:Cr:0:Y:0:Cb:0 12:4:12:4:12:4:12:4 little endian - */ WL_SHM_FORMAT_XVYU12_16161616 = 0x36335658, - /** - * [63:0] X:Cr:Y:Cb 16:16:16:16 little endian - */ WL_SHM_FORMAT_XVYU16161616 = 0x38345658, - /** - * [63:0] A3:A2:Y3:0:Cr0:0:Y2:0:A1:A0:Y1:0:Cb0:0:Y0:0 1:1:8:2:8:2:8:2:1:1:8:2:8:2:8:2 little endian - */ WL_SHM_FORMAT_Y0L0 = 0x304c3059, - /** - * [63:0] X3:X2:Y3:0:Cr0:0:Y2:0:X1:X0:Y1:0:Cb0:0:Y0:0 1:1:8:2:8:2:8:2:1:1:8:2:8:2:8:2 little endian - */ WL_SHM_FORMAT_X0L0 = 0x304c3058, - /** - * [63:0] A3:A2:Y3:Cr0:Y2:A1:A0:Y1:Cb0:Y0 1:1:10:10:10:1:1:10:10:10 little endian - */ WL_SHM_FORMAT_Y0L2 = 0x324c3059, - /** - * [63:0] X3:X2:Y3:Cr0:Y2:X1:X0:Y1:Cb0:Y0 1:1:10:10:10:1:1:10:10:10 little endian - */ WL_SHM_FORMAT_X0L2 = 0x324c3058, WL_SHM_FORMAT_YUV420_8BIT = 0x38305559, WL_SHM_FORMAT_YUV420_10BIT = 0x30315559, @@ -1902,241 +590,69 @@ enum wl_shm_format { WL_SHM_FORMAT_BGR888_A8 = 0x38413842, WL_SHM_FORMAT_RGB565_A8 = 0x38413552, WL_SHM_FORMAT_BGR565_A8 = 0x38413542, - /** - * non-subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV24 = 0x3432564e, - /** - * non-subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV42 = 0x3234564e, - /** - * 2x1 subsampled Cr:Cb plane, 10 bit per channel - */ WL_SHM_FORMAT_P210 = 0x30313250, - /** - * 2x2 subsampled Cr:Cb plane 10 bits per channel - */ WL_SHM_FORMAT_P010 = 0x30313050, - /** - * 2x2 subsampled Cr:Cb plane 12 bits per channel - */ WL_SHM_FORMAT_P012 = 0x32313050, - /** - * 2x2 subsampled Cr:Cb plane 16 bits per channel - */ WL_SHM_FORMAT_P016 = 0x36313050, - /** - * [63:0] A:x:B:x:G:x:R:x 10:6:10:6:10:6:10:6 little endian - */ WL_SHM_FORMAT_AXBXGXRX106106106106 = 0x30314241, - /** - * 2x2 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV15 = 0x3531564e, WL_SHM_FORMAT_Q410 = 0x30313451, WL_SHM_FORMAT_Q401 = 0x31303451, - /** - * [63:0] x:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_XRGB16161616 = 0x38345258, - /** - * [63:0] x:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_XBGR16161616 = 0x38344258, - /** - * [63:0] A:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_ARGB16161616 = 0x38345241, - /** - * [63:0] A:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_ABGR16161616 = 0x38344241, - /** - * [7:0] C0:C1:C2:C3:C4:C5:C6:C7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_C1 = 0x20203143, - /** - * [7:0] C0:C1:C2:C3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_C2 = 0x20203243, - /** - * [7:0] C0:C1 4:4 two pixels/byte - */ WL_SHM_FORMAT_C4 = 0x20203443, - /** - * [7:0] D0:D1:D2:D3:D4:D5:D6:D7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_D1 = 0x20203144, - /** - * [7:0] D0:D1:D2:D3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_D2 = 0x20203244, - /** - * [7:0] D0:D1 4:4 two pixels/byte - */ WL_SHM_FORMAT_D4 = 0x20203444, - /** - * [7:0] D - */ WL_SHM_FORMAT_D8 = 0x20203844, - /** - * [7:0] R0:R1:R2:R3:R4:R5:R6:R7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_R1 = 0x20203152, - /** - * [7:0] R0:R1:R2:R3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_R2 = 0x20203252, - /** - * [7:0] R0:R1 4:4 two pixels/byte - */ WL_SHM_FORMAT_R4 = 0x20203452, - /** - * [15:0] x:R 6:10 little endian - */ WL_SHM_FORMAT_R10 = 0x20303152, - /** - * [15:0] x:R 4:12 little endian - */ WL_SHM_FORMAT_R12 = 0x20323152, - /** - * [31:0] A:Cr:Cb:Y 8:8:8:8 little endian - */ WL_SHM_FORMAT_AVUY8888 = 0x59555641, - /** - * [31:0] X:Cr:Cb:Y 8:8:8:8 little endian - */ WL_SHM_FORMAT_XVUY8888 = 0x59555658, - /** - * 2x2 subsampled Cr:Cb plane 10 bits per channel packed - */ WL_SHM_FORMAT_P030 = 0x30333050, - /** - * [47:0] R:G:B 16:16:16 little endian - */ WL_SHM_FORMAT_RGB161616 = 0x38344752, - /** - * [47:0] B:G:R 16:16:16 little endian - */ WL_SHM_FORMAT_BGR161616 = 0x38344742, - /** - * [15:0] R 16 little endian - */ WL_SHM_FORMAT_R16F = 0x48202052, - /** - * [31:0] G:R 16:16 little endian - */ WL_SHM_FORMAT_GR1616F = 0x48205247, - /** - * [47:0] B:G:R 16:16:16 little endian - */ WL_SHM_FORMAT_BGR161616F = 0x48524742, - /** - * [31:0] R 32 little endian - */ WL_SHM_FORMAT_R32F = 0x46202052, - /** - * [63:0] G:R 32:32 little endian - */ WL_SHM_FORMAT_GR3232F = 0x46205247, - /** - * [95:0] B:G:R 32:32:32 little endian - */ WL_SHM_FORMAT_BGR323232F = 0x46524742, - /** - * [127:0] A:B:G:R 32:32:32:32 little endian - */ WL_SHM_FORMAT_ABGR32323232F = 0x46384241, - /** - * 2x1 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV20 = 0x3032564e, - /** - * non-subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV30 = 0x3033564e, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S010 = 0x30313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S210 = 0x30313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S410 = 0x30313453, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S012 = 0x32313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S212 = 0x32313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S412 = 0x32313453, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S016 = 0x36313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S216 = 0x36313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S416 = 0x36313453, - /** - * [31:0] x:Cr:Cb:Y 2:10:10:10 little endian - */ WL_SHM_FORMAT_XVUY2101010 = 0x30335958, - /** - * 2x1 subsampled Cr:Cb plane 10 bits per channel packed - */ WL_SHM_FORMAT_P230 = 0x30333250, WL_SHM_FORMAT_T430 = 0x30333454, - /** - * 8-bit Y-only - */ WL_SHM_FORMAT_Y8 = 0x59455247, - /** - * [31:0] x:Y2:Y1:Y0 2:10:10:10 little endian - */ WL_SHM_FORMAT_XYYY2101010 = 0x34415059, }; -#endif /* WL_SHM_FORMAT_ENUM */ +#endif -/** - * @ingroup iface_wl_shm - * @struct wl_shm_listener - */ struct wl_shm_listener { - /** - * pixel format description - * - * Informs the client about a valid pixel format that can be used - * for buffers. Known formats include argb8888 and xrgb8888. - * - * Extensions to drm_fourcc.h (or the format enum) do not require - * increasing the wl_shm version; as a result, clients may receive - * format codes which were not in the list at the time the client - * was made. - * @param format buffer pixel format - */ void (*format)(void *data, struct wl_shm *wl_shm, uint32_t format); }; -/** - * @ingroup iface_wl_shm - */ static inline int wl_shm_add_listener(struct wl_shm *wl_shm, const struct wl_shm_listener *listener, void *data) @@ -2148,28 +664,17 @@ wl_shm_add_listener(struct wl_shm *wl_shm, #define WL_SHM_CREATE_POOL 0 #define WL_SHM_RELEASE 1 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_FORMAT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_CREATE_POOL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_RELEASE_SINCE_VERSION 2 -/** @ingroup iface_wl_shm */ static inline void wl_shm_set_user_data(struct wl_shm *wl_shm, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_shm, user_data); } -/** @ingroup iface_wl_shm */ static inline void * wl_shm_get_user_data(struct wl_shm *wl_shm) { @@ -2182,22 +687,12 @@ wl_shm_get_version(struct wl_shm *wl_shm) return wl_proxy_get_version((struct wl_proxy *) wl_shm); } -/** @ingroup iface_wl_shm */ static inline void wl_shm_destroy(struct wl_shm *wl_shm) { wl_proxy_destroy((struct wl_proxy *) wl_shm); } -/** - * @ingroup iface_wl_shm - * - * Create a new wl_shm_pool object. - * - * The pool can be used to create shared memory based buffer - * objects. The server will mmap size bytes of the passed file - * descriptor, to use as backing memory for the pool. - */ static inline struct wl_shm_pool * wl_shm_create_pool(struct wl_shm *wl_shm, int32_t fd, int32_t size) { @@ -2209,14 +704,6 @@ wl_shm_create_pool(struct wl_shm *wl_shm, int32_t fd, int32_t size) return (struct wl_shm_pool *) id; } -/** - * @ingroup iface_wl_shm - * - * Using this request a client can tell the server that it is not going to - * use the shm object anymore. - * - * Objects created via this interface remain unaffected. - */ static inline void wl_shm_release(struct wl_shm *wl_shm) { @@ -2224,36 +711,11 @@ wl_shm_release(struct wl_shm *wl_shm) WL_SHM_RELEASE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shm), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_buffer - * @struct wl_buffer_listener - */ struct wl_buffer_listener { - /** - * compositor releases buffer - * - * Sent when this wl_buffer is no longer used by the compositor. - * - * For more information on when release events may or may not be - * sent, and what consequences it has, please see the description - * of wl_surface.attach. - * - * If a client receives a release event before the frame callback - * requested in the same wl_surface.commit that attaches this - * wl_buffer to a surface, then the client is immediately free to - * reuse the buffer and its backing storage, and does not need a - * second buffer for the next surface content update. Typically - * this is possible, when the compositor maintains a copy of the - * wl_surface contents, e.g. as a GL texture. This is an important - * optimization for GL(ES) compositors with wl_shm clients. - */ void (*release)(void *data, struct wl_buffer *wl_buffer); }; -/** - * @ingroup iface_wl_buffer - */ static inline int wl_buffer_add_listener(struct wl_buffer *wl_buffer, const struct wl_buffer_listener *listener, void *data) @@ -2264,24 +726,16 @@ wl_buffer_add_listener(struct wl_buffer *wl_buffer, #define WL_BUFFER_DESTROY 0 -/** - * @ingroup iface_wl_buffer - */ #define WL_BUFFER_RELEASE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_buffer - */ #define WL_BUFFER_DESTROY_SINCE_VERSION 1 -/** @ingroup iface_wl_buffer */ static inline void wl_buffer_set_user_data(struct wl_buffer *wl_buffer, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_buffer, user_data); } -/** @ingroup iface_wl_buffer */ static inline void * wl_buffer_get_user_data(struct wl_buffer *wl_buffer) { @@ -2294,14 +748,6 @@ wl_buffer_get_version(struct wl_buffer *wl_buffer) return wl_proxy_get_version((struct wl_proxy *) wl_buffer); } -/** - * @ingroup iface_wl_buffer - * - * Destroy a buffer. If and how you need to release the backing - * storage is defined by the buffer factory interface. - * - * For possible side-effects to a surface, see wl_surface.attach. - */ static inline void wl_buffer_destroy(struct wl_buffer *wl_buffer) { @@ -2312,104 +758,25 @@ wl_buffer_destroy(struct wl_buffer *wl_buffer) #ifndef WL_DATA_OFFER_ERROR_ENUM #define WL_DATA_OFFER_ERROR_ENUM enum wl_data_offer_error { - /** - * finish request was called untimely - */ WL_DATA_OFFER_ERROR_INVALID_FINISH = 0, - /** - * action mask contains invalid values - */ WL_DATA_OFFER_ERROR_INVALID_ACTION_MASK = 1, - /** - * action argument has an invalid value - */ WL_DATA_OFFER_ERROR_INVALID_ACTION = 2, - /** - * offer doesn't accept this request - */ WL_DATA_OFFER_ERROR_INVALID_OFFER = 3, }; -#endif /* WL_DATA_OFFER_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_data_offer - * @struct wl_data_offer_listener - */ struct wl_data_offer_listener { - /** - * advertise offered mime type - * - * Sent immediately after creating the wl_data_offer object. One - * event per offered mime type. - * @param mime_type offered mime type - */ void (*offer)(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type); - /** - * notify the source-side available actions - * - * This event indicates the actions offered by the data source. - * It will be sent immediately after creating the wl_data_offer - * object, or anytime the source side changes its offered actions - * through wl_data_source.set_actions. - * @param source_actions actions offered by the data source - * @since 3 - */ void (*source_actions)(void *data, struct wl_data_offer *wl_data_offer, uint32_t source_actions); - /** - * notify the selected action - * - * This event indicates the action selected by the compositor - * after matching the source/destination side actions. Only one - * action (or none) will be offered here. - * - * This event can be emitted multiple times during the - * drag-and-drop operation in response to destination side action - * changes through wl_data_offer.set_actions. - * - * This event will no longer be emitted after wl_data_device.drop - * happened on the drag-and-drop destination, the client must honor - * the last action received, or the last preferred one set through - * wl_data_offer.set_actions when handling an "ask" action. - * - * Compositors may also change the selected action on the fly, - * mainly in response to keyboard modifier changes during the - * drag-and-drop operation. - * - * The most recent action received is always the valid one. Prior - * to receiving wl_data_device.drop, the chosen action may change - * (e.g. due to keyboard modifiers being pressed). At the time of - * receiving wl_data_device.drop the drag-and-drop destination must - * honor the last action received. - * - * Action changes may still happen after wl_data_device.drop, - * especially on "ask" actions, where the drag-and-drop destination - * may choose another action afterwards. Action changes happening - * at this stage are always the result of inter-client negotiation, - * the compositor shall no longer be able to induce a different - * action. - * - * Upon "ask" actions, it is expected that the drag-and-drop - * destination may potentially choose a different action and/or - * mime type, based on wl_data_offer.source_actions and finally - * chosen by the user (e.g. popping up a menu with the available - * options). The final wl_data_offer.set_actions and - * wl_data_offer.accept requests must happen before the call to - * wl_data_offer.finish. - * @param dnd_action action selected by the compositor - * @since 3 - */ void (*action)(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action); }; -/** - * @ingroup iface_wl_data_offer - */ static inline int wl_data_offer_add_listener(struct wl_data_offer *wl_data_offer, const struct wl_data_offer_listener *listener, void *data) @@ -2424,48 +791,22 @@ wl_data_offer_add_listener(struct wl_data_offer *wl_data_offer, #define WL_DATA_OFFER_FINISH 3 #define WL_DATA_OFFER_SET_ACTIONS 4 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_SOURCE_ACTIONS_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_ACTION_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_ACCEPT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_RECEIVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_FINISH_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_SET_ACTIONS_SINCE_VERSION 3 -/** @ingroup iface_wl_data_offer */ static inline void wl_data_offer_set_user_data(struct wl_data_offer *wl_data_offer, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_data_offer, user_data); } -/** @ingroup iface_wl_data_offer */ static inline void * wl_data_offer_get_user_data(struct wl_data_offer *wl_data_offer) { @@ -2478,24 +819,6 @@ wl_data_offer_get_version(struct wl_data_offer *wl_data_offer) return wl_proxy_get_version((struct wl_proxy *) wl_data_offer); } -/** - * @ingroup iface_wl_data_offer - * - * Indicate that the client can accept the given mime type, or - * NULL for not accepted. - * - * For objects of version 2 or older, this request is used by the - * client to give feedback whether the client can receive the given - * mime type, or NULL if none is accepted; the feedback does not - * determine whether the drag-and-drop operation succeeds or not. - * - * For objects of version 3 or newer, this request determines the - * final result of the drag-and-drop operation. If the end result - * is that no mime types were accepted, the drag-and-drop operation - * will be cancelled and the corresponding drag source will receive - * wl_data_source.cancelled. Clients may still use this event in - * conjunction with wl_data_source.action for feedback. - */ static inline void wl_data_offer_accept(struct wl_data_offer *wl_data_offer, uint32_t serial, const char *mime_type) { @@ -2503,25 +826,6 @@ wl_data_offer_accept(struct wl_data_offer *wl_data_offer, uint32_t serial, const WL_DATA_OFFER_ACCEPT, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_offer), 0, serial, mime_type); } -/** - * @ingroup iface_wl_data_offer - * - * To transfer the offered data, the client issues this request - * and indicates the mime type it wants to receive. The transfer - * happens through the passed file descriptor (typically created - * with the pipe system call). The source client writes the data - * in the mime type representation requested and then closes the - * file descriptor. - * - * The receiving client reads from the read end of the pipe until - * EOF and then closes its end, at which point the transfer is - * complete. - * - * This request may happen multiple times for different mime types, - * both before and after wl_data_device.drop. Drag-and-drop destination - * clients may preemptively fetch data or examine it more closely to - * determine acceptance. - */ static inline void wl_data_offer_receive(struct wl_data_offer *wl_data_offer, const char *mime_type, int32_t fd) { @@ -2529,11 +833,6 @@ wl_data_offer_receive(struct wl_data_offer *wl_data_offer, const char *mime_type WL_DATA_OFFER_RECEIVE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_offer), 0, mime_type, fd); } -/** - * @ingroup iface_wl_data_offer - * - * Destroy the data offer. - */ static inline void wl_data_offer_destroy(struct wl_data_offer *wl_data_offer) { @@ -2541,24 +840,6 @@ wl_data_offer_destroy(struct wl_data_offer *wl_data_offer) WL_DATA_OFFER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_offer), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_data_offer - * - * Notifies the compositor that the drag destination successfully - * finished the drag-and-drop operation. - * - * Upon receiving this request, the compositor will emit - * wl_data_source.dnd_finished on the drag source client. - * - * It is a client error to perform other requests than - * wl_data_offer.destroy after this one. It is also an error to perform - * this request after a NULL mime type has been set in - * wl_data_offer.accept or no action was received through - * wl_data_offer.action. - * - * If wl_data_offer.finish request is received for a non drag and drop - * operation, the invalid_finish protocol error is raised. - */ static inline void wl_data_offer_finish(struct wl_data_offer *wl_data_offer) { @@ -2566,41 +847,6 @@ wl_data_offer_finish(struct wl_data_offer *wl_data_offer) WL_DATA_OFFER_FINISH, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_offer), 0); } -/** - * @ingroup iface_wl_data_offer - * - * Sets the actions that the destination side client supports for - * this operation. This request may trigger the emission of - * wl_data_source.action and wl_data_offer.action events if the compositor - * needs to change the selected action. - * - * This request can be called multiple times throughout the - * drag-and-drop operation, typically in response to wl_data_device.enter - * or wl_data_device.motion events. - * - * This request determines the final result of the drag-and-drop - * operation. If the end result is that no action is accepted, - * the drag source will receive wl_data_source.cancelled. - * - * The dnd_actions argument must contain only values expressed in the - * wl_data_device_manager.dnd_actions enum, and the preferred_action - * argument must only contain one of those values set, otherwise it - * will result in a protocol error. - * - * While managing an "ask" action, the destination drag-and-drop client - * may perform further wl_data_offer.receive requests, and is expected - * to perform one last wl_data_offer.set_actions request with a preferred - * action other than "ask" (and optionally wl_data_offer.accept) before - * requesting wl_data_offer.finish, in order to convey the action selected - * by the user. If the preferred action is not in the - * wl_data_offer.source_actions mask, an error will be raised. - * - * If the "ask" action is dismissed (e.g. user cancellation), the client - * is expected to perform wl_data_offer.destroy right away. - * - * This request can only be made on drag-and-drop offers, a protocol error - * will be raised otherwise. - */ static inline void wl_data_offer_set_actions(struct wl_data_offer *wl_data_offer, uint32_t dnd_actions, uint32_t preferred_action) { @@ -2611,144 +857,30 @@ wl_data_offer_set_actions(struct wl_data_offer *wl_data_offer, uint32_t dnd_acti #ifndef WL_DATA_SOURCE_ERROR_ENUM #define WL_DATA_SOURCE_ERROR_ENUM enum wl_data_source_error { - /** - * action mask contains invalid values - */ WL_DATA_SOURCE_ERROR_INVALID_ACTION_MASK = 0, - /** - * source doesn't accept this request - */ WL_DATA_SOURCE_ERROR_INVALID_SOURCE = 1, }; -#endif /* WL_DATA_SOURCE_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_data_source - * @struct wl_data_source_listener - */ struct wl_data_source_listener { - /** - * a target accepts an offered mime type - * - * Sent when a target accepts pointer_focus or motion events. If - * a target does not accept any of the offered types, type is NULL. - * - * Used for feedback during drag-and-drop. - * @param mime_type mime type accepted by the target - */ void (*target)(void *data, struct wl_data_source *wl_data_source, const char *mime_type); - /** - * send the data - * - * Request for data from the client. Send the data as the - * specified mime type over the passed file descriptor, then close - * it. - * @param mime_type mime type for the data - * @param fd file descriptor for the data - */ void (*send)(void *data, struct wl_data_source *wl_data_source, const char *mime_type, int32_t fd); - /** - * selection was cancelled - * - * This data source is no longer valid. There are several reasons - * why this could happen: - * - * - The data source has been replaced by another data source. - - * The drag-and-drop operation was performed, but the drop - * destination did not accept any of the mime types offered through - * wl_data_source.target. - The drag-and-drop operation was - * performed, but the drop destination did not select any of the - * actions present in the mask offered through - * wl_data_source.action. - The drag-and-drop operation was - * performed but didn't happen over a surface. - The compositor - * cancelled the drag-and-drop operation (e.g. compositor dependent - * timeouts to avoid stale drag-and-drop transfers). - * - * The client should clean up and destroy this data source. - * - * For objects of version 2 or older, wl_data_source.cancelled will - * only be emitted if the data source was replaced by another data - * source. - */ void (*cancelled)(void *data, struct wl_data_source *wl_data_source); - /** - * the drag-and-drop operation physically finished - * - * The user performed the drop action. This event does not - * indicate acceptance, wl_data_source.cancelled may still be - * emitted afterwards if the drop destination does not accept any - * mime type. - * - * However, this event might not be received if the compositor - * cancelled the drag-and-drop operation before this event could - * happen. - * - * Note that the data_source may still be used in the future and - * should not be destroyed here. - * @since 3 - */ void (*dnd_drop_performed)(void *data, struct wl_data_source *wl_data_source); - /** - * the drag-and-drop operation concluded - * - * The drop destination finished interoperating with this data - * source, so the client is now free to destroy this data source - * and free all associated data. - * - * If the action used to perform the operation was "move", the - * source can now delete the transferred data. - * @since 3 - */ void (*dnd_finished)(void *data, struct wl_data_source *wl_data_source); - /** - * notify the selected action - * - * This event indicates the action selected by the compositor - * after matching the source/destination side actions. Only one - * action (or none) will be offered here. - * - * This event can be emitted multiple times during the - * drag-and-drop operation, mainly in response to destination side - * changes through wl_data_offer.set_actions, and as the data - * device enters/leaves surfaces. - * - * It is only possible to receive this event after - * wl_data_source.dnd_drop_performed if the drag-and-drop operation - * ended in an "ask" action, in which case the final - * wl_data_source.action event will happen immediately before - * wl_data_source.dnd_finished. - * - * Compositors may also change the selected action on the fly, - * mainly in response to keyboard modifier changes during the - * drag-and-drop operation. - * - * The most recent action received is always the valid one. The - * chosen action may change alongside negotiation (e.g. an "ask" - * action can turn into a "move" operation), so the effects of the - * final action must always be applied in - * wl_data_source.dnd_finished. - * - * Clients can trigger cursor surface changes from this point, so - * they reflect the current action. - * @param dnd_action action selected by the compositor - * @since 3 - */ void (*action)(void *data, struct wl_data_source *wl_data_source, uint32_t dnd_action); }; -/** - * @ingroup iface_wl_data_source - */ static inline int wl_data_source_add_listener(struct wl_data_source *wl_data_source, const struct wl_data_source_listener *listener, void *data) @@ -2761,52 +893,23 @@ wl_data_source_add_listener(struct wl_data_source *wl_data_source, #define WL_DATA_SOURCE_DESTROY 1 #define WL_DATA_SOURCE_SET_ACTIONS 2 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_TARGET_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_SEND_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_CANCELLED_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DND_DROP_PERFORMED_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DND_FINISHED_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_ACTION_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_SET_ACTIONS_SINCE_VERSION 3 -/** @ingroup iface_wl_data_source */ static inline void wl_data_source_set_user_data(struct wl_data_source *wl_data_source, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_data_source, user_data); } -/** @ingroup iface_wl_data_source */ static inline void * wl_data_source_get_user_data(struct wl_data_source *wl_data_source) { @@ -2819,13 +922,6 @@ wl_data_source_get_version(struct wl_data_source *wl_data_source) return wl_proxy_get_version((struct wl_proxy *) wl_data_source); } -/** - * @ingroup iface_wl_data_source - * - * This request adds a mime type to the set of mime types - * advertised to targets. Can be called several times to offer - * multiple types. - */ static inline void wl_data_source_offer(struct wl_data_source *wl_data_source, const char *mime_type) { @@ -2833,11 +929,6 @@ wl_data_source_offer(struct wl_data_source *wl_data_source, const char *mime_typ WL_DATA_SOURCE_OFFER, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_source), 0, mime_type); } -/** - * @ingroup iface_wl_data_source - * - * Destroy the data source. - */ static inline void wl_data_source_destroy(struct wl_data_source *wl_data_source) { @@ -2845,23 +936,6 @@ wl_data_source_destroy(struct wl_data_source *wl_data_source) WL_DATA_SOURCE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_source), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_data_source - * - * Sets the actions that the source side client supports for this - * operation. This request may trigger wl_data_source.action and - * wl_data_offer.action events if the compositor needs to change the - * selected action. - * - * The dnd_actions argument must contain only values expressed in the - * wl_data_device_manager.dnd_actions enum, otherwise it will result - * in a protocol error. - * - * This request must be made once only, and can only be made on sources - * used in drag-and-drop, so it must be performed before - * wl_data_device.start_drag. Attempting to use the source other than - * for drag-and-drop will raise a protocol error. - */ static inline void wl_data_source_set_actions(struct wl_data_source *wl_data_source, uint32_t dnd_actions) { @@ -2872,50 +946,15 @@ wl_data_source_set_actions(struct wl_data_source *wl_data_source, uint32_t dnd_a #ifndef WL_DATA_DEVICE_ERROR_ENUM #define WL_DATA_DEVICE_ERROR_ENUM enum wl_data_device_error { - /** - * given wl_surface has another role - */ WL_DATA_DEVICE_ERROR_ROLE = 0, - /** - * source has already been used - */ WL_DATA_DEVICE_ERROR_USED_SOURCE = 1, }; -#endif /* WL_DATA_DEVICE_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_data_device - * @struct wl_data_device_listener - */ struct wl_data_device_listener { - /** - * introduce a new wl_data_offer - * - * The data_offer event introduces a new wl_data_offer object, - * which will subsequently be used in either the data_device.enter - * event (for drag-and-drop) or the data_device.selection event - * (for selections). Immediately following the - * data_device.data_offer event, the new data_offer object will - * send out data_offer.offer events to describe the mime types it - * offers. - * @param id the new data_offer object - */ void (*data_offer)(void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *id); - /** - * initiate drag-and-drop session - * - * This event is sent when an active drag-and-drop pointer enters - * a surface owned by the client. The position of the pointer at - * enter time is provided by the x and y arguments, in - * surface-local coordinates. - * @param serial serial number of the enter event - * @param surface client surface entered - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @param id source data_offer object - */ void (*enter)(void *data, struct wl_data_device *wl_data_device, uint32_t serial, @@ -2923,75 +962,20 @@ struct wl_data_device_listener { wl_fixed_t x, wl_fixed_t y, struct wl_data_offer *id); - /** - * end drag-and-drop session - * - * This event is sent when the drag-and-drop pointer leaves the - * surface and the session ends. The client must destroy the - * wl_data_offer introduced at enter time at this point. - */ void (*leave)(void *data, struct wl_data_device *wl_data_device); - /** - * drag-and-drop session motion - * - * This event is sent when the drag-and-drop pointer moves within - * the currently focused surface. The new position of the pointer - * is provided by the x and y arguments, in surface-local - * coordinates. - * @param time timestamp with millisecond granularity - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ void (*motion)(void *data, struct wl_data_device *wl_data_device, uint32_t time, wl_fixed_t x, wl_fixed_t y); - /** - * end drag-and-drop session successfully - * - * The event is sent when a drag-and-drop operation is ended - * because the implicit grab is removed. - * - * The drag-and-drop destination is expected to honor the last - * action received through wl_data_offer.action, if the resulting - * action is "copy" or "move", the destination can still perform - * wl_data_offer.receive requests, and is expected to end all - * transfers with a wl_data_offer.finish request. - * - * If the resulting action is "ask", the action will not be - * considered final. The drag-and-drop destination is expected to - * perform one last wl_data_offer.set_actions request, or - * wl_data_offer.destroy in order to cancel the operation. - */ void (*drop)(void *data, struct wl_data_device *wl_data_device); - /** - * advertise new selection - * - * The selection event is sent out to notify the client of a new - * wl_data_offer for the selection for this device. The - * data_device.data_offer and the data_offer.offer events are sent - * out immediately before this event to introduce the data offer - * object. The selection event is sent to a client immediately - * before receiving keyboard focus and when a new selection is set - * while the client has keyboard focus. The data_offer is valid - * until a new data_offer or NULL is received or until the client - * loses keyboard focus. Switching surface with keyboard focus - * within the same client doesn't mean a new selection will be - * sent. The client must destroy the previous selection data_offer, - * if any, upon receiving this event. - * @param id selection data_offer object - */ void (*selection)(void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *id); }; -/** - * @ingroup iface_wl_data_device - */ static inline int wl_data_device_add_listener(struct wl_data_device *wl_data_device, const struct wl_data_device_listener *listener, void *data) @@ -3004,52 +988,23 @@ wl_data_device_add_listener(struct wl_data_device *wl_data_device, #define WL_DATA_DEVICE_SET_SELECTION 1 #define WL_DATA_DEVICE_RELEASE 2 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_DATA_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_DROP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_START_DRAG_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_SET_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_RELEASE_SINCE_VERSION 2 -/** @ingroup iface_wl_data_device */ static inline void wl_data_device_set_user_data(struct wl_data_device *wl_data_device, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_data_device, user_data); } -/** @ingroup iface_wl_data_device */ static inline void * wl_data_device_get_user_data(struct wl_data_device *wl_data_device) { @@ -3062,46 +1017,12 @@ wl_data_device_get_version(struct wl_data_device *wl_data_device) return wl_proxy_get_version((struct wl_proxy *) wl_data_device); } -/** @ingroup iface_wl_data_device */ static inline void wl_data_device_destroy(struct wl_data_device *wl_data_device) { wl_proxy_destroy((struct wl_proxy *) wl_data_device); } -/** - * @ingroup iface_wl_data_device - * - * This request asks the compositor to start a drag-and-drop - * operation on behalf of the client. - * - * The source argument is the data source that provides the data - * for the eventual data transfer. If source is NULL, enter, leave - * and motion events are sent only to the client that initiated the - * drag and the client is expected to handle the data passing - * internally. If source is destroyed, the drag-and-drop session will be - * cancelled. - * - * The origin surface is the surface where the drag originates and - * the client must have an active implicit grab that matches the - * serial. - * - * The icon surface is an optional (can be NULL) surface that - * provides an icon to be moved around with the cursor. Initially, - * the top-left corner of the icon surface is placed at the cursor - * hotspot, but subsequent wl_surface.offset requests can move the - * relative position. Attach requests must be confirmed with - * wl_surface.commit as usual. The icon surface is given the role of - * a drag-and-drop icon. If the icon surface already has another role, - * it raises a protocol error. - * - * The input region is ignored for wl_surfaces with the role of a - * drag-and-drop icon. - * - * The given source may not be used in any further set_selection or - * start_drag requests. Attempting to reuse a previously-used source - * may send a used_source error. - */ static inline void wl_data_device_start_drag(struct wl_data_device *wl_data_device, struct wl_data_source *source, struct wl_surface *origin, struct wl_surface *icon, uint32_t serial) { @@ -3109,18 +1030,6 @@ wl_data_device_start_drag(struct wl_data_device *wl_data_device, struct wl_data_ WL_DATA_DEVICE_START_DRAG, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_device), 0, source, origin, icon, serial); } -/** - * @ingroup iface_wl_data_device - * - * This request asks the compositor to set the selection - * to the data from the source on behalf of the client. - * - * To unset the selection, set the source to NULL. - * - * The given source may not be used in any further set_selection or - * start_drag requests. Attempting to reuse a previously-used source - * may send a used_source error. - */ static inline void wl_data_device_set_selection(struct wl_data_device *wl_data_device, struct wl_data_source *source, uint32_t serial) { @@ -3128,11 +1037,6 @@ wl_data_device_set_selection(struct wl_data_device *wl_data_device, struct wl_da WL_DATA_DEVICE_SET_SELECTION, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_device), 0, source, serial); } -/** - * @ingroup iface_wl_data_device - * - * This request destroys the data device. - */ static inline void wl_data_device_release(struct wl_data_device *wl_data_device) { @@ -3142,80 +1046,29 @@ wl_data_device_release(struct wl_data_device *wl_data_device) #ifndef WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM #define WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM -/** - * @ingroup iface_wl_data_device_manager - * drag and drop actions - * - * This is a bitmask of the available/preferred actions in a - * drag-and-drop operation. - * - * In the compositor, the selected action is a result of matching the - * actions offered by the source and destination sides. "action" events - * with a "none" action will be sent to both source and destination if - * there is no match. All further checks will effectively happen on - * (source actions ∩ destination actions). - * - * In addition, compositors may also pick different actions in - * reaction to key modifiers being pressed. One common design that - * is used in major toolkits (and the behavior recommended for - * compositors) is: - * - * - If no modifiers are pressed, the first match (in bit order) - * will be used. - * - Pressing Shift selects "move", if enabled in the mask. - * - Pressing Control selects "copy", if enabled in the mask. - * - * Behavior beyond that is considered implementation-dependent. - * Compositors may for example bind other modifiers (like Alt/Meta) - * or drags initiated with other buttons than BTN_LEFT to specific - * actions (e.g. "ask"). - */ enum wl_data_device_manager_dnd_action { - /** - * no action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE = 0, - /** - * copy action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY = 1, - /** - * move action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE = 2, - /** - * ask action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK = 4, }; -#endif /* WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM */ +#endif #define WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE 0 #define WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE 1 #define WL_DATA_DEVICE_MANAGER_RELEASE 2 -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_RELEASE_SINCE_VERSION 4 -/** @ingroup iface_wl_data_device_manager */ static inline void wl_data_device_manager_set_user_data(struct wl_data_device_manager *wl_data_device_manager, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_data_device_manager, user_data); } -/** @ingroup iface_wl_data_device_manager */ static inline void * wl_data_device_manager_get_user_data(struct wl_data_device_manager *wl_data_device_manager) { @@ -3228,18 +1081,12 @@ wl_data_device_manager_get_version(struct wl_data_device_manager *wl_data_device return wl_proxy_get_version((struct wl_proxy *) wl_data_device_manager); } -/** @ingroup iface_wl_data_device_manager */ static inline void wl_data_device_manager_destroy(struct wl_data_device_manager *wl_data_device_manager) { wl_proxy_destroy((struct wl_proxy *) wl_data_device_manager); } -/** - * @ingroup iface_wl_data_device_manager - * - * Create a new data source. - */ static inline struct wl_data_source * wl_data_device_manager_create_data_source(struct wl_data_device_manager *wl_data_device_manager) { @@ -3251,11 +1098,6 @@ wl_data_device_manager_create_data_source(struct wl_data_device_manager *wl_data return (struct wl_data_source *) id; } -/** - * @ingroup iface_wl_data_device_manager - * - * Create a new data device for a given seat. - */ static inline struct wl_data_device * wl_data_device_manager_get_data_device(struct wl_data_device_manager *wl_data_device_manager, struct wl_seat *seat) { @@ -3267,12 +1109,6 @@ wl_data_device_manager_get_data_device(struct wl_data_device_manager *wl_data_de return (struct wl_data_device *) id; } -/** - * @ingroup iface_wl_data_device_manager - * - * This request destroys the wl_data_device_manager. This has no effect on any other - * objects. - */ static inline void wl_data_device_manager_release(struct wl_data_device_manager *wl_data_device_manager) { @@ -3283,29 +1119,21 @@ wl_data_device_manager_release(struct wl_data_device_manager *wl_data_device_man #ifndef WL_SHELL_ERROR_ENUM #define WL_SHELL_ERROR_ENUM enum wl_shell_error { - /** - * given wl_surface has another role - */ WL_SHELL_ERROR_ROLE = 0, }; -#endif /* WL_SHELL_ERROR_ENUM */ +#endif #define WL_SHELL_GET_SHELL_SURFACE 0 -/** - * @ingroup iface_wl_shell - */ #define WL_SHELL_GET_SHELL_SURFACE_SINCE_VERSION 1 -/** @ingroup iface_wl_shell */ static inline void wl_shell_set_user_data(struct wl_shell *wl_shell, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_shell, user_data); } -/** @ingroup iface_wl_shell */ static inline void * wl_shell_get_user_data(struct wl_shell *wl_shell) { @@ -3318,22 +1146,12 @@ wl_shell_get_version(struct wl_shell *wl_shell) return wl_proxy_get_version((struct wl_proxy *) wl_shell); } -/** @ingroup iface_wl_shell */ static inline void wl_shell_destroy(struct wl_shell *wl_shell) { wl_proxy_destroy((struct wl_proxy *) wl_shell); } -/** - * @ingroup iface_wl_shell - * - * Create a shell surface for an existing surface. This gives - * the wl_surface the role of a shell surface. If the wl_surface - * already has another role, it raises a protocol error. - * - * Only one shell surface can be associated with a given surface. - */ static inline struct wl_shell_surface * wl_shell_get_shell_surface(struct wl_shell *wl_shell, struct wl_surface *surface) { @@ -3347,160 +1165,49 @@ wl_shell_get_shell_surface(struct wl_shell *wl_shell, struct wl_surface *surface #ifndef WL_SHELL_SURFACE_RESIZE_ENUM #define WL_SHELL_SURFACE_RESIZE_ENUM -/** - * @ingroup iface_wl_shell_surface - * edge values for resizing - * - * These values are used to indicate which edge of a surface - * is being dragged in a resize operation. The server may - * use this information to adapt its behavior, e.g. choose - * an appropriate cursor image. - */ enum wl_shell_surface_resize { - /** - * no edge - */ WL_SHELL_SURFACE_RESIZE_NONE = 0, - /** - * top edge - */ WL_SHELL_SURFACE_RESIZE_TOP = 1, - /** - * bottom edge - */ WL_SHELL_SURFACE_RESIZE_BOTTOM = 2, - /** - * left edge - */ WL_SHELL_SURFACE_RESIZE_LEFT = 4, - /** - * top and left edges - */ WL_SHELL_SURFACE_RESIZE_TOP_LEFT = 5, - /** - * bottom and left edges - */ WL_SHELL_SURFACE_RESIZE_BOTTOM_LEFT = 6, - /** - * right edge - */ WL_SHELL_SURFACE_RESIZE_RIGHT = 8, - /** - * top and right edges - */ WL_SHELL_SURFACE_RESIZE_TOP_RIGHT = 9, - /** - * bottom and right edges - */ WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT = 10, }; -#endif /* WL_SHELL_SURFACE_RESIZE_ENUM */ +#endif #ifndef WL_SHELL_SURFACE_TRANSIENT_ENUM #define WL_SHELL_SURFACE_TRANSIENT_ENUM -/** - * @ingroup iface_wl_shell_surface - * details of transient behaviour - * - * These flags specify details of the expected behaviour - * of transient surfaces. Used in the set_transient request. - */ enum wl_shell_surface_transient { - /** - * do not set keyboard focus - */ WL_SHELL_SURFACE_TRANSIENT_INACTIVE = 0x1, }; -#endif /* WL_SHELL_SURFACE_TRANSIENT_ENUM */ +#endif #ifndef WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM #define WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM -/** - * @ingroup iface_wl_shell_surface - * different method to set the surface fullscreen - * - * Hints to indicate to the compositor how to deal with a conflict - * between the dimensions of the surface and the dimensions of the - * output. The compositor is free to ignore this parameter. - */ enum wl_shell_surface_fullscreen_method { - /** - * no preference, apply default policy - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_DEFAULT = 0, - /** - * scale, preserve the surface's aspect ratio and center on output - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_SCALE = 1, - /** - * switch output mode to the smallest mode that can fit the surface, add black borders to compensate size mismatch - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_DRIVER = 2, - /** - * no upscaling, center on output and add black borders to compensate size mismatch - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_FILL = 3, }; -#endif /* WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM */ +#endif -/** - * @ingroup iface_wl_shell_surface - * @struct wl_shell_surface_listener - */ struct wl_shell_surface_listener { - /** - * ping client - * - * Ping a client to check if it is receiving events and sending - * requests. A client is expected to reply with a pong request. - * @param serial serial number of the ping - */ void (*ping)(void *data, struct wl_shell_surface *wl_shell_surface, uint32_t serial); - /** - * suggest resize - * - * The configure event asks the client to resize its surface. - * - * The size is a hint, in the sense that the client is free to - * ignore it if it doesn't resize, pick a smaller size (to satisfy - * aspect ratio or resize in steps of NxM pixels). - * - * The edges parameter provides a hint about how the surface was - * resized. The client may use this information to decide how to - * adjust its content to the new size (e.g. a scrolling area might - * adjust its content position to leave the viewable content - * unmoved). - * - * The client is free to dismiss all but the last configure event - * it received. - * - * The width and height arguments specify the size of the window in - * surface-local coordinates. - * @param edges how the surface was resized - * @param width new width of the surface - * @param height new height of the surface - */ void (*configure)(void *data, struct wl_shell_surface *wl_shell_surface, uint32_t edges, int32_t width, int32_t height); - /** - * popup interaction is done - * - * The popup_done event is sent out when a popup grab is broken, - * that is, when the user clicks a surface that doesn't belong to - * the client owning the popup surface. - */ void (*popup_done)(void *data, struct wl_shell_surface *wl_shell_surface); }; -/** - * @ingroup iface_wl_shell_surface - */ static inline int wl_shell_surface_add_listener(struct wl_shell_surface *wl_shell_surface, const struct wl_shell_surface_listener *listener, void *data) @@ -3520,68 +1227,27 @@ wl_shell_surface_add_listener(struct wl_shell_surface *wl_shell_surface, #define WL_SHELL_SURFACE_SET_TITLE 8 #define WL_SHELL_SURFACE_SET_CLASS 9 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_PING_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_POPUP_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_PONG_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_MOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_RESIZE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TOPLEVEL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TRANSIENT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_POPUP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TITLE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_CLASS_SINCE_VERSION 1 -/** @ingroup iface_wl_shell_surface */ static inline void wl_shell_surface_set_user_data(struct wl_shell_surface *wl_shell_surface, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_shell_surface, user_data); } -/** @ingroup iface_wl_shell_surface */ static inline void * wl_shell_surface_get_user_data(struct wl_shell_surface *wl_shell_surface) { @@ -3594,19 +1260,12 @@ wl_shell_surface_get_version(struct wl_shell_surface *wl_shell_surface) return wl_proxy_get_version((struct wl_proxy *) wl_shell_surface); } -/** @ingroup iface_wl_shell_surface */ static inline void wl_shell_surface_destroy(struct wl_shell_surface *wl_shell_surface) { wl_proxy_destroy((struct wl_proxy *) wl_shell_surface); } -/** - * @ingroup iface_wl_shell_surface - * - * A client must respond to a ping event with a pong request or - * the client may be deemed unresponsive. - */ static inline void wl_shell_surface_pong(struct wl_shell_surface *wl_shell_surface, uint32_t serial) { @@ -3614,15 +1273,6 @@ wl_shell_surface_pong(struct wl_shell_surface *wl_shell_surface, uint32_t serial WL_SHELL_SURFACE_PONG, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, serial); } -/** - * @ingroup iface_wl_shell_surface - * - * Start a pointer-driven move of the surface. - * - * This request must be used in response to a button press event. - * The server may ignore move requests depending on the state of - * the surface (e.g. fullscreen or maximized). - */ static inline void wl_shell_surface_move(struct wl_shell_surface *wl_shell_surface, struct wl_seat *seat, uint32_t serial) { @@ -3630,15 +1280,6 @@ wl_shell_surface_move(struct wl_shell_surface *wl_shell_surface, struct wl_seat WL_SHELL_SURFACE_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, seat, serial); } -/** - * @ingroup iface_wl_shell_surface - * - * Start a pointer-driven resizing of the surface. - * - * This request must be used in response to a button press event. - * The server may ignore resize requests depending on the state of - * the surface (e.g. fullscreen or maximized). - */ static inline void wl_shell_surface_resize(struct wl_shell_surface *wl_shell_surface, struct wl_seat *seat, uint32_t serial, uint32_t edges) { @@ -3646,13 +1287,6 @@ wl_shell_surface_resize(struct wl_shell_surface *wl_shell_surface, struct wl_sea WL_SHELL_SURFACE_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, seat, serial, edges); } -/** - * @ingroup iface_wl_shell_surface - * - * Map the surface as a toplevel surface. - * - * A toplevel surface is not fullscreen, maximized or transient. - */ static inline void wl_shell_surface_set_toplevel(struct wl_shell_surface *wl_shell_surface) { @@ -3660,17 +1294,6 @@ wl_shell_surface_set_toplevel(struct wl_shell_surface *wl_shell_surface) WL_SHELL_SURFACE_SET_TOPLEVEL, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0); } -/** - * @ingroup iface_wl_shell_surface - * - * Map the surface relative to an existing surface. - * - * The x and y arguments specify the location of the upper left - * corner of the surface relative to the upper left corner of the - * parent surface, in surface-local coordinates. - * - * The flags argument controls details of the transient behaviour. - */ static inline void wl_shell_surface_set_transient(struct wl_shell_surface *wl_shell_surface, struct wl_surface *parent, int32_t x, int32_t y, uint32_t flags) { @@ -3678,43 +1301,6 @@ wl_shell_surface_set_transient(struct wl_shell_surface *wl_shell_surface, struct WL_SHELL_SURFACE_SET_TRANSIENT, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, parent, x, y, flags); } -/** - * @ingroup iface_wl_shell_surface - * - * Map the surface as a fullscreen surface. - * - * If an output parameter is given then the surface will be made - * fullscreen on that output. If the client does not specify the - * output then the compositor will apply its policy - usually - * choosing the output on which the surface has the biggest surface - * area. - * - * The client may specify a method to resolve a size conflict - * between the output size and the surface size - this is provided - * through the method parameter. - * - * The framerate parameter is used only when the method is set - * to "driver", to indicate the preferred framerate. A value of 0 - * indicates that the client does not care about framerate. The - * framerate is specified in mHz, that is framerate of 60000 is 60Hz. - * - * A method of "scale" or "driver" implies a scaling operation of - * the surface, either via a direct scaling operation or a change of - * the output mode. This will override any kind of output scaling, so - * that mapping a surface with a buffer size equal to the mode can - * fill the screen independent of buffer_scale. - * - * A method of "fill" means we don't scale up the buffer, however - * any output scale is applied. This means that you may run into - * an edge case where the application maps a buffer with the same - * size of the output mode but buffer_scale 1 (thus making a - * surface larger than the output). In this case it is allowed to - * downscale the results to fit the screen. - * - * The compositor must reply to this request with a configure event - * with the dimensions for the output on which the surface will - * be made fullscreen. - */ static inline void wl_shell_surface_set_fullscreen(struct wl_shell_surface *wl_shell_surface, uint32_t method, uint32_t framerate, struct wl_output *output) { @@ -3722,29 +1308,6 @@ wl_shell_surface_set_fullscreen(struct wl_shell_surface *wl_shell_surface, uint3 WL_SHELL_SURFACE_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, method, framerate, output); } -/** - * @ingroup iface_wl_shell_surface - * - * Map the surface as a popup. - * - * A popup surface is a transient surface with an added pointer - * grab. - * - * An existing implicit grab will be changed to owner-events mode, - * and the popup grab will continue after the implicit grab ends - * (i.e. releasing the mouse button does not cause the popup to - * be unmapped). - * - * The popup grab continues until the window is destroyed or a - * mouse button is pressed in any other client's window. A click - * in any of the client's surfaces is reported as normal, however, - * clicks in other clients' surfaces will be discarded and trigger - * the callback. - * - * The x and y arguments specify the location of the upper left - * corner of the surface relative to the upper left corner of the - * parent surface, in surface-local coordinates. - */ static inline void wl_shell_surface_set_popup(struct wl_shell_surface *wl_shell_surface, struct wl_seat *seat, uint32_t serial, struct wl_surface *parent, int32_t x, int32_t y, uint32_t flags) { @@ -3752,28 +1315,6 @@ wl_shell_surface_set_popup(struct wl_shell_surface *wl_shell_surface, struct wl_ WL_SHELL_SURFACE_SET_POPUP, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, seat, serial, parent, x, y, flags); } -/** - * @ingroup iface_wl_shell_surface - * - * Map the surface as a maximized surface. - * - * If an output parameter is given then the surface will be - * maximized on that output. If the client does not specify the - * output then the compositor will apply its policy - usually - * choosing the output on which the surface has the biggest surface - * area. - * - * The compositor will reply with a configure event telling - * the expected new surface size. The operation is completed - * on the next buffer attach to this surface. - * - * A maximized surface typically fills the entire output it is - * bound to, except for desktop elements such as panels. This is - * the main difference between a maximized shell surface and a - * fullscreen shell surface. - * - * The details depend on the compositor implementation. - */ static inline void wl_shell_surface_set_maximized(struct wl_shell_surface *wl_shell_surface, struct wl_output *output) { @@ -3781,17 +1322,6 @@ wl_shell_surface_set_maximized(struct wl_shell_surface *wl_shell_surface, struct WL_SHELL_SURFACE_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, output); } -/** - * @ingroup iface_wl_shell_surface - * - * Set a short title for the surface. - * - * This string may be used to identify the surface in a task bar, - * window list, or other user interface elements provided by the - * compositor. - * - * The string must be encoded in UTF-8. - */ static inline void wl_shell_surface_set_title(struct wl_shell_surface *wl_shell_surface, const char *title) { @@ -3799,16 +1329,6 @@ wl_shell_surface_set_title(struct wl_shell_surface *wl_shell_surface, const char WL_SHELL_SURFACE_SET_TITLE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, title); } -/** - * @ingroup iface_wl_shell_surface - * - * Set a class for the surface. - * - * The surface class identifies the general class of applications - * to which the surface belongs. A common convention is to use the - * file name (or the full path if it is a non-standard location) of - * the application's .desktop file as the class. - */ static inline void wl_shell_surface_set_class(struct wl_shell_surface *wl_shell_surface, const char *class_) { @@ -3818,122 +1338,31 @@ wl_shell_surface_set_class(struct wl_shell_surface *wl_shell_surface, const char #ifndef WL_SURFACE_ERROR_ENUM #define WL_SURFACE_ERROR_ENUM -/** - * @ingroup iface_wl_surface - * wl_surface error values - * - * These errors can be emitted in response to wl_surface requests. - */ enum wl_surface_error { - /** - * buffer scale value is invalid - */ WL_SURFACE_ERROR_INVALID_SCALE = 0, - /** - * buffer transform value is invalid - */ WL_SURFACE_ERROR_INVALID_TRANSFORM = 1, - /** - * buffer size is invalid - */ WL_SURFACE_ERROR_INVALID_SIZE = 2, - /** - * buffer offset is invalid - */ WL_SURFACE_ERROR_INVALID_OFFSET = 3, - /** - * surface was destroyed before its role object - */ WL_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 4, - /** - * no buffer was attached - */ WL_SURFACE_ERROR_NO_BUFFER = 5, }; -#endif /* WL_SURFACE_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_surface - * @struct wl_surface_listener - */ struct wl_surface_listener { - /** - * surface enters an output - * - * This is emitted whenever a surface's creation, movement, or - * resizing results in some part of it being within the scanout - * region of an output. - * - * Note that a surface may be overlapping with zero or more - * outputs. - * @param output output entered by the surface - */ void (*enter)(void *data, struct wl_surface *wl_surface, struct wl_output *output); - /** - * surface leaves an output - * - * This is emitted whenever a surface's creation, movement, or - * resizing results in it no longer having any part of it within - * the scanout region of an output. - * - * Clients should not use the number of outputs the surface is on - * for frame throttling purposes. The surface might be hidden even - * if no leave event has been sent, and the compositor might expect - * new surface content updates even if no enter event has been - * sent. The frame event should be used instead. - * @param output output left by the surface - */ void (*leave)(void *data, struct wl_surface *wl_surface, struct wl_output *output); - /** - * preferred buffer scale for the surface - * - * This event indicates the preferred buffer scale for this - * surface. It is sent whenever the compositor's preference - * changes. - * - * Before receiving this event the preferred buffer scale for this - * surface is 1. - * - * It is intended that scaling aware clients use this event to - * scale their content and use wl_surface.set_buffer_scale to - * indicate the scale they have rendered with. This allows clients - * to supply a higher detail buffer. - * - * The compositor shall emit a scale value greater than 0. - * @param factor preferred scaling factor - * @since 6 - */ void (*preferred_buffer_scale)(void *data, struct wl_surface *wl_surface, int32_t factor); - /** - * preferred buffer transform for the surface - * - * This event indicates the preferred buffer transform for this - * surface. It is sent whenever the compositor's preference - * changes. - * - * Before receiving this event the preferred buffer transform for - * this surface is normal. - * - * Applying this transformation to the surface buffer contents and - * using wl_surface.set_buffer_transform might allow the compositor - * to use the surface buffer more efficiently. - * @param transform preferred transform - * @since 6 - */ void (*preferred_buffer_transform)(void *data, struct wl_surface *wl_surface, uint32_t transform); }; -/** - * @ingroup iface_wl_surface - */ static inline int wl_surface_add_listener(struct wl_surface *wl_surface, const struct wl_surface_listener *listener, void *data) @@ -3955,80 +1384,30 @@ wl_surface_add_listener(struct wl_surface *wl_surface, #define WL_SURFACE_OFFSET 10 #define WL_SURFACE_GET_RELEASE 11 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION 6 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_PREFERRED_BUFFER_TRANSFORM_SINCE_VERSION 6 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_ATTACH_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DAMAGE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_FRAME_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_OPAQUE_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_INPUT_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_COMMIT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_BUFFER_TRANSFORM_SINCE_VERSION 2 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_BUFFER_SCALE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION 4 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_OFFSET_SINCE_VERSION 5 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_GET_RELEASE_SINCE_VERSION 7 -/** @ingroup iface_wl_surface */ static inline void wl_surface_set_user_data(struct wl_surface *wl_surface, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_surface, user_data); } -/** @ingroup iface_wl_surface */ static inline void * wl_surface_get_user_data(struct wl_surface *wl_surface) { @@ -4041,11 +1420,6 @@ wl_surface_get_version(struct wl_surface *wl_surface) return wl_proxy_get_version((struct wl_proxy *) wl_surface); } -/** - * @ingroup iface_wl_surface - * - * Deletes the surface and invalidates its object ID. - */ static inline void wl_surface_destroy(struct wl_surface *wl_surface) { @@ -4053,77 +1427,6 @@ wl_surface_destroy(struct wl_surface *wl_surface) WL_SURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_surface - * - * Set a buffer as the content of this surface. - * - * The new size of the surface is calculated based on the buffer - * size transformed by the inverse buffer_transform and the - * inverse buffer_scale. This means that at commit time the supplied - * buffer size must be an integer multiple of the buffer_scale. If - * that's not the case, an invalid_size error is sent. - * - * The x and y arguments specify the location of the new pending - * buffer's upper left corner, relative to the current buffer's upper - * left corner, in surface-local coordinates. In other words, the - * x and y, combined with the new surface size define in which - * directions the surface's size changes. Setting anything other than 0 - * as x and y arguments is discouraged, and should instead be replaced - * with using the separate wl_surface.offset request. - * - * When the bound wl_surface version is 5 or higher, passing any - * non-zero x or y is a protocol violation, and will result in an - * 'invalid_offset' error being raised. The x and y arguments are ignored - * and do not change the pending state. To achieve equivalent semantics, - * use wl_surface.offset. - * - * Surface contents are double-buffered state, see wl_surface.commit. - * - * The initial surface contents are void; there is no content. - * wl_surface.attach assigns the given wl_buffer as the pending - * wl_buffer. wl_surface.commit makes the pending wl_buffer the new - * surface contents, and the size of the surface becomes the size - * calculated from the wl_buffer, as described above. After commit, - * there is no pending buffer until the next attach. - * - * Committing a pending wl_buffer allows the compositor to read the - * pixels in the wl_buffer. The compositor may access the pixels at - * any time after the wl_surface.commit request. When the compositor - * will not access the pixels anymore, it will send the - * wl_buffer.release event. Only after receiving wl_buffer.release, - * the client may reuse the wl_buffer. A wl_buffer that has been - * attached and then replaced by another attach instead of committed - * will not receive a release event, and is not used by the - * compositor. - * - * If a pending wl_buffer has been committed to more than one wl_surface, - * the delivery of wl_buffer.release events becomes undefined. A well - * behaved client should not rely on wl_buffer.release events in this - * case. Instead, clients hitting this case should use - * wl_surface.get_release or use a protocol extension providing per-commit - * release notifications (if none of these options are available, a - * fallback can be implemented by creating multiple wl_buffer objects from - * the same backing storage). - * - * Destroying the wl_buffer after wl_buffer.release does not change - * the surface contents. Destroying the wl_buffer before wl_buffer.release - * is allowed as long as the underlying buffer storage isn't re-used (this - * can happen e.g. on client process termination). However, if the client - * destroys the wl_buffer before receiving the wl_buffer.release event and - * mutates the underlying buffer storage, the surface contents become - * undefined immediately. - * - * If wl_surface.attach is sent with a NULL wl_buffer, the - * following wl_surface.commit will remove the surface content. - * - * If a pending wl_buffer has been destroyed, the result is not specified. - * Many compositors are known to remove the surface content on the following - * wl_surface.commit, but this behaviour is not universal. Clients seeking to - * maximise compatibility should not destroy pending buffers and should - * ensure that they explicitly remove content from surfaces, even after - * destroying buffers. - */ static inline void wl_surface_attach(struct wl_surface *wl_surface, struct wl_buffer *buffer, int32_t x, int32_t y) { @@ -4131,31 +1434,6 @@ wl_surface_attach(struct wl_surface *wl_surface, struct wl_buffer *buffer, int32 WL_SURFACE_ATTACH, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, buffer, x, y); } -/** - * @ingroup iface_wl_surface - * - * This request is used to describe the regions where the pending - * buffer is different from the current surface contents, and where - * the surface therefore needs to be repainted. The compositor - * ignores the parts of the damage that fall outside of the surface. - * - * Damage is double-buffered state, see wl_surface.commit. - * - * The damage rectangle is specified in surface-local coordinates, - * where x and y specify the upper left corner of the damage rectangle. - * - * The initial value for pending damage is empty: no damage. - * wl_surface.damage adds pending damage: the new pending damage - * is the union of old pending damage and the given rectangle. - * - * wl_surface.commit assigns pending damage as the current damage, - * and clears pending damage. The server will clear the current - * damage as it repaints the surface. - * - * Note! New clients should not use this request. Instead damage can be - * posted with wl_surface.damage_buffer which uses buffer coordinates - * instead of surface coordinates. - */ static inline void wl_surface_damage(struct wl_surface *wl_surface, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -4163,42 +1441,6 @@ wl_surface_damage(struct wl_surface *wl_surface, int32_t x, int32_t y, int32_t w WL_SURFACE_DAMAGE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, x, y, width, height); } -/** - * @ingroup iface_wl_surface - * - * Request a notification when it is a good time to start drawing a new - * frame, by creating a frame callback. This is useful for throttling - * redrawing operations, and driving animations. - * - * When a client is animating on a wl_surface, it can use the 'frame' - * request to get notified when it is a good time to draw and commit the - * next frame of animation. If the client commits an update earlier than - * that, it is likely that some updates will not make it to the display, - * and the client is wasting resources by drawing too often. - * - * The frame request will take effect on the next wl_surface.commit. - * The notification will only be posted for one frame unless - * requested again. For a wl_surface, the notifications are posted in - * the order the frame requests were committed. - * - * The server must send the notifications so that a client - * will not send excessive updates, while still allowing - * the highest possible update rate for clients that wait for the reply - * before drawing again. The server should give some time for the client - * to draw and commit after sending the frame callback events to let it - * hit the next output refresh. - * - * A server should avoid signaling the frame callbacks if the - * surface is not visible in any way, e.g. the surface is off-screen, - * or completely obscured by other opaque surfaces. - * - * The object returned by this request will be destroyed by the - * compositor after the callback is fired and as such the client must not - * attempt to use it after that point. - * - * The callback_data passed in the callback is the current time, in - * milliseconds, with an undefined base. - */ static inline struct wl_callback * wl_surface_frame(struct wl_surface *wl_surface) { @@ -4210,34 +1452,6 @@ wl_surface_frame(struct wl_surface *wl_surface) return (struct wl_callback *) callback; } -/** - * @ingroup iface_wl_surface - * - * This request sets the region of the surface that contains - * opaque content. - * - * The opaque region is an optimization hint for the compositor - * that lets it optimize the redrawing of content behind opaque - * regions. Setting an opaque region is not required for correct - * behaviour, but marking transparent content as opaque will result - * in repaint artifacts. - * - * The opaque region is specified in surface-local coordinates. - * - * The compositor ignores the parts of the opaque region that fall - * outside of the surface. - * - * Opaque region is double-buffered state, see wl_surface.commit. - * - * wl_surface.set_opaque_region changes the pending opaque region. - * wl_surface.commit copies the pending region to the current region. - * Otherwise, the pending and current regions are never changed. - * - * The initial value for an opaque region is empty. Setting the pending - * opaque region has copy semantics, and the wl_region object can be - * destroyed immediately. A NULL wl_region causes the pending opaque - * region to be set to empty. - */ static inline void wl_surface_set_opaque_region(struct wl_surface *wl_surface, struct wl_region *region) { @@ -4245,32 +1459,6 @@ wl_surface_set_opaque_region(struct wl_surface *wl_surface, struct wl_region *re WL_SURFACE_SET_OPAQUE_REGION, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, region); } -/** - * @ingroup iface_wl_surface - * - * This request sets the region of the surface that can receive - * pointer and touch events. - * - * Input events happening outside of this region will try the next - * surface in the server surface stack. The compositor ignores the - * parts of the input region that fall outside of the surface. - * - * The input region is specified in surface-local coordinates. - * - * Input region is double-buffered state, see wl_surface.commit. - * - * wl_surface.set_input_region changes the pending input region. - * wl_surface.commit copies the pending region to the current region. - * Otherwise the pending and current regions are never changed, - * except cursor and icon surfaces are special cases, see - * wl_pointer.set_cursor and wl_data_device.start_drag. - * - * The initial value for an input region is infinite. That means the - * whole surface will accept input. Setting the pending input region - * has copy semantics, and the wl_region object can be destroyed - * immediately. A NULL wl_region causes the input region to be set - * to infinite. - */ static inline void wl_surface_set_input_region(struct wl_surface *wl_surface, struct wl_region *region) { @@ -4278,56 +1466,6 @@ wl_surface_set_input_region(struct wl_surface *wl_surface, struct wl_region *reg WL_SURFACE_SET_INPUT_REGION, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, region); } -/** - * @ingroup iface_wl_surface - * - * Surface state (input, opaque, and damage regions, attached buffers, - * etc.) is double-buffered. Protocol requests modify the pending state, - * as opposed to the active state in use by the compositor. - * - * All requests that need a commit to become effective are documented - * to affect double-buffered state. - * - * Other interfaces may add further double-buffered surface state. - * - * A commit request atomically creates a Content Update (CU) from the - * pending state, even if the pending state has not been touched. The - * content update is placed at the end of a per-surface queue until it - * becomes active. After commit, the new pending state is as documented for - * each related request. - * - * A CU is either a Desync Content Update (DCU) or a Sync Content Update - * (SCU). If the surface is effectively synchronized at the commit request, - * it is a SCU, otherwise a DCU. - * - * When a surface transitions from effectively synchronized to effectively - * desynchronized, all SCUs in its queue which are not reachable by any - * DCU become DCUs and dependency edges from outside the queue to these CUs - * are removed. - * - * See wl_subsurface for the definition of 'effectively synchronized' and - * 'effectively desynchronized'. - * - * When a CU is placed in the queue, the CU has a dependency on the CU in - * front of it and to the SCU at end of the queue of every direct child - * surface if that SCU exists and does not have another dependent. This can - * form a directed acyclic graph of CUs with dependencies as edges. - * - * In addition to surface state, the CU can have constraints that must be - * satisfied before it can be applied. Other interfaces may add CU - * constraints. - * - * All DCUs which do not have a SCU in front of themselves in their queue, - * are candidates. If the graph that's reachable by a candidate does not - * have any unsatisfied constraints, the entire graph must be applied - * atomically. - * - * When a CU is applied, the wl_buffer is applied before all other state. - * This means that all coordinates in double-buffered state are relative to - * the newly attached wl_buffers, except for wl_surface.attach itself. If - * there is no newly attached wl_buffer, the coordinates are relative to - * the previous content update. - */ static inline void wl_surface_commit(struct wl_surface *wl_surface) { @@ -4335,41 +1473,6 @@ wl_surface_commit(struct wl_surface *wl_surface) WL_SURFACE_COMMIT, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0); } -/** - * @ingroup iface_wl_surface - * - * This request sets the transformation that the client has already applied - * to the content of the buffer. The accepted values for the transform - * parameter are the values for wl_output.transform. - * - * The compositor applies the inverse of this transformation whenever it - * uses the buffer contents. - * - * Buffer transform is double-buffered state, see wl_surface.commit. - * - * A newly created surface has its buffer transformation set to normal. - * - * wl_surface.set_buffer_transform changes the pending buffer - * transformation. wl_surface.commit copies the pending buffer - * transformation to the current one. Otherwise, the pending and current - * values are never changed. - * - * The purpose of this request is to allow clients to render content - * according to the output transform, thus permitting the compositor to - * use certain optimizations even if the display is rotated. Using - * hardware overlays and scanning out a client buffer for fullscreen - * surfaces are examples of such optimizations. Those optimizations are - * highly dependent on the compositor implementation, so the use of this - * request should be considered on a case-by-case basis. - * - * Note that if the transform value includes 90 or 270 degree rotation, - * the width of the buffer will become the surface height and the height - * of the buffer will become the surface width. - * - * If transform is not one of the values from the - * wl_output.transform enum the invalid_transform protocol error - * is raised. - */ static inline void wl_surface_set_buffer_transform(struct wl_surface *wl_surface, int32_t transform) { @@ -4377,33 +1480,6 @@ wl_surface_set_buffer_transform(struct wl_surface *wl_surface, int32_t transform WL_SURFACE_SET_BUFFER_TRANSFORM, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, transform); } -/** - * @ingroup iface_wl_surface - * - * This request sets an optional scaling factor on how the compositor - * interprets the contents of the buffer attached to the window. - * - * Buffer scale is double-buffered state, see wl_surface.commit. - * - * A newly created surface has its buffer scale set to 1. - * - * wl_surface.set_buffer_scale changes the pending buffer scale. - * wl_surface.commit copies the pending buffer scale to the current one. - * Otherwise, the pending and current values are never changed. - * - * The purpose of this request is to allow clients to supply higher - * resolution buffer data for use on high resolution outputs. It is - * intended that you pick the same buffer scale as the scale of the - * output that the surface is displayed on. This means the compositor - * can avoid scaling when rendering the surface on that output. - * - * Note that if the scale is larger than 1, then you have to attach - * a buffer that is larger (by a factor of scale in each dimension) - * than the desired surface size. - * - * If scale is not greater than 0 the invalid_scale protocol error is - * raised. - */ static inline void wl_surface_set_buffer_scale(struct wl_surface *wl_surface, int32_t scale) { @@ -4411,42 +1487,6 @@ wl_surface_set_buffer_scale(struct wl_surface *wl_surface, int32_t scale) WL_SURFACE_SET_BUFFER_SCALE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, scale); } -/** - * @ingroup iface_wl_surface - * - * This request is used to describe the regions where the pending - * buffer is different from the current surface contents, and where - * the surface therefore needs to be repainted. The compositor - * ignores the parts of the damage that fall outside of the surface. - * - * Damage is double-buffered state, see wl_surface.commit. - * - * The damage rectangle is specified in buffer coordinates, - * where x and y specify the upper left corner of the damage rectangle. - * - * The initial value for pending damage is empty: no damage. - * wl_surface.damage_buffer adds pending damage: the new pending - * damage is the union of old pending damage and the given rectangle. - * - * wl_surface.commit assigns pending damage as the current damage, - * and clears pending damage. The server will clear the current - * damage as it repaints the surface. - * - * This request differs from wl_surface.damage in only one way - it - * takes damage in buffer coordinates instead of surface-local - * coordinates. While this generally is more intuitive than surface - * coordinates, it is especially desirable when using wp_viewport - * or when a drawing library (like EGL) is unaware of buffer scale - * and buffer transform. - * - * Note: Because buffer transformation changes and damage requests may - * be interleaved in the protocol stream, it is impossible to determine - * the actual mapping between surface and buffer damage until - * wl_surface.commit time. Therefore, compositors wishing to take both - * kinds of damage into account will have to accumulate damage from the - * two requests separately and only transform from one to the other - * after receiving the wl_surface.commit. - */ static inline void wl_surface_damage_buffer(struct wl_surface *wl_surface, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -4454,25 +1494,6 @@ wl_surface_damage_buffer(struct wl_surface *wl_surface, int32_t x, int32_t y, in WL_SURFACE_DAMAGE_BUFFER, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, x, y, width, height); } -/** - * @ingroup iface_wl_surface - * - * The x and y arguments specify the location of the new pending - * buffer's upper left corner, relative to the current buffer's upper - * left corner, in surface-local coordinates. In other words, the - * x and y, combined with the new surface size define in which - * directions the surface's size changes. - * - * The exact semantics of wl_surface.offset are role-specific. Refer to - * the documentation of specific roles for more information. - * - * Surface location offset is double-buffered state, see - * wl_surface.commit. - * - * This request is semantically equivalent to and the replaces the x and y - * arguments in the wl_surface.attach request in wl_surface versions prior - * to 5. See wl_surface.attach for details. - */ static inline void wl_surface_offset(struct wl_surface *wl_surface, int32_t x, int32_t y) { @@ -4480,28 +1501,6 @@ wl_surface_offset(struct wl_surface *wl_surface, int32_t x, int32_t y) WL_SURFACE_OFFSET, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, x, y); } -/** - * @ingroup iface_wl_surface - * - * Create a callback for the release of the buffer attached by the client - * with wl_surface.attach. - * - * The compositor will release the buffer when it has finished its usage of - * the underlying storage for the relevant commit. Once the client receives - * this event, and assuming the associated buffer is not pending release - * from other wl_surface.commit requests, the client can safely re-use the - * buffer. - * - * Release callbacks are double-buffered state, and will be associated - * with the pending buffer at wl_surface.commit time. - * - * The callback_data passed in the wl_callback.done event is unused and - * is always zero. - * - * Sending this request without attaching a non-null buffer in the same - * content update is a protocol error. The compositor will send the - * no_buffer error in this case. - */ static inline struct wl_callback * wl_surface_get_release(struct wl_surface *wl_surface) { @@ -4515,118 +1514,29 @@ wl_surface_get_release(struct wl_surface *wl_surface) #ifndef WL_SEAT_CAPABILITY_ENUM #define WL_SEAT_CAPABILITY_ENUM -/** - * @ingroup iface_wl_seat - * seat capability bitmask - * - * This is a bitmask of capabilities this seat has; if a member is - * set, then it is present on the seat. - */ enum wl_seat_capability { - /** - * the seat has pointer devices - */ WL_SEAT_CAPABILITY_POINTER = 1, - /** - * the seat has one or more keyboards - */ WL_SEAT_CAPABILITY_KEYBOARD = 2, - /** - * the seat has touch devices - */ WL_SEAT_CAPABILITY_TOUCH = 4, }; -#endif /* WL_SEAT_CAPABILITY_ENUM */ +#endif #ifndef WL_SEAT_ERROR_ENUM #define WL_SEAT_ERROR_ENUM -/** - * @ingroup iface_wl_seat - * wl_seat error values - * - * These errors can be emitted in response to wl_seat requests. - */ enum wl_seat_error { - /** - * get_pointer, get_keyboard or get_touch called on seat without the matching capability - */ WL_SEAT_ERROR_MISSING_CAPABILITY = 0, }; -#endif /* WL_SEAT_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_seat - * @struct wl_seat_listener - */ struct wl_seat_listener { - /** - * seat capabilities changed - * - * This is sent on binding to the seat global or whenever a seat - * gains or loses the pointer, keyboard or touch capabilities. The - * argument is a capability enum containing the complete set of - * capabilities this seat has. - * - * When the pointer capability is added, a client may create a - * wl_pointer object using the wl_seat.get_pointer request. This - * object will receive pointer events until the capability is - * removed in the future. - * - * When the pointer capability is removed, a client should destroy - * the wl_pointer objects associated with the seat where the - * capability was removed, using the wl_pointer.release request. No - * further pointer events will be received on these objects. - * - * In some compositors, if a seat regains the pointer capability - * and a client has a previously obtained wl_pointer object of - * version 4 or less, that object may start sending pointer events - * again. This behavior is considered a misinterpretation of the - * intended behavior and must not be relied upon by the client. - * wl_pointer objects of version 5 or later must not send events if - * created before the most recent event notifying the client of an - * added pointer capability. - * - * The above behavior also applies to wl_keyboard and wl_touch with - * the keyboard and touch capabilities, respectively. - * @param capabilities capabilities of the seat - */ void (*capabilities)(void *data, struct wl_seat *wl_seat, uint32_t capabilities); - /** - * unique identifier for this seat - * - * In a multi-seat configuration the seat name can be used by - * clients to help identify which physical devices the seat - * represents. - * - * The seat name is a UTF-8 string with no convention defined for - * its contents. Each name is unique among all wl_seat globals. The - * name is only guaranteed to be unique for the current compositor - * instance. - * - * The same seat names are used for all clients. Thus, the name can - * be shared across processes to refer to a specific wl_seat - * global. - * - * The name event is sent after binding to the seat global, and - * should be sent before announcing capabilities. This event is - * only sent once per seat object, and the name does not change - * over the lifetime of the wl_seat global. - * - * Compositors may re-use the same seat name if the wl_seat global - * is destroyed and re-created later. - * @param name seat identifier - * @since 2 - */ void (*name)(void *data, struct wl_seat *wl_seat, const char *name); }; -/** - * @ingroup iface_wl_seat - */ static inline int wl_seat_add_listener(struct wl_seat *wl_seat, const struct wl_seat_listener *listener, void *data) @@ -4640,40 +1550,20 @@ wl_seat_add_listener(struct wl_seat *wl_seat, #define WL_SEAT_GET_TOUCH 2 #define WL_SEAT_RELEASE 3 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_CAPABILITIES_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_NAME_SINCE_VERSION 2 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_POINTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_KEYBOARD_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_TOUCH_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_RELEASE_SINCE_VERSION 5 -/** @ingroup iface_wl_seat */ static inline void wl_seat_set_user_data(struct wl_seat *wl_seat, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_seat, user_data); } -/** @ingroup iface_wl_seat */ static inline void * wl_seat_get_user_data(struct wl_seat *wl_seat) { @@ -4686,25 +1576,12 @@ wl_seat_get_version(struct wl_seat *wl_seat) return wl_proxy_get_version((struct wl_proxy *) wl_seat); } -/** @ingroup iface_wl_seat */ static inline void wl_seat_destroy(struct wl_seat *wl_seat) { wl_proxy_destroy((struct wl_proxy *) wl_seat); } -/** - * @ingroup iface_wl_seat - * - * The ID provided will be initialized to the wl_pointer interface - * for this seat. - * - * This request only takes effect if the seat has the pointer - * capability, or has had the pointer capability in the past. - * It is a protocol violation to issue this request on a seat that has - * never had the pointer capability. The missing_capability error will - * be sent in this case. - */ static inline struct wl_pointer * wl_seat_get_pointer(struct wl_seat *wl_seat) { @@ -4716,18 +1593,6 @@ wl_seat_get_pointer(struct wl_seat *wl_seat) return (struct wl_pointer *) id; } -/** - * @ingroup iface_wl_seat - * - * The ID provided will be initialized to the wl_keyboard interface - * for this seat. - * - * This request only takes effect if the seat has the keyboard - * capability, or has had the keyboard capability in the past. - * It is a protocol violation to issue this request on a seat that has - * never had the keyboard capability. The missing_capability error will - * be sent in this case. - */ static inline struct wl_keyboard * wl_seat_get_keyboard(struct wl_seat *wl_seat) { @@ -4739,18 +1604,6 @@ wl_seat_get_keyboard(struct wl_seat *wl_seat) return (struct wl_keyboard *) id; } -/** - * @ingroup iface_wl_seat - * - * The ID provided will be initialized to the wl_touch interface - * for this seat. - * - * This request only takes effect if the seat has the touch - * capability, or has had the touch capability in the past. - * It is a protocol violation to issue this request on a seat that has - * never had the touch capability. The missing_capability error will - * be sent in this case. - */ static inline struct wl_touch * wl_seat_get_touch(struct wl_seat *wl_seat) { @@ -4762,12 +1615,6 @@ wl_seat_get_touch(struct wl_seat *wl_seat) return (struct wl_touch *) id; } -/** - * @ingroup iface_wl_seat - * - * Using this request a client can tell the server that it is not going to - * use the seat object anymore. - */ static inline void wl_seat_release(struct wl_seat *wl_seat) { @@ -4778,483 +1625,99 @@ wl_seat_release(struct wl_seat *wl_seat) #ifndef WL_POINTER_ERROR_ENUM #define WL_POINTER_ERROR_ENUM enum wl_pointer_error { - /** - * given wl_surface has another role - */ WL_POINTER_ERROR_ROLE = 0, }; -#endif /* WL_POINTER_ERROR_ENUM */ +#endif #ifndef WL_POINTER_BUTTON_STATE_ENUM #define WL_POINTER_BUTTON_STATE_ENUM -/** - * @ingroup iface_wl_pointer - * physical button state - * - * Describes the physical state of a button that produced the button - * event. - */ enum wl_pointer_button_state { - /** - * the button is not pressed - */ WL_POINTER_BUTTON_STATE_RELEASED = 0, - /** - * the button is pressed - */ WL_POINTER_BUTTON_STATE_PRESSED = 1, }; -#endif /* WL_POINTER_BUTTON_STATE_ENUM */ +#endif #ifndef WL_POINTER_AXIS_ENUM #define WL_POINTER_AXIS_ENUM -/** - * @ingroup iface_wl_pointer - * axis types - * - * Describes the axis types of scroll events. - */ enum wl_pointer_axis { - /** - * vertical axis - */ WL_POINTER_AXIS_VERTICAL_SCROLL = 0, - /** - * horizontal axis - */ WL_POINTER_AXIS_HORIZONTAL_SCROLL = 1, }; -#endif /* WL_POINTER_AXIS_ENUM */ +#endif #ifndef WL_POINTER_AXIS_SOURCE_ENUM #define WL_POINTER_AXIS_SOURCE_ENUM -/** - * @ingroup iface_wl_pointer - * axis source types - * - * Describes the source types for axis events. This indicates to the - * client how an axis event was physically generated; a client may - * adjust the user interface accordingly. For example, scroll events - * from a "finger" source may be in a smooth coordinate space with - * kinetic scrolling whereas a "wheel" source may be in discrete steps - * of a number of lines. - * - * The "continuous" axis source is a device generating events in a - * continuous coordinate space, but using something other than a - * finger. One example for this source is button-based scrolling where - * the vertical motion of a device is converted to scroll events while - * a button is held down. - * - * The "wheel tilt" axis source indicates that the actual device is a - * wheel but the scroll event is not caused by a rotation but a - * (usually sideways) tilt of the wheel. - */ enum wl_pointer_axis_source { - /** - * a physical wheel rotation - */ WL_POINTER_AXIS_SOURCE_WHEEL = 0, - /** - * finger on a touch surface - */ WL_POINTER_AXIS_SOURCE_FINGER = 1, - /** - * continuous coordinate space - */ WL_POINTER_AXIS_SOURCE_CONTINUOUS = 2, - /** - * a physical wheel tilt - * @since 6 - */ WL_POINTER_AXIS_SOURCE_WHEEL_TILT = 3, }; -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SOURCE_WHEEL_TILT_SINCE_VERSION 6 -#endif /* WL_POINTER_AXIS_SOURCE_ENUM */ +#endif #ifndef WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM #define WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM -/** - * @ingroup iface_wl_pointer - * axis relative direction - * - * This specifies the direction of the physical motion that caused a - * wl_pointer.axis event, relative to the wl_pointer.axis direction. - */ enum wl_pointer_axis_relative_direction { - /** - * physical motion matches axis direction - */ WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL = 0, - /** - * physical motion is the inverse of the axis direction - */ WL_POINTER_AXIS_RELATIVE_DIRECTION_INVERTED = 1, }; -#endif /* WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM */ +#endif -/** - * @ingroup iface_wl_pointer - * @struct wl_pointer_listener - */ struct wl_pointer_listener { - /** - * enter event - * - * Notification that this seat's pointer is focused on a certain - * surface. - * - * When a seat's focus enters a surface, the pointer image is - * undefined and a client should respond to this event by setting - * an appropriate pointer image with the set_cursor request. - * @param serial serial number of the enter event - * @param surface surface entered by the pointer - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ void (*enter)(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t surface_x, wl_fixed_t surface_y); - /** - * leave event - * - * Notification that this seat's pointer is no longer focused on - * a certain surface. - * - * The leave notification is sent before the enter notification for - * the new focus. - * @param serial serial number of the leave event - * @param surface surface left by the pointer - */ void (*leave)(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface); - /** - * pointer motion event - * - * Notification of pointer location change. The arguments - * surface_x and surface_y are the location relative to the focused - * surface. - * @param time timestamp with millisecond granularity - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ void (*motion)(void *data, struct wl_pointer *wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y); - /** - * pointer button event - * - * Mouse button click and release notifications. - * - * The location of the click is given by the last motion, warp or - * enter event. The time argument is a timestamp with millisecond - * granularity, with an undefined base. - * - * The button is a button code as defined in the Linux kernel's - * linux/input-event-codes.h header file, e.g. BTN_LEFT. - * - * Any 16-bit button code value is reserved for future additions to - * the kernel's event code list. All other button codes above - * 0xFFFF are currently undefined but may be used in future - * versions of this protocol. - * @param serial serial number of the button event - * @param time timestamp with millisecond granularity - * @param button button that produced the event - * @param state physical state of the button - */ void (*button)(void *data, struct wl_pointer *wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state); - /** - * axis event - * - * Scroll and other axis notifications. - * - * For scroll events (vertical and horizontal scroll axes), the - * value parameter is the length of a vector along the specified - * axis in a coordinate space identical to those of motion events, - * representing a relative movement along the specified axis. - * - * For devices that support movements non-parallel to axes multiple - * axis events will be emitted. - * - * When applicable, for example for touch pads, the server can - * choose to emit scroll events where the motion vector is - * equivalent to a motion event vector. - * - * When applicable, a client can transform its content relative to - * the scroll distance. - * @param time timestamp with millisecond granularity - * @param axis axis type - * @param value length of vector in surface-local coordinate space - */ void (*axis)(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value); - /** - * end of a pointer event sequence - * - * Indicates the end of a set of events that logically belong - * together. A client is expected to accumulate the data in all - * events within the frame before proceeding. - * - * All wl_pointer events before a wl_pointer.frame event belong - * logically together. For example, in a diagonal scroll motion the - * compositor will send an optional wl_pointer.axis_source event, - * two wl_pointer.axis events (horizontal and vertical) and finally - * a wl_pointer.frame event. The client may use this information to - * calculate a diagonal vector for scrolling. - * - * When multiple wl_pointer.axis events occur within the same - * frame, the motion vector is the combined motion of all events. - * When a wl_pointer.axis and a wl_pointer.axis_stop event occur - * within the same frame, this indicates that axis movement in one - * axis has stopped but continues in the other axis. When multiple - * wl_pointer.axis_stop events occur within the same frame, this - * indicates that these axes stopped in the same instance. - * - * A wl_pointer.frame event is sent for every logical event group, - * even if the group only contains a single wl_pointer event. - * Specifically, a client may get a sequence: motion, frame, - * button, frame, axis, frame, axis_stop, frame. - * - * The wl_pointer.enter and wl_pointer.leave events are logical - * events generated by the compositor and not the hardware. These - * events are also grouped by a wl_pointer.frame. When a pointer - * moves from one surface to another, a compositor should group the - * wl_pointer.leave event within the same wl_pointer.frame. - * However, a client must not rely on wl_pointer.leave and - * wl_pointer.enter being in the same wl_pointer.frame. - * Compositor-specific policies may require the wl_pointer.leave - * and wl_pointer.enter event being split across multiple - * wl_pointer.frame groups. - * @since 5 - */ void (*frame)(void *data, struct wl_pointer *wl_pointer); - /** - * axis source event - * - * Source information for scroll and other axes. - * - * This event does not occur on its own. It is sent before a - * wl_pointer.frame event and carries the source information for - * all events within that frame. - * - * The source specifies how this event was generated. If the source - * is wl_pointer.axis_source.finger, a wl_pointer.axis_stop event - * will be sent when the user lifts the finger off the device. - * - * If the source is wl_pointer.axis_source.wheel, - * wl_pointer.axis_source.wheel_tilt or - * wl_pointer.axis_source.continuous, a wl_pointer.axis_stop event - * may or may not be sent. Whether a compositor sends an axis_stop - * event for these sources is hardware-specific and - * implementation-dependent; clients must not rely on receiving an - * axis_stop event for these scroll sources and should treat scroll - * sequences from these scroll sources as unterminated by default. - * - * This event is optional. If the source is unknown for a - * particular axis event sequence, no event is sent. Only one - * wl_pointer.axis_source event is permitted per frame. - * - * The order of wl_pointer.axis_discrete and wl_pointer.axis_source - * is not guaranteed. - * @param axis_source source of the axis event - * @since 5 - */ void (*axis_source)(void *data, struct wl_pointer *wl_pointer, uint32_t axis_source); - /** - * axis stop event - * - * Stop notification for scroll and other axes. - * - * For some wl_pointer.axis_source types, a wl_pointer.axis_stop - * event is sent to notify a client that the axis sequence has - * terminated. This enables the client to implement kinetic - * scrolling. See the wl_pointer.axis_source documentation for - * information on when this event may be generated. - * - * Any wl_pointer.axis events with the same axis_source after this - * event should be considered as the start of a new axis motion. - * - * The timestamp is to be interpreted identical to the timestamp in - * the wl_pointer.axis event. The timestamp value may be the same - * as a preceding wl_pointer.axis event. - * @param time timestamp with millisecond granularity - * @param axis the axis stopped with this event - * @since 5 - */ void (*axis_stop)(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis); - /** - * axis click event - * - * Discrete step information for scroll and other axes. - * - * This event carries the axis value of the wl_pointer.axis event - * in discrete steps (e.g. mouse wheel clicks). - * - * This event is deprecated with wl_pointer version 8 - this event - * is not sent to clients supporting version 8 or later. - * - * This event does not occur on its own, it is coupled with a - * wl_pointer.axis event that represents this axis value on a - * continuous scale. The protocol guarantees that each - * axis_discrete event is always followed by exactly one axis event - * with the same axis number within the same wl_pointer.frame. Note - * that the protocol allows for other events to occur between the - * axis_discrete and its coupled axis event, including other - * axis_discrete or axis events. A wl_pointer.frame must not - * contain more than one axis_discrete event per axis type. - * - * This event is optional; continuous scrolling devices like - * two-finger scrolling on touchpads do not have discrete steps and - * do not generate this event. - * - * The discrete value carries the directional information. e.g. a - * value of -2 is two steps towards the negative direction of this - * axis. - * - * The axis number is identical to the axis number in the - * associated axis event. - * - * The order of wl_pointer.axis_discrete and wl_pointer.axis_source - * is not guaranteed. - * @param axis axis type - * @param discrete number of steps - * @since 5 - * @deprecated Deprecated since version 8 - */ void (*axis_discrete)(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t discrete); - /** - * axis high-resolution scroll event - * - * Discrete high-resolution scroll information. - * - * This event carries high-resolution wheel scroll information, - * with each multiple of 120 representing one logical scroll step - * (a wheel detent). For example, an axis_value120 of 30 is one - * quarter of a logical scroll step in the positive direction, a - * value120 of -240 are two logical scroll steps in the negative - * direction within the same hardware event. Clients that rely on - * discrete scrolling should accumulate the value120 to multiples - * of 120 before processing the event. - * - * The value120 must not be zero. - * - * This event replaces the wl_pointer.axis_discrete event in - * clients supporting wl_pointer version 8 or later. - * - * Where a wl_pointer.axis_source event occurs in the same - * wl_pointer.frame, the axis source applies to this event. - * - * The order of wl_pointer.axis_value120 and wl_pointer.axis_source - * is not guaranteed. - * @param axis axis type - * @param value120 scroll distance as fraction of 120 - * @since 8 - */ void (*axis_value120)(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t value120); - /** - * axis relative physical direction event - * - * Relative directional information of the entity causing the - * axis motion. - * - * For a wl_pointer.axis event, the - * wl_pointer.axis_relative_direction event specifies the movement - * direction of the entity causing the wl_pointer.axis event. For - * example: - if a user's fingers on a touchpad move down and this - * causes a wl_pointer.axis vertical_scroll down event, the - * physical direction is 'identical' - if a user's fingers on a - * touchpad move down and this causes a wl_pointer.axis - * vertical_scroll up scroll up event ('natural scrolling'), the - * physical direction is 'inverted'. - * - * A client may use this information to adjust scroll motion of - * components. Specifically, enabling natural scrolling causes the - * content to change direction compared to traditional scrolling. - * Some widgets like volume control sliders should usually match - * the physical direction regardless of whether natural scrolling - * is active. This event enables clients to match the scroll - * direction of a widget to the physical direction. - * - * This event does not occur on its own, it is coupled with a - * wl_pointer.axis event that represents this axis value. The - * protocol guarantees that each axis_relative_direction event is - * always followed by exactly one axis event with the same axis - * number within the same wl_pointer.frame. Note that the protocol - * allows for other events to occur between the - * axis_relative_direction and its coupled axis event. - * - * The axis number is identical to the axis number in the - * associated axis event. - * - * The order of wl_pointer.axis_relative_direction, - * wl_pointer.axis_discrete and wl_pointer.axis_source is not - * guaranteed. - * @param axis axis type - * @param direction physical direction relative to axis motion - * @since 9 - */ void (*axis_relative_direction)(void *data, struct wl_pointer *wl_pointer, uint32_t axis, uint32_t direction); - /** - * pointer warp event - * - * Notification of pointer location change within a surface. - * - * This location change is not due to events on the input device, - * but because either the surface under the pointer was moved and - * thus the relative position of the pointer changed, or because - * the compositor changed the pointer position in response to an - * event like pointer confinement being exited. - * - * The arguments surface_x and surface_y are the location relative - * to the focused surface. - * - * This event must not occur in the same wl_pointer.frame as a - * wl_pointer.enter or wl_pointer.motion event. - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - * @since 11 - */ void (*warp)(void *data, struct wl_pointer *wl_pointer, wl_fixed_t surface_x, wl_fixed_t surface_y); }; -/** - * @ingroup iface_wl_pointer - */ static inline int wl_pointer_add_listener(struct wl_pointer *wl_pointer, const struct wl_pointer_listener *listener, void *data) @@ -5266,72 +1729,28 @@ wl_pointer_add_listener(struct wl_pointer *wl_pointer, #define WL_POINTER_SET_CURSOR 0 #define WL_POINTER_RELEASE 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_BUTTON_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_FRAME_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SOURCE_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_STOP_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_DISCRETE_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_VALUE120_SINCE_VERSION 8 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_RELATIVE_DIRECTION_SINCE_VERSION 9 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_WARP_SINCE_VERSION 11 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_SET_CURSOR_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_RELEASE_SINCE_VERSION 3 -/** @ingroup iface_wl_pointer */ static inline void wl_pointer_set_user_data(struct wl_pointer *wl_pointer, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_pointer, user_data); } -/** @ingroup iface_wl_pointer */ static inline void * wl_pointer_get_user_data(struct wl_pointer *wl_pointer) { @@ -5344,50 +1763,12 @@ wl_pointer_get_version(struct wl_pointer *wl_pointer) return wl_proxy_get_version((struct wl_proxy *) wl_pointer); } -/** @ingroup iface_wl_pointer */ static inline void wl_pointer_destroy(struct wl_pointer *wl_pointer) { wl_proxy_destroy((struct wl_proxy *) wl_pointer); } -/** - * @ingroup iface_wl_pointer - * - * Set the pointer surface, i.e., the surface that contains the - * pointer image (cursor). This request gives the surface the role - * of a cursor. If the surface already has another role, it raises - * a protocol error. - * - * The cursor actually changes only if the pointer - * focus for this device is one of the requesting client's surfaces - * or the surface parameter is the current pointer surface. If - * there was a previous surface set with this request it is - * replaced. If surface is NULL, the pointer image is hidden. - * - * The parameters hotspot_x and hotspot_y define the position of - * the pointer surface relative to the pointer location. Its - * top-left corner is always at (x, y) - (hotspot_x, hotspot_y), - * where (x, y) are the coordinates of the pointer location, in - * surface-local coordinates. - * - * On wl_surface.offset requests to the pointer surface, hotspot_x - * and hotspot_y are decremented by the x and y parameters - * passed to the request. The offset must be applied by - * wl_surface.commit as usual. - * - * The hotspot can also be updated by passing the currently set - * pointer surface to this request with new values for hotspot_x - * and hotspot_y. - * - * The input region is ignored for wl_surfaces with the role of - * a cursor. When the use as a cursor ends, the wl_surface is - * unmapped. - * - * The serial parameter must match the latest wl_pointer.enter - * serial number sent to the client. Otherwise the request will be - * ignored. - */ static inline void wl_pointer_set_cursor(struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, int32_t hotspot_x, int32_t hotspot_y) { @@ -5395,15 +1776,6 @@ wl_pointer_set_cursor(struct wl_pointer *wl_pointer, uint32_t serial, struct wl_ WL_POINTER_SET_CURSOR, NULL, wl_proxy_get_version((struct wl_proxy *) wl_pointer), 0, serial, surface, hotspot_x, hotspot_y); } -/** - * @ingroup iface_wl_pointer - * - * Using this request a client can tell the server that it is not going to - * use the pointer object anymore. - * - * This request destroys the pointer proxy object, so clients must not call - * wl_pointer_destroy() after using this request. - */ static inline void wl_pointer_release(struct wl_pointer *wl_pointer) { @@ -5413,191 +1785,43 @@ wl_pointer_release(struct wl_pointer *wl_pointer) #ifndef WL_KEYBOARD_KEYMAP_FORMAT_ENUM #define WL_KEYBOARD_KEYMAP_FORMAT_ENUM -/** - * @ingroup iface_wl_keyboard - * keyboard mapping format - * - * This specifies the format of the keymap provided to the - * client with the wl_keyboard.keymap event. - */ enum wl_keyboard_keymap_format { - /** - * no keymap; client must understand how to interpret the raw keycode - */ WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP = 0, - /** - * libxkbcommon compatible, null-terminated string; to determine the xkb keycode, clients must add 8 to the key event keycode - */ WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 = 1, }; -#endif /* WL_KEYBOARD_KEYMAP_FORMAT_ENUM */ +#endif #ifndef WL_KEYBOARD_KEY_STATE_ENUM #define WL_KEYBOARD_KEY_STATE_ENUM -/** - * @ingroup iface_wl_keyboard - * physical key state - * - * Describes the physical state of a key that produced the key event. - * - * Since version 10, the key can be in a "repeated" pseudo-state which - * means the same as "pressed", but is used to signal repetition in the - * key event. - * - * The key may only enter the repeated state after entering the pressed - * state and before entering the released state. This event may be - * generated multiple times while the key is down. - */ enum wl_keyboard_key_state { - /** - * key is not pressed - */ WL_KEYBOARD_KEY_STATE_RELEASED = 0, - /** - * key is pressed - */ WL_KEYBOARD_KEY_STATE_PRESSED = 1, - /** - * key was repeated - * @since 10 - */ WL_KEYBOARD_KEY_STATE_REPEATED = 2, }; -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEY_STATE_REPEATED_SINCE_VERSION 10 -#endif /* WL_KEYBOARD_KEY_STATE_ENUM */ +#endif -/** - * @ingroup iface_wl_keyboard - * @struct wl_keyboard_listener - */ struct wl_keyboard_listener { - /** - * keyboard mapping - * - * This event provides a file descriptor to the client which can - * be memory-mapped in read-only mode to provide a keyboard mapping - * description. - * - * From version 7 onwards, the fd must be mapped with MAP_PRIVATE - * by the recipient, as MAP_SHARED may fail. - * @param format keymap format - * @param fd keymap file descriptor - * @param size keymap size, in bytes - */ void (*keymap)(void *data, struct wl_keyboard *wl_keyboard, uint32_t format, int32_t fd, uint32_t size); - /** - * enter event - * - * Notification that this seat's keyboard focus is on a certain - * surface. - * - * The compositor must send the wl_keyboard.modifiers event after - * this event. - * - * In the wl_keyboard logical state, this event sets the active - * surface to the surface argument and the keys currently logically - * down to the keys in the keys argument. The compositor must not - * send this event if the wl_keyboard already had an active surface - * immediately before this event. - * - * Clients should not use the list of pressed keys to emulate - * key-press events. The order of keys in the list is unspecified. - * @param serial serial number of the enter event - * @param surface surface gaining keyboard focus - * @param keys the keys currently logically down - */ void (*enter)(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface, struct wl_array *keys); - /** - * leave event - * - * Notification that this seat's keyboard focus is no longer on a - * certain surface. - * - * The leave notification is sent before the enter notification for - * the new focus. - * - * In the wl_keyboard logical state, this event resets all values - * to their defaults. The compositor must not send this event if - * the active surface of the wl_keyboard was not equal to the - * surface argument immediately before this event. - * @param serial serial number of the leave event - * @param surface surface that lost keyboard focus - */ void (*leave)(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface); - /** - * key event - * - * A key was pressed or released. The time argument is a - * timestamp with millisecond granularity, with an undefined base. - * - * The key is a platform-specific key code that can be interpreted - * by feeding it to the keyboard mapping (see the keymap event). - * - * If this event produces a change in modifiers, then the resulting - * wl_keyboard.modifiers event must be sent after this event. - * - * In the wl_keyboard logical state, this event adds the key to the - * keys currently logically down (if the state argument is pressed) - * or removes the key from the keys currently logically down (if - * the state argument is released). The compositor must not send - * this event if the wl_keyboard did not have an active surface - * immediately before this event. The compositor must not send this - * event if state is pressed (resp. released) and the key was - * already logically down (resp. was not logically down) - * immediately before this event. - * - * Since version 10, compositors may send key events with the - * "repeated" key state when a wl_keyboard.repeat_info event with a - * rate argument of 0 has been received. This allows the compositor - * to take over the responsibility of key repetition. - * @param serial serial number of the key event - * @param time timestamp with millisecond granularity - * @param key key that produced the event - * @param state physical state of the key - */ void (*key)(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state); - /** - * modifier and group state - * - * Notifies clients that the modifier and/or group state has - * changed, and it should update its local state. - * - * The compositor may send this event without a surface of the - * client having keyboard focus, for example to tie modifier - * information to pointer focus instead. If a modifier event with - * pressed modifiers is sent without a prior enter event, the - * client can assume the modifier state is valid until it receives - * the next wl_keyboard.modifiers event. In order to reset the - * modifier state again, the compositor can send a - * wl_keyboard.modifiers event with no pressed modifiers. - * - * In the wl_keyboard logical state, this event updates the - * modifiers and group. - * @param serial serial number of the modifiers event - * @param mods_depressed depressed modifiers - * @param mods_latched latched modifiers - * @param mods_locked locked modifiers - * @param group keyboard layout - */ void (*modifiers)(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, @@ -5605,35 +1829,12 @@ struct wl_keyboard_listener { uint32_t mods_latched, uint32_t mods_locked, uint32_t group); - /** - * repeat rate and delay - * - * Informs the client about the keyboard's repeat rate and delay. - * - * This event is sent as soon as the wl_keyboard object has been - * created, and is guaranteed to be received by the client before - * any key press event. - * - * Negative values for either rate or delay are illegal. A rate of - * zero will disable any repeating (regardless of the value of - * delay). - * - * This event can be sent later on as well with a new value if - * necessary, so clients should continue listening for the event - * past the creation of wl_keyboard. - * @param rate the rate of repeating keys in characters per second - * @param delay delay in milliseconds since key down until repeating starts - * @since 4 - */ void (*repeat_info)(void *data, struct wl_keyboard *wl_keyboard, int32_t rate, int32_t delay); }; -/** - * @ingroup iface_wl_keyboard - */ static inline int wl_keyboard_add_listener(struct wl_keyboard *wl_keyboard, const struct wl_keyboard_listener *listener, void *data) @@ -5644,44 +1845,21 @@ wl_keyboard_add_listener(struct wl_keyboard *wl_keyboard, #define WL_KEYBOARD_RELEASE 0 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEYMAP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_MODIFIERS_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_REPEAT_INFO_SINCE_VERSION 4 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_RELEASE_SINCE_VERSION 3 -/** @ingroup iface_wl_keyboard */ static inline void wl_keyboard_set_user_data(struct wl_keyboard *wl_keyboard, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_keyboard, user_data); } -/** @ingroup iface_wl_keyboard */ static inline void * wl_keyboard_get_user_data(struct wl_keyboard *wl_keyboard) { @@ -5694,16 +1872,12 @@ wl_keyboard_get_version(struct wl_keyboard *wl_keyboard) return wl_proxy_get_version((struct wl_proxy *) wl_keyboard); } -/** @ingroup iface_wl_keyboard */ static inline void wl_keyboard_destroy(struct wl_keyboard *wl_keyboard) { wl_proxy_destroy((struct wl_proxy *) wl_keyboard); } -/** - * @ingroup iface_wl_keyboard - */ static inline void wl_keyboard_release(struct wl_keyboard *wl_keyboard) { @@ -5711,25 +1885,7 @@ wl_keyboard_release(struct wl_keyboard *wl_keyboard) WL_KEYBOARD_RELEASE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_keyboard), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_touch - * @struct wl_touch_listener - */ struct wl_touch_listener { - /** - * touch down event and beginning of a touch sequence - * - * A new touch point has appeared on the surface. This touch - * point is assigned a unique ID. Future events from this touch - * point reference this ID. The ID ceases to be valid after a touch - * up event and may be reused in the future. - * @param serial serial number of the touch down event - * @param time timestamp with millisecond granularity - * @param surface surface touched - * @param id the unique ID of this touch point - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ void (*down)(void *data, struct wl_touch *wl_touch, uint32_t serial, @@ -5738,142 +1894,32 @@ struct wl_touch_listener { int32_t id, wl_fixed_t x, wl_fixed_t y); - /** - * end of a touch event sequence - * - * The touch point has disappeared. No further events will be - * sent for this touch point and the touch point's ID is released - * and may be reused in a future touch down event. - * @param serial serial number of the touch up event - * @param time timestamp with millisecond granularity - * @param id the unique ID of this touch point - */ void (*up)(void *data, struct wl_touch *wl_touch, uint32_t serial, uint32_t time, int32_t id); - /** - * update of touch point coordinates - * - * A touch point has changed coordinates. - * @param time timestamp with millisecond granularity - * @param id the unique ID of this touch point - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ void (*motion)(void *data, struct wl_touch *wl_touch, uint32_t time, int32_t id, wl_fixed_t x, wl_fixed_t y); - /** - * end of touch frame event - * - * Indicates the end of a set of events that logically belong - * together. A client is expected to accumulate the data in all - * events within the frame before proceeding. - * - * A wl_touch.frame terminates at least one event but otherwise no - * guarantee is provided about the set of events within a frame. A - * client must assume that any state not updated in a frame is - * unchanged from the previously known state. - */ void (*frame)(void *data, struct wl_touch *wl_touch); - /** - * touch session cancelled - * - * Sent if the compositor decides the touch stream is a global - * gesture. No further events are sent to the clients from that - * particular gesture. Touch cancellation applies to all touch - * points currently active on this client's surface. The client is - * responsible for finalizing the touch points, future touch points - * on this surface may reuse the touch point ID. - * - * No frame event is required after the cancel event. - */ void (*cancel)(void *data, struct wl_touch *wl_touch); - /** - * update shape of touch point - * - * Sent when a touchpoint has changed its shape. - * - * This event does not occur on its own. It is sent before a - * wl_touch.frame event and carries the new shape information for - * any previously reported, or new touch points of that frame. - * - * Other events describing the touch point such as wl_touch.down, - * wl_touch.motion or wl_touch.orientation may be sent within the - * same wl_touch.frame. A client should treat these events as a - * single logical touch point update. The order of wl_touch.shape, - * wl_touch.orientation and wl_touch.motion is not guaranteed. A - * wl_touch.down event is guaranteed to occur before the first - * wl_touch.shape event for this touch ID but both events may occur - * within the same wl_touch.frame. - * - * A touchpoint shape is approximated by an ellipse through the - * major and minor axis length. The major axis length describes the - * longer diameter of the ellipse, while the minor axis length - * describes the shorter diameter. Major and minor are orthogonal - * and both are specified in surface-local coordinates. The center - * of the ellipse is always at the touchpoint location as reported - * by wl_touch.down or wl_touch.motion. - * - * This event is only sent by the compositor if the touch device - * supports shape reports. The client has to make reasonable - * assumptions about the shape if it did not receive this event. - * @param id the unique ID of this touch point - * @param major length of the major axis in surface-local coordinates - * @param minor length of the minor axis in surface-local coordinates - * @since 6 - */ void (*shape)(void *data, struct wl_touch *wl_touch, int32_t id, wl_fixed_t major, wl_fixed_t minor); - /** - * update orientation of touch point - * - * Sent when a touchpoint has changed its orientation. - * - * This event does not occur on its own. It is sent before a - * wl_touch.frame event and carries the new shape information for - * any previously reported, or new touch points of that frame. - * - * Other events describing the touch point such as wl_touch.down, - * wl_touch.motion or wl_touch.shape may be sent within the same - * wl_touch.frame. A client should treat these events as a single - * logical touch point update. The order of wl_touch.shape, - * wl_touch.orientation and wl_touch.motion is not guaranteed. A - * wl_touch.down event is guaranteed to occur before the first - * wl_touch.orientation event for this touch ID but both events may - * occur within the same wl_touch.frame. - * - * The orientation describes the clockwise angle of a touchpoint's - * major axis to the positive surface y-axis and is normalized to - * the -180 to +180 degree range. The granularity of orientation - * depends on the touch device, some devices only support binary - * rotation values between 0 and 90 degrees. - * - * This event is only sent by the compositor if the touch device - * supports orientation reports. - * @param id the unique ID of this touch point - * @param orientation angle between major axis and positive surface y-axis in degrees - * @since 6 - */ void (*orientation)(void *data, struct wl_touch *wl_touch, int32_t id, wl_fixed_t orientation); }; -/** - * @ingroup iface_wl_touch - */ static inline int wl_touch_add_listener(struct wl_touch *wl_touch, const struct wl_touch_listener *listener, void *data) @@ -5884,48 +1930,22 @@ wl_touch_add_listener(struct wl_touch *wl_touch, #define WL_TOUCH_RELEASE 0 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_DOWN_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_UP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_FRAME_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_CANCEL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_SHAPE_SINCE_VERSION 6 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_ORIENTATION_SINCE_VERSION 6 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_RELEASE_SINCE_VERSION 3 -/** @ingroup iface_wl_touch */ static inline void wl_touch_set_user_data(struct wl_touch *wl_touch, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_touch, user_data); } -/** @ingroup iface_wl_touch */ static inline void * wl_touch_get_user_data(struct wl_touch *wl_touch) { @@ -5938,16 +1958,12 @@ wl_touch_get_version(struct wl_touch *wl_touch) return wl_proxy_get_version((struct wl_proxy *) wl_touch); } -/** @ingroup iface_wl_touch */ static inline void wl_touch_destroy(struct wl_touch *wl_touch) { wl_proxy_destroy((struct wl_proxy *) wl_touch); } -/** - * @ingroup iface_wl_touch - */ static inline void wl_touch_release(struct wl_touch *wl_touch) { @@ -5957,153 +1973,39 @@ wl_touch_release(struct wl_touch *wl_touch) #ifndef WL_OUTPUT_SUBPIXEL_ENUM #define WL_OUTPUT_SUBPIXEL_ENUM -/** - * @ingroup iface_wl_output - * subpixel geometry information - * - * This enumeration describes how the physical - * pixels on an output are laid out. - */ enum wl_output_subpixel { - /** - * unknown geometry - */ WL_OUTPUT_SUBPIXEL_UNKNOWN = 0, - /** - * no geometry - */ WL_OUTPUT_SUBPIXEL_NONE = 1, - /** - * horizontal RGB - */ WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB = 2, - /** - * horizontal BGR - */ WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR = 3, - /** - * vertical RGB - */ WL_OUTPUT_SUBPIXEL_VERTICAL_RGB = 4, - /** - * vertical BGR - */ WL_OUTPUT_SUBPIXEL_VERTICAL_BGR = 5, }; -#endif /* WL_OUTPUT_SUBPIXEL_ENUM */ +#endif #ifndef WL_OUTPUT_TRANSFORM_ENUM #define WL_OUTPUT_TRANSFORM_ENUM -/** - * @ingroup iface_wl_output - * transformation applied to buffer contents - * - * This describes transformations that clients and compositors apply to - * buffer contents. - * - * The flipped values correspond to an initial flip around a - * vertical axis followed by rotation. - * - * The purpose is mainly to allow clients to render accordingly and - * tell the compositor, so that for fullscreen surfaces, the - * compositor will still be able to scan out directly from client - * surfaces. - */ enum wl_output_transform { - /** - * no transform - */ WL_OUTPUT_TRANSFORM_NORMAL = 0, - /** - * 90 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_90 = 1, - /** - * 180 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_180 = 2, - /** - * 270 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_270 = 3, - /** - * 180 degree flip around a vertical axis - */ WL_OUTPUT_TRANSFORM_FLIPPED = 4, - /** - * flip and rotate 90 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_90 = 5, - /** - * flip and rotate 180 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_180 = 6, - /** - * flip and rotate 270 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_270 = 7, }; -#endif /* WL_OUTPUT_TRANSFORM_ENUM */ +#endif #ifndef WL_OUTPUT_MODE_ENUM #define WL_OUTPUT_MODE_ENUM -/** - * @ingroup iface_wl_output - * mode information - * - * These flags describe properties of an output mode. - * They are used in the flags bitfield of the mode event. - */ enum wl_output_mode { - /** - * indicates this is the current mode - */ WL_OUTPUT_MODE_CURRENT = 0x1, - /** - * indicates this is the preferred mode - */ WL_OUTPUT_MODE_PREFERRED = 0x2, }; -#endif /* WL_OUTPUT_MODE_ENUM */ +#endif -/** - * @ingroup iface_wl_output - * @struct wl_output_listener - */ struct wl_output_listener { - /** - * properties of the output - * - * The geometry event describes geometric properties of the - * output. The event is sent when binding to the output object and - * whenever any of the properties change. - * - * The physical size can be set to zero if it doesn't make sense - * for this output (e.g. for projectors or virtual outputs). - * - * The geometry event will be followed by a done event (starting - * from version 2). - * - * Clients should use wl_surface.preferred_buffer_transform instead - * of the transform advertised by this event to find the preferred - * buffer transform to use for a surface. - * - * Note: wl_output only advertises partial information about the - * output position and identification. Some compositors, for - * instance those not implementing a desktop-style output layout or - * those exposing virtual outputs, might fake this information. - * Instead of using x and y, clients should use - * xdg_output.logical_position. Instead of using make and model, - * clients should use name and description. - * @param x x position within the global compositor space - * @param y y position within the global compositor space - * @param physical_width width in millimeters of the output - * @param physical_height height in millimeters of the output - * @param subpixel subpixel orientation of the output - * @param make textual description of the manufacturer - * @param model textual description of the model - * @param transform additional transformation applied to buffer contents during presentation - */ void (*geometry)(void *data, struct wl_output *wl_output, int32_t x, @@ -6114,159 +2016,25 @@ struct wl_output_listener { const char *make, const char *model, int32_t transform); - /** - * advertise available modes for the output - * - * The mode event describes an available mode for the output. - * - * The event is sent when binding to the output object and there - * will always be one mode, the current mode. The event is sent - * again if an output changes mode, for the mode that is now - * current. In other words, the current mode is always the last - * mode that was received with the current flag set. - * - * Non-current modes are deprecated. A compositor can decide to - * only advertise the current mode and never send other modes. - * Clients should not rely on non-current modes. - * - * The size of a mode is given in physical hardware units of the - * output device. This is not necessarily the same as the output - * size in the global compositor space. For instance, the output - * may be scaled, as described in wl_output.scale, or transformed, - * as described in wl_output.transform. Clients willing to retrieve - * the output size in the global compositor space should use - * xdg_output.logical_size instead. - * - * The vertical refresh rate can be set to zero if it doesn't make - * sense for this output (e.g. for virtual outputs). - * - * The mode event will be followed by a done event (starting from - * version 2). - * - * Clients should not use the refresh rate to schedule frames. - * Instead, they should use the wl_surface.frame event or the - * presentation-time protocol. - * - * Note: this information is not always meaningful for all outputs. - * Some compositors, such as those exposing virtual outputs, might - * fake the refresh rate or the size. - * @param flags bitfield of mode flags - * @param width width of the mode in hardware units - * @param height height of the mode in hardware units - * @param refresh vertical refresh rate in mHz - */ void (*mode)(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh); - /** - * sent all information about output - * - * This event is sent after all other properties have been sent - * after binding to the output object and after any other property - * changes done after that. This allows changes to the output - * properties to be seen as atomic, even if they happen via - * multiple events. - * @since 2 - */ void (*done)(void *data, struct wl_output *wl_output); - /** - * output scaling properties - * - * This event contains scaling geometry information that is not - * in the geometry event. It may be sent after binding the output - * object or if the output scale changes later. The compositor will - * emit a non-zero, positive value for scale. If it is not sent, - * the client should assume a scale of 1. - * - * A scale larger than 1 means that the compositor will - * automatically scale surface buffers by this amount when - * rendering. This is used for very high resolution displays where - * applications rendering at the native resolution would be too - * small to be legible. - * - * Clients should use wl_surface.preferred_buffer_scale instead of - * this event to find the preferred buffer scale to use for a - * surface. - * - * The scale event will be followed by a done event. - * @param factor scaling factor of output - * @since 2 - */ void (*scale)(void *data, struct wl_output *wl_output, int32_t factor); - /** - * name of this output - * - * Many compositors will assign user-friendly names to their - * outputs, show them to the user, allow the user to refer to an - * output, etc. The client may wish to know this name as well to - * offer the user similar behaviors. - * - * The name is a UTF-8 string with no convention defined for its - * contents. Each name is unique among all wl_output globals. The - * name is only guaranteed to be unique for the compositor - * instance. - * - * The same output name is used for all clients for a given - * wl_output global. Thus, the name can be shared across processes - * to refer to a specific wl_output global. - * - * The name is not guaranteed to be persistent across sessions, - * thus cannot be used to reliably identify an output in e.g. - * configuration files. - * - * Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. - * However, do not assume that the name is a reflection of an - * underlying DRM connector, X11 connection, etc. - * - * The name event is sent after binding the output object. This - * event is only sent once per output object, and the name does not - * change over the lifetime of the wl_output global. - * - * Compositors may re-use the same output name if the wl_output - * global is destroyed and re-created later. Compositors should - * avoid re-using the same name if possible. - * - * The name event will be followed by a done event. - * @param name output name - * @since 4 - */ void (*name)(void *data, struct wl_output *wl_output, const char *name); - /** - * human-readable description of this output - * - * Many compositors can produce human-readable descriptions of - * their outputs. The client may wish to know this description as - * well, e.g. for output selection purposes. - * - * The description is a UTF-8 string with no convention defined for - * its contents. The description is not guaranteed to be unique - * among all wl_output globals. Examples might include 'Foocorp 11" - * Display' or 'Virtual X11 output via :1'. - * - * The description event is sent after binding the output object - * and whenever the description changes. The description is - * optional, and may not be sent at all. - * - * The description event will be followed by a done event. - * @param description output description - * @since 4 - */ void (*description)(void *data, struct wl_output *wl_output, const char *description); }; -/** - * @ingroup iface_wl_output - */ static inline int wl_output_add_listener(struct wl_output *wl_output, const struct wl_output_listener *listener, void *data) @@ -6277,44 +2045,21 @@ wl_output_add_listener(struct wl_output *wl_output, #define WL_OUTPUT_RELEASE 0 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_GEOMETRY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_MODE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_DONE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_SCALE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_NAME_SINCE_VERSION 4 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_DESCRIPTION_SINCE_VERSION 4 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_RELEASE_SINCE_VERSION 3 -/** @ingroup iface_wl_output */ static inline void wl_output_set_user_data(struct wl_output *wl_output, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_output, user_data); } -/** @ingroup iface_wl_output */ static inline void * wl_output_get_user_data(struct wl_output *wl_output) { @@ -6327,19 +2072,12 @@ wl_output_get_version(struct wl_output *wl_output) return wl_proxy_get_version((struct wl_proxy *) wl_output); } -/** @ingroup iface_wl_output */ static inline void wl_output_destroy(struct wl_output *wl_output) { wl_proxy_destroy((struct wl_proxy *) wl_output); } -/** - * @ingroup iface_wl_output - * - * Using this request a client can tell the server that it is not going to - * use the output object anymore. - */ static inline void wl_output_release(struct wl_output *wl_output) { @@ -6352,27 +2090,16 @@ wl_output_release(struct wl_output *wl_output) #define WL_REGION_SUBTRACT 2 -/** - * @ingroup iface_wl_region - */ #define WL_REGION_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_region - */ #define WL_REGION_ADD_SINCE_VERSION 1 -/** - * @ingroup iface_wl_region - */ #define WL_REGION_SUBTRACT_SINCE_VERSION 1 -/** @ingroup iface_wl_region */ static inline void wl_region_set_user_data(struct wl_region *wl_region, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_region, user_data); } -/** @ingroup iface_wl_region */ static inline void * wl_region_get_user_data(struct wl_region *wl_region) { @@ -6385,11 +2112,6 @@ wl_region_get_version(struct wl_region *wl_region) return wl_proxy_get_version((struct wl_proxy *) wl_region); } -/** - * @ingroup iface_wl_region - * - * Destroy the region. This will invalidate the object ID. - */ static inline void wl_region_destroy(struct wl_region *wl_region) { @@ -6397,11 +2119,6 @@ wl_region_destroy(struct wl_region *wl_region) WL_REGION_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_region), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_region - * - * Add the specified rectangle to the region. - */ static inline void wl_region_add(struct wl_region *wl_region, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -6409,11 +2126,6 @@ wl_region_add(struct wl_region *wl_region, int32_t x, int32_t y, int32_t width, WL_REGION_ADD, NULL, wl_proxy_get_version((struct wl_proxy *) wl_region), 0, x, y, width, height); } -/** - * @ingroup iface_wl_region - * - * Subtract the specified rectangle from the region. - */ static inline void wl_region_subtract(struct wl_region *wl_region, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -6424,38 +2136,24 @@ wl_region_subtract(struct wl_region *wl_region, int32_t x, int32_t y, int32_t wi #ifndef WL_SUBCOMPOSITOR_ERROR_ENUM #define WL_SUBCOMPOSITOR_ERROR_ENUM enum wl_subcompositor_error { - /** - * the to-be sub-surface is invalid - */ WL_SUBCOMPOSITOR_ERROR_BAD_SURFACE = 0, - /** - * the to-be sub-surface parent is invalid - */ WL_SUBCOMPOSITOR_ERROR_BAD_PARENT = 1, }; -#endif /* WL_SUBCOMPOSITOR_ERROR_ENUM */ +#endif #define WL_SUBCOMPOSITOR_DESTROY 0 #define WL_SUBCOMPOSITOR_GET_SUBSURFACE 1 -/** - * @ingroup iface_wl_subcompositor - */ #define WL_SUBCOMPOSITOR_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subcompositor - */ #define WL_SUBCOMPOSITOR_GET_SUBSURFACE_SINCE_VERSION 1 -/** @ingroup iface_wl_subcompositor */ static inline void wl_subcompositor_set_user_data(struct wl_subcompositor *wl_subcompositor, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_subcompositor, user_data); } -/** @ingroup iface_wl_subcompositor */ static inline void * wl_subcompositor_get_user_data(struct wl_subcompositor *wl_subcompositor) { @@ -6468,13 +2166,6 @@ wl_subcompositor_get_version(struct wl_subcompositor *wl_subcompositor) return wl_proxy_get_version((struct wl_proxy *) wl_subcompositor); } -/** - * @ingroup iface_wl_subcompositor - * - * Informs the server that the client will not be using this - * protocol object anymore. This does not affect any other - * objects, wl_subsurface objects included. - */ static inline void wl_subcompositor_destroy(struct wl_subcompositor *wl_subcompositor) { @@ -6482,29 +2173,6 @@ wl_subcompositor_destroy(struct wl_subcompositor *wl_subcompositor) WL_SUBCOMPOSITOR_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subcompositor), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_subcompositor - * - * Create a sub-surface interface for the given surface, and - * associate it with the given parent surface. This turns a - * plain wl_surface into a sub-surface. - * - * The to-be sub-surface must not already have another role, and it - * must not have an existing wl_subsurface object. Otherwise the - * bad_surface protocol error is raised. - * - * Adding sub-surfaces to a parent is a double-buffered operation on the - * parent (see wl_surface.commit). The effect of adding a sub-surface - * becomes visible on the next time the state of the parent surface is - * applied. - * - * The parent surface must not be one of the child surface's descendants, - * and the parent must be different from the child surface, otherwise the - * bad_parent protocol error is raised. - * - * This request modifies the behaviour of wl_surface.commit request on - * the sub-surface, see the documentation on wl_subsurface interface. - */ static inline struct wl_subsurface * wl_subcompositor_get_subsurface(struct wl_subcompositor *wl_subcompositor, struct wl_surface *surface, struct wl_surface *parent) { @@ -6519,12 +2187,9 @@ wl_subcompositor_get_subsurface(struct wl_subcompositor *wl_subcompositor, struc #ifndef WL_SUBSURFACE_ERROR_ENUM #define WL_SUBSURFACE_ERROR_ENUM enum wl_subsurface_error { - /** - * wl_surface is not a sibling or the parent - */ WL_SUBSURFACE_ERROR_BAD_SURFACE = 0, }; -#endif /* WL_SUBSURFACE_ERROR_ENUM */ +#endif #define WL_SUBSURFACE_DESTROY 0 #define WL_SUBSURFACE_SET_POSITION 1 @@ -6534,39 +2199,19 @@ enum wl_subsurface_error { #define WL_SUBSURFACE_SET_DESYNC 5 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_POSITION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_PLACE_ABOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_PLACE_BELOW_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_SYNC_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_DESYNC_SINCE_VERSION 1 -/** @ingroup iface_wl_subsurface */ static inline void wl_subsurface_set_user_data(struct wl_subsurface *wl_subsurface, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_subsurface, user_data); } -/** @ingroup iface_wl_subsurface */ static inline void * wl_subsurface_get_user_data(struct wl_subsurface *wl_subsurface) { @@ -6579,14 +2224,6 @@ wl_subsurface_get_version(struct wl_subsurface *wl_subsurface) return wl_proxy_get_version((struct wl_proxy *) wl_subsurface); } -/** - * @ingroup iface_wl_subsurface - * - * The sub-surface interface is removed from the wl_surface object - * that was turned into a sub-surface with a - * wl_subcompositor.get_subsurface request. The wl_surface's association - * to the parent is deleted. The wl_surface is unmapped immediately. - */ static inline void wl_subsurface_destroy(struct wl_subsurface *wl_subsurface) { @@ -6594,22 +2231,6 @@ wl_subsurface_destroy(struct wl_subsurface *wl_subsurface) WL_SUBSURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subsurface), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_subsurface - * - * This sets the position of the sub-surface, relative to the parent - * surface. - * - * The sub-surface will be moved so that its origin (top left - * corner pixel) will be at the location x, y of the parent surface - * coordinate system. The coordinates are not restricted to the parent - * surface area. Negative values are allowed. - * - * The initial position is 0, 0. - * - * Position is double-buffered state on the parent surface, see - * wl_subsurface and wl_surface.commit for more information. - */ static inline void wl_subsurface_set_position(struct wl_subsurface *wl_subsurface, int32_t x, int32_t y) { @@ -6617,21 +2238,6 @@ wl_subsurface_set_position(struct wl_subsurface *wl_subsurface, int32_t x, int32 WL_SUBSURFACE_SET_POSITION, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subsurface), 0, x, y); } -/** - * @ingroup iface_wl_subsurface - * - * This sub-surface is taken from the stack, and put back just - * above the reference surface, changing the z-order of the sub-surfaces. - * The reference surface must be one of the sibling surfaces, or the - * parent surface. Using any other surface, including this sub-surface, - * will cause a protocol error. - * - * A new sub-surface is initially added as the top-most in the stack - * of its siblings and parent. - * - * Z-order is double-buffered state on the parent surface, see - * wl_subsurface and wl_surface.commit for more information. - */ static inline void wl_subsurface_place_above(struct wl_subsurface *wl_subsurface, struct wl_surface *sibling) { @@ -6639,13 +2245,6 @@ wl_subsurface_place_above(struct wl_subsurface *wl_subsurface, struct wl_surface WL_SUBSURFACE_PLACE_ABOVE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subsurface), 0, sibling); } -/** - * @ingroup iface_wl_subsurface - * - * The sub-surface is placed just below the reference surface. - * - * See wl_subsurface.place_above. - */ static inline void wl_subsurface_place_below(struct wl_subsurface *wl_subsurface, struct wl_surface *sibling) { @@ -6653,14 +2252,6 @@ wl_subsurface_place_below(struct wl_subsurface *wl_subsurface, struct wl_surface WL_SUBSURFACE_PLACE_BELOW, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subsurface), 0, sibling); } -/** - * @ingroup iface_wl_subsurface - * - * Change the commit behaviour of the sub-surface to synchronized - * mode. - * - * See wl_subsurface and wl_surface.commit for more information. - */ static inline void wl_subsurface_set_sync(struct wl_subsurface *wl_subsurface) { @@ -6668,14 +2259,6 @@ wl_subsurface_set_sync(struct wl_subsurface *wl_subsurface) WL_SUBSURFACE_SET_SYNC, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subsurface), 0); } -/** - * @ingroup iface_wl_subsurface - * - * Change the commit behaviour of the sub-surface to desynchronized - * mode. - * - * See wl_subsurface and wl_surface.commit for more information. - */ static inline void wl_subsurface_set_desync(struct wl_subsurface *wl_subsurface) { @@ -6685,46 +2268,26 @@ wl_subsurface_set_desync(struct wl_subsurface *wl_subsurface) #ifndef WL_FIXES_ERROR_ENUM #define WL_FIXES_ERROR_ENUM -/** - * @ingroup iface_wl_fixes - * wl_fixes error values - * - * These errors can be emitted in response to wl_fixes requests. - */ enum wl_fixes_error { - /** - * unknown global or the global is not removed - */ WL_FIXES_ERROR_INVALID_ACK_REMOVE = 0, }; -#endif /* WL_FIXES_ERROR_ENUM */ +#endif #define WL_FIXES_DESTROY 0 #define WL_FIXES_DESTROY_REGISTRY 1 #define WL_FIXES_ACK_GLOBAL_REMOVE 2 -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_DESTROY_REGISTRY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_ACK_GLOBAL_REMOVE_SINCE_VERSION 2 -/** @ingroup iface_wl_fixes */ static inline void wl_fixes_set_user_data(struct wl_fixes *wl_fixes, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_fixes, user_data); } -/** @ingroup iface_wl_fixes */ static inline void * wl_fixes_get_user_data(struct wl_fixes *wl_fixes) { @@ -6737,9 +2300,6 @@ wl_fixes_get_version(struct wl_fixes *wl_fixes) return wl_proxy_get_version((struct wl_proxy *) wl_fixes); } -/** - * @ingroup iface_wl_fixes - */ static inline void wl_fixes_destroy(struct wl_fixes *wl_fixes) { @@ -6747,19 +2307,6 @@ wl_fixes_destroy(struct wl_fixes *wl_fixes) WL_FIXES_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_fixes), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_fixes - * - * This request destroys a wl_registry object. - * - * The client should no longer use the wl_registry after making this - * request. - * - * The compositor will emit a wl_display.delete_id event with the object ID - * of the registry and will no longer emit any events on the registry. The - * client should re-use the object ID once it receives the - * wl_display.delete_id event. - */ static inline void wl_fixes_destroy_registry(struct wl_fixes *wl_fixes, struct wl_registry *registry) { @@ -6767,32 +2314,6 @@ wl_fixes_destroy_registry(struct wl_fixes *wl_fixes, struct wl_registry *registr WL_FIXES_DESTROY_REGISTRY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_fixes), 0, registry); } -/** - * @ingroup iface_wl_fixes - * - * Acknowledge the removal of the specified global. - * - * If no global with the specified name exists or the global is not removed, - * the wl_fixes.invalid_ack_remove protocol error will be posted. - * - * Due to the Wayland protocol being asynchronous, the wl_global objects - * cannot be destroyed immediately. For example, if a wl_global is removed - * and a client attempts to bind that global around same time, it can - * result in a protocol error due to an unknown global name in the bind - * request. - * - * In order to avoid crashing clients, the compositor should remove the - * wl_global once it is guaranteed that no more bind requests will come. - * - * The wl_fixes.ack_global_remove() request is used to signal to the - * compositor that the client will not bind the given global anymore. After - * all clients acknowledge the removal of the global, the compositor can - * safely destroy it. - * - * The client must call the wl_fixes.ack_global_remove() request in - * response to a wl_registry.global_remove() event even if it did not bind - * the corresponding global. - */ static inline void wl_fixes_ack_global_remove(struct wl_fixes *wl_fixes, struct wl_registry *registry, uint32_t name) { diff --git a/tools/vr/third_party/wayland/wayland-client.h b/tools/vr/third_party/wayland/wayland-client.h index 9f70fa3fb..dd6676f6a 100644 --- a/tools/vr/third_party/wayland/wayland-client.h +++ b/tools/vr/third_party/wayland/wayland-client.h @@ -23,15 +23,6 @@ * SOFTWARE. */ -/** \file - * - * \brief Include the client API and protocol C API. - * - * \warning Use of this header file is discouraged. Prefer including - * wayland-client-core.h instead, which does not include the - * client protocol header and as such only defines the library - * API. - */ #ifndef WAYLAND_CLIENT_H #define WAYLAND_CLIENT_H diff --git a/tools/vr/third_party/wayland/wayland-util.h b/tools/vr/third_party/wayland/wayland-util.h index 98c72fded..d411ddd4f 100644 --- a/tools/vr/third_party/wayland/wayland-util.h +++ b/tools/vr/third_party/wayland/wayland-util.h @@ -23,10 +23,6 @@ * SOFTWARE. */ -/** \file wayland-util.h - * - * \brief Utility classes, functions, and macros. - */ #ifndef WAYLAND_UTIL_H #define WAYLAND_UTIL_H @@ -40,14 +36,12 @@ extern "C" { #endif -/** Visibility attribute */ #if defined(__GNUC__) && __GNUC__ >= 4 #define WL_EXPORT __attribute__ ((visibility("default"))) #else #define WL_EXPORT #endif -/** Deprecated attribute */ #if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || (defined(__cplusplus) && __cplusplus >= 201402L) #define WL_DEPRECATED [[deprecated]] #elif defined(__GNUC__) && __GNUC__ >= 4 @@ -56,14 +50,6 @@ extern "C" { #define WL_DEPRECATED #endif -/** - * Printf-style argument attribute - * - * \param x Ordinality of the format string argument - * \param y Ordinality of the argument to check against the format string - * - * \sa https://gcc.gnu.org/onlinedocs/gcc-3.2.1/gcc/Function-Attributes.html - */ #if defined(__GNUC__) && __GNUC__ >= 4 #define WL_PRINTF(x, y) __attribute__((__format__(__printf__, x, y))) #else @@ -76,405 +62,57 @@ extern "C" { #define WL_TYPEOF(expr) __typeof__(expr) #endif -/** \class wl_object - * - * \brief A protocol object. - * - * A `wl_object` is an opaque struct identifying the protocol object - * underlying a `wl_proxy` or `wl_resource`. - * - * \note Functions accessing a `wl_object` are not normally used by client code. - * Clients should normally use the higher level interface generated by the - * scanner to interact with compositor objects. - * - */ struct wl_object; -/** - * The maximum size of a protocol message. - * - * If a message size exceeds this value, the connection will be dropped. - * Servers will send an invalid_method error before disconnecting. - */ #define WL_MAX_MESSAGE_SIZE 4096 -/** - * Protocol message signature - * - * A wl_message describes the signature of an actual protocol message, such as a - * request or event, that adheres to the Wayland protocol wire format. The - * protocol implementation uses a wl_message within its demarshal machinery for - * decoding messages between a compositor and its clients. In a sense, a - * wl_message is to a protocol message like a class is to an object. - * - * The `name` of a wl_message is the name of the corresponding protocol message. - * - * The `signature` is an ordered list of symbols representing the data types - * of message arguments and, optionally, a protocol version and indicators for - * nullability. A leading integer in the `signature` indicates the _since_ - * version of the protocol message. A `?` preceding a data type symbol indicates - * that the following argument type is nullable. While it is a protocol violation - * to send messages with non-nullable arguments set to `NULL`, event handlers in - * clients might still get called with non-nullable object arguments set to - * `NULL`. This can happen when the client destroyed the object being used as - * argument on its side and an event referencing that object was sent before the - * server knew about its destruction. As this race cannot be prevented, clients - * should - as a general rule - program their event handlers such that they can - * handle object arguments declared non-nullable being `NULL` gracefully. - * - * When no arguments accompany a message, `signature` is an empty string. - * - * Symbols: - * - * * `i`: int - * * `u`: uint - * * `f`: fixed - * * `s`: string - * * `o`: object - * * `n`: new_id - * * `a`: array - * * `h`: fd - * * `?`: following argument (`o` or `s`) is nullable - * - * While demarshaling primitive arguments is straightforward, when demarshaling - * messages containing `object` or `new_id` arguments, the protocol - * implementation often must determine the type of the object. The `types` of a - * wl_message is an array of wl_interface references that correspond to `o` and - * `n` arguments in `signature`, with `NULL` placeholders for arguments with - * non-object types. - * - * Consider the protocol event wl_display `delete_id` that has a single `uint` - * argument. The wl_message is: - * - * \code - * { "delete_id", "u", [NULL] } - * \endcode - * - * Here, the message `name` is `"delete_id"`, the `signature` is `"u"`, and the - * argument `types` is `[NULL]`, indicating that the `uint` argument has no - * corresponding wl_interface since it is a primitive argument. - * - * In contrast, consider a `wl_foo` interface supporting protocol request `bar` - * that has existed since version 2, and has two arguments: a `uint` and an - * object of type `wl_baz_interface` that may be `NULL`. Such a `wl_message` - * might be: - * - * \code - * { "bar", "2u?o", [NULL, &wl_baz_interface] } - * \endcode - * - * Here, the message `name` is `"bar"`, and the `signature` is `"2u?o"`. Notice - * how the `2` indicates the protocol version, the `u` indicates the first - * argument type is `uint`, and the `?o` indicates that the second argument - * is an object that may be `NULL`. Lastly, the argument `types` array indicates - * that no wl_interface corresponds to the first argument, while the type - * `wl_baz_interface` corresponds to the second argument. - * - * \sa wl_argument - * \sa wl_interface - * \sa <a href="https://wayland.freedesktop.org/docs/html/ch04.html#sect-Protocol-Wire-Format">Wire Format</a> - */ struct wl_message { - /** Message name */ const char *name; - /** Message signature */ const char *signature; - /** Object argument interfaces */ const struct wl_interface **types; }; -/** - * Protocol object interface - * - * A wl_interface describes the API of a protocol object defined in the Wayland - * protocol specification. The protocol implementation uses a wl_interface - * within its marshalling machinery for encoding client requests. - * - * The `name` of a wl_interface is the name of the corresponding protocol - * interface, and `version` represents the version of the interface. The members - * `method_count` and `event_count` represent the number of `methods` (requests) - * and `events` in the respective wl_message members. - * - * For example, consider a protocol interface `foo`, marked as version `1`, with - * two requests and one event. - * - * \code{.xml} - * <interface name="foo" version="1"> - * <request name="a"></request> - * <request name="b"></request> - * <event name="c"></event> - * </interface> - * \endcode - * - * Given two wl_message arrays `foo_requests` and `foo_events`, a wl_interface - * for `foo` might be: - * - * \code - * struct wl_interface foo_interface = { - * "foo", 1, - * 2, foo_requests, - * 1, foo_events - * }; - * \endcode - * - * \note The server side of the protocol may define interface <em>implementation - * types</em> that incorporate the term `interface` in their name. Take - * care to not confuse these server-side `struct`s with a wl_interface - * variable whose name also ends in `interface`. For example, while the - * server may define a type `struct wl_foo_interface`, the client may - * define a `struct wl_interface wl_foo_interface`. - * - * \sa wl_message - * \sa wl_proxy - * \sa <a href="https://wayland.freedesktop.org/docs/html/ch04.html#sect-Protocol-Interfaces">Interfaces</a> - * \sa <a href="https://wayland.freedesktop.org/docs/html/ch04.html#sect-Protocol-Versioning">Versioning</a> - */ struct wl_interface { - /** Interface name */ const char *name; - /** Interface version */ int version; - /** Number of methods (requests) */ int method_count; - /** Method (request) signatures */ const struct wl_message *methods; - /** Number of events */ int event_count; - /** Event signatures */ const struct wl_message *events; }; -/** \class wl_list - * - * \brief Doubly-linked list - * - * On its own, an instance of `struct wl_list` represents the sentinel head of - * a doubly-linked list, and must be initialized using wl_list_init(). - * When empty, the list head's `next` and `prev` members point to the list head - * itself, otherwise `next` references the first element in the list, and `prev` - * refers to the last element in the list. - * - * Use the `struct wl_list` type to represent both the list head and the links - * between elements within the list. Use wl_list_empty() to determine if the - * list is empty in O(1). - * - * All elements in the list must be of the same type. The element type must have - * a `struct wl_list` member, often named `link` by convention. Prior to - * insertion, there is no need to initialize an element's `link` - invoking - * wl_list_init() on an individual list element's `struct wl_list` member is - * unnecessary if the very next operation is wl_list_insert(). However, a - * common idiom is to initialize an element's `link` prior to removal - ensure - * safety by invoking wl_list_init() before wl_list_remove(). - * - * Consider a list reference `struct wl_list foo_list`, an element type as - * `struct element`, and an element's link member as `struct wl_list link`. - * - * The following code initializes a list and adds three elements to it. - * - * \code - * struct wl_list foo_list; - * - * struct element { - * int foo; - * struct wl_list link; - * }; - * struct element e1, e2, e3; - * - * wl_list_init(&foo_list); - * wl_list_insert(&foo_list, &e1.link); // e1 is the first element - * wl_list_insert(&foo_list, &e2.link); // e2 is now the first element - * wl_list_insert(&e2.link, &e3.link); // insert e3 after e2 - * \endcode - * - * The list now looks like <em>[e2, e3, e1]</em>. - * - * The `wl_list` API provides some iterator macros. For example, to iterate - * a list in ascending order: - * - * \code - * struct element *e; - * wl_list_for_each(e, foo_list, link) { - * do_something_with_element(e); - * } - * \endcode - * - * See the documentation of each iterator for details. - * \sa http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/linux/list.h - */ struct wl_list { - /** Previous list element */ struct wl_list *prev; - /** Next list element */ struct wl_list *next; }; -/** - * Initializes the list. - * - * \param list List to initialize - * - * \memberof wl_list - */ void wl_list_init(struct wl_list *list); -/** - * Inserts an element into the list, after the element represented by \p list. - * When \p list is a reference to the list itself (the head), set the containing - * struct of \p elm as the first element in the list. - * - * \note If \p elm is already part of a list, inserting it again will lead to - * list corruption. - * - * \param list List element after which the new element is inserted - * \param elm Link of the containing struct to insert into the list - * - * \memberof wl_list - */ void wl_list_insert(struct wl_list *list, struct wl_list *elm); -/** - * Removes an element from the list. - * - * \note This operation leaves \p elm in an invalid state. - * - * \param elm Link of the containing struct to remove from the list - * - * \memberof wl_list - */ void wl_list_remove(struct wl_list *elm); -/** - * Determines the length of the list. - * - * \note This is an O(n) operation. - * - * \param list List whose length is to be determined - * - * \return Number of elements in the list - * - * \memberof wl_list - */ int wl_list_length(const struct wl_list *list); -/** - * Determines if the list is empty. - * - * \param list List whose emptiness is to be determined - * - * \return 1 if empty, or 0 if not empty - * - * \memberof wl_list - */ int wl_list_empty(const struct wl_list *list); -/** - * Inserts all of the elements of one list into another, after the element - * represented by \p list. - * - * \note This leaves \p other in an invalid state. - * - * \param list List element after which the other list elements will be inserted - * \param other List of elements to insert - * - * \memberof wl_list - */ void wl_list_insert_list(struct wl_list *list, struct wl_list *other); -/** - * Retrieves a pointer to a containing struct, given a member name. - * - * This macro allows "conversion" from a pointer to a member to its containing - * struct. This is useful if you have a contained item like a wl_list, - * wl_listener, or wl_signal, provided via a callback or other means, and would - * like to retrieve the struct that contains it. - * - * To demonstrate, the following example retrieves a pointer to - * `example_container` given only its `destroy_listener` member: - * - * \code - * struct example_container { - * struct wl_listener destroy_listener; - * // other members... - * }; - * - * void example_container_destroy(struct wl_listener *listener, void *data) - * { - * struct example_container *ctr; - * - * ctr = wl_container_of(listener, ctr, destroy_listener); - * // destroy ctr... - * } - * \endcode - * - * \note `sample` need not be a valid pointer. A null or uninitialised pointer - * is sufficient. - * - * \param ptr Valid pointer to the contained member - * \param sample Pointer to a struct whose type contains \p ptr - * \param member Named location of \p ptr within the \p sample type - * - * \return The container for the specified pointer - */ #define wl_container_of(ptr, sample, member) \ (WL_TYPEOF(sample))((char *)(ptr) - \ offsetof(WL_TYPEOF(*sample), member)) -/** - * Iterates over a list. - * - * This macro expresses a for-each iterator for wl_list. Given a list and - * wl_list link member name (often named `link` by convention), this macro - * assigns each element in the list to \p pos, which can then be referenced in - * a trailing code block. For example, given a wl_list of `struct message` - * elements: - * - * \code - * struct message { - * char *contents; - * wl_list link; - * }; - * - * struct wl_list *message_list; - * // Assume message_list now "contains" many messages - * - * struct message *m; - * wl_list_for_each(m, message_list, link) { - * do_something_with_message(m); - * } - * \endcode - * - * \param pos Cursor that each list element will be assigned to - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each(pos, head, member) \ for (pos = wl_container_of((head)->next, pos, member); \ &pos->member != (head); \ pos = wl_container_of(pos->member.next, pos, member)) -/** - * Iterates over a list, safe against removal of the list element. - * - * \note Only removal of the current element, \p pos, is safe. Removing - * any other element during traversal may lead to a loop malfunction. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param tmp Temporary pointer of the same type as \p pos - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_safe(pos, tmp, head, member) \ for (pos = wl_container_of((head)->next, pos, member), \ tmp = wl_container_of((pos)->member.next, tmp, member); \ @@ -482,37 +120,11 @@ wl_list_insert_list(struct wl_list *list, struct wl_list *other); pos = tmp, \ tmp = wl_container_of(pos->member.next, tmp, member)) -/** - * Iterates backwards over a list. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_reverse(pos, head, member) \ for (pos = wl_container_of((head)->prev, pos, member); \ &pos->member != (head); \ pos = wl_container_of(pos->member.prev, pos, member)) -/** - * Iterates backwards over a list, safe against removal of the list element. - * - * \note Only removal of the current element, \p pos, is safe. Removing - * any other element during traversal may lead to a loop malfunction. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param tmp Temporary pointer of the same type as \p pos - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_reverse_safe(pos, tmp, head, member) \ for (pos = wl_container_of((head)->prev, pos, member), \ tmp = wl_container_of((pos)->member.prev, tmp, member); \ @@ -520,244 +132,75 @@ wl_list_insert_list(struct wl_list *list, struct wl_list *other); pos = tmp, \ tmp = wl_container_of(pos->member.prev, tmp, member)) -/** - * \class wl_array - * - * Dynamic array - * - * A wl_array is a dynamic array that can only grow until released. It is - * intended for relatively small allocations whose size is variable or not known - * in advance. While construction of a wl_array does not require all elements to - * be of the same size, wl_array_for_each() does require all elements to have - * the same type and size. - * - */ struct wl_array { - /** Array size */ size_t size; - /** Allocated space */ size_t alloc; - /** Array data */ void *data; }; -/** - * Initializes the array. - * - * \param array Array to initialize - * - * \memberof wl_array - */ void wl_array_init(struct wl_array *array); -/** - * Releases the array data. - * - * \note Leaves the array in an invalid state. - * - * \param array Array whose data is to be released - * - * \memberof wl_array - */ void wl_array_release(struct wl_array *array); -/** - * Increases the size of the array by \p size bytes. - * - * \param array Array whose size is to be increased - * \param size Number of bytes to increase the size of the array by - * - * \return A pointer to the beginning of the newly appended space, or NULL when - * resizing fails. - * - * \memberof wl_array - */ void * wl_array_add(struct wl_array *array, size_t size); -/** - * Copies the contents of \p source to \p array. - * - * \param array Destination array to copy to - * \param source Source array to copy from - * - * \return 0 on success, or -1 on failure - * - * \memberof wl_array - */ int wl_array_copy(struct wl_array *array, struct wl_array *source); -/** - * Iterates over an array. - * - * This macro expresses a for-each iterator for wl_array. It assigns each - * element in the array to \p pos, which can then be referenced in a trailing - * code block. \p pos must be a pointer to the array element type, and all - * array elements must be of the same type and size. - * - * \param pos Cursor that each array element will be assigned to - * \param array Array to iterate over - * - * \relates wl_array - * \sa wl_list_for_each() - */ #define wl_array_for_each(pos, array) \ for (pos = (array)->data; \ (array)->size != 0 && \ (const char *) pos < ((const char *) (array)->data + (array)->size); \ (pos)++) -/** - * Fixed-point number - * - * A `wl_fixed_t` is a 24.8 signed fixed-point number with a sign bit, 23 bits - * of integer precision and 8 bits of decimal precision. Consider `wl_fixed_t` - * as an opaque struct with methods that facilitate conversion to and from - * `double` and `int` types. - */ typedef int32_t wl_fixed_t; -/** - * Converts a fixed-point number to a floating-point number. - * - * \param f Fixed-point number to convert - * - * \return Floating-point representation of the fixed-point argument - */ static inline double wl_fixed_to_double(wl_fixed_t f) { return f / 256.0; } -/** - * Converts a floating-point number to a fixed-point number. - * - * \param d Floating-point number to convert - * - * \return Fixed-point representation of the floating-point argument - */ static inline wl_fixed_t wl_fixed_from_double(double d) { return (wl_fixed_t) (round(d * 256.0)); } -/** - * Converts a fixed-point number to an integer. - * - * \param f Fixed-point number to convert - * - * \return Integer component of the fixed-point argument - */ static inline int wl_fixed_to_int(wl_fixed_t f) { return f / 256; } -/** - * Converts an integer to a fixed-point number. - * - * \param i Integer to convert - * - * \return Fixed-point representation of the integer argument - */ static inline wl_fixed_t wl_fixed_from_int(int i) { return i * 256; } -/** - * Protocol message argument data types - * - * This union represents all of the argument types in the Wayland protocol wire - * format. The protocol implementation uses wl_argument within its marshalling - * machinery for dispatching messages between a client and a compositor. - * - * \sa wl_message - * \sa wl_interface - * \sa <a href="https://wayland.freedesktop.org/docs/html/ch04.html#sect-Protocol-wire-Format">Wire Format</a> - */ union wl_argument { - int32_t i; /**< `int` */ - uint32_t u; /**< `uint` */ - wl_fixed_t f; /**< `fixed` */ - const char *s; /**< `string` */ - struct wl_object *o; /**< `object` */ - uint32_t n; /**< `new_id` */ - struct wl_array *a; /**< `array` */ - int32_t h; /**< `fd` */ + int32_t i; + uint32_t u; + wl_fixed_t f; + const char *s; + struct wl_object *o; + uint32_t n; + struct wl_array *a; + int32_t h; }; -/** - * Dispatcher function type alias - * - * A dispatcher is a function that handles the emitting of callbacks in client - * code. For programs directly using the C library, this is done by using - * libffi to call function pointers. When binding to languages other than C, - * dispatchers provide a way to abstract the function calling process to be - * friendlier to other function calling systems. - * - * A dispatcher takes five arguments: The first is the dispatcher-specific - * implementation associated with the target object. The second is the object - * upon which the callback is being invoked (either wl_proxy or wl_resource). - * The third and fourth arguments are the opcode and the wl_message - * corresponding to the callback. The final argument is an array of arguments - * received from the other process via the wire protocol. - * - * \param user_data Dispatcher-specific implementation data - * \param target Callback invocation target (wl_proxy or `wl_resource`) - * \param opcode Callback opcode - * \param msg Callback message signature - * \param args Array of received arguments - * - * \return 0 on success, or -1 on failure - */ typedef int (*wl_dispatcher_func_t)(const void *user_data, void *target, uint32_t opcode, const struct wl_message *msg, union wl_argument *args); -/** - * Log function type alias - * - * The C implementation of the Wayland protocol abstracts the details of - * logging. Users may customize the logging behavior, with a function conforming - * to the `wl_log_func_t` type, via `wl_log_set_handler_client` and - * `wl_log_set_handler_server`. - * - * A `wl_log_func_t` must conform to the expectations of `vprintf`, and - * expects two arguments: a string to write and a corresponding variable - * argument list. While the string to write may contain format specifiers and - * use values in the variable argument list, the behavior of any `wl_log_func_t` - * depends on the implementation. - * - * \note Take care to not confuse this with `wl_protocol_logger_func_t`, which - * is a specific server-side logger for requests and events. - * - * \param fmt String to write to the log, containing optional format - * specifiers - * \param args Variable argument list - * - * \sa wl_log_set_handler_client - * \sa wl_log_set_handler_server - */ typedef void (*wl_log_func_t)(const char *fmt, va_list args) WL_PRINTF(1, 0); -/** - * Return value of an iterator function - * - * \sa wl_client_for_each_resource_iterator_func_t - * \sa wl_client_for_each_resource - */ enum wl_iterator_result { - /** Stop the iteration */ WL_ITERATOR_STOP, - /** Continue the iteration */ WL_ITERATOR_CONTINUE }; From 0d0465d19eed6f160ca37fa4d674d87af3d4ba58 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:31:26 -0400 Subject: [PATCH 11/14] Fix Flatpak installs on older Android kernels (#752) --- tools/proot/PATCHES.md | 10 +- tools/proot/build.sh | 1 + .../patches/0015-statx-mnt-id-fdinfo.patch | 44 ++++++ tools/proot/statx_mnt_id.h | 95 +++++++++++++ tools/tests/test_proot_statx_mnt_id.py | 133 ++++++++++++++++++ 5 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 tools/proot/patches/0015-statx-mnt-id-fdinfo.patch create mode 100644 tools/proot/statx_mnt_id.h create mode 100644 tools/tests/test_proot_statx_mnt_id.py diff --git a/tools/proot/PATCHES.md b/tools/proot/PATCHES.md index 342bf79e6..5e501eab9 100644 --- a/tools/proot/PATCHES.md +++ b/tools/proot/PATCHES.md @@ -78,8 +78,16 @@ Added by DroidDeck: exit.c act on (`TCSETSF`, the four termios2 requests, `FICLONE`) and allows the rest at once, and the ioctl block is emitted first (after WinNative 53836ca9, which traces only the termios2 ones). The `faccessat2` exit stop (termux 5ba8b95, for glibc's ENOSYS fallback on kernels before 5.8) and - the `statx` one (emulation on kernels before 4.11) are dropped when the running kernel is newer. + the `statx` one (emulation on kernels before 4.11) are dropped on kernels 5.8 and newer; patch + `0015` keeps it on 4.11–5.7 to return the mount ID added in 5.8. Host build: `ioctl` 31.6 -> 0.5 us; `stat`/`statx`/`faccessat2` about 44 -> 28 us (one stop). +- `0015-statx-mnt-id-fdinfo.patch` - kernels before 5.8 can complete `statx` without the mount ID + that libglnx requires while chasing an open descriptor. PRoot keeps the exit stop on those + kernels and fills only a missing ordinary `STATX_MNT_ID` for `AT_EMPTY_PATH` after pinning the + tracee descriptor, matching its device and inode to the successful statx result, and reading + `mnt_id` from the pin's fdinfo. A later close/reuse of the tracee fd cannot redirect that read; + a concurrent reuse for the same inode through a different bind mount is indistinguishable at + the exit stop. It does not synthesize `STATX_MNT_ID_UNIQUE`. Prototype (inert unless `PROOT_FASTPATH` is set; see `docs/development/proot-performance.md`): diff --git a/tools/proot/build.sh b/tools/proot/build.sh index a899370fd..718190552 100755 --- a/tools/proot/build.sh +++ b/tools/proot/build.sh @@ -30,6 +30,7 @@ fetch() { } fetch proot.tar.gz "https://github.com/termux/proot/archive/${PROOT_COMMIT}.tar.gz" "$PROOT_SHA256" fetch talloc.tar.gz "https://www.samba.org/ftp/talloc/talloc-${TALLOC_VERSION}.tar.gz" "$TALLOC_SHA256" +cp "$HERE/statx_mnt_id.h" "$WORK/proot/src/tracee/statx_mnt_id.h" for patch in "$HERE"/patches/*.patch; do patch -d "$WORK/proot" -p1 --forward --quiet < "$patch" done diff --git a/tools/proot/patches/0015-statx-mnt-id-fdinfo.patch b/tools/proot/patches/0015-statx-mnt-id-fdinfo.patch new file mode 100644 index 000000000..aa5b564f8 --- /dev/null +++ b/tools/proot/patches/0015-statx-mnt-id-fdinfo.patch @@ -0,0 +1,44 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: DroidDeck <droiddeck@users.noreply.github.com> +Date: Tue, 13 Oct 2026 00:00:00 +0000 +Subject: [PATCH] statx: supply mount IDs on kernels before 5.8 + +On kernels before 5.8, statx can succeed without STATX_MNT_ID. Recover it +from the already-open descriptor after verifying its identity. Keep the +compatibility exit stop only on kernels that need the missing field. +--- +diff --git a/src/syscall/seccomp.c b/src/syscall/seccomp.c +index ee7b8acd68660f38fd52f56e951ffb94e9061d68..ff3a79b60e9092a296bb444c919bb3861338727f 100644 +--- a/src/syscall/seccomp.c ++++ b/src/syscall/seccomp.c +@@ -738,4 +738,4 @@ int enable_syscall_filtering(const Tracee *tracee) +- * the statx one emulates the call when it fails, which only a kernel +- * before 4.11 does for want of it - a statx Android's policy traps is +- * emulated from SIGSYS instead. Both stops re-read and re-translate +- * the path, and are paid on every call. ++ * the statx one emulates the syscall before 4.11 and supplies MNT_ID ++ * from fdinfo through 5.7. Android policy traps are still emulated from ++ * SIGSYS instead. Both stops re-read and re-translate the path, and are ++ * paid on every call. +@@ -764 +764,4 @@ static void drop_unneeded_exit_stops(void) +- if (major > 4 || (major == 4 && minor >= 11)) ++ /* Linux 4.11 introduced statx; Linux 5.8 added MNT_ID. ++ * Keep its exit stop through 5.7 so the missing ID can be ++ * recovered from the descriptor's fdinfo. */ ++ if (major > 5 || (major == 5 && minor >= 8)) +diff --git a/src/tracee/statx.c b/src/tracee/statx.c +index ca972f892e84461cb694dd13a37bfbc67527b320..824771ccf5934e1f870fc36a29fb22fdcad0a856 100644 +--- a/src/tracee/statx.c ++++ b/src/tracee/statx.c +@@ -4,0 +5 @@ ++#include "tracee/statx_mnt_id.h" +@@ -125,0 +127,9 @@ int handle_statx_syscall(Tracee *tracee, bool from_sigsys) { ++ /* libglnx chases an already-open fd with AT_EMPTY_PATH and requires a ++ * mount ID. Kernels before 5.8 can succeed without returning that field. ++ * The tracee is stopped at this syscall exit; accept only the ordinary ++ * mount ID and verify the fdinfo inode against the statx result. */ ++ if (droiddeck_proot_statx_fill_mnt_id(tracee->pid, (int) dirfd, ++ (int) flags, guest_path, (unsigned int) mask, ++ &state.statx_buf)) ++ state.updated_stats = true; ++ diff --git a/tools/proot/statx_mnt_id.h b/tools/proot/statx_mnt_id.h new file mode 100644 index 000000000..deb1789b8 --- /dev/null +++ b/tools/proot/statx_mnt_id.h @@ -0,0 +1,95 @@ +#ifndef DROIDDECK_PROOT_STATX_MNT_ID_H +#define DROIDDECK_PROOT_STATX_MNT_ID_H + +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/sysmacros.h> +#include <sys/types.h> +#include <unistd.h> + +static inline bool droiddeck_proot_read_fdinfo(int fd, uint64_t *mnt_id, uint64_t *ino) { + if (fd < 0 || !mnt_id || !ino) return false; + + char fdinfo_path[64]; + int n = snprintf(fdinfo_path, sizeof fdinfo_path, "/proc/self/fdinfo/%d", fd); + if (n < 0 || (size_t)n >= sizeof fdinfo_path) return false; + + FILE *fdinfo = fopen(fdinfo_path, "r"); + if (!fdinfo) return false; + + bool have_mnt_id = false, have_ino = false; + uint64_t parsed_mnt_id = 0, parsed_ino = 0; + char line[256]; + while (fgets(line, sizeof line, fdinfo)) { + const char *value = NULL; + uint64_t *field = NULL; + if (strncmp(line, "mnt_id:", 7) == 0) { + value = line + 7; + field = &parsed_mnt_id; + have_mnt_id = true; + } else if (strncmp(line, "ino:", 4) == 0) { + value = line + 4; + field = &parsed_ino; + have_ino = true; + } + if (!value) continue; + + errno = 0; + char *end = NULL; + unsigned long long parsed = strtoull(value, &end, 10); + if (errno || end == value || (*end && *end != '\n' && *end != '\r')) { + if (field == &parsed_mnt_id) have_mnt_id = false; + else have_ino = false; + continue; + } + *field = (uint64_t)parsed; + } + bool read_error = ferror(fdinfo); + fclose(fdinfo); + + if (read_error || !have_mnt_id || !have_ino || parsed_mnt_id == 0) return false; + *mnt_id = parsed_mnt_id; + *ino = parsed_ino; + return true; +} + +static inline bool droiddeck_proot_statx_fill_mnt_id(pid_t pid, int fd, + int flags, const char *path, + unsigned int mask, + struct statx *buf) { + if (fd < 0 || !(flags & AT_EMPTY_PATH) || !path || path[0] != '\0' + || !(mask & STATX_MNT_ID) || (buf->stx_mask & STATX_MNT_ID) + || !(buf->stx_mask & STATX_INO)) + return false; + + char tracee_fd_path[64]; + int n = snprintf(tracee_fd_path, sizeof tracee_fd_path, "/proc/%ld/fd/%d", (long)pid, fd); + if (n < 0 || (size_t)n >= sizeof tracee_fd_path) return false; + + /* Another thread sharing the tracee's fd table can close/reuse fd after statx returns. Pin the + * current referent, then require both device and inode to match statx before trusting its mount. + * Read fdinfo from our pin so a later tracee fd reuse cannot change which mount ID is observed. + * A same-inode bind-mount alias has the same device/inode pair; at this exit stop we cannot + * distinguish that rare concurrent replacement from the mount used by the completed syscall. */ + int pinned = open(tracee_fd_path, O_PATH | O_CLOEXEC); + if (pinned < 0) return false; + struct stat st; + bool same = fstat(pinned, &st) == 0 && st.st_ino == buf->stx_ino && + major(st.st_dev) == buf->stx_dev_major && minor(st.st_dev) == buf->stx_dev_minor; + uint64_t mnt_id = 0, ino = 0; + bool have_fdinfo = same && droiddeck_proot_read_fdinfo(pinned, &mnt_id, &ino); + close(pinned); + if (!have_fdinfo || ino != st.st_ino) return false; + + buf->stx_mnt_id = mnt_id; + buf->stx_mask |= STATX_MNT_ID; + return true; +} + +#endif diff --git a/tools/tests/test_proot_statx_mnt_id.py b/tools/tests/test_proot_statx_mnt_id.py new file mode 100644 index 000000000..e3dd59658 --- /dev/null +++ b/tools/tests/test_proot_statx_mnt_id.py @@ -0,0 +1,133 @@ +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +import unittest + +PROOT = Path(__file__).resolve().parents[1] / "proot" + +PROGRAM = r""" +#define _GNU_SOURCE +#include <fcntl.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> +#include "statx_mnt_id.h" + +static int fdinfo_mnt_id(int fd, uint64_t *out) { + char path[64], line[256]; + snprintf(path, sizeof path, "/proc/self/fdinfo/%d", fd); + FILE *f = fopen(path, "r"); + if (!f) return 0; + int found = 0; + while (fgets(line, sizeof line, f)) { + unsigned long long id; + if (sscanf(line, "mnt_id:%llu", &id) == 1) { + *out = (uint64_t)id; + found = 1; + break; + } + } + fclose(f); + return found; +} + +int main(void) { + char name[] = "/tmp/droiddeck-statx-mnt-id-XXXXXX"; + int fd = mkstemp(name); + if (fd < 0) return 1; + + struct stat st; + if (fstat(fd, &st) < 0) return 2; + uint64_t expected; + if (!fdinfo_mnt_id(fd, &expected) || expected == 0) return 3; + + /* Model a successful pre-5.8 statx: inode fields are present, MNT_ID is absent. */ + struct statx result = {0}; + result.stx_mask = STATX_TYPE | STATX_INO; + result.stx_mode = st.st_mode; + result.stx_ino = st.st_ino; + result.stx_dev_major = major(st.st_dev); + result.stx_dev_minor = minor(st.st_dev); + unsigned flags = AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW; + unsigned mask = STATX_TYPE | STATX_INO | STATX_MNT_ID; + if (!droiddeck_proot_statx_fill_mnt_id(getpid(), fd, flags, "", mask, &result)) return 4; + if (!(result.stx_mask & STATX_MNT_ID) || result.stx_mnt_id != expected) return 5; + + /* A reused fd can name the same inode number on another device. Inode alone is not identity. */ + result.stx_mask = STATX_TYPE | STATX_INO; + result.stx_dev_major = major(st.st_dev) ^ 1U; + result.stx_dev_minor = minor(st.st_dev); + if (droiddeck_proot_statx_fill_mnt_id(getpid(), fd, flags, "", mask, &result)) return 6; + if (result.stx_mask & STATX_MNT_ID) return 7; + + /* Only ordinary MNT_ID is reconstructed; never claim the newer UNIQUE bit. */ + result.stx_ino = st.st_ino; + result.stx_dev_major = major(st.st_dev); + result.stx_dev_minor = minor(st.st_dev); + if (droiddeck_proot_statx_fill_mnt_id(getpid(), fd, flags, "", 0x4000U, &result)) return 8; + if (result.stx_mask & STATX_MNT_ID) return 9; + + if (droiddeck_proot_statx_fill_mnt_id(getpid(), AT_FDCWD, flags, "", mask, &result)) return 10; + if (droiddeck_proot_statx_fill_mnt_id(getpid(), -1, flags, "", mask, &result)) return 11; + if (droiddeck_proot_statx_fill_mnt_id(getpid(), fd, AT_SYMLINK_NOFOLLOW, "", mask, &result)) return 12; + if (droiddeck_proot_statx_fill_mnt_id(getpid(), fd, flags, "file", mask, &result)) return 13; + int stale = dup(fd); + if (stale < 0 || close(stale) < 0) return 14; + if (droiddeck_proot_statx_fill_mnt_id(getpid(), stale, flags, "", mask, &result)) return 15; + + /* Pin the original referent, then reuse its tracee fd number. The pin's fdinfo stays stable, + * while recovery against the reused number must reject the different inode/device. */ + char replacement_name[] = "/tmp/droiddeck-statx-mnt-id-replacement-XXXXXX"; + int replacement = mkstemp(replacement_name); + if (replacement < 0) return 16; + int pinned; + char path[64]; + snprintf(path, sizeof path, "/proc/%ld/fd/%d", (long)getpid(), fd); + pinned = open(path, O_PATH | O_CLOEXEC); + if (pinned < 0) return 17; + uint64_t pinned_mnt_id, pinned_ino; + if (!droiddeck_proot_read_fdinfo(pinned, &pinned_mnt_id, &pinned_ino) || + pinned_mnt_id != expected || pinned_ino != st.st_ino) return 18; + + struct stat replacement_st; + if (fstat(replacement, &replacement_st) < 0) return 19; + if (replacement_st.st_dev == st.st_dev && replacement_st.st_ino == st.st_ino) return 20; + unlink(name); + if (close(fd) < 0 || dup2(replacement, fd) != fd) return 21; + close(replacement); + unlink(replacement_name); + + result.stx_mask = STATX_TYPE | STATX_INO; + result.stx_ino = st.st_ino; + result.stx_dev_major = major(st.st_dev); + result.stx_dev_minor = minor(st.st_dev); + if (droiddeck_proot_statx_fill_mnt_id(getpid(), fd, flags, "", mask, &result)) return 22; + if (result.stx_mask & STATX_MNT_ID) return 23; + if (!droiddeck_proot_read_fdinfo(pinned, &pinned_mnt_id, &pinned_ino) || + pinned_mnt_id != expected || pinned_ino != st.st_ino) return 24; + + close(pinned); + close(fd); + return 0; +} +""" + + +@unittest.skipUnless(shutil.which("cc") and sys.platform.startswith("linux"), "requires Linux and a C compiler") +class ProotStatxMountIdTest(unittest.TestCase): + def test_missing_mount_id_is_recovered_only_for_the_same_open_inode(self): + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "test.c" + program = Path(tmp) / "test" + source.write_text(PROGRAM) + subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-I", str(PROOT), "-o", str(program), str(source)], check=True) + subprocess.run([str(program)], check=True) + + +if __name__ == "__main__": + unittest.main() From d310e59fce3c5a962ebc050bf4247c7d1cd6c1af Mon Sep 17 00:00:00 2001 From: The412Banner <205237651+The412Banner@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:03:28 -0400 Subject: [PATCH 12/14] fix: desktop session: Shift no longer stays held after typing capitals on a physical keyboard (#766) * fix: desktop session: Shift no longer stays held after typing capitals on a physical keyboard Android repeats a held key as more key presses. Those were forwarded to the session as more wl_keyboard presses, and KWin's xkb counts every press of a modifier, so Shift held for a few capitals took several presses and one release and stayed held: digits typed !@#, letters stayed capital. The same applied to Ctrl and Alt. - the compositor no longer sends a press for a key that is already down; releases always go through (clients repeat keys themselves from the repeat_info it already sends) - the app stops forwarding Android's repeat events for keyboard keys Reported by whatsupbros with a Keychron Q7 over USB - thank you. * Remove stale comments from statx patch --------- Co-authored-by: The412Banner <the412banner@users.noreply.github.com> Co-authored-by: xXJsonDeruloXx <danielhimebauch@gmail.com> --- app/src/main/cpp/waylandcomp/src/compositor.c | 6 ++++++ .../com/droiddeck/launcher/SessionActivity.kt | 1 + .../patches/0015-statx-mnt-id-fdinfo.patch | 20 ++----------------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index 68911fc20..677937b52 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -397,6 +397,7 @@ static struct seat_keyboard g_kbs[MAX_PTRS]; static int g_nkbs; static uint32_t g_mods_depressed, g_mods_locked; static uint32_t g_mod_keys_held; +static uint8_t g_keys_down[0x300 / 8]; static struct seat_touch g_touches[MAX_PTRS]; static int g_ntouches; @@ -2845,6 +2846,11 @@ static int update_modifiers(uint32_t evdev, int pressed) { } static void key_event(uint32_t evdev, int pressed) { + if (evdev < 0x300) { + uint8_t bit = (uint8_t)(1u << (evdev & 7)); + if (pressed && (g_keys_down[evdev >> 3] & bit)) return; + if (pressed) g_keys_down[evdev >> 3] |= bit; else g_keys_down[evdev >> 3] &= (uint8_t)~bit; + } int mods_changed = update_modifiers(evdev, pressed); struct surface *target = NULL; struct wl_resource *ime = droiddeck_ime_target(); diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index b414cd672..1c6d32ddf 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -1577,6 +1577,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (event.keyCode != KeyEvent.KEYCODE_BACK && padBridge?.onKeyEvent(event) == true) return true if (CompositorHost.isStarted && event.keyCode != KeyEvent.KEYCODE_BACK && !fromController) { val down = event.action == KeyEvent.ACTION_DOWN + if (down && event.repeatCount > 0) return true if (down || event.action == KeyEvent.ACTION_UP) { var evdev = EvdevKeys.fromKeyCode(event.keyCode) val ch = event.unicodeChar diff --git a/tools/proot/patches/0015-statx-mnt-id-fdinfo.patch b/tools/proot/patches/0015-statx-mnt-id-fdinfo.patch index aa5b564f8..a0821997a 100644 --- a/tools/proot/patches/0015-statx-mnt-id-fdinfo.patch +++ b/tools/proot/patches/0015-statx-mnt-id-fdinfo.patch @@ -11,20 +11,8 @@ diff --git a/src/syscall/seccomp.c b/src/syscall/seccomp.c index ee7b8acd68660f38fd52f56e951ffb94e9061d68..ff3a79b60e9092a296bb444c919bb3861338727f 100644 --- a/src/syscall/seccomp.c +++ b/src/syscall/seccomp.c -@@ -738,4 +738,4 @@ int enable_syscall_filtering(const Tracee *tracee) -- * the statx one emulates the call when it fails, which only a kernel -- * before 4.11 does for want of it - a statx Android's policy traps is -- * emulated from SIGSYS instead. Both stops re-read and re-translate -- * the path, and are paid on every call. -+ * the statx one emulates the syscall before 4.11 and supplies MNT_ID -+ * from fdinfo through 5.7. Android policy traps are still emulated from -+ * SIGSYS instead. Both stops re-read and re-translate the path, and are -+ * paid on every call. -@@ -764 +764,4 @@ static void drop_unneeded_exit_stops(void) +@@ -764 +764 @@ static void drop_unneeded_exit_stops(void) - if (major > 4 || (major == 4 && minor >= 11)) -+ /* Linux 4.11 introduced statx; Linux 5.8 added MNT_ID. -+ * Keep its exit stop through 5.7 so the missing ID can be -+ * recovered from the descriptor's fdinfo. */ + if (major > 5 || (major == 5 && minor >= 8)) diff --git a/src/tracee/statx.c b/src/tracee/statx.c index ca972f892e84461cb694dd13a37bfbc67527b320..824771ccf5934e1f870fc36a29fb22fdcad0a856 100644 @@ -32,11 +20,7 @@ index ca972f892e84461cb694dd13a37bfbc67527b320..824771ccf5934e1f870fc36a29fb22fd +++ b/src/tracee/statx.c @@ -4,0 +5 @@ +#include "tracee/statx_mnt_id.h" -@@ -125,0 +127,9 @@ int handle_statx_syscall(Tracee *tracee, bool from_sigsys) { -+ /* libglnx chases an already-open fd with AT_EMPTY_PATH and requires a -+ * mount ID. Kernels before 5.8 can succeed without returning that field. -+ * The tracee is stopped at this syscall exit; accept only the ordinary -+ * mount ID and verify the fdinfo inode against the statx result. */ +@@ -125,0 +127,5 @@ int handle_statx_syscall(Tracee *tracee, bool from_sigsys) { + if (droiddeck_proot_statx_fill_mnt_id(tracee->pid, (int) dirfd, + (int) flags, guest_path, (unsigned int) mask, + &state.statx_buf)) From a074ba994b038ffd0919b0df9d05ef56e15bd88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=AD?= <juanma@microsoft.com> Date: Tue, 13 Oct 2026 22:29:23 -0400 Subject: [PATCH 13/14] Improve store install storage checks and recovery; adaptive parallel Epic installs (#710) Space checks before Epic, GOG and Amazon installs (measured zero vs unknown), clear failure reasons with retry/clear, resume-aware checks, and safe per-file replacement so a failed or cancelled update never breaks an installed game. Epic installs stream chunks straight into staged files in parallel (most bytes written once, a small budgeted cache for shared chunks, about 1x game size instead of 2x), pace updates by free space, and keep the lookahead, batch and bounded Java writers as hidden alternatives. Co-authored-by: Juan Marti Mercado <juanma@microsoft.com> Co-authored-by: The412Banner <the412banner@users.noreply.github.com> --- .../launcher/session/SessionPrefs.kt | 9 + .../launcher/stores/StoreGameSidecar.kt | 1 + .../launcher/stores/StoreInstallRoot.kt | 57 +- .../launcher/stores/StoreInstalls.kt | 27 +- .../droiddeck/launcher/stores/StoresState.kt | 20 +- .../stores/amazon/AmazonDownloadManager.java | 24 +- .../launcher/stores/download/DownloadQueue.kt | 43 +- .../stores/download/StoreDownloadService.kt | 8 +- .../launcher/stores/epic/EpicBackend.kt | 2 + .../stores/epic/EpicDownloadManager.java | 1139 ++++++++-- .../launcher/stores/epic/EpicNative.kt | 106 +- .../stores/gog/GogDownloadManager.java | 20 +- .../com/droiddeck/launcher/ui/StoresDetail.kt | 26 +- .../droiddeck/launcher/ui/StoresDownloads.kt | 16 +- .../launcher/ui/StoresInstallDialog.kt | 17 +- .../com/droiddeck/launcher/ui/StoresPage.kt | 12 +- app/src/main/res/values/strings.xml | 3 + app/src/main/rust/stores/JNI.md | 91 +- app/src/main/rust/stores/src/epic.rs | 2 + app/src/main/rust/stores/src/epic/adaptive.rs | 1585 +++++++++++++ app/src/main/rust/stores/src/epic/assemble.rs | 29 +- app/src/main/rust/stores/src/epic/chunk.rs | 144 +- app/src/main/rust/stores/src/epic/jni.rs | 269 ++- app/src/main/rust/stores/src/epic/stream.rs | 1956 +++++++++++++++++ app/src/main/rust/stores/src/fetch_core.rs | 24 +- .../launcher/stores/StoreInstallRootTest.kt | 84 + .../stores/amazon/AmazonResumeSpaceTest.kt | 43 + .../stores/download/DownloadQueueRetryTest.kt | 140 ++ .../stores/epic/EpicAdaptivePlanTest.kt | 107 + .../stores/epic/EpicBoundedInstallTest.kt | 646 ++++++ .../stores/epic/EpicJsonManifestTest.kt | 136 ++ .../launcher/stores/gog/GogResumeSpaceTest.kt | 43 + docs/development/stores.md | 180 +- tools/release/check_android_libraries.py | 1 + 34 files changed, 6777 insertions(+), 233 deletions(-) create mode 100644 app/src/main/rust/stores/src/epic/adaptive.rs create mode 100644 app/src/main/rust/stores/src/epic/stream.rs create mode 100644 app/src/test/java/com/droiddeck/launcher/stores/amazon/AmazonResumeSpaceTest.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/stores/download/DownloadQueueRetryTest.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/stores/epic/EpicAdaptivePlanTest.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/stores/epic/EpicBoundedInstallTest.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/stores/gog/GogResumeSpaceTest.kt 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 0d4b827fe..9ed3ec818 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -82,6 +82,15 @@ 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 storesOpenTab(context: Context): String = prefs(context).getString("storesOpenTab", STORES_OPEN_LIBRARY)?.takeIf { it == STORES_OPEN_STORE } ?: STORES_OPEN_LIBRARY diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt index 1753003d5..25850f747 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt @@ -68,6 +68,7 @@ class StoreGameSidecar( const val VERSION = 1 const val STATE_INSTALLED = "installed" const val STATE_INSTALLING = "installing" + const val STATE_UPDATING = "updating" fun file(folder: File): File = File(folder, FILE_NAME) 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 e122aa057..d36f88678 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt @@ -9,12 +9,67 @@ import java.io.File import java.util.zip.CRC32 object StoreInstallRoot { + const val SPACE_UNKNOWN = -1L private const val INTERNAL = "root/Games/Stores" private const val ON_VOLUME = "Games" const val GUEST_EXTERNAL = "/mnt/droiddeck-stores" class Target(val root: File, val label: String, val removable: Boolean) { - val freeBytes: Long get() = try { StatFs((if (root.isDirectory) root else root.parentFile ?: root).path).availableBytes } catch (e: Exception) { 0L } + val freeBytes: Long get() = availableBytes(root) + } + + data class StorageCheck( + val enough: Boolean, + val requiredBytes: Long, + val availableBytes: Long, + val errorMessage: String? = null + ) + + @JvmStatic + fun availableBytes(file: File): Long = probeAvailableBytes(file, { StatFs(it.path).availableBytes }, { it.usableSpace }) + + internal fun probeAvailableBytes(file: File, statBytes: (File) -> Long, usableBytes: (File) -> Long): Long { + var dir: File? = if (file.isDirectory) file else file.parentFile + if (dir == null) dir = file + while (dir != null && !dir.exists()) dir = dir.parentFile + val existing = dir ?: return SPACE_UNKNOWN + return try { + statBytes(existing).coerceAtLeast(SPACE_UNKNOWN) + } catch (e: Exception) { + StoresState.logLine("storage: StatFs failed for store install target (${e.javaClass.simpleName})") + try { + // File's zero also means an unavailable measurement; only StatFs establishes zero. + usableBytes(existing).takeIf { it > 0L } ?: SPACE_UNKNOWN + } catch (fallback: Exception) { + StoresState.logLine("storage: usable-space fallback failed for store install target (${fallback.javaClass.simpleName})") + SPACE_UNKNOWN + } + } + } + + @JvmStatic + fun insufficientSpace(required: Long, available: Long): Boolean = required > 0L && available >= 0L && available < required + + /** + * The catalog size is the known minimum for an install. Epic's actual chunk-cache requirement + * is determined from its manifest before chunk downloads begin. + */ + fun estimatedBytesNeeded(item: CatalogItem): Long = StoreSizes.size(item).coerceAtLeast(0L) + + /** Pre-install storage check to detect insufficient space before starting. */ + fun checkStorage(context: Context, item: CatalogItem, targetRoot: File): StorageCheck { + val size = estimatedBytesNeeded(item) + val existing = existingFolder(context, item.store, item.id, item.title) + val freeTarget = availableBytes(existing ?: targetRoot) + // Backends account for completed files on resume; the catalog cannot describe remaining work. + // Comparing the full catalog size to remaining free space would reject valid resumes. + if (item.store == Store.EPIC || existing != null) return StorageCheck(enough = true, requiredBytes = size, availableBytes = freeTarget) + if (size <= 0L) return StorageCheck(enough = true, requiredBytes = 0L, availableBytes = freeTarget) + if (insufficientSpace(size, freeTarget)) { + val msg = context.getString(com.droiddeck.launcher.R.string.stores_storage_insufficient, formatBytes(size), formatBytes(freeTarget)) + return StorageCheck(enough = false, requiredBytes = size, availableBytes = freeTarget, errorMessage = msg) + } + return StorageCheck(enough = true, requiredBytes = size, availableBytes = freeTarget) } fun internalRoot(context: Context): File = File(LinuxRuntime.rootDir(context), INTERNAL) 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 4616c51e6..0e629a6e7 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt @@ -7,15 +7,35 @@ import com.droiddeck.launcher.frontend.AddedGames import com.droiddeck.launcher.frontend.LibraryCache import com.droiddeck.launcher.frontend.Library import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption object StoreInstalls { private const val TAG = "StoreInstalls" fun begin(folder: File, store: Store, id: String, title: String, cover: String?, hero: String?) { - if (StoreGameSidecar.read(folder)?.isInstalled == true) return + val before = StoreGameSidecar.read(folder) + if (before?.isInstalled == true || before?.state == StoreGameSidecar.STATE_UPDATING) return StoreGameSidecar(store, id, title, exe = "", cover = cover, hero = hero, state = StoreGameSidecar.STATE_INSTALLING).write(folder) } + /** Epic commits verified files individually, not a whole-game transaction. Keep updates resumable. */ + @JvmStatic + fun markUpdating(folder: File) { + val before = StoreGameSidecar.read(folder) ?: return + if (!before.isInstalled) return + val stage = File.createTempFile(".droiddeck-update-", ".stage", folder) + try { + java.io.FileOutputStream(stage).use { + it.write(before.copy(state = StoreGameSidecar.STATE_UPDATING, installVersion = "").toJson().toString(2).toByteArray(Charsets.UTF_8)) + it.fd.sync() + } + Files.move(stage.toPath(), StoreGameSidecar.file(folder).toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) + } finally { + stage.delete() + } + } + fun complete(context: Context, folder: File, sidecar: StoreGameSidecar, onStep: ((Int, Int) -> Unit)? = null) { val app = context.applicationContext onStep?.invoke(0, STEPS) @@ -66,10 +86,11 @@ object StoreInstalls { fun discard(context: Context, folder: File, caches: List<File>) { val app = context.applicationContext Thread({ - val keep = StoreGameSidecar.read(folder)?.isInstalled == true + val sidecar = StoreGameSidecar.read(folder) + val keep = sidecar?.isInstalled == true || sidecar?.state == StoreGameSidecar.STATE_UPDATING caches.forEach { deleteTree(it) } if (!keep) deleteTree(folder) - StoresState.logLine(if (keep) "removed the partial update of ${folder.name}" else "removed ${folder.name}") + StoresState.logLine(if (keep) "removed update caches for ${folder.name}; verified file swaps are retained" else "removed ${folder.name}") StoresState.notifyLibraryChanged(app) }, "store-cancel-clean").start() } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt index 5551535d3..74af04fb8 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt @@ -170,7 +170,25 @@ object StoresState { fun install(context: Context, item: CatalogItem, root: java.io.File? = null) { val app = context.applicationContext - val target = root ?: StoreInstallRoot.installRoot(app) + val folder = StoreInstallRoot.folderFor(app, item.store, item.id, item.title, root ?: StoreInstallRoot.installRoot(app)) + val target = folder.parentFile!!.parentFile!! + val check = StoreInstallRoot.checkStorage(app, item, target) + if (!check.enough) { + val errorMsg = check.errorMessage ?: app.getString(com.droiddeck.launcher.R.string.stores_storage_insufficient, formatBytes(check.requiredBytes), formatBytes(check.availableBytes)) + val entry = com.droiddeck.launcher.stores.download.DownloadEntry( + item.store, + item.id, + item.title, + cover = item.imageUrl, + bytesTotal = item.sizeBytes, + location = StoreInstallRoot.labelFor(app, folder) + ) + com.droiddeck.launcher.stores.download.SavedQueue.remember(app, item, target) + com.droiddeck.launcher.stores.download.DownloadQueue.recordFailed(app, entry, errorMsg) { + install(app, item, target) + } + return + } com.droiddeck.launcher.stores.download.SavedQueue.remember(app, item, target) backends[item.store]?.install(app, item, target) ?: logLine("${item.store.label}: this build cannot install yet") } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java index 2eaaa31a2..6145f0250 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java @@ -69,9 +69,10 @@ public final class AmazonDownloadManager { if (manifestBytes == null) throw new InstallException("The manifest could not be downloaded"); AmazonManifest.ParsedManifest manifest = AmazonManifest.parse(manifestBytes); cb.onLog("amazon: " + manifest.allFiles.size() + " files, " + fmt(manifest.totalInstallSize)); - long usable = installDir.getUsableSpace(); - if (manifest.totalInstallSize > 0 && usable > 0 && manifest.totalInstallSize > usable) - throw new InstallException("Not enough free space: need " + fmt(manifest.totalInstallSize) + ", only " + fmt(usable) + " free"); + long remainingBytes = remainingInstallBytes(manifest.allFiles, installDir); + long usable = com.droiddeck.launcher.stores.StoreInstallRoot.availableBytes(installDir); + if (com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(remainingBytes, usable)) + throw new InstallException("Not enough free space: need " + fmt(remainingBytes) + ", only " + fmt(usable) + " free"); cb.onSizes(manifest.totalInstallSize, manifest.totalInstallSize); cb.onBytes(0, manifest.totalInstallSize, 0); if (cancel.get()) return null; @@ -175,7 +176,7 @@ public final class AmazonDownloadManager { AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps, Callback cb, AtomicBoolean cancel) { File destFile = new File(installDir, file.unixPath()); File tmpFile = new File(installDir, file.unixPath() + ".tmp"); - if (destFile.exists() && destFile.length() == file.size) { totalDownloaded.addAndGet(file.size); return true; } + if (fileComplete(file, destFile)) { totalDownloaded.addAndGet(file.size); return true; } String fileUrl = AmazonApiClient.appendPath(baseUrl, "files/" + file.hashHex()); for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { destFile.getParentFile().mkdirs(); @@ -245,6 +246,21 @@ public final class AmazonDownloadManager { public static boolean isInstalled(File installDir) { return new File(installDir, COMPLETE_MARKER).exists(); } + static long remainingInstallBytes(List<AmazonManifest.ManifestFile> files, File installDir) { + long remaining = 0; + for (AmazonManifest.ManifestFile file : files) { + if (!fileComplete(file, new File(installDir, file.unixPath()))) remaining += file.size; + } + return remaining; + } + + private static boolean fileComplete(AmazonManifest.ManifestFile file, File output) { + if (!output.isFile() || output.length() != file.size) return false; + if (file.hashAlgorithm != 0 || file.hashBytes.length == 0) return true; + try { return Arrays.equals(sha256(output), file.hashBytes); } + catch (Exception e) { return false; } + } + private static byte[] sha256(File file) throws Exception { MessageDigest digest = MessageDigest.getInstance("SHA-256"); try (FileInputStream fis = new FileInputStream(file)) { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt index 93621aa9f..e2549a3be 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt @@ -88,7 +88,7 @@ object DownloadQueue { } } - private class Item(var entry: DownloadEntry, val factory: () -> DownloadJob, var job: DownloadJob? = null, val cancelled: AtomicBoolean = AtomicBoolean(false), var pauseRequested: Boolean = false) { + private class Item(var entry: DownloadEntry, val factory: (() -> DownloadJob)? = null, val retry: (() -> Unit)? = null, var job: DownloadJob? = null, val cancelled: AtomicBoolean = AtomicBoolean(false), var pauseRequested: Boolean = false) { var speedAt = 0L var speedBytes = 0L var speedEwma = 0.0 @@ -137,6 +137,32 @@ object DownloadQueue { advance(app) } + /** Immediately records a failed download (e.g. from pre-flight storage check). */ + fun recordFailed(context: Context, entry: DownloadEntry, error: String, retry: (() -> Unit)? = null) { + val app = context.applicationContext + val now = System.currentTimeMillis() + synchronized(lock) { + if (items[entry.key]?.entry?.isActive == true) return + items.remove(entry.key) + heldPaused.remove(entry.key) + items[entry.key] = Item( + entry.copy( + state = DownloadState.FAILED, + error = error, + speedBps = 0, + etaSeconds = -1, + startedAt = now, + finishedAt = now + ), + retry = retry + ) + renumber() + publishLocked() + } + StoresState.logLine("${entry.store.label}: \"${entry.name}\" failed: $error") + StoreDownloadService.ended(app, entry.copy(state = DownloadState.FAILED, error = error), installed = false, gamesKey = null) + } + fun pause(key: String) { val item = synchronized(lock) { items[key] } ?: return synchronized(lock) { @@ -151,14 +177,21 @@ object DownloadQueue { fun resume(context: Context, key: String) { val app = context.applicationContext + val retry: (() -> Unit)? synchronized(lock) { val item = items[key] ?: return if (item.entry.state != DownloadState.PAUSED && item.entry.state != DownloadState.FAILED) return + retry = item.retry + if (retry == null && item.factory == null) return items.remove(key) - items[key] = Item(item.entry.copy(state = DownloadState.QUEUED, speedBps = 0, etaSeconds = -1, error = null, startedAt = System.currentTimeMillis()), item.factory) + if (retry == null) { + items[key] = Item(item.entry.copy(state = DownloadState.QUEUED, speedBps = 0, etaSeconds = -1, error = null, startedAt = System.currentTimeMillis()), item.factory) + } renumber() publishLocked() } + // A preflight retry rechecks storage and enqueues the real backend, never a running placeholder. + if (retry != null) { retry(); return } StoresState.logLine("resumed \"${entry(key)?.name}\"") advance(app) } @@ -182,7 +215,7 @@ object DownloadQueue { item.cancelled.set(true) item.job?.cancel(deleteFiles = true) } else { - Thread({ runCatching { item.factory().cancel(deleteFiles = true) } }, "store-dl-cancel").start() + Thread({ runCatching { item.factory?.invoke()?.cancel(deleteFiles = true) } }, "store-dl-cancel").start() } StoresState.logLine("cancelled \"${item.entry.name}\"") StoreDownloadService.finish(app, key) @@ -194,7 +227,7 @@ object DownloadQueue { val item = synchronized(lock) { items[key] } ?: return if (item.entry.state == DownloadState.RUNNING || item.entry.state == DownloadState.QUEUED) return if (item.entry.state == DownloadState.FAILED || item.entry.state == DownloadState.PAUSED) { - Thread({ runCatching { item.factory().cancel(deleteFiles = true) } }, "store-dl-clear").start() + Thread({ runCatching { item.factory?.invoke()?.cancel(deleteFiles = true) } }, "store-dl-clear").start() } synchronized(lock) { items.remove(key); publishLocked() } StoreDownloadService.finish(context.applicationContext, key) @@ -276,7 +309,7 @@ object DownloadQueue { var result: String? = null var error: String? = null try { - val job = item.factory() + val job = checkNotNull(item.factory).invoke() synchronized(lock) { item.job = job } StoresState.logLine("${item.entry.store.label}: fetching manifest for \"${item.entry.name}\"") result = job.run(handle) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt index 791dd06b1..a32bceeb6 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt @@ -239,12 +239,16 @@ class StoreDownloadService : Service() { val app = context.applicationContext channels(app) val code = entry.key.hashCode() - val n = Notification.Builder(app, DONE_CHANNEL) + val builder = Notification.Builder(app, DONE_CHANNEL) .setSmallIcon(if (installed) android.R.drawable.stat_sys_download_done else android.R.drawable.stat_notify_error) .setContentTitle(app.getString(if (installed) R.string.stores_notification_installed else R.string.stores_notification_failed, entry.name)) .setAutoCancel(true) .setContentIntent(if (installed && gamesKey != null) openPage(app, gamesKey, code + 10) else openDownloads(app, code + 11)) - .build() + if (!installed && !entry.error.isNullOrBlank()) { + builder.setContentText(entry.error) + builder.style = Notification.BigTextStyle().bigText(entry.error) + } + val n = builder.build() runCatching { app.getSystemService(NotificationManager::class.java)?.notify(ID + 1 + (code and 0xFFFF), n) } } } 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 e631cf78b..5603ba742 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt @@ -136,6 +136,8 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { EpicLibrary.rememberSize(app, item.id, diskBytes) } override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + // Assembly overlaps chunk fetching; only the final install event advances the UI. + if (stage == "stream") return handle.stage(when (stage) { "check" -> DownloadStage.MANIFEST; "verify" -> DownloadStage.VERIFY; else -> DownloadStage.INSTALL }) handle.stageProgress(done, total, items, itemsTotal, bytes = stage == "install") } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java index 958e9b158..88307fd25 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java @@ -4,13 +4,15 @@ import android.content.Context; import android.util.Log; import com.droiddeck.launcher.stores.StoreLog; +import com.droiddeck.launcher.stores.StoreInstallRoot; +import com.droiddeck.launcher.stores.StoreInstalls; +import com.droiddeck.launcher.stores.StoresNative; import com.droiddeck.launcher.stores.download.StoreDownloadTier; import org.json.JSONArray; import org.json.JSONObject; import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -21,6 +23,10 @@ import java.net.URL; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Iterator; @@ -29,11 +35,13 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import java.util.zip.Inflater; @@ -41,6 +49,39 @@ public final class EpicDownloadManager { private static final String TAG = "EpicDownload"; private static final String UA = EpicAuthClient.USER_AGENT; + /** Keep reusable chunk data bounded even when a manifest shares chunks across distant files. */ + static final long MAX_RETAINED_CHUNK_CACHE_BYTES = 64L * 1024 * 1024; + static final int PREFETCH_PARTS = 8; + static final String STAGING_DIRECTORY = ".droiddeck-epic-staging"; + private static final String STAGING_OWNER = "DroidDeck Epic staging v1\n"; + /** Most the adaptive engine caches of shared chunks; more only saves the occasional refetch. */ + static final long ADAPTIVE_CACHE_MAX_BYTES = 1024L * 1024 * 1024; + /** Its budget when free space cannot be read. */ + static final long ADAPTIVE_CACHE_DEFAULT_BYTES = 256L * 1024 * 1024; + /** + * Below this, internal scratch is too small to be worth a cache of its own: the shared chunks + * then go beside the game (still released at their last use) rather than failing the install. + */ + static final long ADAPTIVE_CACHE_MIN_SCRATCH_BYTES = MAX_RETAINED_CHUNK_CACHE_BYTES; + /** Chunks being decoded, written or released at any moment, beyond the budget itself. */ + static final long ADAPTIVE_TRANSIENT_BYTES = 64L * 1024 * 1024; + + static final String MODE_ADAPTIVE = "adaptive"; + static final String MODE_LOOKAHEAD = "lookahead"; + static final String MODE_BATCH = "batch"; + static final String MODE_JAVA = "java"; + + /** Which writer {@link #install} asks for and how wide the native fetch may go. */ + static final class NativeOptions { + final String mode; + final int maxWorkers, processWorkers; + final String tierId; + NativeOptions(String mode, int maxWorkers, int processWorkers, String tierId) { + this.mode = mode; this.maxWorkers = maxWorkers; this.processWorkers = processWorkers; this.tierId = tierId; + } + /** The default: the adaptive engine at the Fast tier's window. */ + static NativeOptions defaults() { return new NativeOptions(MODE_ADAPTIVE, 32, 2, "fast"); } + } public interface Callback { void onProgress(String message, int pct); @@ -93,6 +134,8 @@ public final class EpicDownloadManager { public String prereqPath = ""; public boolean prereqRead = false; public List<CdnUrl> cdnUrls = new ArrayList<>(); + /** The manifest as downloaded, for the native engines (which parse it themselves); null = Java only. */ + byte[] rawBytes = null; } public static final class Result { @@ -125,13 +168,45 @@ public final class EpicDownloadManager { Manifest manifest = parseManifest(manifestBytes); if (manifest == null) throw new InstallException("The manifest could not be parsed"); manifest.cdnUrls = cdnUrls; + manifest.rawBytes = manifestBytes; cb.onLog("epic: chunkDir=" + manifest.chunkDir + " chunks=" + manifest.uniqueChunks.size() + " files=" + manifest.files.size() + " exe=" + manifest.launchExe); + StoreDownloadTier tier = StoreDownloadTier.Companion.current(ctx); + String mode = com.droiddeck.launcher.session.SessionPrefs.INSTANCE.storesEpicMode(ctx); + NativeOptions options = new NativeOptions(mode, Math.max(1, Math.min(128, tier.getNetworkWindow())), Math.max(2, tier.getProcessWorkers()), tier.getId()); + return installManifest(manifest, installDirPath, chunkCacheDirPath, installTags, cancel, cb, options); + } catch (InstallException e) { + throw e; + } catch (Exception e) { + Log.w(TAG, "install failed", e); + throw new InstallException(e.getClass().getSimpleName() + (e.getMessage() != null ? ": " + e.getMessage() : "")); + } + } + static Result installManifest(Manifest manifest, String installDirPath, String chunkCacheDirPath, + List<String> installTags, AtomicBoolean cancel, Callback cb) throws InstallException { + return installManifest(manifest, installDirPath, chunkCacheDirPath, installTags, cancel, cb, NativeOptions.defaults()); + } + + static Result installManifest(Manifest manifest, String installDirPath, String chunkCacheDirPath, + List<String> installTags, AtomicBoolean cancel, Callback cb, + NativeOptions nativeOptions) throws InstallException { + Result result = installSelected(manifest, installDirPath, chunkCacheDirPath, installTags, cancel, cb, nativeOptions); + // A finished install leaves no staging; the owned namespace is recreated by the next run. + if (result != null) releaseOwnedStaging(new File(installDirPath)); + return result; + } + + private static Result installSelected(Manifest manifest, String installDirPath, String chunkCacheDirPath, + List<String> installTags, AtomicBoolean cancel, Callback cb, + NativeOptions nativeOptions) throws InstallException { + try { + List<CdnUrl> cdnUrls = manifest.cdnUrls; File installDir = new File(installDirPath); installDir.mkdirs(); final String cachePath = chunkCacheDirPath == null ? "" : chunkCacheDirPath; final File chunkCacheDir = cachePath.isEmpty() ? new File(installDir, ".chunks") : new File(cachePath); chunkCacheDir.mkdirs(); + File stagingDir = recoverOwnedStaging(installDir, manifest.files); List<FileInfo> selected = resolveInstallFiles(manifest, installTags); cb.onLog("epic: " + selected.size() + "/" + manifest.files.size() + " files for tags " + (installTags == null ? "(all)" : installTags.toString())); @@ -160,111 +235,159 @@ public final class EpicDownloadManager { return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest); } List<ChunkInfo> needed = uniqueChunksForFiles(manifest, pending); - long cacheMissing = 0, pendingBytes = 0; - for (ChunkInfo c : needed) if (!new File(chunkCacheDir, c.guidStr()).isFile()) cacheMissing += Math.max(c.windowSize, 1); - for (FileInfo f : pending) pendingBytes += f.fileSize(); - if (cachePath.isEmpty()) { - long free = installDir.getUsableSpace(); - if (free > 0 && cacheMissing + pendingBytes > free) throw new InstallException("Not enough free space: need " + fmt(cacheMissing + pendingBytes) + " (" + fmt(pendingBytes) + " of game files + " + fmt(cacheMissing) + " of download cache), only " + fmt(free) + " free"); - } else { - long freeCache = chunkCacheDir.getUsableSpace(), freeGame = installDir.getUsableSpace(); - if (freeCache > 0 && cacheMissing > freeCache) throw new InstallException("Not enough internal space for the download cache: need " + fmt(cacheMissing) + ", only " + fmt(freeCache) + " free"); - if (freeGame > 0 && pendingBytes > freeGame) throw new InstallException("Not enough free space: need " + fmt(pendingBytes) + ", only " + fmt(freeGame) + " free"); + Map<String, ChunkInfo> chunksByGuid = new LinkedHashMap<>(); + for (ChunkInfo c : needed) chunksByGuid.put(c.guidStr(), c); + if (!chunkCacheDir.isDirectory() && !chunkCacheDir.mkdirs()) throw new InstallException("Could not create Epic chunk cache"); + long pendingBytes = 0; + Map<String, Long> oldFileBytes = new LinkedHashMap<>(); + for (FileInfo f : pending) { + pendingBytes += f.fileSize(); + File output = new File(installDir, f.filename.replace("\\", "/")); + if (output.isFile()) oldFileBytes.put(f.filename, output.length()); } - cb.onLog("epic: cache " + (cachePath.isEmpty() ? "beside the game" : "scratch") + ", " + fmt(cacheMissing) + " to fetch for " + fmt(pendingBytes) + " of files"); - long totalBytes = 0; - for (ChunkInfo c : needed) totalBytes += Math.max(c.fileSize, 1); - final long fTotalBytes = totalBytes; - final int totalChunks = needed.size(); - final AtomicLong completedBytes = new AtomicLong(0); - final AtomicInteger completedCount = new AtomicInteger(0); - final AtomicInteger failCount = new AtomicInteger(0); - final AtomicLong lastSpeedMs = new AtomicLong(System.currentTimeMillis()); - final AtomicLong lastSpeedBytes = new AtomicLong(0); - final AtomicLong speedBps = new AtomicLong(0); - cb.onSizes(fTotalBytes, planned); - cb.onBytes(0, fTotalBytes, 0); - cb.onLog("epic: " + fmt(fTotalBytes) + " in " + totalChunks + " chunks"); - - boolean javaPool = true; - final int[] pendingIdx = pendingIndices(manifest, pending); - boolean engine = com.droiddeck.launcher.stores.StoresNative.INSTANCE.getAvailable() && pendingIdx != null; - if (engine) { - NativeOutcome r = runNativePool(ctx, manifestBytes, pendingIdx, needed, fTotalBytes, installDirPath, cachePath, cdnUrls, cancel, cb, - completedBytes, completedCount, lastSpeedMs, lastSpeedBytes, speedBps); - if (r.started) { - javaPool = false; - if (r.cancelled) return null; - if (!r.success) { cb.onLog("epic: engine failed: " + r.error); failCount.incrementAndGet(); } - } else cb.onLog("epic: engine not started (" + r.error + "); using the built-in pool"); + NativeOptions options = nativeOptions != null ? nativeOptions : NativeOptions.defaults(); + String mode = options.mode; + if (!MODE_LOOKAHEAD.equals(mode) && !MODE_BATCH.equals(mode) && !MODE_JAVA.equals(mode)) mode = MODE_ADAPTIVE; + boolean nativeAvailable = StoresNative.INSTANCE.ensureLoaded(); + if (!nativeAvailable && !MODE_JAVA.equals(mode)) { + cb.onLog("epic: native engine unavailable; mode=java-bounded"); + mode = MODE_JAVA; + } else if (MODE_JAVA.equals(mode)) { + cb.onLog("epic: mode=java-bounded chosen by storesEpicMode"); } - if (javaPool) { - cb.onLog("epic: engine=built-in (8 threads)"); - ExecutorService pool = Executors.newFixedThreadPool(8, com.droiddeck.launcher.stores.download.DownloadQueue.workerFactory("epic-dl")); - for (ChunkInfo chunk : needed) { - final ChunkInfo fc = chunk; - pool.submit(() -> { - if (cancel.get()) return; - File cached = new File(chunkCacheDir, fc.guidStr()); - if (!cached.exists() && !downloadChunkStreaming(fc, manifest.chunkDir, cdnUrls, cached)) { - cb.onLog("FAIL chunk=" + fc.guidStr()); - failCount.incrementAndGet(); - return; - } - long done = completedBytes.addAndGet(Math.max(fc.fileSize, 1)); - int cnt = completedCount.incrementAndGet(); - sampleSpeed(done, lastSpeedMs, lastSpeedBytes, speedBps); - cb.onProgress("Downloading chunks (" + cnt + "/" + totalChunks + ")" + speedSuffix(speedBps.get()), (int) (done * 80L / Math.max(1, fTotalBytes))); - cb.onBytes(done, fTotalBytes, speedBps.get()); - }); - } - pool.shutdown(); - try { - while (!pool.awaitTermination(250, TimeUnit.MILLISECONDS)) { - if (cancel.get()) { pool.shutdownNow(); pool.awaitTermination(5, TimeUnit.SECONDS); return null; } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - pool.shutdownNow(); - throw new InstallException("interrupted"); - } + if (MODE_ADAPTIVE.equals(mode)) { + NativeAttempt attempt = installAdaptive(manifest, pending, pendingBytes, oldFileBytes, planned, installDir, cachePath, chunkCacheDir, stagingDir, options, cancel, cb); + if (attempt.handled) return attempt.result; + // Its space plan did not fit: the lookahead engine sizes its window to what is free. + mode = MODE_LOOKAHEAD; + } else if (MODE_BATCH.equals(mode)) { + return installBatch(manifest, pending, needed, pendingBytes, planned, installDir, cachePath, chunkCacheDir, options, cancel, cb); + } + boolean lookahead = MODE_LOOKAHEAD.equals(mode); + Map<String, Long> cachedChunkBytes = prepareBoundedCache(chunkCacheDir, needed); + boolean cacheOnInstallVolume = cachePath.isEmpty(); + long peakGameAdditional = lookahead ? 0 : cacheOnInstallVolume + ? estimatePeakAdditionalBytes(pending, chunksByGuid, cachedChunkBytes, true, oldFileBytes) + : estimateStagedFilePeak(pending, oldFileBytes); + long peakCacheAdditional = lookahead || cacheOnInstallVolume + ? 0 : estimatePeakAdditionalBytes(pending, chunksByGuid, cachedChunkBytes, false); + long freeGame = StoreInstallRoot.availableBytes(installDir); + // The lookahead engine plans its own window against both free-space readings. + if (!lookahead && cacheOnInstallVolume) { + if (StoreInstallRoot.insufficientSpace(peakGameAdditional, freeGame)) throw new InstallException("Not enough free space for safe staged Epic replacement: need up to " + fmt(peakGameAdditional) + " additional, only " + fmt(freeGame) + " free"); + } else if (!lookahead) { + long freeCache = StoreInstallRoot.availableBytes(chunkCacheDir); + if (StoreInstallRoot.insufficientSpace(peakCacheAdditional, freeCache)) throw new InstallException("Not enough internal space for the bounded Epic download cache: need up to " + fmt(peakCacheAdditional) + " additional, only " + fmt(freeCache) + " free"); + if (StoreInstallRoot.insufficientSpace(peakGameAdditional, freeGame)) throw new InstallException("Not enough free space for safe staged Epic replacement: need " + fmt(peakGameAdditional) + ", only " + fmt(freeGame) + " free"); } if (cancel.get()) return null; - if (failCount.get() > 0) throw new InstallException(failCount.get() + " chunk(s) failed to download"); - - if (engine) { - NativeOutcome r = runNativeAssembly(manifestBytes, pendingIdx, pending.size(), installDirPath, cachePath, cancel, cb); - if (r.started) { - if (r.cancelled) return null; - if (!r.success) throw new InstallException(r.error.isEmpty() ? "assembly failed" : r.error); - cb.onProgress("Complete", 100); - return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest); - } - cb.onLog("epic: assembler not started (" + r.error + "); writing the files here"); - } - int totalFiles = pending.size(), doneFiles = 0; - long writtenBytes = 0, writeTotal = 0; - for (FileInfo f : pending) writeTotal += f.fileSize(); - for (FileInfo file : pending) { - if (cancel.get()) return null; - String relPath = file.filename.replace("\\", "/"); - File outFile = new File(installDir, relPath); - File parent = outFile.getParentFile(); - if (parent != null) parent.mkdirs(); - cb.onProgress("Writing: " + (relPath.contains("/") ? relPath.substring(relPath.lastIndexOf('/') + 1) : relPath), 80 + (int) (doneFiles * 20L / totalFiles)); - try (FileOutputStream fos = new FileOutputStream(outFile); BufferedOutputStream bos = new BufferedOutputStream(fos, 65536)) { - for (ChunkPart part : file.parts) { - File cachedChunk = new File(chunkCacheDir, part.guidStr()); - if (!cachedChunk.exists()) throw new InstallException("Missing chunk " + part.guidStr() + " for " + relPath); - byte[] data = readFile(cachedChunk); - bos.write(data, part.offset, part.size); + if (lookahead) { + JSONObject request = nativeRequest(manifest, pending, chunksByGuid, cachedChunkBytes, + oldFileBytes, installDir, chunkCacheDir, stagingDir, cacheOnInstallVolume); + request.put("free_game", StoreInstallRoot.availableBytes(installDir)); + request.put("free_cache", StoreInstallRoot.availableBytes(chunkCacheDir)); + final long nativeTotal = pendingBytes; + final long nativeStarted = System.nanoTime(); + cb.onSizes(nativeTotal, planned); + cb.onBytes(0, nativeTotal, 0); + EpicNative.Result outcome = EpicNative.installAdaptive(request.toString(), cancel, new EpicNative.InstallListener() { + @Override public void onPlan(int chunks, long bytes, String plan) { + cb.onLog("epic: mode=lookahead engine=lookahead-rust " + plan); } + @Override public void onProgress(long done, long total, int files, int filesTotal) { + long elapsedMs = Math.max(1, (System.nanoTime() - nativeStarted) / 1_000_000); + cb.onBytes(done, total, done * 1000 / elapsedMs); + cb.onProgress("Downloading chunks…", (int) (done * 80L / Math.max(1, total))); + cb.onStage("stream", done, total, files, filesTotal); + } + @Override public void onLog(String line) { cb.onLog(line); } + @Override public void onComplete(boolean success, String error, long bytes) {} + @Override public boolean beforeSwap() { + try { + if (cancel.get()) return false; + StoreInstalls.markUpdating(installDir); + return !cancel.get(); + } catch (Exception e) { + cb.onLog("epic: cannot mark updating: " + e.getMessage()); + return false; + } + } + }); + if (outcome.cancelled || cancel.get()) return null; + if (!outcome.success) throw new InstallException("Native Epic install failed: " + outcome.error); + long lookaheadMs = Math.max(1, (System.nanoTime() - nativeStarted) / 1_000_000); + cb.onLog("epic: summary mode=lookahead files=" + pending.size() + " bytes_written=" + nativeTotal + " elapsed_ms=" + lookaheadMs + + " avg_MBps=" + String.format(java.util.Locale.ROOT, "%.2f", nativeTotal * 1000.0 / lookaheadMs / 1048576.0)); + cb.onStage("install", nativeTotal, nativeTotal, pending.size(), pending.size()); + cb.onProgress("Complete", 100); + return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest); + } + cb.onLog("epic: cache " + (cacheOnInstallVolume ? "beside the game" : "scratch") + ", peak game additional=" + fmt(peakGameAdditional) + (cacheOnInstallVolume ? "" : ", peak cache additional=" + fmt(peakCacheAdditional)) + " for " + fmt(pendingBytes) + " of files"); + final long fTotalBytes = pendingBytes; + final int totalChunks = needed.size(); + cb.onSizes(fTotalBytes, planned); + cb.onBytes(0, fTotalBytes, 0); + cb.onLog("epic: " + fmt(fTotalBytes) + " to write using " + totalChunks + " unique chunks"); + cb.onLog("epic: engine=bounded-java (8-worker prefetch; native availability does not change cache semantics)"); + long completedBytes = 0, writtenBytes = 0; + long transferStarted = System.nanoTime(); + int completedCount = 0, doneFiles = 0, totalFiles = pending.size(); + Set<String> reservedPaths = new LinkedHashSet<>(); + for (FileInfo file : manifest.files) reservedPaths.add(new File(installDir, file.filename.replace("\\", "/")).getCanonicalPath()); + boolean updateRecorded = false; + try (ChunkPrefetch prefetch = new ChunkPrefetch(pending, chunksByGuid, cachedChunkBytes, + chunkCacheDir, manifest.chunkDir, cdnUrls, cancel)) { + for (FileInfo file : pending) { + if (cancel.get()) return null; + String relPath = file.filename.replace("\\", "/"); + File outFile = new File(installDir, relPath); + cb.onProgress("Downloading chunks: " + (relPath.contains("/") ? relPath.substring(relPath.lastIndexOf('/') + 1) : relPath), (int) (completedBytes * 80L / Math.max(1, fTotalBytes))); + MessageDigest fileSha = expectedDigest(file.sha1); + File partFile = createStagingFile(outFile, reservedPaths, stagingDir); + try { + try (FileOutputStream fos = new FileOutputStream(partFile); BufferedOutputStream bos = new BufferedOutputStream(fos, 65536)) { + for (ChunkPart part : file.parts) { + if (cancel.get()) throw new IOException("cancelled"); + String guid = part.guidStr(); + File cachedChunk = prefetch.await(guid); + completedCount++; + cb.onProgress("Downloading chunks (part " + completedCount + ")", (int) (completedBytes * 80L / Math.max(1, fTotalBytes))); + copyChunkPart(cachedChunk, part, bos, fileSha, cancel, relPath); + completedBytes += part.size & 0xFFFFFFFFL; + long elapsedMs = Math.max(1, (System.nanoTime() - transferStarted) / 1_000_000); + cb.onBytes(completedBytes, fTotalBytes, completedBytes * 1000 / elapsedMs); + prefetch.consume(); + } + bos.flush(); + fos.getFD().sync(); + } + if (cancel.get()) return null; + if (partFile.length() != file.fileSize()) throw new InstallException("Size mismatch for " + relPath); + if (fileSha != null && !MessageDigest.isEqual(fileSha.digest(), file.sha1)) { + throw new InstallException("SHA-1 mismatch for " + relPath); + } + // Once a swap can mix versions, the old installed record is no longer truthful. + if (!updateRecorded) { + StoreInstalls.markUpdating(installDir); + updateRecorded = true; + } + if (cancel.get()) return null; + Files.move(partFile.toPath(), outFile.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + if (cancel.get()) return null; + throw new InstallException("Could not assemble or atomically replace " + relPath + ": " + e.getMessage()); + } finally { + partFile.delete(); + } + doneFiles++; + writtenBytes += file.fileSize(); + cb.onStage("stream", writtenBytes, pendingBytes, doneFiles, totalFiles); } - doneFiles++; - writtenBytes += file.fileSize(); - cb.onStage("install", writtenBytes, writeTotal, doneFiles, totalFiles); } deleteDir(chunkCacheDir); + long boundedMs = Math.max(1, (System.nanoTime() - transferStarted) / 1_000_000); + cb.onLog("epic: summary mode=java-bounded files=" + doneFiles + " bytes_written=" + writtenBytes + " elapsed_ms=" + boundedMs + " avg_MBps=" + String.format(java.util.Locale.ROOT, "%.2f", writtenBytes * 1000.0 / boundedMs / 1048576.0)); + cb.onStage("install", writtenBytes, pendingBytes, doneFiles, totalFiles); cb.onProgress("Complete", 100); return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest); } catch (InstallException e) { @@ -275,9 +398,17 @@ public final class EpicDownloadManager { } } - private static final class NativeOutcome { boolean started, success, cancelled; String error = ""; } + /** What a native attempt did: {@code handled} = the install ended there ({@code result} null when cancelled). */ + private static final class NativeAttempt { + final boolean handled; + final Result result; + private NativeAttempt(boolean handled, Result result) { this.handled = handled; this.result = result; } + static final NativeAttempt FALLBACK = new NativeAttempt(false, null); + static NativeAttempt done(Result result) { return new NativeAttempt(true, result); } + } - private static int[] pendingIndices(Manifest manifest, List<FileInfo> pending) { + /** The pending files as indices into the manifest's file list, what the native runs take; null when one is not in it. */ + static int[] pendingIndices(Manifest manifest, List<FileInfo> pending) { java.util.IdentityHashMap<FileInfo, Integer> index = new java.util.IdentityHashMap<>(); for (int i = 0; i < manifest.files.size(); i++) index.put(manifest.files.get(i), i); int[] pendingIdx = new int[pending.size()]; @@ -289,58 +420,696 @@ public final class EpicDownloadManager { return pendingIdx; } - private static NativeOutcome runNativePool(Context ctx, byte[] manifestBytes, int[] pendingIdx, List<ChunkInfo> needed, long totalBytes, - String installDirPath, String cachePath, List<CdnUrl> cdnUrls, AtomicBoolean cancel, Callback cb, - AtomicLong completedBytes, AtomicInteger completedCount, AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps) { - NativeOutcome r = new NativeOutcome(); - try { - String[] prefixes = new String[cdnUrls.size()]; - for (int i = 0; i < cdnUrls.size(); i++) prefixes[i] = cdnUrls.get(i).baseUrl + cdnUrls.get(i).cloudDir; - final int totalChunks = needed.size(); - StoreDownloadTier tier = StoreDownloadTier.Companion.current(ctx); - int workers = Math.max(1, Math.min(128, tier.getNetworkWindow())); - int process = Math.max(2, tier.getProcessWorkers()); - cb.onLog("epic: engine=native workers=" + workers + " process_workers=" + process + " tier=" + tier.getId() + (cachePath.isEmpty() ? "" : " cache=scratch")); - EpicNative.Result res = EpicNative.run(manifestBytes, installDirPath, cachePath, prefixes, pendingIdx, totalChunks, totalBytes, "", workers, process, cancel, - new EpicNative.Listener() { - @Override public void onPlan(int chunksTotal, long bytesTotal, String chunkDir) { cb.onLog("epic: plan chunks=" + chunksTotal + " bytes=" + bytesTotal + " dir=" + chunkDir); } - @Override public void onProgress(long bytesDone, long bytesTotal, int chunksDone, int chunksTotal) { - completedBytes.set(bytesDone); - completedCount.set(chunksDone); - sampleSpeed(bytesDone, lastSpeedMs, lastSpeedBytes, speedBps); - cb.onProgress("Downloading chunks (" + chunksDone + "/" + totalChunks + ")" + speedSuffix(speedBps.get()), (int) (bytesDone * 80L / Math.max(1, totalBytes))); - cb.onBytes(bytesDone, totalBytes, speedBps.get()); - } - @Override public void onLog(String line) { cb.onLog(line); } - @Override public void onComplete(boolean success, String error, long bytesCredited) {} - }); - r.started = res.started; r.success = res.success; r.cancelled = res.cancelled; r.error = res.error == null ? "" : res.error; - } catch (Throwable t) { - r.started = false; - r.error = t.getClass().getSimpleName() + ": " + t.getMessage(); - } - return r; + private static String[] cdnPrefixes(List<CdnUrl> cdnUrls) { + String[] prefixes = new String[cdnUrls.size()]; + for (int i = 0; i < cdnUrls.size(); i++) prefixes[i] = cdnUrls.get(i).baseUrl + cdnUrls.get(i).cloudDir; + return prefixes; } - private static NativeOutcome runNativeAssembly(byte[] manifestBytes, int[] pendingIdx, int totalFiles, String installDirPath, String cachePath, AtomicBoolean cancel, Callback cb) { - NativeOutcome r = new NativeOutcome(); + /** + * Where the adaptive engine's shared-chunk cache goes and how big it may get, and what each + * volume must hold. A card install keeps the cache on internal scratch (each byte hits the + * card once); when internal storage cannot hold a useful cache, the cache goes beside the game + * instead of failing. {@code error} names the storage that is short, and by how much. + */ + static final class AdaptiveSpacePlan { + boolean cacheBesideGame; + long cacheBudget; + /** + * Most planned bytes of replaced files the engine may stage at once (each sits beside its + * old copy until its swap); -1 = nothing is replaced, no limit. + */ + long overlapBudget = -1; + long gameNeed; + long scratchNeed; + String error; + } + + /** The part of free space the shared cache may use, after {@code need}: half the headroom, capped. */ + static long adaptiveCacheBudget(long free, long need) { + if (free < 0) return ADAPTIVE_CACHE_DEFAULT_BYTES; + long headroom = free - need - ADAPTIVE_TRANSIENT_BYTES; + if (headroom <= 0) return 0; + return Math.min(ADAPTIVE_CACHE_MAX_BYTES, headroom / 2); + } + + /** + * The game volume's net growth at its worst point when files are swapped in order and each + * replaced file's old copy is released at its swap: the running Σ(new − old) after each file. + * What a staged file costs while it sits beside its old copy is the overlap budget's part. + */ + static long settledNetPeak(List<FileInfo> files, Map<String, Long> oldFileBytes) { + long running = 0, peak = 0; + for (FileInfo file : files) { + Long old = oldFileBytes.get(file.filename); + running += file.fileSize() - (old == null ? 0 : old); + peak = Math.max(peak, running); + } + return peak; + } + + /** The largest pending file whose destination has data (the engine's smallest overlap budget). */ + static long largestReplacedFile(List<FileInfo> files, Map<String, Long> oldFileBytes) { + long largest = 0; + for (FileInfo file : files) { + Long old = oldFileBytes.get(file.filename); + if (old != null && old > 0) largest = Math.max(largest, file.fileSize()); + } + return largest; + } + + /** + * The cache placement and budget as before; for an update, the replacement-overlap budget is + * what the game volume has left after the net growth and the cache, never below the largest + * replaced file (one at a time is the minimum that still progresses). The check is the + * lookahead planner's: the need must not exceed the free reading, no extra margin. + */ + static AdaptiveSpacePlan planAdaptiveSpace(long netPeak, long largestReplaced, long freeGame, boolean scratchRequested, long freeScratch) { + AdaptiveSpacePlan plan = new AdaptiveSpacePlan(); + long minimum = netPeak + largestReplaced; + long scratchBudget = scratchRequested ? adaptiveCacheBudget(freeScratch, 0) : 0; + plan.cacheBesideGame = !scratchRequested || (freeScratch >= 0 && scratchBudget < ADAPTIVE_CACHE_MIN_SCRATCH_BYTES); + long cacheOnGame; + if (plan.cacheBesideGame) { + plan.cacheBudget = adaptiveCacheBudget(freeGame, minimum); + cacheOnGame = plan.cacheBudget + ADAPTIVE_TRANSIENT_BYTES; + plan.scratchNeed = 0; + } else { + plan.cacheBudget = scratchBudget; + cacheOnGame = 0; + plan.scratchNeed = plan.cacheBudget + ADAPTIVE_TRANSIENT_BYTES; + } + long overlap = 0; + if (largestReplaced > 0) { + overlap = freeGame < 0 ? largestReplaced : Math.max(largestReplaced, freeGame - netPeak - cacheOnGame); + plan.overlapBudget = overlap; + } + plan.gameNeed = netPeak + overlap + cacheOnGame; + if (StoreInstallRoot.insufficientSpace(plan.gameNeed, freeGame)) { + plan.error = "Not enough free space on the game's storage: need " + fmt(plan.gameNeed) + + (plan.cacheBesideGame && scratchRequested ? " (download cache moved beside the game: internal storage is full)" : "") + + ", only " + fmt(freeGame) + " free (short by " + fmt(plan.gameNeed - freeGame) + ")"; + } else if (!plan.cacheBesideGame && StoreInstallRoot.insufficientSpace(plan.scratchNeed, freeScratch)) { + plan.error = "Not enough internal space for the download cache: need " + fmt(plan.scratchNeed) + ", only " + fmt(freeScratch) + + " free (short by " + fmt(plan.scratchNeed - freeScratch) + ")"; + } + return plan; + } + + /** + * The adaptive native install; the library is loaded. Hands back {@code FALLBACK} (nothing + * fetched or written) only when its space plan does not fit, so the caller can run the + * lookahead engine, which narrows its window to the free space. Any other failure is final. + */ + private static NativeAttempt installAdaptive(Manifest manifest, List<FileInfo> pending, long pendingBytes, Map<String, Long> oldFileBytes, + long planned, File installDir, String cachePath, File chunkCacheDir, File stagingDir, + NativeOptions options, AtomicBoolean cancel, Callback cb) throws Exception { + int[] pendingIdx = pendingIndices(manifest, pending); + if (manifest.rawBytes == null || pendingIdx == null) { + throw new InstallException("Native Epic install failed: the manifest is not available to the native engine"); + } + long netPeak = settledNetPeak(pending, oldFileBytes); + long largestReplaced = largestReplacedFile(pending, oldFileBytes); + long freeGame = StoreInstallRoot.availableBytes(installDir); + boolean scratchRequested = !cachePath.isEmpty(); + long freeScratch = scratchRequested ? StoreInstallRoot.availableBytes(chunkCacheDir) : -1; + AdaptiveSpacePlan space = planAdaptiveSpace(netPeak, largestReplaced, freeGame, scratchRequested, freeScratch); + if (space.error != null) { + cb.onLog("epic: adaptive space plan does not fit (" + space.error + "); mode=lookahead"); + return NativeAttempt.FALLBACK; + } + String engineCache = space.cacheBesideGame ? "" : cachePath; + File engineCacheDir = engineCache.isEmpty() ? new File(installDir, ".chunks") : chunkCacheDir; + if (space.cacheBesideGame && scratchRequested) { + cb.onLog("epic: internal storage cannot hold the shared-chunk cache (" + fmt(freeScratch) + " free); cache beside the game"); + } + if (cancel.get()) return NativeAttempt.done(null); + cb.onLog("epic: mode=adaptive tier=" + options.tierId + " workers=" + options.maxWorkers + " process_workers=" + options.processWorkers + + " cache=" + (engineCache.isEmpty() ? "beside the game" : "scratch") + " budget=" + fmt(space.cacheBudget) + + " overlap_budget=" + (space.overlapBudget < 0 ? "none" : fmt(space.overlapBudget)) + + " game need=" + fmt(space.gameNeed) + " of " + fmt(freeGame) + (space.cacheBesideGame ? "" : ", scratch need=" + fmt(space.scratchNeed) + " of " + fmt(freeScratch))); + cb.onSizes(pendingBytes, planned); + cb.onBytes(0, pendingBytes, 0); + final int totalFiles = pending.size(); + final File fInstallDir = installDir; + final Object lock = new Object(); + final long started = System.nanoTime(); + final long[] state = new long[] {0, 0, System.currentTimeMillis(), 0, 0}; // bytes, files, speed ms, speed bytes, speed bps + EpicNative.Result res = EpicNative.stream(manifest.rawBytes, installDir.getPath(), engineCache, stagingDir.getCanonicalPath(), cdnPrefixes(manifest.cdnUrls), pendingIdx, pendingBytes, + space.cacheBudget, space.overlapBudget, "", options.maxWorkers, options.processWorkers, cancel, new EpicNative.StreamListener() { + @Override public void onStreamProgress(long bytesWritten, long bytesTotal, int filesDone, int filesTotal, long filesBytes) { + synchronized (lock) { + if (bytesWritten < state[0] && filesDone <= state[1]) return; // a late, older sample + state[0] = Math.max(state[0], bytesWritten); + long nowMs = System.currentTimeMillis(); + if (nowMs - state[2] >= 500) { + state[4] = (state[0] - state[3]) * 1000L / Math.max(1, nowMs - state[2]); + state[2] = nowMs; + state[3] = state[0]; + } + cb.onBytes(state[0], pendingBytes, state[4]); + cb.onProgress("Downloading chunks (" + filesDone + "/" + totalFiles + " files)" + speedSuffix(state[4]), + (int) (state[0] * 80L / Math.max(1, pendingBytes))); + if (filesDone > state[1]) { + state[1] = filesDone; + cb.onStage("stream", filesBytes, pendingBytes, filesDone, totalFiles); + } + } + } + @Override public void onLog(String line) { cb.onLog(line); } + @Override public boolean onBeforeReplace() { + // Once a swap can mix versions, the old installed record is no longer truthful. + try { + StoreInstalls.markUpdating(fInstallDir); + return true; + } catch (Exception e) { + cb.onLog("epic: could not mark the install as updating: " + e.getMessage()); + return false; + } + } + @Override public void onComplete(boolean success, String error, long bytes) {} + }); + if (!res.started) throw new InstallException("Native Epic install failed: " + res.error); + long elapsedMs = Math.max(1, (System.nanoTime() - started) / 1_000_000); + if (res.cancelled) return NativeAttempt.done(null); + if (!res.success) throw new InstallException("Native Epic install failed: " + res.error); + deleteDir(engineCacheDir); + deleteDir(chunkCacheDir); + cb.onLog("epic: summary mode=adaptive files=" + totalFiles + " bytes_written=" + pendingBytes + " elapsed_ms=" + elapsedMs + + " avg_MBps=" + String.format(java.util.Locale.ROOT, "%.2f", pendingBytes * 1000.0 / elapsedMs / 1048576.0)); + cb.onStage("install", pendingBytes, pendingBytes, totalFiles, totalFiles); + cb.onProgress("Complete", 100); + return NativeAttempt.done(new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest)); + } + + /** + * The older native batch install, kept for A/B benchmarks: every chunk fetched into the cache + * in parallel, then the files assembled from it. It needs the whole cache and the files at + * once (about twice the game), and it writes files in place rather than staged, so an + * existing install is marked updating before anything is fetched. + */ + private static Result installBatch(Manifest manifest, List<FileInfo> pending, List<ChunkInfo> needed, long pendingBytes, + long planned, File installDir, String cachePath, File chunkCacheDir, NativeOptions options, + AtomicBoolean cancel, Callback cb) throws InstallException { + int[] pendingIdx = pendingIndices(manifest, pending); + if (manifest.rawBytes == null || pendingIdx == null) { + throw new InstallException("Native Epic install failed: the manifest is not available to the native engine"); + } + long cacheMissing = 0, totalBytes = 0; + for (ChunkInfo c : needed) { + if (!new File(chunkCacheDir, c.guidStr()).isFile()) cacheMissing += chunkWindowSize(c); + totalBytes += Math.max(c.fileSize, 1); + } + long freeGame = StoreInstallRoot.availableBytes(installDir); + if (cachePath.isEmpty()) { + long need = cacheMissing + pendingBytes; + if (StoreInstallRoot.insufficientSpace(need, freeGame)) throw new InstallException("Not enough free space for the batch Epic download: need " + fmt(need) + " (" + fmt(pendingBytes) + " of game files + " + fmt(cacheMissing) + " of download cache), only " + fmt(freeGame) + " free (short by " + fmt(need - freeGame) + ")"); + } else { + long freeCache = StoreInstallRoot.availableBytes(chunkCacheDir); + if (StoreInstallRoot.insufficientSpace(cacheMissing, freeCache)) throw new InstallException("Not enough internal space for the batch download cache: need " + fmt(cacheMissing) + ", only " + fmt(freeCache) + " free (short by " + fmt(cacheMissing - freeCache) + ")"); + if (StoreInstallRoot.insufficientSpace(pendingBytes, freeGame)) throw new InstallException("Not enough free space on the game's storage: need " + fmt(pendingBytes) + ", only " + fmt(freeGame) + " free (short by " + fmt(pendingBytes - freeGame) + ")"); + } + if (cancel.get()) return null; + final long fTotal = totalBytes; + final int totalChunks = needed.size(); + final int totalFiles = pending.size(); + cb.onLog("epic: mode=batch tier=" + options.tierId + " workers=" + options.maxWorkers + " process_workers=" + options.processWorkers + (cachePath.isEmpty() ? "" : " cache=scratch") + " cache_missing=" + fmt(cacheMissing)); + cb.onSizes(pendingBytes, planned); + final AtomicLong lastMs = new AtomicLong(System.currentTimeMillis()), lastBytes = new AtomicLong(), speed = new AtomicLong(); + final long started = System.nanoTime(); + EpicNative.Result fetch = EpicNative.run(manifest.rawBytes, installDir.getPath(), cachePath, cdnPrefixes(manifest.cdnUrls), pendingIdx, totalChunks, fTotal, "", + options.maxWorkers, options.processWorkers, cancel, new EpicNative.Listener() { + @Override public void onPlan(int chunksTotal, long bytesTotal, String chunkDir) { cb.onLog("epic: plan chunks=" + chunksTotal + " bytes=" + bytesTotal + " dir=" + chunkDir); } + @Override public void onProgress(long bytesDone, long bytesTotal, int chunksDone, int chunksTotal) { + sampleSpeed(bytesDone, lastMs, lastBytes, speed); + cb.onProgress("Downloading chunks (" + chunksDone + "/" + totalChunks + ")" + speedSuffix(speed.get()), (int) (bytesDone * 80L / Math.max(1, fTotal))); + cb.onBytes(bytesDone, fTotal, speed.get()); + } + @Override public void onLog(String line) { cb.onLog(line); } + @Override public void onComplete(boolean success, String error, long bytes) {} + }); + if (!fetch.started) throw new InstallException("Native Epic install failed: " + fetch.error); + if (fetch.cancelled) return null; + if (!fetch.success) throw new InstallException("Epic batch download failed: " + fetch.error); + // Assembly overwrites files in place: the old installed record stops being truthful now. + try { StoreInstalls.markUpdating(installDir); } catch (Exception e) { throw new InstallException("Could not mark the install as updating: " + e.getMessage()); } + EpicNative.Result asm = EpicNative.assemble(manifest.rawBytes, installDir.getPath(), cachePath, pendingIdx, cancel, new EpicNative.Listener() { + @Override public void onPlan(int chunksTotal, long bytesTotal, String chunkDir) {} + @Override public void onProgress(long bytesDone, long bytesTotal, int done, int total) { + cb.onStage("stream", bytesDone, bytesTotal, done, Math.max(total, totalFiles)); + cb.onProgress("Writing files (" + done + "/" + Math.max(total, totalFiles) + ")", 80 + (int) (done * 20L / Math.max(1, Math.max(total, totalFiles)))); + } + @Override public void onLog(String line) { cb.onLog(line); } + @Override public void onComplete(boolean success, String error, long bytes) {} + }); + if (asm.cancelled) return null; + if (!asm.started || !asm.success) throw new InstallException(asm.error == null || asm.error.isEmpty() ? "Epic batch assembly failed" : asm.error); + long elapsedMs = Math.max(1, (System.nanoTime() - started) / 1_000_000); + cb.onLog("epic: summary mode=batch files=" + totalFiles + " bytes_written=" + pendingBytes + " elapsed_ms=" + elapsedMs + + " avg_MBps=" + String.format(java.util.Locale.ROOT, "%.2f", pendingBytes * 1000.0 / elapsedMs / 1048576.0)); + deleteDir(chunkCacheDir); + cb.onStage("install", pendingBytes, pendingBytes, totalFiles, totalFiles); + cb.onProgress("Complete", 100); + return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest); + } + + private static void sampleSpeed(long done, AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps) { + long nowMs = System.currentTimeMillis(), prevMs = lastSpeedMs.get(); + if (nowMs - prevMs >= 500 && lastSpeedMs.compareAndSet(prevMs, nowMs)) { + long prevB = lastSpeedBytes.getAndSet(done); + long dt = nowMs - prevMs; + if (dt > 0) speedBps.set((done - prevB) * 1000L / dt); + } + } + + private static String speedSuffix(long bps) { + if (bps <= 0) return ""; + return " " + (bps >= 1048576 ? String.format(java.util.Locale.ROOT, "%.1f MB/s", bps / 1048576.0) : (bps / 1024) + " KB/s"); + } + + static File recoverOwnedStaging(File installDir, List<FileInfo> manifestFiles) throws IOException { + File staging = new File(installDir, STAGING_DIRECTORY); + Path expected = installDir.getCanonicalFile().toPath().resolve(STAGING_DIRECTORY); + if (!staging.getCanonicalFile().toPath().equals(expected) || + (Files.exists(staging.toPath(), LinkOption.NOFOLLOW_LINKS) && + !Files.isDirectory(staging.toPath(), LinkOption.NOFOLLOW_LINKS))) { + throw new IOException("Unsafe Epic staging namespace; refusing to traverse it"); + } + String namespace = expected.toString(); + for (FileInfo file : manifestFiles) { + String path = new File(installDir, file.filename.replace("\\", "/")).getCanonicalPath(); + if (path.equals(namespace) || path.startsWith(namespace + File.separator)) { + throw new IOException("Manifest conflicts with Epic staging namespace"); + } + } + File owner = new File(staging, "owner"); + if (Files.exists(staging.toPath(), LinkOption.NOFOLLOW_LINKS)) { + if (!Files.isRegularFile(owner.toPath(), LinkOption.NOFOLLOW_LINKS) || + !owner.getCanonicalFile().toPath().equals(expected.resolve("owner")) || + owner.length() != STAGING_OWNER.length() || + !STAGING_OWNER.equals(new String(Files.readAllBytes(owner.toPath()), StandardCharsets.UTF_8))) { + throw new IOException("Unowned Epic staging namespace; refusing to delete it"); + } + File[] files = staging.listFiles(); + if (files == null) throw new IOException("Could not inspect Epic staging"); + for (File file : files) { + if (file.getName().equals("owner")) continue; + if (!Files.isRegularFile(file.toPath(), LinkOption.NOFOLLOW_LINKS) || + !file.getCanonicalFile().toPath().equals(expected.resolve(file.getName())) || + !file.getName().endsWith(".stage")) throw new IOException("Unexpected Epic staging entry"); + } + for (File file : files) { + if (file.getName().equals("owner")) continue; + if (!file.delete()) throw new IOException("Could not recover orphan Epic staging"); + } + } else { + if (!staging.mkdir()) throw new IOException("Could not create Epic staging namespace"); + try (FileOutputStream out = new FileOutputStream(owner)) { + out.write(STAGING_OWNER.getBytes(StandardCharsets.UTF_8)); + out.getFD().sync(); + } + } + return staging; + } + + /** + * Remove the owned staging namespace when it holds nothing but its own marker. Same no-follow + * and canonical-path checks as {@link #recoverOwnedStaging}; anything unexpected is left alone. + */ + static void releaseOwnedStaging(File installDir) { try { - cb.onLog("epic: assembler=native files=" + totalFiles + (cachePath.isEmpty() ? "" : " cache=scratch")); - EpicNative.Result res = EpicNative.assemble(manifestBytes, installDirPath, cachePath, pendingIdx, cancel, new EpicNative.Listener() { - @Override public void onPlan(int chunksTotal, long bytesTotal, String chunkDir) {} - @Override public void onProgress(long bytesDone, long bytesTotal, int done, int total) { - cb.onStage("install", bytesDone, bytesTotal, done, Math.max(total, totalFiles)); - cb.onProgress("Writing files (" + done + "/" + Math.max(total, totalFiles) + ")", 80 + (int) (done * 20L / Math.max(1, Math.max(total, totalFiles)))); + File staging = new File(installDir, STAGING_DIRECTORY); + if (!Files.isDirectory(staging.toPath(), LinkOption.NOFOLLOW_LINKS)) return; + Path expected = installDir.getCanonicalFile().toPath().resolve(STAGING_DIRECTORY); + if (!staging.getCanonicalFile().toPath().equals(expected)) return; + String[] names = staging.list(); + if (names == null || names.length != 1 || !names[0].equals("owner")) return; + File owner = new File(staging, "owner"); + if (!Files.isRegularFile(owner.toPath(), LinkOption.NOFOLLOW_LINKS) + || owner.length() != STAGING_OWNER.length() + || !STAGING_OWNER.equals(new String(Files.readAllBytes(owner.toPath()), StandardCharsets.UTF_8))) return; + if (owner.delete()) staging.delete(); + } catch (IOException e) { + Log.w(TAG, "staging namespace kept: " + e.getMessage()); + } + } + + private static JSONObject nativeRequest(Manifest manifest, List<FileInfo> files, + Map<String, ChunkInfo> chunks, Map<String, Long> cached, Map<String, Long> old, + File install, File cache, File staging, boolean sameVolume) throws Exception { + JSONObject request = new JSONObject(); + request.put("schema_version", 1); + request.put("install_dir", install.getCanonicalPath()); + request.put("cache_dir", cache.getCanonicalPath()); + request.put("staging_dir", staging.getCanonicalPath()); + request.put("same_volume", sameVolume); + request.put("chunk_dir", manifest.chunkDir); + request.put("max_workers", 32); + JSONArray hosts = new JSONArray(); + for (CdnUrl cdn : manifest.cdnUrls) hosts.put(cdn.baseUrl + cdn.cloudDir); + request.put("hosts", hosts); + JSONArray chunkList = new JSONArray(); + for (ChunkInfo chunk : chunks.values()) { + JSONObject item = new JSONObject(); + item.put("guid", chunk.guidStr()); + item.put("path", chunk.getPath(manifest.chunkDir)); + item.put("size", chunkWindowSize(chunk)); + item.put("exact_size", chunk.windowSize > 0); + item.put("wire_size", Math.max(1, chunk.fileSize)); + item.put("sha1", hexDigest(chunk.sha1)); + item.put("cached", cached.containsKey(chunk.guidStr()) ? cached.get(chunk.guidStr()) : -1); + chunkList.put(item); + } + request.put("chunks", chunkList); + JSONArray fileList = new JSONArray(); + for (FileInfo file : files) { + JSONObject item = new JSONObject(); + item.put("path", file.filename.replace("\\", "/")); + item.put("sha1", hexDigest(file.sha1)); + item.put("old_size", old.containsKey(file.filename) ? old.get(file.filename) : 0); + JSONArray parts = new JSONArray(); + for (ChunkPart part : file.parts) { + parts.put(new JSONObject().put("guid", part.guidStr()).put("offset", part.offset & 0xFFFFFFFFL).put("size", part.size & 0xFFFFFFFFL)); + } + item.put("parts", parts); + fileList.put(item); + } + request.put("files", fileList); + return request; + } + + private static String hexDigest(byte[] digest) { + if (digest == null || digest.length != 20) return ""; + StringBuilder out = new StringBuilder(); + for (byte b : digest) out.append(String.format("%02x", b & 255)); + return out.toString(); + } + + private static File createStagingFile(File output, Set<String> reservedPaths, File staging) throws IOException { + File parent = output.getParentFile(); + if (!parent.isDirectory() && !parent.mkdirs()) throw new IOException("Could not create output directory"); + // Owned staging is on the output volume, never a manifest's literal "<name>.part". + while (true) { + File stage = File.createTempFile("file-", ".stage", staging); + if (!reservedPaths.contains(stage.getCanonicalPath())) return stage; + if (!stage.delete()) throw new IOException("Could not clear staging collision"); + } + } + + static long estimateStagedFilePeak(List<FileInfo> files, Map<String, Long> oldFileBytes) { + long delta = 0, peak = 0; + for (FileInfo file : files) { + delta += file.fileSize(); + peak = Math.max(peak, delta); + delta -= oldFileBytes.containsKey(file.filename) ? oldFileBytes.get(file.filename) : 0; + } + return peak; + } + + /** + * Discard abandoned/orphaned cache entries and retain only verified chunks this install uses. + * A bounded run consumes each chunk at its last part reference, so stale chunks from an older + * all-at-once run must not silently occupy the space needed by the game files. + */ + static Map<String, Long> prepareBoundedCache(File cacheDir, List<ChunkInfo> needed) throws InstallException { + Map<String, ChunkInfo> byGuid = new LinkedHashMap<>(); + for (ChunkInfo chunk : needed) byGuid.put(chunk.guidStr(), chunk); + Map<String, Long> valid = new LinkedHashMap<>(); + File[] entries = cacheDir.listFiles(); + if (entries == null) throw new InstallException("Could not inspect Epic chunk cache"); + for (File entry : entries) { + ChunkInfo chunk = byGuid.get(entry.getName()); + if (chunk == null || !isValidCachedChunk(chunk, entry)) { + deleteDir(entry); + if (entry.exists()) throw new InstallException("Could not clear stale Epic cache entry " + entry.getName()); + continue; + } + valid.put(entry.getName(), entry.length()); + } + trimCacheToBound(cacheDir, valid); + return valid; + } + + /** Evict oldest reusable chunks until retained cache data is under the fixed disk ceiling. */ + private static void trimCacheToBound(File cacheDir, Map<String, Long> cacheSizes) throws InstallException { + for (String guid : evictOldestReusableChunks(cacheSizes).keySet()) { + File chunk = new File(cacheDir, guid); + if (chunk.exists() && !chunk.delete() && chunk.exists()) { + throw new InstallException("Could not release Epic cache chunk " + guid); + } + } + } + + /** + * The writer and space planner use the same oldest-first retained-cache policy. Entries leave + * this map as soon as their last reference is consumed; an oversized incoming chunk is + * therefore allowed to coexist with the retained cache until its current part is written. + */ + private static Map<String, Long> evictOldestReusableChunks(Map<String, Long> cacheSizes) { + long retained = 0; + for (long bytes : cacheSizes.values()) retained += bytes; + Map<String, Long> evicted = new LinkedHashMap<>(); + java.util.Iterator<Map.Entry<String, Long>> iterator = cacheSizes.entrySet().iterator(); + while (retained > MAX_RETAINED_CHUNK_CACHE_BYTES && iterator.hasNext()) { + Map.Entry<String, Long> entry = iterator.next(); + retained -= entry.getValue(); + evicted.put(entry.getKey(), entry.getValue()); + iterator.remove(); + } + return evicted; + } + + private static boolean consumeChunkReference(Map<String, Integer> refs, String guid) { + Integer count = refs.get(guid); + if (count == null) return false; + if (count <= 1) { + refs.remove(guid); + return true; + } + refs.put(guid, count - 1); + return false; + } + + /** + * Pure disk schedule shared by execution and preflight. The next eight part references pin + * at most eight GUIDs (including across file boundaries); other reusable data is oldest-first + * capped at 64 MiB. Incoming downloads reserve their entire decoded size before submission. + */ + static final class PrefetchSchedule { + final List<String> parts = new ArrayList<>(); + final Map<String, Long> resident; + final Map<String, Integer> refs; + final Map<String, ChunkInfo> chunks; + int cursor; + + PrefetchSchedule(List<FileInfo> files, Map<String, ChunkInfo> chunks, Map<String, Long> cached) { + for (FileInfo file : files) for (ChunkPart part : file.parts) parts.add(part.guidStr()); + this.chunks = chunks; + resident = new LinkedHashMap<>(cached); + refs = chunkReferences(files); + } + + Set<String> window() { + return new LinkedHashSet<>(parts.subList(cursor, Math.min(parts.size(), cursor + PREFETCH_PARTS))); + } + + List<String> fill() { + List<String> fetch = new ArrayList<>(); + for (String guid : window()) if (!resident.containsKey(guid)) { + ChunkInfo chunk = chunks.get(guid); + if (chunk == null) throw new IllegalArgumentException("Missing chunk metadata " + guid); + resident.put(guid, chunkWindowSize(chunk)); + fetch.add(guid); + } + return fetch; + } + + List<String> consume() { + List<String> release = new ArrayList<>(); + String guid = parts.get(cursor++); + if (consumeChunkReference(refs, guid)) { + resident.remove(guid); + release.add(guid); + } + Set<String> pinned = window(); + long reusable = 0; + for (Map.Entry<String, Long> entry : resident.entrySet()) { + if (!pinned.contains(entry.getKey())) reusable += entry.getValue(); + } + Iterator<Map.Entry<String, Long>> it = resident.entrySet().iterator(); + while (reusable > MAX_RETAINED_CHUNK_CACHE_BYTES && it.hasNext()) { + Map.Entry<String, Long> entry = it.next(); + if (pinned.contains(entry.getKey())) continue; + reusable -= entry.getValue(); + release.add(entry.getKey()); + it.remove(); + } + return release; + } + + long bytes() { + long bytes = 0; + for (long size : resident.values()) bytes += size; + return bytes; + } + } + + private static final class ChunkPrefetch implements AutoCloseable { + final PrefetchSchedule schedule; + final File cacheDir; + final AtomicBoolean cancel; + final AtomicBoolean stopped = new AtomicBoolean(); + final ExecutorService workers = Executors.newFixedThreadPool(PREFETCH_PARTS); + final Map<String, Future<Boolean>> jobs = new LinkedHashMap<>(); + final Set<HttpURLConnection> connections = ConcurrentHashMap.newKeySet(); + final String chunkDir; + final List<CdnUrl> cdns; + + ChunkPrefetch(List<FileInfo> files, Map<String, ChunkInfo> chunks, Map<String, Long> cached, + File cacheDir, String chunkDir, List<CdnUrl> cdns, AtomicBoolean cancel) { + schedule = new PrefetchSchedule(files, chunks, cached); + this.cacheDir = cacheDir; + this.chunkDir = chunkDir; + this.cdns = cdns; + this.cancel = cancel; + } + + File await(String guid) throws Exception { + for (String fetch : schedule.fill()) { + jobs.put(fetch, workers.submit(() -> downloadChunkStreaming(schedule.chunks.get(fetch), + chunkDir, cdns, new File(cacheDir, fetch), cancel, stopped, connections))); + } + Future<Boolean> job = jobs.get(guid); + if (job != null) while (true) { + if (cancel.get()) throw new IOException("cancelled"); + try { + if (!job.get(100, TimeUnit.MILLISECONDS)) { + if (cancel.get()) throw new IOException("cancelled"); + throw new InstallException("Failed to download Epic chunk " + guid); + } + break; + } catch (TimeoutException ignored) { } + } + return new File(cacheDir, guid); + } + + void consume() throws InstallException { + for (String guid : schedule.consume()) { + // Only the writer advances the schedule, after closing its chunk reader. + jobs.remove(guid); + File chunk = new File(cacheDir, guid); + if (chunk.exists() && !chunk.delete() && chunk.exists()) { + throw new InstallException("Could not release Epic cache chunk " + guid); } - @Override public void onLog(String line) { cb.onLog(line); } - @Override public void onComplete(boolean success, String error, long bytes) {} - }); - r.started = res.started; r.success = res.success; r.cancelled = res.cancelled; r.error = res.error == null ? "" : res.error; - } catch (Throwable t) { - r.started = false; - r.error = t.getClass().getSimpleName() + ": " + t.getMessage(); + } + } + + @Override public void close() { + stopped.set(true); + for (HttpURLConnection connection : connections) connection.disconnect(); + workers.shutdownNow(); + boolean interrupted = false; + while (true) { + try { + if (workers.awaitTermination(100, TimeUnit.MILLISECONDS)) break; + for (HttpURLConnection connection : connections) connection.disconnect(); + } catch (InterruptedException e) { interrupted = true; } + } + if (interrupted) Thread.currentThread().interrupt(); + } + } + + static boolean isValidCachedChunk(ChunkInfo chunk, File file) { + if (!file.isFile()) return false; + if (chunk.windowSize > 0 && file.length() != chunk.windowSize) return false; + byte[] expected = chunk.sha1; + if (expected == null || expected.length != 20) return true; + boolean allZero = true; + for (byte b : expected) if (b != 0) { allZero = false; break; } + if (allZero) return true; + try (FileInputStream in = new FileInputStream(file)) { + MessageDigest sha = MessageDigest.getInstance("SHA-1"); + byte[] buffer = new byte[64 * 1024]; + int count; + while ((count = in.read(buffer)) != -1) sha.update(buffer, 0, count); + return MessageDigest.isEqual(sha.digest(), expected); + } catch (Exception e) { + return false; + } + } + + private static MessageDigest expectedDigest(byte[] expected) throws InstallException { + if (expected == null || expected.length != 20) return null; + for (byte b : expected) if (b != 0) { + try { return MessageDigest.getInstance("SHA-1"); } + catch (java.security.NoSuchAlgorithmException e) { throw new InstallException("SHA-1 verification unavailable"); } + } + return null; + } + + static Map<String, Integer> chunkReferences(List<FileInfo> files) { + Map<String, Integer> refs = new LinkedHashMap<>(); + for (FileInfo file : files) for (ChunkPart part : file.parts) { + String guid = part.guidStr(); + refs.put(guid, refs.containsKey(guid) ? refs.get(guid) + 1 : 1); + } + return refs; + } + + /** + * Simulates reserved parallel prefetch → append part → last-reference eviction order. The returned + * additional-space peak is measured against the current free-space reading: existing verified + * chunks are already reflected in that reading and count as reclaimed only when actually freed. + * Old output bytes are reclaimed after the complete new file is staged and verified, never + * before its parts are written. The same schedule is used for scratch-cache game-volume checks. + */ + static long estimatePeakAdditionalBytes(List<FileInfo> files, Map<String, ChunkInfo> chunksByGuid, + Map<String, Long> cachedChunkBytes, boolean includeFileWrites) { + return estimatePeakAdditionalBytes(files, chunksByGuid, cachedChunkBytes, includeFileWrites, new LinkedHashMap<>()); + } + + static long estimatePeakAdditionalBytes(List<FileInfo> files, Map<String, ChunkInfo> chunksByGuid, + Map<String, Long> cachedChunkBytes, boolean includeFileWrites, + Map<String, Long> oldFileBytes) { + PrefetchSchedule schedule = new PrefetchSchedule(files, chunksByGuid, cachedChunkBytes); + long initialCache = schedule.bytes(); + long fileDelta = 0, peak = 0; + for (FileInfo file : files) { + for (ChunkPart part : file.parts) { + schedule.fill(); + peak = Math.max(peak, fileDelta + schedule.bytes() - initialCache); + if (includeFileWrites) { + fileDelta += part.size & 0xFFFFFFFFL; + peak = Math.max(peak, fileDelta + schedule.bytes() - initialCache); + } + schedule.consume(); + } + // The verified staging file coexists with the old output until its atomic swap. + if (includeFileWrites) fileDelta -= oldFileBytes.containsKey(file.filename) ? oldFileBytes.get(file.filename) : 0; + } + return Math.max(0, peak); + } + + private static void copyChunkPart(File chunkFile, ChunkPart part, BufferedOutputStream out, + MessageDigest fileSha, AtomicBoolean cancel, String relPath) throws IOException { + long offset = part.offset & 0xFFFFFFFFL; + long remaining = part.size & 0xFFFFFFFFL; + long length = chunkFile.length(); + if (offset > length || remaining > length - offset) { + throw new IOException("Chunk " + part.guidStr() + " is too short for " + relPath); + } + try (FileInputStream in = new FileInputStream(chunkFile)) { + in.getChannel().position(offset); + byte[] buffer = new byte[64 * 1024]; + while (remaining > 0) { + if (cancel.get()) throw new IOException("cancelled"); + int count = in.read(buffer, 0, (int) Math.min(buffer.length, remaining)); + if (count < 0) throw new IOException("Unexpected end of chunk " + part.guidStr()); + out.write(buffer, 0, count); + if (fileSha != null) fileSha.update(buffer, 0, count); + remaining -= count; + } } - return r; } @@ -605,14 +1374,19 @@ public final class EpicDownloadManager { } } - private static boolean downloadChunkStreaming(ChunkInfo chunk, String chunkDir, List<CdnUrl> cdnUrls, File outFile) { + private static boolean downloadChunkStreaming(ChunkInfo chunk, String chunkDir, List<CdnUrl> cdnUrls, + File outFile, AtomicBoolean cancel, AtomicBoolean stopped, + Set<HttpURLConnection> connections) { String chunkPath = chunk.getPath(chunkDir); File tmp = new File(outFile.getPath() + ".part"); tmp.delete(); for (CdnUrl cdn : cdnUrls) { + if (cancel.get() || stopped.get()) return false; HttpURLConnection conn = null; try { conn = (HttpURLConnection) new URL(cdn.baseUrl + cdn.cloudDir + "/" + chunkPath).openConnection(); + connections.add(conn); + if (cancel.get() || stopped.get()) return false; conn.setConnectTimeout(30000); conn.setReadTimeout(60000); conn.setRequestProperty("User-Agent", UA); @@ -635,12 +1409,14 @@ public final class EpicDownloadManager { int storedAs = hdr.get() & 0xFF; if (headerSize > 41) skipFully(in, headerSize - 41); byte[] iobuf = new byte[131072]; + long outputBytes = 0; if ((storedAs & 1) != 0) { Inflater inflater = new Inflater(); byte[] obuf = new byte[131072]; int remaining = compressedSize; try { while (remaining > 0 && !inflater.finished()) { + if (cancel.get() || stopped.get()) break; if (inflater.needsInput()) { int n = in.read(iobuf, 0, Math.min(iobuf.length, remaining)); if (n <= 0) break; @@ -648,31 +1424,56 @@ public final class EpicDownloadManager { inflater.setInput(iobuf, 0, n); } int out = inflater.inflate(obuf); - if (out > 0) { fos.write(obuf, 0, out); if (sha != null) sha.update(obuf, 0, out); } + if (out > 0) { + outputBytes = checkedChunkWriteSize(chunk, outputBytes, out); + fos.write(obuf, 0, out); + if (sha != null) sha.update(obuf, 0, out); + } + if (out == 0 && !inflater.needsInput() && !inflater.finished()) { + throw new IOException("invalid compressed Epic chunk"); + } } int out; - while ((out = inflater.inflate(obuf)) > 0) { fos.write(obuf, 0, out); if (sha != null) sha.update(obuf, 0, out); } + while ((out = inflater.inflate(obuf)) > 0) { + if (cancel.get() || stopped.get()) break; + outputBytes = checkedChunkWriteSize(chunk, outputBytes, out); + fos.write(obuf, 0, out); + if (sha != null) sha.update(obuf, 0, out); + } + if (!cancel.get() && !stopped.get() && !inflater.finished()) throw new IOException("truncated compressed Epic chunk"); } finally { inflater.end(); } } else { int remaining = compressedSize; while (remaining > 0) { + if (cancel.get() || stopped.get()) break; int n = in.read(iobuf, 0, Math.min(iobuf.length, remaining)); if (n <= 0) break; + outputBytes = checkedChunkWriteSize(chunk, outputBytes, n); fos.write(iobuf, 0, n); if (sha != null) sha.update(iobuf, 0, n); remaining -= n; } + if (!cancel.get() && !stopped.get() && remaining != 0) throw new IOException("truncated Epic chunk"); } } + if (cancel.get() || stopped.get()) { tmp.delete(); conn.disconnect(); return false; } + if (chunk.windowSize > 0 && tmp.length() != chunk.windowSize) { tmp.delete(); conn.disconnect(); continue; } if (sha != null && !MessageDigest.isEqual(sha.digest(), chunk.sha1)) { tmp.delete(); conn.disconnect(); continue; } if (!tmp.renameTo(outFile)) { tmp.delete(); conn.disconnect(); continue; } conn.disconnect(); return true; } catch (Exception e) { + Log.w(TAG, "chunk " + chunk.guidStr() + " failed: " + e.getMessage()); tmp.delete(); if (conn != null) conn.disconnect(); + } finally { + if (conn != null) { + connections.remove(conn); + conn.disconnect(); + } + tmp.delete(); } } tmp.delete(); @@ -680,6 +1481,17 @@ public final class EpicDownloadManager { return false; } + private static long checkedChunkWriteSize(ChunkInfo chunk, long written, int count) throws IOException { + long next = written + count; + if (next > chunkWindowSize(chunk)) throw new IOException("chunk exceeds expected window size"); + return next; + } + + private static long chunkWindowSize(ChunkInfo chunk) { + // Legacy JSON manifests omit window sizes; their chunks use 1 MiB windows. + return chunk.windowSize > 0 ? chunk.windowSize : 1024L * 1024; + } + private static void readFully(InputStream in, byte[] buf) throws IOException { int off = 0; while (off < buf.length) { int n = in.read(buf, off, buf.length - off); if (n < 0) throw new IOException("stream ended"); off += n; } @@ -768,15 +1580,6 @@ public final class EpicDownloadManager { } - static byte[] readFile(File f) throws IOException { - try (FileInputStream fis = new FileInputStream(f)) { - byte[] data = new byte[(int) f.length()]; - int off = 0; - while (off < data.length) { int r = fis.read(data, off, data.length - off); if (r < 0) break; off += r; } - return data; - } - } - static void deleteDir(File dir) { if (!dir.exists()) return; File[] files = dir.listFiles(); @@ -784,20 +1587,6 @@ public final class EpicDownloadManager { dir.delete(); } - private static void sampleSpeed(long done, AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps) { - long nowMs = System.currentTimeMillis(), prevMs = lastSpeedMs.get(); - if (nowMs - prevMs >= 500 && lastSpeedMs.compareAndSet(prevMs, nowMs)) { - long prevB = lastSpeedBytes.getAndSet(done); - long dt = nowMs - prevMs; - if (dt > 0) speedBps.set((done - prevB) * 1000L / dt); - } - } - - private static String speedSuffix(long bps) { - if (bps <= 0) return ""; - return " " + (bps >= 1048576 ? String.format("%.1f MB/s", bps / 1048576.0) : (bps / 1024) + " KB/s"); - } - private static String fmt(long bytes) { if (bytes >= 1073741824L) return String.format("%.1f GB", bytes / 1073741824.0); if (bytes >= 1048576L) return String.format("%.1f MB", bytes / 1048576.0); diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt index 28510a8f1..41b6637c2 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt @@ -17,6 +17,30 @@ object EpicNative { fun onComplete(success: Boolean, error: String, bytes: Long) } + interface InstallListener : Listener { + /** Synchronous approval before the first verified atomic replacement. False aborts. */ + fun beforeSwap(): Boolean + } + + /** One native budgeted fetch/assembly run. A loaded-engine failure is never a Java retry. */ + @JvmStatic + fun installAdaptive(request: String, cancel: AtomicBoolean, listener: InstallListener): Result = + drive("nativeInstallAdaptive", cancel, listener, strict = true) { inner -> + nativeInstallAdaptive(request, inner) + } + + /** Callbacks from a [stream] run; every method is called on a native thread. */ + interface StreamListener { + /** File bytes written so far (staging included), and the files verified and swapped in so far. */ + fun onStreamProgress(bytesWritten: Long, bytesTotal: Long, filesDone: Int, filesTotal: Int, filesBytes: Long) + /** Engine log line (already in logcat under `EpicNative`). */ + fun onLog(line: String) + /** Once, before the first file replaces its destination; false (or a throw) fails the run with nothing replaced. */ + fun onBeforeReplace(): Boolean + /** Terminal; `bytes` = file bytes written. */ + fun onComplete(success: Boolean, error: String, bytes: Long) + } + class Result( @JvmField val started: Boolean, @JvmField val success: Boolean, @JvmField val cancelled: Boolean, @JvmField val error: String, @JvmField val bytes: Long, @JvmField val done: Int, @JvmField val total: Int, @@ -36,13 +60,69 @@ object EpicNative { fun assemble(manifest: ByteArray, installDir: String, chunkCacheDir: String, fileIdx: IntArray, cancel: AtomicBoolean?, listener: Listener): Result = drive("nativeAssemble", cancel, listener) { inner -> nativeAssemble(manifest, installDir, chunkCacheDir, fileIdx, inner) } - private fun drive(what: String, cancel: AtomicBoolean?, listener: Listener, start: (Listener) -> Long): Result { + /** + * The adaptive install of the files [pendingFileIdx] (indices into the manifest's file list), + * with at most [cacheBudget] bytes of shared chunks cached and at most [overlapBudget] planned + * bytes of replaced files staged at once (`-1` = no limit), staged in [stagingDir] (the + * manager's owned staging directory). `started == false` means nothing was fetched or written. + */ + @JvmStatic + fun stream( + manifest: ByteArray, installDir: String, chunkCacheDir: String, stagingDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedBytes: Long, + cacheBudget: Long, overlapBudget: Long, caBundlePath: String, maxWorkers: Int, processWorkers: Int, cancel: AtomicBoolean?, listener: StreamListener, + ): Result { + if (!StoresNative.ensureLoaded()) return Result(false, false, false, "engine not built", 0L, 0, 0) + val latch = CountDownLatch(1) + val completion = AtomicReference<Completion?>(null) + val doneRef = AtomicReference(0) + val totalRef = AtomicReference(pendingFileIdx.size) + val inner = object : StreamListener { + override fun onStreamProgress(bytesWritten: Long, bytesTotal: Long, filesDone: Int, filesTotal: Int, filesBytes: Long) { + doneRef.set(filesDone); totalRef.set(filesTotal) + listener.onStreamProgress(bytesWritten, bytesTotal, filesDone, filesTotal, filesBytes) + } + override fun onLog(line: String) = listener.onLog(line) + override fun onBeforeReplace(): Boolean = runCatching { listener.onBeforeReplace() }.getOrDefault(false) + override fun onComplete(success: Boolean, error: String, bytes: Long) { + completion.set(Completion(success, error, bytes)) + latch.countDown() + runCatching { listener.onComplete(success, error, bytes) } + } + } + val handle: Long = try { + nativeStream(manifest, installDir, chunkCacheDir, stagingDir, cdnPrefixes, pendingFileIdx, expectedBytes, cacheBudget, overlapBudget, caBundlePath, maxWorkers, processWorkers, inner) + } catch (t: Throwable) { + Log.w(TAG, "engine unavailable: ${t.javaClass.simpleName}: ${t.message}") + return Result(false, false, false, "nativeStream: ${t.javaClass.simpleName}: ${t.message}", 0L, 0, 0) + } + if (handle == 0L) return Result(false, false, false, completion.get()?.error ?: "not started", 0L, 0, totalRef.get()) + var cancelSent = false + try { + while (!latch.await(250, TimeUnit.MILLISECONDS)) { + if (!cancelSent && cancel != null && cancel.get()) { + nativeCancel(handle) + cancelSent = true + } + } + } finally { + if (completion.get() == null) nativeCancel(handle) + nativeRelease(handle) + } + val c = completion.get() + val cancelled = cancelSent || (c != null && !c.success && c.error == "cancelled") + val success = !cancelled && c != null && c.success + val error = when { cancelled -> "cancelled"; c == null -> "no completion"; else -> c.error } + return Result(true, success, cancelled, error, c?.bytes ?: 0L, doneRef.get(), totalRef.get()) + } + + private fun drive(what: String, cancel: AtomicBoolean?, listener: Listener, strict: Boolean = false, start: (Listener) -> Long): Result { if (!StoresNative.ensureLoaded()) return Result(false, false, false, "engine not built", 0L, 0, 0) val latch = CountDownLatch(1) val completion = AtomicReference<Completion?>(null) val doneRef = AtomicReference(0) val totalRef = AtomicReference(0) - val inner = object : Listener { + val inner = object : InstallListener { + override fun beforeSwap(): Boolean = (listener as? InstallListener)?.beforeSwap() ?: false override fun onPlan(chunksTotal: Int, bytesTotal: Long, chunkDir: String) { totalRef.set(chunksTotal); listener.onPlan(chunksTotal, bytesTotal, chunkDir) } override fun onProgress(bytesDone: Long, bytesTotal: Long, done: Int, total: Int) { doneRef.set(done); totalRef.set(total); listener.onProgress(bytesDone, bytesTotal, done, total) } override fun onLog(line: String) = listener.onLog(line) @@ -55,21 +135,26 @@ object EpicNative { val handle: Long = try { start(inner) } catch (t: Throwable) { - Log.w(TAG, "engine unavailable: ${t.javaClass.simpleName}: ${t.message}") - return Result(false, false, false, "$what: ${t.javaClass.simpleName}", 0L, 0, 0) + Log.w(TAG, "${if (strict) "engine start failed (no fallback)" else "engine unavailable"}: ${t.javaClass.simpleName}: ${t.message}") + return Result(strict, false, false, "$what: ${t.javaClass.simpleName}: ${t.message}", 0L, 0, 0) } - if (handle == 0L) return Result(false, false, false, completion.get()?.error ?: "not started", 0L, 0, totalRef.get()) + if (handle == 0L) return Result(strict, false, false, completion.get()?.error ?: "not started", 0L, 0, totalRef.get()) var cancelSent = false try { while (!latch.await(250, TimeUnit.MILLISECONDS)) { if (cancel != null && cancel.get()) { nativeCancel(handle) cancelSent = true - latch.await(5, TimeUnit.SECONDS) + if (strict) { + while (!latch.await(250, TimeUnit.MILLISECONDS)) { } + } else { + latch.await(5, TimeUnit.SECONDS) + } break } } } finally { + if (completion.get() == null) nativeCancel(handle) nativeRelease(handle) } val c = completion.get() @@ -78,6 +163,9 @@ object EpicNative { return Result(true, success, cancelSent, error, c?.bytes ?: 0L, doneRef.get(), totalRef.get()) } + @JvmStatic + private external fun nativeInstallAdaptive(request: String, listener: Listener): Long + @JvmStatic private external fun nativeStart( manifest: ByteArray, installDir: String, chunkCacheDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedChunks: Int, expectedBytes: Long, @@ -87,6 +175,12 @@ object EpicNative { @JvmStatic private external fun nativeAssemble(manifest: ByteArray, installDir: String, chunkCacheDir: String, fileIdx: IntArray, listener: Listener): Long + @JvmStatic + private external fun nativeStream( + manifest: ByteArray, installDir: String, chunkCacheDir: String, stagingDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedBytes: Long, + cacheBudget: Long, overlapBudget: Long, caBundlePath: String, maxWorkers: Int, processWorkers: Int, listener: StreamListener, + ): Long + @JvmStatic private external fun nativeCancel(handle: Long) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java index 0bbbaa238..b61af1e96 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java @@ -152,11 +152,12 @@ public final class GogDownloadManager { installPath.mkdirs(); long plannedBytes = 0; for (DepotFile df : files) plannedBytes += df.totalSize; - if (plannedBytes > 0) { - long usable = installPath.getUsableSpace(); - if (usable > 0 && plannedBytes > usable) { + long remainingBytes = remainingInstallBytes(files, installPath); + if (remainingBytes > 0) { + long usable = com.droiddeck.launcher.stores.StoreInstallRoot.availableBytes(installPath); + if (com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(remainingBytes, usable)) { out.hardStop = true; - out.error = "Not enough free space: need " + formatBytes(plannedBytes) + ", only " + formatBytes(usable) + " free"; + out.error = "Not enough free space: need " + formatBytes(remainingBytes) + ", only " + formatBytes(usable) + " free"; return out; } } @@ -946,7 +947,16 @@ public final class GogDownloadManager { for (File f : files) if (f.isDirectory()) collectExeRecursive(f, out); } - private static final class DepotFile { + static long remainingInstallBytes(List<DepotFile> files, File installPath) { + long remaining = 0; + for (DepotFile file : files) { + File output = new File(installPath, file.relativePath); + if (!fileVerified(output, file.totalSize, file.md5)) remaining += file.totalSize; + } + return remaining; + } + + static final class DepotFile { final String relativePath; final List<ChunkRef> chunks = new ArrayList<>(); String md5; diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt index 1a3480f95..125e4f108 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt @@ -51,7 +51,9 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro val id = key.substringAfter(':') val item = (StoresState.library[store].orEmpty() + StoresState.shelves[store]?.all.orEmpty()).firstOrNull { it.id == id } val installed = StoresState.installedGame(store, id) - val download = StoresState.download("${store.id}:$id")?.takeIf { it.isActive } + val rawDownload = StoresState.download("${store.id}:$id") + val download = rawDownload?.takeIf { it.isActive } + val failedDownload = rawDownload?.takeIf { it.state == DownloadState.FAILED } val title = item?.title ?: installed?.sidecar?.title ?: id Rise(0) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(bottom = 12.dp)) { @@ -73,12 +75,24 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro val state = when { installed != null -> stringResource(R.string.stores_eyebrow_installed) + " · " + com.droiddeck.launcher.stores.StoreInstallRoot.labelFor(ctx, installed.folder) download != null && download.location.isNotBlank() -> stringResource(R.string.stores_eyebrow_library) + " · " + download.location + failedDownload != null && failedDownload.location.isNotBlank() -> stringResource(R.string.stores_eyebrow_library) + " · " + failedDownload.location item?.owned == true -> stringResource(R.string.stores_eyebrow_library) else -> stringResource(R.string.stores_eyebrow_store) } val size = (item?.sizeBytes ?: 0L).takeIf { it > 0 }?.let { " · " + formatBytes(it) } ?: "" Text((state + size).uppercase(), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.2.sp, color = pal.signal) Text(title, fontSize = if (narrow) 24.sp else 30.sp, lineHeight = if (narrow) 27.sp else 33.sp, fontWeight = FontWeight.Black, color = colors.onBackground, maxLines = 2, overflow = TextOverflow.Ellipsis) + if (!failedDownload?.error.isNullOrBlank()) { + Text( + failedDownload!!.error!!, + fontSize = 13.sp, + lineHeight = 17.sp, + fontWeight = FontWeight.Medium, + color = colors.error, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } Actions { when { installed != null -> { @@ -95,6 +109,16 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro else PrimaryButton(stringResource(R.string.stores_dl_resume), main = true) { com.droiddeck.launcher.stores.download.DownloadQueue.resume(ctx, download.key) } ConfirmButton(stringResource(R.string.stores_dl_cancel), stringResource(R.string.stores_dl_cancel_confirm)) { com.droiddeck.launcher.stores.download.DownloadQueue.cancel(ctx, download.key) } } + failedDownload != null -> { + ActionChip(stringResource(R.string.stores_dl_failed), ok = false) + PrimaryButton(stringResource(R.string.stores_dl_resume), main = true) { + if (item != null) StoresState.requestInstall(ctx, item) + else com.droiddeck.launcher.stores.download.DownloadQueue.retry(ctx, failedDownload.key) + } + ConfirmButton(stringResource(R.string.stores_dl_clear), stringResource(R.string.stores_dl_cancel_confirm), compact = true) { + com.droiddeck.launcher.stores.download.DownloadQueue.clear(ctx, failedDownload.key) + } + } item?.owned == true && StoresState.isUnfinished(item) -> { InstallButton(stringResource(R.string.stores_resume_install)) { StoresState.requestInstall(ctx, item) } ConfirmButton(stringResource(R.string.stores_dl_clear), stringResource(R.string.stores_dl_cancel_confirm), compact = true) { StoresState.clearUnfinished(ctx, item) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt index d332ee0e5..d9f2d1417 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt @@ -305,6 +305,7 @@ private fun Modifier.timeRim(left: () -> Float, color: Color): Modifier = drawWi @Composable private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) { val ctx = LocalContext.current + val colors = MaterialTheme.colorScheme val pal = LocalPalette.current val narrow = LocalNarrowPane.current val ink = Color.White @@ -326,7 +327,11 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) val meta = when (d.state) { DownloadState.INSTALLED -> stringResource(R.string.stores_installed_chip) DownloadState.CANCELLED -> stringResource(R.string.stores_dl_cancelled) - DownloadState.FAILED -> if (d.bytesDone > 0) stringResource(R.string.stores_dl_failed_kept, formatBytes(d.bytesDone)) else stringResource(R.string.stores_dl_failed) + DownloadState.FAILED -> { + if (!d.error.isNullOrBlank()) stringResource(R.string.stores_dl_failed_reason, d.error) + else if (d.bytesDone > 0) stringResource(R.string.stores_dl_failed_kept, formatBytes(d.bytesDone)) + else stringResource(R.string.stores_dl_failed) + } DownloadState.PAUSED -> stringResource(R.string.stores_dl_paused) DownloadState.QUEUED -> if (d.queuePosition > 0) stringResource(R.string.stores_dl_queued_at, d.queuePosition) else stringResource(R.string.stores_dl_queued) DownloadState.RUNNING -> { @@ -338,7 +343,8 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) } } } - Text(meta, fontSize = 12.sp, color = dim, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false)) + val metaColor = if (d.state == DownloadState.FAILED) colors.error else dim + Text(meta, fontSize = 12.sp, color = metaColor, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false)) if (d.state == DownloadState.RUNNING && (d.stage == DownloadStage.DOWNLOAD || d.stage == DownloadStage.INSTALL) && d.speedBps > 0) { Text(formatSpeed(d.speedBps), fontSize = 12.sp, color = dim, maxLines = 1) if (d.etaSeconds >= 0) Text(eta(d.etaSeconds), fontSize = 12.sp, color = dim, maxLines = 1) @@ -355,7 +361,10 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) RoundAction(Icons.Filled.PlayArrow, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.resume(ctx, d.key) } CancelAction { DownloadQueue.cancel(ctx, d.key) } } - DownloadState.FAILED -> RoundAction(Icons.Filled.Refresh, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.retry(ctx, d.key) } + DownloadState.FAILED -> { + RoundAction(Icons.Filled.Refresh, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.retry(ctx, d.key) } + CancelAction { DownloadQueue.clear(ctx, d.key) } + } DownloadState.INSTALLED -> Box(Modifier.bloomIn(true, 420)) { RoundAction(Icons.Filled.PlayArrow, stringResource(R.string.stores_play), primary = true) { launchStoreGame(ctx, d.store, d.id, s, a) } } @@ -411,6 +420,7 @@ private fun StageBar(d: DownloadEntry) { val x = i * (segW + gap) drawRoundRect(colors.surfaceVariant, Offset(x, cy - h / 2f), Size(segW, h), CornerRadius(h / 2f)) val base = when { + d.state == DownloadState.FAILED -> colors.error done[i] -> pal.good actives[i] && paused -> pausedColor.copy(alpha = breath) actives[i] && stages[i] == DownloadStage.VERIFY -> verifyColor diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt index fb7f2d4ee..a8392465d 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt @@ -67,10 +67,21 @@ internal fun InstallWhereDialog(item: CatalogItem, onDismiss: () -> Unit) { Rise(1) { Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { targets.forEachIndexed { i, t -> + val needed = com.droiddeck.launcher.stores.StoreInstallRoot.estimatedBytesNeeded(item) + val freeBytes = t.freeBytes + val isLow = com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(needed, freeBytes) + val freeText = if (freeBytes < 0L) { + stringResource(R.string.stores_target_free_unknown) + } else if (isLow) { + stringResource(R.string.stores_storage_low, formatBytes(freeBytes), formatBytes(needed)) + } else { + stringResource(R.string.stores_target_free, formatBytes(freeBytes)) + } TargetRow( label = if (t.removable) t.label else stringResource(R.string.stores_target_internal), - free = stringResource(R.string.stores_target_free, formatBytes(t.freeBytes)), + free = freeText, removable = t.removable, focus = focus[i], + warning = isLow, ) { SessionPrefs.setStoresInstallTarget(ctx, t.root.absolutePath) StoresState.install(ctx, item, t.root) @@ -83,7 +94,7 @@ internal fun InstallWhereDialog(item: CatalogItem, onDismiss: () -> Unit) { } @Composable -private fun TargetRow(label: String, free: String, removable: Boolean, focus: FocusRequester, onPick: () -> Unit) { +private fun TargetRow(label: String, free: String, removable: Boolean, focus: FocusRequester, warning: Boolean = false, onPick: () -> Unit) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current val src = remember { MutableInteractionSource() } @@ -99,6 +110,6 @@ private fun TargetRow(label: String, free: String, removable: Boolean, focus: Fo ) { Icon(if (removable) Icons.Outlined.SdCard else Icons.Outlined.Smartphone, null, tint = if (hot) pal.signal else colors.onBackground, modifier = Modifier.size(22.dp)) Text(label, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f)) - Text(free, fontSize = 12.sp, color = colors.onSurfaceVariant, maxLines = 1) + Text(free, fontSize = 12.sp, color = if (warning) colors.error else colors.onSurfaceVariant, maxLines = 1) } } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt index a070e0158..c0471d560 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt @@ -438,10 +438,20 @@ private fun InstallWhere(item: CatalogItem) { StepTitle(title) StepList { targets.forEachIndexed { i, t -> + val needed = com.droiddeck.launcher.stores.StoreInstallRoot.estimatedBytesNeeded(item) + val freeBytes = t.freeBytes + val isLow = com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(needed, freeBytes) + val trailingText = if (freeBytes < 0L) { + stringResource(R.string.stores_target_free_unknown) + } else if (isLow) { + stringResource(R.string.stores_storage_low, formatBytes(freeBytes), formatBytes(needed)) + } else { + stringResource(R.string.stores_target_free, formatBytes(freeBytes)) + } StepChoice( label = if (t.removable) t.label else internalLabel, icon = if (t.removable) androidx.compose.material.icons.Icons.Outlined.SdCard else androidx.compose.material.icons.Icons.Outlined.Smartphone, - trailing = stringResource(R.string.stores_target_free, formatBytes(t.freeBytes)), + trailing = trailingText, enabled = open, modifier = Modifier.fillMaxWidth().stepItem(1 + i).then(if (i == defaultIndex) Modifier.focusRequester(first) else Modifier), ) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc33a651f..457ef53e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2084,6 +2084,9 @@ <string name="stores_install_title">Install %1$s:</string> <string name="stores_target_internal">Internal</string> <string name="stores_target_free">%1$s free</string> + <string name="stores_target_free_unknown">Free space unknown</string> + <string name="stores_storage_insufficient">Not enough free space: need %1$s, only %2$s free</string> + <string name="stores_storage_low">Insufficient space (%1$s free, need %2$s)</string> <string name="stores_notification_channel">Store downloads</string> <string name="stores_notification_channel_hint">Shows game download progress and keeps downloads running in the background</string> <string name="stores_notification_one">Downloading</string> diff --git a/app/src/main/rust/stores/JNI.md b/app/src/main/rust/stores/JNI.md index 9f1b38550..9cd314dfd 100644 --- a/app/src/main/rust/stores/JNI.md +++ b/app/src/main/rust/stores/JNI.md @@ -112,7 +112,45 @@ On-disk protocol (what a resumed run, by either engine, relies on): a file is as ## `com.droiddeck.launcher.stores.epic.EpicNative` -Two runs make up the native half of an Epic install: +The manager's default Epic install is the **stream** run (`nativeStream`, mode `adaptive`, +below): the chunks are fetched in parallel and written straight into staged files as they arrive; +on device it measured about 83 MB/s on a 71 GB install. The **lookahead** install +(`nativeInstallAdaptive`, mode `lookahead`) is its fallback when the stream engine's space plan +does not fit, and is selectable directly, as is the legacy batch pair (`nativeStart` + +`nativeAssemble`, mode `batch`), through the hidden `storesEpicMode` preference. + +### Lookahead install (snapshot schema v1) + +`nativeInstallAdaptive(request: String, listener: EpicNative.Listener): Long` +exports `Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeInstallAdaptive`. +Its exact Kotlin descriptor is +`(Ljava/lang/String;Lcom/droiddeck/launcher/stores/epic/EpicNative$Listener;)J`. +The listener must also implement `EpicNative.InstallListener.beforeSwap(): Boolean` (`()Z`). +Rust invokes it synchronously after staging/hash/sync and before the first atomic replacement; +false or a Java exception aborts without replacing that output. Java marks the sidecar updating +there. Later verified swaps preserve mixed-update/resume semantics. + +The UTF-8 JSON request contains: + +- `schema_version: 1`, absolute `install_dir`, `cache_dir`, owned `staging_dir`, + `same_volume`, signed `free_game`/`free_cache` (`-1` unknown), `max_workers`, CDN `hosts`. +- `chunks`: distinct `guid`, relative CDN `path`, decoded `size`, `exact_size`, `wire_size`, + SHA-1 hex `sha1` (empty/all-zero means absent), `cached` verified length (`-1` absent). +- `files`: pending manifest-order relative `path`, `sha1`, current `old_size`, ordered `parts` + containing `guid`, unsigned `offset` and `size`. + +Parse, owned-namespace validation and adaptive storage planning happen synchronously. Errors +return zero after `onComplete(false, error, 0)` and are **terminal**, not fallback signals. +`onPlan` reports the selected lookahead/retention/peaks; `onProgress` counts staged file bytes +and verified swapped file counts, not fetched wire bytes. Fetch and staged assembly overlap +in one native run, using the same pure reservation/release policy as the peak planner. +Cancellation/release joins native workers; no worker survives the terminal callback/handle +release. Only `StoresNative.ensureLoaded() == false` selects bounded parallel Java; the same rule +holds for `nativeStream`. + +### Legacy batch APIs (mode `batch`, for A/B runs) + +Two explicit calls provide the legacy batch interface: 1. **Fetch** (`nativeStart`): fills the chunk cache with verified, decompressed chunks for the files the manager says are pending. The Kotlin manager still fetches and parses the manifest @@ -224,6 +262,57 @@ Bannerlator's `BlEpicDownload.run(...)` wraps `nativeStart` in a blocking call t `onComplete`, then `nativeRelease` in `finally`. The same wrapper shape fits `nativeAssemble` (cancel is honoured between files; a multi-GB file finishes before the run stops). +### `nativeStream` — the adaptive install + +| Method | JNI signature | Symbol | +|---|---|---| +| `nativeStream(...)`: Long | `([BLjava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;[IJJJLjava/lang/String;IILcom/droiddeck/launcher/stores/epic/EpicNative$StreamListener;)J` | `Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeStream` | + +```kotlin +@JvmStatic private external fun nativeStream( + manifest: ByteArray, installDir: String, chunkCacheDir: String, stagingDir: String, + cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedBytes: Long, cacheBudget: Long, + overlapBudget: Long, caBundlePath: String, + maxWorkers: Int, processWorkers: Int, listener: StreamListener, +): Long +``` + +Parameters as `nativeStart`, plus `stagingDir` (the manager's owned +`<installDir>/.droiddeck-epic-staging`, which must exist; `""` = stage beside each file), and +except `expectedBytes` (the manager's `Σ fileSize` of the pending +files, `-1` = no cross-check; no chunk count) and `cacheBudget` (the most bytes of shared chunks +the cache may hold; the manager sizes it from free space, `0` = never cache, refetch instead) and +`overlapBudget` (the most planned bytes of *replaced* files - destination already holds data - +staged at once, since each sits beside its old copy until its swap; one is always allowed so the +run progresses; `-1` = no limit). Files are admitted in order and a chunk is dispatched only once +the file of its first use is admitted; fresh files are never held back. +`chunkCacheDir` is `""` (`<installDir>/.chunks`) or a scratch directory; the manager falls back to +`""` for a card install when internal storage cannot hold the budget. + +What the run does (`epic/stream.rs`): a chunk one file part uses is decompressed, verified and +written at the part's offset into a unique `stream-*.stage` file in `stagingDir`; a +chunk several parts use is also kept in the cache, decompressed, as `<GUID>` (a later copy after +an eviction as `<GUID>.<n>`) and deleted after its last covered part. The plan evicts by furthest +next use when the budget would be exceeded and fetches the evicted chunk again later. Each file, +once complete: synced, size and SHA-1 checked, `onBeforeReplace()` once for the whole run, then +renamed over its destination. Cancel or failure removes every unfinished stage file before +`onComplete`, leaves the destinations and the cache; success removes the cache directory. + +Planning runs on the calling thread and also prepares the cache (keeps verified chunks this +install needs, deletes everything else, as the Java writer does). A refusal returns `0` after a +synchronous `onComplete(false, reason, 0)`; nothing was fetched or written, and the manager fails +the install with that reason (it does not switch engines once the library is loaded). + +| `StreamListener` method | JNI signature | When | +|---|---|---| +| `onStreamProgress(bytesWritten: Long, bytesTotal: Long, filesDone: Int, filesTotal: Int, filesBytes: Long)` | `(JJIIJ)V` | after written parts (at most every 50 ms) and after every finished file; `filesBytes` = sizes of the files swapped in | +| `onLog(line: String)` | `(Ljava/lang/String;)V` | engine lines: `stream-plan …`, the fetch core's lines, `stream-summary mode=adaptive …` (peak cache, peak staging, refetches, direct vs cached bytes) | +| `onBeforeReplace(): Boolean` | `()Z` | once, before the first file replaces its destination; `false` or a throw fails the run with nothing replaced | +| `onComplete(success: Boolean, error: String, bytes: Long)` | `(ZLjava/lang/String;J)V` | terminal, exactly once; `bytes` = file bytes written; `error` = `"cancelled"` on cancel | + +Unlike the batch runs, the Kotlin wrapper (`EpicNative.stream`) waits for `onComplete` after a +cancel without a time limit: the stage-file cleanup must be finished before the manager returns. + ## `com.droiddeck.launcher.stores.amazon.AmazonNative` The file-fetch pool of an Amazon Games install: one streamed HTTP GET per manifest file, SHA-256 diff --git a/app/src/main/rust/stores/src/epic.rs b/app/src/main/rust/stores/src/epic.rs index 41ac19767..9e131391b 100644 --- a/app/src/main/rust/stores/src/epic.rs +++ b/app/src/main/rust/stores/src/epic.rs @@ -1,11 +1,13 @@ //! Derived from GameNative's Epic manager (itself derived from Legendary); both are GPL-3.0 and this crate carries the same licence (see Cargo.toml). pub mod assemble; +pub mod adaptive; pub mod chunk; pub mod driver; pub mod jni; pub mod manifest; pub mod plan; +pub mod stream; pub const USER_AGENT: &str = "UELauncher/11.0.1-14907503+++Portal+Release-Live Windows/10.0.19041.1.256.64bit"; diff --git a/app/src/main/rust/stores/src/epic/adaptive.rs b/app/src/main/rust/stores/src/epic/adaptive.rs new file mode 100644 index 000000000..7113723ea --- /dev/null +++ b/app/src/main/rust/stores/src/epic/adaptive.rs @@ -0,0 +1,1585 @@ +//! Budgeted native install: ordered lookahead, shared peak schedule, fetch-core overlap and +//! verified per-file atomic swaps. The JNI snapshot is the Java delta pass's exact pending set. + +use crate::fetch_core::{run_fetch, FetchItem, FetchOptions, FetchSink, SinkError}; +use serde_json::Value; +use sha1::{Digest, Sha1}; +use std::collections::{HashMap, HashSet}; +use std::fs::{self, OpenOptions}; +use std::io::{BufWriter, Write}; +use std::path::{Component, Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Condvar, Mutex}; +use std::time::Duration; + +const MIB: u64 = 1024 * 1024; +const RETAINED: u64 = 64 * MIB; + +#[derive(Clone, Debug)] +pub struct Chunk { + guid: String, + path: String, + size: u64, + wire: u64, + sha: Option<[u8; 20]>, + cached: Option<u64>, + exact: bool, +} +#[derive(Clone, Debug)] +pub struct Part { + chunk: usize, + offset: u64, + size: u64, +} +#[derive(Clone, Debug)] +pub struct Output { + path: String, + sha: Option<[u8; 20]>, + old: u64, + parts: Vec<Part>, +} +#[derive(Clone, Debug)] +pub struct Request { + pub install: PathBuf, + pub cache: PathBuf, + pub staging: PathBuf, + pub same_volume: bool, + pub free_game: Option<u64>, + pub free_cache: Option<u64>, + pub chunks: Vec<Chunk>, + pub files: Vec<Output>, + pub hosts: Vec<String>, + pub max_workers: usize, +} + +fn uint(v: &Value, key: &str) -> Result<u64, String> { + v[key].as_u64().ok_or_else(|| format!("invalid {key}")) +} +fn text(v: &Value, key: &str) -> Result<String, String> { + v[key] + .as_str() + .map(str::to_string) + .ok_or_else(|| format!("invalid {key}")) +} +fn digest(v: &Value) -> Result<Option<[u8; 20]>, String> { + let s = v.as_str().ok_or("invalid sha1")?; + if s.is_empty() { + return Ok(None); + } + if s.len() != 40 || !s.is_ascii() { + return Err("invalid sha1".into()); + } + let mut out = [0; 20]; + for (i, b) in out.iter_mut().enumerate() { + *b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).map_err(|_| "invalid sha1")?; + } + Ok(out.iter().any(|b| *b != 0).then_some(out)) +} +fn relative(s: &str) -> Result<(), String> { + if s.is_empty() + || s.contains('\\') + || s.contains(':') + || Path::new(s) + .components() + .any(|c| !matches!(c, Component::Normal(_))) + { + return Err(format!("unsafe manifest path {s}")); + } + Ok(()) +} +impl Request { + pub fn parse(json: &str) -> Result<Self, String> { + let v: Value = serde_json::from_str(json).map_err(|e| e.to_string())?; + if uint(&v, "schema_version")? != 1 { + return Err("unsupported adaptive request schema".into()); + } + let mut chunks = Vec::new(); + let mut by_guid = HashMap::new(); + for c in v["chunks"].as_array().ok_or("missing chunks")? { + let guid = text(c, "guid")?; + if guid.len() != 32 + || !guid.bytes().all(|b| b.is_ascii_hexdigit()) + || by_guid.contains_key(&guid) + { + return Err("invalid or duplicate chunk GUID".into()); + } + let path = text(c, "path")?; + relative(&path)?; + by_guid.insert(guid.clone(), chunks.len()); + chunks.push(Chunk { + guid, + path, + size: uint(c, "size")?, + wire: uint(c, "wire_size")?, + sha: digest(&c["sha1"])?, + cached: c["cached"].as_u64(), + exact: c["exact_size"].as_bool().ok_or("missing exact_size")?, + }); + } + let mut files = Vec::new(); + for f in v["files"].as_array().ok_or("missing files")? { + let path = text(f, "path")?; + relative(&path)?; + if path.split('/').next() == Some(".droiddeck-epic-staging") { + return Err("manifest conflicts with owned staging".into()); + } + let mut parts = Vec::new(); + for p in f["parts"].as_array().ok_or("missing parts")? { + let chunk = *by_guid + .get(&text(p, "guid")?) + .ok_or("missing chunk metadata")?; + let offset = uint(p, "offset")?; + let size = uint(p, "size")?; + if offset + .checked_add(size) + .filter(|n| *n <= chunks[chunk].size) + .is_none() + { + return Err("part exceeds decoded window".into()); + } + parts.push(Part { + chunk, + offset, + size, + }); + } + files.push(Output { + path, + parts, + sha: digest(&f["sha1"])?, + old: uint(f, "old_size")?, + }); + } + let install = PathBuf::from(text(&v, "install_dir")?); + let staging = PathBuf::from(text(&v, "staging_dir")?); + Self::validate_staging(&install, &staging)?; + let hosts = v["hosts"] + .as_array() + .ok_or("missing hosts")? + .iter() + .map(|h| { + h.as_str() + .map(str::to_string) + .ok_or_else(|| "invalid host".to_string()) + }) + .collect::<Result<Vec<_>, _>>()?; + if hosts.is_empty() { + return Err("no Epic CDN".into()); + } + let cache = PathBuf::from(text(&v, "cache_dir")?); + let declared_same = v["same_volume"].as_bool().ok_or("missing same_volume")?; + let same_volume = declared_same || Self::same_filesystem(&install, &cache); + let game_probe = v["free_game"].as_u64(); + let cache_probe = v["free_cache"].as_u64(); + let free_game = if same_volume { + match (game_probe, cache_probe) { + (Some(a), Some(b)) => Some(a.min(b)), + (a, b) => a.or(b), + } + } else { + game_probe + }; + Ok(Self { + install, + staging, + cache, + chunks, + files, + hosts, + same_volume, + free_game, + free_cache: cache_probe, + max_workers: uint(&v, "max_workers")?.clamp(1, 32) as usize, + }) + } + + fn validate_staging(install: &Path, staging: &Path) -> Result<(), String> { + let expected = fs::canonicalize(install) + .map_err(|e| e.to_string())? + .join(".droiddeck-epic-staging"); + let metadata = fs::symlink_metadata(staging).map_err(|e| e.to_string())?; + if !metadata.file_type().is_dir() + || fs::canonicalize(staging).map_err(|e| e.to_string())? != expected + { + return Err("unsafe staging directory; refusing to traverse it".into()); + } + let owner = staging.join("owner"); + let metadata = fs::symlink_metadata(&owner).map_err(|e| e.to_string())?; + if !metadata.file_type().is_file() + || metadata.len() != 26 + || fs::canonicalize(&owner).map_err(|e| e.to_string())? != expected.join("owner") + || fs::read(&owner).map_err(|e| e.to_string())? != b"DroidDeck Epic staging v1\n" + { + return Err("unowned staging directory".into()); + } + for entry in fs::read_dir(staging).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let name = entry.file_name(); + if name == "owner" { + continue; + } + if !entry.file_type().map_err(|e| e.to_string())?.is_file() + || !name.to_string_lossy().ends_with(".stage") + || fs::canonicalize(entry.path()).map_err(|e| e.to_string())? + != expected.join(&name) + { + return Err("unsafe staging entry".into()); + } + } + Ok(()) + } + + #[cfg(unix)] + fn same_filesystem(a: &Path, b: &Path) -> bool { + use std::os::unix::fs::MetadataExt; + match (fs::metadata(a), fs::metadata(b)) { + (Ok(a), Ok(b)) => a.dev() == b.dev(), + _ => false, + } + } + #[cfg(not(unix))] + fn same_filesystem(_a: &Path, _b: &Path) -> bool { + false + } + fn total(&self) -> u64 { + self.files + .iter() + .flat_map(|f| &f.parts) + .map(|p| p.size) + .sum() + } +} + +/// Resident includes full-size reservations for in-flight writes; only ready chunks can be read. +#[derive(Clone, Debug)] +struct Schedule { + order: Vec<usize>, + cursor: usize, + refs: Vec<usize>, + resident: Vec<Option<u64>>, + oldest: Vec<usize>, + lookahead: usize, + retained: u64, +} +impl Schedule { + fn new(req: &Request, lookahead: usize, retained: u64) -> Self { + let order: Vec<_> = req + .files + .iter() + .flat_map(|f| f.parts.iter().map(|p| p.chunk)) + .collect(); + let mut refs = vec![0; req.chunks.len()]; + for &c in &order { + refs[c] += 1; + } + let resident: Vec<_> = req.chunks.iter().map(|c| c.cached).collect(); + let oldest = (0..resident.len()) + .filter(|&c| resident[c].is_some()) + .collect(); + Self { + order, + cursor: 0, + refs, + resident, + oldest, + lookahead, + retained, + } + } + fn window(&self) -> HashSet<usize> { + self.order[self.cursor..self.order.len().min(self.cursor + self.lookahead)] + .iter() + .copied() + .collect() + } + fn fill(&mut self, req: &Request) -> Vec<usize> { + let mut fetch = Vec::new(); + for &c in &self.order[self.cursor..self.order.len().min(self.cursor + self.lookahead)] { + if self.resident[c].is_none() { + self.resident[c] = Some(req.chunks[c].size); + self.oldest.push(c); + fetch.push(c); + } + } + fetch + } + fn consume(&mut self) -> Vec<usize> { + let c = self.order[self.cursor]; + self.cursor += 1; + self.refs[c] -= 1; + let mut release = Vec::new(); + if self.refs[c] == 0 { + self.resident[c] = None; + release.push(c); + } + let pinned = self.window(); + let mut reusable: u64 = self + .oldest + .iter() + .filter(|c| !pinned.contains(c)) + .map(|&c| self.resident[c].unwrap_or(0)) + .sum(); + for &c in &self.oldest { + if reusable <= self.retained { + break; + } + if pinned.contains(&c) { + continue; + } + if let Some(bytes) = self.resident[c].take() { + reusable -= bytes; + release.push(c); + } + } + self.oldest.retain(|&c| self.resident[c].is_some()); + release + } + fn bytes(&self) -> u64 { + self.oldest + .iter() + .map(|&c| self.resident[c].unwrap_or(0)) + .sum() + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Budget { + pub lookahead: usize, + pub retained: u64, + pub game_peak: u64, + pub cache_peak: u64, +} +fn peaks(req: &Request, lookahead: usize, retained: u64) -> (u64, u64) { + let mut s = Schedule::new(req, lookahead, retained); + let initial = s.bytes() as i128; + let mut game = 0i128; + let mut game_peak = 0i128; + let mut cache_peak = 0i128; + for file in &req.files { + for part in &file.parts { + s.fill(req); + let cache = s.bytes() as i128 - initial; + cache_peak = cache_peak.max(cache); + game_peak = game_peak.max(game + if req.same_volume { cache } else { 0 }); + game += part.size as i128; + game_peak = game_peak.max(game + if req.same_volume { cache } else { 0 }); + s.consume(); + } + game -= file.old as i128; + } + (game_peak.max(0) as u64, cache_peak.max(0) as u64) +} +fn fits(needed: u64, free: Option<u64>) -> bool { + free.is_none_or(|n| needed <= n) +} +pub fn choose_budget(req: &Request) -> Result<Budget, String> { + // Unknown probes deliberately choose a conservative bounded mode, never "0 free". + let unknown = req.free_game.is_none() || (!req.same_volume && req.free_cache.is_none()); + for lookahead in [128, 64, 32, 16, 8, 4, 2, 1] { + if unknown && lookahead > 8 { + continue; + } + for retained in [RETAINED, 16 * MIB, 0] { + let (game_peak, cache_peak) = peaks(req, lookahead, retained); + if fits(game_peak, req.free_game) + && (req.same_volume || fits(cache_peak, req.free_cache)) + { + return Ok(Budget { + lookahead, + retained, + game_peak, + cache_peak, + }); + } + } + } + let (game, cache) = peaks(req, 1, 0); + Err(format!("Not enough free space for safe staged Epic replacement: minimum game additional={game}, cache additional={cache}; free game={:?}, cache={:?}", req.free_game, req.free_cache)) +} + +fn remaining_peaks( + req: &Request, + schedule: &Schedule, + fi: usize, + pi: usize, + disk_cache: u64, +) -> (u64, u64) { + let mut s = schedule.clone(); + let mut game = 0i128; + let mut game_peak = 0i128; + let mut cache_peak = 0i128; + let outstanding = s.bytes() as i128 - disk_cache as i128; + cache_peak = cache_peak.max(outstanding); + if req.same_volume { + game_peak = game_peak.max(outstanding); + } + for (index, file) in req.files.iter().enumerate().skip(fi) { + for part in file.parts.iter().skip(if index == fi { pi } else { 0 }) { + s.fill(req); + let cache = s.bytes() as i128 - disk_cache as i128; + cache_peak = cache_peak.max(cache); + game_peak = game_peak.max(game + if req.same_volume { cache } else { 0 }); + game += part.size as i128; + game_peak = game_peak.max(game + if req.same_volume { cache } else { 0 }); + s.consume(); + } + game -= file.old as i128; + } + (game_peak.max(0) as u64, cache_peak.max(0) as u64) +} + +#[cfg(unix)] +fn available(path: &Path) -> Option<u64> { + use std::os::unix::ffi::OsStrExt; + let path = std::ffi::CString::new(path.as_os_str().as_bytes()).ok()?; + let mut stat = std::mem::MaybeUninit::<libc::statvfs>::uninit(); + if unsafe { libc::statvfs(path.as_ptr(), stat.as_mut_ptr()) } != 0 { + return None; + } + let stat = unsafe { stat.assume_init() }; + Some((stat.f_bavail as u64).saturating_mul(stat.f_frsize as u64)) +} +#[cfg(not(unix))] +fn available(_path: &Path) -> Option<u64> { + None +} + +/// Re-probe at admission boundaries. Already dispatched reservations cannot be withdrawn. +/// Shrink only unsent lookahead/reusable budget; never wait forever for unrelated space use. +fn live_admit( + req: &Request, + state: &mut State, + fi: usize, + pi: usize, + stage_remaining: u64, +) -> Result<(), String> { + let disk_cache = state + .schedule + .oldest + .iter() + .map(|&index| { + if state.ready[index] { + return state.disk_sizes[index]; + } + if !state.writing[index] { + return 0; + } + let c = &req.chunks[index]; + fs::metadata(req.cache.join(&c.guid)) + .map(|m| m.len()) + .unwrap_or(0) + + fs::metadata(super::chunk::part_path(&req.cache.join(&c.guid))) + .map(|m| m.len()) + .unwrap_or(0) + }) + .sum(); + let free_game = available(&req.install); + let free_cache = available(&req.cache); + admit_with_probes( + req, + state, + fi, + pi, + stage_remaining, + disk_cache, + free_game, + free_cache, + ) +} + +fn admit_with_probes( + req: &Request, + state: &mut State, + fi: usize, + pi: usize, + stage_remaining: u64, + disk_cache: u64, + free_game: Option<u64>, + free_cache: Option<u64>, +) -> Result<(), String> { + loop { + let incoming: u64 = state + .schedule + .window() + .iter() + .filter(|&&c| state.schedule.resident[c].is_none()) + .map(|&c| req.chunks[c].size) + .sum(); + let cache_now = state + .schedule + .bytes() + .saturating_add(incoming) + .saturating_sub(disk_cache); + let game_now = stage_remaining.saturating_add(if req.same_volume { cache_now } else { 0 }); + if fits(game_now, free_game) && (req.same_volume || fits(cache_now, free_cache)) { + return Ok(()); + } + // Full remaining simulation is only needed when live admission headroom is exhausted. + let (game, cache) = remaining_peaks(req, &state.schedule, fi, pi, disk_cache); + if fits(game, free_game) && (req.same_volume || fits(cache, free_cache)) { + return Ok(()); + } + if state.schedule.oldest.iter().any(|&c| !state.ready[c]) { + return Err("Not enough free space for already reserved Epic downloads".into()); + } + if state.schedule.retained > 0 { + state.schedule.retained = 0; + continue; + } + if state.schedule.lookahead > 1 { + state.schedule.lookahead /= 2; + continue; + } + return Err(format!("Not enough free space during Epic install: need additional game={game} cache={cache}, free game={free_game:?} cache={free_cache:?}")); + } +} + +struct State { + schedule: Schedule, + ready: Vec<bool>, + writing: Vec<bool>, + disk_sizes: Vec<u64>, + pending: Vec<usize>, + error: Option<String>, + done: bool, +} +struct Shared { + state: Mutex<State>, + changed: Condvar, + stop: AtomicBool, +} +struct Sink<'a> { + req: &'a Request, + shared: &'a Shared, +} +impl FetchSink for Sink<'_> { + fn process(&self, item: &FetchItem, body: Vec<u8>) -> Result<u64, SinkError> { + if self.shared.stop.load(Ordering::Acquire) { + return Err(SinkError::Fatal("cancelled".into())); + } + let c = item.id as usize; + let chunk = &self.req.chunks[c]; + self.shared.state.lock().unwrap().writing[c] = true; + let written = super::chunk::write_verified_chunk_bounded( + &body, + chunk.sha.as_ref(), + &self.req.cache.join(&chunk.guid), + chunk.size, + chunk.exact, + ); + let mut state = self.shared.state.lock().unwrap(); + state.writing[c] = false; + let written = written.map_err(SinkError::Retry)?; + state.ready[c] = true; + state.disk_sizes[c] = written; + self.shared.changed.notify_all(); + Ok(chunk.wire) + } +} + +/// JNI owns this entire blocking run. All scoped workers join before completion is signalled. +pub fn run( + req: &Request, + budget: Budget, + cancel: &AtomicBool, + before_swap: &(dyn Fn() -> Result<(), String> + Sync), + progress: &(dyn Fn(u64, u64, u64, u64) + Sync), + log: &(dyn Fn(&str) + Sync), +) -> Result<u64, String> { + let shared = Shared { + state: Mutex::new(State { + schedule: Schedule::new(req, budget.lookahead, budget.retained), + ready: req.chunks.iter().map(|c| c.cached.is_some()).collect(), + writing: vec![false; req.chunks.len()], + disk_sizes: req.chunks.iter().map(|c| c.cached.unwrap_or(0)).collect(), + pending: Vec::new(), + error: None, + done: false, + }), + changed: Condvar::new(), + stop: AtomicBool::new(false), + }; + log(&format!( + "adaptive-native lookahead={} retained={} game_peak={} cache_peak={} workers={}", + budget.lookahead, + budget.retained, + budget.game_peak, + budget.cache_peak, + req.max_workers.min(budget.lookahead) + )); + let result = std::thread::scope(|scope| { + scope.spawn(|| { + crate::priority::background(); + let sink = Sink { + req, + shared: &shared, + }; + loop { + let batch = { + let mut state = shared.state.lock().unwrap(); + while state.pending.is_empty() + && !state.done + && !shared.stop.load(Ordering::Acquire) + { + state = shared + .changed + .wait_timeout(state, Duration::from_millis(100)) + .unwrap() + .0; + } + if state.done || shared.stop.load(Ordering::Acquire) { + break; + } + std::mem::take(&mut state.pending) + }; + let items = batch + .into_iter() + .map(|c| FetchItem { + id: c as u64, + urls: req + .hosts + .iter() + .map(|h| format!("{h}/{}", req.chunks[c].path)) + .collect(), + reserve: req.chunks[c].wire, + range: None, + }) + .collect(); + let opts = FetchOptions { + max_workers: req.max_workers.min(budget.lookahead), + per_host_cap: super::plan::per_host_cap( + req.max_workers.min(budget.lookahead), + req.hosts.len(), + ), + timeout: Duration::from_secs(60), + process_workers: 2, + headers: vec![("User-Agent".into(), super::USER_AGENT.into())], + label: "epic adaptive".into(), + ..Default::default() + }; + let fetched = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + run_fetch( + items, + &req.hosts, + &opts, + &sink, + &shared.stop, + &|_, _| {}, + log, + ) + })); + let outcome = match fetched { + Ok(outcome) => outcome, + Err(_) => { + let mut state = shared.state.lock().unwrap(); + state.error = Some("native fetch panicked".into()); + shared.stop.store(true, Ordering::Release); + shared.changed.notify_all(); + break; + } + }; + if !outcome.ok() { + let mut state = shared.state.lock().unwrap(); + state.error = Some(outcome.error.unwrap_or_else(|| "cancelled".into())); + shared.stop.store(true, Ordering::Release); + shared.changed.notify_all(); + break; + } + } + }); + // Cancel forwarding is independent of a writer blocked on disk I/O. + scope.spawn(|| { + while !shared.stop.load(Ordering::Acquire) { + if cancel.load(Ordering::Acquire) { + shared.stop.store(true, Ordering::Release); + shared.changed.notify_all(); + break; + } + std::thread::sleep(Duration::from_millis(50)); + } + }); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + assemble(req, &shared, cancel, before_swap, progress) + })) + .unwrap_or_else(|_| Err("native assembly panicked".into())); + shared.stop.store(true, Ordering::Release); + { + let mut state = shared.state.lock().unwrap(); + state.done = true; + shared.changed.notify_all(); + } + result + }); + // No .part writer survives this point; preserve verified resident chunks for resume. + let resident = shared.state.lock().unwrap().schedule.oldest.clone(); + for index in resident { + let _ = fs::remove_file(super::chunk::part_path( + &req.cache.join(&req.chunks[index].guid), + )); + } + if result.is_ok() { + fs::remove_dir_all(&req.cache).map_err(|e| format!("clear cache: {e}"))?; + } + result +} + +fn await_chunk( + req: &Request, + shared: &Shared, + cancel: &AtomicBool, + c: usize, + fi: usize, + pi: usize, + stage_remaining: u64, +) -> Result<(), String> { + let mut state = shared.state.lock().unwrap(); + if cancel.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + live_admit(req, &mut state, fi, pi, stage_remaining)?; + let fetch = state.schedule.fill(req); + state.pending.extend(fetch); + shared.changed.notify_all(); + while !state.ready[c] { + if cancel.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + if let Some(error) = &state.error { + return Err(error.clone()); + } + if shared.stop.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + state = shared + .changed + .wait_timeout(state, Duration::from_millis(100)) + .unwrap() + .0; + } + Ok(()) +} + +fn assemble( + req: &Request, + shared: &Shared, + cancel: &AtomicBool, + before_swap: &(dyn Fn() -> Result<(), String> + Sync), + progress: &(dyn Fn(u64, u64, u64, u64) + Sync), +) -> Result<u64, String> { + let mut done = 0; + let mut marked = false; + let total = req.total(); + for (fi, file) in req.files.iter().enumerate() { + Request::validate_staging(&req.install, &req.staging)?; + if cancel.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + let output = req.install.join(&file.path); + fs::create_dir_all(output.parent().ok_or("missing output parent")?) + .map_err(|e| e.to_string())?; + let stage = req + .staging + .join(format!("native-{}-{fi}.stage", std::process::id())); + let result = (|| { + let stage_file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&stage) + .map_err(|e| e.to_string())?; + let mut out = BufWriter::with_capacity(64 * 1024, stage_file); + let mut sha = Sha1::new(); + let mut written = 0; + let file_total: u64 = file.parts.iter().map(|p| p.size).sum(); + for (pi, part) in file.parts.iter().enumerate() { + // A live free-space probe must include already buffered staging bytes. + out.flush().map_err(|e| e.to_string())?; + await_chunk( + req, + shared, + cancel, + part.chunk, + fi, + pi, + file_total - written, + )?; + written += super::assemble::copy_chunk_part( + &req.cache.join(&req.chunks[part.chunk].guid), + part.offset, + part.size, + &mut out, + &mut sha, + cancel, + )?; + let mut state = shared.state.lock().unwrap(); + for c in state.schedule.consume() { + if !state.ready[c] { + return Err("attempt to release in-flight chunk".into()); + } + fs::remove_file(req.cache.join(&req.chunks[c].guid)) + .map_err(|e| e.to_string())?; + state.ready[c] = false; + } + done += part.size; + progress(done, total, fi as u64, req.files.len() as u64); + } + out.flush().map_err(|e| e.to_string())?; + out.get_ref().sync_all().map_err(|e| e.to_string())?; + drop(out); + if written != file_total { + return Err("file size mismatch".into()); + } + if file + .sha + .is_some_and(|expected| sha.finalize().as_slice() != expected) + { + return Err(format!("SHA-1 mismatch for {}", file.path)); + } + if cancel.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + if !marked { + before_swap()?; + marked = true; + } + if cancel.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + // Android/Linux rename replaces atomically on the same output volume. Never delete first. + fs::rename(&stage, &output) + .map_err(|e| format!("atomic replacement {}: {e}", file.path))?; + progress(done, total, fi as u64 + 1, req.files.len() as u64); + Ok(()) + })(); + let _ = fs::remove_file(&stage); + result?; + } + Ok(done) +} + +#[cfg(test)] +mod tests { + use super::super::chunk::test_support::{build_chunk_body, sha1_of}; + use super::*; + use std::io::Read; + use std::net::TcpListener; + use std::sync::atomic::{AtomicU64, AtomicUsize}; + use std::sync::{Arc, Barrier}; + use std::thread; + use std::time::{Instant, SystemTime, UNIX_EPOCH}; + + static SEQUENCE: AtomicU64 = AtomicU64::new(0); + struct Fixture { + root: PathBuf, + req: Request, + } + impl Fixture { + fn new(count: usize, size: usize) -> Self { + let root = PathBuf::from("target").join(format!( + "epic-adaptive-{}-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(), + SEQUENCE.fetch_add(1, Ordering::Relaxed) + )); + let install = root.join("game"); + let cache = root.join("cache"); + let staging = install.join(".droiddeck-epic-staging"); + fs::create_dir_all(&staging).unwrap(); + fs::create_dir_all(&cache).unwrap(); + fs::write(staging.join("owner"), b"DroidDeck Epic staging v1\n").unwrap(); + let data = vec![42; size]; + let sha = sha1_of(&data); + let chunks: Vec<_> = (0..count) + .map(|c| Chunk { + guid: format!("{c:032X}"), + path: format!("chunks/{c}"), + size: size as u64, + wire: size as u64 + 41, + sha: Some(sha), + cached: None, + exact: true, + }) + .collect(); + let files = (0..count) + .map(|c| Output { + path: format!("file-{c}.bin"), + sha: Some(sha), + old: 0, + parts: vec![Part { + chunk: c, + offset: 0, + size: size as u64, + }], + }) + .collect(); + Self { + root, + req: Request { + install, + cache, + staging, + chunks, + files, + same_volume: false, + free_game: Some(u64::MAX), + free_cache: Some(u64::MAX), + hosts: vec!["http://127.0.0.1:1".into()], + max_workers: 8, + }, + } + } + } + impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } + } + + struct Server { + address: String, + requests: Arc<AtomicUsize>, + active: Arc<AtomicUsize>, + stop: Arc<AtomicBool>, + worker: Option<thread::JoinHandle<()>>, + } + impl Server { + fn start(data: Vec<u8>, barrier_count: usize, stalled: bool) -> Self { + Self::start_mode(data, barrier_count, stalled.then_some(usize::MAX)) + } + + fn start_mode(data: Vec<u8>, barrier_count: usize, stall_chunk: Option<usize>) -> Self { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = format!("http://{}", listener.local_addr().unwrap()); + listener.set_nonblocking(true).unwrap(); + let requests = Arc::new(AtomicUsize::new(0)); + let active = Arc::new(AtomicUsize::new(0)); + let stop = Arc::new(AtomicBool::new(false)); + let requests2 = requests.clone(); + let active2 = active.clone(); + let stop2 = stop.clone(); + let worker = thread::spawn(move || { + let body = Arc::new(build_chunk_body(&data, false, 0, None)); + let barrier = Arc::new(Barrier::new(barrier_count.max(1))); + let mut handlers = Vec::new(); + while !stop2.load(Ordering::Acquire) { + let (mut socket, _) = match listener.accept() { + Ok(s) => s, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(5)); + continue; + } + Err(e) => panic!("{e}"), + }; + let body = body.clone(); + let requests = requests2.clone(); + let active = active2.clone(); + let stop = stop2.clone(); + let barrier = barrier.clone(); + handlers.push(thread::spawn(move || { + active.fetch_add(1, Ordering::Relaxed); + let _ = socket.set_read_timeout(Some(Duration::from_secs(5))); + let mut request = Vec::new(); + let mut byte = [0]; + while socket.read(&mut byte).unwrap_or(0) > 0 { + request.push(byte[0]); + if request.ends_with(b"\r\n\r\n") { + break; + } + } + let n = requests.fetch_add(1, Ordering::Relaxed); + if n < barrier_count { + barrier.wait(); + } + let _ = write!( + socket, + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + let stalled = stall_chunk.is_some_and(|c| { + c == usize::MAX + || String::from_utf8_lossy(&request) + .contains(&format!("/chunks/{c} ")) + }); + if stalled { + // Cancellation must close the native client's connection without data. + let _ = socket.set_read_timeout(Some(Duration::from_millis(100))); + loop { + if stop.load(Ordering::Acquire) { + break; + } + match socket.read(&mut byte) { + Ok(0) => break, + Ok(_) => {} + Err(e) + if e.kind() == std::io::ErrorKind::WouldBlock + || e.kind() == std::io::ErrorKind::TimedOut => {} + Err(_) => break, + } + } + } else { + let _ = socket.write_all(&body); + } + active.fetch_sub(1, Ordering::Relaxed); + })); + } + for handler in handlers { + handler.join().unwrap(); + } + }); + Self { + address, + requests, + active, + stop, + worker: Some(worker), + } + } + } + impl Drop for Server { + fn drop(&mut self) { + self.stop.store(true, Ordering::Release); + if let Some(worker) = self.worker.take() { + worker.join().unwrap(); + } + } + } + + #[test] + fn situation_table_selects_large_tight_minimal_and_rejects_no_fit() { + let mut f = Fixture::new(300, MIB as usize); + assert_eq!(choose_budget(&f.req).unwrap().lookahead, 128); + f.req.free_cache = Some(8 * MIB); + assert_eq!(choose_budget(&f.req).unwrap().lookahead, 8); + f.req.free_cache = Some(MIB); + assert_eq!(choose_budget(&f.req).unwrap().lookahead, 1); + f.req.free_cache = Some(MIB - 1); + assert!(choose_budget(&f.req) + .unwrap_err() + .contains("Not enough free space")); + f.req.free_cache = None; + assert_eq!(choose_budget(&f.req).unwrap().lookahead, 8); + f.req.free_game = Some(0); + assert!(choose_budget(&f.req).is_err()); + } + + #[test] + fn planner_counts_all_incoming_old_outputs_and_existing_cache_release() { + let mut f = Fixture::new(20, 100); + f.req.same_volume = true; + for file in &mut f.req.files { + file.old = 100; + } + assert_eq!(peaks(&f.req, 8, 0), (900, 800)); + for chunk in &mut f.req.chunks { + chunk.cached = Some(100); + } + assert_eq!(peaks(&f.req, 8, 0), (100, 0)); + f.req.same_volume = false; + assert_eq!(peaks(&f.req, 8, 0), (100, 0)); + } + + #[test] + fn shared_references_deduplicate_and_distant_chunks_refetch_under_budget() { + let mut f = Fixture::new(12, (40 * MIB) as usize); + f.req.files.extend(f.req.files.clone()); + let mut schedule = Schedule::new(&f.req, 8, RETAINED); + assert_eq!(schedule.fill(&f.req), (0..8).collect::<Vec<_>>()); + assert!(schedule.fill(&f.req).is_empty()); + assert!(schedule.consume().is_empty()); + assert_eq!(schedule.fill(&f.req), vec![8]); + assert_eq!(schedule.consume(), vec![0]); + let mut refetched = false; + for _ in 2..24 { + refetched |= schedule.fill(&f.req).contains(&0); + assert!(schedule.bytes() <= RETAINED + 8 * 40 * MIB); + let removed = schedule.consume(); + assert!(removed.iter().all(|c| !schedule.window().contains(c))); + } + assert!(refetched); + assert_eq!(schedule.bytes(), 0); + } + + #[test] + fn native_fetch_overlaps_across_files_and_never_fetches_outside_window() { + let mut f = Fixture::new(30, 4096); + let server = Server::start(vec![42; 4096], 8, false); + f.req.hosts = vec![server.address.clone()]; + f.req.free_cache = Some(8 * 4096); + let budget = choose_budget(&f.req).unwrap(); + assert_eq!(budget.lookahead, 8); + let cancel = AtomicBool::new(false); + let swapped = AtomicUsize::new(0); + let progress = |_, _, files, _| { + if files == 0 { + cancel.store(true, Ordering::Release); + } + }; + let result = run( + &f.req, + budget, + &cancel, + &|| { + swapped.fetch_add(1, Ordering::Relaxed); + Ok(()) + }, + &progress, + &|_| {}, + ); + assert!(result.is_err()); + assert_eq!(server.requests.load(Ordering::Relaxed), 8); + assert_eq!(swapped.load(Ordering::Relaxed), 0); + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + assert!(fs::read_dir(&f.req.cache).unwrap().all(|e| !e + .unwrap() + .path() + .to_string_lossy() + .ends_with(".part"))); + } + + #[test] + fn native_success_stages_verifies_swaps_and_reclaims_both_cache_layouts() { + for same_volume in [false, true] { + let mut f = Fixture::new(20, 4096); + let server = Server::start(vec![42; 4096], 8, false); + f.req.hosts = vec![server.address.clone()]; + f.req.same_volume = same_volume; + if same_volume { + f.req.cache = f.req.install.join(".chunks"); + fs::create_dir_all(&f.req.cache).unwrap(); + } + for file in &mut f.req.files { + fs::write(f.req.install.join(&file.path), b"old").unwrap(); + file.old = 3; + } + let swaps = AtomicUsize::new(0); + let before = || { + assert_eq!(fs::read(f.req.install.join("file-0.bin")).unwrap(), b"old"); + swaps.fetch_add(1, Ordering::Relaxed); + Ok(()) + }; + assert_eq!( + run( + &f.req, + choose_budget(&f.req).unwrap(), + &AtomicBool::new(false), + &before, + &|_, _, _, _| {}, + &|_| {} + ) + .unwrap(), + 20 * 4096 + ); + assert_eq!(swaps.load(Ordering::Relaxed), 1); + assert!(!f.req.cache.exists()); + for file in &f.req.files { + assert_eq!( + fs::read(f.req.install.join(&file.path)).unwrap(), + vec![42; 4096] + ); + } + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + } + } + + #[test] + fn native_file_hash_failure_and_updating_failure_keep_old_output() { + for updating_failure in [false, true] { + let mut f = Fixture::new(1, 4096); + let server = Server::start(vec![42; 4096], 1, false); + f.req.hosts = vec![server.address.clone()]; + fs::write(f.req.install.join("file-0.bin"), b"old").unwrap(); + if !updating_failure { + f.req.files[0].sha = Some([1; 20]); + } + let before = || Err("updating record failed".into()); + let result = run( + &f.req, + choose_budget(&f.req).unwrap(), + &AtomicBool::new(false), + &before, + &|_, _, _, _| {}, + &|_| {}, + ); + assert!(result.is_err()); + assert_eq!(fs::read(f.req.install.join("file-0.bin")).unwrap(), b"old"); + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + } + } + + #[test] + fn native_cancellation_closes_stalled_parallel_requests_and_joins_before_return() { + let mut f = Fixture::new(30, 4096); + let server = Server::start(vec![42; 4096], 8, true); + f.req.hosts = vec![server.address.clone()]; + f.req.free_cache = Some(8 * 4096); + let cancel = AtomicBool::new(false); + thread::scope(|scope| { + scope.spawn(|| { + let until = Instant::now() + Duration::from_secs(5); + while server.requests.load(Ordering::Relaxed) < 8 && Instant::now() < until { + thread::sleep(Duration::from_millis(10)); + } + cancel.store(true, Ordering::Release); + }); + let started = Instant::now(); + assert!(run( + &f.req, + choose_budget(&f.req).unwrap(), + &cancel, + &|| Ok(()), + &|_, _, _, _| {}, + &|_| {} + ) + .is_err()); + assert!(started.elapsed() < Duration::from_secs(7)); + }); + let until = Instant::now() + Duration::from_secs(2); + while server.active.load(Ordering::Relaxed) > 0 && Instant::now() < until { + thread::sleep(Duration::from_millis(10)); + } + assert_eq!(server.active.load(Ordering::Relaxed), 0); + assert_eq!(server.requests.load(Ordering::Relaxed), 8); + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + assert_eq!(fs::read_dir(&f.req.cache).unwrap().count(), 0); + } + + #[test] + fn native_bounded_decode_never_publishes_oversized_or_truncated_chunks() { + let f = Fixture::new(1, 10); + let path = f.req.cache.join("chunk"); + let body = build_chunk_body(&[42; 11], true, 0, None); + assert!( + super::super::chunk::write_verified_chunk_bounded(&body, None, &path, 10, true) + .is_err() + ); + assert!(!path.exists()); + assert!(!super::super::chunk::part_path(&path).exists()); + let body = build_chunk_body(&[42; 10], false, 0, Some(11)); + assert!( + super::super::chunk::write_verified_chunk_bounded(&body, None, &path, 10, true) + .is_err() + ); + let data = vec![42; 512 * 1024]; + let mut compressed = build_chunk_body(&data, true, 0, None); + assert_eq!( + super::super::chunk::write_verified_chunk_bounded( + &compressed, + Some(&sha1_of(&data)), + &path, + data.len() as u64, + true + ) + .unwrap(), + data.len() as u64 + ); + fs::remove_file(&path).unwrap(); + compressed.pop(); + // A matching shorter payload header leaves the missing zlib stream end to detect failure. + let short = (compressed.len() - 41) as i32; + compressed[12..16].copy_from_slice(&short.to_le_bytes()); + assert!(super::super::chunk::write_verified_chunk_bounded( + &compressed, + None, + &path, + data.len() as u64, + false + ) + .is_err()); + assert!(!path.exists()); + } + + #[test] + fn live_admission_shrinks_unsent_work_but_never_withdraws_inflight_reservations() { + let f = Fixture::new(30, 4096); + let mut state = State { + schedule: Schedule::new(&f.req, 128, RETAINED), + ready: vec![false; 30], + writing: vec![false; 30], + disk_sizes: vec![0; 30], + pending: vec![], + error: None, + done: false, + }; + admit_with_probes( + &f.req, + &mut state, + 0, + 0, + 4096, + 0, + Some(u64::MAX), + Some(8 * 4096), + ) + .unwrap(); + assert_eq!(state.schedule.lookahead, 8); + assert_eq!(state.schedule.retained, 0); + assert_eq!(state.schedule.fill(&f.req).len(), 8); + assert!(admit_with_probes( + &f.req, + &mut state, + 0, + 0, + 4096, + 0, + Some(u64::MAX), + Some(4096) + ) + .is_err()); + assert_eq!(state.schedule.lookahead, 8); + assert!(state + .schedule + .oldest + .iter() + .all(|&c| state.schedule.resident[c].is_some())); + } + + #[test] + fn snapshot_schema_rejects_unowned_staging_bad_paths_and_bad_version() { + let f = Fixture::new(1, 10); + let mut snapshot = serde_json::json!({ + "schema_version": 1, + "install_dir": f.req.install, "cache_dir": f.req.cache, "staging_dir": f.req.staging, + "same_volume": false, "free_game": -1, "free_cache": 0, "max_workers": 32, + "hosts": ["http://127.0.0.1"], + "chunks": [{ "guid": "00000000000000000000000000000000", "path": "chunks/00/chunk", + "size": 10, "wire_size": 51, "sha1": "", "cached": -1, "exact_size": true }], + "files": [{ "path": "legitimate.part", "old_size": 0, "sha1": "", + "parts": [{ "guid": "00000000000000000000000000000000", "offset": 0, "size": 10 }] }] + }); + let parsed = Request::parse(&snapshot.to_string()).unwrap(); + assert_eq!( + parsed.free_game, + if parsed.same_volume { Some(0) } else { None } + ); + assert_eq!(parsed.free_cache, Some(0)); + assert_eq!(parsed.chunks[0].cached, None); + assert!(choose_budget(&parsed).is_err()); + snapshot["schema_version"] = 2.into(); + assert!(Request::parse(&snapshot.to_string()).is_err()); + snapshot["schema_version"] = 1.into(); + snapshot["files"][0]["path"] = "../outside".into(); + assert!(Request::parse(&snapshot.to_string()).is_err()); + snapshot["files"][0]["path"] = ".droiddeck-epic-staging/owner".into(); + assert!(Request::parse(&snapshot.to_string()).is_err()); + snapshot["files"][0]["path"] = "legitimate.part".into(); + fs::remove_file(f.req.staging.join("owner")).unwrap(); + assert!(Request::parse(&snapshot.to_string()).is_err()); + } + + #[test] + fn native_shared_chunk_survives_cancelled_file_boundary_and_is_reused_on_resume() { + let mut f = Fixture::new(3, 4096); + let server = Server::start(vec![42; 4096], 1, false); + f.req.hosts = vec![server.address.clone()]; + for file in &mut f.req.files { + file.parts[0].chunk = 0; + } + let cancel = AtomicBool::new(false); + let before = AtomicUsize::new(0); + let progress = |_, _, files, _| { + if files == 1 { + cancel.store(true, Ordering::Release); + } + }; + assert!(run( + &f.req, + choose_budget(&f.req).unwrap(), + &cancel, + &|| { + before.fetch_add(1, Ordering::Relaxed); + Ok(()) + }, + &progress, + &|_| {} + ) + .is_err()); + assert_eq!(before.load(Ordering::Relaxed), 1); + assert_eq!(server.requests.load(Ordering::Relaxed), 1); + assert_eq!( + fs::read(f.req.install.join("file-0.bin")).unwrap(), + vec![42; 4096] + ); + assert!(!f.req.install.join("file-1.bin").exists()); + assert!(f.req.cache.join(&f.req.chunks[0].guid).is_file()); + f.req.files.remove(0); + f.req.chunks[0].cached = Some(4096); + assert_eq!( + run( + &f.req, + choose_budget(&f.req).unwrap(), + &AtomicBool::new(false), + &|| Ok(()), + &|_, _, _, _| {}, + &|_| {} + ) + .unwrap(), + 8192 + ); + assert_eq!(server.requests.load(Ordering::Relaxed), 1); + assert!(!f.req.cache.exists()); + } + + #[test] + fn native_later_hash_failure_preserves_verified_swap_and_unreplaced_old_file() { + let mut f = Fixture::new(2, 4096); + let server = Server::start(vec![42; 4096], 2, false); + f.req.hosts = vec![server.address.clone()]; + for file in &mut f.req.files { + fs::write(f.req.install.join(&file.path), b"old").unwrap(); + file.old = 3; + } + f.req.files[1].sha = Some([1; 20]); + let marked = AtomicUsize::new(0); + let error = run( + &f.req, + choose_budget(&f.req).unwrap(), + &AtomicBool::new(false), + &|| { + marked.fetch_add(1, Ordering::Relaxed); + Ok(()) + }, + &|_, _, _, _| {}, + &|_| {}, + ) + .unwrap_err(); + assert!(error.contains("SHA-1 mismatch")); + assert_eq!(marked.load(Ordering::Relaxed), 1); + assert_eq!( + fs::read(f.req.install.join("file-0.bin")).unwrap(), + vec![42; 4096] + ); + assert_eq!(fs::read(f.req.install.join("file-1.bin")).unwrap(), b"old"); + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + } + + #[test] + fn actual_native_file_and_cache_growth_never_exceeds_the_shared_planned_peak() { + let mut f = Fixture::new(20, 4096); + let server = Server::start(vec![42; 4096], 8, false); + f.req.hosts = vec![server.address.clone()]; + f.req.same_volume = true; + f.req.free_game = Some(21 * 4096); + for file in &mut f.req.files { + file.old = 4096; + fs::write(f.req.install.join(&file.path), vec![1; 4096]).unwrap(); + } + let budget = choose_budget(&f.req).unwrap(); + let observed = AtomicU64::new(0); + let progress = |_, _, _, _| { + let game: u64 = f + .req + .files + .iter() + .map(|file| fs::metadata(f.req.install.join(&file.path)).unwrap().len()) + .sum(); + let stage: u64 = fs::read_dir(&f.req.staging) + .unwrap() + .filter_map(Result::ok) + .filter(|e| e.file_name() != "owner") + .map(|e| e.metadata().map(|m| m.len()).unwrap_or(0)) + .sum(); + let cache: u64 = fs::read_dir(&f.req.cache) + .unwrap() + .filter_map(Result::ok) + .map(|e| e.metadata().map(|m| m.len()).unwrap_or(0)) + .sum(); + let additional = (game + stage + cache).saturating_sub(20 * 4096); + observed.fetch_max(additional, Ordering::Relaxed); + assert!( + additional <= budget.game_peak, + "{additional} > planned {}", + budget.game_peak + ); + }; + assert_eq!( + run( + &f.req, + budget, + &AtomicBool::new(false), + &|| Ok(()), + &progress, + &|_| {} + ) + .unwrap(), + 20 * 4096 + ); + assert!(observed.load(Ordering::Relaxed) > 8 * 4096); + } + + #[test] + fn native_assembly_swaps_a_verified_file_while_a_future_chunk_is_still_fetching() { + let mut f = Fixture::new(20, 4096); + let server = Server::start_mode(vec![42; 4096], 8, Some(7)); + f.req.hosts = vec![server.address.clone()]; + f.req.free_cache = Some(8 * 4096); + let cancel = AtomicBool::new(false); + let overlap = AtomicBool::new(false); + let progress = |_, _, files, _| { + if files == 1 { + assert!(server.active.load(Ordering::Relaxed) > 0); + assert_eq!( + fs::read(f.req.install.join("file-0.bin")).unwrap(), + vec![42; 4096] + ); + overlap.store(true, Ordering::Release); + cancel.store(true, Ordering::Release); + } + }; + assert!(run( + &f.req, + choose_budget(&f.req).unwrap(), + &cancel, + &|| Ok(()), + &progress, + &|_| {} + ) + .is_err()); + assert!(overlap.load(Ordering::Acquire)); + assert_eq!(server.requests.load(Ordering::Relaxed), 8); + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + } + + #[cfg(unix)] + #[test] + fn staging_validation_rejects_symlink_namespace_owner_and_entries() { + use std::os::unix::fs::symlink; + let f = Fixture::new(1, 4096); + let outside = f.root.join("outside"); + fs::create_dir(&outside).unwrap(); + fs::write(outside.join("owner"), b"DroidDeck Epic staging v1\n").unwrap(); + fs::write(outside.join("keep.stage"), b"keep").unwrap(); + fs::remove_dir_all(&f.req.staging).unwrap(); + symlink(fs::canonicalize(&outside).unwrap(), &f.req.staging).unwrap(); + assert!(Request::validate_staging(&f.req.install, &f.req.staging).is_err()); + assert!(run( + &f.req, + choose_budget(&f.req).unwrap(), + &AtomicBool::new(false), + &|| Ok(()), + &|_, _, _, _| {}, + &|_| {} + ) + .is_err()); + assert_eq!(fs::read(outside.join("keep.stage")).unwrap(), b"keep"); + fs::remove_file(&f.req.staging).unwrap(); + fs::create_dir(&f.req.staging).unwrap(); + symlink( + fs::canonicalize(outside.join("owner")).unwrap(), + f.req.staging.join("owner"), + ) + .unwrap(); + assert!(Request::validate_staging(&f.req.install, &f.req.staging).is_err()); + fs::remove_file(f.req.staging.join("owner")).unwrap(); + fs::write(f.req.staging.join("owner"), b"DroidDeck Epic staging v1\n").unwrap(); + symlink( + fs::canonicalize(outside.join("keep.stage")).unwrap(), + f.req.staging.join("linked.stage"), + ) + .unwrap(); + assert!(Request::validate_staging(&f.req.install, &f.req.staging).is_err()); + assert_eq!(fs::read(outside.join("keep.stage")).unwrap(), b"keep"); + fs::remove_file(f.req.staging.join("linked.stage")).unwrap(); + } +} diff --git a/app/src/main/rust/stores/src/epic/assemble.rs b/app/src/main/rust/stores/src/epic/assemble.rs index 6881640b8..8094d64fc 100644 --- a/app/src/main/rust/stores/src/epic/assemble.rs +++ b/app/src/main/rust/stores/src/epic/assemble.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::fs::{self, File}; -use std::io::{BufWriter, Write}; +use std::io::{BufWriter, Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -10,6 +10,33 @@ use sha1::{Digest, Sha1}; use super::manifest::{parse_manifest, FileInfo, Manifest}; use super::plan::resolve_cache_dir; +/// Streaming assembly primitive used by the budgeted installer; never holds a decoded chunk +/// in memory and closes the reader before the caller releases the cache reservation. +pub fn copy_chunk_part( + cache_path: &Path, offset: u64, size: u64, output: &mut impl Write, + sha: &mut Sha1, cancel: &AtomicBool, +) -> Result<u64, String> { + let mut input = File::open(cache_path).map_err(|e| format!("read chunk: {e}"))?; + let length = input.metadata().map_err(|e| e.to_string())?.len(); + if offset.checked_add(size).filter(|end| *end <= length).is_none() { + return Err("chunk part exceeds cached data".into()); + } + input.seek(SeekFrom::Start(offset)).map_err(|e| e.to_string())?; + let mut left = size; + let mut buffer = [0u8; 64 * 1024]; + while left > 0 { + if cancel.load(Ordering::Acquire) { return Err("cancelled".into()); } + let take = left.min(buffer.len() as u64) as usize; + let count = input.read(&mut buffer[..take]) + .map_err(|e| e.to_string())?; + if count == 0 { return Err("short cached chunk".into()); } + output.write_all(&buffer[..count]).map_err(|e| e.to_string())?; + sha.update(&buffer[..count]); + left -= count as u64; + } + Ok(size) +} + pub struct AssembleRequest { pub manifest_bytes: Vec<u8>, pub install_dir: String, diff --git a/app/src/main/rust/stores/src/epic/chunk.rs b/app/src/main/rust/stores/src/epic/chunk.rs index 3e0e96c49..e07c26c8c 100644 --- a/app/src/main/rust/stores/src/epic/chunk.rs +++ b/app/src/main/rust/stores/src/epic/chunk.rs @@ -71,10 +71,14 @@ struct HashingWriter<W: Write> { inner: W, sha: Option<Sha1>, written: u64, + limit: u64, } impl<W: Write> Write for HashingWriter<W> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + if self.written.saturating_add(buf.len() as u64) > self.limit { + return Err(io::Error::other("chunk exceeds reserved decoded window")); + } let n = self.inner.write(buf)?; if let Some(sha) = self.sha.as_mut() { sha.update(&buf[..n]); @@ -102,10 +106,21 @@ pub fn write_verified_chunk( let tmp = part_path(final_path); let _ = fs::remove_file(&tmp); - let result = write_part(body, expected_sha1, &tmp, final_path); + let result = write_part(body, expected_sha1, &tmp, final_path, u64::MAX, false); if result.is_err() { let _ = fs::remove_file(&tmp); } + + result +} + +pub fn write_verified_chunk_bounded( + body: &[u8], expected_sha1: Option<&[u8; 20]>, final_path: &Path, + decoded_limit: u64, exact_size: bool, +) -> Result<u64, String> { + let tmp = part_path(final_path); + let result = write_part(body, expected_sha1, &tmp, final_path, decoded_limit, exact_size); + if result.is_err() { let _ = fs::remove_file(tmp); } result } @@ -114,20 +129,43 @@ fn write_part( expected_sha1: Option<&[u8; 20]>, tmp: &Path, final_path: &Path, + limit: u64, + exact_size: bool, ) -> Result<u64, String> { let hdr = parse_chunk_header(body)?; let payload = chunk_payload(body, &hdr)?; + if limit != u64::MAX && (hdr.compressed_size < 0 || payload.len() != hdr.compressed_size as usize) { + return Err("truncated chunk body".into()); + } let file = File::create(tmp).map_err(|e| format!("open {}: {e}", tmp.display()))?; let mut out = HashingWriter { inner: BufWriter::with_capacity(128 * 1024, file), sha: expected_sha1.map(|_| Sha1::new()), written: 0, + limit, }; if hdr.is_compressed() { - let mut dec = flate2::read::ZlibDecoder::new(payload); - io::copy(&mut dec, &mut out).map_err(|e| format!("inflate: {e}"))?; + if limit == u64::MAX { + let mut dec = flate2::read::ZlibDecoder::new(payload); + io::copy(&mut dec, &mut out).map_err(|e| format!("inflate: {e}"))?; + } else { + let mut dec = flate2::Decompress::new(true); + let mut buffer = [0u8; 128 * 1024]; + loop { + let input = dec.total_in() as usize; + let before = dec.total_out(); + let status = dec.decompress(&payload[input..], &mut buffer, flate2::FlushDecompress::None) + .map_err(|e| format!("inflate: {e}"))?; + let count = (dec.total_out() - before) as usize; + out.write_all(&buffer[..count]).map_err(|e| format!("inflate write: {e}"))?; + if status == flate2::Status::StreamEnd { break; } + if count == 0 && dec.total_in() as usize == input { + return Err("truncated compressed Epic chunk".into()); + } + } + } } else { out.write_all(payload).map_err(|e| format!("write: {e}"))?; } @@ -135,6 +173,9 @@ fn write_part( let written = out.written; let digest = out.sha.take().map(|s| s.finalize()); drop(out); + if exact_size && written != limit { + return Err("chunk decoded size mismatch".into()); + } if let (Some(expected), Some(actual)) = (expected_sha1, digest) { if actual.as_slice() != &expected[..] { @@ -146,6 +187,80 @@ fn write_part( Ok(written) } +/// The window a chunk decompresses to: the manifest's, or 1 MiB for a legacy JSON manifest that +/// names none (`EpicDownloadManager.chunkWindowSize`). The streaming installer also plans its +/// shared-chunk cache with this size. +pub fn chunk_window_bytes(window_size: i32) -> u64 { + if window_size > 0 { + window_size as u64 + } else { + 1024 * 1024 + } +} + +/// One downloaded body → the verified, decompressed chunk in memory, for the streaming installer +/// (`stream.rs`), which writes most chunks straight into the files that use them and never puts +/// them in a cache. The checks are the bounded Java writer's (`downloadChunkStreaming`), which is +/// stricter than [`write_verified_chunk`]: a stored payload shorter than `compressedSize` is +/// truncated, a zlib stream must reach its end, the output may not grow past the chunk's window +/// and must fill it exactly when the manifest names one, and the SHA-1 must match when the +/// manifest has one. `Err` = "try another CDN", exactly like a cache write that failed. +pub fn decode_verified_chunk( + body: &[u8], + window_size: i32, + expected_sha1: Option<&[u8; 20]>, +) -> Result<Vec<u8>, String> { + let hdr = parse_chunk_header(body)?; + let start = hdr.payload_start(); + if start > body.len() { + return Err("Stream ended during skip".to_string()); + } + let want = if hdr.compressed_size <= 0 { + 0 + } else { + hdr.compressed_size as usize + }; + let avail = body.len() - start; + let cap = chunk_window_bytes(window_size) as usize; + let out = if hdr.is_compressed() { + // Java stops reading once the inflater finishes, so a body may carry less than + // `compressedSize` as long as the stream itself is complete. + let payload = &body[start..start + want.min(avail)]; + let mut out: Vec<u8> = Vec::with_capacity(cap + 1); + let mut inflater = flate2::Decompress::new(true); + let status = inflater + .decompress_vec(payload, &mut out, flate2::FlushDecompress::Finish) + .map_err(|e| format!("inflate: {e}"))?; + if out.len() > cap { + return Err("chunk exceeds expected window size".to_string()); + } + if status != flate2::Status::StreamEnd { + return Err("truncated compressed Epic chunk".to_string()); + } + out + } else { + if avail < want { + return Err("truncated Epic chunk".to_string()); + } + if want > cap { + return Err("chunk exceeds expected window size".to_string()); + } + body[start..start + want].to_vec() + }; + if window_size > 0 && out.len() != window_size as usize { + return Err(format!( + "chunk is {} bytes, window is {window_size}", + out.len() + )); + } + if let Some(expected) = expected_sha1 { + if Sha1::digest(&out).as_slice() != &expected[..] { + return Err("Chunk SHA-1 mismatch (streaming)".to_string()); + } + } + Ok(out) +} + #[cfg(test)] pub(crate) mod test_support { use super::*; @@ -326,6 +441,29 @@ mod tests { let _ = fs::remove_dir_all(&dir); } + #[test] + fn decode_in_memory_applies_the_bounded_writer_checks() { + let data = sample_data(); + let window = data.len() as i32; + let sha = sha1_of(&data); + let zipped = build_chunk_body(&data, true, 2, None); + assert_eq!(decode_verified_chunk(&zipped, window, Some(&sha)).unwrap(), data); + let stored = build_chunk_body(&data, false, 0, None); + assert_eq!(decode_verified_chunk(&stored, window, Some(&sha)).unwrap(), data); + // No window and no hash (legacy JSON manifest): accepted up to 1 MiB. + assert_eq!(decode_verified_chunk(&zipped, 0, None).unwrap(), data); + // Window mismatch, SHA-1 mismatch, truncation, oversize. + assert!(decode_verified_chunk(&zipped, window + 1, None).is_err()); + let err = decode_verified_chunk(&zipped, window, Some(&[7; 20])).unwrap_err(); + assert!(err.contains("SHA-1 mismatch"), "{err}"); + let cut = zipped.len() - 500; + assert!(decode_verified_chunk(&zipped[..cut], window, None).is_err()); + assert!(decode_verified_chunk(&stored[..stored.len() - 1], 0, None) + .unwrap_err() + .contains("truncated")); + assert!(decode_verified_chunk(&zipped, window - 1, None).is_err()); + } + #[test] fn stale_part_file_is_cleared_before_the_attempt() { let dir = temp_dir("stale"); diff --git a/app/src/main/rust/stores/src/epic/jni.rs b/app/src/main/rust/stores/src/epic/jni.rs index 1c3c2a01e..29edbe4b7 100644 --- a/app/src/main/rust/stores/src/epic/jni.rs +++ b/app/src/main/rust/stores/src/epic/jni.rs @@ -9,6 +9,7 @@ use jni::{JNIEnv, JavaVM}; use super::assemble::{build_assemble_plan, run_assemble, AssembleRequest}; use super::driver::{build_plan, run_plan, EpicRequest}; +use super::stream::{build_stream_plan, run_stream, Hooks, StreamProgress, StreamRequest}; #[cfg(target_os = "android")] #[link(name = "log")] @@ -39,6 +40,7 @@ fn android_log(message: &str) { pub struct EpicDownloadHandle { cancel: Arc<AtomicBool>, + worker: Option<thread::JoinHandle<()>>, } fn clear_pending_exception(env: &mut JNIEnv) { @@ -289,12 +291,11 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_native let cancel = Arc::new(AtomicBool::new(false)); let thread_cancel = Arc::clone(&cancel); - let handle = Box::new(EpicDownloadHandle { cancel }); - - thread::spawn(move || { + let worker = thread::spawn(move || { crate::priority::background(); run_on_thread(vm, listener, plan, req, thread_cancel); }); + let handle = Box::new(EpicDownloadHandle { cancel, worker: Some(worker) }); Box::into_raw(handle) as jlong } @@ -393,9 +394,7 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_native let cancel = Arc::new(AtomicBool::new(false)); let thread_cancel = Arc::clone(&cancel); - let handle = Box::new(EpicDownloadHandle { cancel }); - - thread::spawn(move || { + let worker = thread::spawn(move || { crate::priority::background(); let progress = |bytes_done: u64, bytes_total: u64, files_done: u64, files_total: u64| { let Ok(mut env) = vm.attach_current_thread_as_daemon() else { @@ -422,6 +421,194 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_native outcome.bytes_written, ); }); + let handle = Box::new(EpicDownloadHandle { cancel, worker: Some(worker) }); + + Box::into_raw(handle) as jlong +} + +fn call_on_stream_progress(env: &mut JNIEnv, listener: &JObject, p: &StreamProgress) { + if listener.is_null() { + return; + } + let _ = env.call_method( + listener, + "onStreamProgress", + "(JJIIJ)V", + &[ + JValue::Long(p.bytes_written as jlong), + JValue::Long(p.bytes_total as jlong), + JValue::Int(p.files_done.min(i32::MAX as u64) as jint), + JValue::Int(p.files_total.min(i32::MAX as u64) as jint), + JValue::Long(p.files_bytes as jlong), + ], + ); + clear_pending_exception(env); +} + +/// `boolean onBeforeReplace()`: the Java side marks an existing install `updating`. A thrown +/// exception or `false` is a refusal. +fn call_on_before_replace(env: &mut JNIEnv, listener: &JObject) -> Result<(), String> { + if listener.is_null() { + return Ok(()); + } + let result = env + .call_method(listener, "onBeforeReplace", "()Z", &[]) + .and_then(|v| v.z()); + let threw = env.exception_check().unwrap_or(false); + clear_pending_exception(env); + match result { + Ok(true) if !threw => Ok(()), + _ => Err("could not mark the install as updating".to_string()), + } +} + +/// `EpicNative.nativeStream(...)`: the adaptive install (see `stream.rs`). Parse, plan and cache +/// preparation run on the calling thread, so a refusal returns 0 (after a synchronous +/// `onComplete(false, reason, 0)`) before anything is fetched or written, and the manager runs +/// the bounded Java writer instead. The run itself is on a new thread; `onComplete` fires after +/// every unfinished staging file of a cancelled or failed run is gone. +#[no_mangle] +pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeStream( + mut env: JNIEnv, + _class: JClass, + manifest: JByteArray, + install_dir: JString, + chunk_cache_dir: JString, + staging_dir: JString, + cdn_prefixes: JObjectArray, + pending_file_idx: JIntArray, + expected_bytes: jlong, + cache_budget: jlong, + overlap_budget: jlong, + ca_bundle_path: JString, + max_workers: jint, + process_workers: jint, + listener: JObject, +) -> jlong { + if listener.is_null() { + android_log("nativeStream: null listener"); + return 0; + } + let Ok(manifest_bytes) = env.convert_byte_array(manifest) else { + clear_pending_exception(&mut env); + call_on_complete(&mut env, &listener, false, "manifest bytes unreadable", 0); + return 0; + }; + let install_dir = jstring_to_string(&mut env, &install_dir).unwrap_or_default(); + let chunk_cache_dir = jstring_to_string(&mut env, &chunk_cache_dir).unwrap_or_default(); + let staging_dir = jstring_to_string(&mut env, &staging_dir).unwrap_or_default(); + let cdn_prefixes = string_array_to_vec(&mut env, &cdn_prefixes); + let pending_file_indices: Vec<usize> = int_array_to_vec(&env, &pending_file_idx) + .into_iter() + .map(|i| i.max(0) as usize) + .collect(); + let ca_bundle_path = jstring_to_string(&mut env, &ca_bundle_path).unwrap_or_default(); + let max_workers = max_workers.max(1) as usize; + let process_workers = process_workers.max(1) as usize; + let label = { + let base = std::path::Path::new(&install_dir) + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default(); + format!("epic app={base}") + }; + log_both( + &mut env, + &listener, + &format!( + "engine=rust-stream label=\"{label}\" install_dir={install_dir} cache_dir={} cdns={} pending_files={} cache_budget={} workers={max_workers} process_workers={process_workers} manifest_bytes={}", + if chunk_cache_dir.is_empty() { "<installDir>/.chunks" } else { chunk_cache_dir.as_str() }, + cdn_prefixes.len(), + pending_file_indices.len(), + cache_budget.max(0), + manifest_bytes.len() + ), + ); + + let req = StreamRequest { + manifest_bytes, + install_dir, + chunk_cache_dir, + staging_dir, + cdn_prefixes, + pending_file_indices, + expected_bytes: if expected_bytes >= 0 { + Some(expected_bytes as u64) + } else { + None + }, + cache_budget: cache_budget.max(0) as u64, + // Negative = no limit (nothing the manager knows of is being replaced). + replace_overlap_budget: if overlap_budget < 0 { + u64::MAX + } else { + overlap_budget as u64 + }, + ca_bundle_path, + max_workers, + process_workers, + label, + }; + let plan = match build_stream_plan(&req) { + Ok(plan) => plan, + Err(err) => { + log_both(&mut env, &listener, &format!("not started: {err}")); + call_on_complete(&mut env, &listener, false, &err, 0); + return 0; + } + }; + let Ok(vm) = env.get_java_vm() else { + clear_pending_exception(&mut env); + call_on_complete(&mut env, &listener, false, "JavaVM unavailable", 0); + return 0; + }; + let Ok(listener) = env.new_global_ref(&listener) else { + clear_pending_exception(&mut env); + call_on_complete(&mut env, &listener, false, "listener ref failed", 0); + return 0; + }; + + let cancel = Arc::new(AtomicBool::new(false)); + let thread_cancel = Arc::clone(&cancel); + let worker = thread::spawn(move || { + crate::priority::background(); + let progress = |p: &StreamProgress| { + let Ok(mut env) = vm.attach_current_thread_as_daemon() else { + return; + }; + call_on_stream_progress(&mut env, listener.as_obj(), p); + }; + let before_replace = || { + let Ok(mut env) = vm.attach_current_thread_as_daemon() else { + return Err("JavaVM attach failed".to_string()); + }; + call_on_before_replace(&mut env, listener.as_obj()) + }; + let log = |line: &str| { + android_log(line); + let Ok(mut env) = vm.attach_current_thread_as_daemon() else { + return; + }; + call_on_log(&mut env, listener.as_obj(), line); + }; + let hooks = Hooks { + progress: &progress, + before_replace: &before_replace, + log: &log, + }; + let outcome = run_stream(&plan, &req, thread_cancel.as_ref(), &hooks); + let Ok(mut env) = vm.attach_current_thread_as_daemon() else { + return; + }; + call_on_complete( + &mut env, + listener.as_obj(), + outcome.success, + &outcome.error, + outcome.bytes_written, + ); + }); + let handle = Box::new(EpicDownloadHandle { cancel, worker: Some(worker) }); Box::into_raw(handle) as jlong } @@ -449,5 +636,73 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_native if handle == 0 { return; } - drop(unsafe { Box::from_raw(handle as *mut EpicDownloadHandle) }); + let mut handle = unsafe { Box::from_raw(handle as *mut EpicDownloadHandle) }; + if let Some(worker) = handle.worker.take() { let _ = worker.join(); } +} + +/// ABI v1 snapshot: selected chunks/files, verified cache lengths, old output lengths and both +/// measured free-space probes. Parse/budget failures are terminal, never fallback requests. +#[no_mangle] +pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeInstallAdaptive( + mut env: JNIEnv, _class: JClass, request: JString, listener: JObject, +) -> jlong { + if listener.is_null() { return 0; } + let parsed = jstring_to_string(&mut env, &request).ok_or("unreadable adaptive request".to_string()) + .and_then(|s| super::adaptive::Request::parse(&s)) + .and_then(|req| super::adaptive::choose_budget(&req).map(|budget| (req, budget))); + let (req, budget) = match parsed { + Ok(plan) => plan, + Err(error) => { + call_on_complete(&mut env, &listener, false, &error, 0); + return 0; + } + }; + call_on_plan(&mut env, &listener, req.chunks.len() as u64, 0, + &format!("lookahead={} retained={} game_peak={} cache_peak={}", + budget.lookahead, budget.retained, budget.game_peak, budget.cache_peak)); + let Ok(vm) = env.get_java_vm() else { + call_on_complete(&mut env, &listener, false, "JavaVM unavailable", 0); + return 0; + }; + let Ok(listener) = env.new_global_ref(&listener) else { + call_on_complete(&mut env, &listener, false, "listener ref failed", 0); + return 0; + }; + let cancel = Arc::new(AtomicBool::new(false)); + let thread_cancel = cancel.clone(); + let worker = thread::spawn(move || { + crate::priority::background(); + let progress = |done, total, files, files_total| { + if let Ok(mut env) = vm.attach_current_thread_as_daemon() { + call_on_progress(&mut env, listener.as_obj(), done, total, files, files_total); + } + }; + let before_swap = || -> Result<(), String> { + let mut env = vm.attach_current_thread_as_daemon().map_err(|e| e.to_string())?; + match env.call_method(listener.as_obj(), "beforeSwap", "()Z", &[]) { + Ok(value) => if value.z().unwrap_or(false) { Ok(()) } else { Err("Could not mark installed record updating before replacement".into()) }, + _ => { + clear_pending_exception(&mut env); + Err("Could not mark installed record updating before replacement".into()) + } + } + }; + let log = |line: &str| { + android_log(line); + if let Ok(mut env) = vm.attach_current_thread_as_daemon() { + call_on_log(&mut env, listener.as_obj(), line); + } + }; + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| + super::adaptive::run(&req, budget, &thread_cancel, &before_swap, &progress, &log))) + .unwrap_or_else(|_| Err("Native Epic install panicked".into())); + let (success, error, bytes) = match outcome { + Ok(bytes) => (true, String::new(), bytes), + Err(error) => (false, error, 0), + }; + if let Ok(mut env) = vm.attach_current_thread_as_daemon() { + call_on_complete(&mut env, listener.as_obj(), success, &error, bytes); + } + }); + Box::into_raw(Box::new(EpicDownloadHandle { cancel, worker: Some(worker) })) as jlong } diff --git a/app/src/main/rust/stores/src/epic/stream.rs b/app/src/main/rust/stores/src/epic/stream.rs new file mode 100644 index 000000000..5489801dd --- /dev/null +++ b/app/src/main/rust/stores/src/epic/stream.rs @@ -0,0 +1,1956 @@ +//! The adaptive Epic install: chunks fetched in parallel and written straight into the files. +//! +//! The batch engine ([`super::driver`] + [`super::assemble`]) fetched every chunk into a cache +//! first and assembled the files afterwards: fast, but the cache and the game existed side by side +//! (twice the game's size on disk) and every byte was written twice. The bounded Java writer fixed +//! the space by fetching one chunk at a time, at the cost of the parallel fetch. This module keeps +//! the fetch core's parallel, adaptive download (window ramp, per-host caps, the in-flight byte +//! budget, retries, host rotation, stall handoff — none of it changes) and keeps the bounded +//! writer's safety rules, by deciding up front what each downloaded chunk is for: +//! +//! - **A chunk exactly one file part uses** is decompressed, verified and written straight into +//! that file's staging file at the part's offset (a positioned write, so chunks may arrive in +//! any order). It never touches a cache. +//! - **A chunk several parts use** is written into every part whose file is already open, and the +//! decompressed copy is kept in the cache (`<cache>/<GUID>`, the same verified form and name the +//! batch engine and the Java writer use, so a resume on any engine reuses it) until its last +//! covered part is written; then it is deleted. Decompressed, not raw: a later part is then a +//! plain read and write with no second inflate or hash, and the file is exactly what a resume +//! already knows how to check. +//! - The cache has a **byte budget** (from the Java planner, sized from free space). The plan +//! simulates the whole install in file order and, when the budget would be exceeded, evicts the +//! cached chunk whose next use is furthest away (Belady's rule, the fewest refetches for that +//! budget). An evicted chunk is downloaded again for its later uses, as its own fetch item. +//! +//! **Files open in order.** The fetch items are queued in the order of their first use, so the +//! fetch core works through the game front to back. A file is opened (its staging file created) +//! only once the download has reached it: every file before the first chunk still to arrive, plus +//! the file of every chunk that has arrived. That keeps the set of half-written files to what the +//! fetch window spans, and it is also why the cache never holds more than the plan allowed: a +//! cached chunk is written into every newly reached file at once, so what stays in the cache is +//! exactly what the simulation kept at that point of the file order. +//! +//! **Updates are paced by a replacement-overlap budget.** A staged file that replaces an existing +//! one sits beside the old copy until its swap, so every replaced file open at once costs its full +//! new size in extra space (on a fresh install a staged file simply becomes the file). Files are +//! admitted in order; a replaced file is admitted only while the planned sizes of admitted, +//! not-yet-swapped replaced files stay within the budget the manager computed from free space, +//! and one is always admitted when none is open, so the install always progresses. The fetch core +//! dispatches a chunk only once the file of its first use is admitted ([`run_fetch_gated`]). +//! Admission never changes the order files are reached in, only when, so the cache simulation +//! below (which works in file order) stays exact: the cache holds at any moment what the plan holds +//! at that point of the order. Nothing waits on a gated file: every chunk an admitted file needs is +//! first used at or before that file, so it was dispatched before the gate closed. +//! +//! **A slow disk throttles the download by itself.** Every write happens in the fetch core's +//! process pool, inside [`FetchSink::process`]; the core frees a body's share of its in-flight +//! byte budget only when `process` returns, so while the disk is behind, the budget fills and no +//! new request goes out. Memory is that budget plus one decompressed chunk per pool thread. +//! +//! **Each file is replaced safely**, with the bounded Java writer's rules: written into a unique +//! `.stage` file in the manager's owned staging directory (`<installDir>/.droiddeck-epic-staging`, +//! whose leftovers the manager recovers before the next run; a sibling `.droiddeck-epic-*.stage` +//! when no directory is given), never a manifest path, so a game file legitimately named `x.part` +//! is safe. Its SHA-1 computed as the bytes go out (parts written out of order are +//! read back once the bytes before them are in, while they are still in the page cache), then +//! synced, its size and hash checked, the install marked `updating` once (the Java callback, +//! before the first replacement), and the stage renamed over the destination. On cancel or failure +//! every unfinished stage is removed and the old files are untouched; verified files that were +//! already swapped stay, and the next run's delta pass skips them. The cache is left as it stands +//! for that run, and removed when the install completes. + +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::fs::{self, File, OpenOptions}; +use std::io::ErrorKind; +use std::os::unix::fs::FileExt; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; +use std::sync::Mutex; +use std::time::Instant; + +use sha1::{Digest, Sha1}; + +use crate::fetch_core::{run_fetch_gated, FetchItem, FetchOptions, FetchSink, SinkError}; + +use super::assemble::relative_path; +use super::chunk::{chunk_window_bytes, decode_verified_chunk, part_path}; +use super::driver::REQUEST_TIMEOUT; +use super::manifest::{parse_manifest, ChunkInfo, Manifest}; +use super::plan::{chunk_url, distinct_prefixes, per_host_cap, resolve_cache_dir}; + +/// "No further use" in the schedule's next-use table. +const NONE: usize = usize::MAX; + +/// Progress callbacks are rate-limited to one per this many milliseconds (a finished file always +/// reports): a large game has hundreds of thousands of parts, each a JNI call otherwise. +const PROGRESS_EVERY_MS: u64 = 50; + +// ───────────────────────────────────────────────────────────────────────────────────────────── +// The schedule: which download serves which file part +// ───────────────────────────────────────────────────────────────────────────────────────────── + +/// One download of a chunk (or one cached copy left by an earlier run) and the file parts it +/// serves. Positions index the install's part sequence: every pending file's parts, file by file. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ScheduledEvent { + /// Index into `manifest.unique_chunks`. + pub chunk: usize, + /// The part positions this copy serves, in order. + pub uses: Vec<usize>, + /// Already in the cache when the run started; not fetched. + pub preloaded: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Schedule { + pub events: Vec<ScheduledEvent>, + /// Position → the event that serves it. + pub use_event: Vec<usize>, + /// Cached chunks left by an earlier run that this run will not use (no pending part needs + /// them, or keeping them would exceed the budget): deleted before anything is fetched. + pub discarded: Vec<usize>, + /// The most the cache holds at any point of the file order. + pub peak_cache_bytes: u64, +} + +/// Plans the downloads for the part sequence `chunk_of_use` (the chunk each part position reads) +/// with at most `budget` bytes of cached chunks (`chunk_bytes[c]` each). A chunk with one use is +/// a plain download. A chunk with more uses stays cached from one use to the next; when the cache +/// would exceed the budget, the cached chunk needed furthest in the future (possibly the one just +/// downloaded) is dropped, and its next use becomes a new download. `preloaded` chunks start in +/// the cache, the soonest needed first while they fit. +pub fn schedule( + chunk_of_use: &[usize], + chunk_bytes: &[u64], + preloaded: &[usize], + budget: u64, +) -> Schedule { + let n = chunk_of_use.len(); + let mut next_use = vec![NONE; n]; + let mut first_use: HashMap<usize, usize> = HashMap::new(); + for p in (0..n).rev() { + let c = chunk_of_use[p]; + next_use[p] = first_use.get(&c).copied().unwrap_or(NONE); + first_use.insert(c, p); + } + + let mut out = Schedule { + use_event: vec![NONE; n], + ..Schedule::default() + }; + // Cached chunk → (its event, the position it is next needed at), and the same keyed by next + // use so the furthest one is the last entry. + let mut resident: HashMap<usize, (usize, usize)> = HashMap::new(); + let mut by_next: BTreeSet<(usize, usize)> = BTreeSet::new(); + let mut bytes: u64 = 0; + + let mut pre: Vec<(usize, usize)> = Vec::new(); + let mut seen_pre: HashSet<usize> = HashSet::new(); + for &c in preloaded { + if !seen_pre.insert(c) { + continue; + } + match first_use.get(&c) { + Some(&p) => pre.push((p, c)), + None => out.discarded.push(c), + } + } + pre.sort_unstable(); + for (p, c) in pre { + let size = chunk_bytes[c]; + if bytes + size <= budget { + let e = out.events.len(); + out.events.push(ScheduledEvent { + chunk: c, + uses: Vec::new(), + preloaded: true, + }); + resident.insert(c, (e, p)); + by_next.insert((p, c)); + bytes += size; + } else { + out.discarded.push(c); + } + } + out.peak_cache_bytes = bytes; + + for p in 0..n { + let c = chunk_of_use[p]; + let next = next_use[p]; + if let Some((e, _)) = resident.remove(&c) { + by_next.remove(&(p, c)); + out.events[e].uses.push(p); + out.use_event[p] = e; + if next == NONE { + bytes -= chunk_bytes[c]; + } else { + resident.insert(c, (e, next)); + by_next.insert((next, c)); + } + continue; + } + let e = out.events.len(); + out.events.push(ScheduledEvent { + chunk: c, + uses: vec![p], + preloaded: false, + }); + out.use_event[p] = e; + if next == NONE { + continue; + } + resident.insert(c, (e, next)); + by_next.insert((next, c)); + bytes += chunk_bytes[c]; + while bytes > budget { + let Some(&(far, victim)) = by_next.iter().next_back() else { + break; + }; + by_next.remove(&(far, victim)); + resident.remove(&victim); + bytes -= chunk_bytes[victim]; + } + out.peak_cache_bytes = out.peak_cache_bytes.max(bytes); + } + out +} + +// ───────────────────────────────────────────────────────────────────────────────────────────── +// The plan +// ───────────────────────────────────────────────────────────────────────────────────────────── + +/// Everything the JNI layer hands over for one run. +pub struct StreamRequest { + pub manifest_bytes: Vec<u8>, + pub install_dir: String, + /// `""` = `<installDir>/.chunks`; a path = scratch cache (internal storage for a card install). + pub chunk_cache_dir: String, + /// The manager's owned staging directory on the install volume; `""` = stage beside each file. + pub staging_dir: String, + pub cdn_prefixes: Vec<String>, + /// Indices into `manifest.files` of the files still to write (Java's delta pass), in order. + pub pending_file_indices: Vec<usize>, + /// Java's `Σ fileSize` over the pending files, for a cross-check (`None` = skip). + pub expected_bytes: Option<u64>, + /// Most bytes of shared chunks the cache may hold. + pub cache_budget: u64, + /// Most planned bytes of replaced files (destination already there) staged at once; one is + /// always allowed. Fresh files are not limited by it. + pub replace_overlap_budget: u64, + pub ca_bundle_path: String, + pub max_workers: usize, + pub process_workers: usize, + pub label: String, +} + +#[derive(Debug)] +pub struct PlanPart { + /// Index into `manifest.unique_chunks`. + pub chunk: usize, + pub chunk_offset: usize, + pub file_offset: u64, + pub size: usize, +} + +#[derive(Debug)] +pub struct PlanFile { + pub rel_path: String, + pub size: u64, + /// The manifest's SHA-1 when it has one (all-zero = none, as the Java delta pass treats it). + pub sha1: Option<[u8; 20]>, + pub parts: Vec<PlanPart>, + /// Position of the file's first part in the install's part sequence. + pub first_use: usize, + /// The destination exists with data: staging this file costs space until its swap. + pub replaces: bool, +} + +#[derive(Debug)] +pub struct StreamPlan { + pub manifest: Manifest, + pub install_dir: PathBuf, + pub cache_dir: PathBuf, + pub scratch_cache: bool, + pub files: Vec<PlanFile>, + /// Position → (file, part index in that file). + pub use_file: Vec<usize>, + pub use_part: Vec<usize>, + pub schedule: Schedule, + /// The events that are fetched, in fetch order; a fetch item's id indexes this. + pub fetch_events: Vec<usize>, + /// Event → the file of its first use (0 for a preloaded copy nothing ends up using). + pub event_first_file: Vec<usize>, + /// Event → it is its chunk's first copy, cached as `<GUID>` (later copies get `<GUID>.<event>`, + /// so a copy being deleted can never remove the next one). + pub event_first_copy: Vec<bool>, + /// Every manifest file's destination: a staging name may never be one of them. + pub reserved: HashSet<PathBuf>, + /// Where staging files go; `None` = beside each destination. + pub staging_dir: Option<PathBuf>, + pub bytes_total: u64, + pub budget: u64, + pub overlap_budget: u64, + pub hosts: Vec<String>, + pub direct_chunks: usize, + pub shared_chunks: usize, + pub refetches: usize, +} + +/// A cached chunk left by an earlier run is reused only when it is what the manifest says: +/// the window size when there is one, the SHA-1 when there is one (`isValidCachedChunk`). +pub fn cached_chunk_is_valid(path: &Path, chunk: &ChunkInfo) -> bool { + let Ok(meta) = fs::metadata(path) else { + return false; + }; + if !meta.is_file() { + return false; + } + if chunk.window_size > 0 && meta.len() != chunk.window_size as u64 { + return false; + } + let Some(expected) = chunk.verifiable_sha1() else { + return true; + }; + match fs::read(path) { + Ok(data) => Sha1::digest(&data).as_slice() == &expected[..], + Err(_) => false, + } +} + +/// Parse, check, clean the cache and schedule. `Err` = nothing was fetched or written (stale cache +/// entries may have been removed, as the Java writer's own preparation does); the manager then +/// runs the bounded Java writer instead. +pub fn build_stream_plan(req: &StreamRequest) -> Result<StreamPlan, String> { + if req.install_dir.is_empty() { + return Err("stream: empty install dir".to_string()); + } + let manifest = parse_manifest(&req.manifest_bytes).map_err(|e| format!("stream: {e}"))?; + let by_guid = manifest.chunk_index_by_guid(); + + let mut files: Vec<PlanFile> = Vec::with_capacity(req.pending_file_indices.len()); + let mut use_file: Vec<usize> = Vec::new(); + let mut use_part: Vec<usize> = Vec::new(); + let mut chunk_of_use: Vec<usize> = Vec::new(); + let mut bytes_total: u64 = 0; + for &fi in &req.pending_file_indices { + let Some(f) = manifest.files.get(fi) else { + return Err(format!( + "stream: pending file index {fi} out of range ({} files)", + manifest.files.len() + )); + }; + let rel_path = relative_path(f); + let first_use = chunk_of_use.len(); + let mut parts = Vec::with_capacity(f.parts.len()); + let mut file_offset: u64 = 0; + for (k, part) in f.parts.iter().enumerate() { + let guid = part.guid_str(); + let Some(&ci) = by_guid.get(&guid) else { + return Err(format!("Missing chunk metadata {guid} for {rel_path}")); + }; + let size = part.size as u32 as usize; + let chunk_offset = usize::try_from(part.offset) + .map_err(|_| format!("Chunk {guid} is too short for {rel_path}"))?; + let window = chunk_window_bytes(manifest.unique_chunks[ci].window_size); + if (chunk_offset as u64).saturating_add(size as u64) > window { + return Err(format!("Chunk {guid} is too short for {rel_path}")); + } + parts.push(PlanPart { + chunk: ci, + chunk_offset, + file_offset, + size, + }); + file_offset += size as u64; + use_file.push(files.len()); + use_part.push(k); + chunk_of_use.push(ci); + } + bytes_total += file_offset; + let replaces = fs::metadata(Path::new(&req.install_dir).join(&rel_path)) + .map(|m| m.is_file() && m.len() > 0) + .unwrap_or(false); + files.push(PlanFile { + replaces, + rel_path, + size: file_offset, + sha1: f.sha1.filter(|h| h.iter().any(|&b| b != 0)), + parts, + first_use, + }); + } + if let Some(expected) = req.expected_bytes { + if expected != bytes_total { + return Err(format!( + "stream: byte total mismatch java={expected} rust={bytes_total}" + )); + } + } + let hosts = distinct_prefixes(&req.cdn_prefixes); + if hosts.is_empty() { + return Err("stream: no CDN prefixes".to_string()); + } + + let install_dir = PathBuf::from(&req.install_dir); + let staging_dir = if req.staging_dir.is_empty() { + None + } else { + let dir = PathBuf::from(&req.staging_dir); + if !dir.is_dir() { + return Err(format!("stream: staging dir {} missing", dir.display())); + } + Some(dir) + }; + let cache_dir = resolve_cache_dir(&req.install_dir, &req.chunk_cache_dir); + fs::create_dir_all(&cache_dir) + .map_err(|e| format!("stream: mkdirs {}: {e}", cache_dir.display()))?; + let reserved: HashSet<PathBuf> = manifest + .files + .iter() + .map(|f| install_dir.join(relative_path(f))) + .collect(); + + // The cache keeps only verified chunks this install still needs, as the Java writer's + // `prepareBoundedCache` does: anything else (a stale chunk, a `.part`, a later copy from a + // cancelled run) would only take space the files need. + let needed: HashSet<usize> = chunk_of_use.iter().copied().collect(); + let mut needed_by_name: HashMap<String, usize> = HashMap::new(); + for &ci in &needed { + needed_by_name.insert(manifest.unique_chunks[ci].guid_str(), ci); + } + let entries = fs::read_dir(&cache_dir) + .map_err(|e| format!("stream: list {}: {e}", cache_dir.display()))?; + let mut preloaded: Vec<usize> = Vec::new(); + for entry in entries.flatten() { + let path = entry.path(); + let name = entry.file_name().to_string_lossy().into_owned(); + if let Some(&ci) = needed_by_name.get(&name) { + if cached_chunk_is_valid(&path, &manifest.unique_chunks[ci]) { + preloaded.push(ci); + continue; + } + } + remove_entry(&path); + } + + let chunk_bytes: Vec<u64> = manifest + .unique_chunks + .iter() + .map(|c| chunk_window_bytes(c.window_size)) + .collect(); + let schedule = schedule(&chunk_of_use, &chunk_bytes, &preloaded, req.cache_budget); + for &ci in &schedule.discarded { + remove_entry(&cache_dir.join(manifest.unique_chunks[ci].guid_str())); + } + + let mut fetch_events = Vec::new(); + let mut event_first_file = Vec::with_capacity(schedule.events.len()); + let mut event_first_copy = Vec::with_capacity(schedule.events.len()); + let mut copies: HashMap<usize, usize> = HashMap::new(); + for (e, ev) in schedule.events.iter().enumerate() { + event_first_file.push(ev.uses.first().map(|&p| use_file[p]).unwrap_or(0)); + let count = copies.entry(ev.chunk).or_insert(0); + event_first_copy.push(*count == 0); + *count += 1; + if !ev.preloaded { + fetch_events.push(e); + } + } + let mut uses_per_chunk: HashMap<usize, usize> = HashMap::new(); + for &c in &chunk_of_use { + *uses_per_chunk.entry(c).or_insert(0) += 1; + } + let direct_chunks = uses_per_chunk.values().filter(|&&n| n == 1).count(); + let shared_chunks = uses_per_chunk.len() - direct_chunks; + let refetches: usize = copies.values().map(|&n| n - 1).sum(); + + Ok(StreamPlan { + manifest, + install_dir, + cache_dir, + scratch_cache: !req.chunk_cache_dir.is_empty(), + files, + use_file, + use_part, + schedule, + fetch_events, + event_first_file, + event_first_copy, + reserved, + staging_dir, + bytes_total, + budget: req.cache_budget, + overlap_budget: req.replace_overlap_budget, + hosts, + direct_chunks, + shared_chunks, + refetches, + }) +} + +fn remove_entry(path: &Path) { + if path.is_dir() { + let _ = fs::remove_dir_all(path); + } else { + let _ = fs::remove_file(path); + } +} + +// ───────────────────────────────────────────────────────────────────────────────────────────── +// The run +// ───────────────────────────────────────────────────────────────────────────────────────────── + +/// What the Java side sees of a run's progress. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct StreamProgress { + /// File bytes written so far (into staging files). + pub bytes_written: u64, + pub bytes_total: u64, + pub files_done: u64, + pub files_total: u64, + /// Sizes of the files verified and swapped into place. + pub files_bytes: u64, +} + +/// The run's callbacks. All may be called from any engine thread. +pub struct Hooks<'a> { + pub progress: &'a (dyn Fn(&StreamProgress) + Sync), + /// Called once, before the first file replaces its destination: the Java side marks an + /// existing install `updating`. `Err` fails the run and nothing is replaced. + pub before_replace: &'a (dyn Fn() -> Result<(), String> + Sync), + pub log: &'a (dyn Fn(&str) + Sync), +} + +#[derive(Clone, Debug, Default)] +pub struct StreamOutcome { + pub success: bool, + pub cancelled: bool, + pub error: String, + pub bytes_written: u64, + pub files_done: u64, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum EventState { + /// Not downloaded yet. + Pending, + /// Downloaded, its cache copy being written; uses are only claimed by the downloading thread. + Caching, + /// In the cache: newly reached files read it from there. + Cached, + /// Every use is claimed; no cache copy (left). + Done, +} + +struct Shared { + state: Vec<EventState>, + /// Event → uses not written yet. + remaining: Vec<u32>, + /// Event → bytes its cache copy takes. + cached_size: Vec<u64>, + /// Position → a thread has taken this part to write. + claimed: Vec<bool>, + /// File → parts not written yet. + parts_left: Vec<u32>, + /// Files `[0, active)` have been reached and may be written. + active: usize, + /// One past the highest first-use file of a downloaded event. + reached: usize, + /// `fetch_events[..next_fetch]` have all been downloaded. + next_fetch: usize, + /// Files `[0, admitted)` passed the replacement-overlap budget; `active` never passes it. + admitted: usize, + /// Planned bytes of admitted replaced files not swapped in yet. + overlap_open: u64, + peak_overlap: u64, + cache_bytes: u64, + peak_cache: u64, +} + +#[derive(Default)] +struct FileWriter { + staging: Option<PathBuf>, + file: Option<File>, + sha: Option<Sha1>, + /// Parts `[0, hashed)` are in the SHA-1. + hashed: usize, + done: Vec<bool>, + /// Bytes written into the staging file. + written: u64, +} + +#[derive(Default)] +struct Stats { + written: AtomicU64, + direct: AtomicU64, + from_cache: AtomicU64, + cache_stored: AtomicU64, + staging_now: AtomicU64, + staging_peak: AtomicU64, + files_done: AtomicU64, + files_bytes: AtomicU64, + fetched: AtomicU64, + last_progress_ms: AtomicU64, +} + +/// Parts to write from cache copies (event → positions), and part-less files to finish. +#[derive(Default)] +struct Work { + from_cache: HashMap<usize, Vec<usize>>, + empty_files: Vec<usize>, +} + +/// Unique part of every staging name this process creates. +static STAGE_SEQ: AtomicU64 = AtomicU64::new(0); + +/// The run's state; it is the fetch core's sink. +pub struct Engine<'a> { + plan: &'a StreamPlan, + cancel: &'a AtomicBool, + hooks: &'a Hooks<'a>, + shared: Mutex<Shared>, + writers: Vec<Mutex<FileWriter>>, + replace_gate: Mutex<Option<Result<(), String>>>, + stats: Stats, + started: Instant, + /// `Shared::admitted`, readable by the fetch driver without the lock. + admitted: AtomicUsize, +} + +fn fatal_write(rel: &str, err: impl std::fmt::Display) -> SinkError { + SinkError::Fatal(format!("Could not assemble or atomically replace {rel}: {err}")) +} + +impl<'a> Engine<'a> { + pub fn new(plan: &'a StreamPlan, cancel: &'a AtomicBool, hooks: &'a Hooks<'a>) -> Self { + let events = &plan.schedule.events; + let mut state = Vec::with_capacity(events.len()); + let mut cached_size = vec![0u64; events.len()]; + let mut cache_bytes = 0u64; + for (e, ev) in events.iter().enumerate() { + if !ev.preloaded { + state.push(EventState::Pending); + continue; + } + let path = cache_path(plan, e); + if ev.uses.is_empty() { + // Evicted before its first use in the plan: it only takes space. + let _ = fs::remove_file(&path); + state.push(EventState::Done); + continue; + } + let size = fs::metadata(&path) + .map(|m| m.len()) + .unwrap_or(plan_chunk_bytes(plan, ev.chunk)); + cached_size[e] = size; + cache_bytes += size; + state.push(EventState::Cached); + } + Engine { + plan, + cancel, + hooks, + shared: Mutex::new(Shared { + state, + remaining: events.iter().map(|ev| ev.uses.len() as u32).collect(), + cached_size, + claimed: vec![false; plan.use_file.len()], + parts_left: plan.files.iter().map(|f| f.parts.len() as u32).collect(), + active: 0, + reached: 0, + next_fetch: 0, + admitted: 0, + overlap_open: 0, + peak_overlap: 0, + cache_bytes, + peak_cache: cache_bytes, + }), + writers: plan.files.iter().map(|_| Mutex::new(FileWriter::default())).collect(), + replace_gate: Mutex::new(None), + stats: Stats::default(), + started: Instant::now(), + admitted: AtomicUsize::new(0), + } + } + + /// The fetch driver's gate: fetch item `id` may go out once its first file is admitted. + pub fn may_dispatch(&self, id: usize) -> bool { + let p = self.plan; + match p.fetch_events.get(id) { + Some(&e) => p.event_first_file[e] < self.admitted.load(Ordering::Acquire), + None => true, + } + } + + /// The most planned bytes of replaced files that were staged at once. + pub fn peak_replace_overlap(&self) -> u64 { + self.shared.lock().map(|st| st.peak_overlap).unwrap_or_default() + } + + /// Keep reaching files until nothing new can be written: a file that finishes can admit the + /// next replaced file, whose parts may already be in the cache. Iterative, so a long run of + /// cache-only files never deepens the stack. + fn settle(&self) -> Result<(), SinkError> { + loop { + let mut work = Work::default(); + { + let mut st = self.shared.lock().expect("stream state poisoned"); + self.advance(&mut st, &mut work); + } + if work.from_cache.is_empty() && work.empty_files.is_empty() { + return Ok(()); + } + self.run_work(work)?; + } + } + + /// Before the first fetch: the files before the first chunk to download can be written from + /// the cache an earlier run left (or are empty). + pub fn start(&self) -> Result<(), SinkError> { + self.settle() + } + + /// Reach every file that can be written now: all files before the first chunk still to + /// download, and every file up to the furthest first use of a chunk that has arrived. Parts + /// of newly reached files whose chunk is in the cache are claimed for writing. + fn advance(&self, st: &mut Shared, work: &mut Work) { + let p = self.plan; + while st.admitted < p.files.len() { + let file = &p.files[st.admitted]; + if file.replaces { + if st.overlap_open > 0 && st.overlap_open + file.size > p.overlap_budget { + break; + } + st.overlap_open += file.size; + st.peak_overlap = st.peak_overlap.max(st.overlap_open); + } + st.admitted += 1; + } + self.admitted.store(st.admitted, Ordering::Release); + while st.next_fetch < p.fetch_events.len() + && st.state[p.fetch_events[st.next_fetch]] != EventState::Pending + { + st.next_fetch += 1; + } + let frontier = match p.fetch_events.get(st.next_fetch) { + Some(&e) => p.event_first_file[e], + None => p.files.len(), + }; + let target = st.active.max(frontier).max(st.reached).min(st.admitted); + for f in st.active..target { + let file = &p.files[f]; + if file.parts.is_empty() { + work.empty_files.push(f); + continue; + } + for k in 0..file.parts.len() { + let pos = file.first_use + k; + let e = p.schedule.use_event[pos]; + if st.state[e] == EventState::Cached && !st.claimed[pos] { + st.claimed[pos] = true; + work.from_cache.entry(e).or_default().push(pos); + } + } + } + st.active = target; + } + + /// One downloaded chunk body (fetch item `id`). + fn on_body(&self, id: usize, body: Vec<u8>) -> Result<u64, SinkError> { + let p = self.plan; + let Some(&e) = p.fetch_events.get(id) else { + return Err(SinkError::Fatal(format!("item id {id} out of range"))); + }; + let ev = &p.schedule.events[e]; + let chunk = &p.manifest.unique_chunks[ev.chunk]; + let data = decode_verified_chunk(&body, chunk.window_size, chunk.verifiable_sha1()) + .map_err(|reason| SinkError::Retry(format!("{} {reason}", chunk.guid_str())))?; + drop(body); + self.stats.fetched.fetch_add(1, Ordering::Relaxed); + + let mut work = Work::default(); + let mut now: Vec<usize> = Vec::new(); + let caching; + { + let mut st = self.shared.lock().expect("stream state poisoned"); + if st.state[e] != EventState::Pending { + return Ok(chunk.credit_bytes()); + } + // Provisional: until the cache copy exists, nobody else may claim this chunk's uses. + st.state[e] = EventState::Caching; + st.reached = st.reached.max(p.event_first_file[e] + 1); + self.advance(&mut st, &mut work); + for &pos in &ev.uses { + if p.use_file[pos] < st.active && !st.claimed[pos] { + st.claimed[pos] = true; + now.push(pos); + } + } + caching = now.len() < ev.uses.len(); + if !caching { + st.state[e] = EventState::Done; + } + } + self.write_from_memory(&data, &now)?; + self.run_work(work)?; + if caching { + let path = cache_path(p, e); + write_cache_file(&path, &data).map_err(|err| { + SinkError::Fatal(format!("cache {}: {err}", chunk.guid_str())) + })?; + self.stats + .cache_stored + .fetch_add(data.len() as u64, Ordering::Relaxed); + let mut more = Vec::new(); + { + let mut st = self.shared.lock().expect("stream state poisoned"); + st.state[e] = EventState::Cached; + st.cached_size[e] = data.len() as u64; + st.cache_bytes += data.len() as u64; + st.peak_cache = st.peak_cache.max(st.cache_bytes); + // Files reached while the copy was being written. + for &pos in &ev.uses { + if p.use_file[pos] < st.active && !st.claimed[pos] { + st.claimed[pos] = true; + more.push(pos); + } + } + } + self.write_from_memory(&data, &more)?; + } + self.settle()?; + Ok(chunk.credit_bytes()) + } + + fn write_from_memory(&self, data: &[u8], positions: &[usize]) -> Result<(), SinkError> { + for &pos in positions { + let written = self.write_use(pos, data)?; + self.stats.direct.fetch_add(written, Ordering::Relaxed); + self.complete_use(pos)?; + } + Ok(()) + } + + fn run_work(&self, work: Work) -> Result<(), SinkError> { + let mut groups: Vec<(usize, Vec<usize>)> = work.from_cache.into_iter().collect(); + groups.sort_unstable(); + for (e, positions) in groups { + let ev = &self.plan.schedule.events[e]; + let guid = self.plan.manifest.unique_chunks[ev.chunk].guid_str(); + let data = fs::read(cache_path(self.plan, e)) + .map_err(|err| SinkError::Fatal(format!("read cached chunk {guid}: {err}")))?; + for pos in positions { + let written = self.write_use(pos, &data)?; + self.stats.from_cache.fetch_add(written, Ordering::Relaxed); + self.complete_use(pos)?; + } + } + for f in work.empty_files { + self.finalize(f)?; + } + Ok(()) + } + + /// Write the part at `pos` from its chunk's bytes into its file's staging file. + fn write_use(&self, pos: usize, chunk_data: &[u8]) -> Result<u64, SinkError> { + let p = self.plan; + let f = p.use_file[pos]; + let k = p.use_part[pos]; + let file = &p.files[f]; + let part = &file.parts[k]; + let guid = p.manifest.unique_chunks[part.chunk].guid_str(); + let end = part + .chunk_offset + .checked_add(part.size) + .filter(|&end| end <= chunk_data.len()) + .ok_or_else(|| { + SinkError::Fatal(format!("Chunk {guid} is too short for {}", file.rel_path)) + })?; + let slice = &chunk_data[part.chunk_offset..end]; + { + let mut guard = self.writers[f].lock().expect("writer poisoned"); + if guard.file.is_none() { + self.open_staging(f, &mut guard)?; + } + let FileWriter { + file: out, + sha, + hashed, + done, + written, + .. + } = &mut *guard; + let out = out.as_ref().expect("staging open"); + out.write_all_at(slice, part.file_offset) + .map_err(|err| fatal_write(&file.rel_path, err))?; + done[k] = true; + *written += slice.len() as u64; + // SHA-1 in file order: the part just written comes from memory, parts that arrived + // earlier out of order are read back from the staging file. + if let Some(sha) = sha.as_mut() { + while *hashed < file.parts.len() && done[*hashed] { + let h = *hashed; + if h == k { + sha.update(slice); + } else { + let q = &file.parts[h]; + let mut buf = vec![0u8; q.size]; + out.read_exact_at(&mut buf, q.file_offset) + .map_err(|err| fatal_write(&file.rel_path, err))?; + sha.update(&buf); + } + *hashed += 1; + } + } + } + let len = slice.len() as u64; + let staged = self.stats.staging_now.fetch_add(len, Ordering::Relaxed) + len; + self.stats.staging_peak.fetch_max(staged, Ordering::Relaxed); + self.stats.written.fetch_add(len, Ordering::Relaxed); + self.report(false); + Ok(len) + } + + /// A part is written: release its chunk copy after its last use, finish its file after its + /// last part. + fn complete_use(&self, pos: usize) -> Result<(), SinkError> { + let p = self.plan; + let e = p.schedule.use_event[pos]; + let f = p.use_file[pos]; + let (release, finish) = { + let mut st = self.shared.lock().expect("stream state poisoned"); + st.remaining[e] = st.remaining[e].saturating_sub(1); + let release = st.remaining[e] == 0 && st.state[e] == EventState::Cached; + if release { + st.state[e] = EventState::Done; + st.cache_bytes = st.cache_bytes.saturating_sub(st.cached_size[e]); + } + st.parts_left[f] = st.parts_left[f].saturating_sub(1); + (release, st.parts_left[f] == 0) + }; + if release { + let _ = fs::remove_file(cache_path(p, e)); + } + if finish { + self.finalize(f)?; + } + Ok(()) + } + + /// A unique `.stage` file in the owned staging directory (or beside the destination), never a + /// manifest path (`createStagingFile`). The destination's directory is created either way. + fn open_staging(&self, f: usize, w: &mut FileWriter) -> Result<(), SinkError> { + let file = &self.plan.files[f]; + let dest = self.plan.install_dir.join(&file.rel_path); + let parent = dest + .parent() + .map(Path::to_path_buf) + .unwrap_or_else(|| self.plan.install_dir.clone()); + fs::create_dir_all(&parent).map_err(|err| fatal_write(&file.rel_path, err))?; + let (stage_dir, prefix) = match &self.plan.staging_dir { + Some(dir) => (dir.clone(), "stream-"), + None => (parent.clone(), ".droiddeck-epic-"), + }; + for _ in 0..64 { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.subsec_nanos()) + .unwrap_or(0); + let seq = STAGE_SEQ.fetch_add(1, Ordering::Relaxed); + let path = stage_dir.join(format!( + "{prefix}{}-{seq}-{nanos}.stage", + std::process::id() + )); + if self.plan.reserved.contains(&path) { + continue; + } + match OpenOptions::new() + .read(true) + .write(true) + .create_new(true) + .open(&path) + { + Ok(out) => { + w.staging = Some(path); + w.file = Some(out); + w.sha = file.sha1.map(|_| Sha1::new()); + w.hashed = 0; + w.done = vec![false; file.parts.len()]; + w.written = 0; + return Ok(()); + } + Err(err) if err.kind() == ErrorKind::AlreadyExists => continue, + Err(err) => return Err(fatal_write(&file.rel_path, err)), + } + } + Err(fatal_write(&file.rel_path, "no free staging name")) + } + + /// All parts are in: sync, check size and SHA-1, mark the install updating (once), rename + /// over the destination. On any failure the staging file goes and the destination stays. + fn finalize(&self, f: usize) -> Result<(), SinkError> { + let mut guard = self.writers[f].lock().expect("writer poisoned"); + if guard.file.is_none() { + self.open_staging(f, &mut guard)?; + } + let result = self.finish_file(f, &mut guard); + let staged = guard.written; + if result.is_err() { + guard.file = None; + if let Some(path) = guard.staging.take() { + let _ = fs::remove_file(&path); + } + } + drop(guard); + self.stats + .staging_now + .fetch_sub(staged.min(self.stats.staging_now.load(Ordering::Relaxed)), Ordering::Relaxed); + if result.is_ok() { + let size = self.plan.files[f].size; + if self.plan.files[f].replaces { + let mut st = self.shared.lock().expect("stream state poisoned"); + st.overlap_open = st.overlap_open.saturating_sub(size); + } + self.stats.files_done.fetch_add(1, Ordering::Relaxed); + self.stats.files_bytes.fetch_add(size, Ordering::Relaxed); + self.report(true); + } + result + } + + fn finish_file(&self, f: usize, w: &mut FileWriter) -> Result<(), SinkError> { + let file = &self.plan.files[f]; + let rel = file.rel_path.as_str(); + let out = w.file.take().expect("staging open"); + out.sync_all().map_err(|err| fatal_write(rel, err))?; + if self.cancel.load(Ordering::Relaxed) { + return Err(SinkError::Fatal("cancelled".to_string())); + } + let len = out.metadata().map_err(|err| fatal_write(rel, err))?.len(); + drop(out); + if len != file.size { + return Err(SinkError::Fatal(format!("Size mismatch for {rel}"))); + } + if let (Some(expected), Some(sha)) = (file.sha1.as_ref(), w.sha.take()) { + if w.hashed != file.parts.len() || sha.finalize().as_slice() != &expected[..] { + return Err(SinkError::Fatal(format!("SHA-1 mismatch for {rel}"))); + } + } + // Once a swap can mix versions, the old installed record is no longer truthful. + self.mark_updating().map_err(|err| fatal_write(rel, err))?; + if self.cancel.load(Ordering::Relaxed) { + return Err(SinkError::Fatal("cancelled".to_string())); + } + let staging = w.staging.clone().expect("staging path"); + let dest = self.plan.install_dir.join(&file.rel_path); + fs::rename(&staging, &dest).map_err(|err| fatal_write(rel, err))?; + w.staging = None; + Ok(()) + } + + fn mark_updating(&self) -> Result<(), String> { + let mut gate = self.replace_gate.lock().expect("replace gate poisoned"); + if let Some(result) = gate.as_ref() { + return result.clone(); + } + let result = (self.hooks.before_replace)(); + *gate = Some(result.clone()); + result + } + + fn report(&self, force: bool) { + let now_ms = self.started.elapsed().as_millis() as u64; + let last = self.stats.last_progress_ms.load(Ordering::Relaxed); + if !force { + if now_ms.saturating_sub(last) < PROGRESS_EVERY_MS { + return; + } + if self + .stats + .last_progress_ms + .compare_exchange(last, now_ms, Ordering::Relaxed, Ordering::Relaxed) + .is_err() + { + return; + } + } else { + self.stats.last_progress_ms.store(now_ms, Ordering::Relaxed); + } + (self.hooks.progress)(&self.snapshot()); + } + + fn snapshot(&self) -> StreamProgress { + StreamProgress { + bytes_written: self.stats.written.load(Ordering::Relaxed), + bytes_total: self.plan.bytes_total, + files_done: self.stats.files_done.load(Ordering::Relaxed), + files_total: self.plan.files.len() as u64, + files_bytes: self.stats.files_bytes.load(Ordering::Relaxed), + } + } + + /// Cancel or failure: every unfinished staging file goes; the destinations are untouched and + /// the cache stays for the next run. Call only when no other thread is writing. + pub fn discard_staging(&self) -> usize { + let mut removed = 0; + for writer in &self.writers { + let mut guard = writer.lock().unwrap_or_else(|e| e.into_inner()); + guard.file = None; + if let Some(path) = guard.staging.take() { + if fs::remove_file(&path).is_ok() { + removed += 1; + } + } + } + removed + } + + /// Files not yet swapped into place. + pub fn unfinished_files(&self) -> u64 { + self.plan.files.len() as u64 - self.stats.files_done.load(Ordering::Relaxed) + } + + pub fn summary(&self) -> String { + let secs = self.started.elapsed().as_secs_f64().max(0.001); + let written = self.stats.written.load(Ordering::Relaxed); + let (peak_cache, peak_overlap) = self + .shared + .lock() + .map(|st| (st.peak_cache, st.peak_overlap)) + .unwrap_or_default(); + format!( + "stream-summary mode=adaptive files={}/{} bytes_written={written} direct_bytes={} from_cache_bytes={} cache_stored_bytes={} peak_cache_bytes={peak_cache} budget={} plan_peak_cache_bytes={} peak_staging_bytes={} peak_replace_overlap_bytes={peak_overlap} overlap_budget={} fetched_chunks={} refetches={} elapsed={secs:.1}s avg_MBps={:.2}", + self.stats.files_done.load(Ordering::Relaxed), + self.plan.files.len(), + self.stats.direct.load(Ordering::Relaxed), + self.stats.from_cache.load(Ordering::Relaxed), + self.stats.cache_stored.load(Ordering::Relaxed), + self.plan.budget, + self.plan.schedule.peak_cache_bytes, + self.stats.staging_peak.load(Ordering::Relaxed), + self.plan.overlap_budget, + self.stats.fetched.load(Ordering::Relaxed), + self.plan.refetches, + written as f64 / secs / (1024.0 * 1024.0), + ) + } +} + +impl FetchSink for Engine<'_> { + fn process(&self, item: &FetchItem, body: Vec<u8>) -> Result<u64, SinkError> { + self.on_body(item.id as usize, body) + } +} + +fn plan_chunk_bytes(plan: &StreamPlan, chunk: usize) -> u64 { + chunk_window_bytes(plan.manifest.unique_chunks[chunk].window_size) +} + +/// Where event `e`'s copy lives in the cache. +fn cache_path(plan: &StreamPlan, e: usize) -> PathBuf { + let guid = plan.manifest.unique_chunks[plan.schedule.events[e].chunk].guid_str(); + if plan.event_first_copy[e] { + plan.cache_dir.join(guid) + } else { + plan.cache_dir.join(format!("{guid}.{e}")) + } +} + +/// `.part` + rename, so a cache file under its final name is always complete. +fn write_cache_file(path: &Path, data: &[u8]) -> Result<(), String> { + let tmp = part_path(path); + let _ = fs::remove_file(&tmp); + let result = fs::write(&tmp, data) + .map_err(|e| format!("write {}: {e}", tmp.display())) + .and_then(|()| fs::rename(&tmp, path).map_err(|e| format!("rename: {e}"))); + if result.is_err() { + let _ = fs::remove_file(&tmp); + } + result +} + +/// Run the plan to the end: the fetch core downloads the chunks in order, the engine writes them. +pub fn run_stream( + plan: &StreamPlan, + req: &StreamRequest, + cancel: &AtomicBool, + hooks: &Hooks<'_>, +) -> StreamOutcome { + let log = hooks.log; + let host_cap = per_host_cap(req.max_workers, plan.hosts.len()); + log(&format!( + "stream-plan chunk_dir={} cache_dir={} scratch={} {} files={} bytes={} uses={} chunks_direct={} chunks_shared={} fetches={} preloaded={} refetches={} budget={} plan_peak_cache={} replaced_files={} overlap_budget={} hosts={} workers={} per_host_cap={host_cap} process_workers={}", + plan.manifest.chunk_dir, + plan.cache_dir.display(), + plan.scratch_cache, + crate::priority::log_field(), + plan.files.len(), + plan.bytes_total, + plan.use_file.len(), + plan.direct_chunks, + plan.shared_chunks, + plan.fetch_events.len(), + plan.schedule.events.len() - plan.fetch_events.len(), + plan.refetches, + plan.budget, + plan.schedule.peak_cache_bytes, + plan.files.iter().filter(|f| f.replaces).count(), + plan.overlap_budget, + plan.hosts.len(), + req.max_workers, + req.process_workers + )); + for (i, h) in plan.hosts.iter().enumerate() { + log(&format!("host[{i}]={h}")); + } + + let engine = Engine::new(plan, cancel, hooks); + let mut outcome = StreamOutcome::default(); + let mut error: Option<String> = match engine.start() { + Ok(()) => None, + Err(SinkError::Fatal(m)) | Err(SinkError::Retry(m)) => Some(m), + }; + + if error.is_none() && !cancel.load(Ordering::Relaxed) && !plan.fetch_events.is_empty() { + let items: Vec<FetchItem> = plan + .fetch_events + .iter() + .enumerate() + .map(|(id, &e)| { + let chunk = &plan.manifest.unique_chunks[plan.schedule.events[e].chunk]; + FetchItem { + id: id as u64, + urls: plan + .hosts + .iter() + .map(|h| chunk_url(h, &plan.manifest.chunk_dir, chunk)) + .collect(), + reserve: if chunk.file_size > 0 { + chunk.file_size as u64 + } else { + 0 + }, + range: None, + } + }) + .collect(); + let opts = FetchOptions { + max_workers: req.max_workers.max(1), + per_host_cap: host_cap, + timeout: REQUEST_TIMEOUT, + headers: vec![("User-Agent".to_string(), super::USER_AGENT.to_string())], + ca_bundle_path: req.ca_bundle_path.clone(), + process_workers: req.process_workers.max(1), + label: req.label.clone(), + ..FetchOptions::default() + }; + // Progress is the engine's (file bytes written); the core's own credit only feeds its + // `fetch-end` line. + let no_progress = |_: u64, _: u64| {}; + let gate = |id: usize| engine.may_dispatch(id); + let fetched = run_fetch_gated( + items, + &plan.hosts, + &opts, + &engine, + cancel, + &no_progress, + log, + &gate, + ); + error = fetched.error; + } + + let progress = engine.snapshot(); + outcome.bytes_written = progress.bytes_written; + outcome.files_done = progress.files_done; + if cancel.load(Ordering::Relaxed) { + let removed = engine.discard_staging(); + outcome.cancelled = true; + outcome.error = "cancelled".to_string(); + log(&engine.summary()); + log(&format!( + "cancelled ({}/{} files, {removed} staging files removed)", + progress.files_done, progress.files_total + )); + return outcome; + } + if error.is_none() && engine.unfinished_files() > 0 { + error = Some(format!( + "stream ended with {} files unfinished", + engine.unfinished_files() + )); + } + if let Some(err) = error { + let removed = engine.discard_staging(); + outcome.error = err; + log(&engine.summary()); + log(&format!( + "FAIL {} ({}/{} files ok, {removed} staging files removed)", + outcome.error, progress.files_done, progress.files_total + )); + return outcome; + } + outcome.success = true; + log(&engine.summary()); + match fs::remove_dir_all(&plan.cache_dir) { + Ok(()) => log(&format!("cache removed {}", plan.cache_dir.display())), + Err(e) => log(&format!("cache not removed {}: {e}", plan.cache_dir.display())), + } + outcome +} + +#[cfg(test)] +mod tests { + use super::super::chunk::test_support::{build_chunk_body, sha1_of, temp_dir}; + use super::super::manifest::test_support::{build_manifest, TestChunk, TestFile}; + use super::*; + + // ── schedule ── + + #[test] + fn single_use_chunks_are_plain_downloads_and_shared_ones_stay_cached() { + // Chunk 0 is used at positions 0 and 2, chunk 1 once, chunk 2 once. + let s = schedule(&[0, 1, 0, 2], &[10, 10, 10], &[], 100); + assert_eq!(s.events.len(), 3); + assert_eq!(s.events[0].uses, vec![0, 2]); + assert_eq!(s.events[1].uses, vec![1]); + assert_eq!(s.events[2].uses, vec![3]); + assert_eq!(s.use_event, vec![0, 1, 0, 2]); + assert_eq!(s.peak_cache_bytes, 10, "only the shared chunk is ever cached"); + assert!(s.discarded.is_empty()); + } + + #[test] + fn a_chunk_bigger_than_the_budget_is_downloaded_for_every_use() { + let s = schedule(&[0, 1, 0, 0], &[10, 1], &[], 5); + let chunk0: Vec<&ScheduledEvent> = s.events.iter().filter(|e| e.chunk == 0).collect(); + assert_eq!(chunk0.len(), 3, "never cached → one download per use"); + assert!(chunk0.iter().all(|e| e.uses.len() == 1)); + assert_eq!(s.peak_cache_bytes, 0); + } + + #[test] + fn eviction_drops_the_chunk_needed_furthest_away() { + // Budget fits two chunks. At position 2 chunk 2 arrives (next use 6) while 0 (next 3) + // and 1 (next 4) are cached: chunk 2 itself is the furthest and is not kept. + let uses = [0, 1, 2, 0, 1, 9, 2]; + let s = schedule(&uses, &[4, 4, 4, 0, 0, 0, 0, 0, 0, 1], &[], 8); + let copies = |c: usize| s.events.iter().filter(|e| e.chunk == c).count(); + assert_eq!(copies(0), 1); + assert_eq!(copies(1), 1); + assert_eq!(copies(2), 2, "chunk 2 downloaded again for its later use"); + assert!(s.peak_cache_bytes <= 8); + // Same sequence with room for all three: no refetch. + let s = schedule(&uses, &[4, 4, 4, 0, 0, 0, 0, 0, 0, 1], &[], 12); + assert_eq!(s.events.iter().filter(|e| e.chunk == 2).count(), 1); + assert_eq!(s.peak_cache_bytes, 12); + } + + #[test] + fn preloaded_chunks_serve_their_uses_until_the_budget_says_otherwise() { + // Chunk 0 cached by an earlier run, chunk 1 too but nothing needs it, chunk 2 cached but + // over budget. + let s = schedule(&[0, 2, 0], &[4, 4, 4], &[0, 1, 2], 4); + assert!(s.events[0].preloaded); + assert_eq!(s.events[0].chunk, 0); + assert_eq!(s.events[0].uses, vec![0, 2]); + let mut discarded = s.discarded.clone(); + discarded.sort_unstable(); + assert_eq!(discarded, vec![1, 2]); + let fetched: Vec<&ScheduledEvent> = s.events.iter().filter(|e| !e.preloaded).collect(); + assert_eq!(fetched.len(), 1); + assert_eq!(fetched[0].chunk, 2); + } + + // ── engine (no network: bodies are handed to the sink directly) ── + + const A: [u32; 4] = [1, 0, 0, 0]; + const B: [u32; 4] = [2, 0, 0, 0]; + const C: [u32; 4] = [3, 0, 0, 0]; + + fn data(seed: u32, len: usize) -> Vec<u8> { + (0..len as u32).map(|i| ((i * 7 + seed) % 251) as u8).collect() + } + + struct Fixture { + dir: PathBuf, + install: PathBuf, + chunks: Vec<([u32; 4], Vec<u8>)>, + files: Vec<(String, Vec<([u32; 4], i32, i32)>)>, + } + + impl Fixture { + /// Three files: `a.bin` = A[0..100] + B[0..50]; `dir/b.bin` = A[100..300] (A is shared); + /// `c.bin` = C whole. + fn new(tag: &str) -> Self { + let dir = temp_dir(tag); + let install = dir.join("game"); + fs::create_dir_all(&install).unwrap(); + Fixture { + dir, + install, + chunks: vec![(A, data(1, 300)), (B, data(2, 50)), (C, data(3, 80))], + files: vec![ + ("a.bin".to_string(), vec![(A, 0, 100), (B, 0, 50)]), + ("dir\\b.bin".to_string(), vec![(A, 100, 200)]), + ("c.bin".to_string(), vec![(C, 0, 80)]), + ], + } + } + + fn chunk(&self, guid: [u32; 4]) -> &Vec<u8> { + &self.chunks.iter().find(|(g, _)| *g == guid).unwrap().1 + } + + fn expected(&self, file: usize) -> Vec<u8> { + let mut out = Vec::new(); + for (g, off, size) in &self.files[file].1 { + out.extend_from_slice(&self.chunk(*g)[*off as usize..(*off + *size) as usize]); + } + out + } + + fn manifest(&self) -> Vec<u8> { + let chunks: Vec<TestChunk> = self + .chunks + .iter() + .map(|(g, d)| TestChunk { + guid: *g, + hash: g[0] as u64, + sha1: sha1_of(d), + group: 1, + window: d.len() as i32, + file_size: d.len() as u64, + }) + .collect(); + let files: Vec<TestFile> = (0..self.files.len()) + .map(|i| TestFile { + name: self.files[i].0.clone(), + sha1: sha1_of(&self.expected(i)), + tags: vec![], + parts: self.files[i].1.clone(), + }) + .collect(); + build_manifest(&chunks, &files, 21, true) + } + + fn request(&self, pending: Vec<usize>, budget: u64) -> StreamRequest { + StreamRequest { + manifest_bytes: self.manifest(), + install_dir: self.install.to_str().unwrap().to_string(), + chunk_cache_dir: self.dir.join("cache").to_str().unwrap().to_string(), + staging_dir: String::new(), + cdn_prefixes: vec!["http://127.0.0.1:9/Builds/x".to_string()], + pending_file_indices: pending, + expected_bytes: None, + cache_budget: budget, + replace_overlap_budget: u64::MAX, + ca_bundle_path: String::new(), + max_workers: 4, + process_workers: 2, + label: "epic test".to_string(), + } + } + + fn body_for(&self, plan: &StreamPlan, id: usize) -> Vec<u8> { + let ev = &plan.schedule.events[plan.fetch_events[id]]; + let guid = plan.manifest.unique_chunks[ev.chunk].guid; + build_chunk_body(self.chunk(guid), true, 0, None) + } + + fn stages(&self) -> Vec<PathBuf> { + let mut out = Vec::new(); + let mut dirs = vec![self.install.clone()]; + while let Some(d) = dirs.pop() { + for entry in fs::read_dir(&d).unwrap().flatten() { + let path = entry.path(); + if path.is_dir() { + dirs.push(path); + } else if path.to_string_lossy().ends_with(".stage") { + out.push(path); + } + } + } + out + } + } + + impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.dir); + } + } + + struct Recorder { + marks: Mutex<u32>, + last: Mutex<StreamProgress>, + fail_mark: bool, + } + + impl Recorder { + fn new() -> Self { + Recorder { + marks: Mutex::new(0), + last: Mutex::new(StreamProgress::default()), + fail_mark: false, + } + } + } + + fn with_engine<R>( + plan: &StreamPlan, + cancel: &AtomicBool, + rec: &Recorder, + body: impl FnOnce(&Engine) -> R, + ) -> R { + let progress = |p: &StreamProgress| *rec.last.lock().unwrap() = *p; + let before = || { + *rec.marks.lock().unwrap() += 1; + if rec.fail_mark { + Err("mark failed".to_string()) + } else { + Ok(()) + } + }; + let log = |_: &str| {}; + let hooks = Hooks { + progress: &progress, + before_replace: &before, + log: &log, + }; + let engine = Engine::new(plan, cancel, &hooks); + body(&engine) + } + + #[test] + fn plan_counts_direct_and_shared_chunks_and_orders_fetches_by_first_use() { + let fx = Fixture::new("stream-plan"); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 1 << 20)).unwrap(); + assert_eq!(plan.direct_chunks, 2, "B and C"); + assert_eq!(plan.shared_chunks, 1, "A"); + assert_eq!(plan.fetch_events.len(), 3); + assert_eq!(plan.refetches, 0); + assert_eq!(plan.bytes_total, 150 + 200 + 80); + let order: Vec<[u32; 4]> = plan + .fetch_events + .iter() + .map(|&e| plan.manifest.unique_chunks[plan.schedule.events[e].chunk].guid) + .collect(); + assert_eq!(order, vec![A, B, C]); + let mut req = fx.request(vec![0, 1, 2], 1 << 20); + req.expected_bytes = Some(1); + assert!(build_stream_plan(&req).unwrap_err().contains("byte total mismatch")); + } + + #[test] + fn out_of_order_arrival_writes_verified_files_and_empties_the_cache() { + let fx = Fixture::new("stream-ooo"); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 1 << 20)).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + // C, then B, then A: the shared chunk last. + for id in (0..plan.fetch_events.len()).rev() { + engine.process(&item(id), fx.body_for(&plan, id)).unwrap(); + } + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..3 { + let rel = fx.files[i].0.replace('\\', "/"); + assert_eq!(fs::read(fx.install.join(&rel)).unwrap(), fx.expected(i), "{rel}"); + } + assert!(fx.stages().is_empty()); + assert_eq!(*rec.marks.lock().unwrap(), 1, "marked updating once"); + let last = *rec.last.lock().unwrap(); + assert_eq!(last.files_done, 3); + assert_eq!(last.bytes_written, 430); + assert_eq!(last.files_bytes, 430); + let leftovers = fs::read_dir(&plan.cache_dir).unwrap().count(); + assert_eq!(leftovers, 0, "the shared chunk is released after its last use"); + } + + #[test] + fn a_shared_chunk_arriving_first_is_cached_until_its_later_file_is_reached() { + let fx = Fixture::new("stream-cache"); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 1 << 20)).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); // A + let cached = plan.cache_dir.join(guid_to(A)); + assert!(cached.is_file(), "A waits in the cache for dir/b.bin"); + engine.process(&item(1), fx.body_for(&plan, 1)).unwrap(); // B: reaches b.bin + assert!(!cached.exists(), "released after its last use"); + engine.process(&item(2), fx.body_for(&plan, 2)).unwrap(); // C + }); + assert_eq!(fs::read(fx.install.join("dir/b.bin")).unwrap(), fx.expected(1)); + } + + #[test] + fn a_zero_budget_refetches_shared_chunks_and_never_caches() { + let fx = Fixture::new("stream-budget"); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 0)).unwrap(); + assert_eq!(plan.refetches, 1, "A downloaded once per file"); + assert_eq!(plan.fetch_events.len(), 4); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + for id in [3, 1, 0, 2] { + engine.process(&item(id), fx.body_for(&plan, id)).unwrap(); + assert_eq!(fs::read_dir(&plan.cache_dir).unwrap().count(), 0); + } + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..3 { + let rel = fx.files[i].0.replace('\\', "/"); + assert_eq!(fs::read(fx.install.join(&rel)).unwrap(), fx.expected(i)); + } + } + + #[test] + fn resume_writes_only_pending_files_and_reuses_a_cached_chunk() { + let fx = Fixture::new("stream-resume"); + // An earlier run left A verified in the cache, plus a stale entry and a later copy. + let cache = fx.dir.join("cache"); + fs::create_dir_all(&cache).unwrap(); + fs::write(cache.join(guid_to(A)), fx.chunk(A)).unwrap(); + fs::write(cache.join("FFFFFFFF000000000000000000000000"), b"stale").unwrap(); + fs::write(cache.join(format!("{}.7", guid_to(A))), fx.chunk(A)).unwrap(); + // a.bin was verified by the delta pass; only dir/b.bin and c.bin are pending. + let plan = build_stream_plan(&fx.request(vec![1, 2], 1 << 20)).unwrap(); + assert_eq!(plan.fetch_events.len(), 1, "only C is downloaded"); + assert_eq!(fs::read_dir(&cache).unwrap().count(), 1, "stale entries removed"); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + assert!(fx.install.join("dir/b.bin").is_file(), "written from the cache at start"); + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); + assert_eq!(engine.unfinished_files(), 0); + }); + assert!(!fx.install.join("a.bin").exists()); + assert_eq!(fs::read(fx.install.join("c.bin")).unwrap(), fx.expected(2)); + } + + #[test] + fn cancel_keeps_old_files_removes_staging_and_keeps_the_cache() { + let fx = Fixture::new("stream-cancel"); + fs::write(fx.install.join("a.bin"), b"old a").unwrap(); + fs::write(fx.install.join("c.bin"), b"old c").unwrap(); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 1 << 20)).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + // A arrives: a.bin half written, A cached for dir/b.bin. + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); + assert_eq!(fx.stages().len(), 1); + cancel.store(true, Ordering::Relaxed); + // B completes a.bin, but a cancelled run never swaps it in. + assert!(engine.process(&item(1), fx.body_for(&plan, 1)).is_err()); + assert!(engine.discard_staging() <= 2); + }); + assert!(fx.stages().is_empty()); + assert_eq!(fs::read(fx.install.join("a.bin")).unwrap(), b"old a"); + assert_eq!(fs::read(fx.install.join("c.bin")).unwrap(), b"old c"); + assert_eq!(*rec.marks.lock().unwrap(), 0, "nothing was replaced"); + assert!(plan.cache_dir.join(guid_to(A)).is_file(), "kept for the resume"); + } + + #[test] + fn a_hash_failure_keeps_the_old_file_and_marks_nothing() { + let fx = Fixture::new("stream-hash"); + fs::write(fx.install.join("c.bin"), b"old c").unwrap(); + let mut plan = build_stream_plan(&fx.request(vec![2], 1 << 20)).unwrap(); + // The manifest promises another hash than the bytes the chunk yields. + plan.files[0].sha1 = Some([9; 20]); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + let err = with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + let res = engine.process(&item(0), fx.body_for(&plan, 0)); + engine.discard_staging(); + res + }); + match err { + Err(SinkError::Fatal(m)) => assert!(m.contains("SHA-1 mismatch for c.bin"), "{m}"), + other => panic!("expected a fatal hash failure, got {other:?}"), + } + assert_eq!(fs::read(fx.install.join("c.bin")).unwrap(), b"old c"); + assert!(fx.stages().is_empty()); + assert_eq!(*rec.marks.lock().unwrap(), 0); + } + + #[test] + fn a_corrupt_body_is_a_retry_that_changes_nothing() { + let fx = Fixture::new("stream-corrupt"); + let plan = build_stream_plan(&fx.request(vec![2], 1 << 20)).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + let mut body = fx.body_for(&plan, 0); + let n = body.len(); + body[n - 3] ^= 0xFF; + assert!(matches!(engine.process(&item(0), body), Err(SinkError::Retry(_)))); + assert!(fx.stages().is_empty()); + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); + }); + assert_eq!(fs::read(fx.install.join("c.bin")).unwrap(), fx.expected(2)); + } + + #[test] + fn a_failed_mark_stops_before_any_replacement() { + let fx = Fixture::new("stream-mark"); + fs::write(fx.install.join("c.bin"), b"old c").unwrap(); + let plan = build_stream_plan(&fx.request(vec![2], 1 << 20)).unwrap(); + let cancel = AtomicBool::new(false); + let mut rec = Recorder::new(); + rec.fail_mark = true; + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + let err = engine.process(&item(0), fx.body_for(&plan, 0)).unwrap_err(); + assert!(matches!(err, SinkError::Fatal(ref m) if m.contains("atomically replace"))); + }); + assert_eq!(fs::read(fx.install.join("c.bin")).unwrap(), b"old c"); + assert!(fx.stages().is_empty()); + } + + #[test] + fn an_owned_staging_dir_holds_the_stages_and_is_left_empty() { + let fx = Fixture::new("stream-staging"); + let staging = fx.install.join(".droiddeck-epic-staging"); + fs::create_dir_all(&staging).unwrap(); + let mut req = fx.request(vec![0, 1, 2], 1 << 20); + req.staging_dir = staging.to_str().unwrap().to_string(); + let plan = build_stream_plan(&req).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); + let stages: Vec<_> = fs::read_dir(&staging).unwrap().flatten().collect(); + assert_eq!(stages.len(), 1, "a.bin is staged in the owned directory"); + assert!(stages[0].file_name().to_string_lossy().ends_with(".stage")); + for id in 1..plan.fetch_events.len() { + engine.process(&item(id), fx.body_for(&plan, id)).unwrap(); + } + }); + assert_eq!(fs::read_dir(&staging).unwrap().count(), 0); + assert_eq!(fs::read(fx.install.join("dir/b.bin")).unwrap(), fx.expected(1)); + let mut missing = fx.request(vec![0], 1 << 20); + missing.staging_dir = fx.dir.join("nope").to_str().unwrap().to_string(); + assert!(build_stream_plan(&missing).unwrap_err().contains("staging dir")); + } + + #[test] + fn a_manifest_file_named_like_a_stage_is_never_used_for_staging() { + let fx = Fixture::new("stream-reserved"); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 1 << 20)).unwrap(); + assert!(plan.reserved.contains(&fx.install.join("dir/b.bin"))); + assert!(plan.reserved.contains(&fx.install.join("a.bin"))); + } + + // ── replacement-overlap gate ── + + /// `n` files of `len` bytes, one chunk each. + fn big_fixture(tag: &str, n: usize, len: usize) -> Fixture { + let dir = temp_dir(tag); + let install = dir.join("game"); + fs::create_dir_all(&install).unwrap(); + let guid = |i: usize| [0x100 + i as u32, 0, 0, 0]; + Fixture { + dir, + install, + chunks: (0..n).map(|i| (guid(i), data(i as u32 + 10, len))).collect(), + files: (0..n) + .map(|i| (format!("f{i}.bin"), vec![(guid(i), 0, len as i32)])) + .collect(), + } + } + + /// Drive the engine the way the fetch core does: items in order while the gate allows, + /// bodies handed back newest first (the most out-of-order the core can be). + fn drive_gated(fx: &Fixture, plan: &StreamPlan, engine: &Engine) -> Result<(), SinkError> { + engine.start()?; + let mut next = 0; + let mut in_flight: Vec<usize> = Vec::new(); + while next < plan.fetch_events.len() || !in_flight.is_empty() { + while next < plan.fetch_events.len() && engine.may_dispatch(next) { + in_flight.push(next); + next += 1; + } + let id = in_flight.pop().expect("gate closed with nothing in flight: deadlock"); + engine.process(&item(id), fx.body_for(plan, id))?; + } + Ok(()) + } + + fn write_old(fx: &Fixture, files: &[usize]) { + for &i in files { + fs::write(fx.install.join(&fx.files[i].0), b"old").unwrap(); + } + } + + #[test] + fn replaced_files_stay_within_the_overlap_budget_and_complete() { + let fx = big_fixture("stream-overlap", 6, 1000); + write_old(&fx, &[0, 1, 2, 3, 4, 5]); + let mut req = fx.request((0..6).collect(), 1 << 20); + req.replace_overlap_budget = 2500; + let plan = build_stream_plan(&req).unwrap(); + assert!(plan.files.iter().all(|f| f.replaces)); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + assert!(engine.may_dispatch(1), "two 1000-byte files fit 2500"); + assert!(!engine.may_dispatch(2), "a third does not"); + drive_gated(&fx, &plan, engine).unwrap(); + assert_eq!(engine.peak_replace_overlap(), 2000); + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..6 { + assert_eq!(fs::read(fx.install.join(&fx.files[i].0)).unwrap(), fx.expected(i)); + } + assert!(fx.stages().is_empty()); + } + + #[test] + fn a_budget_below_the_largest_file_still_progresses_one_at_a_time() { + let fx = big_fixture("stream-overlap-one", 4, 800); + write_old(&fx, &[0, 1, 2, 3]); + let mut req = fx.request((0..4).collect(), 1 << 20); + req.replace_overlap_budget = 1; + let plan = build_stream_plan(&req).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + assert!(engine.may_dispatch(0)); + assert!(!engine.may_dispatch(1)); + drive_gated(&fx, &plan, engine).unwrap(); + assert_eq!(engine.peak_replace_overlap(), 800); + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..4 { + assert_eq!(fs::read(fx.install.join(&fx.files[i].0)).unwrap(), fx.expected(i)); + } + } + + #[test] + fn a_fresh_install_is_never_gated() { + let fx = big_fixture("stream-overlap-fresh", 5, 600); + let mut req = fx.request((0..5).collect(), 1 << 20); + req.replace_overlap_budget = 1; + let plan = build_stream_plan(&req).unwrap(); + assert!(plan.files.iter().all(|f| !f.replaces)); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + assert!((0..5).all(|id| engine.may_dispatch(id))); + drive_gated(&fx, &plan, engine).unwrap(); + assert_eq!(engine.peak_replace_overlap(), 0); + }); + assert_eq!(fs::read(fx.install.join("f4.bin")).unwrap(), fx.expected(4)); + } + + #[test] + fn mixed_fresh_and_replaced_files_gate_only_the_replacements() { + let fx = big_fixture("stream-overlap-mixed", 6, 700); + write_old(&fx, &[1, 3, 5]); + let mut req = fx.request((0..6).collect(), 1 << 20); + req.replace_overlap_budget = 700; + let plan = build_stream_plan(&req).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + // f0 fresh, f1 replaced (admitted), f2 fresh, f3 replaced (waits for f1's swap). + assert!(engine.may_dispatch(2)); + assert!(!engine.may_dispatch(3)); + drive_gated(&fx, &plan, engine).unwrap(); + assert_eq!(engine.peak_replace_overlap(), 700); + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..6 { + assert_eq!(fs::read(fx.install.join(&fx.files[i].0)).unwrap(), fx.expected(i)); + } + } + + #[test] + fn a_shared_chunk_for_a_gated_file_waits_in_the_cache() { + // f0 and f2 are replaced and share chunk A; f1 is fresh. With room for one replaced file, + // f2 is admitted only after f0's swap, and A is cached until then. + let dir = temp_dir("stream-overlap-shared"); + let install = dir.join("game"); + fs::create_dir_all(&install).unwrap(); + let fx = Fixture { + dir, + install, + chunks: vec![(A, data(1, 300)), (B, data(2, 50))], + files: vec![ + ("f0.bin".to_string(), vec![(A, 0, 100)]), + ("f1.bin".to_string(), vec![(B, 0, 50)]), + ("f2.bin".to_string(), vec![(A, 100, 200)]), + ], + }; + write_old(&fx, &[0, 2]); + let mut req = fx.request(vec![0, 1, 2], 1 << 20); + req.replace_overlap_budget = 150; + let plan = build_stream_plan(&req).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + drive_gated(&fx, &plan, engine).unwrap(); + assert!(engine.peak_replace_overlap() <= 200); + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..3 { + assert_eq!(fs::read(fx.install.join(&fx.files[i].0)).unwrap(), fx.expected(i)); + } + assert_eq!(fs::read_dir(&plan.cache_dir).unwrap().count(), 0); + } + + #[test] + fn cancelling_a_gated_update_keeps_every_old_file() { + let fx = big_fixture("stream-overlap-cancel", 4, 900); + write_old(&fx, &[0, 1, 2, 3]); + let mut req = fx.request((0..4).collect(), 1 << 20); + req.replace_overlap_budget = 1000; + let plan = build_stream_plan(&req).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); // f0 swapped in + assert!(engine.may_dispatch(1)); + cancel.store(true, Ordering::Relaxed); + assert!(engine.process(&item(1), fx.body_for(&plan, 1)).is_err()); + engine.discard_staging(); + }); + assert_eq!(fs::read(fx.install.join("f0.bin")).unwrap(), fx.expected(0)); + for i in 1..4 { + assert_eq!(fs::read(fx.install.join(&fx.files[i].0)).unwrap(), b"old"); + } + assert!(fx.stages().is_empty()); + } + + fn item(id: usize) -> FetchItem { + FetchItem { + id: id as u64, + urls: vec!["http://127.0.0.1:9/x".to_string()], + reserve: 0, + range: None, + } + } + + fn guid_to(g: [u32; 4]) -> String { + super::super::manifest::guid_to_string(&g) + } +} diff --git a/app/src/main/rust/stores/src/fetch_core.rs b/app/src/main/rust/stores/src/fetch_core.rs index 9dc96faa6..d0a5fa36c 100644 --- a/app/src/main/rust/stores/src/fetch_core.rs +++ b/app/src/main/rust/stores/src/fetch_core.rs @@ -117,6 +117,25 @@ pub fn run_fetch( cancel: &AtomicBool, progress: &(dyn Fn(u64, u64) + Sync), log: &(dyn Fn(&str) + Sync), +) -> FetchOutcome { + run_fetch_gated(items, hosts, opts, sink, cancel, progress, log, &|_: usize| true) +} + +/// [`run_fetch`] with an admission gate: the next fresh item (by index) is only dispatched once +/// `gate(index)` says so. Items are still taken strictly in order, and retries are never held +/// back. While the gate is closed the driver idles as it does on a full window, so a gate that +/// opens as the sink makes progress (e.g. as files finish) paces the run without stalling the +/// requests already in flight. The adaptivity is unchanged. +#[allow(clippy::too_many_arguments)] +pub fn run_fetch_gated( + items: Vec<FetchItem>, + hosts: &[String], + opts: &FetchOptions, + sink: &dyn FetchSink, + cancel: &AtomicBool, + progress: &(dyn Fn(u64, u64) + Sync), + log: &(dyn Fn(&str) + Sync), + gate: &(dyn Fn(usize) -> bool + Sync), ) -> FetchOutcome { let started = Instant::now(); let label = opts.label.as_str(); @@ -359,6 +378,7 @@ mode={}", Ok(rt) => { rt.block_on(run_driver(DriverCtx { items: items.as_slice(), + gate, hosts: hosts.as_slice(), opts, label, @@ -832,6 +852,7 @@ struct FetchDone { struct DriverCtx<'a> { items: &'a [FetchItem], + gate: &'a (dyn Fn(usize) -> bool + Sync), hosts: &'a [String], opts: &'a FetchOptions, label: &'a str, @@ -853,6 +874,7 @@ struct DriverCtx<'a> { async fn run_driver(ctx: DriverCtx<'_>) { let DriverCtx { items, + gate, hosts, opts, label, @@ -997,7 +1019,7 @@ async fn run_driver(ctx: DriverCtx<'_>) { let (idx, attempts) = if use_retry { let p = *retry.front().expect("retry nonempty"); (p.idx, p.attempts) - } else if next_item < items.len() { + } else if next_item < items.len() && gate(next_item) { (next_item, 0u32) } else { break; diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt index 217bc7d5a..de903c80f 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt @@ -11,6 +11,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowStatFs @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) @@ -55,4 +56,87 @@ class StoreInstallRootTest { assertTrue(dir.absolutePath.startsWith(app.cacheDir.absolutePath)) assertEquals("a_b_c", dir.name) } + + @Test fun estimatedBytesNeededUsesKnownInstallSizeForAllStores() { + val item = CatalogItem(Store.EPIC, "game1", "Game 1", imageUrl = null, owned = true, sizeBytes = 10_000_000L) + assertEquals(10_000_000L, StoreInstallRoot.estimatedBytesNeeded(item)) + + val gogItem = CatalogItem(Store.GOG, "game2", "Game 2", imageUrl = null, owned = true, sizeBytes = 10_000_000L) + assertEquals(10_000_000L, StoreInstallRoot.estimatedBytesNeeded(gogItem)) + } + + @Test fun checkStorageReportsEnoughWhenSpaceIsUnknownOrAvailable() { + val zeroItem = CatalogItem(Store.EPIC, "game1", "Game 1", imageUrl = null, owned = true, sizeBytes = 0L) + val check = StoreInstallRoot.checkStorage(app, zeroItem, StoreInstallRoot.internalRoot(app)) + assertTrue(check.enough) + } + + @Test fun availableBytesUsesNearestExistingAncestorWithoutCreatingDirectories() { + val existing = File(app.filesDir, "storage-probe").apply { mkdirs() } + val missing = File(existing, "not-created/another-level/game") + ShadowStatFs.registerStats(existing.path, 1024, 100, 100) + + assertTrue(StoreInstallRoot.availableBytes(missing) > 0L) + assertFalse(File(existing, "not-created").exists()) + } + + @Test fun epicDefersFullSizeChecksToItsManifestDeltaForResume() { + val item = CatalogItem(Store.EPIC, "large", "Large", imageUrl = null, owned = true, sizeBytes = Long.MAX_VALUE) + val check = StoreInstallRoot.checkStorage(app, item, StoreInstallRoot.internalRoot(app)) + assertTrue(check.enough) + assertEquals(Long.MAX_VALUE, check.requiredBytes) + } + + @Test fun gogAndAmazonResumeChecksUseTheExistingVolumeAndDeferRemainingWork() { + val card = File(app.filesDir, "resume-card").apply { mkdirs() } + SessionPrefs.setGameStorage(app, card.path, "Card") + for (store in listOf(Store.GOG, Store.AMAZON)) { + val item = CatalogItem(store, "resume-${store.id}", "Partial ${store.id}", imageUrl = null, sizeBytes = Long.MAX_VALUE) + val folder = File(StoreInstallRoot.storeDir(File(card, "Games"), store), StoreInstallRoot.folderName(item.title, item.id)).apply { mkdirs() } + StoreInstalls.begin(folder, store, item.id, item.title, null, null) + val check = StoreInstallRoot.checkStorage(app, item, StoreInstallRoot.internalRoot(app)) + assertTrue(check.enough) + assertEquals(StoreInstallRoot.availableBytes(folder), check.availableBytes) + assertEquals(folder, StoreInstallRoot.folderFor(app, store, item.id, item.title)) + } + } + + @Test fun freshGogAndAmazonStillRejectInsufficientCatalogSpace() { + StoreInstallRoot.internalRoot(app).mkdirs() + ShadowStatFs.registerStats(StoreInstallRoot.internalRoot(app).path, 1024, 100, 100) + for (store in listOf(Store.GOG, Store.AMAZON)) { + val item = CatalogItem(store, "too-large-${store.id}", "Too large ${store.id}", imageUrl = null, sizeBytes = Long.MAX_VALUE) + assertFalse(StoreInstallRoot.checkStorage(app, item, StoreInstallRoot.internalRoot(app)).enough) + } + } + + @Test fun measuredZeroRejectsFreshGogAndAmazonRatherThanBeingUnknown() { + val root = File(app.filesDir, "zero-space").apply { mkdirs() } + ShadowStatFs.registerStats(root.path, 1024, 0, 0) + val missing = File(root, "not-created/game") + assertEquals(0L, StoreInstallRoot.availableBytes(missing)) + assertFalse(File(root, "not-created").exists()) + for (store in listOf(Store.GOG, Store.AMAZON)) { + val item = CatalogItem(store, "zero-${store.id}", "Zero ${store.id}", imageUrl = null, sizeBytes = 1L) + val check = StoreInstallRoot.checkStorage(app, item, missing) + assertFalse(check.enough) + assertEquals(0L, check.availableBytes) + } + } + + @Test fun failedProbesAreUnknownAndDoNotCreateOrWriteToTargets() { + val root = File(app.filesDir, "read-only-probe").apply { mkdirs() } + val missing = File(root, "not-created/game") + val fail: (File) -> Long = { throw java.io.IOException("unavailable") } + assertEquals(-1L, StoreInstallRoot.probeAvailableBytes(missing, fail, fail)) + assertEquals(-1L, StoreInstallRoot.probeAvailableBytes(missing, fail, { 0L })) + assertEquals(42L, StoreInstallRoot.probeAvailableBytes(missing, fail, { 42L })) + assertEquals(0L, StoreInstallRoot.probeAvailableBytes(missing, { 0L }, fail)) + assertFalse(File(root, "not-created").exists()) + assertTrue(root.listFiles().orEmpty().isEmpty()) + assertFalse(StoreInstallRoot.insufficientSpace(1L, -1L)) + assertTrue(StoreInstallRoot.insufficientSpace(1L, 0L)) + assertFalse(StoreInstallRoot.insufficientSpace(1L, 1L)) + assertFalse(StoreInstallRoot.insufficientSpace(0L, 0L)) + } } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/amazon/AmazonResumeSpaceTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/amazon/AmazonResumeSpaceTest.kt new file mode 100644 index 000000000..6382f9519 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/stores/amazon/AmazonResumeSpaceTest.kt @@ -0,0 +1,43 @@ +package com.droiddeck.launcher.stores.amazon + +import java.io.File +import java.security.MessageDigest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowStatFs +import com.droiddeck.launcher.stores.StoreInstallRoot + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class AmazonResumeSpaceTest { + @Test fun remainingWorkRejectsMeasuredZeroButNotUnknownSpace() { + val root = File(RuntimeEnvironment.getApplication().filesDir, "amazon-zero").apply { mkdirs() } + ShadowStatFs.registerStats(root.path, 1024, 0, 0) + val files = listOf(AmazonManifest.ManifestFile("missing", 4, 0, null)) + val needed = AmazonDownloadManager.remainingInstallBytes(files, root) + assertEquals(4L, needed) + assertTrue(StoreInstallRoot.insufficientSpace(needed, StoreInstallRoot.availableBytes(root))) + assertFalse(StoreInstallRoot.insufficientSpace(needed, StoreInstallRoot.SPACE_UNKNOWN)) + } + + @Test fun remainingSpaceExcludesVerifiedFilesButIncludesCorruptAndPartialFiles() { + val root = File(RuntimeEnvironment.getApplication().filesDir, "amazon-resume").apply { mkdirs() } + val data = byteArrayOf(1, 2, 3, 4) + val hash = MessageDigest.getInstance("SHA-256").digest(data) + val files = listOf("good", "corrupt", "partial", "missing").map { + AmazonManifest.ManifestFile(it, 4, 0, hash) + } + File(root, "good").writeBytes(data) + File(root, "corrupt").writeBytes(ByteArray(4)) + File(root, "partial").writeBytes(byteArrayOf(1)) + assertEquals(12L, AmazonDownloadManager.remainingInstallBytes(files, root)) + for (file in files) File(root, file.unixPath()).writeBytes(data) + assertEquals(0L, AmazonDownloadManager.remainingInstallBytes(files, root)) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadQueueRetryTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadQueueRetryTest.kt new file mode 100644 index 000000000..879e8de91 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadQueueRetryTest.kt @@ -0,0 +1,140 @@ +package com.droiddeck.launcher.stores.download + +import android.content.Context +import com.droiddeck.launcher.session.SessionPrefs +import com.droiddeck.launcher.stores.* +import java.io.File +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import org.junit.After +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import org.json.JSONArray +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowStatFs + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class DownloadQueueRetryTest { + private val app get() = RuntimeEnvironment.getApplication() + + @After fun cleanRows() { DownloadQueue.dismissFinished(); StoreSizes.known.clear() } + + private fun awaitFailure(key: String, error: String) { + val until = System.nanoTime() + TimeUnit.SECONDS.toNanos(5) + while (System.nanoTime() < until) { + val entry = DownloadQueue.entry(key) + if (entry?.state == DownloadState.FAILED && entry.error == error) return + Thread.sleep(10) + } + fail("Expected failed real job: ${DownloadQueue.entry(key)}") + } + + @Test fun storageRetryRechecksThenRunsTheRealBackendAndNormalRetryStillWorks() { + StoreInstallRoot.internalRoot(app).mkdirs() + ShadowStatFs.registerStats(StoreInstallRoot.internalRoot(app).path, 1024, 100, 100) + val item = CatalogItem(Store.GOG, "preflight-retry", "Preflight retry", imageUrl = null, sizeBytes = Long.MAX_VALUE) + val runs = AtomicInteger() + var lastTarget: File? = null + val previous = StoresState.backend(Store.GOG) + val backend = object : StoreBackend { + override val store = Store.GOG + override fun signIn(context: Context) {} + override fun signOut(context: Context) {} + override fun syncLibrary(context: Context, force: Boolean) {} + override fun loadShelves(context: Context, force: Boolean) {} + override fun uninstall(context: Context, game: InstalledStoreGame) {} + override fun install(context: Context, item: CatalogItem, root: File) { + lastTarget = root + DownloadQueue.enqueue(context, DownloadEntry(item.store, item.id, item.title)) { + object : DownloadQueue.DownloadJob { + override fun run(handle: DownloadQueue.JobHandle): String? { + throw IllegalStateException("backend-${runs.incrementAndGet()}") + } + override fun cancel(deleteFiles: Boolean) {} + } + } + } + } + StoresState.register(backend) + try { + StoresState.install(app, item) + assertEquals(DownloadState.FAILED, DownloadQueue.entry(item.key)?.state) + DownloadQueue.retry(app, item.key) + assertEquals(DownloadState.FAILED, DownloadQueue.entry(item.key)?.state) + assertEquals(0, runs.get()) + StoreSizes.known[item.key] = 1 + DownloadQueue.retry(app, item.key) + awaitFailure(item.key, "backend-1") + DownloadQueue.retry(app, item.key) + awaitFailure(item.key, "backend-2") + DownloadQueue.clear(app, item.key) + assertNull(DownloadQueue.entry(item.key)) + + // A card resume (including the restored paused path) must retain its real target. + val card = File(app.filesDir, "retry-card").apply { mkdirs() } + SessionPrefs.setGameStorage(app, card.path, "Card") + val target = File(card, "Games") + val folder = File(StoreInstallRoot.storeDir(target, item.store), StoreInstallRoot.folderName(item.title, item.id)).apply { mkdirs() } + StoreInstalls.begin(folder, item.store, item.id, item.title, null, null) + StoreSizes.known[item.key] = Long.MAX_VALUE + StoresState.unfinished = StoreInstallRoot.unfinished(app) + DownloadQueue.holdPaused(item.key) + StoresState.requestInstall(app, item) + assertEquals(target, lastTarget) + assertEquals(DownloadState.PAUSED, DownloadQueue.entry(item.key)?.state) + assertEquals(2, runs.get()) + val saved = File(app.filesDir, "store-queue.json") + val until = System.nanoTime() + TimeUnit.SECONDS.toNanos(5) + var correctSavedRoot = false + while (!correctSavedRoot && System.nanoTime() < until) { + correctSavedRoot = runCatching { + val rows = JSONArray(saved.readText()) + (0 until rows.length()).any { i -> + val row = rows.getJSONObject(i) + row.getJSONObject("item").getString("id") == item.id && + row.getString("root") == target.absolutePath && row.getBoolean("paused") + } + }.getOrDefault(false) + if (!correctSavedRoot) Thread.sleep(10) + } + assertTrue("Paused saved queue must remember the existing card root", correctSavedRoot) + DownloadQueue.clear(app, item.key) + } finally { + if (previous != null) StoresState.register(previous) + SessionPrefs.setGameStorage(app, SessionPrefs.GAME_STORAGE_OFF, "") + StoresState.unfinished = emptyList() + } + } + + @Test fun preflightClearRemovesTheRowWithoutRunningItsRetry() { + val entry = DownloadEntry(Store.AMAZON, "clear-preflight", "Clear") + DownloadQueue.recordFailed(app, entry, "storage") { fail("Clear must not retry") } + DownloadQueue.clear(app, entry.key) + assertNull(DownloadQueue.entry(entry.key)) + } + + @Test fun heldPausedBackendRestoresAndResumesWithoutAPlaceholder() { + val entry = DownloadEntry(Store.GOG, "restored-paused", "Paused") + val ran = CountDownLatch(1) + DownloadQueue.holdPaused(entry.key) + DownloadQueue.enqueue(app, entry) { + object : DownloadQueue.DownloadJob { + override fun run(handle: DownloadQueue.JobHandle): String? { + ran.countDown() + throw IllegalStateException("restored") + } + override fun cancel(deleteFiles: Boolean) {} + } + } + assertEquals(DownloadState.PAUSED, DownloadQueue.entry(entry.key)?.state) + assertEquals(1L, ran.count) + DownloadQueue.resume(app, entry.key) + assertTrue(ran.await(5, TimeUnit.SECONDS)) + awaitFailure(entry.key, "restored") + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicAdaptivePlanTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicAdaptivePlanTest.kt new file mode 100644 index 000000000..fdf46d0df --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicAdaptivePlanTest.kt @@ -0,0 +1,107 @@ +package com.droiddeck.launcher.stores.epic + +import java.io.File +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class EpicAdaptivePlanTest { + @get:Rule val temp = TemporaryFolder() + private val mib = 1024L * 1024 + private val gib = 1024 * mib + + private fun file(name: String, size: Long) = EpicDownloadManager.FileInfo().apply { + filename = name + var left = size + while (left > 0) { + val part = minOf(left, Int.MAX_VALUE.toLong()) + parts.add(EpicDownloadManager.ChunkPart().apply { this.size = part.toInt() }) + left -= part + } + } + + @Test fun netPeakCreditsEachOldCopyAtItsSwapAndLargestReplacedIgnoresFreshFiles() { + val files = listOf(file("a", 10 * gib), file("b", 2 * gib), file("c", 4 * gib)) + val old = mapOf("a" to 9 * gib, "c" to 5 * gib) + // Running Σ(new − old): +1, +3, +2 GiB. + assertEquals(3 * gib, EpicDownloadManager.settledNetPeak(files, old)) + assertEquals(10 * gib, EpicDownloadManager.largestReplacedFile(files, old)) + assertEquals(16 * gib, EpicDownloadManager.settledNetPeak(files, emptyMap())) + assertEquals(0L, EpicDownloadManager.largestReplacedFile(files, emptyMap())) + assertEquals(0L, EpicDownloadManager.largestReplacedFile(files, mapOf("b" to 0L))) + } + + @Test fun cacheBudgetIsHalfTheHeadroomCappedAndDefaultsWhenSpaceIsUnknown() { + assertEquals(EpicDownloadManager.ADAPTIVE_CACHE_DEFAULT_BYTES, EpicDownloadManager.adaptiveCacheBudget(-1, 0)) + assertEquals(EpicDownloadManager.ADAPTIVE_CACHE_MAX_BYTES, EpicDownloadManager.adaptiveCacheBudget(100 * gib, 10 * gib)) + assertEquals(0L, EpicDownloadManager.adaptiveCacheBudget(gib, gib)) + val free = 1000 * mib + assertEquals((free - 400 * mib - EpicDownloadManager.ADAPTIVE_TRANSIENT_BYTES) / 2, EpicDownloadManager.adaptiveCacheBudget(free, 400 * mib)) + } + + @Test fun freshInstallsHaveNoOverlapLimit() { + val plan = EpicDownloadManager.planAdaptiveSpace(20 * gib, 0, 30 * gib, true, 10 * gib) + assertNull(plan.error) + assertEquals(-1L, plan.overlapBudget) + assertEquals(20 * gib, plan.gameNeed) + } + + @Test fun updateOverlapBudgetIsTheLeftoverAfterNetGrowthAndCache() { + // Card install, cache on internal: the card's leftover after the net growth is the budget. + val roomy = EpicDownloadManager.planAdaptiveSpace(gib, 4 * gib, 20 * gib, true, 10 * gib) + assertNull(roomy.error) + assertFalse(roomy.cacheBesideGame) + assertEquals(19 * gib, roomy.overlapBudget) + assertEquals(20 * gib, roomy.gameNeed) + // Tight: floored at the largest replaced file, one at a time. + val tight = EpicDownloadManager.planAdaptiveSpace(gib, 4 * gib, 5 * gib, true, 10 * gib) + assertNull(tight.error) + assertEquals(4 * gib, tight.overlapBudget) + // Not even one replaced file fits: the shortfall is named. + val short = EpicDownloadManager.planAdaptiveSpace(gib, 4 * gib, 4 * gib, true, 10 * gib) + assertTrue(short.error!!.contains("game's storage")) + assertTrue(short.error!!.contains("short by 1.0 GB")) + // Unknown free space: one replaced file at a time. + assertEquals(4 * gib, EpicDownloadManager.planAdaptiveSpace(gib, 4 * gib, -1, true, -1).overlapBudget) + } + + @Test fun cacheBesideTheGameComesOutOfTheOverlapBudget() { + val plan = EpicDownloadManager.planAdaptiveSpace(gib, 2 * gib, 10 * gib, false, -1) + assertNull(plan.error) + assertTrue(plan.cacheBesideGame) + assertTrue(plan.cacheBudget > 0) + assertEquals(10 * gib - gib - plan.cacheBudget - EpicDownloadManager.ADAPTIVE_TRANSIENT_BYTES, plan.overlapBudget) + assertEquals(10 * gib, plan.gameNeed) + } + + @Test fun fullInternalStorageMovesTheCacheBesideTheGameInsteadOfFailing() { + val plan = EpicDownloadManager.planAdaptiveSpace(20 * gib, 0, 30 * gib, true, 100 * mib) + assertNull(plan.error) + assertTrue(plan.cacheBesideGame) + assertEquals(0L, plan.scratchNeed) + assertTrue(plan.cacheBudget > 0) + assertEquals(20 * gib + plan.cacheBudget + EpicDownloadManager.ADAPTIVE_TRANSIENT_BYTES, plan.gameNeed) + } + + @Test fun ownedStagingIsRemovedOnlyWhenItHoldsJustItsMarker() { + val root = temp.newFolder("game") + val staging = EpicDownloadManager.recoverOwnedStaging(root, emptyList()) + File(staging, "keep.stage").writeText("x") + EpicDownloadManager.releaseOwnedStaging(root) + assertTrue(File(staging, "owner").exists()) + File(staging, "keep.stage").delete() + EpicDownloadManager.releaseOwnedStaging(root) + assertFalse(staging.exists()) + // A directory without the marker is not ours. + staging.mkdir() + File(staging, "owner").writeText("someone else") + EpicDownloadManager.releaseOwnedStaging(root) + assertTrue(staging.exists()) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicBoundedInstallTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicBoundedInstallTest.kt new file mode 100644 index 000000000..37dd7d849 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicBoundedInstallTest.kt @@ -0,0 +1,646 @@ +package com.droiddeck.launcher.stores.epic + +import com.droiddeck.launcher.stores.Store +import com.droiddeck.launcher.stores.StoreGameSidecar +import com.droiddeck.launcher.stores.StoreInstalls +import java.io.File +import java.net.InetAddress +import java.net.ServerSocket +import java.net.SocketException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.security.MessageDigest +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.net.Socket +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowStatFs + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class EpicBoundedInstallTest { + @get:Rule val temp = TemporaryFolder() + private lateinit var server: ServerSocket + private lateinit var worker: Thread + private lateinit var manifest: EpicDownloadManager.Manifest + private lateinit var root: File + private val requests = AtomicInteger() + private val reject = AtomicBoolean() + private val rejectGuid = AtomicInteger() + private val handlers = Executors.newCachedThreadPool() + private val sockets = ConcurrentHashMap.newKeySet<Socket>() + private var requestBarrier: CountDownLatch? = null + private var deliveryGate: CountDownLatch? = null + private var bytes = ByteArray(256 * 1024) { (it % 251).toByte() } + + @Before fun setUp() { + root = temp.newFolder("game") + ShadowStatFs.registerStats(root.path, 1_000_000_000, 1_000_000_000, 1_000_000_000) + server = ServerSocket(0, 8, InetAddress.getByName("127.0.0.1")) + worker = Thread { + while (!server.isClosed) { + val socket = try { server.accept() } catch (e: SocketException) { + if (server.isClosed) break else throw e + } + sockets.add(socket) + handlers.submit { + try { + socket.use { + it.soTimeout = 5000 + val reader = it.getInputStream().bufferedReader() + val request = reader.readLine() + while (!reader.readLine().isNullOrEmpty()) { } + requests.incrementAndGet() + requestBarrier?.let { barrier -> + barrier.countDown() + check(barrier.await(5, TimeUnit.SECONDS)) { "Requests did not overlap" } + } + val out = it.getOutputStream() + val rejectedGuid = rejectGuid.get() + if (reject.get() || (rejectedGuid != 0 && request.contains("_%08X".format(rejectedGuid)))) { + out.write("HTTP/1.1 503 Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".toByteArray()) + } else { + val header = ByteBuffer.allocate(41).order(ByteOrder.LITTLE_ENDIAN) + .putInt(0xB1FE3AA2.toInt()).putInt(1).putInt(41).putInt(bytes.size) + .put(ByteArray(24)).put(0).array() + out.write("HTTP/1.1 200 OK\r\nContent-Length: ${header.size + bytes.size}\r\nConnection: close\r\n\r\n".toByteArray()) + out.write(header) + out.flush() + deliveryGate?.await(5, TimeUnit.SECONDS) + out.write(bytes) + } + out.flush() + } + } catch (_: java.io.IOException) { + // Cancellation disconnects an owned request while this fixture serves it. + } finally { sockets.remove(socket) } + } + } + }.apply { start() } + val chunk = EpicDownloadManager.ChunkInfo().apply { + guid = intArrayOf(1, 0, 0, 0) + windowSize = bytes.size + fileSize = bytes.size.toLong() + sha1 = sha(bytes) + } + manifest = EpicDownloadManager.Manifest().apply { + chunkDir = "chunks" + uniqueChunks = arrayListOf(chunk) + cdnUrls = listOf(EpicDownloadManager.CdnUrl("http://127.0.0.1:${server.localPort}", "", "")) + files = arrayListOf(gameFile("game.bin", 0, bytes.size / 2), gameFile("game.bin.part", bytes.size / 2, bytes.size / 2)) + } + } + + @After fun tearDown() { + deliveryGate?.countDown() + server.close() + worker.join(6000) + sockets.forEach { it.close() } + handlers.shutdownNow() + assertTrue(handlers.awaitTermination(6, TimeUnit.SECONDS)) + assertFalse(worker.isAlive) + } + + private fun sha(data: ByteArray) = MessageDigest.getInstance("SHA-1").digest(data) + + private fun gameFile(name: String, offset: Int, size: Int) = EpicDownloadManager.FileInfo().apply { + filename = name + sha1 = sha(bytes.copyOfRange(offset, offset + size)) + parts.add(EpicDownloadManager.ChunkPart().apply { + guid = intArrayOf(1, 0, 0, 0) + this.offset = offset + this.size = size + }) + } + + private fun install(cancel: AtomicBoolean = AtomicBoolean(), callback: EpicDownloadManager.Callback = + EpicDownloadManager.Callback { _, _ -> }, cache: String = "") = + EpicDownloadManager.installManifest(manifest, root.path, cache, null, cancel, callback) + + private fun oldInstall() { + File(root, "game.bin").writeBytes(byteArrayOf(9, 8, 7)) + File(root, "game.bin.part").writeBytes(byteArrayOf(6, 5, 4)) + StoreGameSidecar(Store.EPIC, "game", "Game", "game.bin", installVersion = "old").write(root) + } + + private fun assertOldInstallIntact() { + assertArrayEquals(byteArrayOf(9, 8, 7), File(root, "game.bin").readBytes()) + assertArrayEquals(byteArrayOf(6, 5, 4), File(root, "game.bin.part").readBytes()) + assertEquals("old", StoreGameSidecar.read(root)?.installVersion) + assertTrue(StoreGameSidecar.read(root)?.isInstalled == true) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + } + + @Test fun networkFailureAfterAcceptedPreflightPreservesInstalledFilesAndRecord() { + oldInstall() + reject.set(true) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertTrue(requests.get() > 0) + assertOldInstallIntact() + } + + @Test fun hashFailureAfterAcceptedPreflightPreservesInstalledFilesAndRecord() { + oldInstall() + manifest.files.first().sha1 = ByteArray(20) { 1 } + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertOldInstallIntact() + } + + @Test fun cancellationWhileStagingPreservesInstalledFilesAndRecord() { + oldInstall() + val cancel = AtomicBoolean() + assertNull(install(cancel, EpicDownloadManager.Callback { message, _ -> + if (message.startsWith("Downloading chunks (")) cancel.set(true) + })) + assertOldInstallIntact() + } + + @Test fun laterFailureRetainsVerifiedSwapsAndMarksMixedUpdateResumable() { + oldInstall() + manifest.files.last().sha1 = ByteArray(20) { 1 } + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + assertArrayEquals(byteArrayOf(6, 5, 4), File(root, "game.bin.part").readBytes()) + val record = StoreGameSidecar.read(root)!! + assertEquals(StoreGameSidecar.STATE_UPDATING, record.state) + assertFalse(record.isInstalled) + assertEquals("", record.installVersion) + StoreInstalls.begin(root, Store.EPIC, "game", "Game", null, null) + assertEquals("game.bin", StoreGameSidecar.read(root)?.exe) + assertEquals(StoreGameSidecar.STATE_UPDATING, StoreGameSidecar.read(root)?.state) + } + + @Test fun laterNetworkFailurePreservesTheUnreplacedOldFile() { + oldInstall() + val secondChunk = EpicDownloadManager.ChunkInfo().apply { + guid = intArrayOf(2, 0, 0, 0) + windowSize = bytes.size + fileSize = bytes.size.toLong() + sha1 = sha(bytes) + } + manifest.uniqueChunks.add(secondChunk) + manifest.files.last().parts.single().guid = secondChunk.guid + // Prefetch may fail early, but replacement is still ordered by the writer. + rejectGuid.set(2) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + assertArrayEquals(byteArrayOf(6, 5, 4), File(root, "game.bin.part").readBytes()) + assertEquals(StoreGameSidecar.STATE_UPDATING, StoreGameSidecar.read(root)?.state) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + } + + @Test fun cancellationAfterFirstSwapLeavesAResumableMixedUpdate() { + oldInstall() + val cancel = AtomicBoolean() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) {} + override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + if (stage == "stream" && items == 1) cancel.set(true) + } + } + assertNull(install(cancel, callback)) + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + assertArrayEquals(byteArrayOf(6, 5, 4), File(root, "game.bin.part").readBytes()) + assertEquals(StoreGameSidecar.STATE_UPDATING, StoreGameSidecar.read(root)?.state) + assertNotNull(install()) + assertArrayEquals(bytes.copyOfRange(bytes.size / 2, bytes.size), File(root, "game.bin.part").readBytes()) + // Registration, not an individual file swap, restores the installed state. + assertFalse(StoreGameSidecar.read(root)!!.isInstalled) + } + + @Test fun atomicRenameFailurePreservesDestinationAndCleansStagingWithoutDeleteFallback() { + val destination = File(root, "game.bin").apply { mkdir() } + File(destination, "old-content").writeText("keep") + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertTrue(error.message.orEmpty().contains("atomically replace")) + assertEquals("keep", File(destination, "old-content").readText()) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + } + + @Test fun safeStagingSpaceBoundaryRejectsWithoutReclaimingAllOldOutputs() { + // The first staged half-file must coexist with the shared chunk and BOTH old outputs. + val old = ByteArray(bytes.size / 2) { 9 } + File(root, "game.bin").writeBytes(old) + File(root, "game.bin.part").writeBytes(old) + val peak = bytes.size + old.size + ShadowStatFs.registerStats(root.path, 1024, peak / 4096 - 1, peak / 4096 - 1) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertEquals(0, requests.get()) + assertArrayEquals(old, File(root, "game.bin").readBytes()) + assertArrayEquals(old, File(root, "game.bin.part").readBytes()) + ShadowStatFs.registerStats(root.path, 1024, peak / 4096, peak / 4096) + assertNotNull(install()) + } + + @Test fun zeroSpaceRejectsPendingFilesWhileUnknownSpaceIsNotTreatedAsZero() { + oldInstall() + ShadowStatFs.registerStats(root.path, 1024, 0, 0) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertOldInstallIntact() + assertEquals(0, requests.get()) + assertTrue(com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(1, 0)) + assertFalse(com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(1, -1)) + } + + @Test fun zeroScratchSpaceRejectsBeforeFetchWithoutTouchingInstalledOutputs() { + oldInstall() + val cache = temp.newFolder("full-scratch") + ShadowStatFs.registerStats(cache.path, 1024, 0, 0) + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install(cache = cache.path) } + assertTrue(error.message.orEmpty().contains("Not enough internal space")) + assertOldInstallIntact() + assertEquals(0, requests.get()) + } + + @Test fun installsSharedChunksAndPreservesLegitimatePartFilename() { + assertNotNull(install()) + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + assertArrayEquals(bytes.copyOfRange(bytes.size / 2, bytes.size), File(root, "game.bin.part").readBytes()) + assertEquals(1, requests.get()) + assertFalse(File(root, ".chunks").exists()) + assertNotNull(install()) + assertEquals("Verified files must not be downloaded again", 1, requests.get()) + } + + private fun oversizedUpdate() { + val file = manifest.files[0] + val part = file.parts[0] + file.parts.clear() + repeat(100_000) { + file.parts.add(EpicDownloadManager.ChunkPart().apply { + guid = part.guid + size = Int.MAX_VALUE + }) + } + } + + @Test fun insufficientInstallSpacePreservesExistingFilesOnBothCacheLayouts() { + val oldGame = byteArrayOf(9, 8, 7) + val oldPart = byteArrayOf(6, 5, 4) + File(root, "game.bin").writeBytes(oldGame) + File(root, "game.bin.part").writeBytes(oldPart) + oversizedUpdate() + assertTrue(manifest.files[0].fileSize() > root.usableSpace) + + for (cache in listOf("", temp.newFolder("scratch-rejected").path)) { + if (cache.isNotEmpty()) ShadowStatFs.registerStats(cache, 1_000_000_000, 1_000_000_000, 1_000_000_000) + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install(cache = cache) } + assertTrue(error.message.orEmpty().contains("Not enough free space")) + assertArrayEquals(oldGame, File(root, "game.bin").readBytes()) + assertArrayEquals(oldPart, File(root, "game.bin.part").readBytes()) + } + assertEquals(0, requests.get()) + } + + @Test fun acceptedUpdateReplacesOutdatedFilesWithVerifiedOutputs() { + oldInstall() + assertNotNull(install()) + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + assertArrayEquals(bytes.copyOfRange(bytes.size / 2, bytes.size), File(root, "game.bin.part").readBytes()) + assertEquals(StoreGameSidecar.STATE_UPDATING, StoreGameSidecar.read(root)?.state) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + } + + @Test fun pauseKeepsCompletedFilesAndResumeReusesSharedCache() { + val cancel = AtomicBoolean() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) {} + override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + if (stage == "stream" && items == 1) cancel.set(true) + } + } + assertNull(install(cancel, callback)) + assertTrue(File(root, "game.bin").exists()) + assertFalse(File(root, "game.bin.part").exists()) + assertEquals(1, requests.get()) + assertNotNull(install()) + assertEquals(1, requests.get()) + } + + @Test fun cancellationDuringChunkDeliveryRemovesIncompleteOutput() { + val cancel = AtomicBoolean() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) { + if (message.startsWith("Downloading chunks (")) cancel.set(true) + } + } + assertNull(install(cancel, callback)) + assertFalse(File(root, "game.bin").exists()) + assertNotNull(install()) + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + } + + @Test fun fileHashFailureIsExplicitAndRetryRepairsIt() { + val expected = manifest.files[0].sha1 + manifest.files[0].sha1 = ByteArray(20) { 1 } + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertTrue(error.message.orEmpty().contains("SHA-1 mismatch")) + assertFalse(File(root, "game.bin").exists()) + manifest.files[0].sha1 = expected + assertNotNull(install()) + } + + @Test fun corruptCacheIsRefetchedAndScratchCacheIsRemovedOnSuccess() { + val cache = temp.newFolder("scratch") + ShadowStatFs.registerStats(cache.path, 1_000_000_000, 1_000_000_000, 1_000_000_000) + File(cache, manifest.uniqueChunks[0].guidStr()).writeBytes(ByteArray(bytes.size)) + assertNotNull(install(cache = cache.path)) + assertEquals(1, requests.get()) + assertFalse(cache.exists()) + } + + @Test fun networkFailureLeavesNoCompletedFileAndCanBeRetried() { + reject.set(true) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertFalse(File(root, "game.bin").exists()) + reject.set(false) + assertNotNull(install()) + } + + @Test fun boundedProgressNeverReentersDownloadAfterEnteringInstall() { + val events = mutableListOf<String>() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) { + if (message.startsWith("Downloading chunks")) events.add("download") + if (message.startsWith("Writing")) events.add("install") + } + override fun onBytes(done: Long, total: Long, speedBps: Long) { events.add("download") } + override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + if (stage == "install") { + assertEquals(total, done) + assertEquals(itemsTotal, items) + events.add("install") + } + } + } + assertNotNull(install(callback = callback)) + val firstInstall = events.indexOf("install") + assertTrue(firstInstall > 0) + assertFalse(events.drop(firstInstall).contains("download")) + } + + @Test fun sharedChunksDoNotCompleteProgressBeforeTheRemainingFilesAreWritten() { + val samples = mutableListOf<Pair<Long, Long>>() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) {} + override fun onBytes(done: Long, total: Long, speedBps: Long) { samples.add(done to total) } + override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + if (stage == "stream" && items < itemsTotal) assertTrue(samples.last().first < samples.last().second) + } + } + assertNotNull(install(callback = callback)) + assertEquals(listOf(0L, bytes.size / 2L, bytes.size.toLong()), samples.map { it.first }) + assertTrue(samples.all { it.second == bytes.size.toLong() }) + } + + @Test fun oversizedSharedChunkStaysPinnedAcrossPrefetchedFilesWithoutChangingStages() { + bytes = ByteArray((EpicDownloadManager.MAX_RETAINED_CHUNK_CACHE_BYTES + 1024).toInt()) { (it % 251).toByte() } + manifest.uniqueChunks.single().apply { + windowSize = bytes.size + fileSize = bytes.size.toLong() + sha1 = sha(bytes) + } + manifest.files = (0..2).map { gameFile("part-$it.bin", it * 1024, 1024) } + val chunk = manifest.uniqueChunks.single() + val estimatedPeak = EpicDownloadManager.estimatePeakAdditionalBytes( + manifest.files, + mapOf(chunk.guidStr() to chunk), + emptyMap(), + true, + ) + assertEquals(EpicDownloadManager.MAX_RETAINED_CHUNK_CACHE_BYTES + 4 * 1024L, estimatedPeak) + val samples = mutableListOf<Long>() + val stages = mutableListOf<String>() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) {} + override fun onBytes(done: Long, total: Long, speedBps: Long) { + assertEquals(3072L, total) + assertFalse(stages.contains("install")) + samples.add(done) + } + override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + stages.add(stage) + if (stage == "stream" && items < itemsTotal) assertTrue(samples.last() < total) + } + } + assertNotNull(install(callback = callback)) + assertEquals(1, requests.get()) + assertEquals(listOf(0L, 1024L, 2048L, 3072L), samples) + assertEquals("install", stages.last()) + repeat(3) { assertArrayEquals(bytes.copyOfRange(it * 1024, (it + 1) * 1024), File(root, "part-$it.bin").readBytes()) } + } + + @Test fun hogwartsSizedSequentialPlanFitsAvailableSpaceWithoutFullChunkCache() { + val gib = 1024L * 1024 * 1024 + val file = EpicDownloadManager.FileInfo() + val chunks = LinkedHashMap<String, EpicDownloadManager.ChunkInfo>() + repeat(72) { index -> + val chunk = EpicDownloadManager.ChunkInfo().apply { + guid = intArrayOf(index + 1, 0, 0, 0) + windowSize = gib.toInt() + } + chunks[chunk.guidStr()] = chunk + file.parts.add(EpicDownloadManager.ChunkPart().apply { + guid = chunk.guid + size = chunk.windowSize + }) + } + val peak = EpicDownloadManager.estimatePeakAdditionalBytes(listOf(file), chunks, emptyMap(), true) + assertEquals(73 * gib, peak) + assertTrue(peak < 96 * gib) + } + + @Test fun plannersReclaimEachOldFileOnlyAfterItsOwnVerifiedSwap() { + val chunks = manifest.uniqueChunks.associateBy { it.guidStr() } + val half = bytes.size / 2L + val old = mapOf("game.bin" to half, "game.bin.part" to 100 * half) + assertEquals(bytes.size + half, EpicDownloadManager.estimatePeakAdditionalBytes(manifest.files, chunks, emptyMap(), true, old)) + assertEquals(half, EpicDownloadManager.estimateStagedFilePeak(manifest.files, old)) + assertEquals(2 * half, EpicDownloadManager.estimateStagedFilePeak(manifest.files, emptyMap())) + assertEquals(bytes.size.toLong(), EpicDownloadManager.estimatePeakAdditionalBytes(manifest.files, chunks, emptyMap(), false, old)) + } + + private fun distinctFiles(count: Int) { + val template = manifest.uniqueChunks.single() + manifest.uniqueChunks = (1..count).map { id -> + EpicDownloadManager.ChunkInfo().apply { + guid = intArrayOf(id, 0, 0, 0) + windowSize = template.windowSize + fileSize = template.fileSize + sha1 = template.sha1 + } + } + manifest.files = manifest.uniqueChunks.mapIndexed { index, chunk -> + gameFile("file-$index.bin", 0, bytes.size).apply { parts.single().guid = chunk.guid } + } + } + + @Test fun prefetchOverlapsEightSinglePartFilesButNeverFetchesTheWholeManifest() { + distinctFiles(20) + requestBarrier = CountDownLatch(8) + val cancel = AtomicBoolean() + var sawWindow = false + assertNull(install(cancel, EpicDownloadManager.Callback { message, _ -> + if (message.startsWith("Downloading chunks (")) { + assertEquals(8, requests.get()) + val cacheBytes = File(root, ".chunks").listFiles()!!.sumOf { it.length() } + assertTrue(cacheBytes <= 8L * bytes.size) + sawWindow = true + cancel.set(true) + } + })) + assertTrue("An eight-request barrier cannot pass with a sequential downloader", sawWindow) + assertEquals(8, requests.get()) + assertTrue(root.walk().none { it.name.endsWith(".stage") || it.name.endsWith(".part") }) + } + + @Test fun parallelWindowSpaceThresholdIsCheckedBeforeStartingAnyRequest() { + distinctFiles(20) + val cache = temp.newFolder("parallel-scratch") + val peak = EpicDownloadManager.estimatePeakAdditionalBytes( + manifest.files, manifest.uniqueChunks.associateBy { it.guidStr() }, emptyMap(), false, + ) + assertEquals(8L * bytes.size, peak) + ShadowStatFs.registerStats(cache.path, 1024, peak.toInt() / 4096 - 1, peak.toInt() / 4096 - 1) + assertThrows(EpicDownloadManager.InstallException::class.java) { install(cache = cache.path) } + assertEquals(0, requests.get()) + ShadowStatFs.registerStats(cache.path, 1024, peak.toInt() / 4096, peak.toInt() / 4096) + requestBarrier = CountDownLatch(8) + assertNotNull(install(cache = cache.path)) + assertEquals(20, requests.get()) + manifest.files.forEach { assertArrayEquals(bytes, File(root, it.filename).readBytes()) } + } + + @Test fun cancellationDuringParallelNetworkReadsJoinsWorkersBeforeReturning() { + distinctFiles(20) + requestBarrier = CountDownLatch(8) + deliveryGate = CountDownLatch(1) + val cancel = AtomicBoolean() + val outcome = java.util.concurrent.atomic.AtomicReference<Any?>() + val installer = Thread { + try { outcome.set(install(cancel)) } catch (e: Throwable) { outcome.set(e) } + }.apply { start() } + try { + assertTrue(requestBarrier!!.await(5, TimeUnit.SECONDS)) + cancel.set(true) + installer.join(10_000) + assertFalse("Installer must disconnect and join all download workers", installer.isAlive) + assertNull(outcome.get()) + val cache = File(root, ".chunks") + val snapshot = cache.listFiles()?.associate { it.name to it.length() }.orEmpty() + deliveryGate!!.countDown() + Thread.sleep(150) + assertEquals(snapshot, cache.listFiles()?.associate { it.name to it.length() }.orEmpty()) + assertTrue(snapshot.keys.none { it.endsWith(".part") }) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + assertEquals(8, requests.get()) + } finally { + cancel.set(true) + deliveryGate!!.countDown() + installer.join(10_000) + } + } + + @Test fun failedParallelFetchJoinsOtherNetworkReadersBeforeReturning() { + distinctFiles(20) + requestBarrier = CountDownLatch(8) + deliveryGate = CountDownLatch(1) + rejectGuid.set(1) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertEquals(8, requests.get()) + val cache = File(root, ".chunks") + val snapshot = cache.listFiles()?.associate { it.name to it.length() }.orEmpty() + deliveryGate!!.countDown() + Thread.sleep(150) + assertEquals(snapshot, cache.listFiles()?.associate { it.name to it.length() }.orEmpty()) + assertTrue(snapshot.keys.none { it.endsWith(".part") }) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + } + + @Test fun ownedOrphanStagingIsRecoveredBeforeTheSpaceProbeWithoutTouchingManifestFiles() { + val staging = EpicDownloadManager.recoverOwnedStaging(root, manifest.files) + File(staging, "native-123-0.stage").writeBytes(ByteArray(bytes.size)) + val legitimate = File(root, ".droiddeck-epic-old.stage").apply { writeText("keep") } + oldInstall() + val peak = 2 * bytes.size + ShadowStatFs.registerStats(root.path, 1024, peak / 4096, peak / 4096) + assertNotNull(install()) + assertEquals("keep", legitimate.readText()) + // The orphan was recovered, and a finished install removes the then-empty owned namespace. + assertFalse(staging.exists()) + } + + @Test fun stagingRecoveryRefusesUnownedAndManifestOwnedNamespaces() { + val staging = File(root, EpicDownloadManager.STAGING_DIRECTORY).apply { mkdir() } + val untouched = File(staging, "legitimate.stage").apply { writeText("keep") } + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertEquals("keep", untouched.readText()) + File(staging, "owner").writeText("DroidDeck Epic staging v1\n") + manifest.files.first().filename = "${EpicDownloadManager.STAGING_DIRECTORY}/legitimate.stage" + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertEquals("keep", untouched.readText()) + assertEquals(0, requests.get()) + } + + @Test fun loadedNativeAbiFailureIsExplicitAndNeverFallsBackToJavaDownloads() { + val field = com.droiddeck.launcher.stores.StoresNative::class.java.getDeclaredField("loaded") + field.isAccessible = true + field.set(null, true) + try { + oldInstall() + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertTrue(error.message.orEmpty().contains("Native Epic install failed")) + assertEquals(0, requests.get()) + assertOldInstallIntact() + } finally { + com.droiddeck.launcher.stores.StoresNative.forget() + } + } + + @Test fun stagingRecoveryRejectsSymlinkNamespaceOwnerAndEntriesWithoutDeletingOutsideData() { + val outside = temp.newFolder("outside-staging") + val externalStage = File(outside, "keep.stage").apply { writeText("keep") } + val externalOwner = File(outside, "owner").apply { writeText("DroidDeck Epic staging v1\n") } + val staging = File(root, EpicDownloadManager.STAGING_DIRECTORY) + try { + java.nio.file.Files.createSymbolicLink(staging.toPath(), outside.toPath()) + } catch (e: java.nio.file.FileSystemException) { + org.junit.Assume.assumeNoException("Host does not permit symbolic links", e) + } + try { + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertTrue(error.message.orEmpty().contains("Unsafe Epic staging")) + assertEquals("keep", externalStage.readText()) + java.nio.file.Files.delete(staging.toPath()) + staging.mkdir() + java.nio.file.Files.createSymbolicLink(File(staging, "owner").toPath(), externalOwner.toPath()) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + java.nio.file.Files.delete(File(staging, "owner").toPath()) + File(staging, "owner").writeText("DroidDeck Epic staging v1\n") + val localStage = File(staging, "local.stage").apply { writeText("local") } + java.nio.file.Files.createSymbolicLink(File(staging, "linked.stage").toPath(), externalStage.toPath()) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertEquals("local", localStage.readText()) + assertEquals("keep", externalStage.readText()) + assertEquals(0, requests.get()) + } finally { + if (java.nio.file.Files.isSymbolicLink(staging.toPath())) java.nio.file.Files.delete(staging.toPath()) + for (name in listOf("owner", "linked.stage")) { + val entry = File(staging, name).toPath() + if (java.nio.file.Files.isSymbolicLink(entry)) java.nio.file.Files.delete(entry) + } + } + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt index 669e0adc4..14bc3d364 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt @@ -1,8 +1,10 @@ package com.droiddeck.launcher.stores.epic import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -11,6 +13,140 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class EpicJsonManifestTest { + @Test + fun boundedInstallPeakReservesParallelIncomingChunksBeforeWriting() { + val first = chunk(1) + val second = chunk(2) + val file = file(part(1, 0, 100), part(2, 0, 100)) + val chunks = mapOf(first.guidStr() to first, second.guidStr() to second) + + assertEquals(300L, EpicDownloadManager.estimatePeakAdditionalBytes(listOf(file), chunks, emptyMap(), true)) + assertEquals(200L, EpicDownloadManager.estimatePeakAdditionalBytes(listOf(file), chunks, emptyMap(), false)) + } + + @Test + fun sharedCachedChunkIsReclaimedOnlyAfterItsLastPart() { + val shared = chunk(1) + val files = listOf(file(part(1, 0, 60)), file(part(1, 60, 40))) + + assertEquals( + 100L, + EpicDownloadManager.estimatePeakAdditionalBytes( + files, mapOf(shared.guidStr() to shared), mapOf(shared.guidStr() to 100L), true, + ), + ) + assertEquals( + 200L, + EpicDownloadManager.estimatePeakAdditionalBytes( + files, mapOf(shared.guidStr() to shared), emptyMap(), true, + ), + ) + } + + @Test + fun sharedChunksWithinTheWindowStayPinnedWithinTheCacheCeiling() { + val first = chunk(1).apply { windowSize = 40 * 1024 * 1024 } + val second = chunk(2).apply { windowSize = 40 * 1024 * 1024 } + val files = listOf( + file(part(1, 0, 40 * 1024 * 1024)), + file(part(2, 0, 40 * 1024 * 1024)), + file(part(1, 0, 40 * 1024 * 1024)), + file(part(2, 0, 40 * 1024 * 1024)), + ) + val chunks = mapOf(first.guidStr() to first, second.guidStr() to second) + + val gamePeak = EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, emptyMap(), true) + val cachePeak = EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, emptyMap(), false) + + assertEquals(200L * 1024 * 1024, gamePeak) + assertTrue(cachePeak <= EpicDownloadManager.MAX_RETAINED_CHUNK_CACHE_BYTES + 40L * 1024 * 1024) + } + + @Test fun scheduleDeduplicatesWindowAndEvictsOnlyUnpinnedReusableChunks() { + val size = 40L * 1024 * 1024 + val chunks = (1..12).map { chunk(it).apply { windowSize = size.toInt() } } + .associateBy { it.guidStr() } + val files = (1..12).map { file(part(it, 0, 1)) } + (1..12).map { file(part(it, 0, 1)) } + val schedule = EpicDownloadManager.PrefetchSchedule(files, chunks, emptyMap()) + assertEquals(chunks.keys.take(8), schedule.fill()) + assertEquals(emptyList<String>(), schedule.fill()) + assertEquals(8 * size, schedule.bytes()) + assertEquals(emptyList<String>(), schedule.consume()) + assertEquals(listOf(chunks.keys.elementAt(8)), schedule.fill()) + assertEquals(9 * size, schedule.bytes()) + assertEquals(listOf(chunks.keys.first()), schedule.consume()) + var refetchedFirst = false + repeat(22) { + val fetched = schedule.fill() + if (chunks.keys.first() in fetched) refetchedFirst = true + assertTrue(schedule.bytes() <= EpicDownloadManager.MAX_RETAINED_CHUNK_CACHE_BYTES + 8 * size) + val released = schedule.consume() + assertTrue(released.none { it in schedule.window() }) + } + assertTrue(refetchedFirst) + assertEquals(0L, schedule.bytes()) + assertEquals(9 * size, EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, emptyMap(), false)) + } + + @Test fun cachedAndOldOutputBytesAreCreditedOnlyWhenTheScheduleReleasesThem() { + val chunks = (1..8).map { chunk(it) }.associateBy { it.guidStr() } + val files = (1..8).map { id -> file(part(id, 0, 100)).apply { filename = "file-$id" } } + val existing = chunks.keys.associateWith { 100L } + val old = files.associate { it.filename to 100L } + assertEquals(800L, EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, emptyMap(), false)) + assertEquals(0L, EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, existing, false)) + assertEquals(900L, EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, emptyMap(), true, old)) + assertEquals(100L, EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, existing, true, old)) + } + + @Test + fun resumeCacheDropsCorruptAndUnneededEntriesButKeepsVerifiedChunks() { + val dir = java.io.File(System.getProperty("user.dir"), "build/epic-cache-test-${System.nanoTime()}") + assertTrue(dir.mkdirs()) + try { + val bytes = ByteArray(100) { (it % 251).toByte() } + val expectedSha = java.security.MessageDigest.getInstance("SHA-1").digest(bytes) + val valid = chunk(1).apply { sha1 = expectedSha } + val corrupt = chunk(2).apply { sha1 = expectedSha } + java.io.File(dir, valid.guidStr()).writeBytes(bytes) + java.io.File(dir, corrupt.guidStr()).writeBytes(ByteArray(100) { 1 }) + java.io.File(dir, "orphan").writeBytes(bytes) + + val retained = EpicDownloadManager.prepareBoundedCache(dir, listOf(valid, corrupt)) + + assertEquals(mapOf(valid.guidStr() to 100L), retained) + assertTrue(java.io.File(dir, valid.guidStr()).exists()) + assertFalse(java.io.File(dir, corrupt.guidStr()).exists()) + assertFalse(java.io.File(dir, "orphan").exists()) + } finally { + EpicDownloadManager.deleteDir(dir) + } + } + + @Test + fun referenceCountsIncludeRepeatedPartsSoCancelledFilesCanBeReplanned() { + val pending = listOf(file(part(1, 0, 20), part(1, 20, 20)), file(part(1, 40, 60))) + + assertEquals(3, EpicDownloadManager.chunkReferences(pending)["00000001000000000000000000000000"]) + } + + private fun chunk(id: Int) = EpicDownloadManager.ChunkInfo().apply { + guid = intArrayOf(id, 0, 0, 0) + windowSize = 100 + fileSize = 100 + } + + private fun part(id: Int, offset: Int, size: Int) = EpicDownloadManager.ChunkPart().apply { + guid = intArrayOf(id, 0, 0, 0) + this.offset = offset + this.size = size + } + + private fun file(vararg parts: EpicDownloadManager.ChunkPart) = EpicDownloadManager.FileInfo().apply { + filename = "test.bin" + this.parts.addAll(parts) + } + @Test fun blobNumbersAreDecimalBytesLeastSignificantFirst() { assertEquals(1L, EpicDownloadManager.blobToNum("001000000000")) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/gog/GogResumeSpaceTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/gog/GogResumeSpaceTest.kt new file mode 100644 index 000000000..4934b5898 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/stores/gog/GogResumeSpaceTest.kt @@ -0,0 +1,43 @@ +package com.droiddeck.launcher.stores.gog + +import java.io.File +import java.security.MessageDigest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowStatFs +import com.droiddeck.launcher.stores.StoreInstallRoot + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class GogResumeSpaceTest { + @Test fun remainingWorkRejectsMeasuredZeroButNotUnknownSpace() { + val root = File(RuntimeEnvironment.getApplication().filesDir, "gog-zero").apply { mkdirs() } + ShadowStatFs.registerStats(root.path, 1024, 0, 0) + val files = listOf(GogDownloadManager.DepotFile("missing").apply { totalSize = 4 }) + val needed = GogDownloadManager.remainingInstallBytes(files, root) + assertEquals(4L, needed) + assertTrue(StoreInstallRoot.insufficientSpace(needed, StoreInstallRoot.availableBytes(root))) + assertFalse(StoreInstallRoot.insufficientSpace(needed, StoreInstallRoot.SPACE_UNKNOWN)) + } + + @Test fun remainingSpaceExcludesVerifiedFilesButIncludesCorruptAndPartialFiles() { + val root = File(RuntimeEnvironment.getApplication().filesDir, "gog-resume").apply { mkdirs() } + val data = byteArrayOf(1, 2, 3, 4) + val hash = MessageDigest.getInstance("MD5").digest(data).joinToString("") { "%02x".format(it) } + val files = listOf("good", "corrupt", "partial", "missing").map { + GogDownloadManager.DepotFile(it).apply { totalSize = 4; md5 = hash } + } + File(root, "good").writeBytes(data) + File(root, "corrupt").writeBytes(ByteArray(4)) + File(root, "partial").writeBytes(byteArrayOf(1)) + assertEquals(12L, GogDownloadManager.remainingInstallBytes(files, root)) + for (file in files) File(root, file.relativePath).writeBytes(data) + assertEquals(0L, GogDownloadManager.remainingInstallBytes(files, root)) + } +} diff --git a/docs/development/stores.md b/docs/development/stores.md index 79b1e2b73..c3c7223c2 100644 --- a/docs/development/stores.md +++ b/docs/development/stores.md @@ -24,15 +24,164 @@ scanned (`StoreInstallRoot.roots`): internal, every card, and the Steam library' installs an earlier build put there. `<Store>` is `GOG`, `Epic` or `Amazon`; `<title>` is the title with unsafe characters dropped, at most 60 characters (`StoreInstallRoot.folderName`), chosen once; a rerun (repair, update) lands on the folder whose sidecar carries the game's id, wherever it is. -An Epic install on a card keeps its in-flight chunks in `cacheDir/stores/epic/<id>/`: the -`chunkCacheDir` of both native calls (`EpicNative.run` fetches into it, `EpicNative.assemble` -writes the files from it and drops each chunk after its last use, then the folder), and of the -manager's own loops when the engine is not there; `""` keeps the cache beside the game, as an -internal install has it. The scratch folder is removed on cancel-with-delete and on uninstall. -The cache holds whole ~1 MiB chunk windows, shared with files this device does not install, so it -is often larger than the game (Metalstorm: 9.1 GB of chunks for 4.4 GB of files); the free-space -check, made after the delta pass, counts the missing chunks and the missing files on their own -volumes. A successful run removes the cache. +An Epic install on a card keeps its in-flight chunks in `cacheDir/stores/epic/<id>/`; +`""` keeps the cache beside the game, as an internal install has it. The scratch folder is +removed on cancel-with-delete and on uninstall. + +Preflight resolves an existing game's folder before selecting its target root, so a resume or +repair on a card checks and remembers that volume rather than internal storage. Fresh GOG and +Amazon installs can reject insufficient catalog-size space immediately; existing installs defer +to manifest checks of the files still needing work (GOG MD5, Amazon SHA-256 where supplied). +Free-space display probes the nearest existing ancestor without creating the target folder; if +the probe cannot determine space, it reports **Free space unknown** rather than blocking an install. +`StoreInstallRoot` uses `-1` for unknown and preserves a measured `StatFs` zero as **0 B free**. +Fresh GOG/Amazon preflight and all three Java backend space checks reject known zero when bytes +remain. The read-only probe walks existing ancestors without creating directories; a failed +`StatFs` probe can use a positive `File.usableSpace` fallback, but that API's ambiguous zero is +unknown, not a measurement of a full disk. +Preflight failures remain failed Downloads rows with Clear and Retry. Retry reruns preflight +outside the worker lifecycle and then enqueues the real backend; it never occupies a queue slot +with a placeholder job. Failures are kept in the Downloads UI, notification and logs rather than +duplicated as transient failure toasts. Active queue persistence and ordinary backend pause/retry +are unchanged. + +Epic installs have four writers, all with the same per-file safety rules (below). The hidden +`storesEpicMode` string in the `session` preferences picks one: `adaptive` (default), `lookahead`, +`batch` or `java`. There is no UI for it; set it on a test device to compare the modes on the same +game. Every mode logs an `epic: summary mode=<mode>` line (files, bytes written, elapsed, average +MB/s). When `libdroiddeckstores.so` does not load, every mode runs the Java writer. Once it is +loaded, an engine failure (refused plan, ABI mismatch, network, hash or I/O failure, unsupported +atomic swap) fails the install; it never silently reruns as Java downloads. + +**Adaptive (default)** - `EpicNative.stream`, `epic/stream.rs`. Measured on device: about +83 MB/s on a 71 GB install. The chunks go through the shared fetch core in the order their files +need them, with its full adaptivity (window ramp, per-host caps, in-flight byte budget, retries, +host rotation, stall handoff). A chunk only one file part uses is verified and written straight +into that file's staging file at the part's offset, in whatever order it arrives; it is never +cached, so each byte is written once. A chunk several parts use is written into every part whose +file the download has reached and kept, decompressed, in the cache until its last part is written. +The cache has a byte budget; the plan simulates the install in file order and, when the budget +would be exceeded, drops the cached chunk needed furthest ahead and downloads it again later. +Files are opened only once the download reaches them, so the set of half-written files stays +within the fetch window. All writes happen in the fetch core's process pool, whose completion +frees the in-flight byte budget: a slow card throttles the download instead of growing memory. +Each file's SHA-1 is computed as its parts are written (parts that arrived out of order are read +back once the bytes before them are in). Stage files go into the owned staging namespace below. +The run logs `stream-plan` (direct vs shared chunks, planned refetches, budget) and +`stream-summary mode=adaptive` (peak cache, peak staging, refetches, bytes written from the +download vs from the cache). + +On an update, a staged file that replaces an existing one sits beside its old copy until its swap, +so the engine paces replacements with an overlap budget: files are admitted in order, a replaced +file only while the planned sizes of admitted, not-yet-swapped replaced files fit the budget (one +is always admitted, so the run progresses), and a chunk is fetched only once its file is admitted. +Fresh files are never held back, and admission only delays files, never reorders them, so the +cache plan stays exact. The run logs `overlap_budget=` in `stream-plan` and +`peak_replace_overlap_bytes=` in `stream-summary`. With room to spare the budget is large and +updates run as wide as fresh installs; on a nearly full volume, replaced files are staged one +after another and the fetch pauses at each replaced-file boundary until the previous one is +swapped in. + +Its space plan (`EpicDownloadManager.planAdaptiveSpace`, unit-tested in `EpicAdaptivePlanTest`) +checks each volume for what lands on it, with the lookahead planner's rule (the need must not +exceed the free reading). The game volume: the net growth at its worst point (`settledNetPeak`, +the running Σ(new − old) with each old copy released at its swap), plus the overlap budget, plus +the cache when it is beside the game. The overlap budget is whatever the game volume has left +after the net growth and the cache, never less than the largest replaced file; when even that +does not fit, the plan hands over to the lookahead engine. The cache volume: the budget plus 64 MiB of +chunks in transit. The cache budget is half the cache volume's free space after those needs, at most +1 GiB (256 MiB when free space is unknown; 0 is allowed and means every reuse is a refetch). On a +card install the cache stays on internal scratch unless internal storage cannot hold at least +64 MiB of it; then the cache goes beside the game (still released chunk by chunk) instead of +failing. A plan that does not fit logs the short storage and the shortfall and hands over to the +lookahead engine, which narrows its window to the space there is (or fails its own preflight with +an explicit error). Running out of space in the middle of an adaptive run is not destructive: a +failed write fails the run, unfinished staging files are removed, the old files stay. + +**Lookahead** - `EpicNative.installAdaptive`, `epic/adaptive.rs`; also the adaptive engine's +fallback when its space plan does not fit. The Java delta pass sends a versioned snapshot of +pending files, chunk metadata, verified cache lengths, old output lengths and both measured +free-space probes to Rust. The native planner and execution share one ordered moving-window +policy. Before any chunk request, Rust chooses the largest safe lookahead (128, 64, 32, 16, 8, 4, +2 or 1 part references) and reusable-cache budget (64 MiB, 16 MiB or zero) whose simulated +staged-file and cache peaks fit. Native device IDs also detect an explicitly named cache on the +game volume, so two separate paths cannot incorrectly turn one filesystem's requirement into two +independent checks. + +| Storage situation | Lookahead policy | +| --- | --- | +| Plenty of room | Up to 128 references ahead, up to 32 network requests, 64 MiB reusable data | +| Tight | Smaller lookahead and/or reusable budget; parallel fetching remains where space permits | +| Barely fits | One-reference lookahead, one request, no reusable retention if needed | +| Does not fit even the minimal schedule | Explicit preflight error before requests or replacement | +| Free space unknown | Conservative bounded lookahead (at most eight), not an invented zero | + +The lookahead fetch coordinator reuses `fetch_core`'s host scheduling, retries, adaptive network +window and decode pool. Each chunk is cached, then its parts are copied into staged files by a +writer that runs concurrently, waiting only for its next required chunk, through the native +assembler's streaming copy primitive. GUIDs in the lookahead are reserved once before submission, +at their full decoded size. Chunks are released at last use on **both** cache layouts; distant +reusable data is oldest-first bounded and may be refetched. At most the selected reusable budget +plus distinct lookahead decoded sizes is resident/reserved. New request admission re-probes both +volumes and can shrink unsent lookahead or retention if headroom disappears; already in-flight +reservations remain protected. If even existing reservations cannot fit, the run fails and joins +its workers rather than waiting indefinitely or deleting old game files. An install to SD retains +its internal scratch cache, so assembled output is the only game data written to the card. + +**Java** - without the native library, or with `storesEpicMode=java`. Eight Java workers prefetch +the next eight chunk-part references, across file boundaries, while one writer assembles and +verifies files in manifest order. Shared GUIDs within that window are fetched only once. Chunks +are deleted after their last use; other reusable chunks are retained up to 64 MiB. At most eight +additional decoded chunks (complete or in flight) coexist with that retained cache. The size +bound is 64 MiB plus the decoded sizes of the window's distinct GUIDs, not a fixed eight-MiB +allowance: manifests can supply larger chunk windows. Evicted distant shared chunks may be +downloaded again. No worker downloads outside the moving window; cancellation and failure +disconnect and join all workers before returning or removing the cache. Its summary line is +`mode=java-bounded`. + +**Batch** - the earlier native design, kept for A/B benchmarks: every chunk fetched into the +cache in parallel, then the files assembled from it (`EpicNative.run` + `EpicNative.assemble`). +It needs the cache and the files at once (about twice the game) and checks exactly that before +starting; it writes files in place, so an existing install is marked `updating` before assembly. +`epic/assemble.rs::build_assemble_plan` enables `scratch_cache` only for a nonempty cache path: +`run_assemble` then removes chunks at their last use; the default beside-game cache is removed at +the end (`default_cache_assembles_like_the_kotlin_loop_and_removes_the_cache_at_the_end` and the +scratch-cache assembly tests cover that distinction). + +The Epic download bar, measured rate and ETA (adaptive, lookahead and Java) count pending file bytes written, not unique +compressed chunks fetched. Shared chunks therefore cannot complete the bar before later files +are written, and an evicted chunk's refetch is reflected in the time spent doing that work. +Streaming stays in the Download stage until all files are written and verified, then enters +Install for registration; it does not bounce between stages for each file. + +For the lookahead and Java writers, the space check follows the manifest delta pass and uses the +same pure moving-window policy as execution, reserving all incoming decoded chunk sizes before writing parts and simulating +last-use releases and unpinned oldest-first evictions. Existing verified cache bytes count against +current free space and are credited only at their scheduled release. This avoids adding the entire +cache to the entire game without undercounting concurrent downloads. Each file is written to a +unique file in an owned staging namespace on its output volume (never the literal `<filename>.part`, which can +be a legitimate manifest file), size/hash verified, synced and atomically renamed over its output. +The old file remains until that swap succeeds. The planner credits its bytes only after that +file's complete staging write, not by subtracting all old outputs up front. Both volume checks +must accept this safe staging peak; insufficient space is rejected explicitly, with no destructive +delete-first fallback. An unsupported/failed atomic rename fails the install and retains the old +destination. Completed files and +verified cached chunks are accounted for on resume. On a card, game and cache requirements +are checked separately. File SHA-1 hashes are checked where supplied by the manifest; failed +or cancelled staging files are removed without deleting old outputs. A successful run removes the +cache. A resume within a large incomplete file restarts that file, not its individual parts. +All three staged writers (adaptive, lookahead, Java) use `<installDir>/.droiddeck-epic-staging/`, whose `owner` marker identifies this +namespace. A successful install removes it when it holds nothing but the marker (same no-follow +and canonical-path checks); the next run recreates it. Before the delta pass and free-space probes, the manager removes orphan `.stage` +entries only from this owned namespace. A manifest naming that directory or any descendant +fails without deletion; an unowned directory or unexpected entry also fails conservatively. +The namespace must be a real directory at the expected canonical child of the install root. +Ownership markers and staging entries must be no-follow regular files; symlink directories, +markers and entries are rejected before recovery deletes anything. +Ordinary game files with `.stage` or `.part` suffixes elsewhere are never recovery targets. +Abrupt process termination is therefore recoverable on the next run without inferring ownership +from a loose filename prefix. Cancellation/failure waits for all native/Java fetch writers before +returning; completed verified cache chunks remain usable on resume. ## The sidecar @@ -59,7 +208,18 @@ store root whose sidecar says `installing`, or that has no sidecar at all, is an install: `AddedGames.scan` leaves it out (it is neither a game nor a Custom folder), and the Stores card and page offer **Resume install**, which reuses that folder - and an Epic `.chunks` beside it - instead of starting a fresh one (`StoreInstallRoot.existingFolder`). A finished install being -repaired keeps its finished sidecar. The launcher and the finished sidecar are written before the +repaired keeps its finished sidecar until Epic reaches the first verified file swap. Immediately +before that swap Epic atomically changes the record to `"state": "updating"`, preserves launch +choices and identifiers, and clears `installVersion`. Network/hash failures or cancellation before +that point leave the old installed record and files intact. A later failure can leave a mixture of +verified new files and untouched old files: replacement is atomic **per file**, not a transactional +whole-game update or rollback. The updating folder is excluded from installed scans and offered +for resume. Cancellation with deletion clears caches but retains that folder and its verified +swaps; retry preserves the updating record's metadata. If marking or swapping fails, no destructive +fallback is attempted (the conservative updating state can remain even when a swap fails). +Completion writes the installed record with the new version only after all selected files succeed. +An already registered live Steam shortcut is not revoked at each file swap; do not launch an +in-progress update. The launcher and the finished sidecar are written before the art and the Steam registration; a failure writing them fails the download with its message. `StoreGameSidecar.parse` refuses anything whose `exe` or `launcher` would point outside the folder. diff --git a/tools/release/check_android_libraries.py b/tools/release/check_android_libraries.py index 765e226ac..8f79e8844 100644 --- a/tools/release/check_android_libraries.py +++ b/tools/release/check_android_libraries.py @@ -14,6 +14,7 @@ EXPORTS = ['Java_com_droiddeck_launcher_stores_StoresNative_nativeVersion', 'Java_com_droiddeck_launcher_stores_gog_GogNative_nativeStart', 'Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeStart', 'Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeAssemble', + 'Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeStream', 'Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeStart'] From 3a408058baedfff89eb0d71262f3cc6ae56c2a9f Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:47:19 -0400 Subject: [PATCH 14/14] Configure CodeRabbit reviews (#774) --- .coderabbit.yaml | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..8d05f9329 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,64 @@ +language: en-US +early_access: false +tone_instructions: >- + Prioritize actionable correctness, security, and runtime regressions. Keep + feedback concise. Do not request explanatory code comments; preserve license + headers and compiler-consumed annotations. +reviews: + profile: chill + request_changes_workflow: false + high_level_summary: true + high_level_summary_in_walkthrough: true + poem: false + sequence_diagrams: false + auto_review: + enabled: true + auto_incremental_review: true + auto_pause_after_reviewed_commits: 0 + drafts: false + pre_merge_checks: + docstrings: + mode: "off" + title: + mode: "off" + description: + mode: "off" + finishing_touches: + docstrings: + enabled: false + unit_tests: + enabled: false + simplify: + enabled: false + autofix: + enabled: false + fix_ci: + enabled: false + resolve_merge_conflict: + enabled: false + path_instructions: + - path: "**/*" + instructions: >- + For cleanup changes, compare executable behavior against the PR merge + base. Check for removed guards, changed return values, weakened compiler + flags, and deleted regression tests. A passing APK build alone does not + establish behavior preservation. Avoid style-only or comment-coverage + requests. + - path: "app/src/main/**" + instructions: >- + Review Android lifecycle, external-display visibility, input cancellation, + permissions, threading, and Compose state. For JNI, native, and Rust + changes, check ABI compatibility, resource ownership, synchronization, + and error paths. Preserve persisted user settings and existing runtime + contracts. + - path: "tools/**" + instructions: >- + Check shell quoting, error handling, path and filesystem semantics, and + Python helper behavior. Preserve MSI same-file protection and strict + compiler checks. Verify downstream patches apply to their pinned sources + and retain intended executable behavior, including embedded scripts. + - path: ".github/workflows/**" + instructions: >- + Check job permissions, triggers, merge validation, build variants, + signing, artifact paths, and release conditions. Verify checks apply to + the current PR head and keep review configuration advisory.